Linux虚拟内存组织结构浅析(一)

本文介绍了Linux内核如何管理和描述物理内存。重点讲述了UMA和NUMA两种内存访问方式,并详细解释了Linux内核如何通过节点、区和page结构来组织和管理物理内存。

众所周知,linux内核支持绝大多数体系结构,因此linux内核必须采取一种与具体体系结构无关的方法来描述物理内存的组织结构,这个问题就是本系列文章要讨论的话题。

 

要理解linux虚拟内存在逻辑上的组织结构,我们首先要明白两个概念:UMAUniform Memory Access)、NUMANon Uniform Memory Access)。UMA指一致性内存访问,这是单CPU机器常用的体系结构,在这种结构下,CPU访问系统内存的任何存储位置的代价都是一样的;而NUMA是指非一致性内存访问,常用于多CPU机器,在这种体系结构中,不同内存相对于不同的CPU而言所处的位置不一样,最典型的就是每个CPU都有自己的本地内存(Local Memory),不同CPU之间通过总线连接起来,如图1所示。在这种结构中,CPU访问本地内存的代价比访问远端的内存代价要小。

 

 

为了支持NUMALinux将物理内存划分成不同的节点(node),节点用结构体pg_data_t表示,以上图为例,图中每个CPU的本地物理内存都称为一个节点;即使在UMA结构中也有节点的概念,此时系统中就只有一个节点;系统中的多个节点被连接起来保存在一个称为pgdat_list的链表上。每个节点又被划分成不同的区(zone),节点是通过其结构体内的数组node_zones来跟踪节点内的区的。区是指一个节点内一段连续的物理内存范围。Linux中主要有3个区:ZONE_DMAZONE_NORMALZONE_HIGHMEM,它们的划分如下:ZONE_DMA位于物理内存开始的一段区域内,主要用来供一些ISA设备使用;ZONE_NORMAL位于ZONE_DMA后面,这个区域被内核直接映射到线性地址的高端部分;ZONE_HIGHMEM指系统中剩下的物理内存,这个区域不能直接被内核映射。为了清晰起见,我们可以看看在x86平台上的区是如何划分的:

                     ZONE_HIGHMEM —— 物理内存起始16M

                     ZONE_NORMAL —— 16M896M

                     ZONE_HIGHMEM —— 896M—物理内存结束

 

  最后,在每个区中都有一个指向mem_map数组中某个单元的指针zone_mem_map,这是干什么的呢?通过查看源代码我们可以知道,mem_map是类型为struct page的数组,而Linux内核正是利用struct page结构体来描述每个物理内存页的,在系统启动时,内核就会为整个系统的内存建立好一个全局的页描述数组mem_map,在以后的运行过程中,Linux内核最终就是根据这个全局内存描述数组来控制对物理内存的分配、回收等操作的。明白了mem_map的作用,我们再来看看每个区中的zone_mem_map指针,这个指针指向的是mem_map中的某一个单元,而这个单元的内容恰恰描述了这个区内的第一页物理内存,这样Linux就把节点、区、page结构联系起来了,它们共同组织起来完成了对系统所有物理内存的描述。因此,我们现在就可以清晰地得出Linux在逻辑上是如何描述所有的物理内存了,其图如图2所示:

 

<think>我们正在讨论新建用户态进程时申请VMA的过程。根据之前的对话,我们知道VMA(Virtual Memory Area)是Linux内核中描述进程虚拟地址空间区域的数据结构,用于管理代码段、数据段、堆、栈等。 新建个用户态进程(例如通过fork()或execve())时,内核需要为进程设置初始的虚拟内存布局。这个过程涉及创建多个VMA,以映射程序的不同部分(如代码段、数据段等)。下面我们将详细描述这过程。 ### 新建用户态进程时申请VMA的过程 #### 1. **进程创建的基本方式** - **fork()**: 创建当前进程的个副本。子进程复制父进程的整个地址空间,包括所有的VMA。此时,子进程的VMA链表是父进程的个拷贝[^2]。 - **execve()**: 加载个新的程序,替换当前进程的地址空间。此时,内核会销毁进程原有的VMA,并根据新程序的内存布局创建组新的VMA[^2]。 #### 2. **execve()过程中的VMA申请** 当进程调用`execve()`加载新程序时,VMA的创建过程如下: **步骤1: 清除旧地址空间** - 内核首先释放进程现有的所有VMA(通过`exit_mmap()`函数),包括解除所有映射并释放VMA结构体[^2]。 **步骤2: 加载可执行文件** - 内核解析可执行文件(如ELF格式)的头部,获取程序的内存布局信息(如代码段、数据段、BSS段等的起始地址、大小、权限等)[^2]。 **步骤3: 创建初始VMA** - 根据ELF文件中的程序头(Program Headers)创建初始VMA。通常至少包括: - 代码段VMA(`LOAD`类型,权限为`r-xp`) - 数据段VMA(`LOAD`类型,权限为`rw-p`) - BSS段VMA(通常与数据段合并,权限为`rw-p`)[^5] - 示例代码段(简化): ```c for (每个程序头) { if (程序头类型为PT_LOAD) { struct vm_area_struct *vma = kmem_cache_alloc(vm_area_cachep); vma->vm_start = ...; // 根据程序头计算虚拟地址 vma->vm_end = vma->vm_start + 程序头内存大小; vma->vm_flags = ...; // 权限标志(VM_READ|VM_EXEC等) vma->vm_file = 可执行文件对象; // 用于文件映射 vma->vm_pgoff = ...; // 文件内偏移(页单位) // 将VMA插入进程的mm->mmap链表和红黑树 insert_vm_struct(mm, vma); } } ``` **步骤4: 创建堆和栈的VMA** - **堆VMA**:创建个起始于数据段结束地址(通常是`end_data`),权限为`rw-p`的匿名映射VMA,初始大小为0,通过`brk`系统调用扩展[^5]。 ```c vma = kmem_cache_alloc(vm_area_cachep); vma->vm_start = 数据段结束地址; vma->vm_end = vma->vm_start; // 初始长度为0 vma->vm_flags = VM_READ | VM_WRITE | VM_GROWSDOWN | VM_GROWSUP; // 堆通常向上增长 insert_vm_struct(mm, vma); mm->brk = vma->vm_end; // 设置brk指针 ``` - **栈VMA**:创建个位于用户地址空间顶部的匿名映射VMA,权限为`rw-p`,并设置`VM_GROWSDOWN`标志(表示栈向下增长)[^3]。 ```c vma = kmem_cache_alloc(vm_area_cachep); vma->vm_start = TASK_SIZE - 栈最大大小; // 例如0xc0000000 - 8MB vma->vm_end = TASK_SIZE; // 结束地址为TASK_SIZE vma->vm_flags = VM_READ | VM_WRITE | VM_GROWSDOWN; insert_vm_struct(mm, vma); ``` **步骤5: 映射动态链接器** - 如果程序是动态链接的,内核会加载动态链接器(如`/lib/ld-linux.so`),并为其创建额外的VMA(代码段和数据段)[^2]。 #### 3. **VMA链表的构建** - 所有创建的VMA通过双向链表(`mm->mmap`)连接,并按虚拟地址从低到高排序[^1][^2]。 - 同时,VMA也被插入红黑树(`mm->mm_rb`)中以支持快速查找[^1]。 #### 4. **初始内存映射的实际占用** - 虽然VMA已经创建,但实际物理内存的分配是延迟的(Lazy Allocation)。例如,代码段和数据段在首次访问时通过缺页中断加载文件内容;堆和栈在首次写入时才分配物理页面[^5]。 ### 总结 新建用户态进程时(特别是通过`execve()`),内核根据可执行文件的布局创建初始VMA,包括代码段、数据段、堆和栈。这些VMA通过双向链表和红黑树管理,物理内存的分配则在首次访问时通过缺页中断完成。 --- ### 相关问题 1. 在`fork()`时,子进程如何复制父进程的VMA链表? 2. `execve()`过程中如何处理动态链接库的映射? 3. 堆VMA在进程运行过程中如何动态扩展? 4. 为什么内核使用两种数据结构(链表和红黑树)管理VMA? [^1]: VMA通过双向链表组织,方便顺序插入和删除操作 [^2]: Linux用户态进程的内存管理浅析 [^3]: Linux 进程虚拟内存 [^4]: Linux用户态进程的内存管理 [^5]: 内存管理vma_Linux用户态进程的内存管理 </think>### 新建用户态进程时申请VMA的过程 新建用户态进程时(如通过`fork()`或`execve()`系统调用),内核会为其构建初始的虚拟内存布局,主要经历以下步骤: #### 1. **创建进程控制块** ```c struct task_struct *task = copy_process(...); // 创建新进程的task_struct struct mm_struct *mm = mm_alloc(); // 分配内存描述符[^2] ``` - 内核创建`task_struct`和`mm_struct`结构体 - 初始化内存描述符的VMA链表头`mm->mmap = NULL` #### 2. **建立初始VMA映射** 内核根据可执行文件格式(如ELF)创建基础VMA: ```mermaid graph LR A[execve系统调用] --> B[解析ELF文件头] B --> C{创建代码段VMA} B --> D{创建数据段VMA} B --> E{创建BSS段VMA} C --> F[权限 r-xp] D --> G[权限 rw-p] E --> H[权限 rw-p] ``` - **代码段VMA**:映射`.text`节,权限为读+执行(VM_READ | VM_EXEC)[^2] - **数据段VMA**:映射`.data`节,权限为读+写(VM_READ | VM_WRITE)[^5] - **BSS段VMA**:映射未初始化数据,初始长度通常为0[^5] #### 3. **创建堆栈VMA** ```c // 创建栈VMA vma_stack->vm_start = TASK_SIZE - stack_size; // 用户空间顶部 vma_stack->vm_end = TASK_SIZE; // IA32默认3GB边界[^3] vma_stack->vm_flags = VM_GROWSDOWN | VM_READ | VM_WRITE; // 创建堆VMA vma_heap->vm_start = 数据段结束地址; vma_heap->vm_end = vma_heap->vm_start; // 初始长度为0[^5] vma_heap->vm_flags = VM_GROWSUP | VM_READ | VM_WRITE; ``` - **栈VMA**:位于用户空间顶端(如`0xc0000000 - 8MB`到`0xc0000000`),带`VM_GROWSDOWN`标志[^3] - **堆VMA**:紧接数据段,初始长度为0,带`VM_GROWSUP`标志 #### 4. **VMA链表构建流程 ```c list_add(&vma_text->vm_list, &mm->mmap); // 插入代码段 list_add_tail(&vma_data->vm_list, &mm->mmap);// 添加数据段 list_add_tail(&vma_bss->vm_list, &mm->mmap); // 添加BSS段 list_add_tail(&vma_heap->vm_list, &mm->mmap);// 添加堆 list_add_tail(&vma_stack->vm_list, &mm->mmap);// 添加栈 ``` - 最终形成地址递增的VMA链表: ``` 代码段VMA → 数据段VMA → BSS段VMA → 堆VMA → 栈VMA ``` #### 5. **物理内存延迟分配** 内核采用写时复制(Copy-on-Write)和按需分页: - 仅建立虚拟地址映射,物理页面在首次访问时分配[^5] - 示例:当进程首次写堆时触发缺页中断: ```c handle_pte_fault() { if (vma->vm_flags & VM_WRITE) { alloc_zeroed_page(); // 分配物理页[^5] set_pte(pte, page); } } ``` #### 6. **内存布局示例** 通过`/proc/pid/maps`可查看: ``` 00400000-00401000 r-xp // 代码段VMA 00600000-00601000 r--p // 只读数据段VMA 00601000-00602000 rw-p // 数据段/BSS段VMA 00602000-00603000 rw-p // 堆VMA(初始空) c0000000-c0800000 rw-p // 栈VMA[^3] ``` ### 关键特点 1. **地址连续性**:VMA链表严格按虚拟地址升序排列,便于合并相邻区域[^1] 2. **权限分离**:不同内存区域(代码/数据/堆栈)有独立权限控制[^4] 3. **延迟分配**:物理内存在首次访问时通过缺页中断分配[^5] 4. **写时复制**:`fork()`时子进程共享父进程VMA,写操作时复制[^2] --- ### 相关问题 1. `fork()`系统调用如何复制父进程的VMA链表? 2. 动态链接库加载时如何创建新的VMA? 3. 堆内存扩展(`brk`系统调用)如何修改VMA链表? 4. 为什么栈溢出可能破坏相邻VMA区域? [^1]: VMA通过双向链表组织,方便顺序插入和删除操作 [^2]: Linux用户态进程的内存管理浅析 [^3]: Linux 进程虚拟内存 [^4]: Linux用户态进程的内存管理 [^5]: 内存管理vma_Linux用户态进程的内存管理
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值