我们都知道在计算机中,程序大体上分为以下几种:
- 应用程序:能让用户直接使用,为用户提供帮助的程序,例如计算机上的办公软件、智能手机和平板电脑上的应用
- 中间件:将对大部分应用程序通用的功能分离出来,以辅助应用程序运行的程序,例如Web服务器、数据库系统
- OS:直接控制硬件设备,同时为应用程序与中间件提供运行环境的程序
Linux 与设备硬件的关系
在 Linux 中,进程通过设备驱动程序来访问设备。如下图所示:

为什么需要设备驱动程序来统一处理设备访问?如果没有设备驱动程序,会产生如下的问题:
- 应用程序开发人员必须精通调用各种设备的方法
- 开发成本高
- 当多个进程同时调用设备时,会引起各种预料之外的问题
在 Linux 中,把这些在内核模式下运行的OS的核心处理整合在一起的程序就叫作内核。如果进程想要使用设备驱动程序等由内核提供的功能,就需要通过被称为系统调用的特殊处理来向内核发出请求。
内核模式和用户模式
在 Linux 中,为了防止某个进程因为bug或者恶意操作而直接访问设备硬件,增加了内核模式和用户模式。
特性 | 内核态 | 用户态 |
---|---|---|
权限 | 拥有完全的硬件访问权限和执行特权指令 | 仅有受限权限,不能直接访问硬件资源 |
执行程序 | 操作系统内核、驱动程序、硬件中断处理程序 | 普通应用程序(如浏览器、文本编辑器、游戏等) |
安全性 | 直接操作硬件,风险较高 | 权限受限,安全性更高 |
切换方式 | 通过系统调用或硬件中断进入内核态 | 通过系统调用返回结果后切回用户态 |
典型操作例子 | 进程调度、内存管理、硬件驱动、设备I/O处理 | 文档编辑、视频播放、网络请求、用户界面操作 |
系统调用
现代操作系统通过系统调用来让用户态程序获得操作系统提供的服务。当运行在用户模式下的进程,使用系统调用向内核发送请求时,CPU会发生名为中断的事件。这时,CPU会从用户模式切换到内核模式,然后根据请求内容进行相应的处理。当内核处理完所有系统调用后,将重新回到用户模式,继续运行进程。如下图所示:

系统调用的种类有:
- 系统控制(创建和删除)
- 内存管理(分配和释放)
- 进程间通信
- 网络管理
- 文件系统操作
- 文件操作(访问设备)
包装函数
如果没有OS的帮助,程序员就不得不根据系统架构为每一个系统调用编写相应的汇编语言代码,然后再从高级编程语言中调用这些代码。
为了解决这样的问题,OS提供了一系列被称为系统调用的包装函数的函数,用于在系统内部发起系统调用。用户程序只需调用包装函数即可,如下图所示:

C标准库
用C语言编写的几乎所有程序都依赖于glibc库。glibc不仅包括系统调用的包装函数,还提供了POSIX标准中定义的函数。因此一般情况下,我们只需要调用 C 标准库的函数就可以了,而不是使用系统调用或者包装函数。
进程
进程管理
在Linux中,创建进程有如下两个目的。
- 将同一个程序分成多个进程进行处理(例如,使用Web服务器接收多个请求)
- 创建另一个程序(例如,从bash启动一个新的程序) 为了达成这两个目的,Linux提供了fork() 函数与execve() 函数(其底层分别请求名为clone() 与execve() 的系统调用)。
fork 函数
fork() 函数对父进程返回子进程的进程ID,而对子进程返回0
创建进程的流程如下图所示:

execve 函数
在打算启动另一个程序时,需要调用execve() 函数。
- 读取可执行文件,并读取创建进程的内存映像所需的信息。
- 用新进程的数据覆盖当前进程的内存。
- 从最初的命令开始运行新的进程。
如下图所示:

结束进程
我们可以使用 _exit() 函数(底层发起exit_group() 系统调用)来结束进程。通常我们很少会直接调用 _exit() 函数,而是通过调用C标准库中的exit() 函数来结束进程的运行。在这种情况下,C标准库会在调用完自身的终止处理后调用 _exit() 函数。
进程的调度
Linux会将多核CPU(现在几乎都是这样的CPU)上的每一个核心都识别为一个CPU。我们将系统识别出来的CPU(这里是指CPU核心)称为逻辑CPU
- 不管同时运行多少个进程,在任意时间点上,只能有一个进程运行在逻辑CPU上
- 在逻辑CPU上运行多个进程时,它们将按轮询调度的方式循环运行,即所有进程按顺序逐个运行,一轮过后重新从第一个进程开始轮流运行
- 每个进程被分配到的时间片的长度大体上相等
- 全部进程运行结束所消耗的时间,随着进程数量的增加而等比例地增加
进程的状态
进程的一部分状态如下所示。

举例来说,处于睡眠态的进程所等待的事件有以下几种:
- 等待指定的时间(比如等待3分钟)
- 等待用户通过键盘或鼠标等设备进行输入
- 等待HDD或SDD等外部存储器的读写结束
- 等待网络的数据收发结束
状态之间的转换如下图所示:

进程的运行时间和执行时间
通过time命令运行进程,就能得到进程从开始到结束所经过的时间,以及实际执行处理所消耗的时间。
- 运行时间:进程从开始运行到运行结束为止所经过的时间。类似于利用秒表从开始运行的时间点开始计时,一直测量到运行结束
- 执行时间:进程实际占用逻辑CPU的时长

使用 time 会有时间差,之所以存在这个时间差,是由于在开始处理前需要估算1毫秒CPU时间对应的计算量(sched.c的loops_per_msec() 函数),这个预处理消耗了一部分时间,如下图所示:

内存
虚拟内存
内核为进程分配内存的时机大体上分为以下两种:
- 在创建进程时
- 在创建完进程后,动态分配内存时
简单的内存分配会造成下列问题:
- 内存碎片化
- 访问用于其他用途的内存区域
- 难以执行多任务
在进程被创建后,如果不断重复执行获取与释放内存的操作,就会引发内存碎片化的问题。
原因如下:
- 进程在每次获取内存后,都需要知道获取的这部分内存涵盖多少个区域,否则就无法使用这些内存,这很不方便
- 进程无法创建比100字节更大的数据块,例如300字节的数组等
为了解决上面出现的问题,现代CPU搭载了被称为虚拟内存的功能。
简而言之,虚拟内存使进程无法直接访问系统上搭载的内存,取而代之的是通过虚拟地址间接访问。进程可以看见的是虚拟地址,系统上搭载的内存的实际地址称为物理地址。此外,可以通过地址访问的范围称为地址空间。

页表
通过保存在内核使用的内存中的页表,可以完成从虚拟地址到物理地址的转换。在虚拟内存中,所有内存以页为单位划分并进行管理,地址转换也以页为单位进行。在页表中,一个页面对应的数据条目称为页表项。页表项记录着虚拟地址与物理地址的对应关系。
一般页表的大小为4kb
虚拟地址空间的大小是固定的,并且页表项中存在一个表示页面是否关联着物理内存的数据。如果访问未关联物理内存的数据,则在CPU上会发生缺页中断。缺页中断可以中止正在执行的命令,并启动内核中的缺页中断机构的处理。内核的缺页中断机构检测到非法访问,向进程发送SIGSEGV信号。接收到该信号的进程通常会被强制结束运行。
为进程分配内存
向进程分配内存(存在虚拟内存时的情形)

在复制完成后,创建进程的页表,并把虚拟地址映射到物理地址

最后,从指定的地址开始运行即可

动态分配内存(存在虚拟内存时的情形)

mmap() 函数会通过系统调用向Linux内核请求新的内存
利用上层进行内存分配
C语言标准库中存在一个名为malloc() 的函数,用于获取内存。在Linux中,这个函数的底层调用了mmap() 函数

mmap() 函数是以页为单位获取内存的,而malloc()函数是以字节为单位获取内存的。为了以字节为单位获取内存,glibc事先通过系统调用mmap() 向内核请求一大块内存区域作为内存池,当程序调用malloc() 函数时,从内存池中根据申请的内存量划分出相应大小(以字节为单位)的内存并返回给程序

顺便一提,虽然部分程序拥有统计自身占用的内存量的功能,但往往程序汇报的值与Linux显示的进程的内存消耗量不同,而后者通常会更大。这是因为,Linux显示的值包括创建进程时以及调用mmap() 函数时分配的所有内存,而程序统计的值通常只有通过调用malloc() 等函数而申请的内存量
虚拟内存如何解决问题
- 通过虚拟内存解决内存碎片化问题

- 访问用于其他用途的内存区域
虚拟地址空间是每个进程独有的。相应地,页表也是每个进程独有的。如下图,进程A和进程B各自拥有独立的虚拟地址空间。

虚拟内存不允许进程访问其他进程的内存空间

只有内核可以访问内核使用的内存
- 难以执行多任务
每个进程拥有独立的虚拟地址空间。因此,我们可以编写运行于专用地址空间的程序,而不用担心干扰其他程序的运行,同时也不用关心自身的内存在哪个物理地址上

虚拟内存的应用
至此,我们已经介绍完虚拟内存的基本机制了,下面我们来介绍几个利用了虚拟内存机制的重要功能。
- 文件映射
- 请求分页
- 利用写时复制快速创建进程
- Swap
- 多级页表
- 标准大页
文件映射
进程在访问文件时,通常会在打开文件后使用read()、write() 以及lseek() 等系统调用。此外,Linux还提供了将文件区域映射到虚拟地址空间的功能。按照指定方式调用mmap() 函数,即可将文件的内容读取到内存中,然后把这个内存区域映射到虚拟地址空间

这样就可以按照访问内存的方式来访问被映射的文件了。
请求分页
有一部分内存在获取后,甚至直到进程运行结束都不会使用,例如:●用于大规模程序中的、程序运行时未使用的功能的代码段和数据段●由glibc保留的内存池中未被用户利用的部分
为了解决这个问题,Linux利用请求分页机制来为进程分配内存。在请求分页机制中,对于虚拟地址空间内的各个页面,只有在进程初次访问页面时,才会为这个页面分配物理内存。页面的状态除了前面提到过的"未分配给进程"与"已分配给进程且已分配物理内存"这两种以外,还存在"已分配给进程但尚未分配物理内存"这种状态。
此时的处理流程如下所示。① 进程访问入口点。② CPU参照页表,筛选出入口点所属的页面中哪些虚拟地址未关联物理地址。③ 在CPU中引发缺页中断。④ 内核中的缺页中断机构为步骤①中访问的页面分配物理内存,并更新其页表。⑤ 回到用户模式,继续运行进程。
但不知大家是否知道,获取内存失败也分为虚拟内存不足与物理内存不足两种情况。虚拟内存不足与剩余多少物理内存无关。在x86架构上,虚拟地址空间仅有4GB,因此数据库之类的大型程序经常会引发虚拟内存不足;但是在x86_64架构上,由于虚拟地址空间扩充到了128TB,所以虚拟内存不足变得非常罕见。物理内存不足与进程的虚拟内存剩余多少无关。与虚拟内存不足相比,物理内存不足的情形应该更容易想象。
写时复制
在发起fork() 系统调用时,并非把父进程的所有内存数据复制给子进程,而是仅复制父进程的页表。虽然在父进程和子进程双方的页表项内都存在表示写入权限的字段,但此时双方的写入权限都将失效(即变得无法进行写入)。
在这之后,假如只进行读取操作,那么父进程和子进程双方都能访问共享的物理页面。但是,当其中一方打算更改任意页面的数据时,则将按照下述流程解除共享。① 由于没有写入权限,所以在尝试写入时,CPU将引发缺页中断。② CPU转换到内核模式,缺页中断机构开始运行。③对于被访问的页面,缺页中断机构将复制一份放到别的地方,然后将其分配给尝试写入的进程,并根据请求更新其中的内容。④为父进程和子进程双方更新与已解除共享的页面对应的页表项。●对于执行写入操作的一方,将其页表项重新连接到新分配的物理页面,并赋予写入权限●对于另一方,也只需对其页表项重新赋予写入权限即可
因为物理内存并非在发起fork() 系统调用时进行复制,而是在尝试写入时才进行复制,所以这个机制被称为写时复制(Copy on Write,CoW)。
尽管父进程的内存使用量超过了100MB,但从调用fork() 到子进程开始往内存写入数据的这段时间,内存使用量仅增加了几百KB●在子进程向内存写入数据后,不但发生缺页中断的次数增加了,系统的内存使用量也增加了100MB(这代表内存共享已解除)
对于共享的内存,父进程和子进程双方会重复计算。因此,所有进程的物理内存使用量的总值会比实际使用量要多。以实验程序为例,在子进程开始写入数据前,父进程和子进程的实际物理内存使用量共为100MB左右,但双方都会认为自己独占了100MB的物理内存。
swap
在系统物理内存不足的情况下,当出现获取物理内存的申请时,物理内存中的一部分页面将被保存到外部存储器中,从而空出充足的可用内存。这里用于保存页面的区域称为交换分区(Swap分区)
换出与换入这两个处理统称为交换。在Linux中,由于交换是以页为单位进行的,所以也称为分页。同时,换入与换出也分别称为页面调入与页面调出。
当系统长期处于内存不足时,访问内存的操作将导致页面不断地被换入和换出,从而导致系统陷入系统抖动(颠簸)状态。
类似于交换这类需要访问外部存储器的缺页中断称为硬性页缺失。与此相对,无须访问外部存储器的缺页中断称为软性页缺失。
多级页表
在x86_64架构上,虚拟地址空间大小为128TB,页面大小为4KB,页表项的大小为8字节。通过上面的信息可以算出,一个进程的页表就需要占用256GB的内存(= 8B×128TB / 4KB)为了避免这样的情况,x86_64架构的页表采用多级结构,而非上面描述的单层结构。这样就能节约大量的内存
当虚拟内存使用量增加到一定程度时,多级页表的内存使用量就会超过单层页表。但这种情况非常罕见,所以从系统整体来看,也是多级页表的内存使用量更少。另外,x86_64架构的页表结构还要更加复杂,达到了4级
标准大页
随着进程的虚拟内存使用量增加,进程页表使用的物理内存量也会增加。此时,除了内存使用量增加的问题之外,还存在fork() 系统调用的执行速度变慢的问题,这是因为fork() 是通过写时复制创建进程的,这虽然不需要复制物理内存的数据,但是需要为子进程复制一份与父进程同样大小的页表。为了解决这个问题,Linux提供了标准大页机制。
顾名思义,标准大页是比普通的页面更大的页。利用这种页面,能有效减少进程页表所需的内存量。
在C语言中,通过为mmap() 函数的flags参数赋予MAP_HUGETLB标志,可以获取标准大页。但在实际应用中,比起让编写的程序直接获取标准大页,更常用的做法是为现有程序开启允许使用标准大页的设置。数据库与虚拟机管理器等都是需要使用大量虚拟内存的软件,它们会提供关于标准大页的设置项,请根据实际情况决定使用与否。通过进行设置,能够减少这类软件的内存使用量,同时还能提高fork() 的执行速度
Linux上还存在一个名为透明大页的机制。当虚拟地址空间内连续多个4KB的页面符合特定条件时,通过透明大页机制能将它们自动转换成一个大页。
存储层次
存储层次结构如下图所示:

高速缓存
在从内存往寄存器读取数据时,数据先被送往高速缓存,再被送往寄存器。所读取的数据的大小取决于缓存块大小(cache line size)的值,该值由各个CPU规定。 将内存地址300 上的数据读取到R0,如下图所示:

当需要将寄存器上的数据重新写回到地址300上时,首先会把改写后的数据写入高速缓存,如图6-5所示。此时依然以缓存块大小为单位写入数据。然后,为这些缓存块添加一个标记,以表明这部分从内存读取的数据被改写了。通常我们会称这些被标记的缓存块"脏了"。把改写后的数据写入高速缓存,如下图所示:

这些被标记的数据会在写入高速缓存后的某个指定时间点,通过后台处理写入内存。随之,这些缓存块就不再脏了
高速缓存不足时
在高速缓存不足时,如果要读写高速缓存中尚不存在的数据,就要销毁一个现有的缓存块。当需要销毁的缓存块脏了的时候,数据将在被销毁前被同步到内存中。
如果在高速缓存不足,且所有缓存块都脏了的时候向内存发起访问,那么将因高速缓存频繁执行读写处理而发生系统抖动,与此同时性能也会大幅降低。
多级缓存
构成分层结构的各高速缓存分别名为L1、L2、L3(L为Level的首字母。
高速缓存的信息可从 /sys/devices/system/cpu/cpu/cache/ index/这一目录下的文件中查看。●type:高速缓存中缓存的数据类型。Data代表仅缓存数据,Code代表仅缓存指令,Unified代表两者都能缓存●shared_cpu_list:共享该缓存的逻辑CPU列表●size:容量大小●coherency_line_size:缓存块大小
访问局部性
大部分程序具有名为访问局部性的特征,具体如下所示。●时间局部性:在某一时间点被访问过的数据,有很大的可能性在不久的将来会再次被访问,例如循环处理中的代码段●空间局部性:在某一时间点访问过某个数据后,有很大的可能性会继续访问其附近的其他数据,例如遍历数组元素
一方面,通过将程序的工作量保持在高速缓存容量的范围内,可以大幅提升程序性能。对于重视运行速度的程序来说,要想最大限度地发挥高速缓存的优势,最重要的是花更多心思在数据结构与算法的设计上,以减小单位时间的内存访问范围。
转译后备缓冲区
进程需要通过下述步骤访问虚拟地址上的特定数据。① 对照物理内存中的页表,把虚拟地址转换为物理地址。② 访问通过步骤①得到的物理地址。
敏锐的人可能已经发现了,这里能发挥高速缓存优势的只有步骤②。这是因为步骤①依然需要访问物理内存,以读取物理内存中的页表。这样一来,特意准备的高速缓存就浪费了。为了解决这一问题,在CPU上存在一个具有与高速缓存同样的访问速度的区域,名为转译后备缓冲区(Translation Lookaside Buffer,TLB),又称为快表或页表缓冲,该区域用于保存虚拟地址与物理地址的转换表。利用这一区域,即可提高步骤①的执行速度。
页面缓存
与CPU访问内存的速度比起来,访问外部存储器的速度慢了几个数量级。内核中用于填补这一速度差的机构称为页面缓存
页面缓存和高速缓存非常相似。高速缓存是把内存上的数据缓存到高速缓存上,而页面缓存则是将外部存储器上的文件数据缓存到内存上。高速缓存以缓存块为单位处理数据,而页面缓存则以页为单位处理数据。
当进程读取文件的数据时,内核并不会直接把文件数据复制到进程的内存中,而是先把数据复制到位于内核的内存上的页面缓存区域,然后再把这些数据复制到进程的内存中

在内核自身的内存中有一个管理区域,该区域中保存着页面缓存所缓存的文件以及这些文件的范围信息等。
在进程向文件写入数据后,内核会把数据写入页面缓存中。这时,管理区域中与这部分数据对应的条目会被添加一个标记,以表明"这些是脏数据,其内容比外部存储器中的数据新"。这些被标记的页面称为脏页。
需要注意的是,只要系统上还存在可用内存,则每当各个进程访问那些尚未读取到页面缓存中的文件时,页面缓存的大小就会随之增大。
而当系统内存不足时,内核将释放页面缓存以空出可用内存。此时,首先丢弃脏页以外的页面。如果还是无法空出足够内存,就对脏页执行回写,然后继续释放页面。当需要释放脏页时,由于需要访问外部存储器,所以恐怕会导致系统性能下降。尤其是当系统上存在大量文件写入操作而导致出现大量脏页时,系统负载往往会变得非常大。内存不足引发大量脏页的回写处理,进而导致系统性能下降的情况非常常见。
同步写入
在页面缓存中还存在脏页的状态下,如果系统出现了强制断电的情况,会发生什么呢?强制断电将导致页面缓存中的脏页丢失。如果不希望文件访问出现这种情况,请在利用open() 系统调用打开文件时将flag参数设定为O_SYNC。这样一来,之后每当对该文件执行write() 系统调用,都会在往页面缓存写入数据时,将数据同步写入外部存储器。
超线程
在使用超线程功能后,可以为CPU核心提供多份(一般为两份)硬件资源(其中包含一部分CPU核心使用的硬件资源,例如寄存器等),然后将其划分为多个会被系统识别为逻辑CPU的超线程。在符合这些特殊条件时,可以同时运行多个超线程。
每个逻辑CPU代表的不是一个CPU核心,而是CPU核心中的一个超线程。只要查看一下 /sys/devices/ system/cpu/cpu/topology/thread_siblings_list,就可以确认哪两个超线程是成对的
文件系统
文件系统以文件为单位管理所有对用户有实际意义的数据块,并为这些数据块添加上名称、位置和大小等辅助信息。它还规范了数据结构,以确定什么文件应该保存到什么位置,内核中的文件系统将依据该规范处理数据。多亏了文件系统的存在,用户不再需要记住所有数据在外部存储器中的位置与大小等繁杂的信息,只需要记住数据(即文件)的名称即可。
只需提供文件名、文件的偏移量以及文件大小,即可读取符合条件的数据

linux的文件系统
Linux能使用的文件系统不止一种,ext4、XFS、Btrfs等不同的文件系统可以共存于Linux上
不管使用哪种文件系统,面向用户的访问接口都统一为下面这些系统调用。
- 创建与删除文件:create()、unlink()
- 打开与关闭文件:open()、close()
- 从已打开的文件中读取数据:read()
- 往已打开的文件中写入数据:write()
- 将已打开的文件移动到指定位置:lseek()
- 除了以上这些操作以外的依赖于文件系统的特殊处理:ioctl()
无关文件系统的种类,可以通过统一的接口进行访问

数据与元数据
文件系统上存在两种数据类型,分别是数据与元数据。
- 数据:用户创建的文档、图片、视频和程序等数据内容
- 元数据:文件的名称、文件在外部存储器中的位置和文件大小等辅助信息
元数据分为以下几种:
- 种类:用于判断文件是保存数据的普通文件,还是目录或其他类型的文件的信息
- 时间信息:包括文件的创建时间、最后一次访问的时间,以及最后一次修改的时间
- 权限信息:表明该文件允许哪些用户访问
容量限制
可以通过磁盘配额(quota)功能来限制各种用途的文件系统的容量。
磁盘配额有以下几种类型。
- 用户配额:限制作为文件所有者的用户的可用容量。例如防止某个用户用光 /home目录的存储空间。ext4与XFS上可以设置用户配额
- 目录配额:限制特定目录的可用容量。例如限制项目成员共用的项目目录的可用容量。ext4与XFS上可以设置目录配额
- 子卷配额:限制文件系统内名为子卷的单元的可用容量。大致上与目录配额的使用方式相同。Btrfs上可以设置子卷配额
文件系统不一致

只要发生过这种不一致的状况,迟早会被文件系统检查出来。如果在挂载时检测到不一致,就会导致文件系统无法被挂载;如果在访问过程中检测到不一致,则可能会以只读模式重新挂载该文件系统,在最坏的情况下甚至可能导致系统出错。
防止文件系统不一致的技术有很多,常用的是日志(journaling)与写时复制。ext4与XFS利用的是日志,而Btrfs利用的是写时复制
文件的种类
我们在前面提到了文件的两种类型:保存用户数据的普通文件,以及保存其他文件的目录。在Linux中还有一种文件,称为设备文件。
Linux会将自身所处的硬件系统上几乎所有的设备呈现为文件形式。因此在Linux上,设备如同文件一般,可以通过open()、read()、write() 等系统调用进行访问。在需要执行设备特有的复杂操作时,就使用ioctl() 系统调用。在通常情况下,只有root用户可以访问设备文件。虽然设备也存在很多种类,但Linux将以文件形式存在的设备分为两种类型,分别为字符设备与块设备。所有设备文件都保存在 /dev目录下。通过设备文件的元数据中保存的以下信息,我们可以识别各个设备。
●文件的种类(字符设备或块设备)●设备的主设备号●设备的次设备号
字符设备
字符设备虽然能执行读写操作,但是无法自行确定读取数据的位置。
下面列出了几个比较具有代表性的字符设备。●终端●键盘●鼠标
块设备
块设备除了能执行普通的读写操作以外,还能进行随机访问,比较具有代表性的块设备是HDD与SSD等外部存储器。但在以下几种情况下,需要直接操作块设备。●更新分区表(利用parted命令等)●块设备级别的数据备份与还原(利用dd命令等)●创建文件系统(利用各文件系统的mkfs命令等)●挂载文件系统(利用mount命令等)●fsck
通过这一系列的实验,大家应该能明白,通过直接操作块设备的设备文件,即可操作外部存储器,并且隐藏在文件系统下的只是保存在外部存储器中的数据而已。
ext4、XFS和Btrfs这3种文件系统,这些都是存在于外部存储器中的文件系统
基于内存的文件系统
tmpfs是一种创建于内存(而非外部存储器)上的文件系统。虽然这个文件系统中保存的数据会在切断电源后消失,但由于访问该文件系统时不再需要访问外部存储器,所以能提高访问速度。

tmpfs通常被用于 /tmp与 /var/run这种"文件内容无须保存到下一次启动时"的文件上
网络文件系统
而网络文件系统则可以通过网络访问远程主机上的文件

虽然网络文件系统同样存在许多种类,但基本上,在访问Windows主机上的文件时,使用名为cifs的文件系统;而在访问搭载Linux等类UNIX系统的主机上的文件时,则使用名为nfs的文件系统
虚拟文件系统
系统上存在着各种各样的文件系统,用于获取内核中的各种信息,以及更改内核的行为。
-
procfsprocfs用于获取系统上所有进程的信息。它通常被挂载在 /proc目录下。通过访问 /proc/pid/ 目录下的文件,即可获取各个进程的信息
-
sysfs sysfs包括但不限于下列文件。●/sys/devices目录下的文件:搭载于系统上的设备的相关信息●/sys/fs目录下的文件:系统上的各种文件系统的相关信息
-
cgroupfs 用于限制单个进程或者由多个进程组成的群组的资源使用量,它需要通过文件系统cgroupfs来操作。
Btrfs
近年来出现了许多功能更加丰富的新文件系统,其中比较具有代表性的就是Btrfs
- 多物理卷
- 快照
- RAID
在写作本书时,虽然ext4与XFS可以通过为元数据附加校验和来检测并丢弃损坏的元数据,但是对数据的检测、丢弃与修复功能则只有Btrfs提供。
外部存储器
HDD的数据读写机制
HDD用磁性信息表示数据,并将这些磁性数据记录在被称为盘片(platter)的磁盘上。HDD读写数据的单位是扇区

HDD的数据传输流程如下图。设备驱动程序将读写数据所需的信息传递给HDD,其中包含扇区序列号、扇区数量以及访问类型(读取或写入)等信息。② 通过摆动磁头摆臂并转动盘片,将磁头对准需要访问的扇区。③ 执行数据读写操作。④ 在执行读取的情况下,执行完HDD的读取处理就能结束数据传输。

HDD的性能特性
HDD能在一次访问请求中读取多个连续扇区上的数据。因为磁头通过磁头摆臂在径向上对准位置后,只需转动盘片,就能读取多个连续扇区上的数据。一次读取的数据量取决于各个HDD自身。
如果需要访问的扇区是连续的,却要分成多次来访问,就会增加访问处理的时间开销,如下图:

在访问扇区0、11、23这种不连续的扇区时,则需要将访问请求分成多次发送给HDD,这时访问轨迹会变长。
由于HDD具有这样的特性,所以各种文件系统都会尽量把文件中的数据存放在连续的区域上。大家在编写程序时也一样,尽量设计成下面这样比较好。●尽量将文件中的数据存放在连续的或者相近的区域上●把针对连续区域的访问请求汇集到一次访问请求中●对于文件,尽量以顺序访问的方式访问尽可能大的数据量
为open() 函数添加O_DIRECT标记,启用直接I/O(Direct I/O)。在启用直接I/O后,可以禁用内核的I/O支援功能
顺序访问
但在单次I/O请求量达到1MB后,性能就已经到达峰值了。这个值就是该HDD单次访问允许的数据量上限,同时也是该HDD设备的性能上限。顺便一提,如果单次I/O请求量超过数据访问的上限值,在通用块层中会将该访问划分为多次请求来执行。
随机访问
可以看到,不管是读取还是写入,随机访问的性能都比顺序访问差。特别是在I/O请求量较小时,差距更加明显。虽然随着I/O请求量变大,程序整体上等待访问完成的时间在减少,吞吐量也得以提升,但还是比不上顺序访问的性能。
通用块层
Linux中将HDD和SSD这类可以随机访问、并且能以一定的大小(在HDD与SSD中是扇区)访问的设备统一归类为块设备。块设备可以通过设备文件直接访问,也可以通过在其上构建的文件系统来间接访问。大部分软件采用的是后一种方式。
由于各种块设备通用的处理有很多,所以这些处理并不会在设备各自的驱动程序中实现,而是被集成到内核中名为通用块层的组件上来实现。

IO调度器
通用块层中的 I/O调度器会将访问块设备的请求积攒一定时间,并在向设备驱动程序发出I/O请求前对这些请求进行如下加工,以提高I/O的性能。●合并:将访问连续扇区的多个I/O请求合并为一个请求●排序:按照扇区的序列号对访问不连续的扇区的多个I/O请求进行排序

在读取时I/O调度器并不会运行
SSD
与HDD最大的不同是,在访问SSD上的数据时,不会发生任何机械处理,只需执行电子处理即可完成访问。由于SSD具有上述特征,所以其随机访问的性能也比HDD快很多。
可以看到,虽然在I/O请求量较小时,I/O支援功能的确起到了提升性能的作用,但在I/O请求量稍微变大后,I/O支援功能却起了反作用,启用I/O支援功能时的性能变得比禁用时更差了。这是因为,SSD无法忽略I/O调度器积攒I/O请求所产生的系统开销,以及排序处理的效果不怎么理想
尽量将文件中的数据存放在连续的或者相近的区域●把针对连续区域的访问请求汇集到一次访问请求中●对于文件,尽量以顺序访问的方式访问尽可能大的数据量
预读
在程序访问数据时具有空间局部性这一特征。通用块层中的预读(read-ahead)机制就是利用这一特征来提升性能的。
当程序访问了外部存储器上的某个区域后,很有可能继续访问紧跟在其后的区域,而预读机制正是基于这样的推测,预先读取那些接下来可能被访问的区域。
可以看到,不管是否启用I/O支援功能,随机读取的性能都几乎没有发生变化。为什么呢?之前曾提到过,在读取时I/O调度器并不会运行,而且因为不是顺序读取,所以预读机制也无法发挥作用