文章参考Linux内核修炼之道。
1. 系统调用概念
大家都知道操作系统的作用是管理计算机的软硬件资源,但是操作系统要向用户提供各种各样的服务,而用户应用程序访问这些服务的方式就是通过系统调用。但是一般来说,我们都是通过操作系统封装好的API(应用编程接口)来间接使用系统调用的。比如在Windows编程中,你可以调用WinAPI,他以C库的形式给出。比如在Linux当中,我们调用的read函数:int read(int handle, void *buf, int nbyte), 我们一般成为我们在使用系统调用read,因为相对应标准C函数库里面有一个fread。但是,实际上我们并没有直接的实现系统调用,我们通过系统封装的read函数去调用了read的系统调用。因为他们名字相同,是对应的关系,所以我们很容易误解。但是系统调用和封装的系统API函数并不是一一对应的关系,比如系统的execl、execlp、execle、execv、execvp、execve等都是通过execve系统调用来执行一个可执行文件的。系统调用最终必须有明确的操作,用户应用程序通过系统调用进入内核后,会执行各个系统调用对应的内核函数,即系统调用的服务例程。比如系统调用getpid的服务例程是内核函数sys_getpid。
2. 系统调用表
内核提供哪些内核函数是对外可用的呢,换句话说内核提供哪些系统调用的服务例程呢?这个在系统调用表里面sys_call_table,他存储了所有的系统调用对应的服务例程的函数地址,在arch/i386/kernel/syscall_table.S文件中,如下:
ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open /* 5 */ .long sys_close .long sys_waitpid .long sys_creat .long sys_link .long sys_unlink /* 10 */
... ...可以很清楚的看到,所有的系统调用都是遵循一定的命名规范的,即在系统名称之前加"sys_"前缀,比如exit系统调用对应的系统服务函数为sys_exit。另外系统总共对外提供的系统调用服务是非常有限的,我们可以看到只有两三百个而已。
3. 系统调用号
系统调用号是什么呢?上述的每个系统服务例程都有一个系统调用号,用户是通过系统调用号来调用这些服务例程而不是通过系统调用的名称。上述的系统调用表存储了所有系统调用的服务例程的地址,系统调用的过程就是从系统调用表中获取地址去执行。那么系统调用符合就必然要和系统调用号联系到一起,这就涉及到另外一个文件,在include/asm-i386/unistd.h文件。
#define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10跟上述的系统调用表一比较,发现除了前缀一个是sys_一个是__NR_外,他们的名称是一模一样的,而且关键他们的顺序也是相同的。不错,内核就是将系统调用号作为下标去获取系统调用表中的系统调用服务例程的函数地址的。
4. 系统调用服务例程
所有的系统调用服务例程集中声明在include/linux/syscalls.h这个文件当中,但是他们的定义分布在不同的文件中。比如getpid系统调用,他的系统服务例程sys_getpid在kernel/timer.c文件中定义:
asmlinkage long sys_getpid(void) { return current->tgid; }asmlinkage标记的含义是仅从堆栈中获取该数据,不从寄存器及其他地方获取。
5. 系统调用的使用
(1). 通过调用C语言库封装函数,比如前面所述的read函数。
(2). 通过syscall函数,原型为int syscall(int number, ...)。
具体可以通过以下getpid的系统调用来展示系统调用的使用。
#include <unistd.h> #include <sys/syscall.h> #include <sys/types.h> //可以从系统调用号文件中查看 #define __NR_gettid 224 int main() { pid_t = tid; tid = syscall(__NR_gettid); printf("call gettid through syscall function, gettid is %d\n", tid); tid = getpid(); printf("call gettid through getpid function, gettid is %d\n", tid); return 0; }
另外大部分的系统调用都包含了一个SYS_符号常量的宏定义来制定调用号,所以上述syscall函数也可以采用如下方式:
tid = syscall(SYS_gettid);
6. 为什么要使用系统调用
(1)系统调用可以为用户空间提供访问硬件资源的统一接口,以至于应用程序不必理会具体硬件访问操作。比如,读写文件时,应用程序不用去关心磁盘的类型,不用关心是什么文件系统,等等。
(2)系统调用对系统有保护作用,保证系统的安全性和稳定性。系统调用规定了用户进程进入内核的具体方式。用户只能通过这种方式安全的进入内核,而不准任意在跳入内核,因而可以保护内核的安全性。