IO复用
服务端编程模型是,客户端发来的请求,服务端会产生一个进程来对其进行服务,每当来一个客户请求就产生一个进程来服务, 然而进程不可能无限制产生,因此为了解决大量客户端访问的问题,就引入了IO复用技术
即一个进程可以同时对多个客户请求进行服务
因此, IO复用的“介质”是进程,复用一个进程来对多个IO进行服务,虽然客户端发来的IO是并发的,但是IO所需的读写数据多数情况下是没有准备好的,因此可以利用一个函数(select\poll)来监听IO所需的这些数据的状态,一旦IO有数据可以进行读写了,进程就来对这样的IO进行服务
∴ IO多路复用指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程
IO多路复用使用场景:
- 当个客户处理多个描述字时(交互式输入?和和网络 套接口),必须使用IO复用
- 当一个客户同时处理多个套接字时
- 如果一个TCP服务器既要处理TCP,又要处理UDP,一般要使用IO复用
- 如果一个服务器要处理多个服务或多个协议,一般使用IO复用
与多进程和多线程技术相比,IO多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销
select
select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经过一段指定的时间后才唤醒它
#include <sys/select.h>
#include >sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
maxfdp1
指定待测试的描述字个数,最大描述字+1,因为前闭后开,扫描的时候最大描述符不能漏掉
readset
、writeset
、exceptset
是指定让内核测试读写异常条件的文件描述符集合,如果对某一个条件不感兴趣就可以设为空指针。
timeout
告知内核等待所指定描述字中的任何一个可花费的时间,这个参数是一个结构体
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
timeout
有三种可能:
1.永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
2.等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
3.根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
select代码示例:https://blog.csdn.net/qq_37299596/article/details/105984045
select的调用过程⭐
- 使用copy_from_user从用户空间拷贝fd_set到内核空间
- 注册回调函数
_pollwait
? - 遍历所有fd
_pollwait
的主要工作就是把当前进程挂到设备的等待队列中(注意把进程挂到等待队列中并不代表进程已经睡眠了)
回调函数返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值
如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则调用schedule_time
调用select的当前进程进入睡眠
当设备驱动发生自身资源可读写后,会唤醒其等待队列上的睡眠进程,如果超过一定的时间还是没被唤醒,调用select的进程也会重新被唤醒获得CPU, 进而重新遍历fd,判断有没有就绪的fd - 把fd_set从内核空间拷贝到用户空间
select睡眠和唤醒过程
select利用等待队列机制让用户进程适当的在没有资源可读写时睡眠,有资源可读写时唤醒
其过程:
- select会循环遍历它所检测的fd_set内所有文件描述符对应的驱动程序的函数(本质就是调用
_pollwait
) - 驱动程序的函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列,如读/写等待队列,然后返回一个
bitmask
告诉select当前哪些资源可用 - 当select循环遍历完所有fd_set内指定的文件描述符对应的函数后,如果没有一个资源可用,则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒或者timeout
- 唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。
- 嗯……就很迷,这是底层了,,要再研究一下
优点:
1)select()的可移植性更好,在某些Unix系统上不支持poll()
2)select() 对于超时值提供了更高的精度:微秒,而poll是毫秒。
缺点
1)单个进程可监视的fd数量被限制
2)需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
3)对fd进行扫描时是线性扫描,fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题(那数组那个?
4)select()函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数
poll
poll本质和select没有区别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制;但一样的缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大
它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它要再次遍历fd
❓
这个过程经历了多次无谓的遍历
poll还有一个特点是“水平触发”,如果报告了fd之后,没有被处理,那么下次poll的时候会再次报告该fd
poll通过一个pollfd
数组向内核传递需要关注的事件,所以没有描述符个数的限制,pollfd中的events
字段和revents
分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次
poll的实现机制与select类似:其对应于内核中sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。poll返回后,需要对pollfd中的每个元素检查其revents值确定事件是否发生❓
poll原型 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
:监听的文件描述符数组
nfds
监听的数组的实际有效监听个数
timeout
超时时长,-1阻塞等待,0立即返回不阻塞进程
返回值:返回满足对应监听事件的文件描述符总个数
监听的文件描述符数组:struct poll client
struct pollfd
{
int fd;
short events;//所要监听的文件描述符的事件
short revents;//传入时为0,如果满足对应事件返回非0
};
poll的C代码实现https://blog.csdn.net/qq_37299596/article/details/105991959
优点
1)poll() 不要求开发者计算最大文件描述符加一的大小。
2)poll() 在应付大数目的文件描述符的时候相比于select速度更快
3)它没有最大连接数的限制,原因是它是基于链表来存储的。
缺点
1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2)与select一样,poll返回后,需要轮询 pollfd来获取就绪的描述符
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本
它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它不是复用文件描述符集合来传递结果而使得开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合
epoll无需遍历整个被侦听的描述符集, 只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了
epoll除了提供select/poll类IO事件的电平触发外, 还提供了边沿触发,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait
的调用,提高应用程序效率
epoll的底层思想
epoll在底层实现了自己的高速缓冲区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件
但凡有点犹豫,就立刻跳转回去看基础https://blog.csdn.net/qq_37299596/article/details/106008912
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
1, int epoll_create(int size);
创建一个epoll的句柄(创建一棵监听红黑树),size用来告诉内核这个监听的数目一共有多大,size是内核保证能够正确处理的最大句柄数,注意的是,当创建好epoll句柄后,它是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽
2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的时间注册函数:第一个参数是创建的红黑树的根节点fd,第二个参数op
,有三个操作,add、mod、del;**第三个参数是需要监听的fd,第四个struct epoll_event
↓类型的参数是告诉内核需要监听什么事 **
struct epoll_event {
__uint32_t events; /* Epoll events */EPOLLIN\EPOLLOUT\EPOLLERR
epoll_data_t data; /* User data variable */ //还是一个结构喔,只用data.fd
};
e.g.
cdf = accept(...)
struct epoll_event tep;
tep.events = EPOLLIN;
tep.data.fd = cfd
res = epoll_ctl(树根节点,EPOLL_CTL_ADD,cfd,&tep);//
3,int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生, 第二个参数events
用来从内核得到事件的集合,maxevents
告诉内核这个events有多大,该值不能大于创建epoll_creat
时的size;
我们调用epoll_ wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以,实际上在你调用epoll_ create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。(?)
epoll实现机制⭐
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket。
这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。
这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层。
简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
epoll的高效就在于,当我们调用epoll_ ctl往里塞入百万个句柄时,epoll_ wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。
这是由于我们在调用epoll_ create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.
当epoll_ wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
感谢参考:https://blog.csdn.net/lixungogogo/article/details/52226479
工作过程:
执行epoll_create时,创建了红黑树和就绪链表(?),执行epoll_ctl时如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据(?)。执行epoll_wait时立刻返回准备就绪链表中的数据
epoll优点
1)支持一个进程打开大数目的socket描述符(FD)
2)IO效率不随FD数目增加而线性下降
3)使用mmap加速内核与用户空间的消息传递。
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。
而epoll其实也需要调用 epoll_ wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。
虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间,这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内 部定义的等待队列),这也能节省不少的开销。