大部分介绍Linux内核的书籍都没有仔细说明系统调用,这应该算是一个失误。内核发展到现在,我们实际需要的系统调用现在已经十分完美,从这个意义上来说,再耗费宝贵的时间去研究系统调用的实现是毫无意义的事情。
然而,对于希望能够对内核有更深理解的我们来说,仔细研究少量系统调用仍是十分值得的。这样就有机会初步了解一些概念,并可以趁机详细了解一下内核编程的特点,就像系统调用本身在应用程序和内核间的桥梁作用一样,学习并理解它也是我们走向内核的一个很好的过渡。
5.1 系统调用概述
一个稳定运行的Linux操作系统需要内核和用户应用程序之间的完美配合,内核提供各种各样的服务,然后用户应用程序通过某种途径使用这些服务,进而契合用户的不同需求。
用户应用程序访问并使用内核所提供的各种服务的途径即是系统调用。在内核和用户应用程序相交界的地方,内核提供了一组系统调用接口,通过这组接口,应用程序可以访问系统硬件和各种操作系统资源。比如用户可以通过文件系统相关的系统调用,请求系统打开文件、关闭文件或读写文件;可以通过时钟相关的系统调用,获得系统时间或设置定时器等。
内核提供的这组系统调用通常也被称之为系统调用接口层。系统调用接口层作为内核和用户应用程序之间的中间层,扮演了一个桥梁,或者说中间人的角色。系统调用把应用程序的请求传达给内核,待内核处理完请求后再将处理结果返回给应用程序。
5.1.1 系统调用、POSIX、C库、系统命令和内核函数
(1)系统调用和POSIX。
系统调用虽然是内核和用户应用程序之间的沟通桥梁,是用户应用程序访问内核的入口点,但通常情况下,应用程序是通过操作系统提供的应用编程接口(API)而不是直接通过系统调用来编程。
操作系统API的主要作用是把操作系统的功能完全展示出来,提供给应用程序,基于该操作系统,与文件、内存、时钟、网络、图形、各种外设等互操作的能力。此外,操作系统API通常还提供许多工具类的功能,比如操纵字符串、各种数据类型、时间日期等。
在UNIX世界里,最通用的操作系统API基于POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)标准。POSIX的诞生和UNIX的发展密不可分,UNIX于20世纪70年代诞生于Bell lab,并于20世纪80年代向美各大高校分发V7版的源码以做研究。UC Berkeley在V7的基础上开发了BSD UNIX。
后来很多商业厂家意识到UNIX的价值也纷纷以Bell Lab的System V或BSD为基础来开发自己的UNIX,较著名的有Sun OS、AIX、VMS等。虽然这带来了UNIX的繁荣,但由于各厂家对UNIX的开发各自为政,UNIX的版本相当混乱,给软件的可移植性带来很大困难,对UNIX的发展极为不利。
为结束这种局面,IEEE制订了POSIX标准,目标是提供一套大体上基于UNIX的可移植操作系统标准,提高UNIX环境下应用程序的可移植性。然而,POSIX并不局限于UNIX。许多其他的操作系统,例如DEC OpenVMS和Microsoft Windows NT,都支持POSIX标准
POSIX标准定义了"POSIX兼容"的操作系统所必须提供的服务。Linux兼容于POSIX标准,提供了根据POSIX而定义的API函数。这些API函数和系统调用之间有着直接的关系,一个API函数可以由一个系统调用实现,也可以通过调用多个系统调用来实现,还可以完全不使用任何系统调用。
(2)系统调用和C库。
操作系统API通常都以C库的方式提供,Linux也是如此。C库提供了POSIX的绝大部分API,同时,内核提供的每个系统调用在C库中都具有相应的封装函数。系统调用与其C库封装函数的名称常常相同,比如,read系统调用在C库中的封装函数即为read函数。
C库中的系统调用封装函数在最终调用到相应系统调用之前,往往不做多少额外的工作。不过,某些情况下会有些例外,比如对于两个相关的系统调用truncate和truncate64,C库中的封装函数truncate函数即需要决定它们中的哪个应该最终被调用。
当然,如图5.1所示,系统调用和C库函数之间并不是一一对应的关系。可能几个不同的函数会调用到同一个系统调用,比如malloc函数和free函数都是通过brk系统调用来扩大或缩小进程的堆栈,execl、execlp、execle、execv、execvp和execve函数都是通过execve系统调用来执行一个可执行文件。
也有可能一个函数调用多个系统调用。更有些函数并不依赖于任何系统调用,比如strcpy函数(复制字符串)和atoi函数(转换ASCII为整数),因为它们并不需要向内核请求任何服务。
图5.1 C库函数与系统调用
实际上,从用户的角度看,系统调用和C库之间的区别并不重要,他们只需通过C库函数完成所需功能。相反,从内核的角度看,需要考虑的则是提供哪些针对确定目的的系统调用,并不需要关注它们如何被使用。
(3)系统调用与系统命令。
系统命令位于C库的更上层,是利用C库实现的可执行程序,比如最为常用的ls、cd等命令。
strace工具可以跟踪命令的执行,使用希望跟踪的命令为参数,并显示出该命令执行过程中所使用到的所有系统调用。比如,如果希望了解在执行pwd命令时都调用了哪些系统调用,可以使用下面的命令:
- $strace pwd
结果会产生大量的信息,显示出pwd命令执行过程中所调用到的各个系统调用:
- ……
- write(1, "/usr/src/linux-2.6.23/n", 22/usr/src/linux-2.6.23) = 22
- close(1) = 0
- munmap(0xb7f5a000, 4096) = 0
- exit_group(0)
(4)系统调用和内核函数。
内核函数与C库函数的区别仅仅是内核函数在内核实现,因此必须遵守内核编程的规则。
系统调用最终必须具有明确的操作。用户应用程序通过系统调用进入内核后,会执行各个系统调用对应的内核函数,即系统调用服务例程,比如系统调用getpid的服务例程是内核函数sys_getpid。
系统调用服务例程之外,内核中存在着大量的内核函数。有些局限于某个内核文件自己使用,有些则是export出来供内核其他部分共同使用。对于export出来的内核函数,可以使用ksyms命令或通过/proc/ksyms文件查看。
说白了,系统调用是内核提供的接口,而不管是系统命令还是标准库函数,都是基于系统调用编写的应用程序,只不过是一些大牛写的啦,程序健壮,够标准,哈哈,我们自己也可以写用户程序做为自己的库函数,自己在内核代码中添加系统调用,这些都是可以做到的
5.1.2 系统调用表
系统调用表sys_call_table存储了所有系统调用对应的服务例程的函数地址,在arch/i386/kernel/syscall_table.S文件中被定义:
001 ENTRY(sys_call_table) 002 .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ 003 .long sys_exit 004 .long sys_fork 005 .long sys_read 006 .long sys_write 007 .long sys_open /* 5 */ ¡¡ 320 .long sys_getcpu 321 .long sys_epoll_pwait 322 .long sys_utimensat /* 320 */ 323 .long sys_signalfd 324 .long sys_timerfd 325 .long sys_eventfd 326 .long sys_fal