#include <sys/select.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
在 Linux 中, 我们可以使用 select 函数实现 I/O 复用, 给 select 函数传递的参数会告诉内核:
- 我们所关心的文件描述符
- 对每个描述符, 我们所关心的状态(读 写 异常).
- 我们要等待多长时间(我们可以等待无限长的时间, 等待固定的一段时间, 或者根本就不等待).
从 select
函数返回后, 内核告诉我们一下信息:
- 对我们的要求已经做好准备的描述符的个数.
- 对于三种条件哪些描述符已经做好准备.
虽然 select
函数, 可以监听多个客户端链接, 有效减少了线程数量.
但是缺点也比较明显:
- 文件描述符数量并不是无限的(1024).
- 当文件描述符过多 IO 效率就会降低.
- 当每次调用 select 函数时, 都会将文件描述符集合从用户态拷贝到内核态.
for
循环 n 次 中的 n 就表示我们所关心的文件描述符, 每判断一次描述符就要循环 32 次, 但是要注意, 如果循环的描述符 大于等于最大描述符+1 时, 循环也会结束.
循环 32 次是因为要判断当前位置有没有注册, 如果没有就会进入下一次循环, 否则就判断数据哪种事件(读 写 异常).
下面是一部分主要代码:
int do_select(int n, fd_set_bits *fds, struct timespec *end_time) {
int retval, i, timed_out = 0;
retval = max_select_fd(n, fds); //获取最大文件描述符
if (retval < 0)
return retval;
n = retval;
retval = 0;
for (;;) {
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
}
}
}
return retval;
}
poll
poll 本质上和 select
没有区别, 它将用户传入的数组拷贝到内核空间, 然后查询每个 fd
对应的设备状态, 如果设备就绪则在设备等待队列中加入一项并继续遍历, 如果遍历完所有 fd
后没有发现就绪设备, 则挂起当前进程, 直到设备就绪或者主动超时, 被唤醒后它又要再次遍历 fd
. 这个过程经历了多次无谓的遍历.
虽然 poll
没有最大描述符限制, 但是有一个 “水平触发” 特点, 也就是说当发现就绪的 fd
后, 没有被处理, 那么下次 poll
还会报告这个 fd
.
epoll
epoll
是基于事件驱动的 I/O 方式, 相对于 select
和 poll
来说, epoll
没有描述符个数限制, 从用户态拷贝到内核态只需要一次. 优点:
没有最大并发连接的限制, 1G 的内存能监听约 10 万个端口.
不是使用轮询方式, 而是使用回调方式. 会使用 callback
函数通知, 而且内核和用户控件使用同一块内存.