Linux 中系统调用过程分析及其应用

系统调用概述

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.SSystemCall() 函数中调用 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() 之间不允许再使用系统调用,且关闭中断的时间应该尽可能短,避免影响系统的实时性。

以上。

comments powered by Disqus