Linux网络编程-5种网络模型

网络 IO,会涉及到两个系统对象, 一个是用户空间调用 IO 的进程或者线程,另一个是内核空间的内核系统

比如发生 IO 操作 read 时,它会经历两个阶段:
1. 等待数据准备就绪,
2. 将数据从内核拷贝到进程或者线程中。

ps:当有数据时,是系统先从网卡将数据读入内核,再由用户态读取内核中的数据,这里发生了2次拷贝,想要优化可以采用zero copy,顾名思义就是用户态直接向网卡读取数据


因为在以上两个阶段上各有不同的情况,所以出现了多种网络 IO 模型

  • 阻塞IO (blocking IO)
  • 非阻塞IO (non-blocking IO)
  • 多路复用IO (IO multiplexing)
  • 异步IO (Asynchronous IO)
  • 信号驱动IO (signal driven IO,SIGIO)

阻塞IO (blocking IO)

在 linux 中,默认情况下所有的 socket 都是 blocking,一个典型的读操作流程

当用户进程调用了 read 这个系统调用, kernel 就开始了 IO 的第一个阶段:准备数据。对于
network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的数据包),
这个时候 kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当 kernel
一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,
用户进程才解除 block 的状态,重新运行起来。
所以, blocking IO 的特点就是在 IO 执行的两个阶段(等待数据和拷贝数据两个阶段)都被
block 了。几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的,这些接口都是阻塞型的。大部分的 socket 接口都是阻塞型的。

所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。实际上,除非特别指定,几乎所有的 IO 接口 ( 包括 socket 接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这就带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求,一个简单的改进方案是在服务器端使用多线程(或多进程)

很多程序员可能会考虑使用“线程池”或“连接池”,这也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且, 所谓“池”始终有其上限,当请求大大超过上限时, “池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小
 

 非阻塞IO (non-blocking IO)

Linux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读
操作时,流程是这个样子:

从图中可以看出,当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回, 所以,在非阻塞式 IO 中,用户进程其实是需要不断的主动询问 kernel数据准备好了没有。在非阻塞状态下, recvfrom() 接口在被调用后立即返回,返回值代表了不同的含义。

如在本例中,
* recvfrom() 返回值大于 0,表示接受数据完毕,返回值即是接受到的字节数;
* recvfrom() 返回 0,表示连接已经正常断开;
* recvfrom() 返回 -1,且 errno 等于 EAGAIN,表示 recv 操作还没执行完成;
* recvfrom() 返回 -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系统错误 errno。

非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。使用如下的函数可以将某句柄 fd 设为非阻塞状态。

fcntl( fd, F_SETFL, O_NONBLOCK );

服务器线程可以通过循环调用 recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐。因为,循环调用 recv()将大幅度推高 CPU占用率;此外,在这个方案中 recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如 select()多路复用模式,可以一次检测多个连接是否活跃。
 

多路复用IO (IO multiplexing)

IO multiplexing 这个词可能有点陌生,但是提到 select/epoll,大概就都能明白了。有些地方也称这种 IO 方式为事件驱动 IO(event driven IO)。我们都知道, select/epoll 的好处就在于单个process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select/epoll 这个 function会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。

当用户进程调用了 select,那么整个进程会被 block,而同时, kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了, select 就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。这个图和 blocking IO 的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select 和 read),而 blocking IO 只调用了一个系统调用(read)。但是使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到
在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方
式才能达到这个目的。多说一句:所以,如果处理的连接数不是很高的话,使用select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 webserver 性能更好,可能延迟还大。 select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

 在多路复用模型中,对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的 process 其实是一直被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。因此 select()与非阻塞 IO 类似。大部分 Unix/Linux 都支持 select 函数,该函数用于探测多个文件句柄的状态变化。下面给出 select 接口的原型:

FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set*exceptfds,struct timeval *timeout)

这里, fd_set 类型可以简单的理解为按 bit 位标记句柄的队列,例如要在某 fd_set
中标记一个值为 16 的句柄,则该 fd_set 的第 16 个 bit 位被标记为 1。具体的置位、验证
可使用 FD_SET、 FD_ISSET 等宏实现。在 select()函数中, readfds、 writefds 和
exceptfds 同时作为输入参数和输出参数。如果输入的 readfds 标记了 16 号句柄,则
select()将检测 16 号句柄是否可读。在 select()返回后,可以通过检查 readfds 有否标
记 16 号句柄,来判断该“可读”事件是否发生。另外,用户可以设置 timeout 时间。

这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发
某个特定的响应。我们可以将这种模型归类为“事件驱动模型”。相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

但这个模型依旧有着很多问题。首先 select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时, select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll, BSD提供了kqueue, Solaris提供了/dev/poll, …。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。

其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。如下例,庞大的执行体 1 的将直接导致响应事件 2 的执行体迟迟得不到执行,并在很大程度上降低了事件探测的及时性。幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有libevent 库,还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择
 

异步IO (Asynchronous IO)

Linux 下的 asynchronous IO 用在磁盘 IO 读写操作,不用于网络 IO,从内核 2.6 版本才开始引
入。

用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后, kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后, kernel 会给用户进程发送一个 signal(或者是设置回调函数),告诉它 read 操作完成了。用异步 IO 实现的服务器这里就不举例了。异步 IO 是真正非阻塞的,它不会对请求进程产生任何的阻塞,因此对高并发的网络服务器实现至关重要
 

信号驱动IO (signal driven IO,SIGIO)

首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用 read 读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它来读取数据报。无论如何处理 SIGIO 信号,这种模型的优势在于等待数据报到达(第一阶段)期间,进程可以继续执行,不被阻塞。免去了 select 的阻塞与轮询,当有活跃套接字时,由注册的 handler 处理。

经过上面的介绍,会发现 non-blocking IO 和 asynchronous IO 的区别还是很明显的。在non-blocking IO 中,虽然进程大部分时间都不会被 block,但是它仍然要求进程去主动的 check
并且当数据准备完成以后,也需要进程主动的再次调用 recvfrom 来将数据拷贝到用户内存。而 asynchronous IO 则完全不同。它就像是用户进程将整个 IO 操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查 IO 操作的状态,也不需要主动的去拷贝数据。


主要用法:

struct sigaction sigio_action;
sigio_action.sa_flags = 0;
sigio_action.sa_handler = do_sigio;
sigaction(SIGIO, &sigio_action, NULL);

相关问题

  1. blocking 和 non-blocking 的区别在哪?
  2. synchronous IO 和 asynchronous IO 的区别在哪?

blocking 与 non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用 blocking IO 会一直 block 住对应的进程直到操作完成(系统调用,如read/recv),而non-blocking IO 在 kernel 还在准备数据的情况下会立刻返回。

两者的区别就在于 synchronous IO 做”IO operation”的时候会将 process 阻塞。按照这个定义,之前所述的 blocking IO, non-blocking IO, IO multiplexing 都属于synchronous IO。有人可能会说, non-blocking IO 并没有被 block 啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的 IO 操作,就是例子中的 read 这个系统调用。 non-blocking IO 在执行 read 这个系统调用的时候,如果 kernel 的数据没有准备好,这时候不会 block 进程。但是当 kernel 中数据准备好的时候, read 会将数据从 kernel 拷贝到用户内存中,这个时候进程是被 block 了,在这段时间内进程是被 block的。 而 asynchronous IO 则不一样,当进程发起 IO 操作之后,就直接返回再也不理睬了,直到 kernel 发送一个信号,告诉进程说 IO 完成。在这整个过程中,进程完全没有被 block。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值