转自:http://edsionte.com/techblog/archives/2071
我们知道,Linux将整个虚拟地址空间划分为两部分:用户空间和内核空间。并且规定,用户空间不能直接访问内核空间,而内核空间则可以访问用户空间。通过这样的级别划分,可以使得内核空间更加的稳定和安全。但是,当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径。
系统调用是内核提供的一组函数接口,它使得用户空间上运行的进程可以和内核之间进行交互。比如,用户进程通过系统调用访问硬件设备或操作系统的某些资源等。系统调用如同内核空间和用户空间的一个传话者。内核如同一个高高在上的帝王,而用户空间的进程则属于级别很小的官员。由于用户进程资质太浅,当它需要得到内核的支持时,它并没有权利直接上报内核,而只能通过系统调用这个传话人来得到内核的支持。
具体的,用户程序通过应用编程接口来使用系统调用,而系统调用则是在内核中通过内核函数来实现的。
应用编程接口(Application Programming Interface,API)其实就是程序员在用户空间下可以直接使用的函数接口。每个API会对应一定的功能。比如strlen(),它所实现的功能就是求所传递字符串的长度。
有时候,某些API所提供的功能会涉及到与内核空间进行交互。那么,这类API内部会封装系统调用。而不涉及与内核进行交互的API则不会封装系统调用。也就是说,API和系统调用并没有严格对应关系,一个API可能恰好只对应一个系统调用,比如read()API和read()系统调用;一个API也可能由多个系统调用实现;有时候,一个API的功能可能并不需要内核提供的服务,那么此时这个API也就不需要任何的系统调用,比如abs()。另外,一个系统调用可能还被多个不同的API内部调用。
对于编程者来说,系统调用和API都是一组函数,并无什么两样;但是事实上,系统调用的实现是在内核完成的,API则是在函数库中实现的。
API是用户程序直接可以使用的函数接口,但如果每个操作系统都拥有只属于自己的API,那么应用程序的移植性将会很差。基于POSIX(Portable Operating System Interface)标准的API拥有很好的可移植性,它定义了一套POSIX兼容标准,这使得按这个标准实现的API可以在各种版本的UNIX中使用。现如今,它也可以在除UNIX之外的操作系统中使用,比如Linux,Windows NT等。
一个.c文件会经过预处理、编译、汇编、链接四个步骤。在汇编阶段,输出的是.o文件,即我们常说的目标文件。目标文件并不能直接执行,它需要链接器的再一次加工。链接器将所有的目标文件集合在一起,加上库文件,最后才能得到可执行文件。
函数库完成了各种API函数的定义,只不过函数库是二进制的形式,我们不能直接去查看这些API函数如何实现。这些API函数的声明则散步在不同的头文件中,比如我们常用(也许你并未感知我们频繁的使用这个函数库)的标准函数库libc.so,在其中包含多个我们常用的函数定义,但是这些函数的声明却分布在stdio.h和string.h等头文件中。
我们每次在链接程序时,都必须告诉链接器需要链接到那个库中。只不过通常默认的链接让我们忽视了这一点。
比如,一个简单的helloworld程序中,仅使用了stdio.h头文件。我们当然可以这样轻松的编译:gcc helloworld.c -o helloworld。之所以可以毫无顾忌是因为stdio.h中所声明的函数都定义在libc.so中,而对于这个函数库,连接器是默然链接的。
如果我们编译如下程序:
01 |
#include < stdio.h > |
02 |
#include < math.h > |
03 |
int main() |
04 |
{ |
05 |
double i; |
06 |
07 |
scanf ( "%lf" ,&i); |
08 |
printf ( "%lf" , sqrt (i)); |
09 |
return 0; |
10 |
} |
按照我们以往的编译方法显然是不行的:
1 |
edsionte@edsionte-desktop:~$ gcc test.o -o test |
2 |
test.o: In function `main': |
3 |
test.c:(.text+0x39): undefined reference to ` sqrt ' |
4 |
collect2: ld returned 1 exit status |
因为在这个程序中使用了math.h头文件,而这个头文件中声明的函数sqrt()被定义在libm.so函数库中。那么,这个时候应该这样编译:gcc test.c -o test -lm。最后的-lm选项即告诉链接器需要加入libm.so函数库。
上述一步到位的编译方法似乎又无形中掩盖了函数库的加入时间。如果我们按照编译程序的四个步骤依次处理相应文件时,就可以发现只有到了最后的链接过程中才会出现上述错误信息。也就是说,函数库的加入是在链接部分。
从上述内容中,我们知道应用程序直接使用的并不是系统调用(不过可以通过_syscallN的方法直接使用系统调用)而是API。内核中提供的每个系统调用都通过libc库封装成相应的API。如果一个API函数中包含系统调用,那么它通常在libc库中会对应一个封装例程(wrapper routine)。封装例程可能正好对应一个与API同名的系统调用,有时为了实现更加复杂的功能会封装多个系统调用。
每一个系统命令其实就是一个可执行的程序,这些可执行程序的实现调用了某些系统调用。并且,这些可执行程序又分为普通用户可使用的命令和管理员可使用的命令。根据上述分类,普通用户可用的命令和管理可用的命令分别被存放于/bin和/sbin目录下。
系统调用的实现是在内核中完成的,它通过封装对应的内核函数(通常是以sys_开头,再加上相应的系统调用名)来实现其代表的功能。内核函数和用户空间中函数并无两样,只不过内核函数是在内核中实现。也就是说,用户程序通过某个系统调用进入内核后,会接着去执行这个系统调用对应的内核函数。这个内核函数也称为系统调用的服务例程。
由于内核函数是在内核中实现的,因此它必须符合内核编程的规则,比如函数名以sys_开始,函数定义时候需加asmlinkage标识符等。