2.1 系统调用
如前所述,进程在执行创建进程、操控硬件等依赖于内核的处理时,必须通过系统调用向内核发起请求。系统调用的种类如下。
- 进程控制(创建和删除)
- 内存管理(分配和释放)
- 进程间通信
- 网络管理
- 文件系统操作
- 文件操作(访问设备)
关于这些系统调用,我们接下来会根据需要进行说明。
● CPU 的模式切换
系统调用需要通过执行特殊的 CPU 命令来发起。通常进程运行在用户模式下,当通过系统调用向内核发送请求时,CPU 会发生名为中断的事件。这时,CPU 将从用户模式切换到内核模式,然后根据请求内容进行相应的处理。当内核处理完所有系统调用后,将重新回到用户模式,继续运行进程(图 2-2)。
图 2-2 CPU 的模式切换
内核在开始进行处理时验证来自进程的请求是否合理(例如,请求的内存量是否大于系统所拥有的内存量等)。如果请求不合理,系统调用将执行失败。
需要注意的是,并不存在用户进程绕过系统调用而直接切换 CPU 运行模式的方法(假如有,内核就失去存在的意义了)。
● 发起系统调用时的情形
如果想了解进程究竟发起了哪些系统调用,可以通过 strace
命令对进程进行追踪。例如,通过 strace
命令运行一个输出消息的程序,也就是大家常说的 hello world
程序(代码清单 2-1)。
代码清单 2-1 hello world 程序(hello.c)
#include <stdio.h>
int main(void)
{
puts("hello world");
return 0;
}
首先,不使用 strace
命令,尝试编译并运行一遍。
$ cc -o hello hello.c
$ ./hello
hello world
$
能在命令行中输出 hello world
就可以。接下来,通过 strace
命令来看看这个程序会发起哪些系统调用。此外,为了防止 strace
命令输出的数据与程序本身的输出混杂在一起,在使用 strace
命令时,我们加上 -o
选项,令其输出保存到指定的文件内。
$ strace -o hello.log ./hello
hello world
$
程序和上一次运行时一样,输出消息后就结束运行了。接下来,打开 hello.log 文件,看看 strace
命令的运行结果1。
1运行结果中的路径等因实际运行环境而不同。
$ cat hello.log
hello"], [/* 28 vars */]) = 0
brk(NULL) = 0x917000
access("/etc/ld.so.nohwcap". F_OK) = -1 ENOENT ↵
(No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE| ↵
MAP_ANONYMOUS, -1, 0) = 0x7f3ff46c2000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT ↵
(No such file or directory)
...
brk(NULL) = 0x917000
brk(0x938000) = 0x938000
write(1, "hello world\n", 12) = 12 ←①
exit_group(0) = ?
+++ exited with 0 +++
$
strace
的运行结果中的每一行对应一个系统调用。虽然输出的内容很多,但现在只需关注①指向的这一行。通过这一行的内容可以了解到,进程通过负责向画面或文件等输出数据的 write()
系统调用,在画面上输出了 hello world\n
这一字符串。
在笔者的计算机中,该进程总共发起了 31 个系统调用。这些系统调用大多是由在 main()
函数之前或之后执行的程序的开始处理和终止处理(OS 提供的功能的一部分)发起的,无须特别关注。
虽然测试用的 hello world
程序是用 C 语言编写的,但无论使用什么编程语言,都必须通过系统调用向内核发起请求。接下来让我们确认一下。代码清单 2-2 所示为用 Python 编写的 hello world
程序。
代码清单 2-2 用 Python 编写的 hello world 程序(hello.py)
print("hello world")
我们通过 strace
命令来运行 hello.py 程序。
$ strace -o hello.py.log python3 ./hello.py
hello world
$
然后查看追踪到的信息。
$ cat hello.py.log
execve("usr/bin/python3", ["python3", "./hello.py"], ↵
]) = 0
brk(NULL) = 0x2120000
access("/etc/ld.so.nohwcap". F_OK) = -1 ENOENT ↵
(No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ ↵
ANONYMOUS, -1, 0) = 0x7f6a9a36d000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT ↵
(No such file or directory)
...
close(3) = 0
write(1, "hello world\n", 12) = 12 ←②
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, ↵
0x7f6a99f3e390}, {0x63f1d0, [], SA_RESTORER, ↵
0x7f6a99f3e390}, 8) = 0
exit_group(0) = ?
+++ exited with 0 +++
这次同样输出了大量内容,但现在只需关注②指向的这一行。可以发现,与 C 语言编写的 hello world
程序一样,本程序同样发起了 write()
这个系统调用。这种情况不仅存在于 hello world
这样简单的程序中,也存在于其他复杂的程序中。
需要指出的是,在 hello.py.log 中,除了 write()
,其他都是由 Python 解析器的初始化处理和终止处理所发起的系统调用。最终共发起了 705 个系统调用2,这比起用 C 语言进行实验时要多得多,不过我们无须关注这部分内容。
2关于各个系统调用的作用,可以利用 man
命令,通过 man 2 write
这样的指令来查询。
请各位读者务必通过 strace
追踪一下各自的程序,看看都发起了哪些系统调用,这是一件很有趣的事情(请注意,如果对运行时间较长的软件使用该命令,将出现大量的输出结果)。
● 实验
sar
命令用于获取进程分别在用户模式与内核模式下运行的时间比例。我们通过每秒3采集一次数据,来看看每个 CPU 核心到底在运行什么。
3可以在 sar
命令的第3 个参数中输入 1
来指定周期。
$ `sar` -P ALL 1
( 略 )
16:29:52 CPU %user %nice %system %iowait %steal %idle
16:29:53 all 0.88 0.00 0.00 0.00 0.00 99.12
16:29:53 0 2.00 0.00 1.00 0.00 0.00 97.00
16:29:53 1 1.00 0.00 0.00 0.00 0.00 99.00
16:29:53 2 0.00 0.00 0.00 0.00 0.00 100.00
16:29:53 3 2.00 0.00 0.00 0.00 0.00 98.00
16:29:53 4 0.00 0.00 0.00 0.00 0.00 100.00
16:29:53 5 1.98 0.00 0.00 0.00 0.00 98.02
16:29:53 6 0.00 0.00 0.00 0.00 0.00 100.00
16:29:53 7 0.99 0.00 0.00 0.00 0.00 99.01
16:29:53 CPU %user %nice %system %iowait %steal %idle
16:29:54 all 0.75 0.00 0.25 0.12 0.00 98.88
16:29:54 0 2.97 0.00 0.00 0.00 0.00 97.03
16:29:54 1 0.99 0.00 0.99 0.00 0.00 98.02
16:29:54 2 0.00 0.00 0.00 0.00 0.00 100.00
16:29:54 3 0.00 0.00 0.00 0.00 0.00 100.00
16:29:54 4 0.00 0.00 0.00 1.00 0.00 99.00
16:29:54 5 1.00 0.00 0.00 0.00 0.00 99.00
16:29:54 6 0.00 0.00 0.00 0.00 0.00 100.00
16:29:54 7 1.00 0.00 0.00 0.00 0.00 99.00
( 略 )
如果在运行过程中按下 Ctrl+C,则 sar
命令会结束运行,并输出已采集的所有数据的平均值。
( 略 )
Average: CPU %user %nice %system %iowait %steal %idle
Average: all 0.71 0.00 0.08 0.04 0.00 99.17
Average: 0 1.66 0.00 0.33 0.00 0.00 98.01
Average: 1 1.00 0.00 0.33 0.00 0.00 98.67
Average: 2 0.33 0.00 0.00 0.00 0.00 99.67
Average: 3 0.67 0.00 0.00 0.00 0.00 99.33
Average: 4 0.00 0.00 0.00 0.33 0.00 99.67
Average: 5 1.00 0.00 0.00 0.00 0.00 99.00
Average: 6 0.00 0.00 0.00 0.00 0.00 100.00
Average: 7 0.99 0.00 0.00 0.00 0.00 99.01
$
在每一行中,从 %user
字段到 %idle
字段表示在 CPU 核心上运行的处理的类型。同一行内全部字段的值的总和是 100%。通过上面展示的数据可以看出,一行数据对应一个 CPU 核心。这里输出的是笔者的计算机上搭载的 8 个 CPU 核心的数据(CPU
字段中值为 all
的那一行数据是全部 CPU 核心的平均值)。
将 %user
字段与 %nice
字段的值相加得到的值是进程在用户模式下运行的时间比例(第4章将说明 %user
与 %nice
的区别),而 CPU 核心在内核模式下执行系统调用等处理所占的时间比例可以通过 %system
字段得到。在采集数据时,所有 CPU 核心的 %idle
字段的值都几乎接近 100%。这里的 %idle
指的是 CPU 核心完全没有运行任何处理时的空闲(idle)状态(详见第4章)。关于剩余的字段,我们将在以后用到时再说明。
另外,也可以通过 sar
命令的第4 个参数来指定采集信息的次数,如下所示4。
4在本例中是每秒采集 1 次。
$ sar -P ALL 1 1
( 略 )
16:32:50 CPU %user %nice %system %iowait %steal %idle
16:32:51 all 0.13 0.00 0.00 0.00 0.00 99.87
16:32:51 0 0.00 0.00 0.00 0.00 0.00 100.00
16:32:51 1 0.00 0.00 0.00 0.00 0.00 100.00
16:32:51 2 0.00 0.00 0.00 0.00 0.00 100.00
16:32:51 3 0.00 0.00 0.00 0.00 0.00 100.00
16:32:51 4 0.99 0.00 0.00 0.00 0.00 99.01
16:32:51 5 1.00 0.00 0.00 0.00 0.00 99.00
16:32:51 6 0.00 0.00 0.00 0.00 0.00 100.00
16:32:51 7 0.00 0.00 0.00 0.00 0.00 100.00
Average: CPU %user %nice %system %iowait %steal %idle
Average: all 0.13 0.00 0.00 0.00 0.00 99.87
Average: 0 0.00 0.00 0.00 0.00 0.00 100.00
Average: 1 0.00 0.00 0.00 0.00 0.00 100.00
Average: 2 0.00 0.00 0.00 0.00 0.00 100.00
Average: 3 0.00 0.00 0.00 0.00 0.00 100.00
Average: 4 0.99 0.00 0.00 0.00 0.00 99.01
Average: 5 1.00 0.00 0.00 0.00 0.00 99.00
Average: 6 0.00 0.00 0.00 0.00 0.00 100.00
Average: 7 0.00 0.00 0.00 0.00 0.00 100.00
$
下面,我们来尝试运行一个不发起任何系统调用,只是单纯地执行循环的程序,并通过 sar
命令查看它在各模式下的运行时间(代码清单 2-3)。
代码清单 2-3 loop 程序(loop.c)
int main(void)
{
for(;;)
;
}
编译并运行这段代码,将出现以下结果。
$ cc -o loop loop.c
$ ./loop &
[1] 13093
$ sar -P ALL 1 1
( 略 )
16:45:45 CPU %user %nice %system %iowait %steal %idle
16:45:46 all 12.86 0.00 0.12 0.00 0.00 87.02
16:45:46 0 100.00 0.00 0.00 0.00 0.00 0.00 ←①
16:45:46 1 0.00 0.00 0.00 0.00 0.00 100.00
16:45:46 2 0.00 0.00 0.00 0.00 0.00 100.00
16:45:46 3 1.00 0.00 0.00 0.00 0.00 99.00
16:45:46 4 0.99 0.00 0.00 0.00 0.00 99.01
16:45:46 5 1.01 0.00 0.00 0.00 0.00 98.99
16:45:46 6 0.00 0.00 0.00 0.00 0.00 100.00
16:45:46 7 0.00 0.00 0.00 0.00 0.00 100.00
( 略 )
参照①指向的那一行数据,可以看出在采集信息的这 1 秒内,用户进程(即 loop
程序)始终运行在 CPU 核心 0 上(图 2-3)。
图 2-3 loop 程序的运行
在测试完成后,记得结束正在运行的 loop
程序5。
5把想结束的程序的进程 ID 指定为 kill
命令的参数即可。在输入运行命令时附加一个 &
,即可获取 loop
程序的进程 ID。
$ kill 13093
$
接着,让我们对循环执行 getppid()
这个用于获取父进程的进程 ID 的系统调用的程序进行相同的操作(代码清单 2-4)。
代码清单 2-4 ppidloop 程序(ppidloop.c)
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
for(;;)
getppid();
}
编译并运行这段代码,将出现以下结果。
$ cc -o ppidloop ppidloop.c
$ ./ppidloop &
[1] 13389
$ sar -P ALL 1 1
( 略 )
16:49:11 CPU %user %nice %system %iowait %steal %idle
16:49:12 all 3.51 0.00 9.02 0.00 0.00 87.47
16:49:12 0 0.00 0.00 0.00 0.00 0.00 100.00
16:49:12 1 28.00 0.00 72.00 0.00 0.00 0.00 ←②
16:49:12 2 0.00 0.00 0.00 0.00 0.00 100.00
16:49:12 3 0.00 0.00 0.00 0.00 0.00 100.00
16:49:12 4 0.99 0.00 0.99 0.00 0.00 98.02
16:49:12 5 0.00 0.00 0.00 0.00 0.00 100.00
16:49:12 6 0.00 0.00 0.00 0.00 0.00 100.00
16:49:12 7 0.00 0.00 0.00 0.00 0.00 100.00
( 略 )
$
参照②指向的这一行数据,可以看出在采集信息的这 1 秒内,发生了以下情况(图 2-4)。
- 在 CPU 核心 1 上,运行
ppidloop
程序占用了 28% 的运行时间 - 根据
ppidloop
程序发出的请求来获取父进程的进程 ID 这一内核处理占用了 72% 的运行时间
图 2-4 ppidloop 程序的运行
为什么 %system
的值不是 100% 呢?这是因为,用于循环执行 main()
函数内的 getppid()
的循环处理,是属于进程自身的处理。
在测试完成后,也不要忘记结束正在运行的程序。
$ kill 13389
$
虽然不能一概而论,但当 %system
的值高达几十时,大多是陷入了系统调用发起过多,或者系统负载过高等糟糕的状态。
● 执行系统调用所需的时间
在 strace
命令后加上 -T
选项,就能以微秒级的精度来采集各种系统调用所消耗的实际时间。在发现 %system
的值过高时,可以通过这个功能来确认到底是哪个系统调用占用了过多的系统资源。下面是对 hello world
程序使用 strace -T
后的输出结果。
$ strace -T -o hello.log ./hello
hello world
$ cat hello.log
hello"], [/* 28 vars */]) ↵
= 0 <0.000225>
brk(NULL) = 0x6c6000 <0.000012>
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT ↵
(No such file or directory) <0.000016>
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE| ↵
MAP_ANONYMOUS, -1, 0) = 0x7ff02b49a000 <0.000013>
access("/etc/ld.so.preload", R_OK) = -1 ENOENT ↵
(No such file or directory) <0.000014>
( 略 )
brk(0x6e7000) = 0x6e7000 <0.000008>
write(1, "hello world\n", 12) = 12 <0.000014>
exit_group(0) = ?
+++ exited with 0 +++
$
通过这些信息可以看出,输出 hello world\n
这一字符串总共花费了 14 微秒。
此外,strace
命令还存在其他选项,例如,使用 -tt
选项能以微秒为单位来显示处理发生的时刻。在使用 strace
命令时,可以根据实际需求来选择不同的选项。