Select,Poll,Epoll是学习I/O多路复用必不可少的一节,这章我将借用对这三个系统调用的讲解,进一步加深对于I/O多路复用的理解
首先我们要知道为什么要有I/O多路复用,可以通过一次系统调用,检查多个文件描述符的状态。这是 I/O 多路复用的主要优点,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。
I/O 多路复用相当于将「遍历所有文件描述符、通过非阻塞 I/O 查看其是否就绪」的过程从用户线程移到了内核中,由内核来负责轮询。
首先回忆一下,I/O多路复用是一种使用少数线程来监听多数网络Socket的一种I/O方法,那么我们怎么使用I/O多路复用呢
Select
select使用一个固定大小的位图来表示文件描述符fd的集合,调用select检查这些fd的状态,每次调用select时都会重新构建位图,并将其传递给内核,内核来判断是否有I/O已经就绪
select的核心数据结构是一个fd_set, 这是一个文件描述符集合,用来管理需要监视的文件描述符
fd_set的核心是一个位图(大小位1024),每一位对应着文件描述符的状态,1表示该描述符需要监视,0表示该描述符不需要监视
typedef __kernel_fd_set fd_set;
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
select的函数原型如下
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
可以看到,有三种fd_set
● readfds -- 可读事件,用来监视事件是否可读
● writefds -- 可写事件,用来监视事件是否可写
● exceptfds -- 描述符是否有异常情况
● nfds -- 要监视的文件描述符的最大值+1
● timeout -- 可以选择的监视时间 可以是 阻塞(NULL),立即返回(0),或者指定的等待时间
1. 在调用select的是否我们需要把需要监视的事件通过函数加入到对应的队列中
2. 进入内核态进行检查,程序在调用select之后,内核会历遍对应的fd查看是否符合对应的状态
a. 符合:如果符合就把事件加入到readfds当中去
b. 不符合:如果所有的都不符合就根据timeout来选择等待的方式和事件
3. 返回,最后会返回符合要求的fd的数量
while (1) {
fd_set rfds;
fd_set wfds;
int32_t maxfd = 0, res = 0;
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 500;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(socket1, &rfds);
FD_SET(socket2, &rfds);
maxfd = socket1 > socket2 ? socket1 : socket2;
res = select(maxfd + 1, &rfds, NULL, NULL, &timeout);
if (res < 0 && errno != EINTR && errno != 0) {
// log it
return;
}
if (FD_ISSET(socket1, &rfds)) {
// do something
}
if (FD_ISSET(socket2, &rfds)) {
// do something
}
}
上面大致讲解了如何使用select,这里其实我们可以很明显的看出一个缺点的,
1. 就是我们并不返回符合要求的fd,而是把所有的fd都返回,所以返回到用户态之后我们要进行fd的历遍最终才能找到有相应的fd,这显然是比较耗费事件的 |