1. 程序编译过程
- gcc HelloWorld.c -E -o HelloWorld.i 预处理:加入头文件,替换宏。
- gcc HelloWorld.c -S -c -o HelloWorld.s 编译:包含预处理,将 C 程序转换成汇编程序。
- gcc HelloWorld.c -c -o HelloWorld.o 汇编:包含预处理和编译,将汇编程序转换成可链接的二进制程序。
- gcc HelloWorld.c -o HelloWorld 链接:包含以上所有操作,将可链接的二进制程序和其它别的库链接在一起,形成可执行的程序文件。
2. 内核结构与设计
计算机资源
- 总线,负责连接各种其它设备,是其它设备工作的基础。
- CPU,即中央处理器,负责执行程序和处理数据运算。
- 内存,负责储存运行时的代码和数据。
- 硬盘,负责长久储存用户文件数据。
- 网卡,负责计算机与计算机之间的通信。
- 显卡,负责显示工作。
- 各种 I/O 设备,如显示器,打印机,键盘,鼠标等。
内存管理计算机资源
- 管理 CPU,由于 CPU 是执行程序的,而内核把运行时的程序抽象成进程,所以又称为进程管理。
- 管理内存,由于程序和数据都要占用内存,内存是非常宝贵的资源,所以内核要非常小心地分配、释放内存。
- 管理硬盘,而硬盘主要存放用户数据,而内核把用户数据抽象成文件,即管理文件,文件需要合理地组织,方便用户查找和读写,所以形成了文件系统。
- 管理显卡,负责显示信息,而现在操作系统都是支持 GUI(图形用户接口)的,管理显卡自然而然地就成了内核中的图形系统。
- 管理网卡,网卡主要完成网络通信,网络通信需要各种通信协议,最后在内核中就形成了网络协议栈,又称网络组件。
- 管理各种 I/O 设备,我们经常把键盘、鼠标、打印机、显示器等统称为 I/O(输入输出)设备,在内核中抽象成 I/O 管理器。
内核要想管理和控制这些硬件就要编写对应的代码,通常这样的代码我们称之为驱动程序。
宏内核结构
宏内核就是把以上诸如管理进程的代码、管理内存的代码、管理各种 I/O 设备的代码、文件系统的代码、图形系统代码以及其它功能模块的代码,把这些所有的代码经过编译,最后链接在一起,形成一个大的可执行程序。
这个大程序里有实现支持这些功能的所有代码,向用户应用软件提供一些接口,这些接口就是常说的系统 API 函数。而这个大程序会在处理器的特权模式下运行,这个模式通常被称为宏内核模式。
宏内核提供内存分配功能的服务过程,具体如下:
- 应用程序调用内存分配的 API(应用程序接口)函数。
- 处理器切换到特权模式,开始运行内核代码。
- 内核里的内存管理代码按照特定的算法,分配一块内存。
- 把分配的内存块的首地址,返回给内存分配的 API 函数。
- 内存分配的 API 函数返回,处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址,并且可以使用这块内存了。
宏内核缺点:
- 没有模块化,没有扩展性、没有移植性,高度耦合在一起,一旦其中一个组件有漏洞,内核中所有的组件可能都会出问题。
- 开发一个新的功能也得重新编译、链接、安装内核。
宏内核优点 :
- 性能很好,因为在内核中,这些组件可以互相调用,性能极高。
微内核结构
微内核功能尽可能少:仅仅只有进程调度、处理中断、内存空间映射、进程间通信等功能。
把实际的进程管理、内存管理、设备管理、文件管理等服务功能,做成一个个服务进程。和用户应用进程一样,只是它们很特殊,宏内核提供的功能,在微内核架构里由这些服务进程专门负责完成。
微内核定义了一种良好的进程间通信的机制——消息。
应用程序要请求相关服务,就向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,接着服务进程会完成相关的服务。服务进程的编程模型就是循环处理来自其它进程的消息,完成相关的服务功能。
微内核提供内存分配功能的服务过程:
- 应用程序发送内存分配的消息,这个发送消息的函数是微内核提供的,相当于系统 API,微内核的 API(应用程序接口)相当少,极端情况下仅需要两个,一个接收消息的 API 和一个发送消息的 API。
- 处理器切换到特权模式,开始运行内核代码。
- 微内核代码让当前进程停止运行,并根据消息包中的数据,确定消息发送给谁,分配内存的消息当然是发送给内存管理服务进程。
- 内存管理服务进程收到消息,分配一块内存。
- 内存管理服务进程,也会通过消息的形式返回分配内存块的地址给内核,然后继续等待下一条消息。
- 微内核把包含内存块地址的消息返回给发送内存分配消息的应用程序。
- 处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址,并且可以使用这块内存了。
微内核优点:
- 系统结构相当清晰利于协作开发。
- 系统有良好的移植性
- 微内核有相当好的伸缩性、扩展性,因为那些系统功能只是一个进程
微内核缺点:
- 同样是分配内存,一来一去的消息带来了非常大的开销,当然各个服务进程的切换开销也不小。这样系统性能就大打折扣。
分离硬件的相关性
- 分离硬件的相关性,就是要把操作硬件和处理硬件功能差异的代码抽离出来,形成一个独立的软件抽象层,对外提供相应的接口,方便上层开发。
- 硬件平台相关的代码都抽离出来,放在一个独立硬件相关层中实现并且定义好相关的调用接口,再在这个层之上开发内核的其它功能代码,就会方便得多,结构也会清晰很多。
- 操作系统的移植性也会大大增强,移植到不同的硬件平台时,就构造开发一个与之对应的硬件相关层。这就是分离硬件相关性的好处。
3.CPU工作模式
- 实模式,单道程序能掌控计算机所有的资源,仅支持 16 位地址空间,分段的内存模型,对指令不加限制地运行,对内存没有保护隔离作用。
- 保护模式,保护模式包含特权级,对指令及其访问的资源进行控制,对内存段与段之间的访问进行严格检查,没有权限的绝不放行,对中断的响应也要进行严格的权限检查,扩展了 CPU 寄存器位宽,使之能够寻址 32 位的内存地址空间和处理 32 位的数据,从而 CPU 的性能大大提高。
- 长模式,又名 AMD64 模式,在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器,使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力。长模式弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU。
4. 虚拟地址与真实地址
虚拟地址到物理地址的转换
虚拟地址到物理地址的转换: 软硬件结合的方式实现,它就是 MMU(内存管理单元)。MMU 可以接受软件给出的地址对应关系数据,进行地址转换。
上图中展示了 MMU 通过地址关系转换表,将 0x80000~0x84000 的虚拟地址空间转换成 0x10000~0x14000 的物理地址空间,而地址关系转换表本身则是放物理内存中的。
分页模型: 把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。根据软件配置不同,这个页的大小可以设置为 4KB、2MB、4MB、1GB
结合图片可以看出,一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放虚拟页地址对应的物理页地址就行了。
MMU
MMU 即内存管理单元,是用硬件电路逻辑实现的一个地址转换器件,它负责接受虚拟地址和地址关系转换表,以及输出物理地址。
上图中,程序代码中的虚拟地址,经过 CPU 的分段机制产生了线性地址,平坦模式和长模式下线性地址和虚拟地址是相等的。
MMU 页表
地址关系转换表——页表。它描述了虚拟地址到物理地址的转换关系,也可以说是虚拟页到物理页的映射关系,所以称为页表。
页表中并不存放虚拟地址和物理地址的对应关系,只存放物理页面的地址,MMU 以虚拟地址为索引去查表返回物理页面地址,而且页表是分级的,总体分为三个部分:一个顶级页目录,多个中级页目录,最后才是页表,逻辑结构图如下.
从上面可以看出,一个虚拟地址被分成从左至右四个位段:
- 第一个位段索引顶级页目录中一个项,该项指向一个中级页目录,
- 然后用第二个位段去索引中级页目录中的一个项,该项指向一个页目录,
- 再用第三个位段去索引页目录中的项,该项指向一个物理页地址,
- 最后用第四个位段作该物理页内的偏移去访问物理内存。这就是 MMU 的工作流程。
5.Cache与内存
内存: 是计算机中用于存储数据和程序的硬件设备。它允许计算机快速读取和写入数据,以及执行指令。内存通常被描述为随机访问存储器(RAM),因为它可以随时存取任意地址的数据。
Cache(缓存):是一种高速缓存存储器,位于计算机内部的CPU和主内存之间。它用于暂时存储处理器频繁访问的数据和指令,以减少对主内存的访问次数,从而提高系统性能。
Cache的工作流程如下:
-
CPU首先从L1 cache开始查询所需数据或指令。
-
如果在当前级别的cache中命中,则直接读取数据或指令;否则,查询下一个级别的cache。
-
依次查询L2和L3 cache,如果命中,则从相应的cache中读取数据或指令,并将其复制到更高一级的cache中供以后使用。
-
如果都未命中,则从主内存中读取数据或指令,并将其复制到所有级别的cache中。
-
写操作时,若数据存在于缓存中,则直接更新缓存中的数据,否则写入主内存。
-
当缓存满时,采用替换策略腾出空间,常见的策略包括LRU、FIFO和随机替换等。
Cache的结构
这是一颗最简单的双核心 CPU,它有三级 Cache,第一级 Cache 是指令和数据分开的,第二级 Cache 是独立于 CPU 核心的,第三级 Cache 是所有 CPU 核心共享的。
Cache缓存一致性问题
- 一个 CPU 核心中的指令 Cache 和数据 Cache 的一致性问题。
- 对于程序代码运行而言,指令都是经过指令 Cache,而指令中涉及到的数据则会经过数据 Cache。
- 修改了内存地址 A 这个位置的代码,这个时候通过储存的方式去写的地址 A,所以新的指令会进入数据 Cache。但是接下来去执行地址 A 处的指令的时候,指令 Cache 里面可能命中的是修改之前的指令。
- 所以,这个时候软件需要把数据 Cache 中的数据写入到内存中,然后让指令 Cache 无效,重新加载内存中的数据。
- 多个 CPU 核心各自的 2 级 Cache 的一致性问题。
- 为了解决这些问题,硬件工程师们开发了多种协议,典型的多核心 Cache 数据同步协议有 MESI 和 MOESI
6. 解决数据同步的四种方法
- 原子变量,在只有单个变量全局数据的情况下,这种变量非常实用,如全局计数器、状态标志变量等。我们利用了 CPU 的原子指令实现了一组操作原子变量的函数。
- 中断的控制。当要操作的数据很多的情况下,用原子变量就不适合了。但是我们发现在单核心的 CPU,同一时刻只有一个代码执行流,除了响应中断导致代码执行流切换,不会有其它条件会干扰全局数据的操作,所以我们只要在操作全局数据时关闭或者开启中断就行了,为此我们开发了控制中断的函数。
- 自旋锁。由于多核心的 CPU 出现,控制中断已经失效了,因为系统中同时有多个代码执行流,为了解决这个问题,我们开发了自旋锁,自旋锁要么一下子获取锁,要么循环等待最终获取锁。
- 信号量。如果长时间等待后才能获取数据,在这样的情况下,前面中断控制和自旋锁都不能很好地解决,于是我们开发了信号量。信号量由一套数据结构和函数组成,它能使获取数据的代码执行流进入睡眠,然后在相关条件满足时被唤醒,这样就能让 CPU 能有时间处理其它任务。所以信号量同时解决了三个问题:等待、互斥、唤醒。