ZB-053-01操作系统和计算机原理

操作系统和计算机原理简介

计算机体系原理

  • 最基本的计算

计算机

  • 核心是一个CPU 中央处理器,它是计算机的大脑只有它负责计算,其它所有东西都是为它服务的
    • 穿孔纸带
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通俗例子:
一个人(CPU)
手里拿着笔和一个小本本(阅读的说明书就是程序的指令)
他面前有几个临时的桶(寄存器) 大部分人做数学题的时候都会把东西抄在一个地方用于下次计算时使用


我们通过一个穿孔纸带,根据穿孔纸带 01二进制码识别指令,
往面前这个桶里放一个数字1,
然后看下一个机器码是什么
如 +2
就把刚才桶里的数加一下放进去


CPU就是一个很聪明的人
它手里拿的小本本——说明书 就是阅读机器码
第一条指令执行 xxx
第二条指令 若 xxx成立 执行 yyy操作

深入

1
2
3
4
5
6
7
8
9
10
CPU(一个聪明的人)
它可以阅读说明书
并且操作面前的内存

说明书实际也放在内存里
内存除了放说明书他还存放指令

因此我们称这种结构叫做 ——
冯·诺伊曼结构 https://zh.wikipedia.org/wiki/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84
一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构

CPU不出错吗?

1
2
3
4
5
6
7
8
9
10
11
CPU里放了上亿的晶体管
用特殊的硬件逻辑完成我们的功能

比如有个东西叫做:与非门 1和0相与取非

CPU它会出错吗?
我们认为不会,但并不能保证它不会出错——一个著名CPU bug 1994 cpu
https://en.wikipedia.org/wiki/Pentium_FDIV_bug 浮点数错误
https://zh.wikipedia.org/wiki/%E5%A5%94%E8%85%BE%E6%B5%AE%E7%82%B9%E9%99%A4%E9%94%99%E8%AF%AF

就是用硬件电路预定义了一些除法算数了,不幸的是焊错了,导致这是一个硬件问题,无法通过软件来解决。

内存就是很长很长的连续数据存储空间,每个单元(字节)代表一个数据或者一个指令

  • 其他之外都是存储的

扩展链接:计算机基础 | 多核、缓存…现代CPU是如何工作的
为什么寄存器比内存快?

程序的装载和执行

1
2
3
每个程序有不同的说明书,如播放器,浏览器,QQ,微信,它们如何区分呢?

通过 把说明书和一些数据的打包叫做 可执行程序 如 .exe 文件

你在你电脑上运行 .exe文件时发生了什么

  • 首先,可执行程序里有一段是 说明书+一些数据 可能它只有说明书
    • 一个exe可以启动很多个进程
    • 假如你每次双击一些记事本就弹出一个窗口
    • 它每次开始执行,它会装载到内存中某个地方里去
    • 你在次点击记事本,就又弹出了一个窗口

这多个进程和 说明书本身有什么联系吗?

- 说明书本身指导了这个程序如何被启动和执行
- 但是这两个进程的数据是互不相同的

这就引出了物理和虚拟寻址

物理和虚拟寻址

你打开了多个记事本,CPU看着说明书 按着exe里的东西一行一行执行

首先这个exe装载了很多次,你打开了多个记事本,输入不同东西,对其他记事本的内容没任何影响。我们称之为虚拟寻址空间

虚拟地址空间

  • 在每个进程自己看来,它自己好像占据了所有内存,实际上是在漫长的内存里分配给它的一小节

假如你有个 32G内存的笔记本

32G的空间就是物理地址,每个程序里内部跳来跳去的地方叫做虚拟地址空间

  • 好处是:每个记事本之间互不干扰的执行,假如一个记事本崩溃了,不会影响其他记事本,同时每个记事本无法获取其他记事本的数据。

为什么一个程序无法跨平台?

CPU傻傻的执行,这个说明书的 .exe里的指令 一条一条的执行。。。

因此,一种CPU和另外一种CPU他们的说明书肯定是不同的。很容易理解这是因为他们的CPU架构不同

为什么最新的 win10还能运行 win95的一些游戏呢?

  • 最广泛的桌面架构x86架构 :它是一种指令集的东西
    • 只要指令集不变,它们就可以用同一份说明书

如果CPU完全相同都是x86为什么 win/linux/mac 它们的程序还不能共用

  • 因为在说明书中可能需要调用在操作系统里的一些相关东西,如windows图形接口这些在linux上没有
  • 一份说明书 .exe需要装载到内存中,这个内存的布局 不同系统是不一样的。

因此我们该怎么办呢?

于是我们有了 python/java 各种高级语言,在操作系统上维护的虚拟机。

它可以解释和当前操作系统说明书完全不同的另外一种说明书叫做——字节码

go是直接编译成 native的所以没法跨平台

这就是程序的装载和执行

程序启动的本质

从硬盘上的一份说明书,加载到内存里变成一个可以执行的程序

动态链接库

1
2
3
4
5
6
执行程序的时候,就是按照说明书的指令
12345~~。。。
有些时候这些指令是不全的
比如在执行某个指令时:说请去调用一个foo函数,但是这个foo函数不在本程序中,而在遥远操作系统的另一个地方

我们称之为 这个程序和它当前要使用的库分离的情况 叫做 动态链接库

好处

  • 把说明书某些关键功能拆出来,单独放在一个部分 省空间,提取公共

坏处

  • 我们在没有这个依赖的说明书运行我们的程序就会报错
    • 我们都知道玩游戏的时候有时候会弹出一个报错 xxx找不到
  • 版本问题
    • 这个游戏需要 一个库版本是0.5
    • 但是你机子安装的是 2.0
  • 最要命的是这个这个崩溃不会立即发生而是项目正常上线一个月之后突然崩了

有关动态链接库推荐看一本书

程序的分时复用

假如你只有一个CPU,就是你的电脑上同时只能有一个人可以不停的做各种运算,但是你却可以看网页,听音乐,下载东西。。。。

这就设计一个概念:分时复用

CPU的速度远远快于内存\IO\磁盘

  • CPU和内存就差了3个数量级
    • 内存做1件事,CPU能做1000件事

所以在CPU看来其他人都是慢的跟蜗牛一样,于是CPU说请在这个时间我去做点别的。

于是又一个概念就是 时间片轮转

  • 执行一会儿A程序,执行一会儿B程序,因为CPU是在太快了,这些程序的响应时间太慢了。
  • 所以在你看来,它好像同时在做很多件事,只是你的时间感知维度不在一个级别。
  • CPU所应对的其他外设是在太慢了

这就是CPU的基本工作流程

是谁可以占据它的时间片呢?

  • 进程/线程都可以

什么时候会放弃时间片

假如有个程序很难缠一直请求 CPU,给你带来直观的感受就是 卡顿。你的鼠标移动的时候都是受CPU控制的。

因此我们不能让任何一个程序如此霸道的占据CPU,于是有了调度

  • 进程的调度是 操作系统

看看你的任务管理器

这里有如此多的进程在进行,因此我们不能让其中的一个程序霸道的占据 CPU的所有,于是有了调度,根据每个程序的优先级来划分程序执行的时间。

挂起和中断

  1. 当你听着歌,开着ide,开着浏览器,开着word,
  2. 然后在 word里写下一段文字按下 ctrl + s 的瞬间,因为保存操作涉及文件的写入就是磁盘的读写 IO
  3. IO的时间非常慢,CPU很快 所以它不会等word写入后在弹出个保存成功
  4. 于是CPU在此时去(轮流)执行其他进程/应用
  5. 当文件成功写入磁盘后,发起一个中断
  6. CPU通过中断处理程序接收到这个中断信号来了之后,停下手中其他的程序,恢复word 程序的执行弹出 保存成功

上下文切换

  • 上下⽂context:程序运⾏的环境(寄存器、PC)临时的数据
  • 放弃CPU时,保存上下⽂
  • 拥有CPU时,恢复上下⽂
1
2
3
4
5
6
7
8
9
10
CPU是一个人
他面前又多个桶 寄存器

他面前还有一堆说明书(一堆应用程序)

操作系统告诉它 你现在该执行谁了。

CPU开始执行 ”说明书1“ 的时候,假如执行到第三条指令,此时“说明书1”的时间片到了,它要去执行 另一个“说明书2”的指令,当恢复执行 “说明书1” 的时候 CPU 如何知道 从那条指令开始?

一定需要一个东西 把 “说明书1” 当前执行的阶段保留,这个东西就叫 "program counter" 也就是 上下文 context

例子

  • CPU轮流执行说明书,当从 说明书2 切换到 说明书1 的时候
  • 要 保存 说明书2 的上下文
  • 此时 说明书1 拥有了 CPU ,恢复 上下文 继续执行
  • 当 “说明书1” 时间片到了, 就 保存 它的上下文
  • 此时 说明书2 拥有了 CPU ,恢复 上下文 继续执行
  • 如此循环往复

program counter 只有一个吗?

1
2
3
4
5
6
是的,program counter 每个CPU只有一个,这也是为什么上下文切换的原因
每当你从一个程序 切换到另一个程序的时候,你都需要把 当前的所有数据 保存到内存中
下次 在此运行这个 程序的时候 再从内存中恢复

只有当前CPU执行的程序的 pc才被 CPU 持有
其他程序的的 数据都保存在 内存里

这样频繁切换不会造成性能浪费吗?

  • 这也是为什么要有 协程 的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
对于 3Ghz CPU
在切换上下文的过程中,大概需要10的4次方个时钟周期,尽管它很慢,但还是比 IO 快
虽然他很浪费,但这是不得已的。因为切换上下文必须这样做。

可以不用不那么浪费? 可以的,
我们希望一个线程自己在跑,它自己内部又维护一些分时复用的东西呢,并行的执行一些工作?
于是出了一个新的概念 —— 用户态线程

还有一个概念叫做 协程

一个线程单独又一个方法栈 大约 1M
而一个协程 大约 4KB 差距200倍
但是你需要自己写一套调度算法

协程是操作系统概念还是编程语言概念

  • 答案是 它是编程语言的概念

什么是操作系统

  • 欺上瞒下(承上启下):
    • 对上层提供统⼀的API,简化应⽤开发逻辑
      • 例如:通过Windows的CreateProcessA⽅法创建新的进程
      • 通过Unix的fork系统调⽤创建新的进程
      • 只要使用统一的 api
    • 对下层通过硬件驱动提供兼容性
      • 非常好的向后兼容

x86

来源于8086处理器 也就是 CPU
90年代,说一个电脑性能 会说它是386/486/586 的机器

因此当你下载一个软件的时候可以指明它CPU是什么的标准 如 ubuntu i586

驱动的概念

  • 比如显示器/鼠标
    • 假如你想正常显示你需要给我这个xxx端口发个信号

在此过程我们发现我们一直在抽象

  • 好处:一套代码,到处运行
  • 坏处:效率低。

计算机科学领域任何一个问题都可以通过添加一个中间层来解决

进程和线程

  • 共同点:
    • 都拥有独⽴的PC(程序计数器),可以独⽴地执⾏程序(性格测试题?)
    • 在Linux上甚⾄⽤同⼀种数据结构处理进程和线程
  • 进程:拥有独⽴的内存地址空间、⽂件等资源
  • 线程:和其他线程共享内存地址空间、⽂件等资源

你把电脑启动之后,打开 chrome,叮叮,vscode,网易云音乐,有道云笔记之后

一个CPU发现操作系统又若干个说明书,每个程序通过 分时复用 的方式执行各种各样的程序这是进程

线程

把其中某个进程 放大一下,如 java进程,一个程序本来只有一个计数器从上往下,当一个程序执行慢的时候,我想要加快速度,在这个进程内部再开一个小的说明书,我们称之为 线程

⽂件系统与IO

  • ⽂件的本质是字节流
  • ⽂件与⽂件指针
    • 读到哪一行,读某块内容
  • 缓存与缓冲区
    • IO很慢 从磁盘读到CPU是非常慢的

源代码是如何变成程序的

  • 远古时代:用针戳纸带
    • 剪贴和粘贴才是真的剪贴和粘贴
  • 机器帮你生成机器码:高级语言到机器码的过程——编译

从源代码到AST(编译器前端)

  • 源代码 -> tokens (解析成一个一个关键字)
  • tokens -> AST (关键字变成一个抽象语法树)

从AST到⽬标代码(编译器后端)

  • AST -> 字节码 (机器指令 0101…的内容)
  • AST -> 平台相关的⽬标代码(真正可以执行的说明书)

AST in JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
let a = 1;
let b = 2;
let sum = (a,b)=>a + b;
sum(a,b);

// 会变成
{
"type": "Program",
"start": 0,
"end": 55,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 10,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
},
{
"type": "VariableDeclaration",
"start": 11,
"end": 21,
"declarations": [
{
"type": "VariableDeclarator",
"start": 15,
"end": 20,
"id": {
"type": "Identifier",
"start": 15,
"end": 16,
"name": "b"
},
"init": {
"type": "Literal",
"start": 19,
"end": 20,
"value": 2,
"raw": "2"
}
}
],
"kind": "let"
},
{
"type": "VariableDeclaration",
"start": 22,
"end": 45,
"declarations": [
{
"type": "VariableDeclarator",
"start": 26,
"end": 44,
"id": {
"type": "Identifier",
"start": 26,
"end": 29,
"name": "sum"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 32,
"end": 44,
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 33,
"end": 34,
"name": "a"
},
{
"type": "Identifier",
"start": 35,
"end": 36,
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"start": 39,
"end": 44,
"left": {
"type": "Identifier",
"start": 39,
"end": 40,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 43,
"end": 44,
"name": "b"
}
}
}
}
],
"kind": "let"
},
{
"type": "ExpressionStatement",
"start": 46,
"end": 55,
"expression": {
"type": "CallExpression",
"start": 46,
"end": 54,
"callee": {
"type": "Identifier",
"start": 46,
"end": 49,
"name": "sum"
},
"arguments": [
{
"type": "Identifier",
"start": 50,
"end": 51,
"name": "a"
},
{
"type": "Identifier",
"start": 52,
"end": 53,
"name": "b"
}
]
}
}
],
"sourceType": "module"
}