深入理解linux系统-用户模式实现的功能

张彤 2023年07月25日 396次浏览

深入理解linux系统是书籍《linux是如何跑起来的》的学习笔记。

一. 用户模式实现的功能

各种进程与OS的关系

各种进程与OS的关系.jpg各种进程与OS的关系

如上图,在虚线上部的是进程的用户态部分,这部分可以由用户进行操作,而虚线的下半部分是进程的内核态,用户不能直接操作,而是交由内核进行操作.

进程在用户态发起调用内核资源的时候,必须首先经过系统调用,进而产生一个cpu中断,切换到内核态进行操作.

频繁的上下文切换会增加cpu 开销,零拷贝的本质也是实现了用户态进行操作,减少甚至不使用内核态操作,减少切换带来的开销.

1.1 系统调用

系统调用如此重要,它的分类如下

  • 进程控制(创建和删除)
  • 内存管理(分配和释放)
  • 进程间通讯
  • 网络管理
  • 文件系统操作
  • 文件操作(访问设备) Linux中一切都是文件,包括设备

1.1.1 CPU模式切换

如上所述,CPU是在内核态和用户态之间,通过系统调用来反复切换的.
cpu模式切换.jpg

内核在收到来自进程的请求后,将对该请求进行合理性检查,比如进程需要开辟一片内存区域,但是这个时候没有相应资源提供,系统调用就会失败.

系统调用时,都发生了哪些事情呢?我们可以实用命令strace来进行追踪.

首先,我们创建一个c语言示例.

(base) [root@ecs0003 linux_pro]# vim hello.c

输入后,wq保存


# include <stdio.h>
int main(void) {
puts("hello world,Linux!");
return 0;
}

解这我们编译这个文件.

(base) [root@ecs0003 linux_pro]# cc -o hello hello.c
(base) [root@ecs0003 linux_pro]# ./hello 
hello world,Linux!

接下来,通过strace来跟踪hello这个程序执行的过程.

(base) [root@ecs0003 linux_pro]# strace -o hello.log ./hello
hello world,Linux!

查看日志

(base) [root@ecs0003 linux_pro]# less hello.log 
execve("./hello", ["./hello"], [/* 32 vars */]) = 0
brk(NULL)                               = 0x25d3000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f859e5ea000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=45852, ...}) = 0
mmap(NULL, 45852, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f859e5de000
close(3)                                = 0
open("/lib64/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\0P%\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2173512, ...}) = 0
mmap(NULL, 3981792, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f859dffd000
mprotect(0x7f859e1c0000, 2093056, PROT_NONE) = 0
mmap(0x7f859e3bf000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c2000) = 0x7f859e3bf000
mmap(0x7f859e3c5000, 16864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f859e3c5000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f859e5dd000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f859e5db000
arch_prctl(ARCH_SET_FS, 0x7f859e5db740) = 0
mprotect(0x7f859e3bf000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7f859e5eb000, 4096, PROT_READ) = 0
munmap(0x7f859e5de000, 45852)           = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f859e5e9000
# 重点关注这个
write(1, "hello world,Linux!\n", 19)    = 19
exit_group(0)                           = ?
+++ exited with 0 +++

strace日志中的每一行代表一个系统调用,虽然系统调用数量很多,但是我们只需要关注有注释的那一行即可,旧是write()这个调用.这个方法向屏幕画面或文件等输出数据.

虽然使用的是c语言,但是无论什么语言,都必须通过系统调用向内核发起请求

下面我们实用python试试

(base) [root@ecs0003 linux_pro]# vim hello.py
# 写人以下命令wq保存
# __*__ coding:utf-8 __*__
print("hello world,Linux!")
(base) [root@ecs0003 linux_pro]# strace -o hello.py.log python ./hello.py

日志内容如下

(base) [root@ecs0003 linux_pro]# less hello.py.log
execve("/mnt/py_oracle/anaconda3/bin/python", ["python", "./hello.py"], [/* 32 vars */]) = 0
brk(NULL)                               = 0x5605f9948000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1ea2c5d000
readlink("/proc/self/exe", "/mnt/py_oracle/anaconda3/bin/pyt"..., 4096) = 38
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/mnt/py_oracle/anaconda3/bin/../lib/tls/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/mnt/py_oracle/anaconda3/bin/../lib/tls/x86_64", 0x7fff504ad720) = -1 ENOENT (No such file or directory)
open("/mnt/py_oracle/anaconda3/bin/../lib/tls/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/mnt/py_oracle/anaconda3/bin/../lib/tls", 0x7fff504ad720) = -1 ENOENT (No such file or directory)
open("/mnt/py_oracle/anaconda3/bin/../lib/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/mnt/py_oracle/anaconda3/bin/../lib/x86_64", 0x7fff504ad720) = -1 ENOENT (No such file or directory)
open("/mnt/py_oracle/anaconda3/bin/../lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/mnt/py_oracle/anaconda3/bin/../lib", {st_mode=S_IFDIR|0755, st_size=40960, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=45852, ...}) = 0
mmap(NULL, 45852, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1ea2c51000
close(3)                                = 0
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0m\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=144792, ...}) = 0
mmap(NULL, 2208904, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1ea2821000
mprotect(0x7f1ea2838000, 2093056, PROT_NONE) = 0
mmap(0x7f1ea2a37000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16000) = 0x7f1ea2a37000
mmap(0x7f1ea2a39000, 13448, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f1ea2a39000
close(3)                                = 0
open("/mnt/py_oracle/anaconda3/bin/../lib/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/lib64/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\0P%\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2173512, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1ea2c50000
....................................................................................................................
read(3, "# __*__ coding:utf-8 __*__\nprint"..., 4096) = 55
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x7f1ea2c55000, 4096)            = 0
# 一样的也有write这个系统调用
write(1, "hello world,Linux!\n", 19)    = 19
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f1ea28306d0}, {0x5605f82f5140, [], SA_RESTORER, 0x7f1ea28306d0}, 8) = 0
sigaltstack(NULL, {ss_sp=0x5605f9966d60, ss_flags=0, ss_size=8192}) = 0
sigaltstack({ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}, NULL) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

与c语言的29次调用相比,python的系统调用就多得多了,总计是747次.

(base) [root@ecs0003 linux_pro]# wc -l hello.py.log 
747 hello.py.log
  • 事实上,python慢这只是一部分原因而已,最大的问题在于解释器,就是python将程序文件编译为汇编语言这一部分是占了大头的.这里就不展开讨论python的性能问题了.

1.1.2 监测CPU切换

sar命令用于获取进程分别在用户态内核态运行的时间比.

我们通过3秒一次采集数据,看看每个CPU核心在运行什么.

(base) [root@ecs0003 linux_pro]# sar -P ALL 3
Linux 3.10.0-862.el7.x86_64 (ecs0003.novalocal) 	07/19/2023 	_x86_64_	(16 CPU)

11:38:58 AM     CPU     %user     %nice   %system   %iowait    %steal     %idle
11:39:01 AM     all      0.10      0.00      0.17      0.00      0.00     99.73
11:39:01 AM       0      0.00      0.00      0.33      0.00      0.00     99.67
11:39:01 AM       1      0.33      0.00      0.00      0.00      0.00     99.67
11:39:01 AM       2      0.33      0.00      0.33      0.00      0.00     99.34
11:39:01 AM       3      0.00      0.00      0.33      0.00      0.00     99.67
11:39:01 AM       4      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM       5      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM       6      0.33      0.00      0.33      0.00      0.00     99.33
11:39:01 AM       7      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM       8      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM       9      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM      10      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM      11      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM      12      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM      13      0.33      0.00      0.00      0.00      0.00     99.67
11:39:01 AM      14      0.00      0.00      0.00      0.00      0.00    100.00
11:39:01 AM      15      0.67      0.00      1.00      0.00      0.00     98.33

# ctl +c 后,输出采集数据的平均值
Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all      0.31      0.00      0.61      0.00      0.00     99.08
Average:          0      0.26      0.00      0.78      0.00      0.00     98.96
Average:          1      0.65      0.00      1.30      0.00      0.00     98.04
Average:          2      0.13      0.00      0.65      0.00      0.00     99.22
Average:          3      0.39      0.00      0.52      0.00      0.00     99.09
Average:          4      0.26      0.00      0.78      0.00      0.00     98.95
Average:          5      0.39      0.00      0.91      0.00      0.00     98.70
Average:          6      0.13      0.00      0.39      0.00      0.00     99.48
Average:          7      0.39      0.00      0.52      0.00      0.00     99.09
Average:          8      0.00      0.00      0.39      0.00      0.00     99.61
Average:          9      0.39      0.00      0.78      0.00      0.00     98.82
Average:         10      0.26      0.00      0.39      0.00      0.00     99.35
Average:         11      0.39      0.00      0.52      0.00      0.00     99.09
Average:         12      0.26      0.00      0.39      0.00      0.00     99.35
Average:         13      0.26      0.00      0.39      0.00      0.00     99.35
Average:         14      0.13      0.00      0.13      0.00      0.00     99.74
Average:         15      0.78      0.00      0.92      0.00      0.00     98.30

很明显我们的用例服务器是16核心的

(base) [root@ecs0003 linux_pro]# lscpu|grep 'On-line CPU(s) list'
On-line CPU(s) list:   0-15
  • 观察输出结果,每一行的加总是100%
  • %user%nice字段值相加得到的值是用户在用户态下所占比例.
  • %system则是内核态下进程所占开销的比例.
  • %idle为核心中没有任何处理时的空闲(idle)的状态.
  • %iowait 表示cpu处于空闲状态,在此期间,系统有一个未完成的磁盘IO等待。
  • %steal当虚拟机管理程序维护另一个虚拟处理器时,虚拟CPU或CPU在非自愿等待中花费的时间比.

1.2 系统调用的包装函数

如果没有OS的帮助,程序员不得不各自编写汇编语言代码,然后再用高级语言区调用这些汇编语言.

这样一来,程序员的开发周期将增加,而且平台间的移植成本也增加了.
如果没有OS.jpg

OS提供了一系列的被成为系统调用的包装函数的函数,用于在系统内部发起系统调用.

实用高级编程语言编写的程序,只需要调用由高级语言提供的包装函数即可

高级语言调用包装函数.jpg

1.3 C标准库

Linux提供了C语言标准库.

通常以GUN项目提供的glibc作为C标准库使用.

用c编写的几乎所有程序都依赖glibc库.

glibc不仅包括系统调用的包装函数,还提供了POSIX标准中定义的函数.

Linux提供了ldd命令来查看程序所依赖的库.

(base) [root@ecs0003 linux_pro]# ldd /bin/echo 
	linux-vdso.so.1 =>  (0x00007ffe7c7bb000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fdbed62a000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fdbed9f7000)

(base) [root@ecs0003 linux_pro]# ldd /mnt/py_oracle/anaconda3/bin/python
	linux-vdso.so.1 =>  (0x00007ffc8a3e6000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb638cf0000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fb638923000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fb63871f000)
	libutil.so.1 => /lib64/libutil.so.1 (0x00007fb63851c000)
	librt.so.1 => /lib64/librt.so.1 (0x00007fb638314000)
	libm.so.6 => /lib64/libm.so.6 (0x00007fb638012000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb639277000)

上述提供了echo命令和python命令的依赖库

除了命令我们通常执行的程序也可以分析出依赖库

(base) [root@ecs0003 linux_pro]# ldd hello
	linux-vdso.so.1 =>  (0x00007ffe92f72000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fe4cf687000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fe4cfa54000)

这几个例子中都出现了libc库,可见在OS层面,C语言依旧是一哥般的存在.

1.4 OS提供的程序

OS提供了丰富的程序

功能说明命令
初始化系统init
变更系统运行方式sysctl,nice,sync
文件操作touch,mkdir
文本数据处理grep,sort,uniq
性能测试sar,lostat
编译gcc
脚本语言运行环境perl,python,ruby
shellbash
视窗系统x