Linux I/O模型

words: 5.8k    views:    time: 22min

操作系统负责对计算机资源进行管理,并对上层应用提供支持。其核心是操作系统内核空间,独立于应用程序,可以访问受保护的内存空间以及底层硬件设备。为了保证内核的安全,避免用户进程直接操作内核,因此将内存空间划分为两部分,即内核空间用户空间

一个进程通过调用内核打开相应的文件,告诉内核它想访问某个 I/O 设备,内核返回一个非负整数,称为描述符。后续对此文件的所有操作通过这个描述符进行,内核会记录有关这个打开文件的所有信息,而应用进程只需记住这个描述符。其中,Linux内核用三个相关的数据结构来表示打开的文件:

  • 描述符表:每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的,每个打开的描述符表项指向文件表中的一个表项

  • 文件表:打开文件的集合由一张文件表来表示,所有的进程共享这张表。每个文件表的表项组成包括当前文件的位置、引用计数(指向当前表项的描述符数目)、以及一个指向 v-node 表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数,内核不会删除这个文件表表项,直到它的引用计数为零

  • v-node表:与文件表一样,所有进程共享 v-node 表。每个表项包含 stat 结构中的大多数信息,包括 st_mode 和 st_size 成员

多个描述符也可以通过不同的文件表项来引用同一个文件,例如以同一个 filename 调用 open 函数两次,就会发生这种情况。另外,Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(STDIN_FILENO=0)、标准输出(STDOUT_FILENO=1)和标准错误(STDERR_FILENO=2)

1. 磁盘IO

1.1. 缓存IO

大多数文件系统的默认 I/O 操作都是缓存 I/O,所以一般也称为标准 I/O 操作。以读操作为例,应用程序需要走 read 系统调用,这样数据会先拷贝到操作系统的内核缓存,然后再从内核拷贝到应用进程空间。反之写操作,需要走 write 调用,也是类似的。

根据程序的局部性原理,最近被访问的信息很可能还要被访问(时间局部性),以及与最近被访问数据邻近的数据也可能被访问(空间局部性)。因此实际读取时,操作系统会帮进程作一部分预读,这样的好处是可以减少对 I/O 设备的实际访问次数,但由于读写操作都要先经过内核缓存,然后再复制到用户空间,因此也多了一次内存复制操作。

1.2. 页缓存 PageCache

PageCache 是操作系统对文件的缓存,用来减少对磁盘的 I/O 操作,以页为单位的,内容就是磁盘上的物理块,页缓存能让程序对文件进行顺序读写时的速度几乎接近于内存的读写速度,主要原因就是由于 OS 使用 PageCache 机制对读写访问操作进行了一下缓冲。

  • 页缓存读取策略:当进程发起一个 read 调用时,系统首先会检查需要的数据是否在页缓存中,如果在,则直接从页缓存中读取,否则内核调度块 I/O 操作从磁盘去读取数据,并预读其后的几个页面(一般是三个页面),然后将数据放入页缓存中。

  • 页缓存写策略:当进程发起 write 调用时,会先写到页缓存,然后方法返回。此时数据并没有真正保存到文件中,Linux 只是将页缓存中的这一页数据标记为 “脏”,并且被加入到脏页链表中。然后,由 flusher 回写线程周期性将脏页链表中的页写到磁盘,让磁盘中的数据和内存中保持一致,最后再清理“脏”标识。

一般导致脏页回写的情况有三种:即当空闲内存低于一个特定阈值时;或者脏页在内存中驻留超过特定时间;再就是进程发起了 sync() 或 fsync() 系统调用。

1.3. 直接IO

直接IO是相对缓存IO来说的,在Linux中打开文件,如果不指定flags为O_DIRECT,那么就是使用缓存I/O来对文件进行读写操作。

1
2
3
4
5
/**
* pathname:指定要打开的文件路径
* flags:指定打开文件的标志
*/
int open(const char *pathname, int flags);

O_DIRECT (Since Linux 2.4.10)
Try to minimize cache effects of the I/O to and from this file. In general this will degrade performance, but it is useful in special situations,
such as when applications do their own caching. File I/O is done directly to/from user space buffers. The I/O is synchronous, that is, at the
completion of a read(2) or write(2), data is guaranteed to have been transferred.

意思是,为了写文件或者读文件的 I/O 高度缓存开销的最小化。一般情况下,该标志会降低性能,但是,在特殊情况下,还是有作用的。例如当应用程序使用自己的高速缓存的时候,文件 I/O 直接接触到用户内存,I/O 操作是同步的,也就是说,一旦 read(2) 或者 write(2) 完成,数据可以保证传输完毕。

直接 I/O 省去了与内核缓存的复制过程,这样虽然使单次 I/O 访问效率高了,但由于没有了缓存,将导致访问 I/O 设备的操作更加频繁,而访问 I/O 设备的延迟要远高于内存间的复制的,因此需要应用自己去权衡。一般的在数据库服务中,会更倾向于建立自己的缓存机制,因为它们往往比操作系统更了解磁盘中存放的数据,从而可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。

1.4. 内存映射IO

操作系统一般都会采用虚拟内存(参考笔记:计算机 存储系统),比如对于32位操作系统,应用进程可以拥有3G的空间,但是对于同时存在的进程,如何让每个进程都感觉自己拥有整个的内存空间,而且互相不影响呢?其实每个进程都有自己的内存映射表,维护着虚地址与内存物理地址的映射关系,进程在实际运行时,可能只需对少部分地址进行映射,即只占用实际物理内存的一部分,这样对于CPU而言,当发生进程切换时,只需要切换进程对应的地址映射表,就能访问到对应的进程空间了

内存映射就是走的虚拟内存方式,而不再像上面一样先打开文件然后再进行读写了。其过程是先建立磁盘文件地址与进程虚拟内存的映射,然后访问文件就相当于访问内存,当然真正访问时还要通过 MMU 将逻辑地址转换成物理地址,如果 MMU 在地址映射表中无法找到对应的物理地址,将产生一个缺页中断,然后将尝试从 swap 中寻找对应的页面,如果还找不到则根据前面建立的映射关系从磁盘读取。但是之后的读写就完全是对内存的操作了,而对应的修改会触发脏页回写,从而保证数据更新到磁盘。

对应的,Linux提供了相关的系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 建立映射
* addr:指定映射的起始地址, 通常设为NULL, 由系统指定
* len: 映射到内存的文件长度
* prot:映射区的保护方式
* PROT_EXEC: 映射区可被执行
* PROT_READ: 映射区可被读取
* PROT_WRITE: 映射区可被写入
* flags:映射区的特性
* MAP_SHARED:写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享
* MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件
* fd: 由open返回的文件描述符,代表要映射的文件
* offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为0,表示从文件头开始映射
*/
void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset );

/**
* 解除映射,成功返回0,否则返回-1
* start:指向的映射内存
* length:表示欲取消的内存大小
*/
int munmap(void *start,size_t length);

2. 网络I/O

对于网络 I/O,由于网络上数据分组到达的延时比较高,因此主要考虑的是如何降低网络延迟对应用进程的阻塞影响。

在具体讨论之前,需要先搞清楚同步和阻塞的概念:

  • 同步与异步:描述的是用户线程的处理方式,同步指用户线程发起IO请求后需要等待或者轮询,直到内核 I/O 操作完成后才能继续执行;而异步是指用户线程发起 I/O 请求后仍然继续执行,当内核 I/O 操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

  • 阻塞与非阻塞:描述是内核 I/O 的处理方式,阻塞是内核等待 I/O 操作需要彻底完成后才返回;而非阻塞是指 I/O 操作被调用后立即返回给用户一个状态值,无需等到 I/O 操作彻底完成。

对于网络 I/O 操作,可以抽象成对 Socket 流的读写,分为两个阶段:

  1. 数据准备:等待网络上的数据分组到达,然后复制到内核缓冲区
  2. 数据拷贝:将数据从内核缓冲区复制到应用进程空间

2.1. 阻塞IO

这种处理比较简单,即用户进程走recv系统调用,然后交给内核处理并进行等待,内核完成操作后再返回给进程,但进程在等待期间将一直处于阻塞状态,而这里的等待包括了上面两个阶段。

其优点是可以及时返回数据,无延迟,而且实现简单,而缺点则是进程一直处于阻塞等待,程序性能较低。

2.2. 非阻塞IO

即内核在进程发出调用后会立即响应一个状态,但这个状态可能是数据还没准备好,因此需要进程自己处理轮询检测状态。

其优点是进程可以在等待期间去做一些其它事情,而缺点是数据接收可能有延迟。

2.3. 多路复用IO

这种方式相当于内核为进程提供了一个状态的轮询服务,进程将对应的socket流注册到轮询服务,这样由操作系统统一负责对多个sockect进行轮询。站在进程的角度,这种方式依然是阻塞的,区别在于之前是直接阻塞在socket上,而现在是阻塞在轮询服务上。这样的方式可以节约计算机资源,因为进程可以用一个线程来同时服务多个socket请求了。

具体的,Linux提供了三种轮询服务:

  • select:select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出完成。它以数组的形式组织文件描述符(64位机器默认2048个),当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。

  • poll:本质与select相同,只是以链表形式存储文件描述符,没有长度限制,时间复杂度也是O(n)。

  • epoll:基于事件驱动,如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,时间复杂度为O(1)。

2.4. 信号驱动式IO

这种方式是先注册一个信号处理函数,然后进程继续运行,当数据准备好的时候,内核向应用程序发送一个信号,进程对信号进行捕捉,并且调用信号处理函数来获取数据报。

其在数据准备阶段是不阻塞的,当数据准备完成之后,会主动通知进程数据已经准备完成,对用户进程做一个回调,但数据拷贝阶段仍是阻塞的,等待数据拷贝。

2.5. 异步IO

进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都直接返回,然后进程去做别的事情。等到 socket 数据准备好了,内核直接复制数据给进程空间,然后再向进程发送通知。

其在整个阶段都是非阻塞的,直接由内核完成后通知进程。

3. Linux零拷贝 sendfile

sendfile 系统调用在内核 2.1 引入,它可以直接在文件描述符与Socket描述符之间传递数据,即直接在内核中操作,从而避免了在内核区与用户区之间的拷贝,因此被称为零拷贝。

1
sendfile(socket, file, len);

其流程大概如下:

  1. sendfile系统调用,文件数据被copy至内核缓冲区

  2. 然后从内核缓冲区copy到内核中socket相关的缓冲区

  3. 最后再从socket缓冲区copy到协议引擎

内核在 2.4 之后,文件描述符结构被改变,引入了 gather机制, sendfile 实现了更简单的方式,当文件数据被拷贝到内核缓冲区时,不再将数据 copy 到 socket 缓冲区,而是只将记录数据位置和长度相关的数据保存到 socket 缓存,而实际的数据则由 DMA 模块直接发送到协议引擎,再次省去了一次 copy 操作。

命令iostat

Linux上可以通过命令iostat来系统设备的IO负载情况,其首次运行时显示自系统启动开始的各项统计信息,之后的运行将显示自上次运行该命令以后的统计信息。一般用来查看磁盘的活动统计情况,但是它只能对系统的整体情况进行分析,如果项对某个进程分析,可以通过命令iotop。

格式:

1
iostat [参数] [时间] [次数]

参数:

1
2
3
4
5
6
7
8
9
10
-c 显示CPU使用情况
-d 显示磁盘使用情况
-k 以 KB 为单位显示
-m 以 M 为单位显示
-N 显示磁盘阵列(LVM) 信息
-n 显示NFS 使用情况
-p [磁盘] 显示磁盘和分区的情况
-t 显示终端和CPU的信息
-x 显示详细信息
-V 显示版本信息

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
avg-cpu:
%user: CPU处在用户模式下的时间百分比
%nice: CPU处在带NICE值的用户模式下的时间百分比
%system:CPU处在系统模式下的时间百分比
%iowait:CPU等待输入输出完成时间的百分比
%steal: 管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比
%idle: CPU空闲时间百分比

Device: 监测设备名称
rrqm/s: 每秒进行的读操作次数
wrqm/s: 每秒进行的写操作次数
r/s: 每秒完成的读I/O设备次数
w/s: 每秒完成的写I/O设备次数
rkB/s: 每秒读取的字节数
wkB/s: 每秒写入的字节数
avgrq-sz:平均每次设备I/O操作的数据大小
avgqu-sz:平均I/O等待队列长度
await: 平均每次设备I/O操作的等待时间 (毫秒)
svctm: 平均每次设备I/O操作的服务时间 (毫秒)
%util: 被I/O需求消耗的CPU百分比

如果%iowait过高,表示磁盘存在I/O瓶颈;如果%idle值高,表示CPU较空闲;
如果%idle值高但系统响应慢,则有可能是CPU等待分配内存,此时应加大内存容量;
如果%idle值持续低于10,那么可能系统的CPU处理能力相对较低,表明系统中最需要解决的资源是CPU;

如果%util接近100,说明I/O请求太多,I/O系统已经满负荷,该磁盘可能存在瓶颈。
如果svctm`接近await,说明I/O几乎没有等待时间;如果await远大于svctm,说明I/O队列太长,响应太慢,需要进行必要优化。

示例:

:[root@bogon ~]# iostat -x -p 1 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Linux 3.10.0-862.el7.x86_64 (bogon)     03/20/22    _x86_64_    (4 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.01 0.00 0.06 0.00 0.00 99.92

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.02 0.13 0.12 3.93 1.29 43.04 0.00 1.94 0.53 3.48 0.85 0.02
sda1 0.00 0.00 0.02 0.00 0.60 0.05 79.35 0.00 0.34 0.32 1.60 0.27 0.00
sda2 0.00 0.02 0.11 0.12 3.30 1.24 40.29 0.00 2.06 0.56 3.48 0.90 0.02
dm-0 0.00 0.00 0.11 0.13 3.18 1.24 37.40 0.00 2.25 0.59 3.59 0.85 0.02
dm-1 0.00 0.00 0.00 0.00 0.08 0.00 48.41 0.00 0.25 0.25 0.00 0.18 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
0.25 0.00 0.25 0.00 0.00 99.51

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

avg-cpu: %user %nice %system %iowait %steal %idle
0.00 0.00 0.00 0.00 0.00 100.00

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

命令netstat

netstat是在内核中访问网络及相关信息的程序,它能提供IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。通过此命令可以知道一个进程占用了哪些端口,或者一个端口被哪个进程占用了。

格式:

1
netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]

参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-a或–all 显示所有连线中的Socket
-A<网络类型>或–<网络类型> 列出该网络类型连线中的相关地址
-c或–continuous 持续列出网络状态
-C或–cache 显示路由器配置的快取信息
-e或–extend 显示网络其他相关信息
-F或–fib 显示FIB
-g或–groups 显示多重广播功能群组组员名单
-h或–help 在线帮助
-i或–interfaces 显示网络界面信息表单
-l或–listening 显示监控中的服务器的Socket
-M或–masquerade 显示伪装的网络连线
-n或–numeric 直接使用IP地址,而不通过域名服务器
-N或–netlink或–symbolic 显示网络硬件外围设备的符号连接名称
-o或–timers 显示计时器
-p或–programs 显示正在使用Socket的程序识别码和程序名称
-r或–route 显示Routing Table
-s或–statistice 显示网络工作信息统计表
-t或–tcp 显示TCP传输协议的连线状况
-u或–udp 显示UDP传输协议的连线状况
-v或–verbose 显示指令执行过程
-V或–version 显示版本信息
-w或–raw 显示RAW传输协议的连线状况
-x或–unix 此参数的效果和指定”-A unix”参数相同
–ip或–inet 此参数的效果和指定”-A inet”参数相同

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Recv-Q 接收队列
Send-Q 发送队列
State 连接状态

状态说明:
LISTEN: 侦听来自远方的TCP端口的连接请求
SYN-SENT: 在发送连接请求后等待匹配的连接请求(如果有大量这样的状态包,检查是否中招了)
SYN-RECEIVED:在收到和发送一个连接请求后等待对方对连接请求的确认(如有大量此状态,估计被flood攻击了)
ESTABLISHED: 代表一个打开的连接
FIN-WAIT-1: 等待远程TCP连接中断请求,或先前的连接中断请求的确认
FIN-WAIT-2: 从远程TCP等待连接中断请求
CLOSE-WAIT: 等待从本地用户发来的连接中断请求
CLOSING: 等待远程TCP对连接中断的确认
LAST-ACK: 等待原来的发向远程TCP的连接中断请求的确认(不是什么好东西,此项出现,检查是否被攻击)
TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认
CLOSED: 没有任何连接状态

示例:

:[root@bogon ~]# netstat -ntp
1
2
3
4
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 192.168.141.21:22 192.168.141.17:51749 ESTABLISHED 55063/sshd: root@pt
tcp 0 0 192.168.141.21:22 192.168.141.17:52641 ESTABLISHED 70231/sshd: root@pt

获取当前ip

查看ip可以直接通过ifconfig或ip addr等命令,但有时需要在脚本中获取使用,所以希望有一种通用的获取办法

1
host_ip=$(ip a | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "255.*.*.*" | grep -v 127.0.0.1 | grep -v 0.0.0.0 | awk 'NR==1{print $1}')


参考:

  1. 《计算机组成原理》
  2. 《深入理解计算机系统》
  3. http://www.tianshouzhi.com/api/tutorials/netty/221
  4. https://blog.csdn.net/u010649766/article/details/80339988