多路复用器: 一种IO复用技术可以让一个进程同时为多个客户端端提供服务,减少了系统调用次数。常见三种:select、poll和epoll。
wakeup callback机制: 这是IO多路复用机制存在的本质。Linux通过socket睡眠队列来管理所有等待socket的某个事件的process,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的process,通知process相关事件发生。通常情况,socket的事件发生的时候,其会顺序遍历socket睡眠队列上的每个process节点,调用每个process节点挂载的callback函数。在遍历的过程中,如果遇到某个节点是排他的,那么就终止遍历,总体上会涉及两大逻辑:(1)睡眠等待逻辑;(2)唤醒逻辑。
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
-
select参数含义:
- nfds: 文件描述符个数。
- struct fd_set: 存放文件描述符(file descriptor)的集合,包括:可读,可写,发生异常三种状态。
- struct timeval: 等待的时间。
-
select原理:
- fd_set: fd_set中的每一bit可以对应一个文件描述符fd,1字节长的fd_set最大可以对应8个fd
- 返回值: 成功时:返回三中描述符集合中准备的文件描述符数量;超时:返回0;错误:返回-1。 select总结:
- select多路复用器是采用 轮询方式,一直在轮询所有的相关内核进程,查看他们的进程状态。若已经就绪(该就绪队列底层由 数组实现),则马上将该内核进程放入到就绪队列。否则,继续查看下一个内核进程状态。在处理内核进程事务之前,app进程首先会从内核空间中将用户连接请求相关数据复制到用户空间。 select缺陷:
- ⭐对所有内核进程采用轮询方式效率会很低。因为对于大多数情况下,内核进程都不属于就绪状态,只有少部分才会是就绪态。所以这种轮询结果大多数是毫无意义的。
- ⭐由于就绪队列底层由数组实现,所以其所能处理的内核进程数量是由限制的,即其能够处理的最大并发连接数是有限制的。
- ⭐从内核空间到用户空间的复制,系统开销大。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
-
poll参数含义:
- fds: 指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件。
- fd: 每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。
- fds: 指定监测fd的事件(输入、输出、错误),每一个事件有多个取值。
- events: 存放文件描述符(file descriptor)的集合,包括:可读,可写,发生异常三种状态。
- revents: revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域,events 域中请求的任何事件都可能在 revents 域中返回。
- nfds: 用来指定第一个参数数组元素个数。
- timeout: 指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回. poll原理:
- fd_set: fd_set中的每一bit可以对应一个文件描述符fd,1字节长的fd_set最大可以对应8个fd
- 返回值: 成功时:poll() 返回结构体中 revents 域不为 0 的文件描述符个数;超时:返回0;错误:返回-1。 poll与select区别:
- 其就绪队列由链表实现;
- 对于要处理的内核进程数量理论上是没有限制。 select缺陷:
- poll并没着手解决性能问题,poll只是解决了select的问题fds集合大小1024限制问题。
epoll的接口非常简单,一共就三个函数:
⭐epoll_create:
int epoll_create(int size);
-
epoll_create函数:
- size: 必须设置一个大于0的值。
- 返回值: 调用成功返回一个非负值的文件描述符pollfd,调用失败返回-1。
⭐epoll_ctl:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
poll_event:
struct epoll_event {
uint32_t events; // 需要检测的fd事件,取值与poll函数一样
epoll_data_t data; // 用户自定义数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
-
epoll_ctl函数:
- epfd: 调用epoll_create函数创建的pollfd。
- op: 表示向epollfd上添加、修改或移除一个其他fd。(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)
- fd: 即需要被操作的fd。
- event: 是一个epoll_event结构体的地址。
- 返回值: 调用成功返回0,调用失败返回-1。
⭐epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
epoll_wait函数:
- epfd: 调用epoll_create函数创建的epollfd。
- event: 是一个epoll_event结构数组的首地址,这是一个输出参数,函数调用成功后,events中存放的是与就绪事件相关epoll_event结构体数组。
- maxevents: 数组元素的个数。
- timeout: 超时时间,单位是毫秒,如果设置为0,epoll_wait会立即返回。
- 返回值: 调用成功会返回有事件的fd数目;如果返回0表示超时;调用失败返回-1。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket。这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。
-
poll与select区别:
- ⭐epoll多路复用器是对select与poll的增强与改进,其不再采用轮询方式,而是采用 回调方式实现对内核进程状态的获取;
- ⭐一旦内核进程就绪,其就会回调epoll多路复用器,进入到多路复用器的就绪队列(由 链表实现);
- ⭐应用程序所使用的数据,也不再从内核空间复制到用户空间了,而是使用mmap (是指文件/对象的内存映射,被映射到多个内存页上)零拷贝机制,所以同一块内存(共享内存)就可以避免这个问题,大大降低了系统开销。
epoll 一定要加定时器
LT模式和ET模式select()、poll()模型都是水平触发模式 (LT),epoll()模型即支持水平触发,也支持边缘触发 (ET),默认是水平触发。边缘触发 是指每当状态变化时发生一个io事件, 条件触发 是只要满足条件就发生一个io事件。
** LT:** 默认/缺省的工作方式,支持 block 和 no_block socket,这种工作方式下,内核会通知你一个 fd 是否就绪,然后才可以对这个就绪的 fd 进行I/O操作。就算你没有任何操作,系统还是会继续提示 fd 已经就绪,不过这种工作方式出错会比较小。(水平触发,只要有数据可以读,不管怎样都会通知)
** ET:** 高速工作方式 ,仅支持 no_block socket,这种工作方式下,当 fd 从未就绪变为就绪时,内核会通知 fd 已经就绪,并且内核认为你知道该 fd 已经就绪,不会再次通知了,除非因为某些操作导致fd就绪状态发生变化。如果一直不对这个 fd 进行I/O操作,导致 fd 变为未就绪时,内核同样不会发送更多的通知,因为 only once。所以这种方式下,出错率比较高,需要增加一些检测程序。(边缘触发,只有状态发生变化时才会通知,可以理解为电平变化)
总结:
1.对于监听的sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。
2.对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。
3.对于读写的connfd,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。



