BIO select poll epoll

BIO

int main()
{
 int sk = socket(AF_INET, SOCK_STREAM, 0);
 connect(sk, ...)
 recv(sk, ...)
}
复制代码

高并发的服务器开发中,这种网络 IO 的性能奇差。因为

1.进程在 recv 的时候大概率会被阻塞掉,导致一次进程切换(数据还没有到)

2.当连接上数据就绪的时候进程又会被唤醒,又是一次进程切换(数据被拷贝到socket接受缓冲区时候唤醒

3.一个进程同时只能等待一条连接,如果有很多并发,则需要很多进程(线程被阻塞在socket的等待队列

在上面的 demo 中虽然只是简单的两三行代码,但实际上用户进程和内核配合做了非常多的工作。先是用户进程发起创建 socket 的指令,然后切换到内核态完成了内核对象的初始化。接下来 Linux 在数据包的接收上,是硬中断和 ksoftirqd 进程在进行处理。当 ksoftirqd 进程处理完以后,再通知到相关的用户进程。

image.png

一、创建一个 socket

socket 函数调用执行完以后,内核在内部创建了一系列的 socket 相关的内核对象(是的,不是只有一个)

image.png

软中断上收到数据包时会通过调用 sk_data_ready 函数指针(实际被设置成了 sock_def_readable()) 来唤醒在 sock 上等待的进程。这个咱们后面介绍软中断的时候再说,这里记住这个就行了。

至此,一个 tcp对象,确切地说是 AF_INET 协议族下 SOCK_STREAM对象就算是创建完成了。这里花费了一次 socket 系统调用的开销

二、等待接收消息

  1. recv 函数底层会执行 recvfrom 的系统调用

  2. 系统调用之后用户进程进入内核态,去查看对应的socket 是否存在数据,如果没有,就将自己阻塞到socket 的等待队列中,让出CPU,等待被唤醒

image.png

三、软中断模块

软中断模块将数据放到 socket 的接受队列后,通过回调函数唤醒等待队列上的进程,该进程进入就绪状态,等待cpu的调用

image.png

四、小结

image.png

第一部分是我们自己代码所在的进程,我们调用的 socket() 函数会进入内核态创建必要内核对象。 recv() 函数在进入内核态以后负责查看接收队列,以及在没有数据可处理的时候把当前进程阻塞掉,让出 CPU。

第二部分是硬中断、软中断上下文(系统进程 ksoftirqd)。在这些组件中,将包处理完后会放到 socket 的接收队列中。然后再根据 socket 内核对象找到其等待队列中正在因为等待而被阻塞掉的进程,然后把它唤醒。

每次一个进程专门为了等一个 socket 上的数据就得被从 CPU 上拿下来。然后再换上另一个进程。等到数据 ready 了,睡眠的进程又会被唤醒。总共两次进程上下文切换开销,根据之前的测试来看,每一次切换大约是 3-5 us(微秒)左右。如果是网络 IO 密集型的应用的话,CPU 就不停地做进程切换这种无用功。

select 和 epoll

epoll与select、poll的对比

1、 用户态将文件描述符传入内核的方式

select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程(select 线程) 可以打开的fd数量限制,默认是1024。

poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。

epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

2、内核态检测文件描述符读写状态的方式

select:采用轮询方式,遍历所有fd, 最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。

poll:同样采用轮询方式,查询每个fd的状态, 如果就绪则在等待队列中加入一项并继续遍历。

epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

3、找到就绪的文件描述符并传递给用户态的方式

select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。(这里遍历的时候,发现一个就会-1)

poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。

这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

4、重复监听的处理方式

select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。

poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。

epoll:无需重新构建红黑树,直接沿用已存在的即可。

调用代码

int main(){
    listen(lfd, ...);

    cfd1 = accept(...);
    cfd2 = accept(...);
    efd = epoll_create(...);

    epoll_ctl(efd, EPOLL_CTL_ADD, cfd1, ...);
    epoll_ctl(efd, EPOLL_CTL_ADD, cfd2, ...);
    epoll_wait(efd, ...)
}
复制代码

一、eventpoll 内核对象

将这张图联合上面那张图一起看,可以发现在创建了 2 个socket对象(5000和5001)之后,创建了一个 eventepoll对象(5003)

image.png

image.png

eventpoll 这个结构体中的几个成员的含义如下:

wq: 等待队列链表。软中断数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。

rbr: 一棵红黑树。为了支持对海量连接的高效查找、插入和删除,eventpoll 内部使用了一棵红黑树。通过这棵树来管理用户进程下添加进来的所有 socket 连接。

rdllist: 就绪的描述符的链表。当有的连接就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历整棵树。

二、epoll_ctl 添加 socket

在使用 epoll_ctl 注册每一个 socket 的时候,内核会做如下三件事情

1.分配一个红黑树节点对象 epitem,

2.添加等待事件到 socket 的等待队列中,其回调函数是 ep_poll_callback

3.将 epitem 插入到 epoll 对象的红黑树里

通过 epoll_ctl 添加两个 socket 以后,这些内核数据结构最终在进程中的关系图大致如下:

image.png

1.分配并初始化 epitem

对于每一个 socket,调用 epoll_ctl 的时候,都会为之分配一个 epitem

这个很好理解, socket 对应一个 epitem,epitem 在 eventepoll 的红黑树上保存。

2. 设置 socket 等待队列

当将 socket 封装注册到 eventpoll 内核对象上之后,会在 socket 的等待队列注册一个回调函数,这里回调函数将会由软中断处理函数进行调用,目的是唤醒在 eventepoll 等待队列上的线程,并将

image.png

3 插入红黑树

分配完 epitem 对象后,紧接着并把它插入到红黑树中。一个插入了一些 socket 描述符的 epoll 里的红黑树的示意图如下:

image.png

选择红黑树,主要原因是在 查找、删除、插入效率上面比较均衡

三 epoll_wait 等待接收

epoll_wait 做的事情不复杂,当它被调用时它观察 eventpoll->rdllist 链表里有没有数据即可。有数据就返回,没有数据就创建一个等待队列项,将其添加到 eventpoll 的等待队列上,然后把自己阻塞掉就完事。

image.png

四 数据来了

流程主要是,软中断处理程序将数据放入 socket 的接受队列后,调用等待队列上的回调函数,先将 socket 对应的 epitem 添加到 epoll 的就绪队列上,然后去唤醒 epoll 等待队列上阻塞的用户线程,用户线程只需要遍历 就绪队列上的 epitem,就可以找到由数据存在 socket 对象,避免了扫描全部的socket。

image.png

五 小结

image.png
总结下,epoll 相关的函数里内核运行环境分两部分:

用户进程内核态。进行调用 epoll_wait 等函数时会将进程陷入内核态来执行。这部分代码负责查看接收队列,以及负责把当前进程阻塞掉,让出 CPU。

硬软中断上下文。在这些组件中,将包从网卡接收过来进行处理,然后放到 socket 的接收队列。对于 epoll 来说,再找到 socket 关联的 epitem,并把它添加到 epoll 对象的就绪链表中。这个时候再捎带检查一下 epoll 上是否有被阻塞的进程,如果有唤醒之。

为了介绍到每个细节,本文涉及到的流程比较多,把阻塞都介绍进来了。

但其实在实践中,只要活儿足够的多,epoll_wait 根本都不会让进程阻塞。用户进程会一直干活,一直干活,直到 epoll_wait 里实在没活儿可干的时候才主动让出 CPU。这就是 epoll 高效的地方所在!

六 select 原理

select本质上是通过设置或检查存放fd标志位的数据结构进行下一步处理。 这带来缺点: – 单个进程可监视的fd数量被限制,即能监听端口的数量有限 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试 一般该数和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认1024个,64位默认2048。

image.png

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

七 边缘触发和水平触发

EPOLLLT和EPOLLET两种:

LT,默认的模式(水平触发): 只要该fd还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作,
ET是“高速”模式(边缘触发):只会提示一次,直到下次再有数据流入之前都不会再提示,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,即读到read返回值小于请求值或遇到EAGAIN错误

epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似回调机制激活该fd,epoll_wait便可收到通知。

EPOLLET触发模式的意义

若用EPOLLLT,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。 而采用EPOLLET,当被监控的文件描述符上有可读写事件发生时,epoll_wait会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait时,它不会通知你,即只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

八 优点

Epoll最大的优点就在于它只关心“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll通过内核和用户空间共享一块内存来实现的

参考

漫画 | 看进程小 P 讲述它的网络性能故事

图解 | 深入理解高性能网络开发路上的绊脚石 – 同步阻塞网络 IO (??新)

图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!

zhuanlan.zhihu.com/p/272891398

blog.csdn.net/JMW1407/art…

mmap使用原理参考

文件描述符

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享