进程地址空间

一、实地址模式

cpu复位或者加电网的时候是以是实模式启动的,在实模式下,内存寻址方式和8086相同,由16位段寄存器的内容乘以16(10H)当做段基地址,加上16位偏移地址形成20位的物理地址,最大寻址空间1MB,最大分段64KB。可以使用32位指令。32位的x86 CPU用做高速的8086。在实模式下,所有的段都是可以读、写和可执行的。

物理地址 = 基地址<<4 + 段内偏移

由于在实地址模式下物理内存会导致物理内存对于外界完全内存,这就产生了暴露问题。

暴露问题:
1)如果用户程序可以寻址内存的每个字节,它们就可以很容易地(故意地或 偶然地)破坏操作系统,从而使系统慢慢地停止运行。即使在只有一个用户进程 运行的情况下,这个问题也是存在的。
2)使用这种模型,想要同时(如果只有一个 CPU 就轮流执行)运行多个程序 是很困难的。在个人计算机上,同时打开几个程序是很常见的(一个文字处理器, 一个邮件程序,一个网络浏览器,其中一个当前正在工作,其余的在按下鼠标的 时候才会被激活)。在系统中没有对物理内存的抽象的情况下,很难做到上述情景,因此,我们需要其他办法。所以就出现了保护模式。

二、保护模式

是一种80286系列和之后的x86兼容cpu操作模式。保护模式有一些新的特色,设计用来增强系统稳定度,像是内存保护,分页 系统,以及硬件支援的 虚拟内存。

我们想要了解保护模式,先了解一些以下名词的概念。

1、基地址:基地址就是局部存储区的起点,比如你要从北京去广州,过了郑州 100 公里, 郑州就是基地址,100 是偏移。

2、逻辑地址:是指由程式产生的和段相关的偏移地址部分。例如,你在进行 C 语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址, 他是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在 Intel 实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu 不进行自动地址转换);

3、线性地址:是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑 地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如 果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用 分页机制,那么线性地址直接就是物理地址。Intel 80386 的线性地址空间容量 为 4G(2 的 32 次方即 32 根地址总线寻址)。

4、物理地址:是指出目前 CPU 外部地址总线上的寻址物理内存的地址信号,是 地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和 页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为 物理地址了。

程序是运行在虚拟地址空间下的,也就是说,在程序员编写程序时指令中出现的地址并不一定时这个程序在内存中运行时真正要访问的内存地址。这样做的目的就是为了能够让程序员在编程时不需要直接操作真实地址,因为当它在真实运行时,内存中各个程序的分布情况是不可能在你编写程序时就知道的。所以这个程序的这条指令到底要访问哪个内存单元是由操作系统来确定的。所以这就是一个从虚拟地址(virtual address)到真实主存中的物理地址(physical address)的转换。

三、进程地址空间的详细划分

进程地址空间_第1张图片

.text 段:指令段,存放的是指令代码,在程序中,我们把局部变量定义(局部变量的 定义是指令而不是数据)还有一些操作指令都存放在.text 中。它的属性是可执行可读不可 写。 .

data 段:数据段,里面存放的是初始化不为 0 的静态局部变量和全局变量。属性是可读可 写不可执行。注意:在 data 段上面有一个 rodata 段(rodata 段的位置在.o 文件时去观察它 是在.bss 段的下方),它的属性是只可读。所以当我们这样定义字符串时:char *p = “hello world”; *p = ‘a’;会报错的原因就是这里的”hello world”在 rodata 段存放,它只可读, 不可修改。 .

bss 段:数据段,里面存放的是未初始化或者初始化为 0 的全局变量和静态局部变量。属性 是可读可写不可执行,在 bss 段中的数据默认都会被修改为 0。

堆:堆内存是在 c 语言中用 malloc 申请或者在 c++中用 new 来申请的一端可能不连续的内存, 堆是由低地址向高地址申请的。

栈:栈中存放的是局部变量,特性是先进后出,并且它是由系统自动开辟以及释放。并且内存是连续的。

命令行参数:main 函数的参数列表。argc 参数个数 argv 参数内容 envp 环境变量

四、分页

我们知道应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理 器直接操作的确实物理内存。所以当应用程序访问一个虚拟地址时,首先必须将 虚拟地址转化为物理地址,然后处理器才能解析地址访问请求。这个转换工作需要通过查询页面才能完成,概括地讲,地址转换需要将虚拟地址分段,使每段虚地址都作为一个索引指向页表,而页表项则指向下一级别的页表或者指向最终的 物理页面。

1、页表:多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。 页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地 址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。

linux 中使用多级页表完成地址转换。多数体系结构中,搜索页表的 工作由硬件完成,下表描述了虚拟地址通过页表找到物理地址的过程

进程地址空间_第2张图片

两级映射: 页表含有 2^20(1MB)个表项,每个 4byte。如果作为一个表来存放的话,他们最多将 占用 4MB 的内存。因此为减少内存的占用量,保护模式(80386)使用两级表。高 20bit 线性地址到物理地址的转换被分为两步来进行,每步使用 10bit。

第一级表:页目录表,它被存放在一个 4kb 的页面中,具有 2^10(1024)个表项。这 些表项指向对应的二级表。线性地址的最高 10bit(位 32~22)用作一级表(页目录) 中的索引值来查找二级表中的对应表项。

第二级表:称为页表,它也具有 2^10(1024)个表项,使用线性地址中间 10bit(21~12) 作为表项索引值,以获取物理基地址。物理基地址和线性地址组合在一起得到物理地址。

2、32位所表示的含义

进程地址空间_第3张图片

10 位(页目录)+10 位(页表)+12()

线性地址的高十位(22~31)作为页目录表的索引,对应的索引指向页表,线性 地址的中间十位(12~21)作为指定的页目录表中的页表项的索引,对应表项所包含的 页码指定物理地址空间的一页。

例:0000 1000 00 00 0100 1001 0101 1011 0000 32 73 1456
高十位表示页目录表的索引,找到 32(页表物理地址)指向页表 中间十位表示页表的索引,找到 73(物理页地址)指向物理页目标字节 1456。

五、运行时数据结构

在 linux 操作系统中,每个进程都通过一个 task_struct 的结构体描叙,每个 进程的地址空间都通过一个 mm_struct 描叙,c 语言中的每个段空间都通过 vm_area_struct 表示。

当一个可执行程序映射到进程虚拟地址空间时,一组 vm_area_struct 数据结 构将被产生。每个 vm_area_struct 数据结构表示可执行印象的一部分;是可执行 代码,或是初始化的数据,以及未初始化的数据等。

linux 操作系统是通过 sys_exec 对可执行文件进行映射以及读取的,有如下 几步:

1)创建一组 vm_area_struct;
2)圈定一个虚拟用户空间,将其起始结束地址(elf 段中已设置好)保存到 vm_start 和 vm_end 中;
3)将磁盘 file 句柄保存在 vm_file 中;
4)将对应段在磁盘 file 中的偏移值(elf 段中已设置好)保存在 vm_pgoff 中;
5)将操作该磁盘 file 的磁盘操作函数保存在 vm_ops 中;

注意:这里没有对应的页目录表项创建页表,更不存在设置页表项了。

从上面我们可以知道,在进程创建的过程中,程序内容被映射到进程的虚拟 内存空间,为了让一个很大的程序在有限的物理内存空间运行,我们可以把这个 程序的开始部分先加载到物理内存空间运行,因为操作系统处理的是进程的虚拟 地址,如果在进行虚拟到物理地址的转换工程中,发现物理地址不存在时,这个 时候就会发生缺页异常(nopage),接着操作系统就会把磁盘上还没有加载到内存 中的数据加载到物理内存中,对应的进程页表进行更新。

2、一个进程是由一个进程控制块来描述的。可想而知,进程控制块中一定包含有进程 地址空间的描述结构体。这个描述进程地址空间的结构体就是 mm_struct。(位于 linux/sched.h 中)

1、mm_struct 结构体:此结构体是用于描述进程地址空间的,所以每个进程都有唯一的 mm_struct 结构体,即 唯一的进程地址空间;

1)Start_code :可执行代码的起始地址;
2)End_code:可执行代码的起始位置 ;
3)Start_data 已初始化的数据的起始位置 ;
4)End_data 已初始化的数据的结束地址;
5)mm_ users 域记录正在使用该地址的进程数目。
6)mmap域与mm_rb域描述的对象是相同的。用mmap作为链表可以方便遍历所有元素; 而 mm_rb 作为红黑树,适合于搜索指定的元素。
所有的 mm_ struct 结构体都通过自身的 enlist 域连接在一个双向链表中,该链表的首元 素是 init_mm 内存描述符,它代表 init 进程的地址空间。操作该链表的时候需要使用 moist_lock 锁来防止并发访问,该锁定义在文件 kernel/fork.c 中。内存描述符的总数存 放在 mmlist_nr 全局变量中,该变量也定义在文件 fork. c 中。在 mm_struct 结构体的第一个域 mmap 指向的 vm_area_struct 结构体中描述了虚 拟内存区域(VMA)。

2、vm_area_struct 结构体

1)每个内存描述符都对应于进程地址空间中的惟一区间。
2)vm_start 域指向区间的 首地址(最低地址), vm_end 域指向区间的尾地址(最高地址)之后的第一个字节, 也就是说, vm_star 是内存区间的开始地址(它本身在区间内),而 vm_end 是内 存区间的结束地址(它本身在区间外),因此 vm_end~ vm_start 的大小便是内存 区间的长度,内存区域的位置就在[ vm_start, vm_end]之中,注意在同一个地址 空间内的不同内存区间不能重叠。
3)vm_mm 域指向和 VMA 相关的 mm_ struct 结构体,注意每个 VMA 对其相关的 mm_ struct 结构体来说都是惟一的,所以即使两个独立的进程将同一个文件映 射到各自的地址空间,它们分别都会有一个 vm_ area_struct 结构体来标志自己 的内存区域,但是如果两个线程共享一个地址空间,那么它们也同时共享其中的 所有 vm_ area struct 结构体。

3、vm_operations_struct 结构体

1)vm_ area_struct 结构体中的 vm_ops 域指向与指定内存区域相关的操作函数 表,内核使用表中的方法操作 VMA。
2)操作函数表由 vm_operations_struct 结构体表示,定义在文件中。

你可能感兴趣的:(Linux)