Linux进程地址空间(包括内核源码的简述)

什么是进程地址空间

简单来讲, 进程的地址空间是进程可用于寻址内存的地址集合, 包括进程的物理地址空间和虚拟地址空间.
操作系统有虚拟内存和物理内存的概念. 但是在很久之前, 操作系统只有物理内存的概念, 程序寻址都是用的物理地址, 寻址空间的大小取决于 cpu 地址线条数, 在 32 位的操作系统上, 寻址的范围是固定的(最多 4G). 也就是说, 每次运行一个程序, 都会给进程分配 4G 的物理内存, 这样就带来了很多麻烦

1. 内存浪费:  每个进程固定分配 4G 物理内存, 对内存要求太高; 
并且物理内存的分配是连续的且随机的, 不能充分利用物理内存, 进而造成内存浪费
2. 效率低下: 内存得不到充分利用, 就会产生过多的进程等待, 等待其他进程
结束后再被载入内存, 操作频繁, 效率低下
3.安全性低: 进程中的指令都是直接访问物理内存的, 这就可能影响其他进程
甚至破坏内核空间, 安全性低

虚拟内存

什么是虚拟内存
简单来说, 虚拟内存其实就是暂时将一部分硬盘空间来充当内存在使用, 使得进程认为拥有一段连续的内存空间, 而实际上它通常被分割成多个物理内存碎片, 还有一部分暂时存储在外部的磁盘存储器上(在需要时进行数据交换) -------- 像 Windows 的"虚拟内存", Linux 的"交换空间"…
虚拟内存空间的组成
虚拟内存空间包括内核空间和用户空间, 在 32 位的 Linux 操作系统上, 1G(高地址)是内核空间, 3G 是用户空间
在这里插入图片描述
我们看下列代码
在这里插入图片描述
通过 size main.out 查看程序段组成
在这里插入图片描述
代码段(text): 存放代码
数据段(data + bss): 存放初始化和未初始化的全局变量和静态变量
堆(Heap): 存放动态内存分配
栈(Stack): 存放函数参数和局部变量
栈和堆会不会碰头
虽然栈是自上而下, 堆是自下而上; 但是他们之间的间隔很大, 一般来说不会碰头, 我们可以写一个程序:

#include <stdio.h>
#include <stdlib.h>    
int bss_var0;    
int bss_var1;    
int data_var0 = 1;    
int main() {    
  printf("-----------------------\n");    
  printf("Stack location:\n");    
  int stack_var0 = 1;    
  printf("\tstack_var0 = %p\n", &stack_var0);    
  int stack_var1 = 1;    
  printf("\tstack_var1 = %p\n", &stack_var1);    
  printf("-----------------------\n");    
  printf("Heap location:\n");    
  int* heap_var0 = (int*)malloc(sizeof(int));    
  *heap_var0 = 1;    
  printf("\theap_var0 = %p\n", heap_var0);    
  int* heap_var1 = (int*)malloc(sizeof(int));    
  *heap_var1 = 1;    
  printf("\theap_var1 = %p\n", heap_var1);    
  printf("-----------------------\n");    
  printf("BSS location:\n");    
  printf("\tbss_var0 = %p\n", &bss_var0);    
  printf("\tbss_var1 = %p\n", &bss_var1);    
  printf("-----------------------\n");    
  printf("Data location:\n");    
  printf("\tdata_var0 = %p\n", &data_var0);    
  static int data_var1 = 1;    
  printf("\tdata_var1 = %p\n", &data_var1);    
  printf("-----------------------\n");    
  printf("Text location:\n");    
  printf("\ttest = %p\n", main);    
  return 0;                                                                                                                             
}    

在这里插入图片描述
以上的地址都是虚拟地址, 我们可以查看一下 Linux 下的 ELF 格式(UNIX下的可执行文件, 目标文件, 核心转储文件, 共享库文件格式)的文件:
在这里插入图片描述
我们在编译完毕的时候, 程序的入口地址就已经确定了.
代码共享, 数据独有
创建一个子进程, 来描述代码共享:

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
int main() {    
  int var = 1;    
  pid_t pid = fork();    
  if (pid < 0) {                                                                                                                        
    perror("fork error");    
    exit(-1);    
  } else if (pid == 0) {    
    printf("child &var = %p\n", &var);    
    printf("child var = %d\n", var);    
  } else {    
    printf("parent &var = %p\n", &var);    
    printf("parent var = %d\n", var);    
  }    
  return 0;    
}    

在这里插入图片描述

这就是代码共享, 子进程复制父进程的pcb, 子进程和父进程的虚拟地址内存相同, 数据的分配地址相同(但是pid不同), 子进程和父进程共用同一段代码, 这样的话子进程可以帮助父进程分摊压力, 但是这就是子进程存在的意义吗? 或者说, 两个进程的物理内存相同吗?
用修改以上代码来描述数据独有:
在这里插入图片描述
在这里插入图片描述

这就是数据独有, 在子进程中修改变量的值, 但是不影响父进程中变量的值, 并且数据的虚拟地址空间依然相同, 这其实不难想象, 虚拟地址相同, 但是物理地址不相同; 而且这里还涉及到了一个关于写时拷贝技术的知识:
写时拷贝技术
简单的来讲, 写时拷贝技术就是内容改变时的一份拷贝; 进程的各段内容发生改变的时候, 才会将父进程的内容拷贝一份给子进程, 这就意味着在进程各段内容发生改变之前, 父进程和子进程共享同一块物理内存, 只有当某段内容发生改变时, 才会将这段的内容拷贝一份给子进程, 然后映射到物理内存的新位置.

虚拟内存的工作

进程控制块(task_struct)
一个进程是由进程控制块来描述的:
在这里插入图片描述

内存描述符(mm_struct)
虚拟内存是由内存描述符来描述的, 在 task_struct 中找到 mm_struct:
在这里插入图片描述

mm_struct 是用来描述进程地址空间的, 每一个进程都有唯一的进程地址空间, 即每个进程拥有唯一的 mm_struct 结构体
在这里插入图片描述
mmap 是把磁盘文件和逻辑地址映射到虚拟地址, 以及把虚拟地址映射到物理地址, mmap 指向的 vm_area_struct 结构体描述了虚拟内存.
mmap和mm_rb描述的对象是相同的, 之所以用两种方式来组织对象, 是因为用mmap作为链表可以方便遍历所有元素;而mm_rb作为红黑树,适合于搜索指定的元素。
在这里插入图片描述

mm_users 是指有多少个线程(进程执行的基本单元)在共享该进程地址空间;
mm_count 是指是否有线程使用该进程地址空间, 如果有则为 1, 否则为 0 并且此结构体被撤销.
mmlist 是连接所有 mm_struct 结构体的双向链表, 表头是 init 进程的进程地址空间.
(虚存管理)vm_area_struct:
它是虚存管理最基本的管理单元, 它以双向链表的形式描述了一块连续的, 具有相同属性的虚拟内存, 该虚拟内存的大小是对应物理内存的整数倍.
在这里插入图片描述
vm_mm 指向了该虚拟内存区域的进程地址空间
vm_start 是进程地址空间的起始地址;
vm_end 是进程地址空间的结束地址;
虚拟地址的范围: [vm_start,vm_end)
vm_flags 保存了对虚拟空间的访问权限
在这里插入图片描述
vm_file: 如果 vm_area_struct 描述的是一个文件映射的虚拟空间, 那么 vm_file 便指向被映射文件的 file 结构, vm_pgoff 是该虚拟内存起始地址在 vm_file 文件里面的偏移量(单位为物理页面).
vm_ops: 当进程创建 vm_area_struct 之后, 只能说明进程可以访问这个虚拟空间, 但有可能还没有分配相应的物理页表来建立好页面映射. 在这种情况下, 访问内存会产生缺页异常, 这时需要通过 vm_ops -> nopage 所指向的函数将产生缺页异常的地址对应的文件数据读取出来.
虚拟内存的工作流程
1.进程创建
2.建立虚拟内存和磁盘文件的映射:
通过 mm_struct 将可执行程序映射到虚拟内存空间时, 一组 vm_area_struct 数据段将被产生

1、创建一组vm_area_struct;

2、圈定一个虚拟用户空间,将其起始结束地址(elf段中已设置好)保存到vm_start和vm_end中;

3、将磁盘file句柄保存在vm_file中;

4、将对应段在磁盘file中的偏移值(elf段中已设置好)保存在vm_pgoff中;

5、将操作该磁盘file的磁盘操作函数保存在vm_ops中;

在这里插入图片描述
3.建立虚拟内存和物理内存的映射(MMU):
当运行到对应的数据段时, 进程去寻找页表, 如果页表中没有映射到物理内存的信息, 则产生缺页异常(有效位为 0), 然后分配物理内存页并将磁盘中的指定文件加载到物理内存之后(有效位为 1), 重新访问

1.cpu依据CR3(current->pgd)找到0x08000011地址对应的pgd[i],由于该pgd[i]内容保持为初始化状态即为0,导致cpu异常。

2.do_page_fault被调用,在该函数中,为pgd[i]在内存中分配一个页表,并让该表项指向它

3.为pte[j]分配一个真正的物理内存页面,依据vm_area_struct中的vm_file、vm_pgoff和vm_ops,调用filemap_nopage将磁盘file中vm_pgoff偏移处的内容读入到该物理页面中

在这里插入图片描述
物理内存满了怎么办
淘汰物理内存中的其他页, 来为新页腾出空间

struct mem_map
{ // 物理页描述

1、本页使用计数,当该页被许多进程共享时计数将大于1

2、age描叙本页的年龄,用来判断该页是否为淘汰或交换的好候选

3、map_nr描叙物理页的页帧号

}

linux使用"最近最少使用(Least Recently Used ,LRU)“页面调度技巧来公平地选择哪个页可以从系统中删除。这种设计系统中每个页都有一个"年龄”,年龄随页面被访问而改变。页面被访问越多它越年轻;被访问越少越老。年老的页是用于交换的最佳候选页。

总结:

我们理解进程地址空间, 其实就是理解进程和线程的一个工作流程. 
当一个程序从静态到动态的这么一个过程中, 系统先通过在内存中产
生进程核心 PCB(task_struct), 然后通过 task_struct 中的
mm_struct 来在建立磁盘到虚拟地址空间的映射, 然后再通过 mm_struct
中的 vm_area_struct 来建立虚拟地址空间到物理地址空间的映射.
这样其实就通过进程地址空间来理解了以下线程的工作流程.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值