内核态:运行操作系统的应用程序,可以操作调度使用硬件资源,比如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问题
epoll
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,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。