Linux基础 | 系统调用

概念

现代操作系统中程序本身没有多少权利访问系统资源,为了保护系统资源,OS会组织程序直接访问系统资源,比如文件、网络、IO、各种设备等。
但是有些场景不借助OS没法很好地办到,比如让程序等待一段时间,如果借助OS,我们可以用sleep(),但是要是自己写的话,可能就是如下这样:

for(int i=0;i<1000000;i++);

可以是可以,但是这样会浪费CPU时间。特别是这段程序在不同频率的cpu上耗费时间不一样:

  • 100MHz的cpu耗时1s
  • 1000MHz的cpu耗时0.1s

因此这样绝不是一个好办法。为了让程序更好地借助OS实现一些操作,OS会提供一套接口,让程序使用。这些接口往往通过中断来实现,比如Linux使用0x80号中断作为系统调用入口

系统调用缺点

  1. 使用不便。OS提供的接口比较原始,没有很好地包装
  2. 各个OS系统调用不兼容。linux、windows、unix都不同

为了解决这个问题,我们可以在系统调用和程序之间做一层抽象层来统一管理,这就是运行库。比如对于读取文件,我们可以用C语言中的fread函数,其底层在windows下函数实现可能是ReadFile,linux下可能是read系统调用,但是没关系,不管在哪个平台,都可以用fread来读文件。

系统调用原理

特权级与中断

现代CPU可以在多种截然不同特权级别下执行指令,现代OS有用户模式(User Mode)内核模式(Kernel Mode),也就是用户态内核态。普通程序运行在用户态,对操作有很多限制,比如没法直接访问硬件设备、开关中断、改变特权模式等。

用户态程序想要执行内核态代码一般要通过中断(Interrupt)来从用户态切换到内核态。**中断就是一个硬件或者软件发出的请求,要求cpu暂停当前工作转手去处理更加重要的事情。**举个例子,我们在编辑文件的时候,键盘上的键按下,cpu如何获知这一点呢?

  1. 第一种是轮询(poll),cpu每隔一小段时间(几十到几百毫秒)询问键盘是否被按下,大部分情况都是没有键被按下的回应,这样就很浪费cpu时间
  2. 第二种是发信号,cpu不理睬键盘,键盘被按下之后,键盘芯片给cpu发送一个信号,cpu接受到信号去询问哪个键被按下。这个信号就是一个中断

中断有两个属性,提及中断要有中断号(从0开始)和中断处理程序(Interrupt Service Routine,ISR),类似于信号,有信号id和信号处理程序。内核里有一个中断向量表(Interrupt Vector Table),这个数组的第n项表示第n号中断的中断处理程序。中断来了之后,cpu暂停当前代码执行并保存当前上下文,根据中断号查找对应的处理程序调用。(这里涉及进程上下文切换)

中断有两种:

  1. 硬件中断:硬件异常或者电源断电、键盘被按下等
  2. 软件中断:软件中断通常是指令,带有一个参数记录中断号,使用这个指令用户可以手动触发某个中断并执行其中断处理程序。比如在i386下,int 0x80这个指令会去执行0x80号中断处理程序

对于中断和系统调用又如何绑定的呢?
由于中断号很有限,OS不会让一个中断号对应一个系统调用。Linux用int 0x80来触发所有系统调用。那么对于同一个中断号,OS怎么知道哪一个系统调用要执行呢?首先系统调用号会被放入一个固定的寄存器eax,用户把系统调用号传入eax,然后使用int 0x80调用中断,中断服务程序就可以eax取得系统调用号,进而调用对应的函数。

基于int的linux系统调用实现

下面以fork(系统调用号为2)为例来看系统调用如何执行的:
在这里插入图片描述
一步一步看细节:

触发中断
  1. 程序在代码中调用一个系统调用时,用函数来实现的:
int main()
{
	fork();
}

fork是对系统调用fork的封装,可以用下面这个宏来定位它:

_syscall0(pid_t,fork);

_syscall0是一个宏函数,用来定义一个没有参数的系统调用的封装。参数意义:

  1. pid_t:表示系统调用返回值类型,代表进程id
  2. fork:表示这个系统调用名称是fork

这个宏定义展开之后如下带有AT&T格式的汇编代码的程序:

pid_t fork(void)
{
	long __res;
	__asm__ volatile ("int $0x80"
		: "=a" (__res)
		: "0" (__NR_fork);
	__syscall_return(pid_t,__res);
	)
}

翻译一下就是:

pid_t fork(void)
{
	long __res;
	$eax=__NR__fork;
	int $0x80;
	__res=$eax;
	__syscall_return(pid_t,__res);
}

__NR__fork是fork的系统调用号2,__syscall_return是另一个宏,用于检查系统调用返回值。

当用户调用某个系统调用的时候,实际是执行了以上一段汇编代码。CPU执行到int $0x80时,会保存现场以便恢复,接着会将特权状态切换到内核态。然后CPU便会查找中断向量表中的第0x80号元素。

切换堆栈

在实际执行中断向量表中的第0x80号元素所对应的函数之前,CPU首先还要进行栈的切换。在Linux中,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。但在应用程序调用0x80号中断时,程序的执行流程从用户态切换到内核态,这时程序的当前栈必须也相应地从用户栈切换到内核栈从中断处理函数中返回时,程序的当前栈还要从内核栈切换回用户栈。所谓的“当前栈”,指的是ESP的值所在的栈空间。如果ESP的值位于用户栈的范围内,那么程序的当前栈就是用户栈,反之亦然。此外,寄存器SS的值还应该指向当前栈所在的页。

所以,将当前栈由用户栈切换为内核栈的实际行为就是:

  1. 保存当前的寄存器ESPSS的值。
  2. ESPSS的值设置为内核栈的相应值。

反过赖将当前栈从内核态转为用户态就是:

  • 恢复原来的ESPSS的值

0x80号中断发生的时候,CPU除了切入内核态之外,还会自动完成下列几件事:

  1. 找到当前进程的内核栈(每。 个进程都有自己的内核栈)。
  2. 在内核栈中依次压入用户态的寄存器SSESPEFLAGSCSEIP.

而当内核从系统调用中返回的时候,须要调用iret指令来回到用户态,iret指令则会从内核栈里弹出寄存器SSESPEFLAGSCSEIP 的值,使得栈恢复到用户态的状态。这个过程可以用下图来表示。

在这里插入图片描述

中断处理程序

栈切换完成之后,程序流程就切换到了中断向量表中记录的0x80号中断处理程序。
在这里插入图片描述
具体见《程序员的自我修养》,下面直接上图调用流程:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值