本文基于mstar801平台Linux2.6.35.11版本。
首先说明:系统调用不会导致进程上下文切换。
一、介绍系统调用——Linux用户空间主动进入内核空间的唯一方法
1.系统调用是操作系统提供给用户程序调用的一组“特殊”接口;用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口;把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。
2.系统调用按照功能逻辑大致可分为“进程控制”、“文件系统控制”、“系统控制”、“存储管理”、“网络管理”、“socket控制”、“用户管理”和“进程间通信”几类。
3.内核接口:kernel2.6.35.11/arch/arm/include/asm/unistd.h
二、系统调用的主要用途1.控制硬件——系统调用往往作为硬件资源和用户空间的抽象接口,比如读写文件时用到的write/read调用。
2.设置系统状态或读取内核数据——因为系统调用是用户空间和内核的唯一通讯手段,所以用户设置系统状态、比如开/关某项内核服务(设置某个内核变量)或读取内核数据都必须通过系统调用。比如getpid、getpriority、setpriority和sethostname等。3.进程管理——系统调用接口用来保证系统中进程能以多任务在虚拟内存环境下运行。比如fork、clone、execve和exit等。
三、内核函数和系统调用、用户编程接口(API)、系统命令的关系1.系统调用并非直接和程序员或系统管理员打交道,它仅仅是一个通过软中断机制向内核提交请求、获取内核服务的接口。而在实际使用中程序员调用的多是用户编程接口——api,而管理员使用的则多是系统命令。
2.用户编程接口(API)其实是一个函数定义,说明了如何获得一个给定的服务,比如read()、malloc()、free()、abs()等。
它有可能和系统调用形式一致,比如read()接口就和read系统调用一一对应;往往会出现几种不同的API内部用到同一个系统调用,比如malloc()、free()内部利用brk()系统调用来扩大或缩小进程的堆;
或一个API利用了好几个系统调用组合来完成任务;更有些API甚至不需要任何系统调用,因为它不需要内核服务、如计算整数绝对值的abs()接口。
Linux系统中这些API主要是通过C库(libc)实现的;它除了定义的一些标准的C函数外,一个重要的任务是提供了一套封装例程、将系统调用在用户空间包装后供用户编程使用。说明:上述封装并非必须;如果你愿意直接调用,Linux提供了一个syscall()的系统调用函数来实现调用。
3.系统命令相对编程接口更高了一层,它是内部引用API的可执行程序,比如我们常用的系统命令ls、hostname等。三、内核函数和系统调用的关系
内核函数没有想像中那么复杂;它们和普通函数很像、只不过在内核实现,因此要满足一些内核编程的要求。系统调用是一层用户进入内核的接口,它本身并非内核函数;进入内核后,不同的系统调用会找到对应到各自的内核函数——专业术语叫:系统调用服务例程。
总结:从用户角度向内核看;依次是系统命令、编程接口、系统调用和内核函数。系统调用利用了ARM体系结构中的软件中断,软件中断和我们常说的中断(硬件中断)不同之处在于———它是通过软件指令触发而并非外设,也就是说由编程人员发出的一种异常;具体地讲就是调用SWI汇编指令(x86上int $0x80),这条汇编指令将产生向量为128的编程异常,ARM从用户模式切入管理模式、并强制R15-PC(程序计数器)为0x0000 0008,Linux从用户态进入内核态。见:《ARM与Linux些许问题》第一章:ARM工作模式
之所以系统调用需要借助异常实现;是因为当用户态的进程调用一个系统调用时,CPU便被切换到内核态执行内核函数。我们前边分析ARM体系结构部分已经讲过进入内核态——ARM高特权模式,必须经过系统的门机制——异常(SWI汇编指令(x86上int $0x80等);其他异常用户空间无法利用,都是由内核使用的。)。
1.SWI汇编指令(x86上int $0x80)指令的目的是产生一个编号为128的编程异常,这个编程异常对应中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:vector_SWI()(x86上system_call())。注意:不是系统调用服务程序本身。
即:系统命令——>用户编程API——>系统调用(调用SWI汇编指令异常)——>系统调用处理函数(vector_SWI)——>具体的系统调用服务程序。其中蓝色部分是内核态函数。
2.所有的用户空间系统调用函数都是通过调用SWI汇编指令(x86上int $0x80)异常、进入内核态,此时、ARM默认从某一固定地址执行程序(vector_SWI()(x86上system_call())的地址)。vector_SWI()(x86上system_call())这个内核函数又怎样分发这些系统调用到各自的内核服务程序中呢?Linux为每个系统调用都进行了编号(0——_NR_syscall);同时在内核中保存一张系统调用表,该表中保存了系统调用编号和其对应的服务例程。因此,在系统调用通过门陷入内核前,需要把系统调用号一并传入内核。这个传递工作是通过把系统调用号装入相应寄存器实现的。
有了如上的分析:系统调用处理程序vector_SWI()(x86上system_call())一旦运行;就可以从相应寄存器中得到数据,然后再去系统调用表中寻找相应的服务例程了。
注意:除了系统调用号之外,有的系统调用还需要传递一些参数给内核;这是Linux在vector_SWI()(x86上system_call())调用时将参数等值传入其他寄存器。
内核系统服务例程结束时,system_call()从相应寄存器中获得系统调用返回值,并把这个返回值放在曾保存用户态相应寄存器栈单元的那个位置;然后跳转到ret_from_sys_call(),终止系统调用处理程序的执行。
====================================================================================================================================五、主要路径
1.用户空间:libc库没有研究代码,大体机制如下——
调用 SWI汇编指令(x86上int $0x80)) 软中断进入内核,并传入中断向量号相关中断向量号arm-2010.09/arm-none-linux-gnueabi/libc/usr/include/bits/syscall.h
#define SYS_getuid __NR_getuid 2.内核空间:ARM中系统调用号定义路径:kernel2.6.35.11/arch/arm/include/asm/unistd.h
#define __NR_getuid (__NR_SYSCALL_BASE+ 24)
异常进入内核空间函数路径:kernel2.6.35.11/arch/arm/kernel/entry-common.s
默认执行vector_SWI(x86上system_call())函数
ARM中系统调用表定义路径:kernel2.6.35.11/arch/arm/kernel/calls.S
CALL(sys_getuid) //第24个,要与前面unistd中对应ARM中系统调用服务程序的声明路径:kernel2.6.35.11/include/linux/syscalls.h
asmlinkage long sys_getuid(void);
====================================================================================================================================x86平台相关路径(内核):
系统调用号路径——kernel2.6.35.11/arch/x86/include/asm/unistd.h
system_call()函数路径——kernel2.6.35.11/arch/x86/kernel/entry.S
系统调用表路径——kernel2.6.35.11/arch/x86/kernel/syscall_table.S或直接在entry.S中定义