select、poll和epoll总结

select、poll和epoll总结

@author:Jingdai
@date:2021.07.12

select、poll和epoll都是操作系统实现多路复用的方式,多路复用是对线程的复用,让一个线程高效的处理多个socket,下面介绍这几种多路复用方式。

select

首先来看操作系统提供的函数。

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
/**
    nfds:        监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
    readfds:    监控有读数据到达文件描述符集合,传入传出参数
    writefds:   监控写数据到达文件描述符集合,传入传出参数
    exceptfds:  监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
    timeout:    定时阻塞时间,3种情况
                1.NULL,永远等下去
                2.设置timeval,等待固定时间
                3.设置timeval里时间均为0,检查描述字后立即返回
    struct timeval {
        long tv_sec;   // seconds 
        long tv_usec;  // microseconds 
    };
*/
void FD_CLR(int fd, fd_set *set);   // 把文件描述符集合里fd清0
int  FD_ISSET(int fd, fd_set *set); // 测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set);   // 文件描述符集合里fd位置1
void FD_ZERO(fd_set *set);          // 文件描述符集合里所有位清0

fd_set结构

fd_set 是一段长1024位的数组,通过上面的FD_CLR、FD_ISSET等宏来对这个fd_set中的位数据进行操作。

伪代码例子

// 1. 创建fd_set
fd_set readfdset;

// 2. 初始化fd_set
FD_ZERO(&readfdset);
FD_SET(listensock, &readfdset);
maxfd = listensock;

// 3. 调用select方法
while (1) {
    // 重新初始化 fd_set
    xxx
    // 阻塞
    int infds = select(maxfd + 1, &tmpfdset, NULL, NULL, NULL);
    for (int eventfd = 0; eventfd <= maxfd; eventfd++) {
        // 如果是监听连接的socket
        if (eventfd == listensock) {
            // 接收请求并把连接加入fd_set
            xxx
        }
        // 如果是接收数据的socket
        if (xx) {
            xxx
        } 
    }
}

select 调用时,会把该进程放到所有它监听的socket的等待队列中,当有事件发生时,再把该进程从所有等待队列中取出并放入工作队列中。同时,程序并不知道哪个socket发生了事件,还需要遍历一遍fd_set来查看哪个socket发生了事件。

总结:select每次调用都要把fd_set重新拷贝一遍,从用户态拷贝进内核态。同时,用户并不知道具体是哪个socket有事件发生,所以需要遍历一遍传回的fd_set,当socket很多的时候,性能就很低了。最后,select监控的socket个数也有限制,bitmap一共1024位,128个字节,同时0、1、2文件描述符用于标准输入输出,一共能监控的socket也就1021个。

poll

操作系统提供的API。

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

typedef struct pollfd {
        int fd;                         /* 需要被检测或选择的文件描述符*/
        short events;                   /* 对文件描述符fd上感兴趣的事件,常用的有POLLRDNORM,普通事件可读 */
        short revents;                  /* 文件描述符fd上当前实际发生的事件,函数返回时通过此判断发生了哪些事件*/
} pollfd_t;

/*
 * poll()函数返回fds集合中就绪的读、写,或出错的文件描述符数量,返回0表示超时,返回-1表示出错;
   fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;
   nfds:记录数组fds中描述符的总数量;
   timeout:调用poll函数阻塞的超时时间,单位毫秒;
*/

poll本质和select类似,但是poll不是用的bitmap,所以没有1024的数量限制,但是在函数调用时仍然需要拷贝fds数组进内核,在函数返回时仍然需要遍历数组才知道哪个socket发生了事件,性能和select差别不大。

伪代码例子

// 1. 初始化 fds
struct pollfd fds[MAXNFDS];
for (int i = 0; i < MAXNFDS; i++) {
    fds[i].fd = -1;
}

// 2. 添加listen socket
fds[listensock].fd = listensock;
fds[listensock].events = POLLIN;
maxfd = listensock;

// 调用poll方法
while (1) {
    int infds = poll(fds, maxfd + 1, 6000);
    // 遍历事件
    for (int eventfd = 0; eventfd <= maxfd; eventfd ++) {
        // 如果是监听连接的socket
        if (eventfd == listensock) {
            // 接收请求并把连接加入fds,同时更新maxfd
            xxx
        }
        // 如果是接收数据的socket
        if (xx) {
            xxx
        }
    }
}

epoll

操作系统提供的API。

int epoll_create(int size);
// 创建一个 eventpoll对象,返回它的文件描述符,自从linux2.6.8之后,size参数被忽略。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 向 eventpoll 对象中添加需要监听的socket对象即事件类型
/**
  epfd:eventpoll对象的文件描述符
  op:有三个宏参数 EPOLL_CTL_ADD:添加fd到epfd中;EPOLL_CTL_MOD:修改已添加的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd;
  fd:要监听的socket的文件描述符
  event:监听的事件类型
*/

// epoll_event 结构
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};
// events常用的几个宏:
/**
 EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
 EPOLLOUT:表示对应的文件描述符可以写;
 EPOLLERR:表示对应的文件描述符发生错误;
 EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,
*/

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 
// 如果函数调用成功,返回有事件的socket文件描述符数目,返回0表示已超时。
/**
  epfd:eventpoll对象的文件描述符
  events:已经分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中
  maxevents:events的大小
  timeout:超时事件。单位毫秒
*/

伪代码例子

// 1. 创建eventpoll
int epollfd;
epollfd = epoll_create(1);

// 2. 添加监听事件
struct epoll_event ev;
ev.data.fd = listensock;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &ev);

// 3. 调用epoll 方法
while (1) {
    // 创建保存事件发生的socket数组
    struct epoll_event events[MAXEVENTS];
    // 阻塞
    int infds = epoll_wait(epollfd, events, MAXEVENTS, -1);
    // 遍历事件发生的数组
    for (int i = 0; i < infds; i++) {
   	    // 如果是监听连接的socket
        if ((events[i].data.fd == listensock) && (events[i].events & EPOLLIN)) {
            // 接收请求并把连接加入epoll
            xxx
        }
        // 如果是接收数据的socket
        else if (events[i].events & EPOLLIN) {
            xxx
        } 
    }
}

在说epoll之前,想一下select和poll的缺点,每次调用阻塞方法时/有事件到来时,都需要遍历一遍socket,将进程加入/移出这些socket的等待队列。每次还需要将 fds 从用户态拷贝到内核态,也有性能开销。同时,在结果返回时都需要遍历一遍传入的 fds,找出具体是哪个socket有事件发生。

epoll 不同于 select 和 poll,它本身会创建一个eventpoll对象,是一个fd。eventpoll最重要的组成是 rdlist 就绪列表和 一个监视列表(一个红黑树),同时,和socket一样,它有自己的等待队列。

当通过epoll_ctl 添加socket时,会在 socket 的等待队列中增加了一个元素,并注册它的回调函数,这个回调函数在执行时会把这个socket(其实是epitem)添加到 eventpoll的 rdllist 就绪队列中,接着去看 eventpoll 的等待队列中是否有等待项,如果有等待项,就唤醒它。

eventpoll 通过一个 rdlist 就绪列表记录已经就绪的socket,这样直接从这个列表就可以知道哪个socket有事件发生,而不用再去遍历。当 socket 上数据就绪时,中断程序会操作对应的eventpoll对象,在eventpoll对象的 rdlist 就绪列表中添加上这个 socket 的引用(其实是 epitem),即 eventpoll 对象相当于是socket和进程之间的中介,socket的数据接收现在并不直接影响进程,而是去改变eventpoll的就绪列表。而之后程序 epoll_wait 时,如果rdlist已经有了 epitem 元素,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。

最后,为了实现socket有事件了方便添加;当不再监控这个socket时,如果socket已经在rdlist中,也应该删除这个socket,方便删除。rdlist使用了方便添加、删除的双向链表实现。监视列表同样要方便添加、删除,还要便于搜索,以避免重复添加。监视列表使用了红黑树的数据结构实现。

总结:epoll 在执行 epoll_wait 方法时,不会再将进程放入 socket 的等待队列中,而是放入了 eventpoll 的等待队列中,而监控的 socket 的等待队列中有 eventpoll 的引用,当有事件发生时中断处理程序会把这个 socket 的引用添加到 rdlist 的就绪列表中,同时rdlist就绪列表不为空就会唤醒进程,进程也可以通过这个rdlist 知道哪些socket 发生了事件。在执行 epoll_wait 方法时,只需要通过 epoll_ctl 方法把要监控的socket 添加一次就行,不用在用户态和内核态之间来回拷贝。方法返回时,直接返回的就是有事件发生的 socket列表,用户程序也不用再遍历去寻找哪些socket发生了事件。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值