系统调用原理及详细过程

系统调用原理及详细过程

为什么要有系统调用?

由于系统的有限资源可能被多个不同的应用程序访问,因此,如果不加以保护,那么用程序难免产生冲突。所以,现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。这些系统资源包括文件、网络、IO、各种设备等。

为了让应用程序有能力访问系统资源,也为了让应用程序能够借助操作系统做一些必须由操作系统支持的行为。每个操作系统都会提供一套接口,以供应用程序使用。这些接口往往通过中断来实现。

系统调用与运行库(标准库)

系统调用的弊端
  1. 使用不便。操作系统提供的系统个调用接口往往过于原始,程序员需要了解很多与操作系统相关的细节。如果没有很好的包装,使用起来不方便
  2. 各个操作系统之间的系统调用不兼容。首先Windows系统和Linux系统之间的系统调用就基本上完全不同。即使是同系列的操作系统的系统调用都不一样,比如Linux和Unix就不相同

为了解决这个问题,运行库作为系统调用与程序之间的一个抽象层挺身而出,它有一下特点:

  1. 使用简便。
  2. 形式统一。运行库有它的标准,叫做标准库,凡是所有遵循这个标准的运行库理论上都是相互兼容的,不会随着操作系统或编译器的变化而变化。

系统调用的原理:

现代操作系统通常让代码运行在两种不同特权的模式下
——用户态和内核态——以限制他们的权力。系统调用要操作一些有限的资源,无疑是运行在内核态的。那么用户态程序如何运行内核态的代码呢?操作系统一般是通过中断来从用户态切换到内核态。

中断

中断是指一个硬件或软件发出的请求(电信号),要求CPU暂停当前的工作转手去处理更加重要的事情。

中断一般有两个属性,中断号中断处理程序(ISR,Interrupt Service Routine)。在内核中,有一个数组称为中断向量表,包含了中断号及其对应中断处理程序的指针。中断到来时,CPU暂停当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU会继续执行之前的代码。

中断有两种类型:一种称为硬件中断,这种中断来自于硬件的异常或事件发生;另一种称为软件中断,软件中断通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断并执行中断处理程序。

由于中断号是有限的,操作系统不舍得每一个系统调用对应一个中断号,而更倾向于用一个或少数几个中断号来对应所有的系统调用。Linux则使用int 0x80来触发所有系统调用。每个系统调用对应一份系统调用号,这个系统调用号在执行int 0x80指令前会放置在某个固定的寄存器里(eax),对应的中断代码会取得这个系统调用号,并且调用正确的函数。

系统调用的详细过程

系统调用原理及详细过程_第1张图片

1. 触发中断

用户程序在代码中调用系统调用,执行int指令前将系统调用号放入eax寄存器中,执行int 0x80指令
(int 指令最后执行的函数是system_call,该函数验证系统调用号的有效性,查找系统调用函数并执行,最后通过itret从中断处理程序返回)

2. 切换堆栈(此步在int指令中完成)

在实际执行0x80号中断向量所对应的中断处理程序(system_call)之前,CPU首先要进行堆栈切换,即从用户态切换到内核态。从中断处理函数中返回时,程序当前栈还要从内核栈切换回用户栈。

  • 所谓的当前栈,值得是esp(栈指针)的值所在的栈空间。如果esp的值位于用户栈的范围内,那么程序的当前栈就是用户栈,反之亦然。此外,寄存器ss的值还应该指向当前栈所在的页。
    所以,将当前栈由用户栈切换为内核栈的实际行为就是:
    (1) 保存当前的esp,ss的值(保证存在内核栈上,有int指令自动地由硬件完成)
    (2) 将esp,ss的值设置为内核栈的相应值
  • 当0x80号中断发生的时候,cpu除了切入内核态之外,还会自动完成下列几件事:
    (1)找到当前进程的内核栈(每一个进程都有自己的内核栈)
    (2)在内核栈中一次压入用户态的寄存器ss、esp、eflags、cs、eip
  • 而当内核从系统调用返回的时候,须要调用iret指令来回到用户态,iret指令则会从内核栈里弹出寄存器ss、esp、eflags、cs、eip的值,使得栈恢复到用户态的状态。
    系统调用原理及详细过程_第2张图片
3. 中断处理程序

在int指令切换内核栈之后,程序就切换到了中断向量表中的0x80号中断处理程序。Linux中0x80向量对应的中断处理程序是system_call。

system_call中断服务程序首先检查系统调用号的有效性,再根据eax寄存器存储的系统调用号从系统调用表上找到相应的系统调用并调用。调用完成后从system_call中返回。以下是部分内核代码:

// system_call的开头部分
......
SAVE_ALL	// 保存寄存器的值到栈中,以免被覆盖
......
cmpl $(nr_syscalls), %eax	// 比较eax寄存器中的值和系统调用号大1的值(验证系统调用号的有效性)
jae syscall_badsys	// 如果系统调用无效,指向syscall_badsys


// 如果系统调用号有效,则会执行以下代码
syscall_call:
	call *sys_call_table(0, %eax, 4)	// 查找中断服务程序并执行, sys_call_table其实就是系统调用表
	.....
	RESTORE_REGS	// 恢复之前保存的寄存器
	......
	iret	// 从中断程序返回
	

系统调用原理及详细过程_第3张图片

4. 从中断处理程序返回

在3中有描述

总结

用户运行库函数(系统调用的封装),函数里面其实是执行的int 0x80指令。系统调用先把系统调用号保存在eax寄存器中,然后执行int0x80指令。int 0x80指令先进行切换堆栈(找到进程的堆栈,将寄存器值压入到内核栈中,将esp,ss设置成对应内核栈的值),查找相应中断向量的中断处理程序(system_call)并调用,随后system_call 从系统调用表中找到相应的系统调用进行调用,调用结束后从system_call中返回。
系统调用原理及详细过程_第4张图片
系统调用原理及详细过程_第5张图片

参考文献

程序员的自我修养-链接、装载与库

你可能感兴趣的:(linux,操作系统,内核)