用户应用可以通过两种方式使用系统调用。第一种方式是通过C库函数,包括系统调用在C库中的封装函数和其他普通函数。
![]() |
图5.2 使用系统调用的两种方式 |
第二种方式是使用_syscall宏。2.6.18版本之前的内核,在include/asm-i386/unistd.h文件中定义有7个_syscall宏,分别是:
_syscall1(int, sysinfo, struct sysinfo *, info);
展开后的形式为:
int sysinfo(struct sysinfo * info)
{
long __res;
__asm__ volatile("int $0x80" : "=a" (__res) : "0" (116),"b" ((long)(info)));
do {
if ((unsigned long)(__res) >= (unsigned long)(-(128 + 1))) {
errno = -(__res);
__res = -1;
}
return (int) (__res);
} while (0);
}
展开后的形式为:
int sysinfo(struct sysinfo * info)
{
long __res;
__asm__ volatile("int $0x80" : "=a" (__res) : "0" (116),"b" ((long)(info)));
do {
if ((unsigned long)(__res) >= (unsigned long)(-(128 + 1))) {
errno = -(__res);
__res = -1;
}
return (int) (__res);
} while (0);
}
可以看出,_syscall1(int, sysinfo, struct sysinfo *, info)展开成一个名为sysinfo的函数,原参数int就是函数的返回类型,原参数struct sysinfo *和info分别构成新函数的参数。
在程序文件里使用_syscall宏定义需要的系统调用,就可以在接下来的代码中通过系统调用名称直接调用该系统调用。下面是一个使用sysinfo系统调用的实例。
可以看出,_syscall1(int, sysinfo, struct sysinfo *, info)展开成一个名为sysinfo的函数,原参数int就是函数的返回类型,原参数struct sysinfo *和info分别构成新函数的参数。
在程序文件里使用_syscall宏定义需要的系统调用,就可以在接下来的代码中通过系统调用名称直接调用该系统调用。下面是一个使用sysinfo系统调用的实例。
代码清单5.1 sysinfo系统调用使用实例
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <linux/unistd.h>
#include <linux/kernel.h> /* for struct sysinfo */
_syscall1(int, sysinfo, struct sysinfo *, info);
int main(void)
{
struct sysinfo s_info;
int error;
error = sysinfo(&s_info);
printf("code error = %d/n", error);
printf("Uptime = %lds/nLoad: 1 min %lu / 5 min %lu / 15 min %lu/n"
"RAM: total %lu / free %lu / shared %lu/n"
"Memory in buffers = %lu/nSwap: total %lu / free %lu/n"
"Number of processes = %d/n",
s_info.uptime, s_info.loads[0],
s_info.loads[1], s_info.loads[2],
s_info.totalram, s_info.freeram,
s_info.sharedram, s_info.bufferram,
s_info.totalswap, s_info.freeswap,
s_info.procs);
exit(EXIT_SUCCESS);
但是自2.6.19版本开始,_syscall宏被废除,我们需要使用syscall函数,通过指定系统调用号和一组参数来调用系统调用。
syscall函数原型为:
int syscall(int number, ...);
其中number是系统调用号,number后面应顺序接上该系统调用的所有参数。下面是gettid系统调用的调用实例。
代码清单5.2 gettid系统调用使用实例
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#define __NR_gettid 224
int main(int argc, char *argv[])
{
pid_t tid;
tid = syscall(__NR_gettid);
}
大部分系统调用都包括了一个SYS_符号常量来指定自己到系统调用号的映射,因此上面第10行可重写为:
tid = syscall(SYS_gettid);
系统调用执行过程
系统调用的执行过程主要包括如图5.3与图5.4所示的两个阶段:用户空间到内核空间的转换阶段,以及系统调用处理程序system_call函数到系统调用服务例程的阶段。
![]() |
图5.3 用户空间到内核空间 |
![]() |
图5.4 system_call函数到系统调用服务例程 |
(1)用户空间到内核空间。
如图5.3所示,系统调用的执行需要一个用户空间到内核空间的状态转换,不同的平台具有不同的指令可以完成这种转换,这种指令也被称作操作系统陷入(operating system trap)指令。
Linux通过软中断来实现这种陷入,具体对于X86架构来说,是软中断0x80,也即int $0x80汇编指令。软中断和我们常说的中断(硬件中断)不同之处在于-它由软件指令触发而并非由硬件外设引发。
int 0x80指令被封装在C库中,对于用户应用来说,基于可移植性的考虑,不应该直接调用int $0x80指令。陷入指令的平台依赖性,也正是系统调用需要在C库进行封装的原因之一。
通过软中断0x80,系统会跳转到一个预设的内核空间地址,它指向了系统调用处理程序(不要和系统调用服务例程相混淆),即在arch/i386/kernel/entry.S文件中使用汇编语言编写的system_call函数。
(2)system_call函数到系统调用服务例程。
很显然,所有的系统调用都会统一跳转到这个地址进而执行system_call函数,但正如前面所述,到2.6.23版为止,内核提供的系统调用已经达到了325个,那么system_call函数又该如何派发它们到各自的服务例程呢?
软中断指令int 0x80执行时,系统调用号会被放入eax寄存器,同时,sys_call_table每一项占用4个字节。这样,如图5.5所示,system_call函数可以读取eax寄存器获得当前系统调用的系统调用号,将其乘以4生成偏移地址,然后以sys_call_table为基址,基址加上偏移地址所指向的内容即是应该执行的系统调用服务例程的地址。
另外,除了传递系统调用号到eax寄存器,如果需要,还会传递一些参数到内核,比如write系统调用的服务例程原型为:
- sys_write(unsigned int fd, const char * buf, size_t count);
调用write系统调用时就需要传递文件描述符fd、要写入的内容buf以及写入字节数count等几个内容到内核。ebx、ecx、edx、esi以及edi寄存器可以用于传递这些额外的参数。
正如之前所述,系统调用服务例程定义中的asmlinkage标记表示,编译器仅从堆栈中获取该函数的参数,而不需要从寄存器中获得任何参数。进入system_call函数前,用户应用将参数存放到对应寄存器中,system_call函数执行时会首先将这些寄存器压入堆栈。
对于系统调用服务例程,可以直接从system_call函数压入的堆栈中获得参数,对参数的修改也可以一直在堆栈中进行。在system_call函数退出后,用户应用可以直接从寄存器中获得被修改过的参数。
并不是所有的系统调用服务例程都有实际的内容,有一个服务例程sys_ni_syscall除了返回-ENOSYS外不做任何其他工作,在kernel/sys_ni.c文件中定义。
asmlinkage long sys_ni_syscall(void)
{
return -ENOSYS;
}
sys_ni_syscall的确是最简单的系统调用服务例程,表面上看,它可能并没有什么用处,但是,它在sys_call_table中占据了很多位置。多数位置上的sys_ni_syscal都代表了那些已经被内核中淘汰的系统调用,比如:
long sys_ni_syscall /* old stty syscall holder */
long sys_ni_syscall /* old gtty syscall holder */
就分别代替了已经废弃的stty和gtty系统调用。如果一个系统调用被淘汰,它所对应的服务例程就要被指定为sys_ni_syscall。
我们并不能将它们的位置分配给其他的系统调用,因为一些老的代码可能还会使用到它们。否则,如果某个用户应用试图调用这些已经被淘汰的系统调用,所得到的结果,比如打开了一个文件,就会与预期完全不同,这将令人感到非常奇怪。
其实,sys_ni_syscall中的"ni"即表示"not implemented(没有实现)"。
系统调用通过软中断0x80陷入内核,跳转到系统调用处理程序system_call函数,并执行相应的服务例程,但由于是代表用户进程,所以这个执行过程并不属于中断上下文,而是处于进程上下文。
因此,系统调用执行过程中,可以访问用户进程的许多信息,可以被其他进程抢占(因为新的进程可能使用相同的系统调用,所以必须保证系统调用可重入),可以休眠(比如在系统调用阻塞时或显式调用schedule函数时)。
这些特点涉及进程调度的问题,在此不做深究,读者只需要理解当系统调用完成后,把控制权交回到发起调用的用户进程前,内核会有一次调度。如果发现有优先级更高的进程或当前进程的时间片用完,那么就会选择高优先级的进程或重新选择进程运行。
fork()与syscall0(int,fork) 关系
static inline _syscall0(int,fork)
其中_syscall0()是unistd.h中的内嵌宏代码,它以嵌入汇编的形式调用Linux的系统调用中断int0x80。根据include/unistd.h文件第133行上的宏定义,我们把这个宏展开并替代进上面一行中就可以看出这条语句实际上是intfork()创建进程系统调用,见如下所示。
// unistd.h文件中_syscall0()的定义。即为不带参数的系统调用宏函数:
type name(void)。
#define _syscall0(type,name)
type name(void)
{
long __res;
__asm__ volatile ("int $0x80"
//调用系统中断0x80。
: "=a"(__res) //返回值eax(__res)。
: "0"(__NR_##name)); //输入为系统中断调用号__NR_name。
if (__res >= 0) //如果返回值>=0,则直接返回该值。
return(type) __res;
errno = -__res; //否则置出错号,并返回-
return -1;
}
static inline int fork(void)
{
long __res;
__asm__ volatile ("int $0x80" : "=a" (__res) : "0"(__NR_fork));
if (__res >= 0)
return (int)__res;
errno = -__res;
return -1;
}
gcc会把上述“函数”体中的语句直接插入到调用fork()语句的代码处,因此执行fork()不会引起函数调用。另外,宏名称字符串“syscall0”中最后的0表示无参数,1表示带1个参数。如果系统调用带有1个参数,那么就应该使用宏_syscall1()。