目录
1:select
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
select 函数是 POSIX 标准定义的一个系统调用,用于监视多个文件描述符(file descriptors),以确定它们是否具有可读、可写或异常条件。
1. 参数解释
1: int nfds:这是监视的文件描述符集合中最大的文件描述符加一。也就是说,如果你监视的文件描述符最大是 fd_max,那么你应该将 nfds 设置为 fd_max + 1。
2: fd_set *readfds:这是一个指向 fd_set 结构的指针,该结构包含了你想要监视以检查是否有数据可读的文件描述符集合。如果 NULL,则表示不监视任何读文件描述符。
3: fd_set *writefds:这是一个指向 fd_set 结构的指针,包含了你想要监视以检查是否可写的文件描述符集合。如果 NULL,则表示不监视任何写文件描述符。
4: fd_set *exceptfds:这是一个指向 fd_set 结构的指针,包含了你想要监视以检查是否有异常条件的文件描述符集合。在大多数情况下,这用于检查 OOB(out-of-band,紧急)数据。如果 NULL,则表示不监视任何异常文件描述符。
5: struct timeval *timeout:这是一个指向 timeval 结构的指针,它指定了 select 调用等待文件描述符状态改变的最大时间。
参数 timeout 取值:
• NULL: 则表示 select() 没有 timeout, select 将一直被阻塞, 直到某个文件描述符上发生了事件;
• 0: 仅检测描述符集合的状态, 然后立即返回, 并不等待外部事件的发生。
• 特定的时间值: 如果在指定的时间段里没有事件发生, select 将超时返回。
2. 函数返回值
1: 如果有文件描述符准备好了,select 返回准备好的文件描述符的数量。
2: 如果返回 0 代表在描述词状态改变前已超过 timeout 时间, 没有返回。
3: 当有错误发生时则返回-1, 错误原因存于 errno, 此时参数 readfds, writefds,exceptfds 和 timeout 的值变成不可预测。
错误值可能为:
• EBADF 文件描述词为无效的或该文件已关闭。
• EINTR 此调用被信号所中断。
• EINVAL 参数 n 为负值。
• ENOMEM 核心内存不足。
3. fd_set
其实这个结构就是一个整数数组, 更严格的说, 是一个 "位图"。使用位图中对应的位来表示要监视的文件描述符。
4. fd_set 相关接口
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关fd 的位
void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位
5. timeval
5. 常见使用
fd_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){……}
6. 理解 select 执行过程
fd_set 长度为 1 字节, fd_set中的每一 bit 可以对应一个文件描述符 fd。 则 1 字节长的 fd_set 最大可以对应 8 个 fd。
(1): FD_ZERO(&set);
初始化fd_set
,所有位都为0,即0000,0000
。
(2):FD_SET(5, &set);
将第5位设置为1,其他位保持不变,变为0001,0000
。
(3):接着将fd=2
和fd=1
加入集合,分别设置第2位和第1位为1,变为0001,0011
。
(4):select(6, &set, 0, 0, 0);
调用select
函数,监控fd_set
中文件描述符的可读事件。参数6表示监控的文件描述符最大值为5(因为是从0开始计数的,所以6-1=5),select
会阻塞直到有事件发生或超时。
(5):如果fd=1
和fd=2
上都发生了可读事件,select
会返回,此时fd_set
中只有第1位和第2位为1,变为0000,0011
。注意,没有事件发生的fd=5
对应的位被清空了,因为select
函数的行为是只保留那些有事件发生的文件描述符。
7. select 的特点
1:可监控的文件描述符个数取决于 sizeof(fd_set) 的值. 我这边服务器上sizeof(fd_set)= 512, 每 bit 表示一个文件描述符, 则服务器上支持的最大文件描述符是 512*8=4096。
2:将 fd 加入 select 监控集的同时, 还要再使用一个数据结构 array (辅助数组)保存放到 select监控集中的 fd。
一:是用于再 select 返回后, array 作为源数据和 fd_set 进行 FD_ISSET 判断。
二:是 select 返回后会把以前加入的但并无事件发生的 fd 清空, 则每次开始select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先), 扫描 array 的同时取得 fd 最大值 maxfd, 用于 select 的第一个参数。
8. select 缺点
1:每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便。
2:每次调用 select, 都需要把 fd 集合从用户态拷贝到内核态, 这个开销在 fd 很多时会很大。
3:同时每次调用 select 都需要在内核遍历传递进来的所有 fd, 这个开销在 fd 很多时也很大。
5:select 支持的文件描述符数量太小。
9. select 应用
2:socket 就绪条件
1. 读事件就绪(Readable)
• socket 内核中, 接收缓冲区中的字节数, 大于等于低水位标记 SO_RCVLOWAT。 此时可以无阻塞的读该文件描述符, 并且返回值大于 0;
• socket TCP 通信中, 对端关闭连接, 此时对该 socket 读, 则返回 0;
• 监听的 socket 上有新的连接请求;
• socket 上有未处理的错误;
2. 写就绪(Writable)
• socket 内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于 0;
• socket 的写操作被关闭(close 或者 shutdown). 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号;
• socket 使用非阻塞 connect 连接成功或失败之后;
• socket 上有未读取的错误;
3. 异常事件就绪(Exception)
•异常事件就绪意味着socket上发生了一些非正常的事件,如连接断开、错误发生等。
•这通常发生在连接被对方强制关闭,或者在socket上发生了一些错误,需要立即处理。
•在select
函数中,如果一个socket的异常事件就绪,那么在调用select
后,该socket会在返回的异常文件描述符集合中被标记。
3:poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 需要监控的事件 */
short revents; /* 已经发生的事件,由 `poll` 函数填充 */
};
参数说明:
fd:需要监控的文件描述符。
events:需要监控的事件类型,可以是以下宏的组合:
POLLIN:有数据可读。
POLLOUT:写入不会阻塞。
POLLERR:发生错误。
POLLHUP:对端关闭连接。
revents:实际发生的事件,由 poll 函数在调用返回后填充。
返回值:
返回值小于 0, 表示出错;
返回值等于 0, 表示 poll 函数等待超时;
返回值大于 0, 表示 poll 由于监听的文件描述符就绪而返回;
1. events 和 revents
2. poll优点
1. 不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
2. pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便。
3. poll并没有最大数量限制 (但是数量过大后性能也是会下降)。
3. poll缺点
1. 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
2. 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
3. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降。
4. poll 应用
Poll · XiangChao/Linux - 码云 - 开源中国 (gitee.com)https://gitee.com/RuofengMao/linux/tree/master/Poll
4:epoll
1:
创建 epoll 实例:
int epoll_create(int size);
size 参数通常被忽略,但可以作为内核的提示,告知可能监控的文件描述符数量。
2:
使用 epoll_ctl 注册事件:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll 实例的文件描述符。
op:操作类型,如 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。
fd:要监控的文件描述符。
event:指向 struct 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 : 表示对应的文件描述符可以写;
• EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
• EPOLLERR : 表示对应的文件描述符发生错误;
• EPOLLHUP : 表示对应的文件描述符被挂断;
• EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
• EPOLLONESHOT: 只监听一次事件, 当监听完这次事件之后, 如果还需要继
续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.
3:
等待事件的发生:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll 实例的文件描述符。
events:用于存储发生的事件的数组。
maxevents:数组的大小,即最多可以返回的事件数量。
timeout:等待时间(毫秒),-1 表示无限期等待,0 表示立即返回。
4:
处理事件:
在 epoll_wait 返回后,遍历 events 数组,处理每个发生的事件。
1. epoll_wait
• 参数 events 是分配好的 epoll_event 结构体数组。
• epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针, 内核只负责把数据复制到这个 events 数组中, 不会去帮助我们在用户态中分配内存)。
• maxevents 告之内核这个 events 有多大, 这个 maxevents 的值不能大于创建epoll_create()时的 size。
• 参数 timeout 是超时时间 (毫秒, 0 会立即返回, -1 是永久阻塞)。
• 如果函数调用成功, 返回对应 I/O 上已准备好的文件描述符数目, 如返回 0 表示已超时, 返回小于 0 表示函数失败。
2. epoll 工作原理
• 每一个 epoll 对象都有一个独立的 eventpoll 结构体, 用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。
• 这些事件都会挂载在红黑树中, 如此, 重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是 lgn, 其中 n 为树的高度)。
• 而所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系, 也就是说, 当响应的事件发生时会调用这个回调方法。
• 这个回调方法在内核中叫 ep_poll_callback,它会将发生的事件添加到 rdlist 双链表中。
• 在 epoll 中, 对于每一个事件, 都会建立一个 epitem 结构体。
• 当调用 epoll_wait 检查是否有事件发生时, 只需要检查 eventpoll 对象中的rdlist 双链表中是否有 epitem 元素即可。
• 如果 rdlist 不为空, 则把发生的事件复制到用户态, 同时将事件数量返回给用户. 这个操作的时间复杂度是 O(1)。
3. 接口总结
• 调用 epoll_create 创建一个 epoll 句柄;
• 调用 epoll_ctl, 将要监控的文件描述符进行注册;
• 调用 epoll_wait, 等待文件描述符就绪;
4. epoll 的优点
• 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开。
• 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)。
• 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1)。 即使文件描述符数目很多, 效率也不会受到影响。
• 没有数量限制: 文件描述符数目无上限。
注意:
我们定义的 struct epoll_event 是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中。
5. epoll 应用
Epoll · XiangChao/Linux - 码云 - 开源中国 (gitee.com)https://gitee.com/RuofengMao/linux/tree/master/Epoll