Select IO多路复用。
内核态:运行操作系统的应用程序,可以操作调度使用硬件资源,比如CPU。
用户态:用户态的应用程序,不能直接操作硬件资源,内存空间有限。
疑问:单线程处理多个请求,会不会被丢弃(线程在处理A请求的同时,B发请求过来了,B会不会被丢弃)?
不会,因为不是CPU在处理B的IO请求,而是DMA直接内存访问。
进程间的切换变化:
1、保存处理机上下文,包括程序计数器和其他寄存器
2、更新PCB信息
3、把进程的PCB移入相应的队列,如就绪、阻塞队列。
4、选择另一个进程执行,更新PCB
5、更新内存管理的数据结构
6、恢复处理机上下文。
上下文切换资源开销是较大的。
linux系统的五大IO
1、同步阻塞IO BIO
2、同步非阻塞IO NIO
3、I/O多路复用 IO Multiplexing
4、信号驱动I/O
5、异步IO AIO
select、poll、epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,
一旦某个描述符就绪(一般指读就绪或者写就绪),能够通知程序进行相应的读写操作。
但是select、poll,epoll本质上都是同步阻塞I/O,因为他们都需要在读写时间就绪以后自己负责进行读写,
也就是整个读写过程是足鳃的,而异步I/O也需要自己负责读写,异步IO的实现会把数据从内核态拷贝到用户空间。
优势:比如select就是批量进行从用户态拷贝到内核态进行判断,但是如果是在用户态进行访问,会增大系统开销,会单个进行一次用户态和内核态的切换。
Select(阻塞)
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
// nfds:监控的文件描述符集里最大文件描述符加1
// readfds:监控有读数据到达文件描述符集合,传入传出参数
// writefds:监控写数据到达文件描述符集合,传入传出参数
// exceptfds:监控异常发生达文件描述符集合, 传入传出参数
// timeout:定时阻塞监控时间,3种情况
// 1.NULL,永远等下去
// 2.设置timeval,等待固定时间
// 3.设置timeval里时间均为0,检查描述字后立即返回,轮询
select函数监视的文件描述符分为3类,分别是writefds,readfds,exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者except),或者超时(timeout指定等待时间)函数返回,当select函数返回后可以通过遍历fdset,来找到就绪的描述符。
服务端:创建一个线程不断接收客户端的连接请求,并把socket文件放入文件描述符的list中。
while(1) { connfd = accept(listenfd); fcntl(connfd, F_SETFL, O_NONBLOCK); fdlist.add(connfd); }
处理任务
启动一个线程,不是调用select,将这批文件描述符list交给操作系统遍历
while(1) { // 把一堆文件描述符 list 传给 select 函数 // 有已就绪的文件描述符就返回,nready 表示有多少个就绪的 nready = select(list); ... }
不过,当 select 函数返回后,用户依然需要遍历刚刚提交给操作系统的 list。
只不过,操作系统会将准备就绪的文件描述符做上标识,用户层将不会再有无意义的系统调用开销。
while(1) { nready = select(list); // 用户层依然要遍历,只不过少了很多无效的系统调用 for(fd <-- fdlist) { if(fd != -1) { // 只读已就绪的文件描述符 read(fd, buf); // 总共只有 nready 个已就绪描述符,不用过多遍历 if(--nready == 0) break; } } }
select函数就是将文件描述符从用户态拷贝到内核态,交由内核来判断哪个FD有数据
缺点:
1、1024bitmp (有1024的限制,bitmap默认空间大小)
2、rset 每次循环都必须从头开始,不可以重复使用
3、用户-》内核态 开销 (rset从用户态到内核态,内核态判断是否有数据,但是还是存在着拷贝的开销)
4、ON遍历 fd(i) (可以优化为只返回就绪的队列)
poll
用的pollfd{
int f d
short events;
short revents;
}
执行流程:
1、将文件描述符拷贝到内核态
2、poll为阻塞方法,执行poll方法,如果有数据将fd的revents置为pollin
3、方法返回后循环遍历查找那个fd的revent被置为pollin
4、将revents重新复位便于复用
5、对被置的fd进行处理
解决了问题:
1、解决了bitmap大小限制问题
2、解决了rset复用问题(代码中处理后revent后,会置为0,恢复默认,就达到了重用)
epoll
epoll函数是非阻塞的
epoll执行流程:
1、当有数据的时候,会把相应的描述符置位,但是epoll没有revent标志位,所以并不是真正的置位。这时候会把有事件的描述符放到队首。
2、epoll会返回有事件的描述符个数
3、根据个数读取前N个描述符
4、读取数据进行处理
基本解决了select问题
select
fd_set 使用数组实现
1.fd_size 有限制 1024 bitmap
fd【i】 = accept()
2.fdset不可重用,新的fd进来,重新创建
3.用户态和内核态拷贝产生开销
4.O(n)时间复杂度的轮询
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0
具有超时时间
poll
基于结构体存储fd
struct pollfd{
int fd;
short events;
short revents; //可重用
}
解决了select的1,2两点缺点
epoll
解决select的1,2,3,4
不需要轮询,时间复杂度为O(1)
epoll_create 创建一个白板 存放fd_events
epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符
两种触发模式:
LT:水平触发
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。
下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,
因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
Select、poll、epoll:
I/O多路复用是一种使得程序能够同时监听多个文件描述的技术,从而提高程序性能。I/O多路复用能够在单个线程中,通过监视多个I/O流,一旦检测到某个文件描述符上
我们关心的事情发生(就绪),能够通知程序进行相应的处理(读写操作)。
Linux下实现I/O复用的系统调用主要是Select的主旨思想:
1、SELECT:首先要构造一个关于文件描述符的列表,将要监听的文件描述符列表,这个文件描述符的列表数据类型为:fd_set,是一个整形数组,总共1024个比特位。
每一个比特代表是一个文件描述符的状态。比如当需要对Select检测时,这一位0表示不检测对应的文件描述符的事件。为1表示检测对应文件描述符的事件。调用select()系统调用
监听该列表中文件描述符的时间,这个时间是阻塞的,直到这些描述符中一个或者多个I/O操作时,函数才返回。
函数对文件描述符的操作检测是在操作系统的内核完成的,select返回时,会告诉进程有多少描述符要进行I/O操作,接下来遍历文件描述符的列表进行I/O操作。
2、Poll的话其实与Select其实相差不大。只是没有了文件描述符集合的限制。
3、Epoll的话
缺点:
1、每次调用select,都需要把fd文件描述符集合从用户态拷贝到内核态,这个开销很大
2、每次调用select都需要在内核遍历传进来的fd文件描述符列表。
3、select支持的文件描述符数量太小了,1024
4、文件描述符集合不能重用,因为每次内核检测到事件都会修改,所以每次都需要重置。
5、返回后还是需要再遍历整个列表,判断是那些文件描述符变化了。
2、Poll:
优点
poll支持的文件描述符没有限制,是用的链表保存描述符,poll能够操作的fd数量就是linux系统给每个进程默认配置的fd数量的上限。
缺点:
还是需要O(N)的时间复杂度进行遍历,并且需要从用户态->内核态。
3、Epoll
Epoll的设计和实现与Select完全不同,Epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般实现方式是B+树、红黑树等)
一个分为了3个部分:
1、调用epoll_create()创建一个epoll对象(在epoll文件系统中为这个句柄对象分配资源,底层的数据结构是红黑树和双向链表)
2、调用epoll_ctrl向对象epoll添加这100万个连接的套接字(例子)(epoll_ctrl一次性向epoll对象中添加感兴趣的相应事件socket,都添加到底层的红黑树上,效率是非常高的,增删改查也很快,可以达到O(logN))
3、调用epoll_wait收集发生的事件的fd资源(当这个事件发生后,底层的epoll会从红黑树上摘出发生的事件节点,并把它们放到双向链表中,这个双向链表就直接存储了发生事件的记录,在我们应用程序中就不用遍历整个100万个套接字了)epoll_wait返回的集合就是发生事件的集合。
(
- 每次传给内核一个实例句柄。这个句柄是在内核分配的红黑树 rbr+双向链表 rdllist。只要句柄不变,内核就能复用上次计算的结果。
- 每次 socket 状态变化,内核就可以快速从 rbr ,监视进程是否关心这个 socket。同时修改 rdllist,所以 rdllist 实际上是“就绪的 socket”的一个缓存。
- 内核复制 rdllist 的一部分或者全部(LT 和 ET),到专门的 epoll_event 作为出参。
所以监视进程,可以直接一个个处理数据,无需再遍历确认。
)
epoll_create()在内核上创建的eventpoll结构如下:
structceventpoll
{
//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控事件
struct rb_root rbr;
//双向链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
}
1.执行epoll_create时,创建了红黑树和就绪list链表。
2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
LT:每次都要通知,ET只通知一次。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。