一起学《Linux是怎样工作的》第2章2.1节:系统调用

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命令查看程序运行在用户模式和内核模式的占比,执行程序本身的业务逻辑工作在用户模式,发生系统调用时运行在内核模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值