文章目录
一、select
系统提供select函数来实现多路复用输入/输出模型.
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
select函数就是一个就绪事件的通知机制,就绪事件分为两种:读事件就绪和写事件就绪
读事件就绪
- read:底层数据从无 -> 有, 有 -> 多
- select:底层数据只要有,就是读事件就绪
写事件就绪
- write:底层缓冲区剩余空间从无 -> 有, 有 -> 多
- select:底层缓冲区剩余空间只要有,就是写事件就绪
select等到事件就绪后,调用read、recv、write、send等不会被阻塞,因为已经有数据或有空间了
select 可以一次等待多个文件描述符
1.1 select函数接口
man 2 select
#include<sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, struct timeval* timeout);
参数解释:
- nfds:select在等待的多个描述符值中,最大的文件描述符+1(对多个文件描述符进行轮询检测 例如:i = 0; i < 10; i++ )
- fd_set :类似 sigset_t 是一个位图,可以将特定的fd添加(调函数添加和删除)到位图中(0000 0000 1号fd添加到fd_set中变为0000 0010),该参数为输入输出型函数,输入:用户告诉内核哪些文件描述符要监视,输出:内核告诉用户哪些文件描述符事件就绪
- timeout:用来设置select()的等待时间,设置为NULL 《=》阻塞式等待:没有事件就绪的文件描述符一直阻塞等待; 设置为结构体里的时间为0《=》非阻塞式等待:没有事件就绪立即返回
struct timeval{
long tv_sec; //second
long tv_usec; //microseconds
};
提供了一组操作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的全部位
返回值:
- 返回所有事件就绪的文件描述符个数
- 返回0,timeout时间过期了
- 返回-1,等待出错了
错误值可能为:
- EBADF文件描述词为无效的或该文件关闭
- EINTR 被信号中断
- EINVAL 参数n为负值
- ENOMEM 核心内存不足
1.2 select具体使用(代码)
1.3 select的优缺点和使用场景
从上述具体用法的代码可以看出select是存在不少缺点的:
- select能够同时等待的文件描述符是有上限的。因为fd_set是一个具体的数据类型,而在CentOS中
sizeof(fd_set)*8 = 1024
得出文件描述符集中添加的fd最多只有1024个 - 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- select每次调用,都必须重新手动给
fd_set
重新添加fd,一定会影响程序运行的效率,而且非常麻烦容易出错 - 参数maxfd+1:操作系统在检测fd就绪时,需要遍历。所以当有大量的链接时,内核同步select底层遍历,成本会变得越来越高
select的优点:
- select可以同时等待多个fd,而且只负责等待,有具体的accept,recv,send来完成实际的IO操作,这也任何一个fd就绪的概率增加了,服务器可以在单位时间内,等的比重降低,提高了效率
适应场景:
- 适合有大量的链接,但是只有少量是活跃的!多路转接适合的场景!(如:聊天工具)
二 、poll
2.1 poll函数接口
#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
//pollfd结构
struct pollfd{
int fd;
short events; //用户告知内核要关心哪些事件
short revents; //内核告知用户哪些fd的哪些事件就绪了
};
参数说明:
- fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合
- nfds表示fds数组的长度
- timeout表示poll函数的超时时间, 单位是毫秒(ms)。0《=》非阻塞式等待, -1《=》阻塞式等待
events和revents的取值:
返回值与select相同: >0 : 事件就绪的fd个数,=0: 超时无就绪,<0:等待出错
2.2 poll具体使用(代码)
2.3 poll的优缺点
优点:
- 解决了select能检测的文件描述符是有上限的这个问题,数组大小取决于内存大小,理论可以无限大
- 将event和revent分离,不用在每次调用poll的时候,重新添加fd以及fd要关心的事件
缺点:
- 当面临的链接很多,需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 数组的大小越大时,操作系统检测fd就绪还是需要遍历,成本会变高
三、epoll
epoll:是为了处理大批量句柄而做了改进的poll
3.1 epoll的相关系统调用
epoll有3个相关的系统调用
epoll_create —打开一个epoll文件描述符(创建epoll模型,底层包括红黑树、注册回调函数,就绪队列)
#include<sys/epoll.h>
int epoll_create(int size); //成功返回fd,失败返回-1,
// 参数只是为了兼容之前的版本,可以忽略
使用完之后必须close()关闭
epoll_ctl —告诉内核epfd模型中的fd的哪些事件(对底层的红黑树节点进行相关操作)
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数:
- epfd:epoll模型id,epoll_create()的返回值
- op:表示动作,用三个宏来表示
- fd:需要监听的fd
- event:告诉内核需要监听什么事
第二个参数的取值:
- EPOLL_CTL_ADD:注册新的fd到epfd中
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件
- EPOLL_CTL_DEL:从epfd中删除一个fd
struct epoll_event结构如下:
events可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读
- EPOLLOUT : 表示对应的文件描述符可以写
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外来数据)
- EPOLLERR : 表示对应的文件描述符发生错误
- EPOLLHUP : 表示对应的文件描述符被挂断
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需再次把这个socket加入到EPOLL队列里
epoll_wait —告诉用户epfd模型中要关心的哪些事件就绪了
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents,
int timeout);
参数:
- epfd:epoll模型id
- events:epoll会将发生的事件赋值到events数组中(events不可以是空指针)
- maxevents:告诉内核这个events有多大,maxevents不能大于epoll_create()的size
- timeout:超时时间,0立即返回,-1永久阻塞
3.2 epoll的工作机制
- 当某一个进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll{
... ...
//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
struct rb_root rbr;
//双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
... ...
};
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进行来的事件
- 这些事件都会挂在到红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系。也就是说,当响应的事件发生时会调用这个回调方法
- 这个回调方法在内核叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
- 在epoll中,对于每一个事件,都会建立一个epitem结构体
struct epitem{
struct rb_node rbn; //红黑树节点
struct list_head rbllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll* ep //指向其所属的eventpoll对象
struct epoll_event event; /期待发生的事件类型
};
- 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
- 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。这个操作时间复杂度尾O(1)
3.3 epoll具体使用(代码)
3.4 epoll两种工作方式
epoll有2种工作方式-水平触发(LT )和边缘触发(ET)
两种工作方式本质就是数据就绪通知方式的不同
epoll的默认工作方式是LT,水平触发
- 工作机制:以读事件为例,底层只要有数据,就会一直通知用户拿走数据
epoll的ET工作方式,边缘触发
- 工作机制:以读事件为例,底层数据从无到有,从有到多会通知一次用户,之后不再通知
ET的通知方式有一个问题,就是如何保证将本次数据全部读完:
- 循环读取,直到实际读取的数据小于要读取的数据
- 如果实际读取的数据刚好为0,就会发生阻塞,这又要怎么办呢?
所以ET模式下的所有fd,必须将该fd设置为非阻塞!
ET模式倒逼用户一次全部取走数据,tcp的滑动窗口会更新的更大,传输层能传输更多的数据,也减少了内核通知用户事件就绪的重复次数
3.5 epoll的优点(和select的缺点对应)
- 接口使用方便:接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响
- 没有数量限制: 文件描述符数目无上限