本文对Linux系统调用的机制进行了大致分析,并以此为基础对Linux进程管理(多进程调用、进程同步等)进行初步探索。在研究进程
fork()
函数的过程中,笔者产生了很多问题,也通过查阅资料、动手实验等方式对这些问题有了一定的研究和理解。本文将按照笔者对问题研究的历程入手,对Linux进程管理的部分基本过程和背后机理进行阐述。在本文中,你将看到:
- 系统调用(system call)的流程
POSIX
基本进程管理函数——fork()
/vfork()
/clone()
/wait()
/waitpid()
…
fork()
和vfork()
的区别、发展vfork()
等待机制(Completion 进程同步机制)wait()
函数等待机制(详解)
Linux 系统调用(system call)
Introduction
System Call (syscall),是运行在操作系统上的用户程序调用系统功能的方式。为了安全,用户程序在运行时处于用户态,不能调用系统内核函数,也不能访问属于操作系统的内存空间。那么,用户程序如何使用操作系统提供的功能?答案是:使用系统调用。
系统调用与API不同,后者定义了一些系统功能调用的接口(函数原型),而前者通过中断向内核发出功能请求。标准库和硬件兼容标准库定义了一部分API,为用户程序提供系统调用服务,而这些API后的封装函数通过系统调用请求系统的资源和功能。
以Linux为例,其glibc
库函数实现了一些POSIX标准指定的API,如malloc
、free
等,也实现了标准库函数printf
等,这些函数内部都使用系统调用来请求系统实现对应功能,前两个函数都使用brk
系统调用来控制进程堆的大小,printf
则使用了write
系统调用来向控制台打印信息。
Procedure
从用户程序开始,使用一个系统调用并返回的流程如下(以printf举例):
-
用户调用printf,完成参数传递。
printf("Hello, %s.\n", name);
-
glibc完成参数解析,填充
write()
调用需要的参数,并触发软中断(汇编):handle_args(); // 处理传入参数 prepare_args_for_write(); // 为调用write配置参数 int 0x80; // 使用系统中断
- 其中,从
prepare_args_for_write()
开始,程序使用汇编语言编写,配置参数通常为:给对应的寄存器赋值,或是将参数压入栈中。
- 其中,从
-
系统调用处理程序(system call handler):识别并处理软中断,并转由对应的 系统调用服务例程(system call service routine)处理。
-
系统调用服务例程(system call service routine)一般为C语言程序,处理系统调用后将返回值返还给用户进程。
Detail
进入系统调用
系统调用一般通过两种方式进入,且都为汇编语言指令。以下两个指令的功能相同:
int 0x80;
syscall;
- 第一条指令能在
x86
和x64
平台上使用,但第二条指令只有x64
支持。 - 两个指令的机器码完全不同,(这给x64架构上的ROP攻击提供了更多的机会XD)
不同的系统调用通过系统调用号进行区分,它被置于eax/rax
寄存器上,下面是几个常用的系统调用:
NR | syscall name |
---|---|
0 | read |
1 | write |
2 | open |
12 | brk |
59 | execve |
- 如果需要参数,它们一般被保存在寄存器中,顺序一般为
rdi
rsi
rdx
…
退出系统调用
内核仍然通过汇编语言指令从系统调用中退出,CPU切换回用户态,继续执行用户程序
iret;
sysexit;
- 就像函数拥有返回值,一般系统调用也有返回值,在返回前,系统将值保存在
eax
中,并返回给用户进程。
兼容性问题
为了同时支持两种系统调用方式,libc
库需要使用特殊的兼容方法。在程序初始化时,它会创建一个较小的ELF共享对象,这个对象将被映射到用户进程的地址空间中。它将一个最有效的系统调用方式定义在__kernel_vsyscall
函数中,所有系统调用都转而执行此函数,此函数使用系统和内核支持的系统调用方式进行系统调用。
Others
下部分,我们将探究Linux进程管理相关的函数以及背后的机理。
参考资料:《深入理解Linux内核》、Linux man page