包括 linux,大多现代操作系统都提供了用户进程和内核交互的接口。通过这些接口,用户进程能够在内核的监督下访问硬件设备,创建新进程或者与其他进程通信。可以说,这些接口充当了用户进程和内核的中转站。
在内核的监督下,可以避免用户进程的肆意妄为,做出一些损害系统的事情。
在 linux 中,除了异常和陷入,系统调用是用户空间访问内核空间的唯一合法手段。
系统调用介于用户空间和内核空间之间,充当信息转达中间人的角色。有了系统调用,内核可以基于用户所在组等权限信息决定是否响应用户空间的操作,从而保证系统的安全性。
此外,系统调用层可以为硬件虚拟出一套通用的接口供用户空间的进程使用。例如,用户空间进程采取系统调用读写文件,就无需关心磁盘的类型、介质、甚至文件系统了,系统调用会根据不同的情况自适应的完成需求。
而且,如果用户空间进程不通过系统调用直接访问硬件,linux 内核就无法得知这些信息,也就无法实现多任务的协调安排,整个系统的稳定性就无从谈起。
一般来说,用户空间进程并不直接使用系统调用,而是通过同样在用户空间实现的 API(Application Program Interface,应用程序接口)使用。定义一个 API 可以使用一个系统调用,也可以使用多个系统调用,不使用任何系统调用也是可以的。对于C语言程序开发来说,这些 API 主要由 C 库提供。
这样的设计其实很清晰,程序员并不需要关心系统调用,只需要使用好 API 就可以了。而 linux 内核则不需关心 API,只需要处理好系统调用即可。
系统调用处理程序
之前我们讨论过,进程运行在虚拟空间里,linux 内核空间则驻留在受保护的地址空间里,所以用户空间进程无法直接调用内核空间里的函数。
如果用户空间进程需要执行一个系统调用,只能以某种方式向 linux 内核申请,之后内核才有可能代表用户进程在内核空间执行系统调用。
例如,用户空间进程可以利用软中断机制实现,通过引发一个异常来促使系统切换到内核态执行异常处理程序,这里的“异常处理程序”其实就是系统调用处理程序。
linux 内核已支持的系统调用都有一个唯一的系统调用号,例如:
8 #define __NR_restart_syscall 0
9 #define __NR_exit 1
10 #define __NR_fork 2
11 #define __NR_read 3
12 #define __NR_write 4
13 #define __NR_open 5
14 #define __NR_close 6
15 #define __NR_waitpid 7
16 #define __NR_creat 8
17 #define __NR_link 9
18 #define __NR_unlink 10
19 #define __NR_execve 11
20 #define __NR_chdir 12
21 #define __NR_time 13
22 #define __NR_mknod 14
23 #define __NR_chmod 15
24 #define __NR_lchown 16
25 #define __NR_break 17
26 #define __NR_oldstat 18
27 #define __NR_lseek 19
...
用户空间进程申请系统调用时,可以把系统调用号和参数通过寄存器传递给内核,这样内核就能知道该执行哪一个系统调用。程序员也可以直接调用 syscall() 函数申请系统调用,该函数的使用手册可以通过 man 命令查询:
定义自己的系统调用
重新定义一个系统调用并不难,以 my_fun() 作为新定义的系统调用函数名为例。首先,将其加入系统调用表:
18 ENTRY(sys_call_table)
19 .long sys_restart_syscall /* 0 - old "setup()" system call*/
20 .long sys_exit
21 .long sys_fork
22 .long sys_read
23 .long sys_write
24 .long sys_open /* 5 */
...
343 .long sys_fallocate
344 .long sys_timerfd_settime /* 325 */
345 .long sys_timerfd_gettime
346 .long sys_my_fun
注意,为了不影响原系统调用,应将其加到表的最后。另外,虽然没有明确指明系统调用号,但是系统仍然可以根据表的顺序自动计算出 my_fun() 的系统调用号。
接着,将系统调用号加入到 asm/unistd_32.h 里:
8 #define __NR_restart_syscall 0
9 #define __NR_exit 1
10 #define __NR_fork 2
11 #define __NR_read 3
12 #define __NR_write 4
13 #define __NR_open 5
...
329 #define __NR_signalfd 321
330 #define __NR_timerfd_create 322
331 #define __NR_eventfd 323
332 #define __NR_fallocate 324
333 #define __NR_timerfd_settime 325
334 #define __NR_timerfd_gettime 326
335 #define __NR_my_fun 327
...
准备工作做好以后,就可以写实现 my_fun() 的C语言代码了:
#include <asm/page.h>
asmlinkage long sys_my_fun(void)
{
// do something
return 0;
}
写好 my_fun() 的C语言代码后,可以将其放在功能相关的文件里。至此,我们就实现了一个新的系统调用。需要说明的是,若想使用 my_fun() 系统调用,必须先将其编译进内核映像,不能编译成模块。
慎用系统调用
容易看出,定义一个新的系统调用非常简单,而且 linux 系统调用的性能也非常高效,但是仍然应尽量使用其他方法代替新建一个系统调用。
新建系统调用需要系统调用号,虽然可以将其放在系统调用表最后,但是不能保证不会与以后的 linux 官方版本新定义的系统调用冲突。
而且,系统调用一旦加入内核,就被固化了,为了保持兼容性,可能之后很多年都不允许修改。此外,因为系统调用不能从文件系统直接访问,所以一些脚本也就不容易调用该功能了。