linux进程内存地址范围,Linux 内存地址映射

66b52468c121889b900d4956032f1009.png

8种机械键盘轴体对比

本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

Linux 采用页式内存管理.优点:页面固定大小, 便于管理.

灵活度高, 在将内存交换到磁盘上时, 按内存页进行交换.

页式内存管理有内部碎片, 无外部碎片.

即使 i386 CPU 在硬件上限制必须先段式再页式的映射方式, Linux 也是避开了段式映射, 在 Linux 内存映射时(除用来模拟80286的 VM86模式), 段式映射的基址总是 0, 所以线性地址与虚拟地址总是一致, 相当于没有使用段式内存映射. 当然 i386 存在段式映射是有历史原因的, 对于其他 CPU 来说就不存在这一层了.

段式映射

在获取到一个虚拟地址后, 首先确定其属于进程的哪一段内存, 然后获取相应内存段的段寄存器中的值. Linux 在实现时没有区分除代码段以外的其他各内存段, 统一设置为数据段.1

2

3

4

5

6

7

8

9

10

11

12

13

14// 创建进程时设置对应进程的段寄存器和栈顶,栈底寄存器

// Linux 在实现时没有区分除代码段以外的其他各内存段, 统一设置为数据段.

__asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));

set_fs(USER_DS);

regs->xds = __USER_DS;

regs->xes = __USER_DS;

regs->xss = __USER_DS;

regs->xcs = __USER_CS;

regs->eip = new_eip;

regs->esp = new_esp;

} while (0)1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16// include/asm-i386/segment.h

#ifndef _ASM_SEGMENT_H

#define _ASM_SEGMENT_H

// 内核代码段寄存器初始值

#define __KERNEL_CS0x10

// 内核数据段寄存器初始值

#define __KERNEL_DS0x18

// 用户进程代码段寄存器初始值

#define __USER_CS0x23

// 用户进程数据段寄存器初始值

#define __USER_DS0x2B

#endif

这里需要介绍一下段寄存器, RPL 为所要求的特权级别, 共分为 4 级, 00 为最高权限, 11 为最低权限, Linux 在实现时只使用了这两个级别. TI 为 0 时表示使用 GDT, 为 1 时表示使用 LDT. Index 表示对应段描述符表的下标.

%E6%AE%B5%E5%AF%84%E5%AD%98%E5%99%A8%E6%A0%BC%E5%BC%8F%E5%AE%9A%E4%B9%89.png

通过之前介绍, 解析 Linux 段寄存器初始值:

Linux%E6%AE%B5%E5%AF%84%E5%AD%98%E5%99%A8%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%90_1.png

Linux%E6%AE%B5%E5%AF%84%E5%AD%98%E5%99%A8%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%90_2.png

TI 值均为 0, 说明都是 GDT, 没有使用 LDT, 在 Linux 中 LDT 只在 VM86模式 下使用. RPL 内核使用 00 级, 普通进程使用 11 级. index 为固定值.

在 Linux 实现时会将 GDT 初始化为固定的值, GDT 第一项(下标为 0)会初始化为 0x0000000000000000, 这是为了防止加电后段寄存器未经初始化就进入保护模式并使用 GDT. 第 2 ~ 5 项对应之前四中段寄存器.1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22// arch/i386/kernel/head.S

/*

* This contains typically 140 quadwords, depending on NR_CPUS.

*

* 这通常包含140个四字(64位), 取决于NR_CPUS.

*

* NOTE! Make sure the gdt descriptor in head.S matches this if you

* change anything.

*

* 注意! 如果你改变任何东西, 请确保 head.S 中的 gdt 描述符与此匹配.

*/

// 初始化 GDT

ENTRY(gdt_table)

.quad 0x0000000000000000/* NULL descriptor */

.quad 0x0000000000000000/* not used */

.quad 0x00cf9a000000ffff/* 0x10 kernel 4GB code at 0x00000000, __KERNEL_CS 指向的空间 */

.quad 0x00cf92000000ffff/* 0x18 kernel 4GB data at 0x00000000, __KERNEL_DS 指向的空间 */

.quad 0x00cffa000000ffff/* 0x23 user 4GB code at 0x00000000, __USER_CS 指向的空间 */

.quad 0x00cff2000000ffff/* 0x2b user 4GB data at 0x00000000, __USER_DS 指向的空间 */

.quad 0x0000000000000000/* not used */

.quad 0x0000000000000000/* not used */

将 2 ~ 5 项按照二进制展开:

%E5%9B%9B%E7%A7%8D%E6%AE%B5%E5%AF%84%E5%AD%98%E5%99%A8%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%B1%95%E5%BC%80.png

再按照段描述符表项内容进行分析:

%E6%AE%B5%E6%8F%8F%E8%BF%B0%E9%A1%B9%E5%AE%9A%E4%B9%89.png相同:B0~B15, B16~B31 都是 0. 段基址全为 0.

L0~L15, L16~L19 都是 1. 段长度全为 0xfffff.

G 位都是 1. 段长度单位均为 4KB.

D 位都是 1. 对四个段的访问指令都是 32 位指令.

P 位都是 1. 四个段都在内存中.

结论: 每个段都是从 0 地址开始的整个 4G 虚拟空间, 虚拟地址到线性地址的映射保持原值不变.不同:__KERNEL_CS : DPL = 0, 表示 0 级; S 位为 1, 表示代码段或数据段; type 为 1010, 表示代码段, 可读, 可执行, 尚未受到访问.

__KERNEL_DS : DPL = 0, 表示 0 级; S 位为 1, 表示代码段或数据段; type 为 0010, 表示数据段, 可读, 可执行, 尚未受到访问.

__USER_CS : DPL = 3, 表示 3 级; S 位为 1, 表示代码段或数据段; type 为 1010, 表示代码段, 可读, 可执行, 尚未受到访问.

__USER_DS : DPL = 3, 表示 3 级; S 位为 1, 表示代码段或数据段; type 为 0010, 表示数据段, 可读, 可执行, 尚未受到访问.

页式映射

每个进程都有其自身的页目录 PGD, 指向这个目录的指针保存在每个进程的 mm_struct 数据结构(数据结构中介绍)中. 每当一个进程运行时, 内核都要为其设置控制寄存器 CR3, 而 MMU 的硬件总是从 CR3 中取得指向当前页目录的指针. CPU 在执行过程中使用的是虚拟地址, 而 MMU 硬件在进行映射时使用的是物理地址, 这其中的计算则是通过 基本框架 中介绍的 __ps 进行计算的. 这里存在一个问题: 使用不同的页目录, 不会使程序不能连续执行吗? 答案是不会. 因为所有进程的 PGD 对系统空间的 1G 空间映射完全相同.

在获取到 PGD 地址后获取线性地址的最高 10 位作为下标, 在这个 PGD 表项中保存着 PT 的地址. 然后将线性地址接下来的 10 位作为 PT 的下标, 获取到对应的物理页面地址. 线性地址的最低 12 位为物理页面中的偏移量, 就是线性地址在物理内存中的地址.

在页面映射的过程中, i386 CPU 需要访问三次内存. 第一次访问 PGD, 第二次访问 PT, 第三次才是真正的目标. 所以高效的虚拟内存有赖于两方面:高速缓存(cache)的实现. 除第一次访问需要这三步, 之后就可以在高速缓存中找到.

这个过程由硬件实现, 速度很快.

本文整理自《Linux内核源代码情景分析》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值