综述
进程的正常工作离不开与IO打交道,当我们需要读写文件时,需要通过IO与磁盘打交道;当我们需要与远程进程打交道时,需要通过IO与socket打交道,可以将socket看作是一个文件。在Linux中,一切皆文件,所以IO的作用非常重要。一般而言,如果一个进程需要读某个文件时,首先会系统调用询问某个文件描述符是否读就绪,如果文件描述符读就绪了,内核首先会将要读的数据从外部设备拷贝到内核空间,然后从内核空间拷贝到用户空间,完成文件的数据读到进程的过程。写文件的过程是类似的,首先系统调用询问某个文件描述符时候写就绪,如果文件描述符写就绪了,内核首先会将要写的数据从用户空间拷贝到内核空间,然后从内核空间拷贝到外部设备,完成文件的数据写到外部设备的过程。
阻塞IO
阻塞IO是处理IO的默认方式。当进程需要处理文件时,会系统调用进入内核态,内核态会检查文件描述符是否就绪,如果未就绪,进程会阻塞,直到文件描述符就绪了。
非阻塞IO
当进程需要处理文件时,会系统调用进入内核态,内核态会检查文件描述符是否就绪,如果未就绪,进程不会阻塞。在这种情况下,进程需要不断询问文件是否就绪,如果就绪了才会开始文件处理。
多路复用IO
多路复用IO是指使用一个进程或一个线程处理多个IO,常用的有select,poll和epoll。
select
进程会将多个文件描述符组成一个数组,然后系统调用检查这些文件描述符是否有就绪的,如果没有就绪,进程会进入阻塞状态,直到有文件描述符就绪了或等待到规定时间。在调用select时,进程会把文件描述符数组拷贝到内核空间,由内核负责监控这些文件描述符是否存在就绪。select返回时,内核将文件描述符的状态拷贝到进程空间,进程可以知道文件描述符数组中有多少个就绪,但并不知道具体是哪个就绪,所以还需要遍历一遍数组寻找就绪的文件描述符。select有文件描述符监控上限,一般为1024。
poll
poll的原理和select几乎一致,不同的是poll将文件描述符组成一个链表,所以poll没有文件描述符监控的数量上限。
epoll
epoll也需要将文件描述符从用户空间拷贝到内核空间,但不同的是,select和poll每次调用都会重新将用户空间的文件描述符打包拷贝到内核空间,而epoll中每个文件描述符只会拷贝到内核空间一次。epoll在内核使用红黑树组织文件描述符,每当新增或减少或更新某个文件描述符的监控时会在红黑树上执行插入,删除,查找更新操作。当某个文件描述符就绪了,会触发回调函数,epoll会将文件描述符添加到就绪表。当进程调用epoll_wait函数时,epoll会直接返回就绪表的内容,所以进程可以直接操作epoll_wait的返回结果而无需再遍历一遍所有监控的文件描述符。
epoll有两种触发模式,一种是水平触发,另一种是边缘触发。select和poll都是水平触发。水平触发是指当文件描述符就绪了,如果这次不操作或没有完全操作,下次依然会返回就绪,就像高电平触发模型。边缘触发是指当文件描述符就绪了,无论这次是否操作或是否完全操作,都会重新将文件描述符置为未就绪,下次是否就绪取决于下次文件描述符是否从未就绪状态变为就绪状态,就像电平边缘触发模式。
信号IO
在非阻塞IO中,进程需要不断询问文件描述符是否就绪,当文件描述符已经就绪时,内核不会主动告诉进程文件描述符已经就绪。信号IO是指当文件描述符就绪了,内核会发送一个信号通知进程,进程无需再反复询问内核了。得到就绪信号后,进程通过系统调用完成IO操作。
异步IO
异步IO是指进程系统调用后,后续所有操作都不需要管了,内核会帮进程检查文件描述符是否就绪,就绪后也会直接将内容拷贝到进程的用户空间。进程除了发起IO请求,没有再参与完成IO操作的整个过程,内核完成IO操作后会通知进程,整个IO处理过程已完成。