趣谈Linux操作系统
刘超
网易杭州研究院云计算技术部首席架构师
立即订阅
19393 人已学习
课程目录
已完结 72 讲
0/4登录后,你可以任选4讲全文学习。
入门准备篇 (3讲)
开篇词 | 为什么要学习Linux操作系统?
免费
01 | 入学测验:你究竟对Linux操作系统了解多少?
02 | 学习路径:爬过这六个陡坡,你就能对Linux了如指掌
核心原理篇:第一部分 Linux操作系统综述 (3讲)
03 | 你可以把Linux内核当成一家软件外包公司的老板
04 | 快速上手几个Linux命令:每家公司都有自己的黑话
05 | 学会几个系统调用:咱们公司能接哪些类型的项目?
核心原理篇:第二部分 系统初始化 (4讲)
06 | x86架构:有了开放的架构,才能打造开放的营商环境
07 | 从BIOS到bootloader:创业伊始,有活儿老板自己上
08 | 内核初始化:生意做大了就得成立公司
09 | 系统调用:公司成立好了就要开始接项目
核心原理篇:第三部分 进程管理 (10讲)
10 | 进程:公司接这么多项目,如何管?
11 | 线程:如何让复杂的项目并行执行?
12 | 进程数据结构(上):项目多了就需要项目管理系统
13 | 进程数据结构(中):项目多了就需要项目管理系统
14 | 进程数据结构(下):项目多了就需要项目管理系统
15 | 调度(上):如何制定项目管理流程?
16 | 调度(中):主动调度是如何发生的?
17 | 调度(下):抢占式调度是如何发生的?
18 | 进程的创建:如何发起一个新项目?
19 | 线程的创建:如何执行一个新子项目?
核心原理篇:第四部分 内存管理 (7讲)
20 | 内存管理(上):为客户保密,规划进程内存空间布局
21 | 内存管理(下):为客户保密,项目组独享会议室封闭开发
22 | 进程空间管理:项目组还可以自行布置会议室
23 | 物理内存管理(上):会议室管理员如何分配会议室?
24 | 物理内存管理(下):会议室管理员如何分配会议室?
25 | 用户态内存映射:如何找到正确的会议室?
26 | 内核态内存映射:如何找到正确的会议室?
核心原理篇:第五部分 文件系统 (4讲)
27 | 文件系统:项目成果要归档,我们就需要档案库
28 | 硬盘文件系统:如何最合理地组织档案库的文档?
29 | 虚拟文件系统:文件多了就需要档案管理系统
30 | 文件缓存:常用文档应该放在触手可得的地方
核心原理篇:第六部分 输入输出系统 (5讲)
31 | 输入与输出:如何建立售前售后生态体系?
32 | 字符设备(上):如何建立直销模式?
33 | 字符设备(下):如何建立直销模式?
34 | 块设备(上):如何建立代理商销售模式?
35 | 块设备(下):如何建立代理商销售模式?
核心原理篇:第七部分 进程间通信 (7讲)
36 | 进程间通信:遇到大项目需要项目组之间的合作才行
37 | 信号(上):项目组A完成了,如何及时通知项目组B?
38 | 信号(下):项目组A完成了,如何及时通知项目组B?
39 | 管道:项目组A完成了,如何交接给项目组B?
40 | IPC(上):不同项目组之间抢资源,如何协调?
41 | IPC(中):不同项目组之间抢资源,如何协调?
42 | IPC(下):不同项目组之间抢资源,如何协调?
核心原理篇:第八部分 网络系统 (7讲)
43 预习 | Socket通信之网络协议基本原理
43 | Socket通信:遇上特大项目,要学会和其他公司合作
44 | Socket内核数据结构:如何成立特大项目合作部?
45 | 发送网络包(上):如何表达我们想让合作伙伴做什么?
46 | 发送网络包(下):如何表达我们想让合作伙伴做什么?
47 | 接收网络包(上):如何搞明白合作伙伴让我们做什么?
48 | 接收网络包(下):如何搞明白合作伙伴让我们做什么?
核心原理篇:第九部分 虚拟化 (7讲)
49 | 虚拟机:如何成立子公司,让公司变集团?
50 | 计算虚拟化之CPU(上):如何复用集团的人力资源?
51 | 计算虚拟化之CPU(下):如何复用集团的人力资源?
52 | 计算虚拟化之内存:如何建立独立的办公室?
53 | 存储虚拟化(上):如何建立自己保管的单独档案库?
54 | 存储虚拟化(下):如何建立自己保管的单独档案库?
55 | 网络虚拟化:如何成立独立的合作部?
核心原理篇:第十部分 容器化 (4讲)
56 | 容器:大公司为保持创新,鼓励内部创业
57 | Namespace技术:内部创业公司应该独立运营
58 | CGroup技术:内部创业公司应该独立核算成本
59 | 数据中心操作系统:上市敲钟
实战串讲篇 (9讲)
60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔
61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔
62 | 知识串讲:用一个创业故事串起操作系统原理(一)
63 | 知识串讲:用一个创业故事串起操作系统原理(二)
64 | 知识串讲:用一个创业故事串起操作系统原理(三)
65 | 知识串讲:用一个创业故事串起操作系统原理(四)
66 | 知识串讲:用一个创业故事串起操作系统原理(五)
67 | 期末测试:这些操作系统问题,你真的掌握了吗?
结束语 | 永远别轻视任何技术,也永远别轻视自己
免费
专栏加餐 (2讲)
学习攻略(一):学好操作系统,需要掌握哪些前置知识?
“趣谈Linux操作系统”食用指南
免费
趣谈Linux操作系统
登录|注册

09 | 系统调用:公司成立好了就要开始接项目

刘超 2019-04-15
上一节,系统终于进入了用户态,公司由一个“皮包公司”进入正轨,可以开始接项目了。
这一节,我们来解析 Linux 接项目的办事大厅是如何实现的,这是因为后面介绍的每一个模块,都涉及系统调用。站在系统调用的角度,层层深入下去,就能从某个系统调用的场景出发,了解内核中各个模块的实现机制。
有的时候,我们的客户觉得,直接去办事大厅还是不够方便。没问题,Linux 还提供了 glibc 这个中介。它更熟悉系统调用的细节,并且可以封装成更加友好的接口。你可以直接用。

glibc 对系统调用的封装

我们以最常用的系统调用 open,打开一个文件为线索,看看系统调用是怎么实现的。这一节我们仅仅会解析到从 glibc 如何调用到内核的 open,至于 open 怎么实现,怎么打开一个文件,留到文件系统那一节讲。
现在我们就开始在用户态进程里面调用 open 函数。
为了方便,大部分用户会选择使用中介,也就是说,调用的是 glibc 里面的 open 函数。这个函数是如何定义的呢?
int open(const char *pathname, int flags, mode_t mode)
在 glibc 的源代码中,有个文件 syscalls.list,里面列着所有 glibc 的函数对应的系统调用,就像下面这个样子:
# File name Caller Syscall name Args Strong name Weak names
open - open Ci:siv __libc_open __open open
另外,glibc 还有一个脚本 make-syscall.sh,可以根据上面的配置文件,对于每一个封装好的系统调用,生成一个文件。这个文件里面定义了一些宏,例如 #define SYSCALL_NAME open。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《趣谈Linux操作系统》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(76)

  • 孟晓冬
    这个专栏要有一定的知识储备才能学习,起码要熟悉c,数据结构,linux系统管理,否则只会一脸懵逼的进来,一脸懵逼的出去

    作者回复: 是的

    2019-04-15
    167
  • why
    - 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 头文件, 并定义了系统调用表(数组)
    2019-04-15
    47
  • weihebuken
    我想问,想看懂这篇,我先需要看哪些书,或者贮备哪些知识先,真的很懵。。。

    作者回复: 主要理解过程,不必纠结代码

    2019-04-15
    12
  • 望天
    这些东西我觉得不必要深入每一行代码,大概过一遍,知道整体流程,宏观流程就OK了(比如上面图片的概括)。反正很多细节过一段时间也会忘。

    作者回复: 对的

    2019-05-23
    9
  • Sharry
    什么是系统调用?

    系统调用是操作系统提供给程序设计人员使用系统服务的接口

    系统调用流程

    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 返回用户态**
    2019-05-15
    9
  • William
    大家可以参考glibc的源码理解,https://www.gnu.org/software/libc/started.html。 主要过程是CPU上下文切换的过程。

    作者回复: 赞

    2019-04-15
    7
  • 刘強
    这个专栏,源码是linux哪个版本的?

    作者回复: https://elixir.bootlin.com/linux/v4.13.16

    2019-04-15
    1
    6
  • 春明
    开始吃力了,只能排除细节,先了解几个重要阶段了。

    作者回复: 对的,就是这个方法

    2019-04-15
    5
  • kdb_reboot
    参数如果超过6个存在哪里?(32/64两种情况

    作者回复: 不许超过,系统调用可以查一下,没这么多参数

    2019-04-15
    5
  • 张迪
    老师你好,什么是用户态什么是内核态,

    作者回复: 前几节讲的呀,保护模式

    2019-06-12
    1
    3
  • 阿恺
    老师,请教个问题,对于64位,DO_CALL在两个地方有地址,sysdeps/unix/sysv/linux/x86_64/sysdep.h:179和sysdeps/unix/x86_64/sysdep.h:26,我采用的最新的glibc的git下载。看到的和您给的代码不一样,您采用了前者的注释,后者的代码,两者使用的寄存器不一样。如何知道是通过哪个入口。sysdeps/unix/sysv/linux/x86_64/sysdep.h:179中注释写到,将系统调用号放在rax,后面的代码中的是eax,这里没有看懂。

    作者回复: 64位是rax

    2019-04-15
    3
  • windcaller
    够硬核的课程!
    2019-07-06
    2
  • ZYecho
    老师你好,这个地方保存的时候是保存在pt-regs结构体中,那么当中断通过iret进行返回的时候,cpu是如何知道我们的现场是存储在pt-regs结构体当中呢?
    我理解iret指令应该只会操作cpu当中的寄存器才对。

    作者回复: 栈顶呀

    2019-09-06
    1
  • hello
    请问老师列的代码是哪个版本的内核代码?和我看的2.6.34和5.2.8的不一样让我很慌张啊

    作者回复: 4.13

    2019-08-15
    1
  • 江山未
    宏是什么?给像我一样不懂C的人:
    1,使用命令 #define 定义宏。该命令允许把一个名称指定成任何所需的文本,例如一个常量值或者一条语句。在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。
    2,宏的名称一般使用全大写的形式。
    3,宏可以定义参数,参数列表需要使用圆括号包裹,且必须紧跟名称,中间不能有空格。
    4,使用#undef NAME取消宏的定义,从而可以重新定义或使用与宏重名的函数或变量。
    5,出现在字符串中的宏名称不会被预编译器展开。

    作者回复: 太好了,谢谢补充

    2019-07-26
    1
  • perfect
    麻烦解释一下宏

    作者回复: the c programming language,这本书还是挺薄的

    2019-06-20
    1
  • 逆流的鱼
    系统调用都会导致用户态切换内核态?而纯计算的不会?

    作者回复: 系统调用都会,纯计算看算什么了,算加法不用进内核

    2019-04-21
    1
  • 陈锴
    有个小问题,64位内核是不是已经取消使用cs 代码寄存器 和 ds数据段寄存器了(或者说默认设为0了),也就是只采用分页而不采用分段了

    作者回复: 分段还是有,只不过是残废的状态,就像你说的一样,到了内存那一节会详细说这个问题

    2019-04-17
    1
  • 青石
    完全不懂C,内容看起来真的吃力,以下是个人理解,不知道对不对,还请指正。

    文中有一部分代码是在glibc源码中的,git clone git://sourceware.org/git/glibc.git

    32位系统调用过程:

    1. 系统调用最多6个参数;
    2. syscall number 对应的是%eax,所有参数存放在寄存器里;
    3. 执行系统调用时执行int $0x80触发软中断陷入内核;
    4. 陷入内核态之前需要保存用户态,这里面有个pushl %eax(和2中寄存器地址相同),将%eax存入pt_regs结构中,保存完用户态现场应该就进入内核态了;
    5. 进入内核态后调用do_syscall_32_irqs_on,取出syscall number及参数,do_syscall_32_irqs_on中的ax、bx、cx、dx、si、di、bp对应system call arguments中的%eax、%ebx、%ecx、%edx、%esi、%edi、%ebp,这些值是06节讲到的32位系统通用寄存器的数据单元。在内核态执行系统调用;
    6. 完成后调用INTERRUPT_RETURN恢复用户态保存的现场。


    64位系统调用:

    1. 同样最多6个参数,但是与32位寄存器地址不同,64位系统的寄存器地址有16个,以r开始,32位系统的寄存器地址有8个,以e开始;
    2. syscall_name转为syscall number存入寄存器%rax;
    3. 调用syscall执行系统调用,期间的过程同样会陷入内核态;
    4. 调用MSR_LSTAR -> entry_SYSCALL_64保存用户态现场,具体参数如何传进去的看不懂;
    5. 调用do_syscall_64,取出系统调用号和参数(和32位没区别),在内核态执行系统调用;
    6. 完成后调用USERGS_SYSRET64返回,sysretq恢复用户态现场。
    2019-04-16
    1
  • 小颜
    此处仅展示32位系统调用:

    glibc大部分使用脚本封装生成代码,用到三种文件:
    1.make-syscall.sh:读取syscalls.list文件的内容,对文件的每一行进行解析。根据每一行的内容生成一个.S汇编文件,汇编文件封装了一个系统调用。文件路径:sysdeps/unix/make-syscall.sh
    2.syscall-template.S:是系统调用封装代码的模板文件。生成的.S汇编文件都调用它。文件路径:sysdeps/unix/syscall-template.S
    3.syscalls.list:是数据文件,定义了全部的系统调用信息。文件路径有多个:sysdeps/unix/syscalls.list,sysdeps/unix/sysv/linux/syscalls.list,sysdeps/unix/sysv/linux/generic/syscalls.list,sysdeps/unix/sysv/linux/i386/syscalls.list

    1.syscall-template.S生成汇编文件
    在syscall-template.S生成汇编文件中在78行有调用T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)方法,
    2.T_PSEUDO是一个宏定义,此文件会引用#include <sysdep.h>
    3.在sysdep.h文件中175行定义了函数PSEUDO:
    /* Linux takes system call arguments in registers:

    syscall number %eax call-clobbered /* 保存系统调用号 */
    arg 1 %ebx call-saved
    arg 2 %ecx call-clobbered
    arg 3 %edx call-clobbered
    arg 4 %esi call-saved
    arg 5 %edi call-saved
    arg 6 %ebp call-saved

    The stack layout upon entering the function is:

    24(%esp) Arg# 6
    20(%esp) Arg# 5
    16(%esp) Arg# 4
    12(%esp) Arg# 3
    8(%esp) Arg# 2
    4(%esp) Arg# 1
    (%esp) Return address */
    #define DO_CALL(syscall_name, args) \
    PUSHARGS_##args \
    DOARGS_##args \
    movl $SYS_ify (syscall_name), %eax; \
    ENTER_KERNEL \
    POPARGS_##args
    在此函数中调用了DO_CALL:将 系统调用 号保存在eax寄存器,其他参数分别保存至其他寄存器
    4。然后调用ENTER_KERNEL,该宏定义在在125行:# define ENTER_KERNEL int $0x80,
    5.int $0x80是一个软中断,将会触发软中断触发函数entry_INT80_32,
    6.entry_INT80_32将用户态的一些信息保存在pt_regs,最终调用do_syscall_32_irqs_on,
    7.do_syscall_32_irqs_on函数将从eax寄存器取出系统调用号,然后根据系统调用号从系统调用表中取出索引,最终取出对应函数,参数从pt_regs中,最终调用系统调用
    8.在函数最后调用INTERRUPT_RETURN iret最终返回数据保存在pt_regs的eax中,并将pt_regs的用户态数据恢复
    2019-04-16
    1
收起评论
76
返回
顶部