【操作系统】CSAPP学习笔记

CSAPP学习笔记

前言

在阅读本书前,最好先了解一下书本的结构,然后根据结构,网上查查网评。最好能找到一些最佳阅读技巧。可以给自己定一个大一点的目标,比如,期望读完这本书,可以自己设计一个操作系统。而不是仅仅学会给代码调优(这个目的会让你走火入魔)。不要企图或者期待这本书给你带来很多开箱即用的最佳实践。你不是在刷算法题。

阅读时的模式:明白技术点所要解决的问题,以及如何解决的。(问题驱动阅读)

了解书的目录:

## 第一部分 程序结构和执行
  - 数据类型
  - 程序指令,系统是如何执行程序指令操作数据的
  - CPU结构,CPU是怎么工作的?
  - 谈谈花里花俏的程序性能优化(其实还不如学算法优化实在)
  - 存储,了解数据可以存在操作系统的什么地方,CPU?内存?硬盘?
前言:这一章核心,计算机怎么处理数据,计算机怎么处理指令,计算机怎么存储数据。简而言之,就是操作系统自身是怎么工作。

## 第二部分 在系统上运行程序
  - 链接,怎么从文件种将杂七杂八的代码组装起来(制作图灵机的纸带?)
  - 异常处理,如果用户输入垃圾代码,操作系统怎么擦屁股,收拾烂摊子?
  - 操作系统,怎么提供自己的存储空间给用户呢?(聊聊地主怎么分地皮)

前言:程序是操作系统提供用户执行作业的接口。了解操作系统是怎么提供这个接口给用户。简而言之,就是了解操作系统怎么对提供服务,对就是你想的那种服务。侧重于异常和存储分配(指令这些就不管啦,CPU内部封装的,对用户不可见)。

## 第三部分 程序间的交互和通信
 - 同一个操作系统,进程怎么聊
 - 不同操作系统,进程怎么聊
 - 操作系统是一个单例,所以怎么处理多个进程资源复用。

前言:系统是一个房子,程序就是住户,怎么通信和共同生活,也是一个问题。

本书分为3大部分:讲讲操作系统怎么工作,讲讲用户怎么让操作系统工作,讲讲怎么让操作系统有条不紊的完成用户的多个任务。

所以整本书的最终目的就是,搞清楚,怎么让CPU,内存,硬盘,帮用户做很多工作。

简化后的目录:

一,操作系统
	- 数据
	- 指令
    - 存储
    - 
二,程序接口
	- 编译
	- 异常
	- 内存

三,进程通信
	- 系统IO
	- 网络编程
	- 并发


一,操作系统

如果想要做操作系统,这一部分的知识就是入门材料了。

1.数据

简单快读

  • 了解位运算法则,与或非异或
  • Integer最高位存放负数,其他位正常表示。(这个简单)
  • double浮点提供了高11位用于记录指数阶级。52位记录尾数,以及高于尾数后丢精度的特性。(根据IEEE规则,float也一样)
  • 有符号比无符号表示法,在正数值空间少一半。
  • 补码技术可以使得CPU只用加法器就能进行减法运算。
  • 计算机怎么计算乘除法呢?
  • 左移带不带符号呢?假如带符号位一起移动,符号最高位会消失,java中不给带符号位移动,所以<<<是无效操作。
  • 正数带不带符号移动,都一个样子。源码等于补码。
  • 负数的话,不带符号好说。不带符号,就是和整数一样。
  • 带了符号就不好说了。因为他是以补码的形式移动的。而且-1怎么右移都是-1.因为负数的补码和原码不一样。
  • 时刻注意溢出,计算机运算不是数学运算,随时准备溢出。
  • 浮点数的出现是为了用小空间表示大数而设计的,这个做法就是只记录大数的前几位,以及他的数量级,却不精确的记录所有位,因为这个成本太高了。比如记录太阳到地球的距离,如果要记录精确记录每一位,不要说计算机,用全地球的木材做成草稿纸都不够写。
  • 浮点数怎么掉精度呢?
  • 补码技术怎么代替减法器呢?
  • 什么是规范值,什么是非规范值?什么是特殊值?他们的意义是什么?
  • 浮点数的加法乘法。
  • 浮点的四舍五入也是和常量不一样,比如2.885会得到2.88.而2.895却是2.90
  • 认识什么是大端数,什么是小端数。我们平常看到的数是都是大端数,计算机CPU比较特殊,采用小端数。互联网属于多属于应用层的产品,所以也理应用了大端数(小端数太奇葩了)。

2.指令

了解概念

2.1 汇编语言操作对象-寄存器,指令栈

  • 冯诺依曼体把数据和指令混合存储再内存中,CPU怎么区分的?
  • CPU结构,除了寄存器,PC,加法器,还有什么?CPU的本质功能是什么?
  • CPU怎么定位数据?
  • CPU的奇淫巧计----栈与队列?
  • 内存怎么存数据和指令?
  • 计算机高度抽象后是什么,CPU,内存,程序又在其中充当什么角色。
  • 机器码才是计算机的指令和数据的集合。汇编码和C语言是接口用户,这就意味着,需要在接口进行预处理,因此,我们定义,【 语法 + 编译工具】 = 编程语言。
  • 从CPU接口的角度上来说,机器码和C语言都是一样的,面向用户的程序接口。只是友好度不一样,但都是给用户操作CPU用的语言。
  • 我们把 【操作符 + 数据 = 指令】 并把指令存放在内存,这就是所谓的冯诺依曼体。CPU执行的指令本身包含了数据。指令在内存看来就是数据了,但是对于CPU来说又是一个操作命令。因为冯诺依曼体在物理层不区分数据和操作。
  • PC指针,执行了CPU将要执行的命令,所以我们可以理解为CPU本质就是一个不停读取PC的while循环。
    PC象征着无尽的命令。CPU象征无尽的循环,其次寄存器可以看成PC的附属品,用于寄放数据。

2.2 指令的种类

  • 而PC的命令有1个典型的:修改命令,一切加减乘除和跳转命令都是修改操作。CPU为了提高修改的性能,
    发明了几个专用器件,比如加法器,条件跳转器等等。
  • 我们没有办法用一条指令完成内存间的数据交换。所有指令一定要寄存器参与,因为CPU只能接触寄存器做数据修改。
  • 大于,等于,小于,(溢出).是CPU专门为条件if语句设计的器件,可以理解为【比较器】,就好像加法器一样。
  • 所有的switch,while,if,等于条件比较有关的都会由比较器来实现。

3.存储

了解结构

3.1 物理知识

  • 硬盘虽然是旧社会的产品,但确实一种很耐用很实在的东西,尤其是微服务和大数据横行,有必要认真学习。
  • 静态RAM和动态RAM的区别,动态RAM需要经常刷新且便宜。但他们都是内存
  • ROM用来写驱动和BIOS(系统引导程序)。
  • 硬盘的核心部件,磁头,主轴,磁盘。
  • 机械硬盘有许多片磁盘(platter)组成,每一片磁盘有两面;每一面由一圈圈的磁道(track)组成,而每个磁道会被分隔成不同的扇区(sector)。这里概念层层递进,可以结合下图仔细辨析清楚。
  • 最小存储单位是某一个磁道上的某一个扇区。
  • 存储坐标:磁盘片,片的面,磁道,扇区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ogaDAsod-1588815445133)(:/076d018da076413398072526001b4a70)]

  • 外磁道比内磁道能存更多数据。(像废话)
  • 容量 Capacity = 每个扇区的字节数(bytes/sector) x 磁道上的平均扇区数(avg sectors/track) x 磁盘一面的磁道数(tracks/surface) x 磁盘的面数(surfaces/platter) x 硬盘包含的磁盘数(platters/disk)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gtBlVhJj-1588815445143)(:/ee8c55044e7542358d12fe96c3e8709d)]

  • 假设我们现在已经从蓝色区域读取完了数据,接下来需要从红色区域读,首先需要寻址,把读取的指针放到红色区域所在的磁道,然后等待磁盘旋转,旋转到红色区域之后,才可以开始真正的数据传输过程。

  • 总的访问时间 Taccess = 寻址时间 Tavg seek + 旋转时间 Tavg rotation + 传输时间 Tavg transfer

    • 寻址时间 Tavg seek 因为物理规律的限制,一般是 3-9 ms
    • 旋转延迟 Tavg rotation 取决于硬盘具体的转速,一般来说是 7200 RPM
    • 传输时间 Tavg tranfer 就是需要读取的扇区数目
  • 磁盘访问大部分时间花在寻址上,读取扇区是很快的过程。

  • 寻址包括了,选盘面,选磁道,和转盘。转盘一般是4ms,选盘是4ms,选磁道也是4ms.

  • 硬盘比 SRAM 慢 40,000 倍,比 DRAM 慢 2500 倍。

  • 固态硬盘是闪存,他的分级为:块–》页。一页为512k,写入时页为单位,刷存时块为单位。刷存影响寿命。

  • 在硬件上看来,所有的设备都是连接在一条总线上,由CPU通过信号驱动。CPU的信号可用触达任何存储模块。但是数据的提交过程却时层层上报的,也就是说,硬盘的数据需要加载到内存,才能被CPU读取到cache(尽管有总线,但无法直接传,因为IO速度差太大)。

3.2 系统软件知识:

  • 关于计算机程序的几个现象:
    • 时间局部性,如果一个数据刚被访问,那么他很可能会再被访问,所以计算机到处是缓存。
    • 空间局部性,如果一个数据被访问了,那么他周围的数据也很可能被方位,顺序访问。
    • 指令局部性,大部分的程序指令都不会到处goto,不会瞎跳。
  • CPU的缓存有寄存器和L1级高速缓存和L2高速缓存(其实是SRAM)。
  • 但是CPU还有一个很特别的寄存器,叫TLB,虚拟地址缓存。这个东西可以不要,但是由于操作系统大多是通过虚拟地址加页基准地址来寻址的,所以TLB存放页基准地址可以加速内存地址翻译速度,所以可以加速地址翻译。
  • 另外一个事实,低级存储的都把数据缓存在高一级的存储空间里。
  • 一个有趣的事实:网络通信IO缓存比硬盘慢,所以大部分网络数据都是缓存在硬盘里的。然后再按需要读取到内存提供给CPU。(比如浏览器的页面缓存)
  • 但是,在互联网时间,大并发高速网络IO需求背景,如果数据传输要经过硬盘才能读取是相当慢。因此直接内存代替了硬盘上的网络缓存,【零拷贝技术】孕育而生。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-182SpaCj-1588815445146)(:/ce42c1c91ad747ce87a62af2212b89b7)]

通过这种图,我们可以知道:

  • 内存才是操作系统级别的,高速缓存cache等是编译器和硬件级别的。
  • 怎么才可以让缓存利用率最大呢?除了操作系统给我们做了足够保证,我们程序该如何优化呢?
  • 缓存失效的原因是什么?未命中!为什么未命中?缓存为空,or 缓存数据不完整不合法 or 访问的数据大于缓存。
  • 程序是操作系统的概念。内核程序也是程序。任何计算机至少都有一个程序在运行,那就是操作系统的内核程序。

3.3 再聊一聊硬件知识–缓存级别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ngdleEly-1588815445148)(:/a4ee83cd3060425590453c1ea362c538)]

  • 从这个图可以看出,CPU其实只需要寄存器组和运算单元就可以工作了。高速缓存SRAM只是作为加速器缓存来使用。

  • 所有的数据最终都是来源于总线接口,总线的数据来源于内存空间。

  • 这不是重点,重点是有没有发现这个当先流行的web服务架构一毛一样。总线接口等价于数据库IO接口,高速缓存相当于Redis等NoSQL内存存储模块,寄存器就是本机内存和硬盘了。算术逻辑单元就是我们的服务工作线程。

  • 讲这个不是为了要用类似的思维去学习他,而是想说,不妨感受一下高速缓存的结构和意义,就像感受redis的奇妙一样。

  • 高速缓存的结构是类似完全二叉树的树形结构,第一层是集合,第二层是行,第三层是块。块才是真正存储数据的节点。

  • 通过一个集合都只有一行数据。等价于没有集合的概念(或者没有行的概念),这样的话,可以看做一行有很多块。只需要给出行坐标(集合坐标)以及块坐标即可定位。标准的地址是要给出集合坐标,行坐标,块坐标才可以的。

  • CPU怎么将写入内存?先写缓存,再写内存,需要写内存,比较慢。先写缓存,等缓存失效了再同步回内存,这种写入块,但是需要处理缓存内容过期问题(并发问题)。

  • 缓存的和内存谁是主,是一个很重要的话题。

  • 缓存读写策略有哪些?什么场景用什么策略呢?

缓存写入miss的策略
在写入 miss 的时候,同样有两种方式:

Write-allocate: 载入到缓存中,并更新缓存(如果之后还需要对其操作,这个方式就比较好)
No-write-allocate: 直接写入到内存中,不载入到缓存
这四种策略通常的搭配是:

Write-through + No-write-allocate
Write-back + Write-allocate

3.4 进程的内存结构

  • 在内存里,数据可以是【操作符+数据】也可以只是【数据】。这是冯诺依曼体的特点。

  • 程序内存空间从逻辑上可以分为:栈,堆,代码区。在程序被加载的时候就会分配程序空间。

  • 那么问题来了,程序的内存空间是按照什么规则分配的?可以动态扩大或者缩小么?

  • 按进程位单位分配的,其中栈是固定的,分配时映射了内存,堆是动态的,用的时候做映射,其他代码段,Data段,text段,库段时固定的。堆段时可以无限扩展的( 堆分为2,存放小对象的brk,和大对象的内存映射段)。

  • 一个程序可以分配多少内存?答案是:操作系统的全部可用内存,因为操作系统分配给进程的内存是虚拟内存,或者说是逻辑内存,在进程看来,整个系统的内存都是他们可以用的。比如操作系统有4G内存,那么每个进程都可以拿到4G内存。

3.5 堆空间内存映射

  • 仅当进程对内存进行读写的那一次,才会触发物理内存映射,将内存页与虚拟内存绑定(物理内存是按页分配的)

  • 每触发一次物理内存与虚拟内存的映射,下一个进程的可用内存都会减少。所以越往后面,新进程的可用内存就越少。除非有旧内存释放空间(接触物理内存页与虚拟内存的绑定)

  • 根据上面的推理,可用内存大小是动态变化(堆栈的可用大小是动态变化的)。

  • 栈有空间限制,堆没有。堆有多少就可以申请到多少。

  • 栈的数据是有固定生命周期的,从函数创建到函数结束。堆的数据生命周期不固定。

  • 页中断是为了让物理内存与虚拟内存建立联系。堆内存分配会触发昂贵的页中断,而栈不会,栈空间在进程初始化就分配好,并做好了页映射。

  • 由于堆分配需要进行页中断,成本高,所以建议一次性分配多点堆空间(比如1G哈哈哈)。不要每次分配几个字节。(这是理论上来说,实际上,内存管理器预料到会这样,所以给了解决方案。)

  • 当然为了适应玩家系统分配几个字节小内存的爱好,内存管理器,会为进程分配一个专门存放小内存的页,这个页不会随便回收,而是当空闲区比较大的时候才会回收页。所以并不会经常触发页中断。

  • 我们把小内存映射页叫heap,把大块内存映射区叫,内存映射段 Memory Mapping Segment。

  • 什么是Segment?进程讲虚拟内存空间划分的各种区域,这些区域就叫Segment,比如stack Segment 栈空间,head Segment,memory mapping Segment. data Segment, text Segment

3.6 堆空间大小约束

  • linux系统提供了很多函数给我们提前限制各个segment的大小上限。防止进程胡作非为,影响核心进程。
  • 有几种角度来设计内存限制:对用于分配内存的malloc函数进行限制,对程序访问的地址,在系统级别做限制,程序自身做检测限制。 分别是,系统读写限制,程序自身业务限制。

总结

操作系统的核心知识是进程管理和内存管理。进程管理涉及对算力的管理,内存管理涉及对存储的管理。进程对于操作系统,就是算力和存储的集合。重点关注【缓存局部性原理的利用】

二、程序接口

这一章主要是讲操作系统怎么抽象出进程的概念。

1.编译

认真阅读,接口产品设计,程序(进程的抽象)
- 关键词:编译,汇编,链接
- 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vCHPqZhe-1588815445149)(:/4da3af334c1048838d2dfb860e4615f5)]

  • 运行时包括了堆栈段,读写的代码区包括字符串,静态变量之类的,只读的大多存放方法代码。
  • 静态库,是函数方法和变量的集合,在程序链接执行的时候会被加载到代码段中。
  • 动态库,也叫共享库,为了减少库的反复加载浪费内存,会将其封装为一个共享的方法集合,只会在内存加载一份。所有可执行程序共享。
  • 代码在汇编成可执行对象后,将代码分为:只读代码段,已初始化数据,未初始化数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fxfcZAP5-1588815445151)(:/a8ff84645f0e419a979a188f6883d5a8)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wOT457x-1588815445153)(:/e933d0ef75db48d3b1a5a8fef9535e7d)]

2.异常

认真阅读,表面上讲异常,其实是讲操作系统怎么实现CPU共享。而下一张则是讲操作系统的内存共享
  • 进程可能是计算机系统中最伟大的抽象。进程这个概念背后,其实隐藏着一整套系统级机制,从进程切换、用户态与内核态的转换到系统实时响应各种事件,都离不开一个相当熟悉又陌生的概念——异常。

  • 计算机启动后就会不停的读取内核程序的指令并不停的执行指令。

  • 除此之外,计算机还会做一件重要的事情那就是跳转

    • 在程序内跳转,就是分支goto切换。
    • 在程序外跳转,就是函数调用和返回。(函数是为模块化而生的抽象概念,是对程序的抽象。)
    • 中断跳转(异常中断,用户退出中断,定时器中断。),称之为异常控制流(exceptional control flow)
  • 异常控制流不止是异常,他的定义包含了用户中断和定时器中断。

  • 我们常说的Exception,大多由硬件和操作系统触发。

  • 进程的切换也是异常控制流ECF的结果,进程的切换实现了对CPU的共享,该中断由硬件和操作系统协助完成。

  • 信号,只是操作系统的一种实现。

  • 什么是非本地跳转呢?非本地跳转(Nonlocal Jumps)。

  • 异常控制流只是一种机构上的概念吗?还是说是由于操作系统实现的一种机制?我倾向认为是前者。

  • 异常控制流,可以从硬件到操作系统,也可以从操作系统到用户程序,可以说无处不在。

  • 计算机从控制权的角度分为3层,硬件,操作系统,用户程序。最终的控制权位于操作系统,硬件作为服务组件为操作系统提高平台和必要的中断,操作系统监督程序运行。

  • 我们常说异常Exception,就是由硬件或者操作系统自身触发,并利用操作系统拥有的最高控制权反映到用户程序的执行流程中。

  • 系统会通过异常表(Exception Table)来确定跳转的位置,每种事件都有对应的唯一的异常编号,发生对应异常时就会调用对应的异常处理代码。

    • 这一点跟JVM很像。如果单单从指令跳转的的角度来看,异常表和我们平常写的switch case 语句一样,区别就是这个函数调用的返回时从系统返回,还是由用户自己的程序返回。
    • 除此之外,还有一种异步异常,即从系统突然中断触发的,这个时候用户指令会被挂起,操作系统重新掌控CPU,执行对应的中断程序,结束后返回用户空间。
      比如操作系统接受到IO中断,需要讲网络IO的数据存放到硬盘空间,此时就会临时征用CPU资源。
  • 系统调用走的其实时异常控制流程,因为他一旦用户程序调用了系统函数,就会把CPU控制权交回给了操作系统。等系统执行完中断指令后才会返回用户空间。

    • (看起来很复杂跳来跳去的,但是站在CPU的角度,就是在一直while循环,偶尔进行跳转。)
    • 触发中断的动作可以是用户程序主动调用系统函数. 也可以是操作系统,强行终止用户程序运行,跳转到自身中断程序。
    • 这就意味着用户程序,其实在操作系统的监督下运行的,操作系统随时都可以往用户程序指令集中间插入任何goto指令。
    • 所有的指令在CPU看来就像是队里中的流水。而操作系统就像流程线上的管理员,指令就是流水线上的商品。

科普一个概念,程序理论上可以访问所有的内存空间地址(包括虚拟内存)。
一旦用户往内存地址上写入数据,操作系统就会将与内存地址所在的内存页与用户进程绑定,表示这个页已经被某个进程映射绑定了。
内存页相对于进程来说就是一种相互竞争的物理资源。是操作系统提供给用户程序的资源。但是对于底层硬件来说,则是一种逻辑资源,页内存可以是硬盘,也可以是内存条或者是闪存。
如果页内存是硬盘资源,那么当用户访问页内存的时候操作系统就可以将页数据从硬盘加载到真正的内存提供给用户进程,这个过程对用户进程是不可见的。
提处页内存的目的有两个:
1.使得内存资源可以按批分配,提高资源的利用效率。
2.对物理资源进行虚拟,解除物理存储与用户数据的耦合关系,使得用户内存数据可以存放到硬盘,实现虚拟内存技术。
3.进一步适配各种奇奇怪怪的内存厂商标准。(也许是吧)

  • 进程才是程序(指令和数据)的真正运行实例。
  • 操作系统管理进程上下文切换,让多个进程分时共用CPU,让每个进程都感觉在独占CPU。
  • 操作系统管理内存映射关系,在硬件层实现内存页虚拟(页置换算法是虚拟技术的一个组成),在系统层实现页映射算法,让每一个进程都感觉在独占内存条。
  • 操作系统代理了CPU硬件和内存硬件的管理,使得用户编程正常情况不再需要关心,如何让多进程共享CPU和内存的问题。
  • 在用户看来一台就好像只跑了一个进程一样,不需要担心自己的程序是否会影响到其他进程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxEyDV6g-1588815445156)(:/67a4a0ee316f4ac581c55bcc549988c3)]

  • 页错误page fault 有哪些情况?内存页未被置换到内存中。被访问地址未发生页映射绑定(未分配内存)。页错误会切换到内核,内核会对页错误进行检查,如果只是未置换,则尝试置换,置换失败就抛异常(很少见),如果是其它情况就只能抛异常了。
  • segmentation fault 段错误,当程序访问的地址属于segmentation未分配的或者未映射的页内存。
  • segmentation fault 属于页错误的一种。
  • 内核会维护着一个进程队列,每一个进程对象都会保存着寄存器上下文,当cpu发生上下文切换的时候,进程的寄存器上下文就会被换入cpu中,在进程看来这就像独占cpu一样。因为进程是操作系统虚拟的概念,可以认为一个进程就是一个虚拟的计算机。所有进程共享一台物理硬件。
  • 内核会代理进程指令的指向过程,这就意味着进程指令会是不是的被内核注入中断指令。比如,用于进程调度的中断指令。具体过程如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mf0XF0Py-1588815445157)(:/c0a8e011101543a58926b7f5d12390ed)]

  • 如果站在cpu的角度看来就是这样的一堆指令:a进程指令-》切换寄存器指令-》b进程指令-》切换寄存器指令-》a进程指令。切换寄存器指令就是内核代码了。

  • 我们说的指令重排,其实就是内核往进程代码注入内核指令的行为。

  • 一旦发生系统调用或者硬件中断,就会触发内核上下文切换。

  • 我刚说了进程可以理解为是虚拟计算机,那么进程的几种状态是不是和虚拟机可以对应上呢?答案是肯定的,我会会创建虚拟机new,启动虚拟机ready,运行虚拟机作业running,暂停虚拟机作业stop, 重启虚拟机作业resume,终止虚拟机terminated.

  • 而终止虚拟机的几种方法有,直接断电,等于发送kill信号。调用exit退出函数,等价于点击关闭系统按钮,最优雅的动作。或者程序执行完所有代码(理论上不可能发送。)

  • 说一个有趣的函数fork,fork可以字面上一颗创建子进程的函数,但是这样理解是肤浅的。对,就是肤浅的。如果学习了操作系统后,我们应该从操作系统的本质去理解,操作系统是一个资源管理系统,操作系统的服务对象是进程,操作系统的资源分为共享资源和私有资源。共享资源就是系统里存储的外设数据,比如文件描述符(字节序),或者可以笼统的理解为内核级别的资源,这种资源fork不会进行复制,另外一种资源则是进程独占的资源具体为“堆栈segment,data text segment, 库代码segment,bss segment,以及最重要的进程寄存器上下文”.这些资源会被复制一份。而页表映射绑定关系也会被复制一份,但是默认情况下不会触发页表重新映射,除非发生了写冲突,就会把原页表拷贝一份并重新绑定页内存。所以fork函数就是复制进程独占资源的函数,唯一不一样的进程独占资源就是fork函数返回寄存器值不一样,父进程的fork返回值是子进程的pid号,而子进程的fork函数返回值寄存器值是0.

  • 当fork函数执行完后,两个进程的pc指针都指向fork函数这个内核函数的返回处。并各自开始执行自己的业务。

  • 信号是一种内核自己发明的异常控制流,每一个进程都有内核默认的信号处理函数,比如kill -9的终止信号,默认的内核处理函数就是终止进程。又其它进程发送给另一种进程(这个信号的发送者需要有足够的系统权限)

  • 信号作为内核进程提供给进程的通信方式,由此可见,所有利用信号进行的进程通信都需要有内核进程进行协调,内核进程就充当了代理或者是中介的角色。

  • 信号是一种异步的通讯机制,就类似消息队列,a进程发送给b进程,信号消息会先暂存在系统的内核进程等待发送,这就意味着,信号有一种交pending的状态(内核是用set来暂存信号,所以信号不会重复)。接受者可以暂时阻塞一些特定信号(又进程的启动者对指定阻塞哪些信号),这就好比接受者在系统内核有多个快递箱,一些快递箱可以暂存不处理的信号,另一些快递箱则是存放相对实时的信号。

  • 相比,硬件中断和系统中断,这两种异常控制流而言,系统内核发明的信号异常控制流就没那么伟大了,信号仅仅为了进程通讯,而硬件中断和系统中断则是为了实现cpu共享和协调io等外设。

  • 进程组这个概念目前看来只是为了批量维护进程。比如可以给进程组id发送信号,这样就等于批量发送信号给进程组内的所有进程。

  • ctrl+z是发送挂起信号给当前前台进程。并不会终止进程。

  • ctrl+c是发送stop信号给当前前台进程。

  • kill 是用来发送信号的系统调用

  • 每一个信号都是对应在一个状态字的一个比特位。换句话,每一个比特位都对应一个中断函数。

  • 每个信号的中断函数是可以被重写的(重定向函数句柄),通过signal系统调用来重写。

  • 操作系统的所有进程代码都是被内核进程注入监控指令的,以至于每个用户进程隔一段时间就会去执行内核进程的指令,看看内核进程这个顶级上司有何指示,没指示就继续做自己的事情。这样的机制就是用来实现,操作系统信号中断等异常控制流的。(有了这样的机制,进程就像是操作系统的租户。)
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HPvjccY6-1588815445158)(:/3b056fb188264506bce9fac0e95836bf)]

  • 最有趣的是操作系统内核进程指令除了会注入到用户进程意外,还会注入到自己的内核调用。有了这个满世界都是注入指令的代码,就是随时随地中断任何程序,包括自己。比如,信号处理器在处理中断信号的时候也可以被其它信号给再次中断。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sOs1ZmeF-1588815445160)(:/9a56c2f4938f4384884f9a30124504cd)]

  • 信号处理器相对你内核进程来说是异步的,那就意味着要面向并发编程的数据同步问题,在操作系统里叫“异步信号安全”(我们在JVM里叫线程安全),这里的安全手段也和大部分的计算机并发问题处理没什么区别,比如
    • 对共享变量采用volatile,确保内存可见性和读取一致性(应该也具备防指令重排的作用),
    • 采用局部变量而非全局变量,从而避免资源共享。
    • 对异步函数(信号处理函数),声明原子性,具体就是在执行函数期间屏蔽其它中断信号。
    • 避免长时间复杂作业
    • 避免使用非安全的内核调用(大部分内核调用都是并发不安全的,毕竟并发问题是罕见的场景)。
  • Posix 标准指定了 117 个异步信号安全(async-signal-safe)的函数,这个就类似Java的JUC库了
  • 说一个看起来鸡肋的知识,“非本地跳转”其实就是跨函数跳转,比如我们在递归函数栈时,可以不用按照函数栈一步一步回溯,而是直接从栈顶直接回到栈底的main函数,感觉没什么应用场景。着实鸡肋。

3.内存

认真阅读,资源管理算法
  • 了解虚拟内存管理器,核心是围绕“内存页关联表”的缓存和翻译。
  • 了解“多级内存页关联表“是怎么利用搜索树算法加速查询。
  • 了解动态内存分配的过程(本质上就是学习“内存页关联表”的工作过程)
  • 外部碎片是内存管理器对物理内存的碎片管理,看似来似乎没什么可展开学习的。
  • 内部碎片,其设计页内对象空间管理。常常与垃圾回收器挂钩,尤其是在JVM中垃圾回收器是重头。
  • 了解内存异常,stackoverflow,segmentfault.
  • 不同进程访问同一个地址,他们的虚地址一样吗?一样的,因为他们有各自的页表。A进程访问第0页第5个偏移,查A的页表,会被映射到第6页物理内存的第5个偏移,而B进程访问第0页第5个偏移,会查B的页表,会被映射到第17页物理内存的第5个偏移。(这段话的关键词是,每个进程都有各自的页表)。确定这样的理解对吗?对的。
  • 缓存的两种写策略,write-back,换出的时候写回(懒写)write-through,改缓存的时候及时写入(饿写)。
  • 虚拟内存实际上是操作系统对内存的重定义,操作系统不再把物理内存(内存条DRAM)看作是直接可用的内存,而是将磁盘看作内存空间,而DRAM被当作是内存空间的缓存空间。当需要访问虚拟内存条的时候,就会查询页表,如果页表有记录,则表面硬盘里的内存数据被缓存在DRAM中那么直接访问即可,如果页表中不存在改内存数据的缓存,则触发异常中断,执行页置换算法,将硬盘的内存数据调入DRAM中,中断结束就继续刚刚的虚拟内存访问流程。
  • 因此虚拟内存技术可以看作是对物理内存的代理技术,而被代理的对象是进程,不是操作系统,这就意味着,只要寻址空间允许(即便物理内存只有1G),你也可以创建几十万个进程,并且还能保证每个进程有3G的可用内存。
  • 由于局部性原理的存在,这项代理技术非常有存在的价值。
  • 问一个问题,“给定4G的物理内存DRAM,和一个1T的硬盘,并且声明操作系统内核进程最多只用1G物理内存。试问有没有可能创建10个用户进程,约定每个用户进程都会申请2G的内存空间?” 答案是可能,因为现代操作系统将物理内存DRAM看作是cache,真正的内存空间位于磁盘等外设上,因此只要确保磁盘空间足够容纳每个进程独占的虚拟内存空间,那么就可以提供超过物理内存上线的进程数,并且还能保证每个进程拥有好几G的独占的虚拟内存空间。(而最终大的进程上限只能由操作系统本事进行软限制了,硬件可以说是无法限制进程的个数的。)
  • 页表会限制内存块的“读、写、执行”的权限。
  • 给定一个固定规格的空间,固定空间内的碎片空间成为内部碎片,固定空间所在的外部空间碎片成为外部碎片。
  • 内存分配算法和内存回收算法都是进程在自己动态内存空间的管理算法。
  • 写入超出堆栈缓冲区的内存会怎么?(缓冲区溢出攻击)
    • 如果写入的内存是堆栈段内,则会扰乱堆栈内其它数据值。
    • 如果超过堆栈段,则会出现segmentfault.
    • 问题是万一缓冲区位于寄存器附近,且没有报segmentfault,就会导致PC执行了攻击者预先留下的代码空间,如果这段代码是系统调用代码,就很有可能使得攻击者获得shell权限,万一被攻击的程序是root权限的程序,那么就等于攻击者获得了root权限的shell了。所以建议大家不要随意用root执行程序
  • 工作集,WorkingSet 就是只某一个进程某一段时间内会用到的内存页总和(包括了共享内存和独占内存),这个概念提出有利于我们统计物理内存DRAM的使用情况。我们把每一个进程的工作集加起来就是认为这是物理内存DRAM的实际使用情况了。(非工作集的内存数据在硬盘交换区里躺着。)
  • linux swap值是所有进程共用的swap值上限?还是单个进程能用的swap值上限?根据实践经验,我觉得是前者,因为我们平常free -h查看swap空间使用情况是,返回的时候并没说某一个进程用了多少。而是直接现实一个使用量。
  • swappiness可以优化linux对swap空间的使用策略,0~100,为100表示尽可能的用磁盘,为0表示尽可能用物理内存。

三、进程通信

这一张讲用户进程怎么实现业务。

1.系统IO

认真阅读,进程协作,学习文件系统
- 在linux里,【所有东西都是文件】(这句话有待考证)
- 文件可以看成是字节序
- 标准c文件接口都带缓存。
- 而unixI/O的文件访问接口open,close,read,write都不带缓存。
- open可以理解为调用内核函数初始化文件上下文,close就等于调用内核函数关闭文件上下文。
- read则是向设备发送字节序复制指令。
- 文件头有几个重要数据,type,inode号,inode引用次数。
- 同一个文件系统inode号表示同一个文件,inode引用次数是专门为硬链接设计的字段,用于表示该文件有多少个硬链接。
- 只有硬链接变成0才允许被删除。(一个inode可以有多个硬链接)
- 软件其实是一种文件类型,因为他有不一样的inode号,文件系统是根据inode来区分是否为同一个文件。
- 目录也是一种文件,他的文件内容保持着上一个目录的指针,以及文件索引列表。
- 每一个由Linux shell打开的进程,都会打开3个系统资源文件,即,标准输入流0.标准输出流1,标准错误流2,0、1、2 分别是这3个文件的文件描述符fd。
- write和read这两个UNIX IO接口的最小事务粒度是1个字符,即,要么读写一个字符,要么读写0个字符,没有半个字符的说法。

- 文件的数据结构很复杂,由多个文件元数据组成,其中与inode有关的元数据如下:
	struct stat
{
    dev_t           st_dev;     // Device
    ino_t           st_ino;     // inode
    mode_t          st_mode;    // Protection & file type
    nlink_t         st_nlink;   // Number of hard links
    uid_t           st_uid;     // User ID of owner
    gid_t           st_gid;     // Group ID of owner
    dev_t           st_rdev;    // Device type (if inode device)
    off_t           st_size;    // Total size, in bytes
    unsigned long   st_blksize; // Blocksize for filesystem I/O
    unsigned long   st_blocks;  // Number of blocks allocated
    time_t          st_atime;   // Time of last access
    time_t          st_mtime;   // Time of last modification
    time_t          st_ctime;   // Time of last change
}


  • 其中 st_ino,st_mode,st_nlink 就是我们刚刚说的inode号,文件类型,inode硬链接数。
  • 当有进程打开文件时,内核会把文件索引对象(即文件属性)中的引用计数器+1,进程自己也有一个索引表记录了当前进程打开了什么文件。
  • 子进程所谓的继承父进程的文件索引表,实际上就是拷贝了一份文件描述符表过来而已。
  • C标准库 IO 会用流的形式打开文件(这一点和UNIX IO不一样,区别在于流)
  • 什么是流,流(stream)实际上是文件描述符和缓冲区(buffer)在内存中的抽象。
  • 也就说,带缓冲区的方式打开文件,就称之为流,分为输入流,输出流。
  • 如果用 Unix I/O 的方式来进行调用,是非常昂贵的,比如说 read 和 write 因为需要内核调用,需要大于 10000 个时钟周期。
  • 简单的说就是read和write是内核接口,需要从用户态指令切换到内核态指令,需要切换寄存器上下文很浪费时间。
  • 如果为了减少来回切换用户态到内核态,采用批量调用的方法,减少内核调用次数。先把内核缓存在用户内存,后来在一次性刷入内核态。
  • 说说java,java更恶心,需要把数据流写入到JVM的堆,然后再刷回用户态堆内存,然后再刷回内核态内存,需要花费两次字符串复制。因此JAVA采用直接内存技术可以优化性能,即,将数据直接写入用户态缓存,然后再刷回内核态,从而减少了一次内存复制的过程。
  • Unix I/O 中的方法都是异步信号安全(async-signal-safe)的,也就是说,可以在信号处理器中调用。
  • 什么是异步信号安全呢?就是在调用UnixIO的时候允许被其他中断信号打断,之后再继续IO也不会造成异常丢数据或者死锁,这种不会独占CPU。
  • 标准 C I/O 如果用来处理网络IO,记得及时flush缓冲区,要不然信息会丢失,其次就是标准C IO不是异步信号安全的。
  • 异步信号安全,分为2种,1.可重入安全,2.互斥其他信号安全。
  • 尽管标准 C I/O有互斥锁对buffer进行保护,但是他互斥不了信号中断,因此信号中断,如果再次调用同一个函数就会形成相互等待的死锁场景。
  • 互斥锁不是内核层面的,所以无法阻塞中断信号。
  • 所以不要在中断函数里调用一些标准C的东西就是这个道理,毕竟中断函数是内核级别的函数。

2.网络编程

认真阅读,为互联网打基础,其实对于看过tcp协议和计算机网络的人来说,这一章完全可以不读的了。

3.并发

认真阅读,为高并发打基础
  • 介绍了3种最简的并发模型,多进程并发IO,基于事件单进程IO,基于多线程IO并发。
  • 其中多线程IO模型可以理解为共享动态内存但是不共享寄存器的IO模型。
  • 基于事件IO模型,其原理则是利用操作系统IO时延远大于CPU业务处理时延(想象一下一个Byte传播需要1ms,cpu可以执行多少指令)。
    显然cpu大部分时间处于空闲状态,尤其是当我们网络请求的事务都是短作业的时候尤为明显(即服务器每个请求处理速度特别快)。
    正因为我们大部分的业务都是短作业的,所以没必要用并发。换而言之,我们把所有请求发给一个进程即可。
    而要实现这个技术关键点在于理解【linux套接字】的定义,套接字包括了【发送方IP,发送方端口,请求方IP,请求方端口】
    多进程模型里,一个套接字对应一个进程。而基于时间的模型,我们让一个进程维护一个套接字数组,并且开启一个中断监听。
    一旦数组中任意一个套接字缓存接收到字节序,则唤醒业务进程处理收到的字节序。所有套接字的字节序是穿插着排队被处理的。
    • 总结一下,就是基于事件,监听,观察。
  • 大部分生产环境都是结合这3个核心组合而成,现代操作系统,常常用事件IO和多线程集合。
  • POSIX Threads是一个线程库,他提供了多线程。
  • POSIX API 中大致共有 100 个函数调用,全都以 pthread_ 开头。
  • 我们通过学习 POSIX API可以了解一个线程库可以划分成哪些模块。
  • POSIX API提供功能如下:
    • 最基本的线程创建和终止,线程启动API。这个等同于和进程管理一样。
    • 提供多进程也有的线程同步功能,比如,join等待(和wait差不多)
    • 还有多进程所没有的,同步工具:Mutex,条件变量,读写锁。
  • Mutex,条件变量,读写锁同步3个工具对应着3种应用场景。
  • 除了POSIX的同步功能,还有一种作为补充的第三方同步工具Semaphore API用于实现其他场景。
  • 每个线程有单独的线程上下文(线程 ID,栈,栈指针,PC,条件码,GP 寄存器)
  • 所有线程共享进程资源,除了栈空间和CPU寄存器数据。
  • volatile 关键字声明,在C语言里表示写入采用write-through策略。(牺牲缓存性能获得可见性)
  • 临界区块是一种利用同步工具实现的一个概念,即确保一段指令只能被一个线程执行,实现同步效果。
  • 消费者和生产者的应用场景很多,其中视频帧的生成和渲染就是一种基于消费生产模型实现的。其中帧就是信息载体。
  • Reentrant Functions 是线程安全函数非常重要的子集,其表现为,函数被中断后,
    再回来原函数执行,其上下文没有被改变。(他不是通过临界区模型,而是纯代码实现,即不fang’w全局变量)
  • 阿姆达尔定律 amdahl’s law指,将以一个作业分为可并行逻辑和不可并行逻辑,然后来考虑采用多核并行后的价值。从而指导我们考察业务场景是否采用并行计算。
  • 超线程,指线程的分发和CPU绑定之间又加了一层代理,从而实现一个核心可以执行多个线程?有待研究。

四、总结:

CSAPP,第一部分讲了数据类型和汇编指令简单知识,基本上就是在可怕逻辑上的hello world入门了。第二部

,重点讲了操作系统怎么利用中断实现多进程共享算力,以及怎么定义进程虚拟内存和实现。第三部分讲了进程通信技术。

从目录结构上看,整本书核心就是讲进程,可见对于操作系统来说,进程的重要性。甚至我们可以说一切都是为了
讲明白进程而作的铺垫。

整本书就是讲:硬件怎么支持实现操作系统,操作系统怎么支持用户进程,用户进程怎么支持业务实现(从计算机的架构看来,业务的本质就是IO,计算只是为了更好的IO)。

引用声明:

  • https://wdxtub.com/csapp/thin-csapp-3/2016/04/16/ 小土刀博客
  • https://www.jianshu.com/p/9a3720912164 简书,进程和线程的内存组织结构
  • 《深入理解计算机系统》作者: Randal E.Bryant / David O’Hallaron
  • https://zh.wikipedia.org/wiki/%E9%98%BF%E5%A7%86%E8%BE%BE%E5%B0%94%E5%AE%9A%E5%BE%8B 阿姆达尔定律

你可能感兴趣的:(操作系统)