进程地址空间
嵌入式学习Day5
1.什么是进程地址空间
进程地址空间就是内核给每个进程画出来的一张“虚拟内存地图”,进程看到的是自己独占的一大片连续地址,比如从低地址到高地址一整条空间;但实际上这些地址不一定真的连续放在物理内存里,很多甚至暂时还没分配物理页。中间靠 页表 和 MMU 做转换。
它解决了什么问题:
- 隔离:进程 A 不能随便访问进程 B 的内存
- 统一:程序总觉得自己在用一整块连续内存
- 灵活:代码、堆、栈、共享库、文件映射都能方便组织
- 按需分配:不是一上来就把所有物理内存都分好
2.进程空间中有什么
高地址
+---------------------+
| 栈 stack | //向下增长,保存函数调用现场,局部变量,保存的寄存器等
+---------------------+
| 内存映射区 mmap | //动态链接库 libc.so、ld.so|mmap() 映射的文件|共享内存|大块内存分配
+---------------------+
| 堆 heap | //向上增长,用来做动态内存分配,比如 malloc/new 申请的内存通常来自堆。
+---------------------+
| .data / .bss | //bss 段:未初始化或初始化为 0 的全局/静态变量 data 段:已初始化的全局/静态变量
+---------------------+
| 代码段 text | //放程序指令,也就是机器码。
+---------------------+
低地址
3.内存描述符
3.1 mm_struct
mm_struct是一个进程用户地址空间的总描述符,管整个进程的:
- 虚拟内存区域|页表|代码段/数据段/堆/栈边界|内存统计|地址空间相关同步
内核要表示一个进程,主要用 task_struct。而 task_struct 里面会有一个指针,指向这个进程的 mm_struct。
mm_struct 下面挂着一个个 vm_area_struct,vm_area_struct对应每一段连续的虚拟内存区域
mm_struct
+-- vm_area_struct 代码段
+-- vm_area_struct 数据段
+-- vm_area_struct 堆
+-- vm_area_struct 栈
+-- vm_area_struct 某个共享库映射
+-- vm_area_struct 某个 mmap 文件映射
mm_struct代表了页表上下文,它“拥有并代表”这一整套地址空间所使用的页表。页表是“具体映射”,而之前说的VMA是虚拟地址
3.1.1 VMA 管理相关相关字段
- mmap代表“这进程有哪些虚拟内存区域”的入口,也就是说,通过 mm_struct 里的这个入口,内核可以找到:
- 代码段 VMA|数据段 VMA|堆 VMA|栈 VMA| mmap() 建出来的 VMA|动态库映射
- map_count 当前 mm_struct 里一共有多少个 VMA
- mm_rb 用来组织所有 VMA 的红黑树根,红黑树可以VMA 查找
3.1.2 页表相关字段
- pgd 当前地址空间顶层页表的入口
pgd_t *pgd;
3.1.3 地址边界相关:代码段、数据段、堆、栈在哪里
start_code/end_code————表示代码段的起止地址start_data/end_data————表示数据段的起止地址。start_brk/brkstart_brk:堆的起始位置brk:当前堆顶位置
start_stack————表示用户栈的起始位置
3.1.4 引用计数相关
mm_users有多少“用户”正在使用这套地址空间,因为线程通常共享同一个mm_struct,所以mm_users能反映:“还有多少执行实体在用这个地址空间”mm_count更像“对象生命周期的引用计数”,这个 mm_struct 对象本身还能不能释放,本身还有多少地方拿着它的引用,不允许销毁
3.1.5 并发保护
mmap_lock- 当内核要查 VMA 时,可能要读锁
- 当内核要插入/删除/修改 VMA 时,可能要写锁
3.1.6 内存统计相关
total_vm该进程总共映射了多少页的虚拟内存rss真正驻留在物理内存中的页数
3.2 线程与进程创建时的区别
创建进程时调用fork(),利用copy_mm() 复制父进程的内存描述符 子进程中的 mm_struct 实际是通过 allocate_mm() 从 mm_cachep slab 缓存中分配得到的。 也就是说:
新 mm_struct 这个壳子,本身是内核分配出来的。
然后才会往里面填:
- VMA 信息
- 页表根
- 统计字段
- 引用计数
- 地址边界等
如果父进程希望和子进程共享地址空间,可以在调用 clone() 时设置 CLONE_VM 标志。我们把这样的“进程”称作线程。也就是说线程和进程共享一个mm_struct
if (clone_flags & CLONE_VM) {
atomic_inc(¤t->mm->mm_users);
tsk->mm = current->mm;
}
当线程创建成功时就是把之前我说的字段,mm_users自增
总结: 在 Linux 中,进程的地址空间由 mm_struct 描述,并通过 task_struct 中的 mm 指针与进程关联。普通 fork 时,子进程通常会建立自己的 mm_struct;而在 clone() 指定 CLONE_VM 时,父子任务共享同一个 mm_struct,此时内核只需让子任务的 mm 指向父任务的 mm,并增加 mm_users 计数。这样的任务通常被称为线程。
3.2.1 为什么“每个线程有自己的栈”
既然共享同一个 mm_struct,那栈为什么还不是完全同一个? 答案是:
- 空间是同一个
- 但同一地址空间里可以有多块不同的栈区域 也就是说:
- 线程 A 的用户栈,是这个 mm_struct 里的某一块 VMA
- 线程 B 的用户栈,是同一个 mm_struct 里的另一块 VMA 所以: 线程共享地址空间,但每个线程在这个共享地址空间里有各自独立的栈区域。 这不矛盾。
3.3 内核线程没有自己的用户地址空间,是怎么运行的
mm == NULL
active_mm != NULL
active_mm 通常借自上一个用户进程
内核线程(Kernel Thread)确实没有属于自己的用户地址空间(在 Linux 的进程描述符 task_struct 中,它们的 mm 指针为空 NULL)。 核心在于共享的内核地址空间以及一种非常巧妙的“借用”页表机制。
