系统调用

一、系统调用

系统调用

系统调用

                                                 Unix/Linux体系架构

如上图所示,从宏观上来看,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

二、系统调用和库函数的区别?

函数库调用 系统调用
在所有的ANSI C编译器版本中,C库函数是相同 各个操作系统的系统调用是不同
它调用函数库中的一段程序(或函数) 它调用系统内核的服务
用户程序相联系 操作系统的一个入口点
在用户地址空间执行 在内核地址空间执行
它的运行时间属于“用户时间 它的运行时间属于“系统时间
属于过程调用,调用开销较小 需要在用户空间和内核上下文环境间切换,开销较大
在C函数库libc中有大约300个函数 在UNIX中大约有90个系统调用
典型的C函数库调用:system fprintf malloc 典型的系统调用:chdir fork write brk;
三、内核态和用户态

现代计算机机中都有几种不同的指令级别,在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态,而在相应的低级别执行状态下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动。因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。


在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。所以,CPU将指令分为特权指令和非特权指令。对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用不会造成灾难的指令。(这样就很难把整个系统都给搞崩溃喽。。。

五、从用户态到内核态的转换??

重点来了。。。。。。。。。


很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程

到底在什么情况下会发生从用户态到内核态的切换,一般存在以下三种情况:

1)当然就是系统调用:原因如上的分析。

2)异常事件: 当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。

3)外围设备的中断:当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。


系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断,这是操作系统为用户特别开放的一种中断,如Linux int 80h中断。所以,从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。


 X86体系结构中包括了一个特殊的段类型:任务状态段(TSS),用它来存放硬件上下文。TSS反映了CPU上的当前进程的特权级。  linux为每一个cpu提供一个tss段,并且在tr寄存器中保存该段。(linux中之所以为每一个cpu提供一个tss段,而不是为每个进程提供一个tss段,主要原因是tr寄存器永远指向它,在任务切换的适合不必切换tr寄存器,从而减小开销。)

在进程从用户态到内核态切换过程中,Linux主要做的事:  

1:读取tr寄存器,访问TSS段  

2:从TSS段中的esp0获取进程内核栈的栈顶指针  

3:  由控制单元在内核栈中保存当前eflags,cs,ss,eip,esp寄存器的值。 

4:由SAVE_ALL保存其寄存器的值到内核栈  

5:把内核代码选择符写入CS寄存器,内核栈指针写入ESP寄存器,把内核入口点的线性地址写入EIP寄存器  

此时,CPU已经切换到内核态,根据EIP中的值开始执行内核入口点的第一条指令。

esp寄存器是CPU栈指针,存放内核栈栈顶地址。在X86体系中,栈开始于末端,并朝内存区开始的方向增长。从用户态刚切换到内核态时,进程的内核栈总是空的,此时esp指向这个栈的顶端。    

在X86中调用int指令型系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。所以在进入内核态之前要保存进程的上下文,中断结束后恢复进程上下文,那靠的就是内核栈。   


每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在进行系统调用是只要指定对应的系统调用号,就可以明确的要调用哪个系统调用,这就完成了系统调用的函数名称的转换

举个例子来说一下吧

系统调用系统调用

当用户进程执行到int fd = open(file)时,产生0x80软中断,操作系统先将系统调用号通过eax寄存器读到内核中,检查系统调用号看请求哪种服务,然后在系统调用表找所调用的内核函数入口地址,调用内核函数,将结果通过寄存器返回给用户进程。

系统调用

/////////////////////////////////////////////////////////////////////////////////////////////////

再来仔细分析一下中断处理的完整过程吧。。。

interrupt(ex:int0x80)-save//发生系统调用

//保存cs:eip的值,堆栈寄存器当前的栈顶,当前的标志寄存器 
cs:eip/ss:esp/efalgs(current)to kernel stack

//当前加载了中断信号和系统调用相关联的中断服务程序的入口,把它加载到当前cs:eip的里面
then load cs:eip(entry of a specific ISR)

//同时也要把当前的esp和堆栈段也就是指向内核的信息也加载到cpu里面,这是由中断向量或者说是int指令完成的
and ss:esp(point to kernel stack)  

SAVE_ALL

----.........                  //内核代码,完成中断服务

RESTOER_ALL   //完成之后再返回到原来的状态

itret-pop

cs:eip/ss:esp/efalgs  from keinel stack


  六、总结

 计算机科学中有一句话,任何计算机相关问题都可以通过加一个中间层来解决。操作系统的系统调用也是这样,system_call将API和系统函数连接起来,这样可以保证内核的安全,不会因为用户的失误操作而造成问题。操作系统为了安全,把一些重要的调用放在内核部分,这样只能通过触发系统调用来完成相应功能,这样可以保证内核的安全,但是不可避免的也造成了系统调用的消耗比较大。


over。。。。。。。。。。。。。。。。。。。。。。。