程序员的自我修养 - 系统调用与API

一、什么是系统调用

由于系统资源有限(包括文件、网络、IO、各种设备等),并且同时可能有多个程序访问同一资源,为了不造成访问冲突,现代操作系统将可能产生冲突的资源保护起来,而保护的手段便是通过系统调用来统一管理。

系统调用覆盖的范围很广:

  • 程序运行所必需的支持:创建、退出进程和线程,进程内存管理等
  • 对系统资源的访问:文件、网络、设备、图形界面的操作

无论是windows还是Linux,都将提供一整套接口以便应用程序来访问系统资源。windows以0x2E号中断作为系统调用入口,而linux以0x80中断号作为系统调用入口。

二、linux系统调用

以0x80中断号作为系统调用入口,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号(EAX=1表示exit,退出进程;EAX=2表示fork,创建进程……),每个系统调用对应内核源代码中的一个函数,它们以”sys_”开头。当系统调用返回时,EAX又作为调用结果的返回值。

系统调用以C语言的形式定义在”/usr/include/unistd.h”。我们完全可以绕过glibc的fopen、fread、fwrite而直接调用对应的系统调用函数open、read、write。

由于系统调用移植性弱,因此引入运行库,作为应用程序与系统调用之间的桥梁。当然运行库有自己的缺陷,为保证多个平台能够相互通用,于是它只能取各个平台的交集。

三、系统调用原理

1.特权级与中断

现代操作系统中,通常有两种特权级别,分别是用户模式和内核模式,也称为用户态和内核态。普通应用程序运行在用户态,诸多操作受限,如访问硬件设备、开关中断、改变特权模式等。

系统调用运行在内核态,而应用程序基本都运行在用户态的。用户态的程序如何运行内核态的代码呢?OS一般通过中断来从用户态切换到内核态。中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作去处理更加重要的事情。

中断有两个属性,一个称为中断号,一个是中断处理程序(Interrupt Service Routine, ISR)。不同的中断有不同的中断号,不同的中断号对应不同的中断处理程序。在内核中,有一个数组称为中断向量表,向量表的第n项包含指第n号中断处理程序的指针。

这里写图片描述

中断有两种类型:

  • 硬中断:来自于硬件的异常或者其他事件的发生,如电源断电、键盘被按下等
  • 软中断:通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断执行其中断处理程序。

下面重点介绍软中断。
由于中断号有限,操作系统不会舍得用一个中断号对应一个系统调用,而更倾向于用一个或少数几个中断号对应所有的系统调用。windows绝大多数系统调用由int 0x2E来触发,linux用int 0x80来触发所有系统调用。

但新问题来了:同一中断号,怎么区分不同的系统调用呢?和中断一样,每个系统调用都有一个系统调用号,这个系统调用号就是系统调用在系统调用表的位置,例如linux里fork系统调用为2。这个系统调用号在执行int指令前会被放置于某个固定寄存器中,对应的中断代码会取得系统调用号,并且调用正确的函数。以linux以例,这个固定寄存器为eax。

2.基于int的linux经典系统调用实现

以fork为例,流程如下:

(1)触发中断

不贴代码,只讲框架。

  • 通过_syscall*系列函数来执行实际的函数调用,eax寄存器既用来传递参数,又存储函数返回值。若传递给函数的参数大于1个,则可用EBX, ECX, EDX, ESI, EDI, EBP来传递参数(仅对x86的linux系统)。
  • 将返回值相应地转换为错误码errno,这个错误码既可以是全局变量,也可以存储于TLS(Thread Local Storage,线程局部存储,每个线程独有)

(2)堆栈切换

在实际执行int 0x80对应的函数之前,CPU要先进行栈的切换。linux下,用户态和内核态使用不同的栈,各自负责各自的函数调用,互不干扰。同理,中断处理程序返回时,程序的当前栈还要从内核栈切换回用户栈。

如果ESP指向用户栈的范围内,那么程序的当前栈就是用户栈,反之则为内核栈。此外,寄存器SS还指向当前栈所在的页。所以,用户栈切换到内核栈的动作实际为:

  • 找到当前进程的内核栈
  • 保存当前的ESP, SS值到内核栈
  • 在内核栈中依次压入用户态的寄存器SS, ESP, EFLAGS, CS, EIP
  • 将ESP, SS设置为内核栈的值

从内核栈切换回用户栈动作正好相反,不再赘述。

(3)中断处理程序

这里写图片描述

从上图可知,不同的中断号对应不同的中断处理程序。而这部分实现可在Linux/arch/i386/kernel/traps.c中找到。

中断流程:

  • 将用户态寄存器压入内核栈(这些寄存器用来传递函数参数的,分别为ebx, ecx, edx, esi, edi, ebp)
  • 比较系统调用号与最大系统调用号,以判断系统调用的有效性
  • 若系统调用判断为有效,则通过sys_call_table来查找中断处理程序并执行
  • 执行完中断处理程序后,恢复步骤1中被保存的用户态寄存器
  • 最后通过iret指令从中断处理程序中返回

从以上系统调用的流程可以看到,系统调用的开销较普通函数调用的开销要大的多,主要开销在于用户栈与内核栈来回切换,更多寄存器的入栈出栈等。具体可参考下述链接:

https://www.zhihu.com/question/32043825

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值