LINUX常见IO模型介绍
UNIX中常见IO模型有五种:阻塞 IO、非阻塞IO、多路复用IO、异步IO、信号驱动IO。其中阻塞IO、非阻塞IO、多路复用IO、信号驱动IO都属于同步IO。
同步IO和异步IO
-
同步IO:应用程序主动向内核查询是否有可用数据,如果有自己负责把数据从内核copy到用户空间。
- 异步IO:应用程序向内核发起读数据请求需要:(1)告诉内核数据存放位置(2)注册回调函数,当内核完成数据copy后调用回调通知应用程序取数据。
- 同步/异步最大区别:数据从内核空间到用户空间的copy动作由应用程序自己完成。而异步IO则是注册回调函数并告知内核用户空间缓冲区存放地址,数据copy由内核完成。
下面以read操作为例简单介绍每种IO处理流程。
阻塞IO模型
图1 阻塞IO模型时序图
这种模型特点如其名,没有可用数据时read操作会被阻塞,用户线程也会阻塞在这个read操作上。这种模型对一些高速IO设备或者对于吞吐量要求高的场景比较合适。为最大限度发挥这种模型优势,需要提高每个线程处理效率,线程会长时间处于组塞状态会降低系统资源利用率。对于需要监听大量文件句柄,且每个句柄读写操作不频繁的场景,这种IO模型。不适合。比如:web聊天程序,需要保持大量长连接,但是每个连接产生数据的速度很慢。在此场景下一个线程对应一个连接,线程busy时间较少,系统利用率低,系统资源被大量浪费(文件句柄、连接句柄、线程占用的内存等宝贵资源)。这种场景多路复用IO非常合适。
非阻塞IO模型
图2 非阻塞IO模型时序图
这种IO模型和阻塞IO不同。在无数据可读时,read操作并不会被阻塞,而是返回一个错误码。应用程序取到这个错误码后可知当前并无数据可用,从而进行其它逻辑处理(可以继续尝试读取数据,也可以做其它事情)。这种IO模型在处理web聊天程序场景的效率会比阻塞IO模型好点,至少不会被阻塞住,这样可以遍历所有套接字句柄,直到找到一个有数据可读套接字,然后为这个连接服务。但这也有一个明显的缺点:用户需要多次调用read操作然后根据返回值判断当前句柄是否有数据可读,多次无用循环浪费了cpu而且无用 read调用也使程序运行效率下降。
多路复用IO模型
阻塞IO缺点:线程在无数据可用时候会被阻塞,每个线程只监听自己关心的文件句柄。
非阻塞IO缺点:read操作可能读取不到数据。多路复用IO模型可以很好解决上述缺点。
多路复用IO解决上述问题的思路:(1)用一个线程(Selector)监听所有已注册的文件句柄,(2)只有当监听的文件句柄有可用数据时才通知应用程序去读取数据。
多路复用IO常用有三种实现方式:select、pool、epoll。下面分别介绍这三种IO模型实现原理。Select模型
图3 Select模型时序图
图4 select模型流程图
每种事件类型对应一个fd_set,比如:OP_READ、OP_WRITE、OP_EX各有一个fd_set与之对应。应用程序使用多个fd_set位图保存不同事件类型的文件句柄。
select模型有两个比较明显的缺点:-
fd_set采用bitmap实现(bitmap大小是1024),最大文件句柄数为1024,也就是说每种事件类型最多可以监听1024个文件句柄。
-
两次fd_set拷贝:调用select时把fd_set从用户空间copy到内核空间,select返回时把fd_set从内核空间copy到用户空间。
-
Select返回后,用户需要遍历所有fd_set找出可用句柄
Poll模型
Poll是select增强版,原理和select基本一样。它使用链表结构代替了位图结构,解决了最多只能监听1024个文件句柄的问题。但是第2和第3个缺点还是没有解决。下面重点介绍Epoll,它解决了select所有问题。
EPoll模型
图5 Epoll模型流程图
Epoll模型在初始时做了2件事
- 把fd_list拷贝到内核空间,只copy一次
- 在需要监听IO设备上注册select线程,并添加回调函数。这个回调函数的作用就是把可用fd放到就绪队列中。每当设备可用时,设备驱动调用回调函数并且唤醒处在休眠中的select线程。
信号驱动IO模型
图6 信号驱动IO序列图
进程先向内核注册一个信号处理函数,然后返回做其它事情。当有数据可用时,进程会收到信号并且调用信号处理函数完成数据拷贝和处理。数据从内核拷贝到用户空间是由应用进程完成的,所以信号驱动IO也属于同步IO。
异步IO模型
图7 异步信号IO模型
异步io调用需要完成两件事:(1)注册信号处理函数(2)告诉内核IO缓冲区的地址。
异步IO模型和前面几种IO模型最大的区别是:当有数据可读时,内核负责把数据copy到用户空间缓冲区中。应用进程收到信号后调用信号处理函数处理,信号处理函数直接从缓冲区取数据,而不必从内核拷贝。