OS为用户态运行的进程与硬件设备进行交互提供了一组接口,有三个优点:从低级编程中解放、提高安全性、可移植性。
API与系统调用:前者是函数定义,后者通过陷阱明确向内核发请求。每个系统调用一般对应于一个C库中的封装例程,而这个封装例程,即为应用程序API。一个单独的API函数可能调用N个系统调用,也可能没对应任何系统调用,也有可能几个API对应同一个系统调用如malloc与free都是brk系统调用。POSIX标准针对API而非系统调用,编程者来看无差别,内核设计者观点看系统调用属内核,用户态库函数不属于内核。用户态进程调用一系统调用时,CPU切换到内核态并开始执行一个内核函数,即“系统调用处理程序”。内核中系统调用按号码识别,用户态的系统调用在封装例程中会自动生成系统调用号,存在eax寄存器中,如fork()存编号2。系统调用返回也经过封装例程处理后返回给用户态进程。调用系统调用时,在封装例程中,将参数处理完后,由汇编指令如int 0x80进入内核的系统调用处理程序,它将寄存器入内核栈,调用系统调用服务例程,最后从处理程序返回。
封装例程中进入系统调用有两种:一是int 0x80,这是老linux内核进入内核态的唯一方式。内核初始化时,将中断描述符表项相关字段初始化为内核代码段、将系统调用处理程序指针初始化为eip、设为陷阱、特权等级为3。此int指令跳转到之前所说的system_call(),该函数将系统调用号和异常处理程序所用的CPU寄存器入栈(不包括硬件自动保存的eflags、cs、eip、ss、esp)、检查有无调试程序跟踪、检查系统调用号。系统调用号与系统调用服务例程的对应关系存在系统调用分派表中。如果没问题,就调用分派表中对应的服务例程。用户态给的其它参数怎么办?不能靠栈传,因为这里涉及用户栈与内核栈,所以只能用寄存器间接传。事实上在封装例程中参数已入寄存器。系统调用处理程序中将寄存器入栈,造成服务例程可直接从内核栈中取参数。用Regs传参数的限制是参数不可多于6个,否则寄存器数量不够。此外参数长度也受限制。参数传来后,得检查其中的地址是否属于此进程的地址空间,但这花时间多且此错误发生可能性较小。后来,仅仅判断地址是否越入内核地址空间、之前的错误放到后面去捕捉,单独将是否侵入内核地址空间的检查提前是因为用户态进程可能误将内核地址空间的地址作为参数传,还能在不引起缺页异常的情况对内存中现有任何页读写。在系统调用服务例程中,需频繁访问进程地址空间数据,若之前粗略检查的地址号虽在0xc0000000前但不属于进程地址空间会发生什么?先回顾缺页异常在内核可能发生的条件:1.访问用户态地址空间的页,但页框不存在或试图写只读页。这时用请求调页分配页框即可;2.内核寻址到属于其他进程地址空间的页,但表项未初始化,此时只要在页表中建立相应的表项(发生在非连续内存访问);3.内核函数编程错误或硬件出问题;4.读一个由系统调用参数传入的地址,但它不属于进程的地址空间。后两种可如此解决:考虑访问进程地址空间的指令有限,因此可将它们列一个表,叫异常表,每个表由指令的线性地址和发生这种情况时要调用的修正措施的汇编代码地址,后者称作修正代码。这样,当缺页异常的3、4两种情况发生时,查异常表,若有,则说明调用参数非法,若无,则说明内核出bug了。异常表在建立内核程序映像时由C编译器生成,存放在内核代码段的__ex_table节中。此外,动态装载的内核模块也有自己的局部异常表,也在建立模块映像时C编译器生成。修正通常是强制服务例程返回出错码给用户态进程。
另一种新的进入系统调用指令是sysenter,它因为有特殊寄存器的帮助,所以速度快,总体的过程差不多。
内核线程也可调用系统调用,但因为它不能用库函数,Linux用7个宏来完成对应工作。
版权声明:本文为博主原创文章,未经博主允许不得转载。