Linux是怎样工作的

Linux是怎样工作的
💻 计算机系统的概要
操作系统(OS)的定义
操作系统是直接控制计算机硬件设备,并为应用程序提供运行环境的程序。

计算机系统的结构
计算机系统大体上由以下几个部分构成:

部件 描述
CPU 中央处理器,负责执行指令和处理数据。
内存 存储程序和数据的地方。
外部设备 输入/输出设备,如显示器、键盘、鼠标等。
外部存储器 如硬盘驱动器(HDD)和固态硬盘(SSD),用于持久存储数据。
网络设备 网络适配器,用于连接其他计算机。
计算机系统的工作流程
在计算机系统运行时,硬件设备会重复执行以下步骤:

通过输入设备或网络适配器向计算机发起请求。
读取内存中的命令,并在CPU上执行,将结果写入内存。
将内存中的数据写入硬盘或通过网络发送给其他计算机。
返回步骤1,循环处理。
程序的分类
程序大体上分为以下几种:

程序类型 描述
应用程序 直接为用户提供帮助的程序,如办公软件。
中间件 辅助应用程序运行的程序,如Web服务器、数据库系统。
操作系统 控制硬件设备并提供运行环境的程序,如Linux。
进程的概念
进程是程序在操作系统上运行的基本单位。
每个程序可能由一个或多个进程构成,操作系统能够同时运行多个进程。
设备调用的处理
在没有操作系统的情况下,每个进程都需单独编写调用设备的代码,这会导致:

应用程序开发人员必须精通各种设备调用。
增加开发成本。
多个进程同时调用设备时产生意外问题。
设备驱动程序
Linux通过设备驱动程序整合设备调用处理,使得进程可以通过设备驱动程序访问设备。

进程的访问权限
为了避免进程直接访问设备,Linux利用CPU的内核模式和用户模式:

内核模式:允许访问设备,主要由设备驱动程序等关键系统组件运行。
用户模式:进程在此模式下运行,无法直接访问设备。
系统调用
进程需要通过系统调用向内核请求使用设备驱动程序提供的功能。

结论
操作系统(OS)不仅仅指内核,它是由内核与众多用户模式下运行的程序共同构成的。通过理解计算机系统的结构和工作流程,读者可以更好地开发软件、设计系统,并处理与操作系统和硬件设备相关的问题。

💻 计算机系统的概要
内核与资源管理
“内核负责管理计算机系统上的CPU和内存等各种资源,然后把这些资源按需分配给在系统上运行的各个进程。”

资源分配
内核负责管理的资源包括:
CPU
内存
资源的分配由进程调度器负责。
进程调度器
进程调度器的详细内容将在第4章讨论。
存储系统层次结构
“在进程运行的过程中,各种数据会以内存为中心,在CPU上的寄存器或外部存储器等各种存储器之间进行交换。”

存储层次
存储器类型 容量 价格 访问速度
CPU寄存器 非常小 高 非常快
内存 小 中 快
外部存储器 大 低 慢
各种存储器在容量、价格和访问速度等方面各有优缺点,构成了存储系统的层次结构。
文件系统的访问
“通常会利用被称为文件系统的程序进行访问外部存储器中的数据。”

文件系统
文件系统的详细内容将在第7章讨论。
外部存储器的重要性
“外部存储器是不可或缺的。”

启动过程
启动系统时,需要从外部存储器中读取操作系统(OS)。
为了防止数据丢失,需在关闭电源前将数据写入外部存储器。
BIOS与UEFI
在读取OS之前,通过BIOS(基本输入/输出系统)或UEFI(统一可扩展固件接口)初始化硬件设备。
用户模式与内核模式
进程与OS的关系
“OS不仅由内核构成,还包含许多在用户模式下运行的程序。”

组件 描述
用户模式程序 以库的形式存在或作为单独的进程运行。
系统调用 进程通过系统调用向内核发送请求。
内核 提供用户模式下程序所需的服务。
系统调用概述
系统调用的种类
进程控制(创建和删除)
内存管理(分配和释放)
进程间通信
网络管理
文件系统操作
文件操作(访问设备)
CPU模式切换
系统调用需要执行特殊的CPU命令。
通过系统调用,CPU从用户模式切换到内核模式,进行相应处理。
请求验证
内核会验证请求的合理性,例如请求的内存量是否超过系统可用内存。
示例:使用strace追踪系统调用
C语言示例
c
Copy
#include <stdio.h>
int main(void) {
puts(“hello world”);
return 0;
}
使用strace命令追踪程序的系统调用:
bash
Copy
$ strace -o hello.log ./hello
Python示例
python
Copy
print(“hello world”)
使用strace命令追踪Python程序的系统调用:
bash
Copy
$ strace -o hello.py.log python3 ./hello.py
CPU运行时间分析
sar命令
“sar命令用于获取进程分别在用户模式与内核模式下运行的时间比例。”

通过sar命令可以获取CPU各个核心的运行状态。
统计项 描述
%user 用户模式下运行时间的比例
%system 内核模式下执行系统调用等处理的时间比例
%idle CPU核心完全没有运行处理时的空闲状态
实验:监测CPU模式
编写循环程序并使用sar命令监测其在各模式下的运行时间。
C语言循环程序示例
c
Copy
int main(void) {
for(;😉;
}
编译并运行循环程序,观察其在CPU上的表现。
结论
通过监测程序的系统调用和CPU运行时间,可以深入理解计算机系统的工作原理和效率。
🖥️ 系统调用与进程管理

  1. 系统调用概述
    “系统调用是操作系统提供给用户程序的接口,允许程序请求操作系统进行特定的低级任务。”

1.1 系统调用的性质
系统调用是用户模式与内核模式之间的桥梁。
用户程序通过系统调用请求内核执行任务,如文件操作、进程管理等。
1.2 CPU 运行模式
在 CPU 中,有两种运行模式:

用户模式:限制程序访问硬件和内存。
内核模式:允许程序访问系统资源和硬件。
1.3 时间变化示例
在运行 ppidloop 程序时的时间分配:

运行模式 占用时间 (%)
用户模式 28
内核模式 72
2. 进程管理
2.1 创建与删除进程
在 Linux 中,创建进程的主要目的是:

将同一个程序分成多个进程进行处理,例如接收多个请求的 Web 服务器。
创建新程序,例如从 bash 启动新的程序。
2.2 fork() 函数
fork() 函数用于创建新进程,其流程如下:

为子进程申请内存空间,并复制父进程的内存。
父进程与子进程开始执行不同的代码。
示例代码:

c
Copy
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

static void child() {
printf(“I’m child! my pid is %d.\n”, getpid());
exit(EXIT_SUCCESS);
}

static void parent(pid_t pid_c) {
printf(“I’m parent! my pid is %d and the pid of my child is %d.\n”, getpid(), pid_c);
exit(EXIT_SUCCESS);
}

int main(void) {
pid_t ret;
ret = fork();
if(ret == -1)
err(EXIT_FAILURE, “fork() failed”);
if(ret == 0) {
child();
} else {
parent(ret);
}
err(EXIT_FAILURE, “shouldn’t reach here”);
}
2.3 execve() 函数
execve() 函数用于启动另一个程序,流程如下:

读取可执行文件并获取所需信息。
用新程序的数据覆盖当前进程的内存。
从最初的命令开始运行新的进程。
2.4 ELF 文件格式
Linux 的可执行文件遵循 ELF(Executable and Linkable Format)格式,相关信息可以通过 readelf 命令获取。

示例:

bash
Copy
$ readelf -h /bin/sleep
Entry point address: 0x401760
3. 系统调用的包装函数
Linux 提供了系统调用的包装函数,简化了程序员的工作。它们允许用户程序以更高层次的抽象进行系统调用,而不需要直接编写汇编语言。

3.1 C 标准库
C 语言标准库(如 glibc)提供了系统调用的包装函数,以及 POSIX 标准中定义的函数。几乎所有用 C 语言编写的程序都依赖于 glibc 库。

3.2 OS 提供的程序
操作系统提供的程序是用户程序的不可或缺的部分,常见的包括:

功能 程序示例
初始化系统 init
变更系统运行方式 sysctl, nice
文件操作 touch, mkdir
文本数据处理 grep, sort
性能测试 sar, iostat
编译 gcc
脚本语言运行环境 perl, python
shell bash
视窗系统 X
3.3 依赖库检查
使用 ldd 命令可以查看程序所依赖的库。例如:

bash
Copy
$ ldd /bin/echo
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
4. 性能监控与分析
使用 strace 命令可以监控程序的系统调用,并了解其性能:

bash
Copy
$ strace -T -o hello.log ./hello
通过这些信息,程序员可以识别出哪些系统调用消耗了过多的资源,从而进行优化。

💻 进程管理与调度
进程的内存映像
在查看进程的内存映像时,可以使用以下命令:

bash
Copy
$ cat /proc/3967/maps
该命令将输出当前进程的内存映像,其中包含不同段的信息。例如:

代码段:00400000-00407000 r-xp 00000000 08:01 23994 /bin/sleep ←①
数据段:00607000-00608000 rw-p 00007000 08:01 23994 /bin/sleep ←②
代码段是存放执行代码的区域,而数据段则用于存放程序运行时的变量和其他数据。

在查看完内存映像后,记得结束正在运行的程序:

bash
Copy
$ kill 3967
进程创建:fork 和 exec
在创建新进程时,通常采用fork and exec的方式。具体流程如下:

父进程调用 fork() 创建子进程。
子进程调用 exec() 来执行新的程序。
下图展示了由bash进程创建echo进程的流程:

text
Copy
内存
内存
内存
在调用exec()后开始运行
echo
调用
fork()
在从fork()调用中恢复后立刻开始调用exec()
在从fork()调用中恢复后继续运行
内核的内存
内核的内存
内核的内存
bash进程的内存
bash进程的内存
bash进程的内存
bash子进程的内存
echo进程的内存
execve() 函数
execve() 函数用于创建新的进程并执行指定程序。以下是实现该功能的示例代码(fork-and-exec.c):

c
Copy
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>

static void child() {
char *args[] = {“/bin/echo”, “hello”, NULL};
printf(“I’m child! my pid is %d.\n”, getpid());
fflush(stdout);
execve(“/bin/echo”, args, NULL);
err(EXIT_FAILURE, “exec() failed”);
}

static void parent(pid_t pid_c) {
printf(“I’m parent! my pid is %d and the pid of my child is %d.\n”, getpid(), pid_c);
exit(EXIT_SUCCESS);
}

int main(void) {
pid_t ret;
ret = fork();
if (ret == -1)
err(EXIT_FAILURE, “fork() failed”);
if (ret == 0) {
child();
} else {
parent(ret);
}
err(EXIT_FAILURE, “shouldn’t reach here”);
}
代码执行示例
编译并运行此程序的结果如下:

bash
Copy
$ cc -o fork-and-exec fork-and-exec.c
$ ./fork-and-exec
I’m parent! my pid is 4203 and the pid of my child is 4204.
I’m child! my pid is 4204.
$ hello
结束进程
结束进程通常使用 _exit() 函数,它直接调用 exit_group() 系统调用来终止进程。通常我们会通过 C 标准库中的 exit() 函数来结束进程,这样可以确保在结束之前完成必要的处理。

注意:C 标准库会在调用完自身的终止处理后调用 _exit() 函数。

进程调度器
Linux 内核具有进程调度器的功能,使得多个进程可以同时运行。调度器的基本运行原理是:

一个 CPU 同时只运行一个进程。
在多个进程同时运行时,每个进程会依次获得一个时间片(time slice)在 CPU 上执行。
调度器运作示例
以三个进程 p0、p1 和 p2 为例,调度器的运作方式如图所示:

text
Copy
p0
p1
p0正在运行
p2
CPU
p0
p1
p1正在运行
p2
CPU
p0
p1
p2正在运行
p2
CPU
实验程序设计
在本章中,通过实验程序来验证调度器的运作方式。实验程序的参数设计如下:

参数名称 描述
n 同时运行的进程数量
total 程序运行的总时长(单位:毫秒)
resol 采集统计信息的间隔(单位:毫秒)
实验程序示例代码(sched.c)
c
Copy
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

#define NLOOP_FOR_ESTIMATION 1000000000UL
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL

static inline long diff_nsec(struct timespec before, struct timespec after) {
return ((after.tv_sec * NSECS_PER_SEC + after.tv_nsec) - (before.tv_sec * NSECS_PER_SEC + before.tv_nsec));
}

static unsigned long loops_per_msec() {
struct timespec before, after;
clock_gettime(CLOCK_MONOTONIC, &before);
unsigned long i;
for (i = 0; i < NLOOP_FOR_ESTIMATION; i++)
;
clock_gettime(CLOCK_MONOTONIC, &after);
return NLOOP_FOR_ESTIMATION * NSECS_PER_MSEC / diff_nsec(before, after);
}

// 其他代码省略
实验结果分析
在不同进程数量下,程序的调度情况会有所不同。以下是实验的三种情况:

实验4-A:运行1个进程
实验4-B:运行2个进程
实验4-C:运行4个进程
通过图表分析,可以得出以下结论:

进程在 CPU 上是轮流使用的,而不是同时运行。
随着进程数量的增加,每个进程的时间片会相应减少。
示例实验数据
实验4-A运行结果示例:

text
Copy
$ taskset -c 0 ./sched 1 100 1
estimating workload which takes just one milisecond
end estimation
0
1

通过以上实验和分析,可以加深对进程调度器运作方式的理解。

🖥️ 进程调度与状态
进程的基本概念
进程是计算机中正在运行的程序的实例,是系统资源分配的基本单位。
进程调度
进程运行与时间消耗
在实验中发现,随着进程数量的增加,所有进程运行结束所消耗的时间约为单个进程所需时间的四倍。
每个时间片的进度大致为单个进程的1/4。
上下文切换
定义
上下文切换是指切换正在逻辑CPU上运行的进程。

特点
当一个时间片用完后,系统会强制进行上下文切换。
例如,函数bar()并不一定紧接在foo()之后执行,可能由于时间片耗尽而被延后。
示例
c
Copy
void main(void)
{

foo()
bar()

}
上下文切换可能导致bar()执行延迟,影响程序的执行顺序。
进程的状态
状态概述
进程的状态主要包括以下几种:

状态名 含义
运行态 正在逻辑CPU上运行
就绪态 进程具备运行条件,等待分配CPU时间
睡眠态 进程不准备运行,除非发生某事件,期间不消耗CPU时间
僵死状态 进程运行结束,等待父进程回收
进程状态转换
进程在创建后,会在运行态、就绪态和睡眠态之间不断转换,生命周期并非简单的“用完时间片就结束”。
吞吐量与延迟
定义
吞吐量:单位时间内完成的工作量,越大越好。
延迟:处理开始到结束所需的时间,越短越好。
吞吐量公式
KaTeX can only parse string typed expression
吞吐量=
耗费的时间
处理完成的进程数量

延迟公式
KaTeX can only parse string typed expression
延迟=结束处理的时间−开始处理的时间
实验观察
当进程数量增加,吞吐量保持在10个进程/秒,延迟有所增加。
例如,1个进程的延迟为100毫秒,2个进程的延迟为200毫秒。
实际命令与示例
使用命令ps ax可以查看当前系统中运行的进程。
ps ax | wc -l可以统计当前运行的进程数量。
示例输出
text
Copy
$ ps ax | wc -l
365
输出结果显示当前系统中有300多个进程在运行。
进程状态的实际例子
状态转换示例
在逻辑CPU上只存在进程0时,该进程的状态转换和逻辑CPU的执行状态如下:
text
Copy
运行态
→ 睡眠态
→ 运行态
在存在多个进程时,逻辑CPU的状态会变得更加复杂,但始终遵循原则:同一时间只能运行一个进程,睡眠态的进程不占用CPU时间。
重要总结
理解进程的状态和调度对计算机系统的性能至关重要。
以吞吐量和延迟为指标,可以更好地评估和优化系统性能。
📊 吞吐量与延迟
计算结果汇总
将不同进程数量的吞吐量与延迟计算结果总结如下:

进程数量 吞吐量(进程数量/秒) 延迟(毫秒)
1 10 100
2 20 100
4 20 200
吞吐量与延迟的关系
吞吐量与延迟呈现出一种此消彼长的关系:当逻辑CPU处于工作状态时,吞吐量和延迟达到最优值。
当逻辑CPU的计算能力耗尽后,继续增加进程数量时,吞吐量将不再变化。
随着进程数量的增加,延迟会逐步增长。
进程调度的影响
在调度多个进程时,如果调度器不是采用轮询调度,可能会出现延迟不均的情况。具体如下:

进程0和进程1同时开始运行,但前者的延迟为100毫秒,而后者的延迟为200毫秒,造成不公平的调度结果。
为避免这种情况,调度器将逻辑CPU的CPU时间划分为短时间片,以便公平分配给多个进程。
现实中的进程调度
在实际系统中,逻辑CPU会在以下状态之间转换:

空闲状态:吞吐量降低。
运行状态:理想状态,吞吐量高,延迟短。
存在就绪态的进程:吞吐量大,但延迟增加。
优化系统性能
为了优化系统性能,需关注以下数据:

%idle字段:表示CPU空闲的百分比。
runq-sz字段:显示处于运行态或就绪态的进程总数。
实验方法与结果
实验设置
使用以下命令确认逻辑CPU数量:

bash
Copy
$ grep -c processor /proc/cpuinfo
运行实验
运行实验程序时需选择逻辑CPU0及逻辑CPU数量的一半,以避免共享高速缓存带来的影响。
实验名称 n total resol
实验4-D 1 100 1
实验4-E 2 100 1
实验4-F 4 100 1
实验结果分析
实验4-D:1个进程,结束运行后计算吞吐量和延迟。
实验4-E:2个进程,几乎同时结束运行,计算吞吐量和延迟。
实验4-F:4个进程,进程在逻辑CPU上轮流执行。
通过以上实验得出的结论:

单核CPU仅能同时运行一个进程。
多核CPU提高吞吐量的关键在于同时运行多个进程。
运行时间与执行时间
运行时间:进程从开始运行到结束所经过的时间。
执行时间:进程实际占用逻辑CPU的时长。
计算示例
使用time命令获取运行时间和执行时间示例:

bash
Copy
$ time taskset -c 0 ./sched 1 10000 10000
real表示运行时间,user和sys的和为执行时间。
在实验中,如果多个进程并行运行,执行时间将明显低于运行时间,因每个进程只能在有限的时间内使用CPU资源。

⚙️ 进程调度与优先级变更
进程和逻辑CPU的关系
“通过采集平常使用的程序的数据,可以看到它们各自的特征。”

运行进程示例
命令执行:
bash
Copy
$ taskset -c 0 python3 ./loop.py &
该命令在逻辑CPU 0上运行 loop.py 程序。
进程监控:
bash
Copy
$ ps -eo pid,comm,etime,time | grep python3
输出示例:
text
Copy
21304 python3 00:19 00:00:10
21306 python3 00:19 00:00:09
逻辑CPU数量与进程数量的关系
示例:
bash
Copy
$ taskset -c 0,4 python3 ./loop.py &
运行两个进程并分配给两个逻辑CPU。
执行时间分析
进程ID 执行时间 备注
21304 00:00:10 共享了逻辑CPU0
21306 00:00:09 共享了逻辑CPU0
“运行时间与执行时间一样,这是因为它独占一个逻辑CPU。”

变更进程优先级
nice() 系统调用
功能: 为特定进程指定优先级。
优先级范围: -19(最高)到 20(最低),默认值为0。
权限限制: 除了降低优先级,只有root用户可以提高优先级。
编写sched_nice程序
c
Copy
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

#define NLOOP_FOR_ESTIMATION 1000000000UL
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL

static inline long diff_usec(struct timespec before, struct timespec after) {
return ((after.tv_sec * NSECS_PER_SEC + after.tv_nsec) -
(before.tv_sec * NSECS_PER_SEC + before.tv_nsec));
}
程序运行示例
编译和运行:
bash
Copy
$ cc -o sched_nice sched_nice.c
$ taskset -c 0 ./sched_nice 100 1
CPU时间分配观察
进程ID 时间(ms) 进度 (%)
0 1 1
1 195 96
“优先级高的进程0比优先级低的进程1获得了更多的CPU时间。”

使用nice命令修改优先级
命令示例:

bash
Copy
$ nice -n 5 echo hello
Hello
sar命令输出分析:

bash
Copy
$ sar -P ALL 1 1
输出示例:
text
Copy
18:28:28 CPU %user %nice %system %iowait %steal %idle
18:28:28 all 0.25 12.52 0.00 0.00 0.00 87.23
结束运行的进程
命令:
bash
Copy
$ kill 17831
任务设置命令
taskset: 用于将程序限定在指定的逻辑CPU上运行。
sched_setaffinity(): 系统调用实现该功能。
🖥️ 内存管理
5.3 简单的内存分配
在进程创建后,如果需要申请更多内存,进程将向内核发出系统调用请求。内核根据请求量在可用内存中分配相应大小的内存,并返回该内存的起始地址。

内存分配的两种时机
在创建进程时
在创建完进程后,动态分配内存时
内存分配的问题
问题类型 描述
内存碎片化 不断获取与释放内存会导致可用内存分散,形成多个小块,无法满足大内存请求。
访问其他用途的内存区域 进程可通过内存地址访问内核或其他进程的内存,存在数据损毁风险。
难以执行多任务 多个进程同时运行时,内存地址的冲突可能导致程序无法正常运行。
内存碎片化示例
内存碎片化示例

如果可用内存分散在多个位置,进程无法申请到足够大的内存区域。

内存访问风险
内存访问风险示例

进程可以访问其他进程的内存,可能导致数据损毁或泄露。

多任务执行问题
如图5-10所示,多个进程之间的内存地址冲突会导致程序无法正常运行。

5.4 虚拟内存
为了解决上述问题,现代CPU引入了虚拟内存,Linux也利用这一功能。虚拟内存使得进程无法直接访问物理内存,而是通过虚拟地址进行间接访问。

虚拟地址与物理地址
虚拟地址:进程可见的地址
物理地址:系统上真实的内存地址
虚拟内存示意图

地址转换示例
当进程访问虚拟地址100时,实际上访问的是物理地址600。

5.5 页表
页表用于完成虚拟地址到物理地址的转换。虚拟内存中的内存管理以页为单位进行。

页表项
页表项记录着虚拟地址与物理地址的对应关系。页面大小取决于CPU架构,通常为4KB。

页表示意图

5.6 缺页中断
如果进程访问未分配物理内存的虚拟地址,将引发缺页中断,内核会处理该中断并可能强制结束进程。

5.7 为进程分配内存
创建进程时的内存分配
内核首先读取可执行文件及辅助信息,计算运行程序所需的内存大小,然后在物理内存中分配相应区域。

动态分配内存时
当进程请求更多内存时,内核会为其分配新的内存并创建相应的页表。

动态分配示意图

5.9 利用上层进行内存分配
C语言的malloc()函数用于获取内存,底层调用了mmap()函数。malloc()以字节为单位申请内存,而mmap()以页为单位。

5.10 解决问题
虚拟内存通过独立的虚拟地址空间解决了内存碎片化、内存访问风险及多任务执行的问题。每个进程都有独立的虚拟地址空间,无法访问其他进程的内存。

解决内存碎片化示意图
每个进程独立的虚拟地址空间示意图
虚拟内存不允许访问其他进程内存示意图

🖥️ 内存管理与虚拟内存
内核内存映射
“内核的内存区域被映射到了所有进程的虚拟地址空间中,但只有在CPU运行在内核模式下时才能访问,因此用户模式下的进程无法访问或损毁这部分内存。”

内核模式专用: 页表项上注有“内核模式专用”信息,表明该内存区域只能在内核模式下访问。
虚拟地址空间示意图
地址范围 物理内存 说明
0~100 0~100 内核可访问
100~200 100~200 进程A可访问
200~300 200~300 进程B可访问
300~400 500~600 进程C可访问
400~500 不可访问 无法访问
独立虚拟地址空间: 每个进程拥有独立的虚拟地址空间,使得程序可以在不干扰其他程序的情况下运行。

虚拟内存的应用
文件映射
“在访问文件时,Linux提供了将文件区域映射到虚拟地址空间的功能。”

mmap() 函数: 用于将文件内容读取到内存中,并将该内存区域映射到虚拟地址空间。
文件映射示意图
内存 虚拟地址空间 说明
进程使用的内存 文件副本 通过内存访问文件内容
外部存储器 写入 被访问区域写入外部存储器
文件映射实验
实验目标:
文件是否被映射到虚拟地址空间?
能否读取映射区域的文件内容?
能否向映射区域写入数据?
实验步骤
创建名为 testfile 的文件并写入内容。
编写程序实现以下功能:
显示进程的内存映射信息。
打开文件并使用 mmap() 映射文件。
显示内存映射信息并读取映射区域数据。
修改映射区域中的数据。
请求分页
“请求分页机制仅在进程首次访问页面时,才会为该页面分配物理内存。”

请求分页处理流程
在创建进程时,虚拟地址空间中的页面添加“已为进程分配该区域”的信息,但未分配物理内存。
当进程访问入口点时,触发缺页中断,内核为页面分配物理内存并更新页表。
请求分页示意图
虚拟地址空间 物理地址 状态
0~100 未分配物理内存 已分配给进程但尚未分配物理内存
100~200 已分配物理内存 进程已访问
请求分页实验
实验目标:
在获取内存后,虚拟内存使用量是否增加,但物理内存使用量是否不变?
在访问已获取的内存时,物理内存使用量是否会增加?
实验步骤
预先输出提示信息,等待用户按下 Enter 键。
获取 100 MB 的内存,并再次提示用户。
逐页访问已获取的内存,每访问 10 MB 输出提示信息。
示例代码:请求分页实验
c
Copy
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

#define BUFFER_SIZE (100 * 1024 * 1024)
#define PAGE_SIZE 4096

int main(void) {
char *p;
time_t t;
char *s;

t = time(NULL);
s = ctime(&t);
printf("%.*s: before allocation, please press Enter key\n", (int)(strlen(s) - 1), s);
getchar();

p = malloc(BUFFER_SIZE);
if (p == NULL) 
    err(EXIT_FAILURE, "malloc() failed");

t = time(NULL);
s = ctime(&t);
printf("%.*s: allocated %dMB, please press Enter key\n", (int)(strlen(s) - 1), s, BUFFER_SIZE / (1024 * 1024));
getchar();

for (int i = 0; i < BUFFER_SIZE; i += PAGE_SIZE) {
    p[i] = 0;
    int cycle = i / (BUFFER_SIZE / 10);
    if (cycle != 0 && i % (BUFFER_SIZE / 10) == 0) {
        t = time(NULL);
        s = ctime(&t);
        printf("%.*s: touched %dMB\n", (int)(strlen(s) - 1), s, i / (1024 * 1024));
        sleep(1);
    }
}

t = time(NULL);
s = ctime(&t);
printf("%.*s: touched %dMB, please press Enter key\n", (int)(strlen(s) - 1), s, BUFFER_SIZE / (1024 * 1024));
getchar();

exit(EXIT_SUCCESS);

}
实验结果: 实验时需在一个终端运行程序,在另一个终端使用 sar -r 命令收集系统内存信息。这样可以观察到请求分页的实际效果。
📊 内存管理
内存监控
在内存监控中,通过对多个时间点的内存使用量进行记录,可以得出以下结论:

时间点 物理内存使用量 (kbmemused) 每秒获取内存 (MB) 备注
开始访问前 基本不变 0 即使已分配内存区域,物理内存使用量几乎不变
开始访问后 每秒增加约10 10 访问内存后,内存使用量增加
访问结束后 不再变化 0 访问结束后,物理内存使用量稳定
进程结束后 回到初始状态 0 进程结束后,内存使用量回到初始
缺页中断监控
使用 sar -B 命令监控缺页中断的发生情况,结果如下:

时间点 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s
开始访问前 0.00 0.00 2.00 0.00 33.00
开始访问后 0.00 0.00 338.00 0.00 35.00
访问结束后 0.00 0.00 293.00 0.00 34.00
重要观察点
在访问内存的过程中,fault/s 字段值增加,表示缺页中断发生。
即使再次访问同一内存区域,不会再次引发缺页中断,因为物理内存在第一次访问时已完成分配。
虚拟内存与物理内存
虚拟内存不足与物理内存不足
虚拟内存不足是指进程已经使用完所有虚拟地址空间,而物理内存可能仍有剩余。示例如下:

虚拟内存不足的示例:

在虚拟地址空间为500字节时,若已使用完所有虚拟内存,即使物理内存有300字节可用,也会引发虚拟内存不足。

物理内存不足:

物理内存不足则是指系统的物理内存被耗尽,与虚拟内存的剩余无关。

写时复制(Copy on Write, CoW)
在创建进程时,使用 fork() 系统调用,不会立即复制父进程的所有内存数据,而是仅复制父进程的页表。具体过程如下:

父子进程共享内存:调用 fork() 后,父子进程共享相同的物理页面,写入权限被暂时禁用。
缺页中断处理:
当一方尝试写入时,缺页中断触发,内核开始处理。
内核复制页面并分配给尝试写入的进程,并更新页表项。
解除共享:一旦发生写时复制,父进程和子进程可以各自对新分配的页面进行读写操作。
实验观察
通过实验可以确认以下事项:

在调用 fork() 到开始写入的时间内,父子进程共享内存。
写入操作时会引发缺页中断。
代码示例:

c
Copy
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#define BUFFER_SIZE (100 * 1024 * 1024)
#define PAGE_SIZE 4096
#define COMMAND_SIZE 4096

static char *p;
static char command[COMMAND_SIZE];

static void child_fn(char *p) {
// 省略代码
}

static void parent_fn(void) {
wait(NULL);
exit(EXIT_SUCCESS);
}

int main(void) {
char *buf;
p = malloc(BUFFER_SIZE);
// 省略代码
}
通过这一系列的实验和监控,可以对内存管理中的请求分页机制有更深入的理解。

📦 进程内存管理
进程的内存使用情况
在讲解中,我们分析了一个程序的内存使用情况。以下是程序运行的关键点:

程序编译与运行:

bash
Copy
$ cc -o cow cow.c
$ ./cow
内存使用信息:

在调用 fork() 之前,内存状态如下:
text
Copy
*** free memory info before fork ***:
total used free shared buff/cache available
Mem: 32942008 1967716 21552784 298152 9421508 30031664
Swap: 0 0 0
子进程和父进程的内存使用情况:

在子进程开始写入数据后,内存使用量显著增加,表明内存共享已解除。
重要的内存管理点
观察点 说明
内存使用量增加 调用 fork() 后,父进程的内存使用量超过100 MB,但在子进程写入数据前,内存使用量仅增加几百KB。
缺页中断 当子进程写入数据后,缺页中断的次数增加,系统内存使用量增加了100 MB。
物理内存使用量的计算 父进程和子进程的物理内存使用量会重复计算,导致总值高于实际使用量。
交换分区 (Swap)
交换分区的定义
“交换分区是Linux系统的一种机制,在物理内存不足时,可以将部分内存页面暂时保存到外部存储器中,从而释放出可用内存。”

交换分区的工作原理
当物理内存不足时,内核会将部分页面保存到交换分区,称为“换出”。
当需要访问已换出的页面时,内核会将页面从交换分区重新加载到物理内存中,称为“换入”。
交换的示意图
在物理内存不足的情况下,需要使用更多的物理内存,以下是相关的内存管理过程:

操作 描述
缺页中断 进程访问未关联物理内存的虚拟地址,系统进行缺页中断处理。
换出 将物理内存中的某些页面保存到交换分区以释放内存。
换入 从交换分区将页面重新加载到物理内存以满足访问请求。
监控交换分区
可以通过运行以下命令查看系统交换分区的信息:

bash
Copy
$ swapon --show
交换分区的性能问题
系统抖动:当系统频繁发生交换处理时,可能导致性能下降,甚至引发系统崩溃。应避免在服务器上部署容易引发系统抖动的系统。
页表管理
单级页表与多级页表
在虚拟地址空间中,页表用于保存虚拟地址与物理地址的映射关系。考虑到内存使用的效率,使用多级页表结构能够有效节省内存。

结构类型 描述
单级页表 每个进程的页表占用大量内存,不适合大规模使用。
多级页表 通过分层结构,减少页表内存占用,适应更大的虚拟地址空间。
透明大页与标准大页
标准大页:通过使用更大的页面来减少页表的内存使用量,并提高 fork() 的执行速度。
透明大页:当多个连续的4 KB页面符合特定条件时,自动转换为一个大页。
监控页表内存使用
通过运行以下命令可查看页表所使用的物理内存量:

bash
Copy
$ sar -r ALL
关键字段解释
字段名称 描述
kbpgtbl 显示页表所使用的物理内存量。
kbswpused 显示交换分区的使用量,监控变动趋势。
总结
本节讲解了进程的内存管理、交换分区的使用、页表的结构及其监控方法。理解这些内容对于操作系统的内存管理至关重要,能够帮助我们更好地优化和管理系统资源。

💾 内存管理与存储层次
5.17 透明大页
“透明大页是一种内存管理机制,能够将多个小页(通常为4 KB)聚合成一个大页,以提高内存访问效率。”

启用与禁用透明大页
通过读取 /sys/kernel/mm/transparent_hugepage/enabled 文件,可以检查系统是否启用了透明大页。
在Ubuntu 16.04中,默认启用透明大页,显示为 always。
bash
Copy
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
若要禁用透明大页,可以将该文件写入 never。
bash
Copy
$ sudo su

echo never >/sys/kernel/mm/transparent_hugepage/enabled

设置为 madvise 表示仅对通过 madvise() 系统调用设置的内存区域启用透明大页机制。
6.1 高速缓存
“高速缓存是为了减少CPU与内存之间的速度差异而设计的一种存储机制。”

存储器层次结构
存储器的层次结构如图所示:
存储器 容量 价格 访问速度
寄存器 小 高 快
高速缓存 大 低 快
内存 更大 更低 中
外部存储器 最大 最低 慢
计算机运作流程
根据指令,将数据从内存读取到寄存器。
基于寄存器上的数据进行运算。
将运算结果写入内存。
高速缓存的运作方式
从内存读取数据时,数据首先被送往高速缓存,再被送往寄存器。
假设缓存块大小为10字节,高速缓存容量为50字节,寄存器有两个(R0与R1)。当读取内存地址300上的数据时,数据存入R0。
数据读取示例
在读取后,再次需要读取相同数据时,将直接从高速缓存读取,避免从内存中读取。
6.2 高速缓存不足时
“当高速缓存满时,新的数据读取需要先销毁现有的缓存块。”

数据读取流程
如果访问高速缓存中尚不存在的数据,需要销毁一个现有的缓存块。
将新数据复制到空出来的缓存块中。
脏数据的处理
如果需要销毁的缓存块是脏的,数据将在被销毁前同步到内存中。
6.3 多级缓存
“现代CPU一般采用分层结构的高速缓存,包括L1、L2、L3缓存。”

缓存层级
层级 类型 共享逻辑CPU 容量 (KB) 缓存块大小 (字节)
L1d 数据 不共享 32 64
L1i 指令 不共享 64 64
L2 数据与指令 不共享 512 64
L3 数据与指令 共享逻辑CPU 0~3 8192 64
查看缓存信息
缓存信息可通过 /sys/devices/system/cpu/cpu0/cache/index0/ 目录下的文件查看。
6.4 关于高速缓存的实验
实验目标
测试高速缓存对访问时间的影响。
编写程序获取指定内存量并执行顺序访问。
示例代码
c
Copy
#include <unistd.h>
#include <sys/mman.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#define CACHE_LINE_SIZE 64
#define NLOOP (41024UL1024*1024)
#define NSECS_PER_SEC 1000000000UL
运行实验
编译并运行程序,记录不同内存量的访问时间。
6.5 访问局部性
“访问局部性是指程序在某一时间点倾向于访问相近的内存数据。”

类型
时间局部性:近期被访问的数据在不久后可能再次被访问。
空间局部性:近期访问的数据附近的数据也可能被访问。
6.7 转译后备缓冲区
“转译后备缓冲区(TLB)用于提高虚拟地址到物理地址转换的速度。”

TLB的作用
在访问虚拟地址时,TLB保存虚拟地址和物理地址的转换表,从而提高访问速度。
6.8 页面缓存
“页面缓存用于填补CPU访问内存与外部存储器之间的速度差异。”

页面缓存的运作流程
当进程读取文件数据时,数据首先复制到页面缓存。
再将数据从页面缓存复制到进程的内存中。
脏页处理
写入操作首先在页面缓存中执行,脏页在后台处理后会同步到外部存储器。
💾 脏页与内存管理
脏页的概念
“脏页是指在内存中的页,其内容与外部存储器中的对应页内容不一致。”

系统负载与脏页
脏页回写:当系统内存不足时,大量脏页需要被回写到外部存储器,这会导致系统性能显著下降。
同步写入
强制断电的影响
在页面缓存中存在脏页时,如果发生强制断电,脏页将会丢失。
使用 O_SYNC 标志
“为了避免强制断电导致的数据丢失,可以在使用 open() 系统调用打开文件时,将 flag 参数设定为 O_SYNC。”

缓冲区缓存
“缓冲区缓存是与页面缓存类似的机制,用于在直接访问外部存储器时使用的区域。”

页面缓存与缓冲区缓存的对比
特点 页面缓存 缓冲区缓存
目的 将外部存储器中的数据放入内存 直接访问外部存储器
使用场景 文件系统访问 设备文件访问
读取文件的实验
实验目的
验证页面缓存的效果,通过对同一文件进行两次读取操作,比较两次所需时间。
实验步骤
创建测试文件 testfile,大小为 1 GB:

bash
Copy
dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K
记录系统页面缓存使用情况:

bash
Copy
free
第一次读取文件:

bash
Copy
time cat testfile >/dev/null
记录页面缓存使用情况:

bash
Copy
free
第二次读取文件:

bash
Copy
time cat testfile >/dev/null
再次记录页面缓存使用情况:

bash
Copy
free
实验结果
第一次读取时间约为 2 秒,主要等待外部存储器的读取。
第二次读取速度约为第一次的 20 倍,因为此时数据已在页面缓存中。
系统统计信息收集
统计信息
页面调入次数:从外部存储器读取到页面缓存的次数。
页面调出次数:从页面缓存写入到外部存储器的次数。
外部存储器的 I/O 吞吐量。
脚本示例
bash
Copy
#!/bin/bash
rm -f testfile
echo “(date):startfilecreation"ddif=/dev/zeroof=testfileoflag=directbs=1Mcount=1Kecho"(date): start file creation" dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K echo "(date):startfilecreation"ddif=/dev/zeroof=testfileoflag=directbs=1Mcount=1Kecho"(date): end file creation”
echo “(date):sleep3seconds"sleep3echo"(date): sleep 3 seconds" sleep 3 echo "(date):sleep3seconds"sleep3echo"(date): start 1st read”
cat testfile >/dev/null
echo “(date):end1stread"echo"(date): end 1st read" echo "(date):end1stread"echo"(date): sleep 3 seconds”
sleep 3
echo “(date):start2ndread"cattestfile>/dev/nullecho"(date): start 2nd read" cat testfile >/dev/null echo "(date):start2ndread"cattestfile>/dev/nullecho"(date): end 2nd read”
rm -f testfile
写入文件的实验
写入处理验证
测试在禁用页面缓存的直写模式下写入文件的时间:

bash
Copy
time dd if=/dev/zero of=testfile oflag=direct bs=1M count=1K
测试正常使用页面缓存时的写入时间:

bash
Copy
time dd if=/dev/zero of=testfile bs=1M count=1K
实验结果
使用页面缓存时的写入速度接近原来的 9 倍,显示了页面缓存的优势。
统计信息收集
使用 sar -B 命令收集写入期间的统计信息。
🖥️ Linux 系统监控与调优

  1. sar 命令输出解析
    页缓存统计
    在使用 sar -B 命令时,输出结果显示了以下统计数据:

时间 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
14:11:33 0.00 0.00 2.00 0.00 1.00 0.00 0.00 0.00 0.00
14:11:34 0.00 0.00 0.00 0.00 2.00 0.00 0.00 0.00 0.00
14:11:35 0.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 0.00
14:11:36 0.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 0.00
14:11:37 0.00 0.00 1027.00 0.00 263477.00 0.00 0.00 0.00 0.00
14:11:38 0.00 0.00 0.00 0.00 4.00 0.00 0.00 0.00 0.00
从上述数据可以得知,在写入期间(14:11:37 与 14:11:38),并没有发生页面调出(page out)。

  1. I/O 吞吐量统计
    使用 sar -d -p 命令获取 I/O 吞吐量数据:

时间 DEV tps rd_sec/s wr_sec/s argrq-sz avgqu-sz await svctm %util
14:17:42 sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:17:44 sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:17:45 sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:17:46 sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:17:47 sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:17:48 sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:17:49 sda 1.00 0.00 16.00 16.00 0.00 0.00 0.00 0.00
通过上面的信息可以得知,在写入期间,保存着根文件系统的设备上没有发生 I/O 处理(14:17:48 与 14:17:49)。

  1. 调优参数
    页面缓存调优
    脏页回写周期: 使用 vm.dirty_writeback_centisecs 参数来控制脏页的回写周期,默认值为500厘秒(即5秒)。
    bash
    Copy
    $ sysctl vm.dirty_writeback_centisecs
    vm.dirty_writeback_centisecs = 500
    脏页背景比例: 通过 vm.dirty_background_ratio 参数设置的默认值为10%。
    bash
    Copy
    $ sysctl vm.dirty_background_ratio
    vm.dirty_background_ratio = 10
    脏页比例: 当脏页的内存占比超过 vm.dirty_ratio 参数指定的20%时,将阻塞进程的写入。
    bash
    Copy
    $ sysctl vm.dirty_ratio
    vm.dirty_ratio = 20
    清理页面缓存
    通过写入值3到 /proc/sys/vm/drop_caches 文件来清理页面缓存。

bash
Copy

echo 3 > /proc/sys/vm/drop_caches

  1. 超线程
    超线程工作原理
    超线程技术允许一个物理 CPU 核心同时作为两个逻辑 CPU 运行,目的是有效利用等待数据传输时的 CPU 资源浪费。

性能影响: 启用超线程后,吞吐量的提升通常在20%到30%之间,具体效果依赖于进程的行为。
实验结果
禁用超线程: 逻辑 CPU 数量为8,编译时间约为93.7秒。
bash
Copy
$ time make -j8 >/dev/null 2>&1
启用超线程: 逻辑 CPU 数量为16,编译时间约为73.3秒,提升约22%。
bash
Copy
$ time make -j16 >/dev/null 2>&1
在启用超线程后,可以看到编译效率确实提升。

  1. Linux 文件系统
    文件系统的作用
    文件系统通过管理数据的名称、位置和大小,让用户无需记住数据的存放位置。

常用系统调用
创建与删除文件: create()、unlink()
打开与关闭文件: open()、close()
读写文件: read()、write()
文件移动: lseek()
特殊处理: ioctl()
文件系统结构
文件系统呈现树状结构,各种文件系统可以共存于 Linux 上,例如 ext4、XFS、Btrfs 等。

操作 系统调用
创建文件 create()
删除文件 unlink()
打开文件 open()
关闭文件 close()
读取数据 read()
写入数据 write()
移动文件 lseek()
特殊处理 ioctl()
通过以上系统调用,用户可以高效地操作文件系统。

📁 文件系统
7.2 数据与元数据
文件系统上存在两种数据类型,分别是数据与元数据。

数据:用户创建的文档、图片、视频和程序等数据内容。
元数据:文件的名称、文件在外部存储器中的位置和文件大小等辅助信息。
元数据的种类
元数据类型 描述
种类 用于判断文件是保存数据的普通文件,还是目录或其他类型的文件。
时间信息 包括文件的创建时间、最后一次访问的时间,以及最后一次修改的时间。
权限信息 表明该文件允许哪些用户访问。
通过 df 命令得到的文件系统所用的存储空间,不仅包括用户创建的文件所占用的空间,还包括所有元数据所占用的空间。

bash
Copy
$ df
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sdc1 95990964 61104 91030668 1% /mnt
创建目录的示例
bash
Copy
$ sudo su

cd /mnt

for ((i=0;i<100000;i++)) ; do mkdir $i ; done

创建目录(目录的数据类型为元数据)。

7.3 容量限制
当系统被同时用于多种用途时,如果不限制文件系统的容量,有可能导致其他用途所需的存储容量不足。特别是在 root 权限下运行的进程,当系统管理相关的处理所需的容量不足时,系统将无法正常运行。

为避免这种情况,可以通过磁盘配额(quota)功能来限制不同用途的文件系统容量。

磁盘配额的类型
配额类型 描述
用户配额 限制作为文件所有者的用户的可用容量。
目录配额 限制特定目录的可用容量。
子卷配额 限制文件系统内名为子卷的单元的可用容量。
7.4 文件系统不一致
文件系统的内容可能不一致,例如在对外部存储器读写数据时被强制切断电源的情况。

原子操作
移动一个目录的操作称为原子操作,这个过程是不可分割的。若在操作中断时,可能导致文件系统变成不一致的状态。

防止文件系统不一致的技术
日志(journaling)
写时复制(Copy-on-Write)
7.5 日志
日志功能在文件系统中提供了一个名为日志区域的特殊区域。文件系统更新的步骤如下:

将更新所需的原子操作的概要暂时写入日志区域。
基于日志区域中的内容进行文件系统的更新。
日志的作用
如果在更新日志记录的过程中被强制切断电源,只需丢弃日志区域的数据,数据本身依旧是开始处理前的状态。

7.6 写时复制
在写时复制的文件系统上,每次更新处理都会把数据写入不同的位置。这样,即使在执行过程中发生中断,系统依旧可以通过删除未处理的数据来避免不一致的情况。

7.9 文件的种类
在Linux中,文件类型主要有:

普通文件:保存用户数据。
目录:保存其他文件。
设备文件:将硬件系统的设备以文件形式呈现。
设备文件的分类
设备类型 描述
字符设备 无法自行确定读取数据的位置。
块设备 能进行随机访问,支持读写操作。
示例:字符设备与块设备
字符设备的示例
终端
键盘
鼠标
块设备的示例
HDD
SSD
通过块设备的设备文件,可以直接操作外部存储器的数据,执行分区表更新、数据备份、创建文件系统等操作。

📁 文件系统与块设备的关系
文件系统与块设备的概念
“隐藏在文件系统下的只是保存在外部存储器中的数据而已。”

在本次实验中,我们探讨文件系统与块设备之间的关系。通过块设备直接更改文件系统的内容,但需谨记以下几点:

实验中替换文件内容的方法 仅适用于当前版本的ext4文件系统,不能保证适用于其他文件系统或未来版本。
随意通过块设备更改文件系统的内容是非常危险的,建议仅在测试用的文件系统上执行此操作。
不要在文件系统已挂载的状态下 访问保存该文件系统的块设备,以免破坏文件系统的一致性和数据。
各种文件系统
常见文件系统
我们已经介绍了ext4、XFS 和 Btrfs 这三种文件系统。除此之外,Linux 上还有多种文件系统,以下是部分介绍:

基于内存的文件系统
tmpfs 是一种创建于内存上的文件系统,数据在断电后会消失,但访问速度更快。
tmpfs 通常用于 /tmp 和 /var/run,这些文件内容在下次启动时无需保存。
作用 描述
tmpfs 创建于内存的文件系统,提供更快的访问速度
挂载选项 在挂载时通过 size 选项指定最大容量
网络文件系统
网络文件系统允许通过网络访问远程主机上的文件,常见的有:
cifs:用于访问Windows主机上的文件。
nfs:用于访问Linux等类UNIX系统的主机上的文件。
虚拟文件系统
procfs
procfs 用于获取系统上所有进程的信息,通常挂载在 /proc 目录下。
通过访问 /proc/pid/ 目录下的文件,可以获取进程的详细信息。
procfs 相关信息
文件路径 描述
/proc/cpuinfo 系统上CPU的相关信息
/proc/meminfo 系统内存的相关信息
/proc/diskstat 外部存储器的相关信息
sysfs
sysfs 是专门用于存放非进程信息的文件系统,通常挂载在 /sys 目录下。
cgroupfs
cgroup 用于限制单个进程或进程组的资源使用量,cgroupfs 通常挂载在 /sys/fs/cgroup 目录下。
资源类型 描述
CPU 设置使用比例,例如限制为50%
内存 限制物理内存使用量,例如最多1 GB
Btrfs 文件系统
Btrfs 是一种新兴的文件系统,提供多种先进功能:

多物理卷:允许在存储池上创建可挂载的区域(子卷),支持添加、删除、替换外部存储器。
快照:以子卷为单位创建快照,速度更快且空间成本低。
RAID 支持:支持多种RAID级别,如RAID 0、RAID 1、RAID 5等。
数据损坏检测与恢复:通过校验和机制检测数据完整性,能够自动修复损坏的数据。
Btrfs 的优势
特性 描述
快照 创建快照速度快且不需复制数据
RAID 在文件系统级别配置RAID,提供数据冗余
数据恢复 具备检测和自动修复损坏数据的能力
结论
本章介绍了文件系统与块设备之间的关系,以及多种文件系统的特点和应用。了解这些知识对于掌握文件系统的设计与管理至关重要。

📂 外部存储器实验与性能分析

  1. 实验程序的基本结构
    实验程序的核心部分包括对命令行参数的解析以及对外部存储器的读写操作。以下是关键步骤:

1.1 命令行参数解析
帮助信息开关:

使用 help 变量判断帮助信息是否开启。
写入/读取标志:

使用 write_flag 变量判断是读取(r)还是写入(w)。
访问模式:

使用 random 变量判断访问模式是顺序(seq)还是随机(rand)。
1.2 关键参数的设置
块大小:通过命令行参数设置 block_size,并验证其有效性。
内存分配:使用 malloc 为 offset 数组分配内存,并确保分配成功。
2. 直接I/O的使用
2.1 O_DIRECT 标志
在使用 open() 函数时,添加 O_DIRECT 标志以启用直接I/O。此标志的作用是:

禁用内核的I/O 支援功能,直接与硬件交互,减少缓存干扰。
2.2 扇区大小获取
通过 ioctl() 函数获取外部存储器的扇区大小,这对后续的内存分配和读写操作至关重要。

  1. 内存对齐与缓冲区管理
    3.1 使用 posix_memalign
    buf 变量用于保存外部存储器传输过来的数据,采用 posix_memalign() 函数进行内存分配,确保缓冲区的起始地址与扇区大小对齐。

3.2 I/O 操作
在循环中执行读写操作,使用 lseek() 定位到正确的块,然后根据 write_flag 决定是执行写入还是读取。

  1. 性能实验与结果分析
    4.1 顺序访问实验
    使用命令行参数设置进行数据采集,实验结果显示:

单次 I/O 请求量 (KB) 读取性能 (MB/s) 写入性能 (MB/s)
4 0.695 0.595
8 0.916 0.695
16 1.311 0.537
32 1.972 0.509
随着单次I/O请求量的增大,吞吐量都有所提升,但在达到1 MB后,性能趋于平稳。

4.2 随机访问实验
随机访问的性能普遍低于顺序访问,尤其在小的I/O请求量下,性能差距更加明显。

访问类型 吞吐量 (MB/s)
顺序访问 较高
随机访问 较低
5. 通用块层与I/O调度
5.1 通用块层
在 Linux 中,HDD 和 SSD 统一归为块设备,可以通过设备文件直接访问。

5.2 I/O 调度器功能
合并请求:将连续扇区的多个I/O请求合并成一个请求。
排序请求:根据扇区序列号对不连续的请求进行排序。
5.3 预读机制
利用空间局部性,预读机制在程序访问外部存储器时,提前读取可能后续需要的数据,提高顺序访问的性能。

  1. 实验结果总结
    启用I/O 支援功能后,读取与写入性能均显著提升,特别是读取性能得益于预读机制的优化。在具体实验中,能够通过 iostat 命令监控I/O性能,得出更为直观的数据。

💾 外部存储器
I/O 处理与性能分析
I/O 统计信息
使用命令 iostat -x -p sdb 1 可以获取外部存储器的 I/O 统计信息,包括以下字段:

字段 含义
rrqm/s 每秒合并的读请求数
wrqm/s 每秒合并的写请求数
r/s 每秒读取请求数
w/s 每秒写入请求数
rkB/s 每秒读取的千字节数
wkB/s 每秒写入的千字节数
avgrq-sz 平均请求大小
avgqu-sz 平均队列长度
await 每个请求的平均等待时间
r_await 每个读取请求的平均等待时间
w_await 每个写入请求的平均等待时间
svctm 每个请求的平均服务时间
%util 设备的利用率
随机与顺序访问性能对比
在外部存储器的 I/O 处理过程中,我们可以观察到以下现象:

在随机访问中,随着 I/O 请求量的增加,性能逐渐逼近顺序访问的性能,但依然无法达到同级别。
读取性能的吞吐量与写入性能的吞吐量均随 I/O 请求量的增加而增加。
实验结果示例
I/O 请求量(KB) 吞吐量(MB/s) 读取性能 写入性能
2 0 150 100
4 0 100 75
6 0 50 50
图8-18 显示了 HDD 的读取性能,图8-19 显示了写入性能。可以看到,在较小的 I/O 请求量下,吞吐量明显低于顺序访问。

SSD 与 HDD 的比较
SSD 与 HDD 在性能上有显著差异,主要体现在以下几个方面:

SSD 没有机械移动部件,因此在随机访问时性能显著优于 HDD。
虽然 SSD 的价格相对较高,但由于其性能优势,两者将在未来继续共存。
SSD 实验结果
I/O 请求量(KB) HDD 吞吐量(MB/s) SSD 吞吐量(MB/s)
2 100 400
4 200 500
6 300 600
在图8-24 和图8-25 中,可以看出无论是读取还是写入,SSD 的速度均比 HDD 快得多。

启用与禁用 I/O 支援功能的影响
在实验中禁用 I/O 支援功能时,SSD 的性能表现如下:

对于较小的 I/O 请求量,性能提升明显。
在较大的 I/O 请求量下,启用 I/O 支援功能可能导致性能下降,尤其是在写入操作中。
性能比值分析
图8-40 显示了启用 I/O 支援功能时的吞吐量与禁用时的比值,能够清晰地看出在不同 I/O 请求量下的性能差异。

小 I/O 请求量: 启用 I/O 支援功能时性能提升。
大 I/O 请求量: 启用 I/O 支援功能可能导致性能下降。
总结
通过以上实验和数据分析,可以得出以下结论:

随机访问性能在 I/O 请求量较小时优于顺序访问,但随着请求量增加,两者性能差距逐渐减小。
SSD 在随机访问时性能明显优于 HDD,但在成本上仍存在劣势。
I/O 支援功能的启用与否对性能的影响在不同情境下表现出不同的效果,需根据实际需求选择。
📂 I/O 支援功能与性能影响
I/O 支援功能的性能影响
“支援功能时的性能变得比禁用时更差了。这是因为,SSD 无法忽略I/O 调度器积攒I/O 请求所产生的系统开销,以及排序处理的效果不怎么理想。”

性能下降原因
I/O 调度器:在支援功能开启的情况下,I/O 调度器会积攒I/O 请求,导致系统开销增加。
排序处理:在某些情况下,排序处理的效果不理想,进一步导致性能下降。
I/O 支援功能的优化策略
通过本章中的实验,可以总结出以下优化访问的建议:

优化策略 说明
数据存放位置 尽量将文件中的数据存放在连续的或者相近的区域。
请求的汇集 把针对连续区域的访问请求汇集到一次访问请求中。
顺序访问 对于文件,尽量以顺序访问的方式访问尽可能大的数据量。
章节总结
“借助内核提供的I/O 支援功能,即便用户不太了解HDD与SSD的特性,也能在一定程度上对访问进行优化。但是,并非什么时候都能最大限度地发挥出外部存储器的性能。”

外部存储器性能:并非在所有情况下,外部存储器的性能都能得到最大化的发挥,特别是在SSD中,I/O 调度器可能会导致性能下降。
参考资料推荐
以下是一些推荐的参考资料,以帮助更深入地理解计算机系统:

书籍名称 说明
Computer Organization and Design 经典名著,介绍计算机系统的硬件架构。
“What Every Programmer Should Know About Memory” 全方位说明内存的一篇文章。
《Linux 程序设计(第2版)》 使用C语言简明扼要解释Linux编程。
Systems Performance: Enterprise and the Cloud 系统阐述Linux与Solaris的性能测试。
Linux Kernel Development 通过源代码介绍Linux内核。
读者建议
“大家没必要读完所有推荐的文献,可以只挑感兴趣的部分阅读,这是防止在阅读时产生厌倦情绪的诀窍。”

灵活运用:建议读者结合本书学到的知识,静下心来阅读推荐文献,以更好地理解计算机系统的世界。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值