Linux系统调用原理

Stella981
• 阅读 963

一、什么是系统调用

系统调用 跟用户自定义函数一样也是一个函数,不同的是 系统调用 运行在内核态,而用户自定义函数运行在用户态。由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运行在内核态,所以操作系统必须提供一种能够进入内核态的方式,系统调用 就是这样的一种机制。

系统调用 是 Linux 内核提供的一段代码(函数),其实现了一些特定的功能,用户可以通过 int 0x80 中断(x86 CPU)或者 syscall 指令(x64 CPU)来调用 系统调用

二、进入系统调用

本文主要介绍的是 x86 CPU 进入系统调用的方式

Linux 提供了 int 0x80 中断来让用户程序进入 系统调用,我们来看看 Linux 对 int 0x80 中断的处理初始化过程:

void __init trap_init(void){    ...    set_system_gate(SYSCALL_VECTOR, &system_call);    ...}

系统初始化时,会在 trap_init() 函数中对 int 0x80 中断处理进行初始化,设置其中断处理过程入口为 system_callsystem_call 是一段由汇编语言编写的代码,我们看看关键部分,如下:

ENTRY(system_call)    ...    call *SYMBOL_NAME(sys_call_table)(,%eax,4)    movl %eax,EAX(%esp)     # save the return value    ...

我们把上面的汇编改写成 C 代码如下:

void system_call(){    ...    // 变量 eax 代表 eax 寄存器的值    syscall = sys_call_table[eax];    eax = syscall();    ...}

sys_call_table 变量是一个数组,数组的每一个元素代表一个 系统调用 的入口,其定义如下(在文件 arch/i386/kernel/entry.S 中):

.dataENTRY(sys_call_table)    .long SYMBOL_NAME(sys_ni_syscall)    .long SYMBOL_NAME(sys_exit)    .long SYMBOL_NAME(sys_fork)    .long SYMBOL_NAME(sys_read)    .long SYMBOL_NAME(sys_write)    .long SYMBOL_NAME(sys_open)    .long SYMBOL_NAME(sys_close)    ...

翻译成 C 代码如下:

long sys_call_table[] = {   sys_ni_syscall,   sys_exit,   sys_fork,   sys_read,   sys_write,   sys_open,   sys_close,   ...};

用户调用 系统调用 时,通过向 eax 寄存器写入要调用的 系统调用 编号,这个编号就是 sys_call_table 数组的下标。 system_call 过程获取 eax 寄存器的值,然后通过 eax 寄存器的值找到要调用的 系统调用 入口,并且进行调用。调用完成后,系统调用 会把返回值保存到 eax 寄存器中。

原理如下图(图片来源 https://developer.ibm.com/zh/technologies/linux/tutorials/l-system-calls/ ):

Linux系统调用原理

三、系统调用实现

当用户要调用 系统调用 时,需要通过向 eax 寄存器写入要调用的 系统调用 编号。因为 用户态 和 内核态 使用的栈不同,而调用 系统调用 是在用户态调用的,而进入 系统调用 后会变成内核态,所以参数就不能通过栈来传递。Linux 使用寄存器来传递参数,参数与寄存器的关系如下:

  • 第1个参数放置在 ebx 寄存器。

  • 第2个参数放置在 ecx 寄存器。

  • 第3个参数放置在 edx 寄存器。

  • 第4个参数放置在 esi 寄存器。

  • 第5个参数放置在 edi 寄存器。

  • 第6个参数放置在 ebp 寄存器。

而 Linux 进入中断处理程序时,会把这些寄存器的值保存到内核栈中,这样 系统调用 就能通过内核栈来获取到参数。

下面我们通过 sys_open() 系统调用来说明一下 系统调用 的运作方式,sys_open() 实现如下:

asmlinkage long sys_open(const char *filename, int flags, int mode){    ...}

一般 系统调用 都需要使用 asmlinkage 编译选项,asmlinkage 编译选项是告诉编译器从栈中读取参数,其实际是封装了 GCC 的编译选项,如下:

#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

__attribute__((regparm(0))) 就是告诉 GCC 所有参数都从栈中读取,而 Linux 进入中断处理上下文时,会把 ebxecxedxesiediebp 寄存器的值保存到内核栈中,那么 系统调用 就可以从内核栈获取到参数的值。

但由于寄存器只能传递 32 位的整型值(x86 CPU),所以参数一般只能传递指针或者整型的数值,如果要获取指针对应结构的数据,就必须通过从用户空间复制到内核空间,如 sys_open() 系统调用获取要打开的文件路径:

asmlinkage long sys_open(const char *filename, int flags, int mode){    char * tmp;    ...    tmp = getname(filename);    ...}

getname() 函数就是用于从用户空间复制数据到内核空间。

本文分享自微信公众号 - Linux内核那些事(like_linux)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
3年前
strace命令使用
命令介绍strace是Linux环境下的一款程序调试工具,用来输出一个应用程序所使用的系统调用。strace底层使用内核的ptrace特性来实现其功能。什么是系统调用?系统调用是通向操作系统本身的接口,是面向底层硬件的。通过系统调用,可以使得用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互,是操作系统留给
Stella981 Stella981
3年前
Linux下的strace命令介绍
简介strace常用来跟踪进程执行时的系统调用和所接收的信号。在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。输出参数含义
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Linux内核编译及添加系统调用
1总体设计思路系统调用的本质是调用内核函数,以内核态运行程序。为了在内核态下运行,本实验针对Linux的内核进行修改,增加自定义系统调用函数实现用户态程序对任意进程的nice值进行修改或者读取来进行测试。2主要函数的接口设计核心态程序SYSCALL\_DEFINE3(mysetnice,pid\_t,pid,int,flag,i
Stella981 Stella981
3年前
Linux 系统调用(system call)
1系统调用:(SYSTEMCALL)操作系统(operatingsystem)内核中有一组实现系统功能的过程,系统调用就是对上述过程的调用。程序员利用系统调用,向OS提出服务请求,由OS代为完成。一般情况下进程是不能够存取系统内核的。它不能存取内核使用的内核段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是个例
Stella981 Stella981
3年前
Socket与系统调用深度分析
Socket与系统调用深度分析实验环境:Linux5.0.1内核32位系统的MenuOS本文主要解决两个问题用户态如何通过中断进入socket的系统调用socket抽象层如何通过多态的机制,来支持不同的传输层的协议。也就是socket作为父类,TCP/UDP为子类,父类指向子类对象,实现多态
Stella981 Stella981
3年前
Linux 面试知识点笔记
问:linux的体系结构?!(https://oscimg.oschina.net/oscnet/7e41e5605a979bbf6cc4263647ae72292f8.jpg)体系结构主要分为用户态(用户上层活动)和内核态内核:本质是一段管理计算机硬件设备的程序系统调用:内核的访问接口,是一种能再简化的操作