文章目录
- 内核通信
- API、POSIX、C库
- 系统调用
- 系统调用号
- 系统调用的性能
- 系统调用处理程序
- 指定恰当的系统调用
- 参数传递
- 系统调用的实现
- 实现系统调用
- 参数验证
- 系统调用上下文
- 绑定一个系统调用的最后步骤
- 从用户空间绯闻系统调用
内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。
API、POSIX、C库
通常,应用程序通过在用户空间实现的应用变成接口(API)来进行系统调用,即应用程序调用 C 库,而库函数内部进行系统调用,而不是直接调用系统调用。
POSIX、API、C 库以及系统调用之间的关系:
在 Unix 中,最流行的的应用编程接口是基于 POSIX 标准的。
系统调用
要访问系统调用(在Linux中常称作 syscall),通常通过 C 库内部进行系统调用。
系统调用号
在 Linux 中,每个系统调用都被赋予了一个系统调用号。这个系统调用号用于绑定系统调用,是系统调用的唯一标识符。调用系统调用的时候也需要通过该系统调用号来寻找系统调用。
系统调用号一旦分配就不可变更,否则编译好的应用程序就会崩溃。此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用,否则以前编译过的代码会调用这个系统调用,但事实上是调用的另一个系统调用。
sys_ni_syscall() 是 Linux 未实现的系统调用,用于给不可用的系统调用占位,即填补空缺。
记录了所有已注册的系统调用的列表:sys_call_table,是个数组,数组索引便是系统调用号,其所有的元素都是函数指针。
位于:arch/i386/kernel/syscall_64.c
系统调用的性能
Linux 的系统调用比其它操作系统执行得要快,因为:
- Linux 上下文切换的时间很短。
- Linux 系统调用处理程序和每个系统调用本身也都非常简洁。
系统调用处理程序
用户空间无法直接进行系统调用,而是需要通过先访问调用库,再通过库内部进行系统调用。应用程序通过软中断通知内核进行系统调用,在 x86 系统上预定义的软中断是中断号 128,通过 int $0x80 指令触发该中断。通过这个指令从用户态陷入到内核态,执行第 128 号中断处理程序,而这个程序便是系统调用处理程序,名为 system_call()。它的实现与硬件体系结构密切相关,x86-64 系统在 entry_64.S 文件中使用汇编实现。
知识扩展:sysenter 指令
指定恰当的系统调用
通过软中断从用户态陷入到内核态,此外陷入前我们还需要传入一个参数,该参数便是系统调用号,用于指定需要调用的那个系统调用。该参数放入到 eax 寄存器中。
参数传递
除了系统调用外,有时还需要其它参数,所以在陷入内核的时候都应该从用户空间传给内核。
在 x86-32 系统上,ebx、ecx、edx、esi 和 edi 按照顺序存放前五个参数。需要六个及以上的情况很少,此时应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
返回值也通过寄存器传递,在 x86 系统上,它存放在 eax 寄存器中。
系统调用的实现
实现系统调用
- 明确该系统调用的功能。
- 考虑参数、返回值、错误码是什么。
- 系统调用的接口应该力求简洁,参数尽可能少。
- 设计接口的时候要尽量为将来做考虑。
- 系统调用设计越通用越好。
- 减少对函数不必要的限制。
- 不要假设该系统调用现在怎么用的将来就一定这么用。
- 系统调用的目的可能不变,但用法可能改变。
- 该系统调用是否可移植?
- 别对机器的字节长度和字节序做假设。
- 写系统调用时,要时刻注意可移植性和健壮性,不但要考虑当前,也要考虑将来。
参数验证
最重要的一种检查是用户提供的指针是否有效。内核得到一个错误的指针后果是很重要的。
在接收一个用户空间的指针之前,内核必须保证:
- 指针指向的内存区域属于用户空间。(进程决不能欺骗内核去读取内核空间的数据)
- 指针指向的内存区域在进程的地址空间里。(进程决不允许欺骗内核去读其它进程的数据)
- 若是读,该内存应被标记为可读;若是写,该内存应被标记为可写;若是可写可读,则也同理。进程决不允许绕开内存访问限制。
内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。
- copy_to_user()
- copy_from_user()
最后一项检查是检查是否有访问权限。内核提供了函数如下:
- suser():检查用户是否为 Root。
- capable():检查对某个资源是否有访问权限。
capable 举例:capable(CAP_SYS_NICE) 检查调用者是否有权改变其它进程的 nice 值。
include/linux/capability.h
系统调用上下文
内核在执行系统调用的时候处于进程上下文。current 指向当前任务,即引发系统调用的那个进程。
在进行上下文中,内核可以休眠,并且可以被抢占,这里指的都是陷入内核后系统调用的那个进程,而不是用户态的用户进程。
能休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力给内核编程带来了极大遍历。
能被抢占表名,像用户空间内的进程一样,当前的进程同样可以被其它进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,要保证该系统调用是可重入的。
当系统调用返回的时候,控制器仍然在 system_call() 中,它最终会负责切换到用户空间,并让用户进程继续执行下去。
注意:中断处理程序不能休眠,这使得中断处理程序所能进行的操作较之运行在进程上下文中的系统调用所能进行的操作受到了极大的限制。
绑定一个系统调用的最后步骤
-
在系统调用表添加一个表项。表中的位置序号(索引)就是对应的系统调用号。
位置:
arch/m32r/kernel/syscall_table.S
-
对于所支持的各种体系结构,系统调用号都必须定义于
arch/alpha/include/asm/unistd.h
中。 -
系统调用必须被编译进内核映像(不能被编译成模块)。这只要把它放进 kernel 下的一个相关文件中就可以了,如 sys.c 它包含了各种各样的系统调用。
演示: 新增系统调用 foo(),此时这个函数只是系统调用的具体实现,是在内核中的。
第一步:在系统调用表中添加一个表项。
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call*/
.long sys_exit
.long sys_fork
.long sys_read
; ...
.long sys_ni_syscall /* Reserved for sys_vserver */
.long sys_ni_syscall /* Reserved for sys_mbind */
.long sys_ni_syscall /* Reserved for sys_get_mempolicy */
.long sys_ni_syscall /* Reserved for sys_set_mempolicy */
.long sys_mq_open
.long sys_mq_unlink
.long sys_mq_timedsend
.long sys_mq_timedreceive /* 280 */
.long sys_mq_notify
.long sys_mq_getsetattr
.long sys_ni_syscall /* reserved for kexec */
.long sys_waitid
.long sys_ni_syscall /* 285 */ /* available */
; ...
.long sys_foo /* 新增的表项 */
第二步:将系统调用号添加到 unistd.h 中。
#define __NR_osf_syscall 0 /* not implemented */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_osf_old_open 5 /* not implemented */
// ...
#define __NR_preadv 490
#define __NR_pwritev 491
#define __NR_rt_tgsigqueueinfo 492
#define __NR_perf_event_open 493
#define __NR_foo 494 // 新增的
第三步:实现 foo() 函数,即实现 foo() 系统调用。该系统调用必须编译到内核中,因此需要放到 kernel 下,这里我们放到 kernel/sys.c 中。
asmlinkage long sys_foo(void) {
return THREAD_SIZE;
}
完成了后,启动内核在用户空间就可以调用 foo() 系统调用了。
从用户空间绯闻系统调用
若靠C库来间接的进行系统调用,可以通过 _syscallX()
进行系统调用,这时 Linux 提供的一组宏。X 的绯闻从 0 到 6,代表传递给系统调用的参数个数。