Linux内核源代码情景分析之预备知识
阅读原文时间:2021年04月20日阅读:1

1.内核简介 1.1 Linux源代码组成

linux-2.4.20.8        -|arch CPU平台相关代码
      -|drivers 驱动程序代码
      -|fs 文件系统代码
      -|include 所有头文件
      -|init 内核main函数及初始化过程代码
      -|ipc 进程间通信实现代码                   
      -|kernel 进程管理和调度
      -|lib 通用的工具程序       
      -|mm 内存管理
      -|net 网络
      -|scripts 系统配置命令文件
      -|Documentation Linux内核文档
      -|Makefile 编译内核make文件
      -|README 安装和使用说明
      
2. Intel X86 2.1 X86系列
X86系列是指intel从16位微处理器8086开始的整个CPU系列,系列中每个产品与以前保持兼容性,主要有
8086,8088,80186,80286,80386,80486等等,从80386开始为32位处理器,简称i386(intel 80386).
2.2 实地址模式
常说的CPU的16位或者32位只的是CPU的“算数逻辑单元”的宽度,也就是参与计算的数据的宽度。数据总
线通常与ALU具有相同的宽度,最自然的地址总线与数据总线保持一样的宽度,也就是说一个16位的指针的地
址空间为64K(2的16次方)。
在16位CPU的时候,intel根据当时需要决定8086采用1M(2的20次方)的地址空间,于是地址总线宽度就被
确定为20位,但数据总线依然为16位,intel为解决这个问题增加了4个16位“段寄存器”:CS,DS,SS,ES,
分别用于存放指令,数据,堆栈,和其它。每个段寄存器对应于地址总线的高16位,每个访问内存的指令的内部
地址都是16位,在送上地址总线之前都在CPU内部自动的与某个段寄存器的地址相加,形成一个20位的地址,这
样就完成了从16地址到20位地址的转换,注意这里是将内部地址的高12位与寄存器地址想加,低4位不变。也就是
说寄存器存放的是“基地址”,这样一个进程总能访问从基地址开始的64K地址空间。
在当时,因为修改寄存器地址的指令不是特权指令,所以应用程序是可以修改寄存器内容的,所以这样一个进
程就可以访问任何想反问的位置,也就丝毫不受限制,没有内存管理可言,于是这种缺乏对内存空间保护的地址
模式就被称为“实地址模式”。
2.3 保护模式和段式内存管理
针对实地址模式缺陷,在32位80386CPU上,开始进行改进寻址模式,intel选择了在段寄存器基础上进行模式构
造并且保留段寄存器为16位,但是又添加了2个段寄存器FS和GS,为了实现保护模式,光有段寄存器来确定基地址
是不够的,还得有段地址的长度信息,访问权限控制等等。所以这里需要的是一个数据结构,而不是地址了。
所以基本思路为,当一条访问内存的指令发出一个内存地址时,CPU这样来归纳实际的地址:
(1)根据指令的性质来确定应该使用哪一个段寄存器,例如转移指令中的地址在代码段,而取数据的指令在数据
段。这一点跟实地址模式相同。
(2)根据段寄存器内容找到相应的“地址段描述结构”。
(3)从地址段描述结构得到基地址
(4)根据指令发出的地址作为位移,与描述段结构规定的段长度相比,看看是否越界。
(5)根据指令的性质和描述段结构中的访问权限来确定是否越权。
(6)根据指令发出的地址作为位移,与基地址相加而得到实际的物理地址。
具体实现:
首先,在80386 CPU中增加2个寄存器:一个是全局的段描述结构寄存器 GDTR(global descriptortable register),
另外一个是局部性段描述结构寄存器LDTR(local descriptor table register),分别用来指向存储在内存的一个
段描述结构数组,由于这2个寄存器是新增的,所以访问这2个寄存器的指令被设计为“特权指令”。
在此基础上,寄存器的高13位用作段描述表的下标如图1.1:

GDTR或LDTR的描述表指针和段寄存器的给出的下标结合在一起才可以读取出段描述表项,每个段描述表项为8个字节
如图1.2:

描述字段如图1.3:

struct 段描述表项{
unsigned int B24_B31:8; // 段基地址 bit 16~24
unsigned int G:1; // 段长度单位,0表示字节,1表示4KB
unsigned int D:1; // 存取方式,0=16位,1=31位
unsigned int A:1; // 可供系统软件使用
unsigned int unused:1;
unsigned int L16_L19:4; // 段长度 bit 16~19
unsigned int P:1; // 段描述项是否有效
unsigned int DPL:2; // DPL=00-01,权限
unsigned int S:1; // S=0,系统描述项,S=1代码或数据段描述项
unsigned int type:4; // 读写属性描述 
unsigned int B23_B16:8; // 段基地址 bit 24~31
unsigned int B15_B0:16;
unsigned int L15_L0:16; // 段长度 bit 0~16
};

特别指出当一个段寄存器的内容改变了,CPU就会将新的段描述项装入CPU中,CPU会检查P标志,如果位0,就会发生一次异常,
而相应的服务程序便会从磁盘交换区将这一段内容读入内存,再次重新设置基地址和P标志位1。相应的,可以将暂时不用的存储段
写入磁盘,将P设置为0。
在80386段式内存管理中,如果把每个基地址设置为0,而段长度设置为最大(4GB),此时物理地址和逻辑地址相同,CPU放到地址总线
上的地址就是指令携带的地址。这中方式就称为“平面地址”,linux正是采用这种结构。
为了起到保护段式内存管理的作用,装入和存储GDTR或者LDTR的指令都是特权指令,80386划分4个特权级别,0为最高级,3为最低级。
一般的程序的当前运行级别由其代码段的局部描述项中的dpl字段决定。而全局描述表则表示需要的级别。

struct 段寄存器结构 {
unsigned short set_idx:13;
unsigned short ti:1;
unsigned short rpl:2;
};

16位段寄存器的高13位为下标,剩下的3位,rpl表示要求的权限。当改变一个寄存器内容时,CPU会检查,当前程序的执行权限和段寄存器
所指定要求的权限均不低于所要访问的那一段内存的权限dpl。

3. 页式内存管理
3.1 以段式管理作为基础
段式管理是保护模式的实现,如果脱离段式管理单独实现也是页式内存管理,则保护机制要重新实现,因此注定了
页式管理要在段式管理上进行实现,而段式管理将段描述项中采用平面地址,也称地址为线性地址。
(??进程权限检查??)
3.2 实现
页式内存将线性地址空间划分为4K字节的页面,每个页面可以被映射任意一块4K字节的大小区间,连续的逻辑地址经过映射的地址不一定
连续。
线性地址结构:
struct 线性地址 {
unsigned int dir:10; // 用作页面表在目录表中的下标
unsigned int page:10; // 用作页面项在页面表的下标
unsigned int offset:12; // 在4K字节物理页面的偏移
};
如图1.4

类似GDTR和LDTR段描述表一样,intel增加了一个CR3寄存器作为当前页面目录的指针。则从线性地址到物理地址的映射过程为:
(1)从CR3取得页面目录的基地址。
(2)从线性地址的dir位取出页面表在页面目录的下标,取得页面表地址。
(3)从线性地址的page位取出页面项在页面表的下标,取的页面描述项。
(4)将页面项的基地址与线性地址的offset想加得到物理地址。
如图1.5

页面目录项结构

typedef 页面目录项 {
    unsigned  int  ptba:20;        // 页表基地址
    unsigned  int  avail:3;        // 系统程序使用
    unsigned  int  g:1;            // 全局性页面
    unsigned  int  ps:1;           // 页面大小 ps=1时页面大小位4M,线性地址的剩下的22位全部作为位移
    unsigned  int  reserved:1;     // 保留
    unsigned  int  a:1;            // 已被访问过
    unsigned  int  pcd:1;          // 关闭缓冲存储器
    unsigned  int  pwt:1;          // 用于缓冲存储器
    unsigned  int  u_s:1;          // 0,为系统权限,1为用户权限
    unsigned  int  r_w:1;          // 只读或可写
    unsigned  int  p:1;            // 为0表示相应的页面不在内存中
};

目录项结构如图1.6

页表项结构
typedef 页表项 {
    unsigned  int  ptba:20;        // 页表基地址
    unsigned  int  avail:3;        // 系统程序使用
    unsigned  int  g:1;            // 全局性页面
    unsigned  int  reserved:1;     // 保留
    unsigned  int  d:1;     // 页面已经被写过
    unsigned  int  a:1;            // 已被访问过
    unsigned  int  pcd:1;          // 关闭缓冲存储器
    unsigned  int  pwt:1;          // 用于缓冲存储器
    unsigned  int  u_s:1;          // 0,为系统权限,1为用户权限
    unsigned  int  r_w:1;          // 只读或可写
    unsigned  int  p:1;            // 为0表示相应的页面不在内存中
};

注意:当页面目录项或者页表项中的p标志位0时,就会产出缺页中断,可以用来与磁盘进行页面交换
3.5 拓展
i386的寄存器CR0,其最高位PG是页式管理的开关,从Pentium Pro开始,intel在另一个寄存器CR4又增加了一位PAE,
当PAE位为1时,地址总线宽度变成36位,增加了4位,地址空间为64G。