深入理解LInux内核——内存寻址

以下来自深入理解Linux内核的总结以及部分自己理解,如有错误,希望能在评论区指正,谢谢!

1、内存地址

  1. 逻辑地址:包含在机器语言指令中的一个操作数或者一条指令的地址,每条逻辑地址由一个段选择符和偏移量组成。
  2. 线性地址(虚拟地址):32位无符号整数,表示高达4GB地址。
  3. 物理地址:芯片级内存单元寻址。与懂微处理器的地址引脚发送到内存总线上的电信号对应。
    逻辑地址——>分段单元——>线性地址——>分页单元——>物理地址

比如CPU运行一个指令,发出该指令中携带的逻辑地址,如(MOV
A,0x99999999),这个逻辑地址带着段选择符(16位)和偏移量,然后通过分段单元得到一个线性地址0x12345678,接着通过分页单元得到0x58214796,最后这个地址发送到地址总线上向内存请求数据。

2、分段单元:逻辑地址转化为线性地址

  • 段寄存器:快速方便找到段选择符。6个寄存器,3个专门用途,包括CS代码段寄存器,SS栈段寄存器,DS数据段寄存器,其他三个做一般用途,可以访问任意段。
  • 段描述符:每个段由一个8字节的段描述符表示,描述段的特征,段描述符放在GDT或LDT中。每个描述符中包括段基址、段限长、段类型以及访问权限等。
 - 32位Base域,段基址
 - 20位Limit域,段长度 = Limit*(G?1:4096,单位字节
 - G,粒度值,表明段限长Limit递增1时,实际长度增加1B(G=0)还是4KB(G=1)- S,系统标志,0系统段,存储内核数据结构。1普通代码段或者数据段。
 - 4位Type,描述段的类型特征和存取权限

在这里的撒插入图片描述

  • 段选择符:16位长,其中13位的索引(说明GDT最多有2^13 -1个段描述符,减一是因为GDT第一项总设为0),指定GDT或LDT中相应段描述符的入口。TI(1位)标志指明段描述符在GDT还是在IDT中,两位RPL描述相应段装载到Cs寄存器时,CPU的当前特权级。
  • 为了加快逻辑地址到线性地址的转化,Intel提供了一种附加的非编程寄存器,当把短选择符装载到段寄存器时,就将段描述符装入对应的非编程的CPU寄存器中,这样针对那个段就不必取访问内存中的段描述符了。

换一个简单一点的模型,GDT就好像一个申请好的一个结构体数组空间,而段描述符就是这个数组之中的元素,它表示了某一段线性地址空间的特征。段选择符类比数组的下标,需要哪个元素,就指向哪个元素的下标。当然为了加快数组内元素的访问,我们添加一个临时变量(非编程寄存器)保存某个数组元素,当下次使用这个元素时直接从寄存器中读取。

在这里插入图片描述

2.1、逻辑地址到线性地址的转化

  1. 将段选择符加载到段寄存器中,检查其TI位,查看在所找的段描述符在GDT数组还是在IDT数组中。(GDT和IDT数组的地址可在GDTR或者LDTR寄存器中找到)
  2. 从段选择符的索引域计算段描述符的地址,索引域的值乘以8(段描述符长度8个字节,比如第二个段描述符,所以值为2,那么该段描述符的入口地址在GDT首地址偏移16个字节处)。结果放在GDTR或者LDTR中。
  3. 逻辑地址的偏移量与段描述符基地址Base域相加,得到线性地址。

2.2、Linux中的段

Linux认为,段和页都可以划分进程的空间,段可以给每个进程分配不同的线性空间,页可以把相同的线性地址空间映射到不同的物理空间。Linux更喜欢分页方式。
原因有二:

  1. 所有进程都是使用相同的段寄存器值,进程能共享同样的线性地址。
  2. 许多RISC处理器支持的段功能有限。

Linux用到几种段及其描述符:

段名基地址Base段限长LimitG粒度值特权级DPLType
内核代码段0x000000000xfffff10内核态0xa 可读可执行代码段
内核数据段0x000000000xfffff10内核态0x2 可读可写数据段
用户代码段0x000000000xfffff13用户态0xa 可读可执行代码段
用户数据段0x000000000xfffff13用户态0x2 可读可写数据段

可以看到这几个段的基地址都为0,段限长为4GB,所以LInux下,逻辑地址=线性地址
除了以上的几种段,还有任务状态段TSS,每个进程都有一个有一个,他的描述符存放在GDT中,用户态下不能访问。**局部描述符LDT段。**这两个段将在进程一章详细讲明。

3、分页单元:线性地址转化为物理地址

  1. 页框:假设RAM被划分成固定长度的页框,也叫物理页。
  2. 页:进程的一页数据块,可以被放在任何页框或磁盘中。
  3. 页表:把线性地址映射到物理地址的数据结构叫页表,放在主存中,启用分页单元前必须由内核进程初始化。

磁盘像锅,页框像碗,有很多碗,很多碗组成了内存。页像饭,每个进程都有一份饭或多份饭(数据),进程的饭可以盛到任何一个碗中,当进程不吃这份饭的时候就倒回锅里,而进程的这个碗就空了,其他的进程就可以将自己饭从锅里盛出来继续吃。

比喻有点low,大家原谅。
32位模式下的分页:
在这里插入图片描述
线性地址转换由两步完成:正在使用的页目录表的物理地址存放在处理器的CR3寄存器中,线性地址内的目录(高10位)决定了他在页目录表的哪一项,指向哪个页表,接下来,地址中的页表(中间10位)决定页表中的一项,此项中含有页所找页框的物理地址。偏移量域决定了本页框内的相对位置,偏移量域12位,故每一页4KB。

举个例子:CR3指向中国(页目录表),中国有很多省份(页目录表的很多项),线性空间高10位帮我们找其中的一个省份,接着中间10位帮我们找到众多市中的某一个市,最后12位帮我们找到市中的你。

3.1、Linux中的分页

Linux采用“三级分页”,这种方式在64位也可行。
在这里插入图片描述
Linux中,内核代码和数据结构存在一组保留的页框中,这些页框不能被动态的分配。Linux被放在第二个MB开始,也就是物理地址的0x00100000开始的存放的。因为物理内存的第一个MB由BIOS使用。

  1. 进程页表
    一个进程的线性地址空间被分为两部分:0x00000000~PAGE_OFFSET-1的线性地址,内核态和用户态均可访问,PAGE_OFFSET ~ 0xffffffff的线性地址,只有内核态进程能访问。PAGE_OFFSET通常为0xc0000000。
  2. 内核页表
    初始化内核自己页表。
    第一阶段,内核创建足以把自己装入RAM的4MB的地址空间。
    第二阶段,利用剩余的4MB中未用完的RAM建立分页页表。
    创建临时页表
    在进入保护模式之前(实模式),创建一个临时页表使得进入保护模式后,同样可以通过实模式的地址访问到前1MB和保护模式的地址访问前4MB,所以我们需要选择大的范围,也就是前4MB,就是将0x00000000~0x003fffff前4MB和0xc0000000 ~0xc03fffff 映射到 物理地址0x00000000 ~ 0x003fffffff。这两个映射实在内核编译过程中被静态的初始化。
    最终内核页表
    创建好临时页表时,我们也开启了CR0的允许分页位(PG),现在我们可以正常使用分页功能了。这个时候内核必须把从PAGE_OFFSET开始的线性地址转化为从0开始的物理地址。
    为什么要这样呢?我理解的是内核一开始执行在PAGE_OFFSET之后的线性空间,初始化内核时未建立用户态进程,所以不必访问PAGE_OFFSET之前的空间。而内核的代码和数据结构存储在物理空间的第1个页框,由于也要使用BIOS的数据,所以也需要访问第0个页框。那么PAGE_OFFSET之后的线性空间要映射到从0开始的物理空间。

如果以上的理解有错误,特别是临时页表和最终页表的部分,希望大家指正,谢谢!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值