最近一段时间面试了一些公司后发现, 自己对操作系统的一些概念还是理解的不够深刻, 之前看的是《操作系统概念 第六版》, 这次觉得应该加点难度, 正好就开始看这本《Linux 内核源代码情景分析》好了。
1.1 Linux内核简介
1. 微内核和宏内核的区别
内核中提供各种服务的成分和使用这些服务的进程之间形成了一种典型的Client/Server 关系, 由于有些服务提供者并不是一定非得留在内核中, 他们仅仅只是服务一部分对象, 这些服务提供者完全可以从内核中转移出来到进程的层次上, 这样就可以让内核得到精简。最终内核中仅剩下进程间通信的服务了。此时的内核, 我们称为微内核。 与之相对应的内核称为 宏内核。
2. linux 源码的安装位置
一般在 /usr/src/linux 目录下。
像我的ubuntu 是在如图所示的位置中, (终于知道源码的位置了, 想想有些小高兴=_=~~)

3. linux 版本号
linux 版本号 x.yy.zz, x 表示内核在涉及或者实现上的重大改变, yy 表示版本变迁, 偶数为稳定版, 奇数为开发版
4. gpl 协议
如果一个软件是在gnu源代码的基础上修改扩充来的, 那么这个软件的源代码也必须对使用者公开, 但可以收费
1.2 Intel X86 CPU 系列的寻址方式
1. 实模式
为处理地址总线和数据总线数目不匹配的问题(20 地址总线宽度, 16 数据总线宽度), intel 在8086cpu 中提出了 用段地址 + 偏移的寻址方式。在这种模式下, 内存是完全暴露的, 很容易被攻击
2. 保护模式
保护模式还是建立在段寄存器的基础上的, 通过添加两个新的段寄存器 FS, GS实现。
他的基本思路是, 在保护模式下改变段寄存器的功能, 使其从一个单纯的基地址(变相的基地址)编程一个指针。
如果说实模式是直接通过段寄存器获取目标位置的地址, 保护模式实际上是在这个过程中间加了一层间接性, 他需要通过访问相应的 地址段描述结构 才能获取基地址
为实现这个效果80386cpu 增加了两个寄存器, GDTR 全局性的段描述表寄存器, 和 LDTR 局部性的段描述表寄存器, 访问这两个寄存器的指令被设计为特权指令。
同时, 段寄存器的定义也被相应修改了
只有将段寄存器中的低 3 位屏蔽后, 与GDTR 或者 LDTR 中的基地址相加 才能得到描述表项的起始地址
3. intel 的平面地址
这其实是将段寄存器设置为0 的一种特例情形
4. 80386 虚存管理
当需要访问一段内存的时候, 先访问到他的描述表项, cpu 会检查描述表项中的 p 标志位, (p 标记当前描述表项所指示的内容是否在内存中), 如果 p 为0, 内容不在内存中, cpu 执行一次中断, 将相应内容载入内存空间
5. 80386 权限管理
当需要改变一个段寄存器的内容的时候, cpu 会检查, 确保该段程序的当前执行权限和段寄存器所指定的要求的权限均不低于所要访问的那一段内存的权限dpl
2.1 i386 的页式内存管理机制
1. 段式管理和页式管理
页式管理相对段式管理更加先进。 段式存储管理机制的灵活性和效率都比较差。 一方面是 ‘ 段’ 是可变长度的, 这就给盘区的交换操作带来不便, 另一方面, 如果未来增加灵活性而将进程的空间划分成很多的小段的时候, 就需要在程序中频繁的改变段寄存器的内容。 因而, 段式管理相对而言是比较差的。
2. 80386 的页式管理
由于80386 是通过段式存储来实现保护模式的, 因而, 80386 的页式管理就必须建立在段式存储管理的基础上。
ie, 80386中, 页式存储管理是通过在段式存储管理所映射的地址上再加一层地址映射得到的。
线性内存, 由段式存储管理所映射得到的地址。
总结一下: 段式存储管理先将逻辑地址映射成线性地址, 然后再由页式存储管理将线性地址映射成物理地址, 或者, 当不使用页式存储管理的时候, 就将线性地址直接用作物理地址。
3. 线性地址到物理地址的映射过程
- 从CR3取得页面目录的基地址
- 以线性地址中的dir 位段为下标, 在目录中获取相应页面表的基地址
- 以线性地址中的page位段为下标, 找到相应的页面描述项
- 将页面描述向中给出的页面基地址与线性地址中的offset 位段 相加得到物理地址
Linux 内核源代码情景分析 chap 1 预备知识1.1 Linux内核简介1.2 Intel X86 CPU 系列的寻址方式2.1 i386 的页式内存管理机制1.4 Linux 内核中的C语言代码1.5 Linux 内核源码中的汇编语言 Linux 内核源代码情景分析 chap 1 预备知识1.1 Linux内核简介1.2 Intel X86 CPU 系列的寻址方式2.1 i386 的页式内存管理机制1.4 Linux 内核中的C语言代码1.5 Linux 内核源码中的汇编语言
4. 为什么使用两个层次结构呢
出于空间效率的考虑, 如果只是单个层次的话, 由于很难有程序会用到4G的全部空间, 大部分情况下表项是空的, 造成浪费。
然而使用两个层次的结构, 页表可以根据需要来设定,可以不必为空的目录表项设置相应的页表, 节省空间
32bit cpu 页表项最多1024 个, ie, 一个页面 4KB, 而 64bit 中 一个页面时 8KB
1.4 Linux 内核中的C语言代码
1. 单次执行宏
#define DUMP_WRITE(addr, nr) do {memcpy(bufp, addr, nr); bufp += nr;} while ()
2. 空操作宏
#define prepare_to_switch() do {} while ()
3. Linux 内核中的双链队列管理
底层是一个list 链表
struct list_head{
struct list_head * next, * prev;
}
通过将这个部分加入到需要管理的对象中, 可以实现对相应对象的管理
typedef struct page{
struct list_head list;
.........
struct page * next_hash;
.........
struct list_head lru;
.........
} mem_map_t;
那么如何获取一个page的地址呢?
#define memlist_entry list_entry
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *))->member)))
而page地址的获取方式如下:
page = memlist_entry(curr, struct page, list)
经过C 预处理器的文字替换之后得到:
ie, 通俗的来讲:
已知 结构体中 list 项的地址信息, 只要减去这个list项在这个结构体中的偏移, 就可以得到这个结构体的地址了
1.5 Linux 内核源码中的汇编语言
1. 使用汇编的重要性
- 有些需要和硬件打交道的代码, C语言没有相应相应的指令
- CPU 的一些特殊指令也没有C 语言部分
- 有些函数,程序, 过程会被频繁的调用, 时间效率非常重要, 需要使用高效率的汇编
- 有些空间效率有要求的, 也必须使用汇编
2. GUN 的386 汇编语言
GNU 中采用的是 AT&T 的汇编语言, 这和intel 的汇编语言的差别是相当大的
3. 嵌入C代码中的386汇编语言程序段
- 这里的代码都是AT&T 风格的, 有不明白的需要对照 intel 的才能看懂
-
在C 中插入汇编比较复杂, 会涉及如何分配寄存器, 以及如何与C 代码中的变量相结合的问题。
一般的代码片表示形式:
指令部 : 输出部 : 输入部 : 损坏部