1. 系统调用

内核提供的用户空间和内核进行交互的一组接口,应用程序受限地访问硬件设备,提供创建新线程并与已有的进程进行通信,也提供了申请操作系统其他资源的能力。

1.2 系统调用形式

asmlinkage long sys_getpid(void)

asmlinkage限定词是一个编译指令,通知编译器仅从栈中提取该函数的参数,每个系统调用都需要这个限定词。

为了兼容32位和64位系统,函数返回long。

1.3 用户空间访问内核方式

  1. 系统调用
  2. 异常
  3. 陷入

1.4 系统调用作用

  1. 为用户空间提供了一个硬件的抽象接口,应用程序不需要管磁盘的类型和硬件介质,甚至文件系统类型。
  2. 保证系统的稳定和安全,内核可基于权限、用户类型和其他规则对需要进行的访问进行裁决。
  3. 保证系统可以实现虚拟内存和多任务,应用程序若是随意访问硬件内核不知道,那无法保证每个程序都运行在虚拟系统。

1.5 系统调用号

每个系统调用对应一个系统调用号,进程不会提及系统调用的名称,通过系统调用号指明是哪个系统调用。Linux系统中的sys_ni_syscall()是专门针对无效的系统调用而设立,如果一个系统调用被删除或者不可用,这个函数将负责“填补空缺“。内核将所有的已注册过的系统调用和系统调用号,存储在sys_call_table中。

2. 系统调用性能

Linux系统调用比其他许多操作系统执行得要快,得益于以下三个方面简洁高效的设计。

  1. 进出内核的上下文切换
  2. 系统调用处理程序
  3. 系统调用本身

2.1系统调用处理程序

内核运行在受保护的地址空间,用户空间的应用程序无法直接执行内核代码, 而是通过软中断通知内核去执行系统调用的机制。软中断是通过引发一个异常来促使系统切换到内核态去执行异常处理程序。在x86系统预定义的软中断的中断号是128,通过int $0x80指令触发该中断,该指令触发一个异常导致系统切换到内核态并执行第128号异常处理程序,即系统调用处理程序system_call()。sysenter指令比int中断指令更快、更专业陷入内核。

2.1.1指定恰当的系统调用

在x86-64中,系统调用号是通过eax寄存器传递给内核,用户空间将需要的系统调用号放入eax寄存器,系统调用处理程序(system_call)一旦运行就从eax寄存器中获取系统调用号。 system_call()函数通过从eax寄存器获取的系统调用号与NR_syscalls进行比较来检查其有效性,当它大于或等于NR_syscalls,函数返回-ENOSYS,否则执行相应的系统调用call *sys_call_table(, %rax, 8)。由于系统调用表的表项是以64位(8字节)存放,所以需要将给定的系统调用号乘以4。

2.1.2参数传递

系统调用的参数传递也是需要放在寄存器,在x86-32系统中,参数存储在ebx、ecx、edx、esi和edi寄存器,超过五个或以上, 存储在一个单独的寄存器指向所有参数在用户空间地址的指针。

2.1.3参数验证

系统调用必须检查传递的参数是否合法有效,最重要的一种检查是检查用户提供的指针是否有效。

内核必须保证:

  1. 指针指向的是用户空间,进程不能让内核去读内核的空间的数据。
  2. 指针指向的是进程的地址空间, 进程不能让内核去读其他进程的数据。
  3. 进程不能绕过内存访问限制,内存的可读、可写和可执行。

内核和用户空间之间的数据来回拷贝,如下两个函数:

  1. copy_to_user()函数:向用户空间写入数据
  2. copy_from_user()函数: 从用户空间读取数据

注意:copy_to_user()和copy_from_user()可能引起阻塞,当用户数据缺页被换出到硬盘,而不在物理内存中。线程会进入休眠状态, 直到缺页处理程序将硬盘重新换回到物理内存。

最后,将针对检查是否有合法权限。

  1. 在老版本Linux内核中,需要超级用户权限的系统调用是通过suser()函数来检查用户是否为超级用户。
  2. 现在,采用更细粒度的权能机制对特殊资源进行权限管理,全部的权能操作在<linux/capability.h>可找到,包含系统能够理解的所有权能,在不修改内核源代码情况下,驱动工程师和系统管理员就无法定义新的权能。内核专门为许可管理使用权能并导出了两个系统调用capget和capset。

2.2系统调用上下文

内核执行系统调用时处于进程上下文,current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核是可以休眠(例如系统调用阻塞和显性调用schedule())并且可以被抢占。当被拥有相同系统调用的进程抢占,必须保证系统调用是可重入的。

当系统调用返回时,控制权还是在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去。

2.2.1向内核绑定一个系统调用

  1. 在系统调用表中添加表项,一般是在entry.s文件,每一个硬件架构都会有一个单独系统调用表,不同硬件架构的系统调用号对应系统调用是不一样的,每个支持该系统调用的硬件架构都得往系统调用表中添加表项,系统调用在该表的位置即为系统调用号,从0开始递增。
  2. 对于所支持的各种体系结构, 系统调用号都必须定义在<asm/unistd.h>。
  3. 系统调用必须编译进去内核映象(不能编译成模块),只要放在kernel/下的相关文件即可。

2.2.2从用户空间访问系统调用

Linux本身提供了一组宏,直接可以访问系统调用,它会设置好寄存器并调用陷入指令。这些宏是__syscalln(),n的范围从0到6,代表传递给系统调用的个数。

例如:

long open(const char *filename, int flag, int mode)

用户空间通过宏直接调用open()系统调用的形式为:

#define NR_open 5

//对于宏来说, 参数个数为2+2*n
__syscall3(long,open, const char*, filename,int,flags,int,mode)

3. 实现并调用一个系统调用

  1. 先将系统调用添加到所支持的硬件架构的系统调用表。例如sys_foo在系统调用表中第400的位置。
.long sys_foo
  1. 在<asm/unistd.h>中加入系统调用号。
#define __NR_foo 400
  1. 系统调用必须编译进去内核映象,例如sys_foo()放在<kernel/sys.c>中。
#include <asm/page.h>

/*
 * sys_foo
 * 返回每个进程的内核栈大小
 */
asmlinkage long sys_foo(void)
{
    return THREAD_SIZE;
}
  1. 应用程序在用户空间中调用这个系统调用,前三步骤是在内核中实现一个系统调用。
#define __NR_foo 400
__syscall0(long,foo)

int main()
{
    long stack_size;
    stack_size = foo();
    return 0;
}