Linux进程地址空间与IO访问机制:从虚拟内存布局到设备驱动实战

发布时间:2026/10/21 2:23:06
Linux进程地址空间与IO访问机制:从虚拟内存布局到设备驱动实战 在操作系统的内存管理体系中,进程地址空间是连接程序与物理内存之间的关键桥梁。无论是日常开发中的指针操作、动态内存分配,还是系统层面的页表映射与缺页中断,都离不开对进程地址空间布局的理解。本文将从虚拟内存的基本概念出发,系统梳理进程地址空间的整体结构,并逐一介绍代码段、数据段、堆、栈以及内核区等核心区域的划分与作用,帮助读者建立起清晰的内存布局认知框架。此外,我们还会顺带说明与地址空间密切相关的 IO 寄存器映射机制,理解外设寄存器如何被纳入进程可见的地址范围,有助于更完整地把握程序与硬件交互的底层路径。1. 进程地址空间布局进程地址空间是操作系统为每个进程分配的虚拟内存区域,它决定了程序运行时各个数据段在内存中的位置。理解进程地址空间布局,是深入掌握内存管理、链接装载以及程序运行机制的基础。从 C 程序的角度来看,进程虚拟地址空间通常由正文段、数据段、栈和堆等几个核心区域组成,如下图所示,下面逐一说明。正文段正文段是 CPU 执行的机器指令所在的部分。通常,正文段是共享的,因此即使是频繁执行的程序,在存储器中也只需要一个副本;同时正文段是只读的。有些人认为还存在一个“文字常量区”,例如语句char *p = "12345"中存放 "12345" 的空间,这里将其归入正文段。数据段数据段用于存放程序中的全局变量和 static 变量,又可分为初始化数据段和非初始化数据段两部分。初始化数据段:存放有初始值的全局变量和 static 变量。非初始化数据段:通常称为 bss 段,存放没有初始化的全局变量和 static 变量。在程序开始执行之前,内核会将此段中的数据初始化为 0 或空指针。栈自动变量以及每次函数调用时所需要保存的信息都存放在栈段中。每次调用函数时,其返回地址以及调用者的环境信息(例如某些机器寄存器的值)都会存放在栈中。随后,最近被调用的函数会在栈上为其自动变量和临时变量分配存储空间。通过以这种方式使用栈,可以递归调用 C 函数:递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数调用实例中的变量。堆堆通常用于进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段和栈之间。那内核是如何管理这些段的呢?前面介绍的正文段、数据段、栈和堆,只是从 C 程序视角看到的虚拟地址空间划分。真正负责管理这些区域的,是操作系统内核。内核并不直接面向“正文段”“数据段”这样的概念,而是通过一套更底层的机制来组织和管理每个进程的虚拟内存。在 Linux 内核中,每个进程都由一个task_struct结构体描述,它被称为进程描述符,是内核管理进程的核心数据结构。进程的 PID、状态、优先级、打开的文件、信号处理等信息都记录在其中。而与我们这里讨论的地址空间直接相关的,是task_struct中的一个关键成员——mm指针,它指向该进程的内存描述符mm_struct。mm_struct可以看作进程虚拟地址空间的“总管家”,它记录了整个地址空间的整体信息,例如代码段、数据段、堆、栈的起始地址和结束地址,以及页表的总入口。通过mm_struct,内核就能知道一个进程的虚拟地址空间被划分成了哪些区域、每个区域从哪里开始到哪里结束。不过,mm_struct只负责“宏观”的划分。真正把虚拟地址空间落实到物理内存的,是页表。页表是内核为每个进程维护的一张映射表,它记录了进程虚拟地址空间中的每一个虚拟页,对应到物理内存中的哪一个物理页框。当程序访问某个虚拟地址时,CPU 的 MMU(内存管理单元)会通过页表完成虚拟地址到物理地址的转换;如果对应的物理页尚未分配,就会触发缺页异常,由内核按需分配物理内存并更新页表。综合来看,从task_struct到mm_struct,再到页表,构成了内核管理进程地址空间的完整链条:task_struct通过mm指针找到mm_struct,mm_struct描述虚拟地址空间的整体布局,而页表则负责把这段虚拟空间逐页映射到真实的物理内存。理解了这条链路,也就理解了前面所说的正文段、数据段、栈和堆,最终是如何被内核真正管理起来的。2. 虚拟内存空间我们首先来看一下一个进程的虚拟内存布局在实现层面是如何建立的。当一个程序(如/bin/gonzo)一旦被加载到内存执行,内核首先会分配一个task_struct结构体来表示该新进程的信息,然后初始化该进程的mm_struct结构体。mm_struct结构体中的mmap域指向该进程的虚拟内存空间(也就是我们上面说到的那几个段),如下图示。每个虚拟内存区域都是一个连续的虚拟地址空间;这些区域是不会重叠的。一个vm_area_struct表示一个这样的内存区域,它包括该虚拟内存区域的起始和结束地址,flags描述了该区域的访问权限和行为,vm_file表示哪一个文件被映射到这块区域。一个 VMA 没有映射一个文件被称为是匿名的。上面提到的vm_area_struct(VMA)对应的是进程的虚拟地址空间,它只是描述了虚拟内存区域的布局和属性。但是当程序运行时,去访问一个虚拟地址时,内核就必须给它映射一个物理地址,否则程序无法真正访问到数据。那么,这个映射是如何完成的呢?答案就是页表(Page Table)。3.页表页表是内核为每个进程维护的一张映射表,它记录了进程虚拟地址空间中的每一个虚拟页,对应到物理内存中的哪一个物理页框。当程序访问某个虚拟地址时,CPU 的 MMU(内存管理单元)会通过页表完成虚拟地址到物理地址的转换;如果对应的物理页尚未分配,就会触发缺页异常,由内核按需分配物理内存并更新页表。3.1 PTE介绍对每一个虚拟页在页表中都有对应的一个页表项(PTE),通常 x86 下的一页对应一个 4 字节的如下记录:Linux 提供一系列用来读写 PTE 的函数。P 位告诉处理器该虚拟页目前是否在物理内存中。0 表示不在,此时访问该页的动作会触发一个缺页异常。请记住,当 P 位为 0 时,内核是不检测其它所有的位的,内核可以用这些位另做它用。R/W 位表示读/写;0 表示只读。U/S 位代表用户和系统;0 表示该页只能被 kernel 访问。D和A位分别对应表示脏数据和已被访问过。一个脏页表示被写过,而被访问包括被读和被写。这两个标志都有些特别:都只有处理器来设置它们,但是由内核来复位它们。最后,PTE中储存的是对应的page的开始物理地址,并且会与4KB对齐。3.2 虚拟地址与物理地址Linux 内核是怎样把一个虚拟地址(或叫线性地址)映射成实际的内存物理地址的呢?我们假如内核只用一级页表结构,那么映射过程如下图所示:不过,内核的实现中都是采用二级页表或三级页表结构:通过上面的分析,我们得知,当程序被加载到内存时,内核会为进程的虚拟空间布局创建必要的vm_area_struct结构(或在进程执行过程中动态创建,如malloc时),而当进程真正需要访问物理内存时,内核是通过该进程的页表把虚拟地址映射成物理地址,如下图:那进程的页表项是怎么创建的?是不是在内核为进程创建vm_area_struct时就把相应的物理内存分配,建立页表项的呢?页表项是在当进程真正需要访问物理内存时创建的。当进程需要访问一个虚拟地址时(如程序对指针操作),内核通过该进程pgd域,去查找页表数组,当在数组里找不到相应的页表项时,这时内核会调用do_page_fault() - find_vma(),find_vma就是找到该虚拟地址对应的vm_area_struct结构体,如果没有对应的vm_area_struct就说明该虚拟地址非法,报告一段错误,终止该进程。如果找到相应的vm_area_struct,就调用该结构体的域vm_ops来分配和申请物理页。4.内存页管理和分配Linux 内核用结构体 struct page 来表示一个物理内存页。内核在启动的时候,会进行一个物理内存检测的过程,把所有物理内存页的信息保存在全局数组 struct page *mem_map(mm/memory.c)。内核模块需要用到物理内存时,就需要分配一个或多个 struct page 结构,内核提供了如下函数接口分配和释放物理页:// 分配物理页 struct page *alloc_pages(gfp_t gfp_mask, unsigned int order); struct page *alloc_page(gfp_t gfp_mask); // 分配单个物理页 unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); unsigned long __get_free_page(gfp_t gfp_mask); // 分配单个物理页 unsigned long get_zeroed_page(gfp_t gfp_mask); // 分配并清零单个物理页 // 释放物理页 void __free_pages(struct page *page, unsigned int order); void free_pages(unsigned long addr, unsigned int order); void free_page(unsigned long addr); // 释放单个物理页其中,alloc_pages和__get_free_pages用于分配连续的物理页,order表示分配 2 的 order 次方个连续物理页;alloc_page和__get_free_page是分配单个物理页的简化封装。释放时,__free_pages和free_pages分别对应按struct page指针和虚拟地址释放连续物理页,free_page用于释放单个物理页。分配与释放必须成对使用,且 order 值要保持一致,否则会造成内存泄漏或错误释放。在分配 page 后,就可以用内核提供的 PTE(页表项)函数或宏(如 mk_pte),填充页表项。5.mmap函数实现mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针来做而不需要read/write函数。5.1 用户层实现mmap是 Linux 提供的用于建立虚拟地址空间映射的系统调用,其函数原型如下:#include sys/mman.h void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);各参数含义如下:addr:映射区的起始虚拟地址。通常传入 NULL,由内核自动选择合适的地址;若指定地址,则要求该地址按页对齐,且内核会尽量在该地址附近创建映射。length:映射区的长度,单位为字节。内核会将该长度向上取整为页大小的整数倍。prot:映射区的访问权限,可取以下值的按位或组合:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(不可访问)。flags:映射类型与行为标志,常用值包括MAP_SHARED(对映射区的修改会写回文件,可与其他进程共享)、MAP_PRIVATE(对映射区的修改不写回文件,采用写时复制)、MAP_ANONYMOUS(匿名映射,不关联文件,fd 传 -1)。fd:要映射的文件描述符。对于匿名映射,该参数传 -1。offset:文件映射的起始偏移量,单位为字节,且必须按页大小对齐。调用成功时,mmap返回映射区的起始虚拟地址;失败时返回MAP_FAILED(即(void *)-1),并设置 errno 以指示错误原因。与之对应的munmap函数用于解除映射,原型为

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询