Linux核心系统调用详解
[toc]
# 引言
操作系统的诞生本质就是:
- 计算机资源管理:透明后台自动完成
- 应用程序提供抽象:针对系统文件进行创建、修改、写入、删除
这其中关于进程的内核调用在安全管理层面也有着非常出色的设计,了解内核态调用可以更好地辅助开发人员理解操作系统工作机制,更好地完成一些生产问题的排查,所以本文将针对此话题展开更进一步的分析和探讨,希望对你有所帮助。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 基于open函数了解进程内核态调用执行步骤
# 操作系统的本职工作
现代操作系统本质上就是对于多道处理程序(解决CPU单位时间内只能执行一条指令时阻塞带来的空转问题)的一种抽象,一种对于物理层面的一种封装,帮助人类完成计算机资源管理和应用程序的统一抽象管理。以本文要讲的内容为例,当要读取系统文件时,对应的程序需要发起一次系统调用,即通过syscall指令进入操作系统内核。 随后,内核代码针对该指令进行必要的参数检查后,执行文件读取调用,并将控制返回给系统调用后的指令,而这个过程也就是我们常说的内核调用。这种调用方式在系统调用和中断处理时都会触发,对于一般的用户态调用则不会进行上下文切换:

# 详解操作系统中的I/O调用
针对操作系统的内核态调用,我们还是以最经典的I/O文件读取展开探讨,以下以笔者的read.c代码为例,可以看到针对Linux服务器下的txt文件读取,大体分为如下几步:
- 要通过open函数执行调用获取文件描述符
- 调用read函数读取文件中的数据,并返回读取到的字节数
- 输出打印到系统终端
- 关闭文件描述符资源
这其中open和read调用都涉及进程上下文切换调用阻塞等待结果并返回:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main() {
int fd = open("example.txt", O_RDONLY); // 打开文件
if (fd == -1) {
perror("Error opening file");
return 1;
}
char buffer[128];
ssize_t bytesRead;
// 读取文件内容
while ((bytesRead = read(fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytesRead] = '\0'; // 确保字符串以 '\0' 结尾
printf("bytesRead: %zd\n", bytesRead);
printf("%s", buffer);
}
if (bytesRead == -1) {
perror("Error reading file");
}
close(fd); // 关闭文件
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
考虑到代码示例的完整性,笔者也给出编译和执行的指令:
# 编译c程序
gcc read.c -o read
# 执行程序
./read
2
3
4
最终输出结果如下,可以看到对于hello c lang读取结果为13字节(c语言字符串末尾默认添加一个\0结束符),同时输出的字符也很预期结果一致:
bytesRead: 13
hello c lang
2
# 深入剖析read调用的工作机制
上面这个程序对应的open和read都涉及内核态调用,以read为例,该调用返回的是文件读取的字节数。在执行该函数调用时,调用程序会先将函数参数存到RDI、RSI、RDX、R10、R8、R9这组寄存器上,一旦超过6个参数就会将多出来的参数直接压入堆栈。以我们的read调用为例,因为只有3个参数,所以只会用到RDI、RSI、RDX这几个寄存器。
一旦发起read系统调用读取磁盘数据时,read库函数就会将系统调用号存放到RAX寄存器上,因为考虑到应用程序安全层面的不稳定因素,操作系统会通过一个陷阱指令,对应我们的X86-64的CPU也就是syscall调用,指令让read调用的线程切换到内核态中,内核检查RAX寄存器中的系统调用号将请求分发给对应的内核函数。 在此期间该线程会因为IO调用而发起阻塞,所以在多道程序设计的理念上,为避免CPU因为IO阻塞而未能处理其它进程的程序的指令,操作系统在此时会将进程的上下文保存到其结构体中(本质就是内存),然后去执行其它程序的指令,将其它指令需要的数据存放到寄存器上执行。
我们的线程在内核完成文件读取后,内核将结果返回给用户程序,继续执行用户程序的下一条指令。 对应我们也给出read系统调用返回的完整流程图,可以看到我们会将read参数分别放到3个寄存器上,并将系统调用号存到RAX寄存器上,在随后进行的syscall指令的上下文切换中,内核代码会根据RAX中的系统调用号定位到对应的内核函数完成数据读取,并将结果以函数返回值的方式返回给用户程序:

# 内核态调用常见问题
# 为什么调用要进入内核态调用
应用程序不受操作系统的完全控制,考虑到安全性,所有涉及系统资源的操作都封装成内核函数
# 为什么不直接在open函数上配置指令地址
为避免应用程序识别内核函数地址直接违法调用,系统会变化指令地址,所以就需要通过映射表进行管理
# 小结
操作系统本质上是针对用户程序的抽象和系统资源的统一管理,对于日常的I/O调用,应用程序执行时会将参数存放到有限的寄存器上或堆栈上,然后陷阱指令(对应x86-64 CPU也就是syscall指令)进入Linux内核,此时内核代码就会根据RAX寄存器中的系统调用号定位到对应的内核函数执行系统调用,并将结果以函数返回值的方式返回给应用程序。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 参考
《现代操作系统》