一、什么是系统调用
系统调用 跟用户自定义函数一样也是一个函数,不同的是 系统调用 运行在内核态,而用户自定义函数运行在用户态。由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运行在内核态,所以操作系统必须提供一种能够进入内核态的方式,系统调用 就是这样的一种机制。
系统调用 是 Linux 内核提供的一段代码(函数),其实现了一些特定的功能,用户可以通过 int 0x80 中断(x86 CPU)或者 syscall 指令(x64 CPU)来调用 系统调用。 内核提供用户空间程序与内核空间进行交互的一套标准接口,这些接口让用户态程序能受限访问硬件设备,比如申请系统资源,操作设备读写,创建新进程等。用户空间发生请求,内核空间负责执行,这些接口便是用户空间和内核空间共同识别的桥梁,这里提到两个字“受限”,是由于为了保证内核稳定性,而不能让用户空间程序随意更改系统,必须是内核对外开放的且满足权限的程序才能调用相应接口。
在用户空间和内核空间之间,有一个叫做Syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。对于每个系统调用都会有一个对应的系统调用号,比很多操作系统要少很多。
安全性与稳定性:内核驻留在受保护的地址空间,用户空间程序无法直接执行内核代码,也无法访问内核数据,通过系统调用
二、进入系统调用
本文主要介绍的是 x86 CPU 进入系统调用的方式
Linux 提供了 int 0x80 中断来让用户程序进入 系统调用,我们来看看 Linux 对 int 0x80 中断的处理初始化过程:
void __init trap_init(void)
{
...
set_system_gate(SYSCALL_VECTOR, &system_call);
...
}
- 1
- 2
- 3
- 4
- 5
- 6
系统初始化时,会在 trap_init() 函数中对 int 0x80 中断处理进行初始化,设置其中断处理过程入口为 system_call。system_call 是一段由汇编语言编写的代码,我们看看关键部分,如下:
ENTRY(system_call)
...
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
...
- 1
- 2
- 3
- 4
- 5
我们把上面的汇编改写成 C 代码如下:
void system_call()
{
...
// 变量 eax 代表 eax 寄存器的值
syscall = sys_call_table[eax];
eax = syscall();
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
sys_call_table 变量是一个数组,数组的每一个元素代表一个 系统调用 的入口,其定义如下(在文件 arch/i386/kernel/entry.S 中):
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open)
.long SYMBOL_NAME(sys_close)
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
翻译成 C 代码如下:
long sys_call_table[] = {
sys_ni_syscall,
sys_exit,
sys_fork,
sys_read,
sys_write,
sys_open,
sys_close,
...
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
用户调用 系统调用 时,通过向 eax 寄存器写入要调用的 系统调用 编号,这个编号就是 sys_call_table 数组的下标。 system_call 过程获取 eax 寄存器的值,然后通过 eax 寄存器的值找到要调用的 系统调用 入口,并且进行调用。调用完成后,系统调用 会把返回值保存到 eax 寄存器中。
原理如下图:
三、系统调用实现
当用户要调用 系统调用 时,需要通过向 eax 寄存器写入要调用的 系统调用 编号。因为 用户态 和 内核态 使用的栈不同,而调用 系统调用 是在用户态调用的,而进入 系统调用 后会变成内核态,所以参数就不能通过栈来传递。Linux 使用寄存器来传递参数,参数与寄存器的关系如下:
- 第1个参数放置在 ebx 寄存器。
- 第2个参数放置在 ecx 寄存器。
- 第3个参数放置在 edx 寄存器。
- 第4个参数放置在 esi 寄存器。
- 第5个参数放置在 edi 寄存器。
- 第6个参数放置在 ebp 寄存器。
而 Linux 进入中断处理程序时,会把这些寄存器的值保存到内核栈中,这样 系统调用 就能通过内核栈来获取到参数。
下面我们通过 sys_open() 系统调用来说明一下 系统调用 的运作方式,sys_open() 实现如下:
asmlinkage long sys_open(const char *filename, int flags, int mode)
{
...
}
- 1
- 2
- 3
- 4
一般 系统调用 都需要使用 asmlinkage 编译选项,asmlinkage 编译选项是告诉编译器从栈中读取参数,其实际是封装了 GCC 的编译选项,如下:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
- 1
attribute((regparm(0))) 就是告诉 GCC 所有参数都从栈中读取,而 Linux 进入中断处理上下文时,会把 ebx、ecx、edx、esi、edi、ebp 寄存器的值保存到内核栈中,那么 系统调用 就可以从内核栈获取到参数的值。
但由于寄存器只能传递 32 位的整型值(x86 CPU),所以参数一般只能传递指针或者整型的数值,如果要获取指针对应结构的数据,就必须通过从用户空间复制到内核空间,如 sys_open() 系统调用获取要打开的文件路径:
asmlinkage long sys_open(const char *filename, int flags, int mode)
{
char * tmp;
...
tmp = getname(filename);
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
getname() 函数就是用于从用户空间复制数据到内核空间。
四、总结
4.1 内核空间
系统调用的函数原型的指针:位于文件/kernel/arch/arm/kernel/calls.S,格式为CALL(sys_xxx),指定了目标函数的入口地址。
系统调用号的宏定义:位于文件/kernel/arch/arm/include/Uapi/asm/unistd.h,记录着内核空间的系统调用号,格式为#define__NR_xxx (__NR_SYSCALL_BASE+[num])
系统调用的函数声明:位于文件/kernel/include/linux/syscalls.h,格式为asmlinkage long sys_xxx(args …);
系统调用的函数实现:不同函数位于不同文件,比如kill()位于/kernel/kernel/signal.c文件,格式为SYSCALL_DEFINEx(x, sname, …)
前面这4步都是在内核空间相关的文件定义,有了这些,那么内核就可以使用相应的系统调用号。
4.2 用户空间
系统调用号的宏定义:位于文件/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h,记录着用户空间的系统调用号,格式为#define__NR_xxx (__NR_SYSCALL_BASE+[num])。这个文件就是由内核空间同名的头文件自动生成的,所以该文件与内核空间的系统调用号是完全一致。
汇编定义相关函数的中断调用过程:位于文件/bionic/libc/arch-arm/syscalls/xxx.S,比如kill()位于kill.S,格式为:
-
ENTRY(xxx)
-
mov ip, r7
-
ldr r7, =__NR_xxx
-
swi #0
-
mov r7, ip
-
cmn r0, #(MAX_ERRNO + 1)
-
bxls lr
-
neg r0, r0
-
b __set_errno_internal
-
END(xxx)
不同体系CPU寄存器不一样,X86处理器使用eax、ebx、ecx、edx、esi、edi、edp寄存器,ARM处理器使用r0-r15,spc/pr/ssr/gbr/mach/mac寄存器
自己理解:库函数调用到系统调用函数_syscall_xxx,用户态程序将系统调用号保存至寄存器,然后软中断程序从用户态切换到内核态,通过从寄存器读取系统调用编号,在中断向量表中找到系统调用函数入口CALL(sys_socket),asmlinkage标识通过内核堆栈传递参数,内核态完成sys_xxx(sys_socket)调用返回值继续保持在寄存器中。整个过程中程序执行从用户态堆栈切换到内核态堆栈,即特权级指令切换,而普通函数调用没有堆栈切换,函数调用参数直接传入堆栈,调用完返回值保存函数调用堆栈中。
后续理解待更新。。。