深入理解linux系统是书籍《linux是如何跑起来的》的学习笔记。
一. 用户模式实现的功能
各种进程与OS的关系
如上图,在虚线上部的是进程的用户态部分,这部分可以由用户进行操作,而虚线的下半部分是进程的内核态,用户不能直接操作,而是交由内核进行操作.
进程在用户态发起调用内核资源的时候,必须首先经过系统调用,进而产生一个cpu中断,切换到内核态进行操作.
频繁的上下文切换会增加cpu 开销,零拷贝的本质也是实现了用户态进行操作,减少甚至不使用内核态操作,减少切换带来的开销.
1.1 系统调用
系统调用如此重要,它的分类如下
- 进程控制(创建和删除)
- 内存管理(分配和释放)
- 进程间通讯
- 网络管理
- 文件系统操作
- 文件操作(访问设备) Linux中一切都是文件,包括设备
1.1.1 CPU模式切换
如上所述,CPU是在内核态和用户态之间,通过系统调用来反复切换的.
内核在收到来自进程的请求后,将对该请求进行合理性检查,比如进程需要开辟一片内存区域,但是这个时候没有相应资源提供,系统调用就会失败.
系统调用时,都发生了哪些事情呢?我们可以实用命令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提供了一系列的被成为系统调用的包装函数
的函数,用于在系统内部发起系统调用.
实用高级编程语言编写的程序,只需要调用由高级语言提供的包装函数即可
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 |
shell | bash |
视窗系统 | x |