IO多路复用的本质是通过系统内核缓冲IO数据让单个进程可以监视多个文件描述符,一旦某个进程描述符就绪(读/写就绪),就能够通知程序进行相应的读写操作。
select poll epoll都是Linux提供的IO复用方式,它们本质上都是同步IO,因为它们都需要在读写事件就绪后自己负责进行读写,读写的过程是阻塞的。
IO复用技术最大优势就是系统开销小,系统不必创建进程或线程,也不必维护这些进程线程。
基础知识
用户空间与内核空间:操作系统将虚拟空间划分为两个部分,一部分为内核空间,一部分为用户空间。内核可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
进程切换:为了控制进程执行,内核必须有能力挂起正在CPU执行的进程,然后恢复之前被挂起的进程。进程切换非常消耗资源
进程阻塞:正在执行的进程,由于等待某些事件,如请求资源、等待某些操作完成、等待读取新数据,这些系统自动执行阻塞原语,使进程变为阻塞态。所以说进程的阻塞是进程自身的主动行为。进入阻塞态是不会占用CPU资源的。
文件描述符:形式上是一个非负整数,实际上是个索引,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个文件或者创建一个新文件时,内核向进程返回一个文件描述符。
缓存IO:操作系统会将IO的数据缓存在文件系统的页缓存中,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从内核缓冲区拷贝到应用程序的地址空间。
select运行机制
提供fd_set
的数据结构,实际上为一个long类型的数组,每一个数组元素都能与一个打开的文件句柄建立联系。当调用select时,内核根据IO状态修改fd_set的内容,由此来通知执行了select的进程哪一个文件可读。
从流程上看,select函数与同步阻塞模型无太大区别,甚至多了添加监视socket,以及调用select的额外操作。
但是select以后最大的优势就是用户可以在一个线程内同时处理多个socket的Io请求。用户可以注册多个socket,然后不断调用select读取被激活的socket。达成同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
select机制的问题
1、每次调用select,都需要把fd_set集合从用户态拷贝到内核态,集合很大时开销也大
2、每次调用select都需要在内核中遍历fd_set,如果集合很大,开销也大
3、为了减少数据拷贝带来的性能损失,内核对被监控的fd_set集合大小做限制,限制为1024
Poll
poll机制本质上与select没啥区别,管理多个描述符也是进行轮询,根据描述符状态进行处理,但是poll没有最大描述符数量限制。
epoll
epoll是基于事件驱动的IO方式。
1、相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样用户空间和内核空间的copy只需要一次。
2、并且,它在获取事件的时候,无须遍历整个被侦听的描述符集合,只要遍历哪些被内核IO事件异步唤醒而加入redy队列的描述符集合就行了。
3、epoll除了提供select/poll那种IO事件的水平触发,还提供了边缘触发,使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
水平触发:当epoll_wait检测到某描述符事件就绪,并且通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件
边缘触发:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait的时候,不会再次通知此事件。
一图总结: