15.4系统调用进程同内核交互是通过一组定义好的函数来进行的,这些函数称为系统调用。在讨论支持网络的系统调用之前,我们先来看看系统调用机制的本身。从进程到内核中的受保护的环境的转换是与机器和实现相关的。在下面的讨论中,我们使用Net/3在386上的实现来说明如何实现有关的操作。

在BSD内核中,每一个系统调用均被编号,当进程执行一个系统调用时,硬件被配置成仅传送控制给一个内核函数。将标识系统调用的整数作为参数传给该内核函数。在386实现中,这个内核函数为syscall。利用系统调用的编号,syscall在表中找到请求的系统调用的sysent结构。表中的每一个单元均为一个sysent结构。


struct sysent {

    int sy_narg; /* number of arguments */

    int (*sy_call) (); /* implementing function */

}; /* system call table entry */

表中有几个项是从sysent数组中来的,该数组是在kern/init_sysent.c中定义的。


struct sysent[] = {

    /* . . . */

    { 3, recvmsg }, /* 27 = recvmsg */

    { 3, sendmsg }, /* 28 = sendmsg */

    { 6, recvfrom }, /* 29 = recvfrom */

    { 3, accept }, /* 30 = accept */

    { 3, getpeername }, /* 31 = getpeername */

    { 3, getsockname }, /* 32 = getsockname */

    /* . . . */

}

recvmsg系统调用在系统调用表中的第27个项,它有两个参数,利用内核中的recvmsg函数实现。syscall将参数从调用进程复制到内核中,并且分配一个数组来保存系统调用的结果。然后,当系统调用执行完成后,syscall将结果返回给进程。syscall将控制交给与系统调用相对应的内核函数。在386实现中,调用有点像:


struct sysent *callp;

error = (*callp->sy_call) (p, args, rval);

这里指针callp指向相关的sysent结构;指针p则指向调用系统调用的进程表项;args作为参数传给系统调用,它是一个32 bit长的字数组;而rval则是一个用来保存系统调用的返回结果的数组,数组有两个元素,每个元素是一个32 bit长的字。当我们用“系统调用”这个词时,我们指的是被syscall调用的内核中的函数,而不是应用调用的进程中的函数。

想深入了解如何拦截系统调用吗?可以参考这个内核模块系统调用拦截源码。syscall期望系统调用函数(即sy_call指向的函数)在没有差错时返回0,否则返回非0的差错代码。如果没有差错出现,内核将rval中的值作为系统调用(应用调用的)的返回值传送给进程。如果有差错,syscall忽略rval中的值,并以与机器相关的方式返回差错代码给进程,使得进程能从外部变量errno中得到差错代码。

应用调用的函数则返回-1或一个空指针表示应用应该查看errno获得差错信息。在386上的实现,设置进位比特(carry bit)来表示syscall的返回值是一个差错代码。进程中的系统调用残桩将差错代码赋给errno,并返回-1或空指针给应用。如果没有设置进位比特,应用调用的函数返回正常值。

更多关于Linux内核中系统调用实现的细节,请查看这篇Linux内核添加系统调用的指南!你是否好奇BSD内核中各类黑客技术?BSD ROOTKIT设计内核黑客指引书可能正是你需要的资源。

探索这些链接,揭开系统调用的神秘面纱吧!