select、epoll相关

3 篇文章 0 订阅
  • select函数:
int select(
    int nfds,                     // 监控的文件描述符集里最大文件描述符加1
    fd_set *readfds,              // 监控有读数据到达文件描述符集合,引用类型的参数
    fd_set *writefds,             // 监控写数据到达文件描述符集合,引用类型的参数
    fd_set *exceptfds,            // 监控异常发生达文件描述符集合,引用类型的参数
    struct timeval *timeout);     // 定时阻塞监控时间

select的执行流程:

如上图,假设进程A同时要监听socket文件描述符3、4、5,如果这三个连接都没有数据到达时,则进程A会让出CPU,进入阻塞状态,同时会将进程A的文件描述符和被唤醒时用到的回调函数组成等待队列加入到socket3、4、5的进程等待队列中。注意,这是select调用时,被监控的文件描述符集合(readfds/writefds/exceptfds)会从用户空间拷贝到内核空间。

当网卡接收网线传来的数据,经过DMA传输,IO通路选择等处理后,将收到的数据写入到内存,网卡将接收到的网络数据写入内存后,网卡向CPU发出一个中断信号,CPU捕获这个信号后,执行相应的中断处理程序,中断处理程序主要做了两件事:

1、将网络数据写入到对应socket的数据接收队列里;

2、唤醒队列中的等待进程A,重新将进程A放入CPU的运行队列中。

假设socket3、5有数据到达网卡(注意此时select调用结束时,被监控的文件描述符集合会从内核空间拷贝到用户空间,全量拷贝。),则执行以下流程:

由此可见,select有以下缺点:

1、性能开销大:①调用select时会陷入内核,这时需要将被监听的文件描述符从用户空间拷贝到内核空间;select执行完毕后,还需要将文件描述符从内核空间拷贝到用户空间,高并发场景下这样的拷贝会消耗极大资源(epoll优化为不拷贝);②进程被唤醒后,不知道哪些连接已经就绪(即收到数据),需要遍历传递出来的所有文件描述符的每一位,不管他们是否就绪(epoll优化为异步事件通知);③select只返回就绪文件的个数,具体哪个文件可读还需要遍历(epoll优化为只返回就绪的文件描述符,无需做无效的遍历)

2、同时能监控的文件描述符太少,受限于sizeof(fd_set)大小,在编译内核时就确定了且无法更改。一般32位操作系统是1024,64位操作系统是2048

  • epoll相关函数:
int epoll_create(int size);   // 创建一个 eventpoll 内核对象

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);   // 将连接到socket对象添加到 eventpoll 对象上,epoll_event是要监听的事件

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);      // 等待连接 socket 的数据是否到达

1、epoll_create(int size):

创建一个struct evenpoll内核对象,evenpoll对象的内部结构如下:

evenpoll主要包含三个字段:

struct eventpoll {

    //sys_epoll_wait用到的等待队列
    wait_queue_head_t wq;

    //接收就绪的描述符都会放到这里
    struct list_head rdllist;

    //红黑树,管理用户进程下添加进来的所有 socket 连接
    struct rb_root rbr;

    ......
}

①wq:等待队列链表,如果当前进程没有数据需要处理,会把当前进程描述符和回调函数default_wake_function构造一个等待队列项,放入当前wq队列,软中断数据就绪的时候,会通过wq来找到阻塞在epoll对象上的用户进程;

②rbr:一颗红黑树,管理用户进程下添加进来的所有socket连接;

③rdlist:就绪的文件描述符链表。当有socket的连接数据就绪时,内核会把就绪的连接放到rdlist链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用取遍历整颗树。

2、epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)添加socket:

epoll_ctl函数主要把服务端和客户端建立的socket连接注册到eventpoll里,会做三件事:

①创建要给epitem对象,主要包含两个字段:socket fd,连接的文件描述符;所属的eventpoll对象的指针;

struct epitem {

    //红黑树节点
    struct rb_node rbn;

    //socket文件描述符信息
    struct epoll_filefd ffd;

    //所归属的 eventpoll 对象
    struct eventpoll *ep;

    //等待队列
    struct list_head pwqlist;
}

②将一个数据到达时用到的回调函数添加到socket的进程等待队列中,其回调函数是ep_poll_callback

③将epitem插入到epoll对象的红黑树里;

完事之后的数据结构如下:

3、epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

epoll_wait首先会检查eventpoll对象的就绪链表rdlist上是否有数据到达,如果没有就把当前的进程描述符添加到eventpoll的等待队列里,然后把自己阻塞掉就完事。

  • epoll处理到达数据

前面epoll_ctl函数执行时,内核为每一个socket上都添加了一个等待队列。在epoll_wait运行完的时候,又在eventpoll对象上添加了等待队列元素。目前结构图如下:

数据到达的处理流程如下:

①socket的数据接收队列有数据到达时,会通过进程等待队列的回调函数ep_poll_callback唤醒红黑树的节点epitem

②ep_poll_callback函数将有数据到达的epitem添加到eventpoll对象的就绪队列rdlist中

③ep_poll_callback函数检查eventpoll对象的进程等待队列上是否有等待项,如果有,通过default_wake_function唤醒这个进程,进行数据的处理

④当进程醒来后,继续从epoll_wait时暂停的代码继续执行,把rdlist中就绪的事件返回给用户进程,让用户进程调用recv把已经到达内核socket等待队列的数据拷贝到用户空间使用

  • 总结

在epoll相关的函数里,内核运行环境分为两部分:①用户进程内核态。进程调用epoll_wait函数时,会将进程陷入内核态来执行,这部分代码负责查看接收队列,以及负责把当前进程阻塞掉,让出CPU。②硬软中断上下文:在这些组件中将数据包从网卡接收过来进行处理,然后放到socket的接收队列。对于epoll来说,再找到socket关联的epitem,再把它添加到epoll对象的就绪链表rdlist中,这个时候再捎带检查一下epoll上是否有被阻塞的线程,如果有则唤醒之。

另外,我们可以看到wake up回调函数机制被频繁使用:

一是阻塞IO中数据到达socket的等待队列时,通过回调函数唤醒进程;

二是epoll数据到达socket数据接收队列时,通过回调函数ep_poll_callback找到eventpoll中红黑树的epitem节点,将其加入到就绪队列rdlist

三是通过回调函数default_wake_function唤醒用户进程,并将rdlist传递给用户进程,让用户进程读取数据。从中可知,这种回调机制能够定向准确的通知程序要处理的事件,而不需要每次都循环遍历查找数据是否到达,以及该由哪个进程处理,提高了程序效率。

在实践中,只要活足够多,epoll_wait根本就不会让进程阻塞,用户进程会一直干活,知道epoll_wait实在没活可干,参会主动让出CPU。这就是epoll高效的地方。

参考:https://cloud.tencent.com/developer/article/2188691

https://cloud.tencent.com/developer/article/1964472

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值