09 | 系统调用:公司成立好了就要开始接项目
下载APP
关闭
渠道合作
推荐作者
09 | 系统调用:公司成立好了就要开始接项目
2019-04-15 刘超 来自北京
《趣谈Linux操作系统》
课程介绍
讲述:刘超
时长14:27大小13.20M
上一节,系统终于进入了用户态,公司由一个“皮包公司”进入正轨,可以开始接项目了。
这一节,我们来解析 Linux 接项目的办事大厅是如何实现的,这是因为后面介绍的每一个模块,都涉及系统调用。站在系统调用的角度,层层深入下去,就能从某个系统调用的场景出发,了解内核中各个模块的实现机制。
有的时候,我们的客户觉得,直接去办事大厅还是不够方便。没问题,Linux 还提供了 glibc 这个中介。它更熟悉系统调用的细节,并且可以封装成更加友好的接口。你可以直接用。
glibc 对系统调用的封装
我们以最常用的系统调用 open,打开一个文件为线索,看看系统调用是怎么实现的。这一节我们仅仅会解析到从 glibc 如何调用到内核的 open,至于 open 怎么实现,怎么打开一个文件,留到文件系统那一节讲。
现在我们就开始在用户态进程里面调用 open 函数。
为了方便,大部分用户会选择使用中介,也就是说,调用的是 glibc 里面的 open 函数。这个函数是如何定义的呢?
在 glibc 的源代码中,有个文件 syscalls.list,里面列着所有 glibc 的函数对应的系统调用,就像下面这个样子:
另外,glibc 还有一个脚本 make-syscall.sh,可以根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件。这个文件里面定义了一些宏,例如 #define SYSCALL_NAME open。
glibc 还有一个文件 syscall-template.S,使用上面这个宏,定义了这个系统调用的调用方式。
这里的 PSEUDO 也是一个宏,它的定义如下:
里面对于任何一个系统调用,会调用 DO_CALL。这也是一个宏,这个宏 32 位和 64 位的定义是不一样的。
32 位系统调用过程
我们先来看 32 位的情况(i386 目录下的 sysdep.h 文件)。
这里,我们将请求参数放在寄存器里面,根据系统调用的名称,得到系统调用号,放在寄存器 eax 里面,然后执行 ENTER_KERNEL。
在 Linux 的源代码注释里面,我们可以清晰地看到,这些寄存器是如何传递系统调用号和参数的。
这里面的 ENTER_KERNEL 是什么呢?
int 就是 interrupt,也就是“中断”的意思。int $0x80 就是触发一个软中断,通过它就可以陷入(trap)内核。
在内核启动的时候,还记得有一个 trap_init(),其中有这样的代码:
这是一个软中断的陷入门。当接收到一个系统调用的时候,entry_INT80_32 就被调用了。
通过 push 和 SAVE_ALL 将当前用户态的寄存器,保存在 pt_regs 结构里面。
进入内核之前,保存所有的寄存器,然后调用 do_syscall_32_irqs_on。它的实现如下:
在这里,我们看到,将系统调用号从 eax 里面取出来,然后根据系统调用号,在系统调用表中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。如果仔细比对,就能发现,这些参数所对应的寄存器,和 Linux 的注释是一样的。
根据宏定义,#define ia32_sys_call_table sys_call_table,系统调用就是放在这个表里面。至于这个表是如何形成的,我们后面讲。
当系统调用结束之后,在 entry_INT80_32 之后,紧接着调用的是 INTERRUPT_RETURN,我们能够找到它的定义,也就是 iret。
iret 指令将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候用户态进程恢复执行。
这里我总结一下 32 位的系统调用是如何执行的。
64 位系统调用过程
我们再来看 64 位的情况(x86_64 下的 sysdep.h 文件)。
和之前一样,还是将系统调用名称转换为系统调用号,放到寄存器 rax。这里是真正进行调用,不是用中断了,而是改用 syscall 指令了。并且,通过注释我们也可以知道,传递参数的寄存器也变了。
syscall 指令还使用了一种特殊的寄存器,我们叫特殊模块寄存器(Model Specific Registers,简称 MSR)。这种寄存器是 CPU 为了完成某些特殊控制功能为目的的寄存器,其中就有系统调用。
在系统初始化的时候,trap_init 除了初始化上面的中断模式,这里面还会调用 cpu_init->syscall_init。这里面有这样的代码:
rdmsr 和 wrmsr 是用来读写特殊模块寄存器的。MSR_LSTAR 就是这样一个特殊的寄存器,当 syscall 指令调用的时候,会从这个寄存器里面拿出函数地址来调用,也就是调用 entry_SYSCALL_64。
在 arch/x86/entry/entry_64.S 中定义了 entry_SYSCALL_64。
这里先保存了很多寄存器到 pt_regs 结构里面,例如用户态的代码段、数据段、保存参数的寄存器,然后调用 entry_SYSCALL64_slow_pat->do_syscall_64。
在 do_syscall_64 里面,从 rax 里面拿出系统调用号,然后根据系统调用号,在系统调用表 sys_call_table 中找到相应的函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。如果仔细比对,你就能发现,这些参数所对应的寄存器,和 Linux 的注释又是一样的。
所以,无论是 32 位,还是 64 位,都会到系统调用表 sys_call_table 这里来。
在研究系统调用表之前,我们看 64 位的系统调用返回的时候,执行的是 USERGS_SYSRET64。定义如下:
这里,返回用户态的指令变成了 sysretq。
我们这里总结一下 64 位的系统调用是如何执行的。
系统调用表
前面我们重点关注了系统调用的方式,都是最终到了系统调用表,但是到底调用内核的什么函数,还没有解读。
现在我们再来看,系统调用表 sys_call_table 是怎么形成的呢?
32 位的系统调用表定义在 arch/x86/entry/syscalls/syscall_32.tbl 文件里。例如 open 是这样定义的:
64 位的系统调用定义在另一个文件 arch/x86/entry/syscalls/syscall_64.tbl 里。例如 open 是这样定义的:
第一列的数字是系统调用号。可以看出,32 位和 64 位的系统调用号是不一样的。第三列是系统调用的名字,第四列是系统调用在内核的实现函数。不过,它们都是以 sys_ 开头。
系统调用在内核中的实现函数要有一个声明。声明往往在 include/linux/syscalls.h 文件中。例如 sys_open 是这样声明的:
真正的实现这个系统调用,一般在一个.c 文件里面,例如 sys_open 的实现在 fs/open.c 里面,但是你会发现样子很奇怪。
SYSCALL_DEFINE3 是一个宏系统调用最多六个参数,根据参数的数目选择宏。具体是这样定义的:
如果我们把宏展开之后,实现如下,和声明的是一样的。
声明和实现都好了。接下来,在编译的过程中,需要根据 syscall_32.tbl 和 syscall_64.tbl 生成自己的 unistd_32.h 和 unistd_64.h。生成方式在 arch/x86/entry/syscalls/Makefile 中。
这里面会使用两个脚本,其中第一个脚本 arch/x86/entry/syscalls/syscallhdr.sh,会在文件中生成 #define __NR_open;第二个脚本 arch/x86/entry/syscalls/syscalltbl.sh,会在文件中生成 __SYSCALL(__NR_open, sys_open)。这样,unistd_32.h 和 unistd_64.h 是对应的系统调用号和系统调用实现函数之间的对应关系。
在文件 arch/x86/entry/syscall_32.c,定义了这样一个表,里面 include 了这个头文件,从而所有的 sys_ 系统调用都在这个表里面了。
同理,在文件 arch/x86/entry/syscall_64.c,定义了这样一个表,里面 include 了这个头文件,这样所有的 sys_ 系统调用就都在这个表里面了。
总结时刻
系统调用的过程还是挺复杂的吧?如果加上上一节的内核态和用户态的模式切换,就更复杂了。这里我们重点分析 64 位的系统调用,我将整个完整的过程画了一张图,帮你总结、梳理一下。
课堂练习
请你根据这一节的分析,看一下与 open 这个系统调用相关的文件都有哪些,在每个文件里面都做了什么?如果你要自己实现一个系统调用,能不能照着 open 来一个呢?
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 37
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
08 | 内核初始化:生意做大了就得成立公司
下一篇
10 | 进程:公司接这么多项目,如何管?
精选留言(124)
- 孟晓冬2019-04-15这个专栏要有一定的知识储备才能学习,起码要熟悉c,数据结构,linux系统管理,否则只会一脸懵逼的进来,一脸懵逼的出去
作者回复: 是的
共 19 条评论247 - why2019-04-15- glibc 将系统调用封装成更友好的接口 - 本节解析 glibc 函数如何调用到内核的 open --- - 用户进程调用 open 函数 - glibc 的 syscal.list 列出 glibc 函数对应的系统调用 - glibc 的脚本 make_syscall.sh 根据 syscal.list 生成对应的宏定义(函数映射到系统调用) - glibc 的 syscal-template.S 使用这些宏, 定义了系统调用的调用方式(也是通过宏) - 其中会调用 DO_CALL (也是一个宏), 32位与 64位实现不同 --- - 32位 DO_CALL (位于 i386 目录下 sysdep.h) - 将调用参数放入寄存器中, 由系统调用名得到系统调用号, 放入 eax - 执行 ENTER_KERNEL(一个宏), 对应 int $0x80 触发软中断, 进入内核 - 调用软中断处理函数 entry_INT80_32(内核启动时, 由 trap_init() 配置) - entry_INT80_32 将用户态寄存器存入 pt_regs 中(保存现场以及系统调用参数), 调用 do_syscall_32_iraq_on - do_syscall_32_iraq_on 从 pt_regs 中取系统调用号(eax), 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用 - entry_INT80_32 调用 INTERRUPT_RUTURN(一个宏)对应 iret 指令, 系统调用结果存在 pt_regs 的 eax 位置, 根据 pt_regs 恢复用户态进程 --- - 64位 DO_CALL (位于 x86_64 目录下 sysdep.h) - 通过系统调用名得到系统调用号, 存入 rax; 不同中断, 执行 syscall 指令 - MSR(特殊模块寄存器), 辅助完成某些功能(包括系统调用) - trap_init() 会调用 cpu_init->syscall_init 设置该寄存器 - syscall 从 MSR 寄存器中, 拿出函数地址进行调用, 即调用 entry_SYSCALL_64 - entry_SYSCALL_64 先保存用户态寄存器到 pt_regs 中 - 调用 entry_SYSCALL64_slow_pat->do_syscall_64 - do_syscall_64 从 rax 取系统调用号, 从系统调用表得到对应实现函数, 取 pt_regs 中存储的参数, 调用系统调用 - 返回执行 USERGS_SYSRET64(一个宏), 对应执行 swapgs 和 sysretq 指令; 系统调用结果存在 pt_regs 的 ax 位置, 根据 pt_regs 恢复用户态进程 --- - 系统调用表 sys_call_table - 32位 定义在 arch/x86/entry/syscalls/syscall_32.tbl - 64位 定义在 arch/x86/entry/syscalls/syscall_64.tbl - syscall_*.tbl 内容包括: 系统调用号, 系统调用名, 内核实现函数名(以 sys 开头) - 内核实现函数的声明: include/linux/syscall.h - 内核实现函数的实现: 某个 .c 文件, 例如 sys_open 的实现在 fs/open.c - .c 文件中, 以宏的方式替代函数名, 用多层宏构建函数头 - 编译过程中, 通过 syscall_*.tbl 生成 unistd_*.h 文件 - unistd_*.h 包含系统调用与实现函数的对应关系 - syscall_*.h include 了 unistd_*.h 头文件, 并定义了系统调用表(数组)展开共 1 条评论150
- Sharry2019-05-15什么是系统调用? 系统调用是操作系统提供给程序设计人员使用系统服务的接口 系统调用流程 Linux 提供了 glibc 库, 它封装了系统调用接口, 对上层更友好的提供服务, 系统调用最终都会通过 DO_CALL 发起, 这是一个宏定义, 其 32 位和 64 位的定义是不同的 - 32 位系统调用 - 用户态 - 将请求参数保存到寄存器 - 将系统调用名称转为系统调用号保存到寄存器 eax 中 - 通过软中断 ENTER_KERNEL 进入内核态 - 内核态 - 将用户态的寄存器保存到 pt_regs 中 - 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数 - 执行函数实现, 将返回值写入 pt_regs 的 ax 位置 - 通过 INTERRUPT_RETURN 根据 pt_regs 恢复用户态进程 - 64 位系统调用 - 用户态 - 将请求参数保存到寄存器 - 将系统调用名称转为系统调用号保存到寄存器 rax 中 - **通过 syscall 进入内核态** - 内核态 - 将用户态的寄存器保存到 pt_regs 中 - 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数 - 执行函数实现, 将返回值写入 pt_regs 的 ax 位置 - **通过 sysretq 返回用户态**展开共 8 条评论93
- 江山未2019-07-26宏是什么?给像我一样不懂C的人: 1,使用命令 #define 定义宏。该命令允许把一个名称指定成任何所需的文本,例如一个常量值或者一条语句。在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。 2,宏的名称一般使用全大写的形式。 3,宏可以定义参数,参数列表需要使用圆括号包裹,且必须紧跟名称,中间不能有空格。 4,使用#undef NAME取消宏的定义,从而可以重新定义或使用与宏重名的函数或变量。 5,出现在字符串中的宏名称不会被预编译器展开。展开
作者回复: 太好了,谢谢补充
共 2 条评论45 - weihebuken2019-04-15我想问,想看懂这篇,我先需要看哪些书,或者贮备哪些知识先,真的很懵。。。
作者回复: 主要理解过程,不必纠结代码
共 5 条评论34 - 望天2019-05-23这些东西我觉得不必要深入每一行代码,大概过一遍,知道整体流程,宏观流程就OK了(比如上面图片的概括)。反正很多细节过一段时间也会忘。
作者回复: 对的
共 2 条评论31 - William2019-04-15大家可以参考glibc的源码理解,https://www.gnu.org/software/libc/started.html。 主要过程是CPU上下文切换的过程。
作者回复: 赞
共 2 条评论18 - 春和景明2019-04-15开始吃力了,只能排除细节,先了解几个重要阶段了。
作者回复: 对的,就是这个方法
13 - 刘強2019-04-15这个专栏,源码是linux哪个版本的?
作者回复: https://elixir.bootlin.com/linux/v4.13.16
共 5 条评论12 - garlic2019-06-031 用户态glibc 32位置 sysdeps\unix\syscall.list sysdeps\unix\syscall-tempate.S sysdeps\unix\make-syscalls.sh sysdeps\unix\sysv\linux\i386\sysdep.h (32) sysdeps\unix\sysv\linux\x86_64\sysdep.h (64) 生成用户接口 2. 内核态: X86_32 /init/main.c start_kernel ->trap_init /arch/x86/kernel/traps.c trap_init -> idt_setup_traps /arch/x86/kernel/idt.c idt_setup_traps->idt_setup_from_table idt_setup_from_table->entry_INT80_32 /arch/x86/entry/entry_32.S entry_INT80_32->do_int80_syscall_32 /arch/x86/entry/common.c do_int80_syscall_32->do_syscall_32_irqs_on /arch/x86/entry/syscall_32.c ia32_sys_call_table[__NR_syscall_compat_max+1] /arch/x86/entry/entry_32.S entry_INT80_32->INTERRUPT_RETURN /arch/x86/include/asm/irqflags.h swapgs, sysretl 内核态: X86_64 /init/main.c start_kernel ->trap_init /arch/x86/kernel/traps.c trap_init -> idt_setup_traps /arch/x86/kernel/idt.c idt_setup_traps->idt_setup_from_table idt_setup_from_table->entry_INT80_32 /arch/x86/kernel/cpu/common.c cpu_init->syscall_init /arch/x86/entry/entry_64.S entry_SYSCALL_64->USERGS_SYSRET64 /arch/x86/entry/common.c do_syscall_64->do_syscall_32_irqs_on /arch/x86/entry/syscall_64.c sys_call_table[__NR_syscall_compat_max+1] arch/x86/entry/entry_64.S entry_SYSCALL_64-> USERGS_SYSRET64 /arch/x86/include/asm/irqflags.h swapgs; sysretq; 3. 增加一个系统调用 linux-5.2-rc2/arch/x86/entry/syscalls/syscall_64.tbl 新增编号 linux-5.2-rc2/include/linux/syscalls.h 增加声明 kernel/linux-5.2-rc2/fs/iadd_test.c 增加定义目录可选 kernel/linux-5.2-rc2/fs/Makefile 修改makefile加入新增源文件。 作业笔记:https://garlicspace.com/2019/06/02/linux下实现一个系统调用/展开共 1 条评论10
- kdb_reboot2019-04-15参数如果超过6个存在哪里?(32/64两种情况
作者回复: 不许超过,系统调用可以查一下,没这么多参数
共 3 条评论10 - 张迪2019-06-12老师你好,什么是用户态什么是内核态,
作者回复: 前几节讲的呀,保护模式
共 2 条评论6 - windcaller2019-07-06够硬核的课程!共 1 条评论5
- Tianz2019-05-08系统调用层: 1 用户在应用空间想要用内核环境的资源,怎么办捏?linux死规定了,就只能通过系统调用层 2 用户想要用什么资源就得通过调用对应的系统调用函数并加上参数 3 什么时候才真正实现了得到你想要的资源呢?那就是进入到内核空间(在中断处理函数里就可以),并调用了对应的系统调用函数(通过你在应用空间使用的函数(这些是名字固定了的) --> 里面有函数计算出对应的(映射的)真正系统调用号(就是真正系统调用函数地址在系统调用数组里的位置) --> 通过现在得到的系统调用号从系统调用数组中拿出这个真正的系统调用函数并执行,肯定加上一起传下来的参数了 --> 返回展开
作者回复: 赞
共 2 条评论5 - 时间是最真的答案2019-04-15想问一下,java开发的,会一些基础的linux命令,怎么学好这个专栏?感觉看的一头雾水,消化不了,有什么建议吗
作者回复: 重点理解过程,不要纠结代码,可以跳过这一节看下面的
4 - Geek_e53af32020-09-01老师请问: 1. 32位执行int $80是硬件发出中断信号,然后cpu直接处理吗 2. 此时是谁在执行代码,此时有进程概念吗,还是说内核这个软件本身在执行,是否属于用户进程的时间,时间片用的是谁的? 3. 进入内核态后有什么状态来标志吗,为什么用户态之前不能做的操作,进入内核态就可以做了? 4. 64位的内核态切换就很像单纯的函数调用,为什么也可以进入内核态?展开共 2 条评论3
- 蹦哒2020-06-10老师请问:中断和syscall有什么区别呢?看着实际的过程主要都是操作寄存器(虽然32位和64位操作的寄存器不一样)
作者回复: 从软中断改特殊指令,性能更好
3 - Daiver2019-11-03还好做过内核添加系统调用实验,不然真的是一脸懵逼3
- ZYecho2019-09-06老师你好,这个地方保存的时候是保存在pt-regs结构体中,那么当中断通过iret进行返回的时候,cpu是如何知道我们的现场是存储在pt-regs结构体当中呢? 我理解iret指令应该只会操作cpu当中的寄存器才对。
作者回复: 栈顶呀
3 - 陈锴2019-04-17有个小问题,64位内核是不是已经取消使用cs 代码寄存器 和 ds数据段寄存器了(或者说默认设为0了),也就是只采用分页而不采用分段了
作者回复: 分段还是有,只不过是残废的状态,就像你说的一样,到了内存那一节会详细说这个问题
3