简介
之前提到 Linux 系统不止包含内核,还包括用户模式运行的程序。本章主要讲述用户模式实现的功能。
这张图形象的说明了进程与 OS 的关系。应用程序可以直接通过系统调用访问内核,也可以通过第三方或者 OS 提供的库来操作。本章逐次讲解系统调用、OS 提供的库的内容。
1. 系统调用
常见的系统调用主要有几类:进程控制(创建/删除)、内存管理(分配和释放)、进程间通信、网络管理、文件系统操作、文件操作(访问设备)等等。
CPU 的模式切换
系统调用过程涉及 CPU 模式的切换,需要执行特殊的 CPU 指令。进程在用户模式下,发起系统调用,CPU 实际会发起名为中段的事件, CPU 此时从用户模式切换到内核模式进行处理,内核完成处理后,重新回到用户模式,继续运行进程。用户进程无法绕开系统调用切换 CPU 运行模式(否则就失去意义了)。
发生系统调用时的情形
可以通过 strace 命令查看运行过程发生的系统调用。
# -T 系统调用开销,微秒级精度采集
# -tt 展示发生的时刻
strace -T -tt -o xx.log ./exe
实验
本章列举的实验可以看到 C 程序 hello world 总共执行了 34 个系统调用,而 Python 程序发起了 705 个。可见 Python 解析器的开销。
除了 strace,我们还可以通过 sar 命令查看进程在用户模式与内核模式下运行的时间比例。
# 每秒采集 1 次
# %user 字段和 %nice 字段的值相加即用户模式运行的时间比例,内核模式为 %system 字段。其它的如 %idle 字段后续章节会介绍
sar -P ALL 1
# 第 4 个参数 1 表示采集次数
sar -P ALL 1 1
作者构造了 2 个程序,1 个是 for 死循环,1 个是 for 循环 getppid(),来实验,可以看到前者 %user 几乎跑满,后者 %system 比例则很高。借此展示 CPU 不同模式下的运行时间比例。
## 2. 系统调用的包装函数
系统调用无法直接通过高级编程语言发起,只能通过与系统架构紧密相连的汇编语言代码来发起。Linux 提供库函数来方便用户编写程序。
类似如下:
0x6e 是 getppid 系统调用编号
mov $0x6e.%eax
通过 syscall 发起系统调用,并切换到内核模式
syscall
假设 OS 没有提供包装函数,各个应用程序就必须手写对应系统的汇编代码来发起系统调用,遇到实现跨平台应用的场景,就得为每个系统架构都实现一遍,复杂度很高。如下图所示:
正因为如此,Linux 提供了包装函数来帮助简化用户程序。
3. C 标准库
那这些包装函数在哪?我们知道 C 语言拥有 ISO 组织定义的标准库,Linux 提供了这些 C 标准库,通常会以GNU项目提供的glibc作为C标准库使用。用C语言编写的几乎所有程序都依赖于glibc库。
glibc 除了会包含前面提到的包装系统调用的函数,还提供了 POSIX 定义的函数。POSIX 定义了基于 Unix 的 OS 应该具备的各种功能。
在 Linux,可以通过 ldd 查看程序所依赖的库。用 ldd 查看 echo 可以看到依赖 glibc,几乎所有程序都会依赖。甚至 Python 都用到 glibc,可以看出在 OS 层面,C 语言依然发挥了很大作用,C 标准库不可或缺。
当然除了 C 标准库,Linux 还提供了诸如 C++ 等各种编程语言的标准库,以及绝大部分程序会依赖的库。
4. OS 提供的程序
除了提供库,OS 在用户模式还提供了各类程序。
本书后续章节会进行介绍。