Linux系统调用FAQ
1. Linux系统调用的作用?
系统调用是操作系统为用户态运行的进程与系统内核、硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口,在应用程序和硬件之间设置一个额外层的优点包括:
1. 用户编程更加简单,不必学习硬件设备的低级编程特性;
2. 提高了系统的安全性,内核在试图满足某个请求前在接口级可以检查请求正确性。
3. 这组接口使得程序具有可移植性,只要内核所提供的接口相同,使用这些接口的 程序即可正确的编译和执行。
2. POSIX API与系统调用的关系?
1. API是函数定义(如libc库),而系统调用是通过软中断想内核态发出的请求;
2. 一个API没有必要对应一个特定的系统调用。API可能直接提供用户态服务,如数学函数;有些API函数可能调用几个系统调用;几个API函数可能调用同一系统调用(如malloc,calloc,free均使用brk系统调用实现)。
3. POSIX标准针对API而不是针对系统调用,判断一个系统是否与POSIX兼容,要看它是否提供了一组合适的API。
4. 从应用程序设计者的角度看,两者的差别仅在于名字、参数、返回值的不同;从内核设计者的角度看,系统调用属于内核,而API不属于内核。
3. 内核如何处理系统调用?
1. 每一个系统调用拥有一个系统调用号的标识,当用户态请求系统调用时,需传递系统调用号及其它信息作为参数,eax寄存器用于传递系统调用号(同时也用于传递返回值,系统调用号与返回值类型相同)。内核从寄存器读到系统调用的参数,并执行对应的系统调用服务例程。
2. 下图显示了应用程序、相应的封装例程、系统调用处理程序、系统调用服务例程之间的关系。xyz()是应用程序的调用接口,xyz在libc中的调用实现通过调用SYSCALL汇编指令实现,该指令使得CPU切换到内核态(SYSEXIT反之);内核态通过sys_call系统调用处理程序来最终调用xyz的服务例程sys_xyz()。
3. 为了将系统调用号与相应的服务例程关联起来,内核利用一个系统调用分派表,该表存放在sys_call_table数组中,有NR_syscalls个表项,第n个表项包含系统调用号为n的服务例程的地址。
4. 如何进入和退出系统调用?
本地应用可以通过两种方式调用系统调用:
1. 执行int $0x80汇编语言指令(老版本linux中从用户态切换到内核态的唯一方式)
2. 执行sysenter汇编语言指令,Intel Pentium II微处理器引入该指令,linux2.6内核支持这条指令。
同样,内核可通过两种方式从系统调用退出,使CPU切换回到用户态:
1. 执行iret汇编语言指令
2. 执行sysexit汇编语言指令
对以上两种方式涉及到汇编代码,我也解释不太清楚,更详细的了解,请参考《深入理解linux内核》。
5. 系统调用参数如何传递?
1. 普通C函数的参数传递是通过把参数值写入到活动的程序栈(用户态或内核态堆栈)实现的,而系统调用同时跨越内核态和用户态,同时操作两个栈是很复杂的,而采用下面的方式(寄存器传递参数)将使得系统调用处理程序与其它异常处理程序的结构类似。
2. 在发出系统调用之前,系统调用的参数被写入到CPU的寄存器,然后在调用系统服务例程之前,内核再把存放在CPU中的参数拷贝到内核态堆栈(系统调用服务例程是普通的C函数)。
3. 使用寄存器传递参数必须满足:每个参数的长度不能超过寄存器的长度(容易解决,长度不够,可以通过传递参数的地址解决),并且参数的个数不能超过6个(系统调用号除外),因为80x86处理器的寄存器的数量是有限的(用于存放调用号和调用参数的寄存器是eax、ebx、ecx、edx、esi、edi、ebp)。
6. 如何保证系统调用的安全性?
内核在满足用户的系统调用请求前,必须仔细的检查所有的系统调用参数,如write系统调用的参数fd,内核需检查fd是否对应于一个打开的文件,该文件是否允许写操作等,这些检查操作依赖于系统调用本身,也依赖于特定的参数。但有一种检查对所有的系统调用是通用的,只要参数指定的是地址,内核必须检查它是否属于用户地址空间,可通过检查地址所在的线性区,或直接与PAGE_OFFSET对比等两种方式实现(linux2.2开始实现第二种方式)。
7. 内核如何访问用户态数据?
系统调用服务例程需要非常频繁的读写进程地址空间的数据,Linux提供一组宏来实现这个目的(__开头的宏不包含对线性地址的有效性检查)。
get_user __get_user 从用户空间读一个整数(1、2或4个字节)
put_user __put_user 向用户空间写一个整数(1、2或4个字节)
copy_from_user __copy_from_user 从用户空间复制任意大小的块
copy_to_user __copy_to_user 把任意大小的块复制到用户空间
strncpy_from_user __strncpy_from_user 从用户空间复制一个以null结束的字符串
strlen_user strnlen_user 返回用户空间以null结束的字符串的长度
clear_user __clear_user 用0填充用户空间的一个内存区域
8. 在内核态能否调用系统调用?
尽管系统调用主要由内核态进程使用,但也可以被内核线程调用,内核线程不能使用库函数。linux定义了7个从_syscall0到_syscall6的一组宏来封装相应例程,宏名中的数字0-6对应着系统调用所用的参数个数(系统调用号除外)。每个宏需要2 + 2 * n个参数(n为系统调用参数的个数),前两个参数是系统调用号返回值类型和名字;每一对附加参数指明相应的系统调用参数的类型和名字。