Linux中有5种IO模型:
如果条件不满足,则会使调用进程或线程阻塞,让出CPU控制权,并一直持续到条件满足为止,在Linux中,阻塞式IO一般作为默认属性出现,如mq_receive、sem_wait、sem_post等
在默认情况下,所有的套接字都是阻塞的,我们以UDP套接字为例来展示阻塞式IO模型。
进程调用recvfrom接收数据,但由于内核还未准备好,进程就会阻塞;直到内核准备好数据,recvfrom完成数据复制工作,进程才能解除阻塞状态。
非阻塞型IO:顾名思义,非阻塞式IO不会使调用进程或线程永远阻塞,具体表现为:如果IO操作不能完成,则立即出错返回,调用进程或线程继续向下执行。对于一个给定的描述符,有两种将其指定为非阻塞式IO的方法:调用open创建或打开文件时指定O_NONBLOCK标志对于一个已经打开的描述符,调用fcntl改变其属性,为其设置O_NONBLOCK标志。
#include int fcntl(int fd, int cmd, ... /* int arg */);
IO多路复用(I/O multiplexing):多个进程使用同一个IO流,一旦发现进程指定的一个或者多个描述符可进行无阻塞IO访问时,它就通知该进程。
多个Sock复用一个IO线程这个功能是在内核+驱动层实现的。
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。
类似:ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
select, poll, epoll 都是I/O多路复用的具体的实现,他们出现是有先后顺序的。
select在1983年BSD UNIX里面实现的
select流程伪代码如下:
{ select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } }
具体的API :
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
select 被实现以后,很快就暴露出了很多问题。
· select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
· select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍。
· select 只能监视1024个链接,linux 定义在头文件中的,参见FD_SETSIZE。
· select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如
· poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
· poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
· epoll 现在是线程安全的。
· epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
在这种模型下,如果请求的I/O操作阻塞,且它不是真正阻塞I/O,而是让其中的一个函数等待,在此期间,I/O还能进行其他操作。
信号驱动I/O模型:在这种模型下,进程要定义一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O。这是由内核通知用户何时可以启动一个I/O操作决定的。
异步I/O模型:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知进程I/O操作何时完成的。现在,并不是所有的系统都支持这种模型。