Linux内核学习——系统调用

系统调用

在操作系统中,内核提供了用户进程与内核进行交互的一组接口。

与内核通信

系统调用在用户空间与硬件设备之间添加了一个中间层,该层主要有三个作用:

  • 提供硬件的抽象接口,这样用户就不需要理会硬件类型;
  • 保障系统的稳定和安全,内核可以基于权限等规则去对用户的访问进行裁决;
  • 因为每个进程都运行在虚拟内存中,如果允许用户进程随意访问硬件,就无法实现多任务和虚拟内存了;

API, POSIX和C库

API定义了一组应用程序使用的编程接口,它可以是一个系统调用,也可以是通过多个系统调用来实现。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。

系统调用

通常我们会通过C库中定义的函数来调用系统调用(syscall)。系统调用往往具有一个明确的操作,例如getpid()系统调用,它会返回当前进程,在内核中的实现为:

1
2
3
4
5
6
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);//这个宏定义了一个无参数的系统调用,因此这里数字为0
}
//展开后
asmlinkage long sys_getpid(void)

函数声明中的asmlinkage为一个编译指令,通知编译器仅仅从栈中提取参数,其次,为了保证32位和64位的兼容,应该返回long(调用失败返回负数,返回0通常表明成功)。另外,在调用出现错误的时候,C库会把错误码写入errno这个全局变量。

系统调用号

在Linux中,每个系统调用都被赋予一个系统调用号,而且系统调用号不能变更,也不能删除。另外,为了避免一个系统调用被删除或者出现问题后不可用,Linux用一个“未实现”的系统调用sys_ni_syscall()来填补空缺。

内核在arch/i386/kernel/syscall_64.c中为已经注册过的系统调用表存储系统调用号——sys_call_table。

系统调用的性能

Linux系统调用通常非常快:

  • 上下文切换时间较短;
  • 操作本身比较简洁;

系统调用处理程序

由于用户空间的程序无法直接执行内核代码,所以需要某种机制保证用户去通知内核执行某个系统调用。

这个机制是用软中断实现的,通过引发一个异常(int $0x80指令)使得系统切换到内核态去执行异常处理程序(第128号异常处理程序),而这个异常处理程序实际上就是系统调用处理程序——system_call()。

指定恰当的系统调用

刚刚的操作只是切换到内核态,但我们仍然需要告诉内核用户进程的系统调用号,在x86里,该系统调用号是通过寄存器eax传递的。在陷入内核态之前将系统调用号存放到eax里,这样就可以在系统调用程序运行的时候拿到数据。

在拿到系统调用号后,system_call()就会将系统调用号和NR_syscalls作比对,检查有效性。

参数传递

多数的系统调用需要一些外部的参数输入,而这些参数的输入通常也是存放在寄存器里传递的,例如前五个参数通过ebx,ecx,edx,esi和edi这五个寄存器传递,如果参数超过5个,就存放一个指向参数所在用户空间的地址。

返回值也是通过寄存器eax传递。

系统调用上下文

内核在执行系统调用的时候处于进程的上下文,并且在进程上下文中,内核可以休眠且可以被抢占。