系统调用概述
Linux 系统对系统调用的支持从 C 库函数开始,内核提供的每个系统调用在 C 库中都具有相应的封装函数,且二者名称常常相同,如 read
系统调用在 C 库中的封装函数即为 read()
函数。但系统调用和 C 库函数之间并不是一一对应的关系,可能几个不同的库函数共享同一个系统调用,如 malloc()
函数和 free()
函数都是通过 brk
系统调用来扩大或缩小进程的堆栈;也有可能一个 C 库函数调用多个系统调用。更有些 C 库函数不需要使用系统调用就能实现需要的功能,如 strcpy()
函数和 atoi()
函数等。
在 Linux 系统中,需要使用系统调用的库函数,将在库函数内调用处理器提供的系统调用指令,只要处理器支持操作系统的系统调用功能,都有类似的这种指令,PowerPC 处理器有一条「sc 指令」是为系统调用定制的,Intel i386 处理器则使用「int 0x80」实现类似的功能。如果在处理器中没有这种指令,系统调用的功能将无法实现。下面将以 PowerPC 为例说明系统调用的执行流程。
系统调用执行流程
在 unistd.h
中定义了 Linux 系统支持的所有的系统调用号。下图为系统调用的大致执行流程(以 write()
函数为例):
在应用程序中调用 write()
函数后,在 write()
函数中调用了 sc
汇编指令,PowerPC 产生系统调用异常,Linux 截获处理器产生的系统调用异常。在 Linux 系统启动时,在 head_fsl_booke.S
中使用 SET_IVOR(8, SystemCall);
将函数 SystemCall()
作为系统调用异常处理函数。Linux 截获到系统调用异常后,就调用 SystemCall()
函数。 SystemCall()
函数先检查传入的参数,判断是否我们自己定制的系统调用号等。若是,则调用定制的函数后使用 RFI
指令返回;否则,调用 DoSyscall()
函数。
DoSyscall()
函数在 entry_32.S
文件中定义。所有系统调用进入 DoSyscall()
函数后,使用 sys_call_table
变量的值作为系统调用表的基地址,再加上根据系统调用号(寄存器 r0
的值)计算出的偏移量,得到对应的系统调用服务程序的地址并执行。 sys_call_table
变量在 systbl.S
文件中定义,此文件又包含了 systbl.h
文件。 systbl.h
文件就是系统调用表。其中的系统调用服务程序存放的(相对)顺序需要与 unistd.h
文件中定义的系统调用号一致。在 systbl.h
文件中通过宏将 sys_*()
函数加入系统调用表。
系统调用的参数传递
在 DoSyscall()
函数之前,参数都是通过寄存器传递的。其中, r0
用于传递系统调用号, r3
及其后几个通用寄存器用于传递真正的函数参数。
系统调用服务程序(如 sys_write()
)的定义中,都加了 asmlinkage
标记。这个标记的作用是,告诉编译器不要在寄存器中查找这个函数的参数,而只在栈中查找。
在 DoSyscall()
函数中,将各个参数压入栈中,并调用相应的系统调用服务程序。加了 asmlinkage
标记的系统调用服务程序就只从栈中查找各个参数。
实践情况
根据系统调用执行流程的特点,以实现开启和关闭外部中断为例,说明如何增加自己的系统调用。
在 unistd.h
中定义了一个宏 NR_syscalls
,用于存放系统调用的总数。若自己增加系统调用,首先需相应地递增这个宏的值,然后再增加系统调用号。现增加两个系统调用,修改如下:
#define __NR_fuwq 308 /* 等待队列 */ #define __NR_fuirq 309 /* 中断请求 */ #define __NR_syscalls 310
再编写两个对应的系统调用服务程序( sys_fuwq()
、 sys_fuirq()
放在 fuwq.c
中),并在 systbl.h
中添加如下:
#ifdef CONFIG_UF_WQ SYSCALL_SPU(fuwq) #else SYSCALL(ni_syscall) #endif #ifdef CONFIG_UF_FSA SYSCALL_SPU(fuirq) #else SYSCALL(ni_syscall) #endif
这样,内核部分就修改完成。还需要添加两个用户态的接口函数 intLock()
和 intUnlock()
,充当 C 库函数的作用。这两个函数分别调用 sc_fuirq_save()
函数和 sc_fuirq_restore()
函数,如下:
#define FSA_OP_IRQ_SAVE 0x7676 #define FSA_OP_IRQ_RESTORE 0x7878 #define sc_fuirq_save() \ ({ \ INTERNAL_SYSCALL_DECL (__err); \ long int __ret; \ \ __ret = INTERNAL_SYSCALL (fuirq, __err, 2, \ FSA_OP_IRQ_SAVE, 0); \ INTERNAL_SYSCALL_ERROR_P (__ret, __err) ? -__ret : __ret; \ }) #define sc_fuirq_restore(flags) \ ({ \ INTERNAL_SYSCALL_DECL (__err); \ long int __ret; \ \ __ret = INTERNAL_SYSCALL (fuirq, __err, 2, \ FSA_OP_IRQ_RESTORE, flags); \ INTERNAL_SYSCALL_ERROR_P (__ret, __err) ? -__ret : __ret; \ })
在 intLock()
函数与 intUnlock()
函数中,使用系统调用号 309(__NR_fuirq
),但并没有实现具体的 sys_fuirq()
函数,而只是在 head_fsl_booke.S
的 SystemCall()
函数中调用 SYSCALL_EXCEPTION_PROLOG
做了特殊处理,判断出系统调用号是 309,再根据调用 intLock()
或 intUnlock()
函数时传入的第一个参数,判断是对中断做关闭(0x7676)还是开启(0x7878)操作。最后调用 RFI
指令返回。因此 sys_fuirq()
函数并不会被调用。
在 SYSCALL_EXCEPTION_PROLOG
中,通过修改 SRR1
寄存器实现将 EE
位的置 1 操作( SRR1
寄存器的位定义与 MSR
寄存器完全相同)。当调用 sc
指令后, MSR
寄存器会被复制到 SRR1
中,若直接修改 MSR
寄存器的 EE
位,在调用 rfi
指令后, SRR1
的内容又被复制回 MSR
,覆盖掉对 EE
位的修改,不能实现锁中断。
这样修改以后,在应用程序中使用 intLock()
就可以实现关闭外部中断功能,使用 intUnlock()
重新开启外部中断。另外,由于 intLock()
/ intUnlock()
是使用系统调用方式实现的,因此在 intLock()
和 intUnlock()
之间不允许再使用系统调用,且关闭中断的时间应该尽可能短,避免影响系统的实时性。
以上。
Originally published at https://blog.lancitou.net on November 21, 2010.