Linux 系统-网络I/O模型

简介: 网络 I/O操作过程中会涉及到两个系统对象,一个是用户空间I/O操作的进程或者线程,另一个是内核空间的内核系统,比如发生 I/O read操作时,它会经历两个阶段

前言

网络 I/O操作过程中会涉及到两个系统对象,一个是用户空间I/O操作的进程或者线程,另一个是内核

空间的内核系统,比如发生 I/O read操作时,它会经历两个阶段:


1.等待数据准备就绪

2.将数据从内核拷贝到进程或者线程中。

在以上两个阶段上有不同的处理方式,因此出现了多种网络 IO 模型。

说明:本文的知识基本来源于其他博客,只是重新整理了一番,相当于COPY


I/O网络模型

阻塞I/O(blocking I/O)

默认情况下,socket网络编程都是阻塞的,典型的读操作流程如下

0a71500dab60c5822fc4d62fc3383a12_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_16,color_FFFFFF,t_70,g_se,x_16.png

当用户进程调用了 read 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据。对于网络 I/O 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的数据包),这个时候 kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当 kernel一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO 的特点就是在 IO 执行的两个阶段(等待数据和拷贝数据两个阶段)都被block 了。几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的,这些接口都是阻塞型的。使用这些接口可以很方便的构建服务器/客户机的模型。下面是一个简单地“一问一答”的服务器模型。

b4bfef50a99a447fe37c029461d3fbb4_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_20,color_FFFFFF,t_70,g_se,x_16.png

大部分的socket接口都是阻塞性的,比如accept没有收到客户端的连接时,会一直被阻塞。recv()没有收到对等方数据时也会阻塞到对方发送数据为止。

为了解决服务器支持多个客户端的问题,首先想到的是采用多线程或者线程池的解决方式,即是说,开启一个线程为客户端服务,一段时间服务器去心跳检测客户端是否还处于活动中,如果处于非活动状态,则主动断开和客户端的连接。关于阻塞性模式的测试代码见:socket 编程-单线程和多线程版本.


即使是使用多线程或者线程池的模式,也很难支持大量客户端同时连接服务器的情况,主要是由于服务器能创建线程以及资源的限制。


非阻塞I/O(non-blocking I/O)

可以通过修改socket的属性来将其修改为非阻塞模式,非阻塞模式的流程如下所示

194ecd384ebb061f04f3d3853117ef23_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_20,color_FFFFFF,t_70,g_se,x_16.png

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


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


多路复用I/O

多路I/O复用也称为事件驱动I/O,linux中主要包括select/epoll/poll模型,这些模型的共同特点就是一个线程可以同时处理多个网络连接的I/O,基本原理是这些模型会不断的轮训所有的socket,当某个socket有数据到达时,就告知用户进程,可以进行相应的读取操作了。

488ae87a3b4c8d16ec10257fe2154a77_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_20,color_FFFFFF,t_70,g_se,x_16.png

I/O多路复用在调用select/poll接口的时候会被阻塞的,但是使用 select /poll以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。当然,如果处理的连接数不是很多的情况下,可能使用多线程的方式更加优越。


在多路复用模型中,对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的 process 其实是一直被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。因此 select()与非阻塞 IO 类似。


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


幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有libevent 库,还有作为 libevent 替代者的 libev 库。


例子代码:

select服务器模型

poll模型

epoll模型


异步I/O

linux下的异步I/O只能用于磁盘I/O操作,不用于网络I/O,window下有IOCP提供支持。

03c58dd560b095e89fbe5efc43ac6871_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_20,color_FFFFFF,t_70,g_se,x_16.png

用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。


信号驱动I/O

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

78782b89ac3f24da8f476a7f7f52b9a3_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWJjZDU1MjE5MTg2OA==,size_20,color_FFFFFF,t_70,g_se,x_16.png


阻塞/非阻塞,异步/同步

通过网络模型的介绍,我们在来说说阻塞/非阻塞和异步/同步的区别。


阻塞和非阻塞

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


同步非阻塞和异步

在non-blocking IO 中,虽然进程大部分时间都不会被 block,但是它仍然要求进程去主动的 检查,并且当数据准备完成以后,也需要进程主动的再次调用 recvfrom 来将数据拷贝到用户内存。


而 asynchronous IO 则完全不同。它就像是用户进程将整个 IO 操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查 IO 操作的状态,也不需要主动的去拷贝数据。也就是说网络I/O的2个过程都不需要调用者去主动巡查,用户只要发出一个系统调用,然后等待内核返回结果即可。

以上五种模型中,只有异步I/O才是真正的实现异步操作,其他的几种网络I/O模型都是同步的。


相关文章
|
3天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
18 3
|
3天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
15 2
|
3天前
|
安全 网络协议 Linux
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。通过掌握 ping 命令,读者可以轻松测试网络连通性、诊断网络问题并提升网络管理能力。
18 3
|
6天前
|
安全 Linux 数据安全/隐私保护
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。本文介绍了使用 `ls -l` 和 `stat` 命令查找文件所有者的基本方法,以及通过文件路径、通配符和结合其他命令的高级技巧。还提供了实际案例分析和注意事项,帮助读者更好地掌握这一操作。
23 6
|
11天前
|
缓存 监控 Linux
|
14天前
|
Linux Shell 数据安全/隐私保护
|
15天前
|
域名解析 网络协议 安全
|
21天前
|
运维 监控 网络协议
|
22天前
|
监控 Linux Shell
|
6天前
|
Linux
在 Linux 系统中,`find` 命令是一个强大的文件查找工具
在 Linux 系统中,`find` 命令是一个强大的文件查找工具。本文详细介绍了 `find` 命令的基本语法、常用选项和具体应用示例,帮助用户快速掌握如何根据文件名、类型、大小、修改时间等条件查找文件,并展示了如何结合逻辑运算符、正则表达式和排除特定目录等高级用法。
30 6