Skip to content

Latest commit

 

History

History
183 lines (118 loc) · 7.26 KB

multiplexing-network.md

File metadata and controls

183 lines (118 loc) · 7.26 KB

网络中的i/o复用

本节主要描述select和poll函数,来源是 unix网络编程卷1 3th 第6章。

介绍

网络应用中,i/o复用常用在:

  • 处理多个描述符(unix下的文件句柄)时,必须使用i/o复用
  • 一个客户同时处理多个socket,这种情况比较少见。
  • 一个tcp服务既要监听socket,也要处理已连接的socket,一般使用i/o复用
  • 一个服务既要处理tcp,也要处理udp,一般要用i/o复用
  • 一个服务要处理多个服务/或多个协议时,需要使用i/o复用

上面仅仅是i/o复用在网络中的使用场景,其他方面的应用可查看前面的复用文档。

i/o模型

unix下有5种i/o模型:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O复用(select,poll)
  • 信号驱动式I/O (sigio)
  • 异步I/O (posix系列)

网络程序怎么将网络那边的数据获取到:

  • 等待数据准备好
  • 网络传输,通过网卡设备接收,缓存到内核的某个缓冲区
  • 把数据从内核缓冲区复制到应用进程的缓冲区

阻塞i/o模型

最流行的模型,默认情况所有socket都是阻塞的。

recvfrom(),接收udp数据的系统调用,执行过程如下:

  • 应用程序调用recvfrom(), 用户态
  • recvfrom内部调用了系统调用,等待内核准备数据 内核态
  • 内核会等待数据准备好,直到将数据复制到应用程序的缓冲中 内核态
  • recvfrom接收到成功提示,返回 用户态

整个过程中,recvfrom是阻塞的。 应用程序调用recvfrom时,系统会将应用程序挂起,等执行完之后,唤醒应用程序。

非阻塞I/O模型

如果socket被指定为非阻塞时,调用recvfrom,应用程序挂起, 内核发现socket是非阻塞的,会立马将recvfrom唤醒,此时数据还没到内核缓存区, recvfrom会返回一个特定错误,然后应用程序可以去干其他事,内核继续等待数据。 再次调用recvfrom,如果内核发现数据已经准备好,会立马给recvfrom并返回成功。

整个过程中,recvfrom会多次调用,被称为轮询 polling。

一般不知道数据什么时候会准备好,polling会浪费大量cpu时间,所以这种模型很少见, 只出现在某些特殊的系统功能上。

I/O 复用模型

使用select/poll系统调用,这时不会阻塞在i/o上,而是阻塞在这两个系统调用上。 和阻塞模型的区别:阻塞是单个调用阻塞,10个调用,会阻塞10次;而这个复用模型, 可以让10个调用同时阻塞,哪个好了就唤醒哪个,说白了就是将10个串行的阻塞(阻塞模型) 变成了10个并行的阻塞(复用模型)。

从使用上,复用模型比阻塞模型复杂一点,毕竟多了一个select调用,这是劣势, 优势是可以等待多个文件描述符。

信号驱动式的I/O模型

和前面几种模型都有些不一样,利用信号实现,在内核准备好数据后,会发送一个信号给 进程,进程调用recvfrom来读取数据,数据准备好之前,进程是非阻塞的。

这个模型和非阻塞有啥区别:在数据没有准备好之前都是非阻塞,不过后面就不一样了, 这个模型是等内核发信号告诉自己数据已经就绪,而非阻塞模型就是自己取轮询。

异步I/O模型

asynchronous I/O, 是posix规范中定义的。

这个模型也是内核告诉进程,和信号驱动式模型不一样的是: 信号驱动式发送信号,是告知可以进行i/o操作了,而这个异步模型发送信号, 是告诉进程,i/o操作何时完成了。

这个模型是在数据就绪,并从内核态缓冲区复制到进程缓冲区后通知进程。

目前位置,支持posix异步io模型的系统还较少。

5种模型的对比

如果将调用分成两阶段:数据就绪和数据从内核缓冲复制到进程缓冲。

前4种的第2阶段都是一样的,数据从内核缓冲复制到进程缓冲,期间进程阻塞, 不一样的是第1阶段是否(多)阻塞,或非阻塞的情况,如何触发取数据。

posix的定义,同步io操作做是请求会导致进程阻塞,直到io操作完成, 这样前4种io模型都属于这列,异步io就是指异步io模型。

select

archlinux中的实现是同步io复用

函数功能:允许进程指示内核等待多个事件中的任何一个发生, 并只在有一个或多个事情发生或经历一段指定的时间后才唤醒它。

#include <sys/select.h>

int select(int nfds, fd_set *restrict readfds,
   fd_set *restrict writefds, fd_set *restrict errorfds,
   struct timeval *restrict timeout);

最后一个参数是时间:

  • 为空,表示永久等待
  • 为0, 表示不等待,检查完状态立马返回
  • 非空非零值,表示超时时间

FD_ZERO/ FD_SET/ FD_CLR/ FD_ISSET 4个宏配合描述符集使用。 第一个参数是最大描述符个数,= 待测试的最大描述符 + 1

通常的使用过程:

  1. 用宏配置好描述符集
  2. 调用select
  3. select返回后,用FD_ISSET检查,并处理
  4. 重新配置描述符集,然后继续select,就是1-3的循环

socket准备好读,满足下列4个条件之一即可:

  1. socket接收缓冲区的数据字节数大于"接收缓存区低水位标记的大小,默认是1"
  2. tcp连接接收到了fin,此时不会阻塞,会返回0
  3. socket是一个监听socket,并已完成的连接数不是0,此时accept一般不会阻塞
  4. 有一个socket错误待处理,此时不会阻塞,并返回-1

socket准备好写,满足下列4个条件之一即可:

  1. 发送缓冲区的自己数大于"发送缓冲区低水位标记的大小,默认是2048"
  2. tcp连接已接受到fin,写会产生sigpipe信号
  3. socket是非阻塞,并已经connect,或connect失败了
  4. socket有待处理错误

pselect

posix 整出来的一个新东西,和select做的事是类似的。有两个变化:

  1. 时间参数的精度从毫秒改为纳秒
  2. 增加一个新参数,执行信号掩码的指针

一般情况是这样的:

  1. 检查有没有信号,有就调用信号处理函数(因为信号不是事件,而是异步消息)
  2. 调用select来捕获事件和信号

如果1和2中间发生了信号,那么select就无法捕获,而此时进程会被无限阻塞, pselect的新增参数就可以解决这个问题。

poll

#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

提供的功能和select类似。

第一个参数是数组,里面是pollfd结构体.select的fd数是有限制的,poll使用数组的方式, 解决了select最大fd是1024的限制。

epoll

和poll类似,不过epoll是事件驱动的,每次调用后,会告诉用户哪些socket的事件触发了, select和poll是将整个描述符集返回给用户,让用户去遍历,去用宏测试, 一旦描述符多了,效率差异就很大了。

其次epoll还有水平触发lt和边缘触发et,水平触发是用户如果不处理,下次调用还会通知, 边缘触发是用户不处理,那后面也不会再提示了。

另外每次调用select/poll,都需要将fd拷贝到内核态,epoll只需要拷贝一次。

说明

  • 1984年,实现了select,此时有了io复用
  • 1997年,实现了poll,当时监听多个socket的程序就很多了,有了需求就有了发展
  • 2002年,实现了epoll,提升了效率