套接字的IO操作,如recvfrom,分为两个阶段:
(1)等待内核中的接收缓冲区中有数据可读。
(2)将接收缓冲区中的数据复制进应用缓冲区。
1,阻塞式IO
文件描述符open时,如果没有指定flags为O_NONBLOCK,或者open后,没有使用fcntl设置O_NONBLOCK,默认文件描述符为阻塞模式。
阻塞式IO在等待接收缓冲区数据到来时,会阻塞;
数据到来,进行数据复制时,也会阻塞。
2,非阻塞式IO
如上所述,可以通过open或者fcntl设置文件描述符为非阻塞模式。
非阻塞式IO,当内核缓冲区没有数据时,不会阻塞,会立即返回一个错误——EAGAIN或者EWOULDBLOCK。EAGAIN表示需要再次调用recvfrom,以判断数据是否准备好,这也是非阻塞式IO的用法,不断的调用recvfrom,以判断是否可以读。EWOULDBLOCK是虚拟语气,表示“本应该阻塞”,其实没有阻塞。由于并不确定返回EAGAIN还是EWOULDBLOCK,因此需要对这两个值都进行判断。
从接收缓冲区向应用缓冲区复制数据阶段,调用进程阻塞。
3,IO复用
在阻塞式IO中,如果接收缓冲区没有数据,调用进程阻塞于recvfrom操作。使用select或者poll,可以在此情况下使进程阻塞于select或者poll操作(因此要把文件描述符设置为非阻塞式),而且可以同时检测多个文件描述符是否可读,即检测这些描述符对应的内核中的接收缓冲区是否有数据。
从接收缓冲区向应用缓冲区复制数据阶段,调用进程阻塞。
4,信号驱动式IO
信号驱动式IO与上述三个IO模型相比,即不像阻塞式IO那样阻塞于recvfrom操作,也不像非阻塞式IO那样需要多次调用甚至轮询recvfrom才能得知是否有数据,也不像IO复用那样阻塞于select或者poll,而是当内核接收缓冲区有数据时向调用进程发送一个信号。
从接收缓冲区向应用缓冲区复制数据阶段,调用进程阻塞。
5,异步IO
上述4种IO模型,其不同点在于当接收缓冲区没有数据时,如何判断数据已经到来:阻塞式IO中recvfrom会阻塞直到接收缓冲区有数据;非阻塞式IO通过轮询recvfrom以判断接收缓冲区是否有数据;IO复用中使用select或者poll以判断接收缓冲区是否有数据;信号驱动IO通过信号通知接收缓冲区是否有数据。
其相同点在于,IO操作的第二个阶段,即从内核接收缓冲区向应用缓冲区复制数据时,调用recvfrom的进程会阻塞。
可见,上述4种IO模型都会使进程阻塞,直到IO操作的两个阶段都完成才能执行其他操作,因此称为同步IO。
异步IO模型中,IO操作的两个阶段都不阻塞,因此称为异步IO。
六、几种I/O模型的比较
前四种模型的区别是第一阶段,第二阶段基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步I/O的两个阶段都不同于前四个模型。
同步I/O和异步I/O
a.同步I/O操作引起请求进程阻塞,直到I/O操作完成。异步I/O操作不引起请求进程阻塞。
b.我们的前四个模型都是同步I/O,只有最后一个异步I/O模型是异步I/O。
- 举例
有一个钓鱼的例子经常被拿来比喻这5种IO模型,这里也不免俗套的讲一下:
阻塞式IO:老蒋钓鱼时把鱼竿伸进水中,需要一直守着鱼竿(第一阶段的阻塞),直到有鱼上钩了才拉杆(拉杆是第二阶段的阻塞);
非阻塞IO:老宋鱼竿有个功能,可以显示是否有鱼上钩(返回EAGAIN或者EWOULDBLOCK),因此他可以边和旁边的MM聊天,边看看是否有鱼上钩(轮询调用recvfrom);
IO复用:老孔的鱼竿和老蒋的一样,但他想了一个好办法,同时放了多个鱼竿,然后一直守着,一旦某个鱼竿上有鱼,马上将其拉起;
信号驱动IO:老陈很有钱,但又喜欢拉杆的快感,于是他雇了个人帮他守着,一旦那人发现有鱼上钩,马上通知老陈收杆;
异步IO:老刘也有钱,但很懒,直接雇了个人帮他钓鱼,一旦钓上了鱼就通知老刘。