# IO 多路复用
在进行网络通信的时候,服务端需要通过监听的文件描述符去接收客户端的请求,并为这些请求分配用于通信的文件描述符。当客户端的数据还问到达服务端网卡时,服务端会堵塞在 read
调用处,在没有多线程 / 多进程的情况下,无法实现并发. IO 多路复用就是通过单线程 / 单进程的方式去实现并发的监听,其思想是将监听这些文件描述符的工作交给内核去处理,当客户端的数据到达服务端网卡时,内核再告知应用进程,而不会堵塞。服务端.
# 同步堵塞 IO (Blocking IO)
# 同步非堵塞 IO (Nonblocking IO)
# select
将需要检测的文件描述符保存在位图中,同时将位图由用户区拷贝到内核区,交由内核区检测对应文件描述符的一些读、写或异常事件。当有相应事件触发时,回返回相应事件的数目。同时内核会将有事件触发的文件描述符做上标记并由内核区拷贝到用户区.
#include <sys/select.h> | |
#include <sys/time.h> | |
struct timeval{ | |
time_t tv_sec; /* seconds */ | |
suseconds tv_usec; /* microseconds */ | |
}; | |
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); | |
/* | |
fd_set: 占用 1024bit,用于记录文件描述符的状态 (0/1) | |
*/ |
maxfd
:最大文件描述符 + 1.readset
:需要监听的可读事件集合writeset
:需要监听的可写事件集合exceptset
:需要监听的异常事件集合timeout
:超时时间(永久等待null
、正常超时、立即返回0
)
操作函数
void FD_CLR(int fd, fd_set *set); /* 将文件描述符对应的标志位设置为 0 */ | |
int FD_ISSET(int fd, fd_set *set); /* 判读文件描述符对应的标志位是否为 1 */ | |
void FD_SET(int fd, fd_set *set); /* 将文件描述符对应的标志位设置为 1 */ | |
void FD_ZERO(fd_set *set); /* 将所有的文件描述符标志位设为 0 */ |
select
的执行原理:
- 将当前进程待检测的文件描述符对应的位图一次性从用户空间拷贝到内核空间.
- 在内核空间,操作系统会去遍历这些文件描述符,判断其是否有数据到达.
- 操作系统将所有的文件描述符从内核空间拷贝到用户空间,同时返回已就绪的文件描述符的数目.
- 在用户态遍历这些文件描述符,通过返回的状态判断该文件描述符是否处于就绪状态.
不足:
- 可检测的文件描述符的数目具有限制,一般是 1024 长度.
- 对于内核空间,操作系统并不知道那些文件描述符需要被检测,会依次的遍历文件描述符对应的位图,需要花费 的时间复杂度。对于用户空间,应用程序并不知道具体是哪些文件描述符有事件触发,同样也需要遍历文件描述符对应的位图来判断,需要花费 的时间复杂度
- 位图是一个传入传出参数,在传入时用于标识需要检测的文件描述符,传出时用于标识已就绪的文件描述符,无法得到复用.
- 调用一次
select
会产生两次的内存拷贝,当频繁的调用影响系统开销
# poll
使用结构体将文件描述符、需要检测的事件和检测到的事件封装起来. poll
也就不会有 1024 个文件描述符的限制,因此相对于 select
其可承受更高的并发
#include <poll.h> | |
struct pollfd{ | |
int fd; /* 委托内核检测的文件描述符 */ | |
short events; /* 检测的事件,POLLIN | POLLOUT | POLLERR */ | |
short revents /* 触发的事件 */ | |
}; | |
int poll(struct pollfd *fdarray, nfds_t nfds, int timeout); |
fdarray
:pollfd
数组的首地址nfds
:pollfd
数组的长度timeout
:超时时间(无限等待INFTIM/负值
、超时时间、立即返回0
)
poll
的执行执行原理:
- 将当前进程带检测的
poll
文件描述符对应的结构体数组复制到内核空间 - 在内核空间,操作系统依次遍历结构体数组,判断是否有相应的数据到达.
- 将结构体数组由内核空间拷贝到用户空间,同时返回已就绪的文件描述符的数目
- 在用户态遍历结构体数组,判断相应的事件是否就绪.
不足:
- 对于用户空间,应用程序并不知道具体那些文件描述符有事件触发,需要依次遍历文件描述符对应的结构体去检查
revents
位置的信息. - 调用一次
poll
会产生两次的内存拷贝,当频繁的调用影响系统开销
# epoll
# epoll_create
int epoll_create(int size); /* 创建 epoll 实例,通过一颗红黑树来管理待检测文件描述符集合 */ | |
int epoll_create1(int flags); /* */ | |
/* | |
Synopsis: | |
#include <sys/epoll.h> | |
Description: | |
创建 epoll 实例,通过一颗红黑树来管理待检测文件描述符集合;在 Linux2.6.8 之后,size 参数可被省略,但是 size 传入的值必须大于 0. | |
size: epoll 树实例可以管理的最大文件描述符集合的大小 | |
return: | |
调用成功:返回一个用于表示 epoll 实例的文件描述符.(非负数) | |
调用失败:返回 - 1. | |
*/ |
# epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /* 管理红黑树上的文件描述符(添加,修改、删除) */ | |
/* | |
Synopsis: | |
#include <sys/epoll.h> | |
Description: | |
epfd: 用于表示 epoll 实例的文件描述符 | |
op: 操作类型 | |
1. EPOLL_CTL_ADD: 将文件描述符 fd 添加至 epoll 实例中,监听该文件描述符的 epoll_event 事件 | |
2. EPOLL_CTL_MOD: 修改文件描述符 fd 监听事件修改为 epoll_event | |
3. EPOLL_CTL_DEL: 删除文件描述符 fd, epoll_event 可以设置为空 | |
fd: 待操作的文件描述符 | |
epoll_event: 待操作的文件描述符需要监听的事件. | |
return: | |
调用成功:返回 0 | |
调用失败:返回 - 1. | |
*/ | |
/* 事件结构体 */ | |
typedef union epoll_data{ | |
void *ptr; | |
int fd; | |
uint32_t u32; | |
uint64_t u64; | |
}epoll_data_t; | |
struct epoll_event{ | |
uint32_t events; /* epoll event */ | |
epoll_data_t data; /* User data */ | |
}; | |
/* | |
events: epoll events (需要检测的事件) | |
1. EPOLLIN: 读事件(检测文件描述符的读缓冲区是否有数据) | |
2. EPOLLOUT: 写事件(检测文件描述符的写缓冲区是否可写,未满即可写) | |
3. EPOLLERR: 错误事件 | |
*/ |
# epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); /* 检测 epoll 树中是否有就绪的文件描述符 */ | |
/* | |
*/ |
# 参考
- [1] IO 多路复用
- [2] select、poll、epoll 这三种 IO 多路复用技术的执行原理