Linux内核原理之系统调用

文章目录

    • 系统调用
      • 与内核通信
      • API、POSIX和C库
      • 系统调用
        • 系统调用号
        • 系统调用的性能
      • 系统调用处理程序
        • 指定恰当的系统调用
        • 参数传递
      • 系统调用的实现
        • 实现系统调用
        • 参数验证
      • 系统调用上下文
        • 绑定一个系统调用的最后步骤
        • 从用户空间访问系统调用
    • 参考资料

系统调用

与内核通信

系统调用在用户空间进程和硬件设备之间添加了一个中间层,主要作用是:

  • 为用户空间提供了硬件的抽象接口
  • 保证了系统的稳定和安全,可以基于权限、用户类型和其他一些规则对需要进行的访问进行裁决

系统调用是用户空间访问内核的唯一手段,除了异常和陷入之外,它是内核唯一的合法入口

API、POSIX和C库

应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来变成,一个API定义了一组应用程序使用的编程接口,可以实现为一个或多个系统调用,或者完全不使用任何系统调用

POSIX、API、C库以及系统调用的关系如下图
Linux内核原理之系统调用_第1张图片

系统调用

系统调用号

在Linux中,每个系统调用被赋予了一个系统调用号。

系统调用号的特点:

  • 系统调用号一旦分配就不能再有变更,否则编译好的程序有可能崩溃
  • 如果系统调用被删除,所占用的系统调用号不允许被回收利用,否则以前编译过的代码会调用这个系统调用,但是却调用的另一个系统调用,Linux使用“未实现”系统调用sys_ni_syscall()来填补这种空缺,它除了返回-ENOSYS外不做任何工作

系统调用表sys_call_table,为每一个有效的系统调用指定了唯一的系统调用号

系统调用的性能

Linux系统调用比其他许多操作系统都要快,原因是:

  • Linux很短的上下文切换时间,进出内核被优化的很简洁
  • 系统调用处理程序和每个系统调用本身也很简洁

系统调用处理程序

通知内核的机制通过软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序

在x86系统上预定义的软中断是中断号128,通过int $0x80指令触发中断,这条指令触发一个异常导致系统切换到内核态并执行第128号异常处理程序(这个异常处理程序就是系统调用处理程序),它的名字是system_call()

指定恰当的系统调用

因为所有系统调用陷入内核的方式都一样,所以需要把系统调用号传给内核用于区分每种系统调用。

在x86上,系统调用号通过eax寄存器传递给内核,在陷入内核之前,用户空间把相应系统调用号放入eax中,system_call()函数将给定的系统调用号与NR_syscalls作比较来检查其有效性,如果大于或等于NR_syscalls,就返回-ENOSYS,否则,执行相应的系统调用

call *sys_call_table(, %rax, 8)

参数传递

系统调用额外的参数也是存放在寄存器传递给内核。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前5个参数,如果超过5个参数,需要用单独的寄存器存放所有指向这些参数在用户空间地址的指针

给用户空间的返回值也通过寄存机传递,在x86系统中,它存放在eax寄存器

Linux内核原理之系统调用_第2张图片

系统调用的实现

实现系统调用

实现的几个原则:

  • 尽量提供单一功能
  • 系统调用应该提供标志参数以确保向前兼容,扩展了功能和选项
  • 系统调用要考虑通用性,提供机制而不是策略

参数验证

由于系统调用在内核空间执行,为了保证系统的安全和稳定,系统调用必须仔细检查所有的参数是否合法

其中,最重要的一项就是检查用户提供的指针是否有效。在接收一个用户空间的指针之前,内核必须保证:

  • 指针指向的内存区域属于用户空间,进程决不能哄骗内核去读内核空间的数据
  • 指针指向的内存区域在进程的地址空间中,进程决不能哄骗内核去读其他进程的数据
  • 内存应该标记为对应的读、写或者可执行的权限,进程决不能绕过内存访问权限

内核提供两个方法完成必须的检查和内核空间与用户空间之间数据的来回拷贝,分别为copy_to_user()copy_from_user()

如果执行失败,这两个函数返回没能完成拷贝的数据的字节数;如果成功,返回0。两个方法都有可能阻塞,当包含用户数据的页被换出到磁盘而不再物理内存上时,就可能发生,此时进程休眠,知道缺页异常程序将改页换入物理内存

注意:内核无论何时都不能轻率地接受来自用户空间的指针!

最后一项检查:进程是否有对应系统调用的合法权限,如reboot()系统调用,需要确保进程拥有CAP_SYS_REBOOT功能

系统调用上下文

内核在执行系统调用事处于进程上下文,current指针指向当前任务,即引发系统调用的那个进程

在进程上下文中,内核可以休眠(比如在系统调用阻塞或者调用schedule()时),这说明了:

  • 系统调用可以使用内核提供的绝大多数功能
  • 系统调用的进程可以被其他进程抢占,新的进程可以使用相同的系统调用,因此需要保证系统调用是可重入的

绑定一个系统调用的最后步骤

注册一个正式的系统调用的过程为:

  • 在系统调用表的最后加入一个表项,从0开始递增
  • 对于所支持的各种体系结构,系统调用号都必须定义于
  • 系统调用必须被编译进内核映像(不能被编译成模块),只需要将它放进kernel/下的相关文件就可以

例如,一个虚构的系统调用foo()注册的过程:

  1. 将sys_foo加入到系统调用表中,对于大多数体系结构,该表位于entry.s文件,形式如下图,将新的系统调用.long sys_foo加入表的末尾

Linux内核原理之系统调用_第3张图片

  1. 将系统调用号加入到,格式如下图:

Linux内核原理之系统调用_第4张图片

然后在该表中加入一行

#define __NR_foo 338
  1. 最后,实现foo()系统调用,把实现代码放进kernel/sys.c文件中(asmlinkage限定词是一个编译指令,通知编译器仅从栈中提取该函数的参数)
asmlinkage long sys_foo()
{
    return THREAD_SIZE
}

从用户空间访问系统调用

用户程序除了通过标准头文件和C库链接来使用系统调用之外,Linux本身提供了一组宏,用于直接访问系统调用,它会设置好寄存器并调用陷入指令,这些宏的形式为_syscalln()(n的范围是0~6,代表传递给系统调用的参数个数)

例如,open()系统调用的定义是:

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

不依靠库的支持,直接调用此系统调用的宏形式为:

# define NR_open 5
_syscall3(long, open, const char*, filename, int, flags, int, mode)

每个宏都有2+2*n个参数,第一个参数对应系统调用的返回类型,第二个参数是系统调用的名称,之后就是系统调用参数顺序排列的每个参数的类型和名称。该宏会扩展成内嵌汇编的C函数,汇编语言执行将系统调用号压入寄存器并触发软中断陷入内核的过程

参考资料

  • Linux内核设计与实现
  • 深入Linux内核架构

你可能感兴趣的:(Linux内核)