0.前言
最近朋友推荐了一本比较容易轻松易懂的讲一些Linux机制的书:《Linux是怎样工作的》,作者武内觉是一位曾经就职于日本富士通的工程师。
这本书翻起来觉着蛮有意思,因此将学习的过程记录一下,当做是笔记review,期间书中的代码和实际运行情况也记录于此,如有一些心得体会也一并记录。
第1章讲了一些计算机系统的概要,不做展开,因此读书笔记从第2章开始。
1.系统调用
Linux系统调用是操作系统内核提供给应用程序的一系列接口,允许应用程序请求操作系统完成某些特定的任务。这些调用是操作系统内核和应用程序之间的桥梁,使得应用程序能够访问硬件设备、管理文件系统、进行网络通信等。
Linux系统调用可以分为以下几类:
- 进程控制:用于创建、终止进程,以及控制进程间的通信。
- 文件系统控制:用于文件的读写、创建、删除等操作。
- 系统控制:用于获取系统信息、设置系统时间等。
- 内存管理:用于内存的分配、释放等操作。
- 网络管理:用于网络通信相关的操作。
- 用户管理:用于管理用户账户和权限等。
上面这些属于百度出来的比较官方的说法,后面跟着书中的描述,会介绍到一些系统调用,但不会全部都覆盖到,感兴趣的话可以查询别的资料了解一下。
1.1发起系统调用时的情形
通过使用strace命令对进程进行追踪,例如,通过strace命令运行一个简单的“hello world”的程序(代码如下)。
首先编译生成可执行文件hello。
#include "stdio.h"
void main()
{
puts("hello world\r\n");
}
使用strace命令来看看这个程序会发起哪些系统调用。将strace的结果重映射到hello.log当中,以防止strace的输出结果混淆在正常的“hello可执行程序”的打印当中。
Alex@ubuntu:test2_1$ gcc -o hello hello.c
Alex@ubuntu:test2_1$ ls
hello hello.c hello.log
Alex@ubuntu:test2_1$ strace -o hello.log ./hello
hello world
Alex@ubuntu:test2_1$
让我们看看hello.log中有什么:
Alex@ubuntu:test2_1$ cat hello.log
execve("./hello", ["./hello"], 0x7ffd32a72d30 /* 65 vars */) = 0
brk(NULL) = 0x5595c12e4000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd8fd39b40) = -1 EINVAL (无效的参数)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=69872, ...}) = 0
mmap(NULL, 69872, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f445c845000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300A\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\7\2C\n\357_\243\335\2449\206V>\237\374\304"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029592, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f445c843000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\7\2C\n\357_\243\335\2449\206V>\237\374\304"..., 68, 880) = 68
mmap(NULL, 2037344, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f445c651000
mmap(0x7f445c673000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f445c673000
mmap(0x7f445c7eb000, 319488, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19a000) = 0x7f445c7eb000
mmap(0x7f445c839000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f445c839000
mmap(0x7f445c83f000, 13920, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f445c83f000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f445c844540) = 0
mprotect(0x7f445c839000, 16384, PROT_READ) = 0
mprotect(0x5595c0864000, 4096, PROT_READ) = 0
mprotect(0x7f445c884000, 4096, PROT_READ) = 0
munmap(0x7f445c845000, 69872) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
brk(NULL) = 0x5595c12e4000
brk(0x5595c1305000) = 0x5595c1305000
write(1, "hello world\r\n", 13) = 13
write(1, "\n", 1) = 1
exit_group(14) = ?
+++ exited with 14 +++
log里的每一行都对应一个系统调用,重点看到第34行,进程通过负责向画面或文件等输出数据的write()系统调用,输出了hello world这一串字符。
上面的系统调用流程,是通过C语言编码生成可执行程序实现的。那么用其他编程语言写出来执行打印“hello world”的话,会不会也能有write()系统调用?下面看一个Python写的程序的例子。
print("hello world")
同样用strace查看系统调用的信息。
Alex@ubuntu:test2_2$ strace -o hello.py.log python3 ./hello.py
hello world
Alex@ubuntu:test2_2$ cat hello.py.log
execve("/usr/bin/python3", ["python3", "./hello.py"], 0x7fff2948c9b8 /* 65 vars */) = 0
brk(NULL) = 0x1429000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffbe7656d0) = -1 EINVAL (无效的参数)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=69872, ...}) = 0
mmap(NULL, 69872, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f25f7300000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
...省略
ioctl(3, TCGETS, 0x7fffbe7654e0) = -1 ENOTTY (对设备不适当的 ioctl 操作)
lseek(3, 0, SEEK_CUR) = 0
fstat(3, {st_mode=S_IFREG|0664, st_size=20, ...}) = 0
read(3, "print(\"hello world\")", 4096) = 20
lseek(3, 0, SEEK_SET) = 0
read(3, "print(\"hello world\")", 4096) = 20
read(3, "", 4096) = 0
close(3) = 0
write(1, "hello world\n", 12) = 12
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f25f714f090}, {sa_handler=0x629050, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f25f714f090}, 8) = 0
sigaltstack(NULL, {ss_sp=0x1470290, ss_flags=0, ss_size=16384}) = 0
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}, NULL) = 0
exit_group(0) = ?
+++ exited with 0 +++
可以看到第22行也有write()的系统调用输出“hello world”。
可见不管是C语言还是Python,在实现同样的打印输出的功能时,最终都会执行相同的系统调用。
1.2关于用户模式和内核模式的实验
关于用户模式和内核模式,详细解读网上有很多材料,这里仅做一些说明。我们编写的应用代码在执行逻辑和业务相关时,CPU核心是运行在用户模式,当去发起系统调用时,就会运行在内核模式,内核调用完成后重新回到用户模式。
举个例子,下面运行一个不发起任何系统调用,只是单纯执行循环的程序,看看它在各模式下的运行时间。可以通过sar命令来获取进程分别在用户模式和内核模式下运行的时间比例。
#include "stdio.h"
void main()
{
for(;;)
;
}
Alex@ubuntu:test2_3$ cc -o loop loop.c
Alex@ubuntu:test2_3$ ./loop &
[1] 5816
Alex@ubuntu:test2_3$ sar -P ALL 1 1
Linux 5.15.0-117-generic (ubuntu) 2024年08月18日 _x86_64_ (4 CPU)
22时12分43秒 CPU %user %nice %system %iowait %steal %idle
22时12分44秒 all 25.49 0.00 5.64 0.00 0.00 68.87
22时12分44秒 0 0.00 0.00 10.28 0.00 0.00 89.72
22时12分44秒 1 2.97 0.00 6.93 0.00 0.00 90.10
22时12分44秒 2 100.00 0.00 0.00 0.00 0.00 0.00
22时12分44秒 3 1.00 0.00 5.00 0.00 0.00 94.00
平均时间: CPU %user %nice %system %iowait %steal %idle
平均时间: all 25.49 0.00 5.64 0.00 0.00 68.87
平均时间: 0 0.00 0.00 10.28 0.00 0.00 89.72
平均时间: 1 2.97 0.00 6.93 0.00 0.00 90.10
平均时间: 2 100.00 0.00 0.00 0.00 0.00 0.00
平均时间: 3 1.00 0.00 5.00 0.00 0.00 94.00
可以看到第11行,CPU2的%user列为100%,表示在采集信息的这1秒内,这个loop进程始终运行在CPU2上,且运行在用户模式下。
测试完成后记得杀掉进程。
Alex@ubuntu:test2_3$ kill 5816
接下来,执行一个循环执行getppid()这个系统调用的程序看看会有什么不同。
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
for(;;)
getppid();
}
Alex@ubuntu:test2_4$ cc -o ppidloop ppidloop.c
Alex@ubuntu:test2_4$ ./ppidloop &
[1] 6009
Alex@ubuntu:test2_4$ sar -P ALL 1 1
Linux 5.15.0-117-generic (ubuntu) 2024年08月18日 _x86_64_ (4 CPU)
22时20分22秒 CPU %user %nice %system %iowait %steal %idle
22时20分23秒 all 10.20 0.00 17.91 0.00 0.00 71.89
22时20分23秒 0 41.00 0.00 59.00 0.00 0.00 0.00
22时20分23秒 1 0.00 0.00 3.03 0.00 0.00 96.97
22时20分23秒 2 0.00 0.00 6.80 0.00 0.00 93.20
22时20分23秒 3 0.00 0.00 3.00 0.00 0.00 97.00
平均时间: CPU %user %nice %system %iowait %steal %idle
平均时间: all 10.20 0.00 17.91 0.00 0.00 71.89
平均时间: 0 41.00 0.00 59.00 0.00 0.00 0.00
平均时间: 1 0.00 0.00 3.03 0.00 0.00 96.97
平均时间: 2 0.00 0.00 6.80 0.00 0.00 93.20
平均时间: 3 0.00 0.00 3.00 0.00 0.00 97.00
可以看到第9行,CPU0的%user为41,%system为59。说明运行ppidloop程序运行在用户模式时间为41%的,而运行在内核模式时间为59%,二者加一起为100%。
那为什么%system不是100%?因为用于执行系统调用的那个for循环,是属于进程本身的处理。
测试完同样记得杀掉进程。
Alex@ubuntu:test2_4$ kill 6009
2.小结
用不同的编程语言实现相同的功能时(例如打印“hello world”),最终都会调用到相同的系统调用;可以用sar命令查看程序运行在用户模式和内核模式的占比,执行程序本身的业务逻辑工作在用户模式,发生系统调用时运行在内核模式。