I/O多路复用之——背景知识
Posted 清水寺扫地僧
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路复用之——背景知识相关的知识,希望对你有一定的参考价值。
用户空间和内核空间
- 操作系统根据寻址空间,划分为内核空间与用户空间。 对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。Linux 操作系统将这4个G的寻址空间中高位1G字节为内核使用,就是内核空间;低位3G字节位用户使用 ,也就用户空间。
- 为啥要设置这两个空间?在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。当进程运行在 Ring3 级别(使用安全指令)时被称为运行在用户态,此时进程运行在用户空间。而运行在 Ring0 级别(使用危险指令)时被称为运行在内核态,此时程运行在内核空间中。
- 在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/OPermission Bitmap)中规定的可访问端口进行直接访问。
- 区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。读写磁盘文件,分配回收内存,从网络接口读写数据等等都是在内核空间中完成的,应用程序是无法直接进行这样的操作的,但是我们可以通过内核提供的接口来完成这样的任务。对于从网络接口读写数据,我们就会用到socket。
socket
- socket有三层含义:第一层是指一种通讯机制,第二层是指套接口,第三层是指套接字;
- socket是一套用于Unix进程间通信的机制。 光有机制不行,还是要有代码实现这套机制才能做进一步开发,于是有人写代码实现了这套机制,这些代码就是这套通讯机制在操作系统(Unix、Mac O、Windows等)上的应用程序接口(套接口,socket API),这就是上面所说的多种内核提供的接口中的一种,后面的程序员每次都调用这套API(套接口,套接字相关的接口,socket API)就能实现操作系统中进程之间的通信了(同一台机器上不同进程或者不同计算机上的进程间跨网络的通信都是通过套接口实现的)。最早实现这套API的是伯克利 socket,后来Windows也实现了该接口;
- 第三层含义套接字(也是传播最广泛的含义),见下文:
文件描述符fd和套接字
- 文件描述符是Unix系统标识文件的 int,Unix的哲学一切皆文件,所以各自资源(包括常规意义的文件、目录、管道、POSIX IPC)都可以看成文件。文件描述符是内核提供给用户来安全地操作文件的标识,不像指针,拥有了指针后你能随意修改文件。文件描述符在形式上是一个非负整数。实际上,fd 是一个索引值,该索引指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。拥有了描述符后,你只能传入描述符给特定的接口(系统函数),当你将文件描述符 fd 传入套接口(socket API)时,这时候你就得到了传说中的套接字(pid…),进一步,进程之间的通讯以及数据的读写操作由内核读取用户输入的参数后来安全地执行。
也就是说,套接字就是使用套接口处理的文件描述符,因为是文件描述符,所以叫做套接"字"。而IP+端口就是网络socket(套接字)的地址,IP报文有8个字节分别代表源和目的地的IP地址,UDP头和TCP头有4个字节分别代表源和目的地的端口,就像以太头有12个字节分别代表源和目的地的MAC地址。
I/O操作是什么
- Linux系统继承了Unix系统的概念,一切皆文件。文件是指Linux系统中的VFS文件系统下的文件,VFS文件系统提供通用的接口对接各种类型的二进制流。因此,不管socket、还是FIFO、管道、终端等,对于LInux来说、一切都是文件、一切都是流。
- 在信息交换的过程中,对这些流进行数据的收发操作,就简称为I/O操作(input and output)。I/O分为磁盘I/O和网络I/O。访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,也即是应用程序需要从磁盘中读取数据,就是磁盘I/O。而应用程序从网络中获取、分发数据说的就是网络I/O。
基于socket的I/O是什么类型
- socket 是一种操作系统提供的进程间通信机制,以及该机制在操作系统上的应用程序接口(socket API)。在TCP/IP协议中“IP地址 + TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP +端口号”就称为套接字地址。在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么两个socket组成的socket pair就唯一标识一个连接。应用程序可以通过套接字接口(socket API),来使用网络套接字,以进行数据交换。因此基于socket的I/O就是网络I/O。
★网络I/O阻塞出现的原因
网络I/O阻塞产生的原因要从数据到达计算机设备开始理解:
- 数据从网线或者无线路由器以无线信号的方式到达计算机网卡,然后通过硬件电路的传输,最终会把数据写入到内存中的某个地址上;
- 当网卡把数据写入到内存后,网卡向 CPU 发出一个中断信号,操作系统便能得知有新的网络数据到来,再通过网卡中断程序去处理数据;
- 当网卡发出的中断信号中断了CPU当前的进程,开启了网卡中断程序之后,就有可能产生阻塞。阻塞是指进程一直处于等待某事件(如接收到网络数据)发生之前的状态。 不同情况下,计算机系统产生阻塞的原因不一样,和具体场景和任务流程有关。一个基础的网络程序如下:
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//处理客户端请求
//接收客户端数据
receive(c, ...);
response(...);
//关闭连接
close(...);
- 先新建 socket 对象,依次调用 bind 、listen 与 accept ,最后调用 receive 接收数据。其中 recv 是个阻塞方法,当程序运行到 receive 时,它会一直等待来自内存的网络数据到达引发的中断信号,直到该中断信号到达,说明接收到数据才往下执行。这样一来就产生了一次I/O阻塞。
- 阻塞的本质是什么 ?操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状
态。运行状态是进程获得 CPU 使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到 receive 时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。下图的计算机中运行着 A、B 与 C 三个进程(工作队列中有 A、B 和 C 三个进程),其中进程 A 执行着上述基础网络程序,一开始,这 3 个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
- 当进程 A 执行到创建 socket 的语句时,操作系统会创建一个由文件系统管理的 socket 对象(套接字对象,一个使用套接口处理的文件描述符,可以认为是一种特殊的数据结构 )(如下图)。这个 socket对象(特殊的数据结构)包含了发送缓冲区、接收缓冲区与等待队列等成员三个组成部分。等待队列是个非常重要的结构,它指向所有需要等待该 socket 事件的进程。
- 当程序执行到 receive 时,操作系统会将进程 A 从工作队列移动到该 socket 的等待队列中(这个操作也就是把socket从运行空间拷贝到内核空间)(如下图)。由于工作队列只剩下了进程 B 和 C,依据进程调度,CPU 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源,但是会占用网络I/O,一直判断是否有进程A需要的网络数据达到。
- 操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,才直接将进程挂到等待队列之下。当 socket 接收到数据后,操作系统将该 socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。同时由于 socket 的接收缓冲区已经有了数据, receive 可以返回接收到的数据。
如下图所示,进程在 receive 阻塞期间,计算机收到了对端传送的数据(步骤①),数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应 socket 的接收缓冲区里面(步骤④),再唤醒进程 A(步骤⑤),重新将进程 A 放入工作队列中。
- 唤醒过程如下:
- 以上就是 处于阻塞状态的进程在得知自己网络数据到来之后被唤起的过程(这种进程因为阻塞被挂起/放入等待队列,之后又被重新唤醒恢复执行的过程被称为进程切换,这里我们可能会思考一个问题,操作系统如何知道网络数据对应于哪个socket?是这样的,因为一个 socket 对应着一个端口号,而网络数据包中包含了 ip 和端口的信息,内核可以通过端口号找到对应的 socket。当然,为了提高处理速度,操作系统会维护端口号到 socket 的索引结构,以快速读取。
怎样提高单个网络I/O利用率
也即是将经典的I/O模型的独占,改为I/O多路复用中的轮询(select, poll)和通知(由内核完成,查询FD_ISSET()
,修改poll.revents
字段,epoll_event.events
字段)的思想来提高网络I/O的利用率。
上图中的红色字,单个I/O只可收发一个I/O读写,说法改为单个socket套接字只能监听处理单个网络I/O请求的问题更好,一个网络I/O可以执行多个I/O操作,改为一个socket套接字可以监听处理多个网络I/O请求,并执行相关操作。
关于线程,进程,进程池,线程池的概念见:进程、线程、协程的区别和联系。
以上是关于I/O多路复用之——背景知识的主要内容,如果未能解决你的问题,请参考以下文章