I/O模型:阻塞I/O、非阻塞I/O、I/O复用、信号I/O、异步I/O
I/O复用:一个单进程、单线程的服务器程序同时监听多个文件描述符上是否有关注的事件发生,如果某些文件描述符上有事件发生,则程序接着处理有事件发生的文件描述符,没有事件发生的文件描述符则不予理会,这样就可以极大的提高程序的性能。使得一个程序能同时监听多个文件描述符。I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的,并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的,如果要实现并发,只能使用多进程或多线程、进程池或线程池等手段。
I/O复用:一个进程或者一个线程,能够同时对多个文件描述符(socket)提供服务
I/O复用方式:select poll epoll
select函数原型:int select(int nfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回值: -1:失败 0:超时 大于0:就绪的文件描述符个数
nfd:最大的文件描述符的值+1
timeout:指定超时时间(NULL表示永久阻塞,直到有文件描述符上事件发生、0为非阻塞,检查完每一个文件描述符就返回、等待固定时间,存在文件描述符就绪或等待时间到达返回)
readfds:可读事件的文件描述符集合、writefds:可写事件的文件描述符集合、exceptfds:异常事件的文件描述符集合,在select调用时,将用户关注的文件描述符传递给系统内核;select返回时,内核也是通过在线修改这三个变量,来通知应用程序哪些文件描述符就绪,基于linux 2.6.37源码
typedef __kernel_fd_set fd_set;
#undef __NFDBITS
#define __NFDBITS (8 * sizeof(unsigned long)) // 8*4 32#undef __FD_SETSIZE
#define __FD_SETSIZE 1024#undef __FDSET_LONGS
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS) //1024/32 32typedef struct {
unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;由此可以看出fd_set是一个无符号大小整形数组,该数组的每个元素的每一位标记一个文件描述符、fd_set能容纳的文件描述符数量由_FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作过于繁琐,使用下面的一系列宏来访问fd_set结构体中的位:
FD_ZERO(fd_set *fdset); 清除fdset的所有位
FD_SET(int fd, fd_set *fdset): 设置fdset的fd位
FD_CLR(int fd, fd_set *fdset); 清除fdset的fd位
int FD_ISSET(int fd, fd_set *fdset); 测试fdset的fd位是否被设置select特点:
关注可读、可写 、异常事件,记录事件的结构(在数组按位记录文件描述符上的事件)
每次做多可以监听1024个文件描述符,并且其最大值1023。因为底层是一个int型32位数组
select函数返回时,通过传递的结构体变量将结果带回(就绪的文件和未就绪的文件描述符),并且内核会修改用户变量,每次都必须循环探测那些文件描述符就绪 时间复杂度为O(n)、每次调用select之前都必须重新设置三个结构体变量
select函数第一个参数,最大的文件描述符值+1,提高底层效率
用户传递的文件描述符和内核反馈的文件描述符都是通过select参数,所以每次调用select之前必须重新设置结构体,内核会将所有的文件描述符返回,所以用户探测就绪文件描述符的时间复杂度O(n)。用户态和内核态交互,内核修改后再传递给用户态。select缺点:
每次调用select,都需要把fd集合从用户态拷贝到内核态。这个开销在fd很多时会很大
文件描述符就绪时,内核会修改readfds,writrfds、execptfds结构,在每次调用select前都要将文件描述符重新注册一遍
每次调用select都需要在内核遍历传递进来的所有fd。这个开销在fd很多时会很大
单个进程能够监视的文件描述符存在最大限制描述符就绪的条件:
满足以下条件,文件描述符准备读
文件描述符接收缓冲区的数据字节数大于文件描述符接收缓冲区中设置的最小值(TCP、UDP默认为1)
该链接的读半部关闭(接收了FIN的TCP)
该文件描述符是一个监听描述符,且已完成的连接数不为0,对于这样的描述符,accept通常不会阻塞
有一个文件描述符错误处理
满足以下条件,文件描述符准备写
该文件描述符发送缓冲区中可用的大小大于等于文件描述符发送缓冲区设置的最小值,或该描述符已连接,或该描述符不需要连接(UDP)
该连接的写半部关闭
使用非阻塞的connect的文件描述符已建立连接,或connect已失败
有一个文件描述符待错误处理
poll函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds: 指向一个结构体数组
nfds: 结构体数组的长度
timeout: 超时时间 -1 为永久阻塞
返回值: 与select函数相同struct pollfd {
int fd; //文件描述符
short events; //事件类型
short revents; //内核填充
};
fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。fds传递的是数组首地址。poll的事件类型:
poll的优点:
常量 说明 是否可作为输入 是否可作为输出 POLLIN 普通或优先级带数据可读 是 是 POLLRDNORM 普通数据可读 是 是 POLLRDBAND 优先级带数据可读 是 是 POLLPRI 高优先级数据可读 是 是 POLLOUT 普通数据可写 是 是 POLLWRNORM 普通数据可写 是 是 POLLWRBAND 优先级带数据可写 是 是 POLLERR 发生错误
否 是 POLLHUP 对方描述符挂起 否 是 POLLNVAL 描述字不是一个打开的文件 是 是
将用户关注的文件描述符的事件单独表示,可以关注更多的事件类型
将用户传递的和内核修改的参数分开,每次调用poll之前不用重新设置
poll函数没有最大文件描述符的限制
poll的缺点:
每次嗲用都需要将用户空间的数组拷贝到内核空间
每次返回都需要将所有的文件描述符拷贝到用户数组中,无论是否就绪
返回的是所有的文件描述符,搜索文件描述符的时间复杂度为O(n)
poll与select的不同:
用户关注的事件类型更多 (不在关注依靠read、 write、 except; 直接依靠short events类型的成员)
内核修改的和用户关注的分开表示,每次调用不需要重新设置。
文件描述符不是在按位表示,直接用int类型、用户关注的文件描述符的值可以更大、用户关注的文件描述符的个数由用户数组决定,所以个数会更多。
相同: poll返回的时候,也是将用户关注的所有文件描述符返回。poll检测就绪文件描述符的时间复杂度 O(n),poll返回后,用户程序依旧需要循环检测那些文件描述符就绪。
epoll函数原型:
int epoll_create(int size); //创建内核事件表,返回事件表标识
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //对于内核事件表的操作
op:EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL
EPOLL_CTL_ADD,往事件表中注册fd上的事件;
EPOLL_CTL_MOD,修改fd上的注册事件;
EPOLL_CTL_DEL,删除fd上的注册事件。
int epoll_wait(int epfd, struct epoll_event *events, int maxEvents,int timeout);
events:指向一个数组,保存内核返回的所有就绪的文件描述符
maxevents: 数组的长度、timeout: 与poll相同、返回值: 与select相同struct epoll_event {
__u32 events; //epoll事件
__u64 data; //用户数据
}epoll_wait如果检测到事件,就将所有的就绪事件从内核事件表中复制到第二个参数events指定的数组总,这个数组只输出epoll_wait检测到的就绪事件,所以,搜索就绪文件描述符的时间复杂度为O(1)
epoll :由内核态事件表保存用户关注的文件描述符以及其事件,减少了用户态数据向内核态的拷贝、epoll_wait 返回的是就绪的文件描述符,所以检验就绪文件描述符的时间复杂度O(1)、epoll内核采用回溯的方式。
select、poll、epoll的对比:
select通过三个结构体分别表示可读、可写、异常事件,poll和epoll用一个short类型的变量表示关注的事件,事件类型更多
select通过32个元素的long类型的数组按位记录文件描述符,最多1024个文件描述符,范围是0—1023.poll和epoll都是通过一个int的fd表示文件描述符.poll通过用户数组记录所有文件描述符,epoll通过内核事件表记录。一般能达到系统允许打开的最大文件描述符。
select通过三个结构体传递文件描述符,也是通过其返回就绪和未就绪的文件描述符,所有每次调用select都必须重新设置三个结构体。epoll将用户关注的事件和内核反馈发生的事件分开表示,poll通过数组返回就绪的文件描述符,epoll和poll不需要重置三个结构体。
select和poll返回的是就绪和未就绪的文件描述符,检测就绪文件描述符的事件复杂度为O(n),epoll直接通过数组仅仅返回所有的就绪文件描述符,检测就绪文件描述符的时间复杂度为O(1)
select和poll内核采用轮询的方式,epoll采用回调的方式;回调:哪个文件就绪了,才会触发。 轮循是一个一个的去检查。
select内核通过数组,poll内核链表,epoll内核是红黑树+链表
select和poll仅仅支持LT模式,epoll支持LT 和ET模式
select和poll都是单独的函数,epoll是一组函数
select和poll每次调用都会把数据从用户态拷贝到内核态,epoll会直接从内核态拷贝
分析得出,epoll的效率最高。但是在有一种情况下:注册10000个文件描述符,每次都有9960个就绪,就意味着活动的文件描述符很大,这种情况下poll的效率反而比epoll的效率高。因为几乎文件描述符都是就绪,概率很大,并且采用轮循就比要回调9960次更快,效率更高。因此epoll使用于连接很多,但是活动连接较少的情况下。LT和ET:
LT模式 -- 电平触发 当epoll_wait检测到文件描述符上有事件发生,并且将此事件通知应用程序之后,应用程序可以不立即处理该事件,当下次调用epoll_wait下次被调用时,还会向应用程序在此通知这个事件,直到事件被处理
ET模式 -- 边沿触发 当epoll_wait检测到文件描述符上有事件发生,将此事件通知应用程序之后,应用程序必须立即处理该事件,并且需要将该事件处理完成。因为epoll_wait下次再被调用时,不会在向应用程序通知该事件。从而降低了同一事件被重复触发的次数,从而效率比LT模式高ET比LT高效的原因:
同一个事件ET只会通知一次,LT会通知多次,epoll_wait函数调用多次,epoll_wait的调用需要消耗时间
LT模式下,epoll_wait因为上一个事件未处理完而直接返回,造成后续就绪事件的延迟处理
ET模式内核实现时,只会将rdlist中的就绪的文件描述符通过txlist拷贝给用户空间,并且rdlist会被清空
LT模式内核实现时,将rdlist中的就绪文件描述符通过txlist拷贝给用户空间,rdlist会被清空,但是会将未处理或未处理完的文件描述符又返回给rdlist,以便下次返回使用
两种高效的事件处理模式
Reactor模式:主线程仅仅负责监听文件描述符上是否有事件发生,如果有事件发生则立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据,接收新的连接,以及处理客户请求均在工作线程中完成
Proactor模式:所有的I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑两种高效的并发模式:
半同步/半异步模式:
同步: 程序完全按照代码序列的顺序执行
异步:程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号灯
领导者/追随者模式:
由多个线程轮流获得事件源集合,轮流监听、分发并处理事件。
在任意一个时间点,程序都仅有一个领导者线程,它负责监听I/O事件。其他线程则都是追随者。
当前的领导者如果检测到I/O事件,首先从线程池中推选出新的领导者线程,然后处理I/O事件。当处理完事件后,就会变为追随者,阻塞到线程池中。