什么是IO
I/O是Input和OutPut的首字母,在Unix系统中一切皆文件,而文件就是一串二进制流,I/O操作就是对来自网络、磁盘等流的首发操作。
对于一个完整的I/O操作,通常包含两个阶段:用户空间 —— 内核空间、内核空间 —— 设备空间,对于读操作,以Socket的读为例,就包含网络设备到内核空间以及内核空间到用户空间这两个步骤。
五种IO模型
阻塞IO
应用进程一直被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区
非阻塞IO
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。轮询需要消耗CPU,因此CPU的利用率会比较低
IO多路复用
进程阻塞在select上,只要等待中的任何一个套接字变为可读,就将可读的数据拷贝到用户进程,相对于阻塞IO中在一个套接字上进行阻塞,IO多路复用可以大大提高系统的吞吐量,可以处理更多的连接
信号驱动IO
应用进程发起系统调用后,内核立即返回,当数据准备好后,会给用户一个进程一个信号,此时应用进程再发起系统调用来拷贝数据,这种方式相对于轮询的非阻塞IO,更加CPU友好
异步IO
应用进程发起调用之后,内核立马返回,当内核数据准备好并将数据拷贝到应用进程时再给应用进程信号告知已经完成
同步异步、阻塞非阻塞
看完上面几种IO模型,那么怎么区别阻塞和非阻塞、同步和异步呢?
阻塞和非阻塞:主要关注第一阶段,在发起系统调用之后,在内核数据未准备好这一阶段,阻塞调用会一直处于等待状态(不能干其他的事情,只能等待在这里),非阻塞的方式在调用后悔立马得到一个结果,需要通过不断轮询来查询数据的状态
同步和非同步:主要关注第二阶段,在数据准备好了之后,应用进程还是需要自己去拷贝数据,而异步IO则自己就把数据拷贝完了,把最终状态告知用户进程
五种IO的对比
- 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
- 异步 I/O:第二阶段应用进程不会阻塞。
同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。
非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。
IO多路复用的几种实现
IO多路复用主要有select、poll和epoll几种实现方式,他们依次出现的顺序是select、poll、epoll
select
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select将等待文件描述符分为三类:读、写和异常,每类使用数组进行存储。可以处理的文件描述符有一定限制
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
poll和select整体是类似的,poll在参数中没有写死支持的文件描述符类型,因而可以支持更多的类型。另外select默认的最大文件描述符是1024个(操作系统中,修改需要重编译),而poll没有限制。
epoll
select和poll在发现可读或写的文件描述符时,都需要遍历所有的文件描述符,因此处理过程非常慢。
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);
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。