系统调用”是操作系统提供给用户程序进行调用的一些服务。这些服务是系统预先提供的函数,在这一点上系统调用与普通的用户程序是没有区别的。而区别则在于“系统调用”是由操作系统提供给用户的,这些服务更接近底层或者要求的安全性更高,因此由操作系统来统一实现和管理。
    程序员在写程序的过程中会经常需要调用“系统调用”来完成特定的任务。我们以教学用的类Linux操作系统xv6为例,以打印操作为主线来说明系统调用的代码实现以及系统调用的全过程,其它系统调用的处理过程实际上道理是一样的。

   打印操作最终封装给用户的形式是printf()函数,它的定义在文件printf.c中。查看printf()的定义,函数中调用了putc()函数来进行输出,继续跟踪putc()函数的定义,我们发现
write函数被调用了,在这里继续跟踪write函数,会发现它的声明在user.h中: int write(int,void*,int),但是并不能找到这个声明所对应的C代码形式的具体实现,这就是一个系统调用了。下面我们来分析该系统调用具体的实现原理和过程。

   为了清楚地理解系统调用过程,我们需要从write函数被编译为汇编代码来说起。当编译器对write(int a, void* b, int c)函数进行汇编时,会将其汇编为这样一种形式:首先将write函数的参数依次压栈,然后通过call语句转到write函数对应的入口,也就是如下这样一种形式:
push a
push b
push c
call write

然而,既然write函数并没有具体C代码形式的定义,那么write函数的入口在哪里呢? 我们来看一下usys.S这一文件,该文件首先定义了一个宏STUB,然后有一句话STUB(write),将该宏语句展开如下:
.global write;
write:
movl $SYS_write, %eax;
int $T_SYSCALL;
ret

至此,我们看到write函数的入口原来就在这里,那么进入这个write入口之后到底在做什么呢? 在syscall.h中,我们发现$SYS_write原来对应这一个编号5,这就是该系统调用所对应的系统调用号。于是我们知道,在write函数里面实际做了两件事情,一是将write所对应的系统调用号存放在eax寄存器中,然后通过int 30h指示处理器去做系统调用操作,接下来就是系统调用的具体处理了。由于系统调用作为中断的一种来处理,所以这里的int 30h所作的构造中断侦,转到内核态等操作可以参考对一般中断处理过程的分析。为了保持思路的连贯性,在这里我们我们跳过这一部分,继续分析一个系统调用号所对应的系统调用代码是如何被找到和执行的。
我们知道,处理器在eax寄存器中拿到系统调用号之后,会到系统调用表中找到该系统调用所对应的入口函数地址,然后执行该函数。那么这个地址在哪里呢?

在syscall.c中,我们可以看到一个存放函数指针的数组static int(*syscalls[])(void)={[SYS_write] sys_write,……},该数组中对于每一个系统调用name,在数组的SYS_name下标中存放了sys_name函数的地址,原来xv6中把所有的系统调用都封装成了int sys_name(void)的形式,比如说write系统调用,所对应的封装是sys_write()函数,这个数组里面将SYS_write这个系统调用号与sys_write这个函数指针相关联,那么这个函数在哪里得到调用呢?

查看void syscall (void)函数的代码:
void syscall(void)
{
int num;
num = cp->tf->eax;
if(num >= 0 && num tf->eax = syscalls[num]();
} else {
cprintf(“%d %s: unknown sys call %d\n”,
cp->pid, cp->name, num);
cp->tf->eax = -1;
}
}
可以看到,处理器将从中断侦存放的eax寄存器中拿到系统调用号num,然后通过cp->tf->eax = syscalls[num]()这句话调用了syscalls[num]这个入口地址所指向的函数,并将函数的返回结果存放在了中断侦eax寄存器里面。那么执行这个函数到底做了什么呢?我们仍以write为例,简单的将sys_write函数代码列在下面:
int  sys_write(void)
{
struct file *f;
int n;
char *p;

if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0)
return -1;
return filewrite(f, p, n);
}
可以看到进入函数之后首先进行了三次取参过程,这恰是前面所说的write函数被编译出的push操作所压入的参数,拿到这些参数之后,就可以根据具体的应用调用不同的函数来完成需要的逻辑了。至此我们看到了完整的系统调用的过程。

ps:应用程序调用write函数,首先进入uclibc,uclibc中会将write的系统调用号及参数保存在r7,及r0-r6中,然后触发软中断,保存在软中断的处理流程前先进性地址空间的转换及堆栈的切换,然后进行中断处理,中断处理中读取中断号及参数,然后找到中断服务例程并执行,退出中断后进行堆栈切换,返回用户态,继续执行用户程序。



GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐