专栏名称: 刘超的通俗云计算
刘超,网易云解决方案首席架构师,代码级略懂OpenStack、Hadoop、Docker、Lucene、Mesos等开源软件,曾出版《Lucene应用开发揭秘》,个人博客可搜索popsuper1982。
目录
相关文章推荐
51好读  ›  专栏  ›  刘超的通俗云计算

最后一天 | Linux操作系统课程导游,20张图全面掌握Linux

刘超的通俗云计算  · 公众号  · 架构  · 2020-01-07 17:51

正文

不知不觉,2020年到了,我的极客时间专栏《趣谈Linux操作系统》也完整交付完毕。


回想坚持更新的日子“痛并快乐着”,通过不断地打磨,从50多篇写到72篇,超出我的预期。


很多人觉得操作系统很难,其实还是没有找到合适的方法;我也一直致力于用“趣谈”的方式,配合「图解 + 源码分析」,帮助大家轻松学透操作系统。


我在专栏里,把我15年的技术经验,倾囊相授,由于内容扩增,跟极客时间协商后, 专栏即将涨价至¥129(现在99元),今天拼团仅需¥79,最后1天了


之前我的《趣谈网络协议》专栏,就超过4w名同学学习,现在操作系统也期待你的加入。



很多人说,操作系统的知识体系非常复杂,而且各种知识点相互关联,很难梳理清楚。比如,在学这个专栏的时候,你是不是也常常有下面这些疑惑:


  • 某些知识点在第一次遇到的时候,感觉解析得深度不够,有的读者觉得太浅了。这是因为专栏有一些初学者是需要循序渐进的,不益在初步遇到的时候,就解析的这么深入,后面会有相应的章节进行更加深入的解读。

  • 某些知识点在第一次遇到的时候,感觉解析的广度不够,有的读者觉得这个知识点有5个部分,而我只讲了3个部分。这是因为这个知识点的3个部分比较浅,适合现在学,而另外2个部分过于深入,需要学习了后面的章节后,才可以理解的。

  • 当课程进入和后期的时候,当无论是深度还是广度都会补齐,这个时候,很多读者觉得过于细节,很多前面讲过的东西想不起来了,或者无法串联起来。


所以我专门写了一个导游攻略,你可以在学习这个课程的时候,按照导游路线来,当觉得深度和广度不够的时候,先放放,多一些耐心,当觉得过于细节的时候,可以按照导游路线回头复习一下,再回来看,就会好很多。



我们从第3节开始。


03 | 你可以把Linux内核当成一家软件外包公司的老板


难度系数 1-10,1最简单,10最难

难度系数:2

这节课是操作系统的总论,这一节里没有学术化地讲操作系统的定义,而是从一个使用QQ的场景,带领你反思操作系统应该有哪些组成部分。因此,在这一节,你不必纠结每一个部分的具体功能以及如何做到这些的,只要有个总体印象就可以了。

如果你想更加细节地了解每个模块,可以按照点击下面的链接去到对应的章节,详细阅读,吃透本文的外延和内涵。

  • 系统调用子系统:详情看第09节

  • 进程管理子系统:详情看第10节至第19节

  • 内存管理子系统:详情看第20节至第26节

  • 文件子系统综述:详情看第27节至第30节

  • 设备子系统综述:详情看第31节至第35节

  • 网络子系统综述:详情看第43节至第48节


04 | 快速上手几个Linux命令:每家公司都有自己的黑话


难度系数:2

这一节,对照大部分读者常用的Windows操作系统的操作,介绍了一些简单的Linux操作系统命令,通过这些命令,可以把Linux简单地用起来。

因为Linux是一门需要上手操作的实践课程,所以对于没有上手过的同学,理解后面的内容比较困难,所以建议这样的同学可以根据这一节的介绍简单上手操作一下。

如果你是一个熟练使用Linux的同学,那这一节可以跳过。

如果你有志于成为一个Linux系统管理员,你会觉得这一节的内容太浅了,因为这门课的目标是Linux操作系统原理,而不是Linux系统管理员,不过可以建议你读下面的书籍,或者订阅极客时间的视频课程《Linux实战技能100讲》。

  • 《UNIX/Linux 系统管理技术手册》作者:Evi Nemeth/Garth Snyder/Trent R. Hein/Ben Whaley

  • 《鸟哥的Linux私房菜》作者:鸟哥


如果你想做更加深入的Linux调优,你可以订阅专栏《Linux性能优化实战》。

这一节的主要内容包括4个部分:


1.如何管理用户与密码

2.如何管理文件


在这一部分,我们提到了每个文件都有一个标识表示文件类型,这里只介绍了普通文件类型和文件夹类型,其实还有其他的类型,在第27节中,讲述了所有文件系统类型,但是具体每种类型的作用,有字符设备文件和块设备文件,详见第31节,有套接字文件,详见第43节,有管道文件,详见第36节,有符号链接文件,详见28节。


3.如何安装删除软件

4.如何运行程序


在这一部分,我们讲了几种常见的程序运行方式,到容器(第56节)和数据中心操作系统(第59节)那一节,还能用到这些程序运行方式。


05 | 学会几个系统调用:咱们公司能接哪些类型的项目?


难度系数:3

命令行操作Linux是一种方式,对于基于Linux开发应用的同学来讲,使用系统调用是另外一种操作Linux的方式,也是基本功。

因为后面的课程里面会有很多的代码,通过调用系统调用做实验,因而先了解一下这些系统调用是非常有必要的。

另外,由于操作系统的概念非常的多,而这些操作系统概念全部都是通过系统调用暴露给用户的,所以系统调用是一个非常好的了解系统调用的入口,这一点请你在学习整个课程的过程中都记住它。

所以这一节,先通过介绍这些系统调用,顺便介绍一下操作系统的主要概念,这样后面深入解析某一部分的时候,如果用到了另一部分的概念的时候,不至于完全没有了解。

如果你是一个熟练使用Linux系统调用的同学,那这一节可以跳过。

如果你有志于称为一个Linux平台的应用开发人员,你会觉得这一节的内容太浅了,因为这门课的目标是Linux操作系统原理,而不是Linux平台的应用开发,不过可以建议您读下面的书籍。

《UNIX环境高级编程》

作者:  [美] W·Richard Stevens

《Linux/UNIX系统编程手册》

作者: Michael Kerrisk

这一节的内容包括:

  • 进程管理的系统调用

    • 第10节会使用这些系统调用创建进程

    • 第11节会使用这些系统调用创建线程

    • 第18节会解析进程创建的系统调用在内核里面是如何实现的

    • 第19节会解析线程创建的系统调用在内核里面是如何实现的

  • 内存管理的系统调用

    • 第20节会使用这些系统调用申请内存,主要的两种方式brk和mmap

    • 第22节会解析使用brk系统调用申请内存的时候在内核是如何实现的

    • 第25节会解析使用mmap系统调用申请内存的时候在内核是如何实现的

  • 文件管理的系统调用

    • 第27节会使用这些系统调用操作文件,主要是打开,读,写

    • 第29节会解析打开文件的系统调用在内核是如何实现的

    • 第30节会解析读写文件的系统调用在内核是如何实现的

  • 信号处理的系统调用

    • 第37节和第38节会介绍信号处理系统调用在内核是如何实现的

  • 进程间通信

    • 第36节会讲如何使用命令或者系统调用来使用这些进程间通信机制

    • 第39节会介绍管道系统调用在内核是如何实现的

    • 第40节到第42节会介绍IPC的系统调用在内核是如何实现的

  • 网络通信

    • 第44节会解析网络系统调用的内核实现:socket, bind, listen, accept, connect

    • 第45节和第46节会解析网络系统调用的内核实现:write

    • 第47节和第48节会解析网络系统调用的内核实现:read

  • 查看源代码中的系统调用

    • 第09节会解析系统调用在内核中的实现

    • 第60节会做一个操作系统的实验,添加一个自己的系统调用,所以学到这里还是要好好看一下

  • 中介与Glibc

    • 其实大部分程序员都是通过glibc来使用系统调用,所以系统学习一下glibc还是很有必要的

    • 第09节解析系统调用的时候,会讲如何从glibc调用到内核系统调用的

    • 第19节讲线程创建的时候,会讲glibc和系统调用合作完成线程机制的

    • 第20节讲内存管理的时候,会讲glibc和系统调用合作完成内存分配的

    • 第38节讲信号的时候,会讲glibc和系统调用合作完成信号处理函数设置的


06 | x86架构:有了开放的架构,才能打造开放的营商环境


难度系数:4

操作系统说到本质就是帮助用户来操作底层的硬件的,因而这一节需要先从底层的x86架构讲起,因为后面的很多机制都是和硬件机制相关的。

底层的机制有一门专门的课程,所以不可能一节课就能全部讲述清楚,如果你想深入了解相关知识,给你推荐下面的书籍。

《IBM PC汇编语言程序设计》

作者: 沈美明 / 等

《深入理解计算机系统》

作者:(美)Randal E.Bryant / David O'Hallaron

也可以订阅极客时间专栏《深入浅出计算机组成原理》

这一节的内容包括:

  • 计算机的工作模式

    • 这里简单讲述了CPU和内存如何合作完成程序的执行的

    • 程序和进程的概念在第10节会更加深入讲解

    • 代码段和数据段的概念在第20节和22节会更加深入的讲解

    • 寄存器的概念将在后面多次用到,例如第8节,第9节,第14节,第16节

  • x86平台的开放性

    • Linux内核对于不同的硬件平台有不同的实现方式,这门课主要是X86平台,也即分析X86或者X86_64文件夹下面的代码

  • 8086的原理

    • 这里讲述了8086的工作原理,因为他最简单,好理解,又非常有代表性,你不需要读像砖头一样厚的Intel的CPU的说明书,就可以简单的认为他们是类似的工作模式,当然后来的Intel CPU要复杂很多,当某个模块用到复杂的机制的时候,我们会调出这部分单独讲一下。

    • 这里讲了几个有代表性的寄存器,指令指针寄存器,代码段寄存器,数据段寄存器,栈寄存器。这几个寄存器的功能要熟记,但是后来的Intel CPU中,这几个寄存器的名称已经变了,但是并不影响他们的工作机制。

    • 这里介绍了函数栈的工作模式,如果学习过C语言的,对这个会比较熟悉,之所以在这里介绍,一方面照顾没有C语言开发经验的同学,另外一方面后面第8节,第9节,第14节,第16节,第22节都会用到。

    • 这里还指出内存空间1M,这也是为什么32位以上处理器的实模式内存空间是1M,第07节系统初始化的时候最初的空间也是1M

  • 32位处理器

    • 从8086到32位处理器是一个本质的跨越,从而引出了实模式和保护模式的概念,这个概念在第7节系统初始化的时候用到。

    • 从32位到64位主要是内存空间和内存布局的变化,对于CPU的工作机制的变化不大,这里也就没有更多的介绍64位

    • 这里介绍了段选择子和段描述符表的概念,这在第7节系统初始化,第21节内存映射的时候会用到


07 | 从BIOS到bootloader:创业伊始,有活儿老板自己上


难度系数:4

Linux系统的启动过程是面试经常考的,有时候运维服务器的时候,我们想干预这个过程,所以要对这个过程又详细的了解。

《UNIX/Linux系统管理技术手册》(作者: Evi Nemeth / Garth Snyder / Trent R. Hein / Ben Whaley)里面有专门的一节引导和关机,详细讲述了启动和关机的流程。

对于底层技术解析比较详细的,推荐下面这本书《x86汇编语言 从实模式到保护模式》(作者: 李忠 / 王晓波 / 余洁)。这本书对于启动过程中,从实模式到保护模式具体做了哪些事情做了详细的阐述,而且这本书还讲了汇编语言,虽然我们这门课不要求精通汇编语言,但是基本的指令和语法还是需要了解的,这本书也是非常好的参考。而且读这本书也有利于进一步了解上一节的x86架构。


  • BIOS时期

    • 只要安装过电脑,基本就知道BIOS,但是对于具体做了哪些事情,很多同学往往不清楚

    • BIOS一开始的内存映射是1M,这是由第6节中实模式下的寻址范围决定的

    • 中断向量表和中断服务程序出现的有点突兀,因为目前还没有深入介绍中断,但是第3节做总体介绍的时候,做过比喻,所以这里也就着比喻大概理解一下,就是为了响应基本的事件的,比如鼠标和键盘。在第33节讲字符设备的时候,会详细讲中断的机制,这里先不必纠结。

  • bootloader时期

    • 这里需要重点掌握的是Grub2,对于启动过程的干预,大部分都是在这里面配置的,也是Linux系统管理员必备技能,在第60节和第61节搭建操作系统实验环境的时候,还会涉及到修改grub

  • 从实模式切换到保护模式

    • 这一步其实要做很多的工作,这里只说了其中的一部分,更加详细的可以看推荐书籍《x86汇编语言 从实模式到保护模式》(作者: 李忠 / 王晓波 / 余洁)。

    • 这里面涉及到的分段和分页,在第21节有详细的描述

    • 这里面涉及到打开第21个地址线,和第6节我们讲过的实模式下一共20位地址线有关

    • 另外推荐一本书《Orange'S:一个操作系统的实现》(作者:  于渊)因为要实现一共完整的操作系统,因而需要实现完备的从实模式到保护模式的代码,因而如果你想理解细节,这本书非常好。但是这本书以分段为主,而第21节我们会讲Linux以分页为主。


08 | 内核初始化:生意做大了就得成立公司


难度系数:6


内核的初始化是一个大工程,我们这一节没有解析的这么细,只讲了大概的流程,是因为每一部分的初始化,例如进程,中断,系统调用,内存,调度,文件系统,网络,设备等,都需要理解这部分的原理后,才能解析,所以这些都放在了相应的章节。

这一节反而是放了更多的精力讲了其他的初始化,也即创建用户态和内核态的祖先进程。

学习这一节的时候,内核初始化的入口函数start_kernel一定要记牢,因为后面解析每一个部分的时候,都需要重新回来看这部分的代码,要从这里面找头绪。

  • 进程管理初始化

    • 第12节到第14节会用到这一节进程管理初始化好的数据结构

  • 中断与系统调用初始化

    • 第9节会更加深入的讲系统调用的初始化

  • 内存管理初始化

    • 第22节到第26节会更加深入的介绍内存管理和内存映射的数据结构的初始化

  • 项目调度初始化

    • 第15节和第16节会更加深入的讲调度数据结构的初始化

  • 其他初始化rest_init(一号进程即用户态的祖先进程,二号进程即内核态的祖先进程)

    • 这里的0号进程,1号进程,2号进程经常考

    • 本来说着进程,前面没怎么提线程的概念,但是这里出现了thread的字眼,会让人困惑,于是就简单的讲了一下线程的概念,并且讲了进程和线程都是任务的说法,但是没有深入分析,第10节和第11节讲线程的时候会深入解析两者的区别,第12节讲进程数据结构的时候,会讲哪些成员变量标识着当前的任务是进程还是线程,第15节到第17节讲调度的时候,更加明确了进程和线程都是任务一起参与调度的说法。

    • 很多书籍都没有解析第一个进程是如何变成用户态的,这里做了解析

    • 这里讲了权限的四个ring,这在第9节系统调用,第21节内存管理,第49节虚拟机都会用的这个概念

    • 这里还讲了系统调用的过程,都是为了解析第一个进程的产生过程,第9节会更加深入的解析系统调用的过程。

    • 这里还讲了状态切换的时候,需要保存寄存器,第9节会更加深入解析保存哪些寄存器,第14节会更加深入的解析寄存器保存在内核的什么数据结构里面,第16节会深入的解析进程切换的时候,如何保存这些寄存器

    • 这里还讲了exec系统调用,用来运行用户态的程序,第10节会详细讲这个系统调用。

    • 这里还涉及到了ELF文件格式,第10节会详细讲这个文件格式

    • 这里还涉及了pt_regs数据结构,第14节会详细讲这个数据结构

    • 这里还涉及__USER_CS等段选择子,这在第6节讲过,在第21节讲内存映射的时候,会深入解析这些段选择子。


由此可见,这一节是综合的一节,而且是需要经常返回来看的一节,如果第一次不能理解这一节,可大概意会他的流程,等学完整门课再来看一遍。


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


难度系数:7




这一节主要解析我们熟悉的函数是如何从用户态经过层层调用最终到达内核态的。之所以解析其他模块之前先解析系统调用,是因为其他模块的内核机制都要以系统调用为入口进行分析。

这一节讲的非常细节,讲到了实现一个系统调用都需要修改哪些文件,之所以这么详细是因为在第60节的操作系统实验中,我们将自己实现一个系统调用。但是如果第一次读到这里,不必纠结每一个细节,只要了解以下的知识点就可以

  • gLibc对系统调用的封装

    • 调用一个函数的时候,要注意这是一个glibc的函数,还是一个系统调用,如第19节线程的创建,就有很多的工作是在glibc里面完成的。

  • 32位系统调用过程

    • 保存了哪些寄存器,寄存器的作用在第6节已有描述,如果对于汇编语言不熟,上面也有参考书籍

    • 通过软中断触发系统调用,如何初始化系统调用软中断要回头看第7节

    • 寄存器保存在pt_regs结构里面,这个结构第8节遇到过一次,第14节会详细讲这个数据结构

    • 返回用户态恢复现场的指令为iret

  • 64位系统调用过程

    • 保存了哪些寄存器,这些寄存器由于是64位,名称有些不一样,可以算对第6节讲过的一个扩展

    • 通过特殊的寄存器触发系统调用,我们叫特殊模块寄存器(Model Specific Registers 简称MSR),这也是64位CPU的一个新的机制,因而初始化这种机制要回头看第7节

    • 寄存器保存在pt_regs结构里面

    • 返回用户态的指令变成了sysretq,这里注意,返回用户态的过程比较复杂,是汇编语言写的代码,第17节会讲从系统调用返回是进程调度的时机。

  • 系统调用表

    • 这里需要记住的是,在内核里面找系统调用,要找SYSCALL_DEFINE这个宏


10 | 进程:公司接这么多项目,如何管?


难度系数:4

从这一节,操作系统课程正式进入各个模块的解析环节,前面的章节更多的是横向的预备知识,从这一节开始,进入纵向的深度挖掘,深度挖掘的过程中会用到其他模块的预备知识,同时会将这个模块的纵向知识解析清楚,也是一个综合的过程。

对于每一个纵向的知识,这个课程都会包含以下几个部分:

  • 命令行部分:这部分不是重点,只会介绍几个简单的命令操作这部分纵向知识

  • 写代码部分:教给你如何使用glibc函数或者系统调用

  • 内核的数据结构和算法部分,这部分是重点

  • 流程解析部分:分析如何从glibc函数或者系统调用到内核的实现


对于内核的纵向知识的解读,总体推荐几本书:

《深入理解Linux内核》(作者:(美)博韦,西斯特)

《深入Linux内核架构》(作者: Wolfgang Mauerer)

《Linux内核设计与实现》(作者: Robert Love)

对于进程,命令行部分第4节已经讲过,这一节讲写代码部分,以及exec系统调用的流程解析部分。

  • 写代码:用系统调用创建进程

    • 写代码要先安装编译环境,这就要用到第4节里面学到的命令行

    • 这门课写的代码都是C语言,如果你完全不懂C语言,可以看一下《C程序设计语言》(作者: (美)Brian W. Kernighan / (美)Dennis M. Ritchi),不太厚的一本书

    • 代码里面用到了fork,虽然第18节才解析他的实现,但是第5节已经介绍了这个系统调用

    • 代码里面用到了exec系统调用,第5节已经介绍了这个系统调用,第8节讲创建第一个用户态进程的时候,已经解析了这个系统调用的内核实现的部分

    • 代码里面创建的进程运行了ls命令,这个命令在第4节也介绍过

    • 所以这一部分的代码是精心设计的,里面所有的知识点前面都出现过,除了你没有编译过代码,接下来就来讲编译的过程。

  • 进行编译:程序的二进制格式

    • 这里讲了程序编译的过程,这个过程一定要熟悉,命令要搞懂,因为后面的每个章节都有代码需要编译,都是按照这一节说的顺序进行编译的。

    • 这里还讲了ELF的格式,编译好的二进制文件就是这个格式,第8节里面第一个进程加载的也是这种格式,内核本身也是这种格式。

    • 这里推荐一本书《程序员的自我修养:链接、装载与库》(作者: 俞甲子 / 石凡 / 潘爱民)

  • 运行程序为进程

    • 这里讲了exec系统调用的一组函数,并且根据第9节讲的系统调用的过程,解析了从用户态的一组函数如何调用到内核的实现(从exec函数组到do_execve),这一部分和第8节讲创建第一个用户态进程的时候,讲的系统调用的内核实现的部分(从do_execve到load_elf_binary),这两部分拼接起来,就是exec的整个调用过程

  • 进程树

    • 第8节讲内核初始化的时候,讲了用户态和内核态的祖先进程,在这里运行ps命令,就可以看到这两棵进程树,印证了第8节的说法。

    • 第8节讲内核初始化的时候,仅仅讲到创建的第一个用户态进程是init,运行init之后就没有讲,这里补充上,init进程在centos里面就是systemd,然后进一步解析初始化的过程,直到bash交互命令行出现,这个时候整个系统初始化才算完毕。也即是由第7节,第8节,以及第10节的这里拼接起来整个Linux启动过程。


11 | 线程:如何让复杂的项目并行执行?


难度系数:4

在第8节内核初始化的时候,因为函数名出现了thread这个字眼,所以简单介绍了一下线程。这一节才真正的深入讲线程。

这一节重点讲线程的写代码部分。

这里推荐两本书《Linux多线程服务端编程》(作者: 陈硕),《POSIX多线程程序设计》(作者: [美] David R.Buten)

  • 为什么要有线程?

  • 如何创建线程?

    • 这里我们构建了一个场景,将主要的线程相关的API都用上,由于是多线程,每个人的输出结果不一定一样,我自己也是运行多次,才得到一个看起来随机一点的输出,你也可以选择在downloadfile这个函数里面sleep一个随机的时间。

  • 线程的数据

  • 数据的保护

    • 这里我们构建了两个场景,将互斥锁和条件变量的API都用上,同理,你执行的结果和我不一样,我也是运行了多次才有了一个典型的输出结果,你可以试着自己运行一下,然后对着我的结果理解。


12 | 进程数据结构(上):项目多了就需要项目管理系统


难度系数:4

第12,13,14节解析进程和线程的内核的数据结构部分。为了知识体系完整性,这里把所有成员变量都解析了,但是有的在后面章节会更加细讲。

这一节首先明确了进程和线程在内核里面都是任务,因而在解析数据结构的时候,大部分场景下不区分来讲。

  • 任务ID

    • 进程和线程都是任务,一个重要的区分就在pid, tgid, group_leader,这是一个重要的知识点

  • 信号处理

    • 信号处理的成员变量没有细讲,会在第37节,38节细讲

  • 任务状态

    • 进程的状态是经常面试考的

    • TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE这两个状态要注意,当后面遇到这两个状态的时候要想的起来,在第16节调度,第38节信号,第42节IPC,第44节Socket

  • 进程调度

    • 这个放到第15节,16节,17节细讲


13 | 进程数据结构(中):项目多了就需要项目管理系统


难度系数:4


接着解析进程和线程的内核的数据结构部分。

  • 运行统计信息

    • 这里的上下文切换详解见第16节

  • 进程亲缘关系

    • 在第10节,我们通过命令行看到了进程的亲缘关系,在内核里面就是通过这些数据结构维护的

  • 进程权限

    • 第4节讲过文件权限,进程能否操作一个文件,权限在这里控制

  • 内存管理

    • 这个放在第20节到第26节细讲

  • 文件与文件系统

    • 这个放在第27节到第20节细讲


14 | 进程数据结构(下):项目多了就需要项目管理系统


难度系数:8


这一节是最难理解的,但是非常重要,搞不懂这一节,涉及到切换的流程都比较难搞懂。

第8节,第9节和这一节加起来会构建出用户态和内核态之间的切换的全貌。

第16节讲进程之间的切换的时候,第33节讲中断的处理过程,第37节和第38节讲信号处理的流程,第50节和第51节讲虚拟机的CPU切换,都要用到这部分知识。

  • 用户态函数栈

    • 第8节简单介绍了函数栈,但是没有介绍栈的结构,这里详细解读了,并且用到了第6节讲的寄存器,64位操作系统下寄存器名字不一样,但是意思差不多

  • 内核态函数栈

    • 第9节讲了系统调用的时候,会讲用户态的寄存器保存在内核栈的pt_regs结构里面,但是没有讲内核栈是如何维护的,pt_regs结构放在那里,这里进行了解读

  • 通过task_struct找内核栈

  • 通过内核栈找task_struct

    • 第12节和第13节讲的进程数据结构和内核栈能够互相找到。

    • 至此,用户态和内核态切换的全貌构建完毕,用户态函数的调用的过程见本节第一部分,一旦涉及用户态调用系统调用,可见第9节的过程,系统调用将寄存器压入的栈见本节第二部分,到了内核里面各种函数的调用使用内核栈,系统调用返回的时候,先是内核栈层层弹出,到了系统调用在内核的入口,弹出寄存器,恢复寄存器,就进入了用户态,然后用户态的用户栈层层弹出,直到程序执行完毕。


15 | 调度(上):如何制定项目管理流程?


难度系数:5


调度总共分了三节,这一节介绍为了调度所需要准备的数据结构,另外两节讲使用这些数据结构进行调度的流程。

  • 调度策略与调度类

    • 调度分实时调度和普通调度,由于篇幅原因,实时调度仅仅简单过了一下,这一节和后面两节重点分析普通调度。普通调度使用的是完全公平调度算法

  • 完全公平调度算法

    • 这里解析算法,主要是一个公式,以及update_curr这个函数,是一个核心函数,需要掌握,第16节和第17节会用到

  • 调度队列与调度实体

    • 这里和第12节讲的进程内核数据结构的调度部分关联上了,要结合起来看

  • 调度类是如何工作的?

    • 这里的函数pick_next_task是一个核心函数,需要掌握,第16节和第17节会用到


16 | 调度(中):主动调度是如何发生的?


难度系数:8

调度分主动和被动,就是我让给别人,还是别人抢我的。这一节讲主动调度。

主动调度就是当前在CPU上运行的进程主动去调用schedule()函数,这本没有什么好解析的,但是复杂在调用之后,都会做哪些事情进行进程切换。

这里注意,其实下一节讲的被动调度只是给当前在CPU上运行的进程打一个标签,说我要抢占你了,最终还是需要在CPU上运行的进程主动去调用schedule()函数的。所以主动调度和被动调度殊途同归,这一节讲的大部分篇幅,也即解析schedule()函数之后的部分,其实是主动调度和被动调度都会走的逻辑。

schedule()函数的逻辑因为要涉及到两个进程切换,比较的绕,可能需要看多遍。

  • 主动调度

    • 这里举了两个例子,看起来有点啰嗦,但是这两个例子是刻意找的,因为进程切换比较绕,只有结合这两个例子,用这两个例子里面实实在在的代码进行解读,才能让比较绕的逻辑清晰起来。

    • 这里解析了schedule的流程,用到了第15节的调度队列和调度实体,然后会调用pick_next_task函数和update_curr函数,这个在第15节也解析过了

  • 进程上下文切换

    • 进程切换说简单也简单,进程无非就是躺在内存里面的两段代码以及这些代码产生的数据,你作为内核是可以直接操作X86硬件的,而硬件是没有思想的,按第6节的讲述,硬件有个指令指针寄存器,你在这个寄存器里面放A进程的代码,A进程就接着这行一行一行的执行下去,同理放B进程也是一样的,你想让谁执行谁就执行。

    • 进程切换说难也难,就是如何保证执行的是对的,断掉的执行流程能够下次接上来。要保证这点,第一是内存,A进程要在他的内存里面操作,B进程也一样,两个互不干扰,切换进程空间这里会简单讲一下,详解见第22节和第25节内存管理与内存映射。第二就是寄存器,其实硬件有切换寄存器的方式TSS和TR寄存器,这个在第6节没有讲这个寄存器,这里算补充上,但是硬件的方式在Linux里面没有全用,只是在系统初始化的时候使用了一下,这就要回到第8节内核初始化的入口代码里面找线索了。

    • 在寄存器里面最重要的就是栈顶指针寄存器,就像第14节解析的那样,切换走再回来的时候,只有栈顶指针寄存器知道当时执行到了那里,栈又分用户态栈和内核态栈,而用户态栈在进程的内存空间里面,随着内存的切换已经完成切换,用户态栈的栈顶指针在内核栈的pt_regs里面,所以重点就是内核栈的栈顶指针。

    • 所以将上面的知识点都串起来,我们才能看懂,为什么进程上下文切换函数context_switch里面重点就做了两件事情,一个是switch_mm_irqs_off,一个是switch_to里面切换栈顶指针寄存器。其实这里面隐含着做的事情就是指令指针的切换

  • 指令指针的保存与恢复

    • 我这里定义了一个“进程调度第一定律”,是我一家之言,为了强调他的重要性。这个定律是进程的调度都最终会调用到__schedule函数

    • 要注意,进程的切换一定是在内核里面的,一定是发生在__schedule函数里面的,内核的代码整个系统只运行一份,无论是从哪个用户进程调用到内核里面来,执行的内核代码也只有一份,所以从CPU的角度来讲,如果他正在执行__schedule函数里面的逻辑,他是不需要区分到底是A进程的,还是B进程的,只有在从__schedule函数返回的那一刻,根据第14节函数栈的原理,是栈里面的内容告诉他,应该返回A进程还是B进程。刚才内核栈都切换过了,因而一返回,就实现了指令指针从指向A进程的代码,变为指向B进程的代码。

    • 当时没有想到很好的比喻,现在想到一个,假设CPU是一个人,他在生活中有两个角色,一个角色是老板,一个角色是歌手,他到底是哪种角色,取决于他穿什么样的衣服,当老板就要穿西装,当歌手就要穿演出服,其实穿的衣服就是上下文。当角色需要切换的时候,这个人需要去更衣室里面换衣服。当CPU执行在__schedule函数里面的时候,就像这个人在更衣室里面一丝不挂,其实无所谓角色,也无所谓在哪个进程上下文里面。当这个人要走出更衣室,变成什么角色,取决于他的助理给他门口挂上什么衣服,挂上西装,他出来就是老板,挂上演出服,他出来就是歌手。所以只要栈顶指针寄存器切换完了,就相当于助理把门口挂的衣服换了,这个时候CPU只要从__schedule函数返回,就相当于一丝不挂的人穿上了助理挂的新衣服,自然而然就变成了新的角色,也即进入了新的进程上下文。


17 | 调度(下):抢占式调度是如何发生的?


难度系数:6

即便是抢占式调度,根据进程调度第一定律,还是需要当前运行的进程去主动调用__schedule函数,才能实现上一节的逻辑。

所以抢占式的调度分三个部分,第一,什么时候标记某个进程应该被抢占,第二,什么时候被抢占的进程会主动调用__schedule函数,第三,通过__schedule函数实现真正的进程切换。

其中第三个问题第16节已经讲过了,对于抢占式的调度,这一节主要解决前两个问题。

  • 抢占式调度

    • 这一部分解决第一个问题

    • 抢占的一个场景是时钟中断,到这里其实还没有讲过中断,中断的原理要在第33节讲,这里只是假设时钟中断了,就会调用scheduler_tick,这里面用到了第15节讲的调度队列,调度实体,调度类,然后会调用pick_next_task函数和update_curr函数,这个在第15节也解析过了

    • 抢占的另一个场景是进程被唤醒

  • 抢占的时机

    • 这一部分解决第二个问题

    • 用户态的抢占时机,一是从系统调用返回,在第9节讲系统调用的时候,当时没太关注返回的过程,需要返回去看代码,会看到返回的时候,发现被标记了,就会调用__schedule函数。二是从中断返回,中断要到第33节讲,但是这里仅仅把返回的代码解析了一下,同样是发现被标记了,就会调用__schedule函数。

    • 内核态的抢占时机,一是调用preempt_enable,二是从中断返回内核,这里解析了中断返回的另一部分代码。所以将来整个中断的流程,应该包括第33节的中断处理流程,上面的返回用户态的流程,这里的返回内核态的流程,三个拼接起来。


18 | 进程的创建:如何发起一个新项目?


难度系数:5

这一节讲进程的流程解析部分。

对于流程解析,推荐几本书

《Linux内核完全注释》(作者:赵炯)

《Linux内核源代码情景分析》(作者: 毛德操/胡希明)

创建进程的系统调用是fork,这个在第5节讲过,但是内核实现的入口在哪里,需要按照第9节对于系统调用的介绍,在代码里面找SYSCALL_DEFINE0(fork),然后沿着里面的代码逻辑跟下去。

  • fork的第一件大事:复制结构

    • 参考第12节,第13节的进程数据结构进行复制,前面都介绍过的成员变量比较容易理解,对于文件系统相关的copy_files和copy_fs,信号相关的copy_sighand和copy_signal,内存相关的copy_mm,在这里可不用深入理解,可等第27节到第20节细讲完文件系统,第20节到第26节细讲完内存管理,第37节,38节细讲完信号后,再回来看

  • fork的第二件大事:唤醒新进程

    • 这里用到了第15节讲的调度队列,调度实体,调度类,然后会调用pick_next_task函数和update_curr函数


19 | 线程的创建:如何执行一个新子项目?


难度系数:5

这一节讲线程的流程解析部分。

  • 用户态创建线程

    • 第11节讲了glibc里面的线程创建函数,这里就要从glibc开始解析。可以对照那一节讲的线程创建的代码,理解glibc里面的实现

    • 线程的创建涉及到内存的分配,要用到mmap函数,在第5节讲过如果要在堆里面malloc一块内存,比较大的话,用mmap,具体mmap的实现,要到第25节详细解析

  • 内核态创建任务

    • 创建线程调用的系统调用是clone,通过查找SYSCALL_DEFINE5(clone),找到内核的实现入口。

    • 线程创建和进程创建的区别就是参数里面有很多的flag,主要流程的区别在于文件系统相关的copy_files和copy_fs,信号相关的copy_sighand和copy_signal,内存相关的copy_mm,在这里可不用深入理解,只要知道线程之间共享这些即可。可等第27节到第20节细讲完文件系统,第20节到第26节细讲完内存管理,第37节,38节细讲完信号后,再回来看

    • 对于亲缘关系的影响,第12节已经讲过区分就在pid, tgid, group_leader,这里从代码层面确认了这一点

  • 用户态执行线程

    • 线程的执行还是要回到用户态来,因而clone系统调用之前,需要准备好用户态的栈,从而使得系统调用返回的时候,回到用户态的start_thread函数执行。这里的原理和第8节第一个用户态进程的创建类似,以及结合第9节系统调用的过程,第14节函数栈的结构,就更加容易理解了


20 | 内存管理(上):为客户保密,规划进程内存空间布局


难度系数:4

从这一节,开始讲内存管理的部分,先推荐书。

《深入理解Linux虚拟内存管理》(作者: [爱尔兰] Mel Gorman)

  • 独享内存空间的原理

    • 虽然前面我们一直说进程应该独享空间保持隔离,但是没有分析底层原因,这里分析了底层原因,并且引入了内存管理的三大问题:虚拟内存,物理内存,内存映射。从第20节到第26节就是按照这个体系来的。

  • 规划虚拟地址空间

    • 先讲虚拟内存的原理,因为写代码能够操作的就是虚拟内存,所以先是内存管理的写代码部分,内存管理的API比较少,所以这里写了一个程序,并以此为基础分析虚拟内存的组成部分。

    • 虚拟地址空间分用户态和内核态,这里还要注意区分进程视角和内核视角。所以就有了进程视角看用户态地址空间,进程视角看内核态地址空间,内核视角看用户态地址空间,内核视角看内核态地址空间。把这四个弄清晰了,就好理解了。这里举了一个会议室的例子,如果还不能理解,可以结合下一节第21节内存映射的原理一起理解。







请到「今天看啥」查看全文