系统调用

与内核通信

系统调用作为用户空间进程和硬件设备的中间层的三个主要作用:

  • 为用户空间提供了一种硬件的抽象接口
  • 系统调用保证了系统的稳定和安全
  • 每个进程都运行在虚拟系统中。

在Linux中,系统调用时用户空间访问内核的唯一手段;除异常和陷入外,它们是唯一的合法入口。


一般情况下,应用程序通过在用户空间实现的应用编程接口API而不是直接通过系统调用来编程。——应用程序使用的这种编程接口实际上并不需要与内核提供的系统调用对应。

*最流行的应用编程接口是基于POSIX标准

内核纸盒系统调用打交道,库函数和应用程序如何使用,内核是不关心的。但是内核必须时刻牢记系统调用所有潜在的用途,保证它们有良好的通用性和灵活性。

关于Unix设计的名言:提供机制而不是策略


访问系统调用(Syscall)通常使用C库中的函数,可能需要定义输入参数(0,1或多个),而且可能会有副作用。

系统调用的返回值,long型——兼容64位硬件结构。
通常:

一个负数的返回值表明错误
返回0表明成功

系统调用在出现错误C库会把错误码写入errno全局变量。通过调用perror(库函数),可以把该变量翻译成用户可以理解的错误字符段。

书上这里举了一个例子,需要注意的地方:

1)asmlinkage限定词,这是一个编程指令,通知编译器仅从栈中提取该函数的参数。

所有的系统调用都需要这个限定词。

2)系统调用get_pid()在内核中被定义成sys_getpid。这是Linux系统调用都应该遵守的命名规则。

系统调用号

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

注意:

一旦被分配不再有任何变更。
既是一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。
Linux有一个“未实现”的系统调用sys_ni_syscall(),它除了返回-ENOSYS以外不做任何工作——这个错误号专门针对无效的系统调用而设。

内核记录了系统调用表中所有已注册过的系统调用列表。存储在sys_call_tatble中。

在x86-64中定义与arch/i386/kernel/syscall_64.c文件中。这个表为每一个有效的系统调用指定了唯一的系统调用号。


Linux系统比其他许多操作系统执行得要快的原因:

Linux很短的上下文切换时间——进出内核都被优化得简洁高效
系统调用处理程序和每个系统调用本身都非常简洁。

系统调用的处理程序:

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

此时的异常处理程序实际上就是系统调用处理程序。

在x86系统上预定义的软中断是中断号128,通过int 0x80指令来触发该中断。

这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序——system_call()

在x86上,系统调用号是通过eax寄存器传递给内核的。

system_call()函数通过给定的系统调用号与NR_syscalls作比较来检查其有效性。如果它大于或等于NR_syscalls,该函数就返回-ENOSYS。否则执行相应的系统调用:

call *sys_call_table(,%rax,8)

由于系统调用表中的表项是以64位(8字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后所得的结果在该表中查询其位置。

参数传递

在x86-32系统上,ebx,ecx,edx,esi按照顺序存放在前五个参数,需要六个及以上参数使用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

系统调用的实现

应该考虑的问题:

它要做什么?
新系统调用的参数,返回值,错误码又应该是什么呢?(力求简洁,参数尽可能少)
系统的语义和行为十分关键(力求稳定,不做改动)
你是不是对函数做了没有必要的限制?(设计接口的时候要尽量为将来考虑)

时刻注意可移植性健壮性


参数验证

最重要的一种检查就是检查用户提供的指针是否有效。

在接受一个用户的指针之前,内核保证:

指针指向的内核区域属于用户空间
指针指向的内核区在进程的地址空间里
如果是读,该内存应被标记为可读,如果是写,该内存应被标记为可写;如果是可执行,该内存应被标记为可执行。

内核无论何时都不能轻易的接受来自用户空间的指针!


内核为了向用户空间写入数据,提供了copy_to_user().
三个参数:

1:进程空间中的内存地址
2:内核空间源地址
3:需要拷贝的数据长度(字节数)

内核为了向用户空间读取数据,提供了copy_from_user().
把第二个参数指定位置上的数据拷贝到第一个参数上,拷贝的数据长度由第三个参数决定。

如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。
如果成功则返回0,出现(内核保证的那几个)错误时,系统调用-EFAULT


检查针对是否有合法权限

capable()

检查是否有权能针对指定的资源进行操作:

返回非0,调用者就有权操作

返回值为0,无权操作。

系统调用上下文

在进程上下文中,内核可以休眠并可以被抢占这两点很重要:

  • 能够休眠说明系统调用可以使用内核提供的绝大部分功能。(中断处理程序不能休眠)
  • 在进程上下文中能够被抢占说明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。

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

1)首先,在系统调用表的最后加入一个表项

2)对于支持的各种体系结构,系统调用号都必须定于<asm/unistd.h>中。

3)系统调用必须被编译进内核映像(不能被编译成模块)。这只能把它放进kernel/下的一个相关文件中就可以了。

对于每种需要支持的体系结构,我们都必须将自己的系统调用加入到其系统调用表中去。每种体系结构都不需要对应相同的系统调用号。系统调用号是专属体系结构ABI(应用程序二进制接口)。


linux内核定义了一组宏_syscalln(),用于直接对系统调用的访问。它会设置好寄存并调用陷入指令。其中n的范围为1-6代表需要传递给系统调用的参数。

对于每个宏来说都有2+2*n个参数。第一个参数对应着返回值类型,第二个参数是系统调用的名称。再以后按照系统调用参数的顺序排列每一个参数的类型和名称。


最后,建立系统调用是需要慎重的

在享受新系统调用的好处前你需要看到的问题:

  • 你需要一个系统调用号,这需要一个内核在处于开发版本的时候官方分配给你。
  • 系统调用加入稳定内核之后被固化,为了避免应用程序的崩溃,它的接口不允许你这么做。
  • 需要将系统调用分别注册到每个需要支持的体系结构中去。
  • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。
  • 由于你需要系统调用号,因此在主内树外是很难维护和使用系统调用的。
  • 如果仅仅进行简单的信息交换,系统调用就大材小用了

代替方法:

  • 实现一个设备节点,并对此实现read()和write()。使用ioctl()对特定的设置进行系统操作。
  • 像信号量这样的某些接口,可以用文件描述符表示,因此也就可以按上述方式对其操作。
    -把增加信息作为一个文件放在sysfs的合适位置。