程序启动过程
我看网上很少讲一个程序到运行的过程那我写一篇目前我也在学习就把目前知道的给大家分享一下。但是还是需要研究这块的有一定的虚拟内存页表的基础不过也可以直接现场百度也不影响阅读。知识串起来也就通了。程序的加载、运行在磁盘物理内存在虚拟内存中所发生的动作使用的工具具体的实现方法有哪些1. 两个视角的区分很多人混淆「段」的概念本质是两个不同阶段的视角链接视角Section.text / .rodata / .data / .bss等是编译器、链接器组织代码数据的细粒度单位用于编译链接加载视角Segment内核加载程序时会把权限相同的多个 section 合并成一个加载段Segment比如.text .rodata合并为「只读加载段」.data .bss合并为「读写加载段」。我们讲加载流程时内核看到的是 Segment而不是单个的 Section。2. 三大底层支撑机制请求调页Demand Paging用到哪一页才从磁盘载入哪一页不提前全量加载写时复制COW, Copy-On-Write共享页面被修改时才单独拷贝一份私有副本读时共享、写时分离MMU 页表虚拟地址与物理地址的翻译桥梁也是缺页中断、权限检查的硬件执行者。完整流程分阶段详解阶段 1触发启动 ——execve系统调用替换进程映像这是程序运行的起点由 Shell 或父进程触发。维度具体动作用户态触发Shell 先fork创建子进程再调用execve(./app, argv, envp)系统调用用新程序替换子进程的全部地址空间。磁盘侧仅读取 ELF 文件的头部信息文件头、程序头表验证文件格式合法性不读取代码和数据。虚拟内存侧清空旧进程的全部虚拟地址映射销毁旧页表为新程序准备空白的地址空间。物理内存侧几乎不分配业务内存仅内核自身创建进程描述符、页表等数据结构。实现机制execve系统调用ELF 加载器内核中的binfmt_elf模块。观测工具strace ./app跟踪系统调用readelf -h app查看 ELF 文件头。阶段 2构建布局 —— 解析程序头建立虚拟地址映射这一步只建立「地址规划」和「磁盘关联」不加载任何真实数据到物理内存。维度具体动作核心动作内核读取 ELF 的程序头表Program Header按加载段的权限、大小在虚拟地址空间中分配对应区间并在页表中登记映射关系。磁盘侧读取程序头表记录每个加载段在文件中的偏移、长度、权限。虚拟内存侧从低地址到高地址完成布局1. 只读加载段.text .rodata虚拟地址连续权限「只读 可执行」2. 读写加载段.data .bss虚拟地址连续权限「可读可写」3. 堆区初始大小从低地址向高地址增长4. mmap 映射区动态库、文件映射、匿名共享内存5. 栈区从高地址向低地址增长6. 顶端为内核空间用户态不可访问页表状态页表项不指向物理内存而是记录「该虚拟页 → 对应磁盘文件的某某字节偏移」并标记为「未驻留Not Present」。特殊处理.bss段不关联磁盘标记为「匿名零页」访问时直接分配全零物理页。物理内存侧仍然没有程序的代码和数据仅页表本身占用少量物理内存。实现机制虚拟内存分配器页表初始化文件映射mmap内核实现。观测工具readelf -l app查看程序头与加载段cat /proc/pid/maps查看进程虚拟地址布局pmap pid可视化虚拟内存分布。阶段 3首次执行 —— 缺页中断载入第一页代码当 CPU 开始从程序入口地址取指执行时第一次真正的内存加载才会发生。维度具体动作触发条件CPU 输出虚拟地址 → MMU 查页表 → 发现标记为「未驻留」 → 触发缺页异常Page Fault→ 陷入内核态处理。磁盘侧内核根据页表记录的文件偏移从磁盘 ELF 文件中读取对应 4KB一页的机器指令数据。虚拟内存侧页表项从「虚拟地址→磁盘偏移」更新为「虚拟地址→物理页地址」设置只读 可执行权限。物理内存侧分配 1 个空闲物理页帧将磁盘读取的机器指令写入该物理页。处理完成内核退出异常返回用户态CPU 重新执行刚才的指令此时 MMU 可以正常翻译地址直接从物理内存取指执行。实现机制请求调页MMU 缺页异常块设备 IO 读取。观测工具perf stat ./app统计缺页中断次数ps -o maj_flt,min_flt pid查看主次缺页数量。关键结论程序启动到第一条指令执行前物理内存里没有任何该程序的代码代码是执行到哪里才加载到哪里。阶段 4持续运行 —— 按需加载 写时复制程序运行过程中不同区域的缺页处理逻辑不同对应之前讲的各个段特性1. 只读区域.text/.rodata首次访问触发缺页从磁盘载入物理页设置只读权限跨进程共享多个进程运行同一个程序时代码页物理内存只有一份所有进程共享极大节省内存全程只读不会触发写操作一旦写入直接触发段错误。2. 已初始化数据区.data首次读取和只读段一样从磁盘载入初始值多进程共享同一份物理页首次写入触发 ** 写时复制COW** 异常 → 内核分配一个新的物理页 → 把原页内容拷贝到新页 → 页表指向新私有页 → 标记为可写 → 再完成写入操作。结果读时共享节省内存写时私有保证进程隔离。3. 匿名内存区.bss/ 堆 / 栈共同特点不关联任何磁盘文件初始值全为 0首次访问缺页时内核直接分配一个全零物理页不产生磁盘 IO属于「次缺页」堆扩展调用malloc空间不足时通过brk/sbrk系统调用扩大堆区的虚拟地址范围栈扩展访问到栈边界时内核自动向下扩展栈的虚拟空间分配物理页。4. 内存映射区动态库 /mmap 文件动态库.so的加载逻辑和主程序完全一致只读代码段共享、读写数据段写时复制普通文件 mmap读写逻辑同上修改后可通过msync同步回磁盘。阶段 5稳态运行与内存回收正常执行常用代码和数据都已载入物理内存MMU 直接地址翻译无中断CPU 全速执行。内存不足时文件页text/rodata/ 文件映射直接丢弃因为磁盘上有原始副本下次用到再重新读入匿名页堆 / 栈 /bss换出到 swap 交换分区腾出物理内存下次访问时再从 swap 换入。程序退出释放所有物理页帧归还操作系统清空页表销毁虚拟地址空间关闭打开的文件释放进程相关内核数据结构。核心工具汇总表工具用途常用命令readelf分析 ELF 文件结构、段、程序头readelf -h文件头readelf -S段表readelf -l加载段objdump反汇编、查看段属性objdump -d反汇编代码段cat /proc/pid/maps查看进程虚拟地址空间布局、权限、映射关系cat /proc/$$/maps查看当前 shellpmap可视化进程虚拟内存分布pmap -x pidstrace跟踪系统调用观察 exec、mmap、brk 等strace ./appperf统计缺页中断、性能事件perf stat -e faults ./appps / top查看物理内存占用RSS、虚拟内存大小VIRTtop按进程查看关键认知澄清不是 “全量加载再运行”程序不是整个拷进内存才开始执行而是边执行边加载启动速度和程序总大小无关只和入口代码量有关。物理内存远小于磁盘大小一个 100MB 的程序运行时可能只用到几 MB 物理内存没执行到的代码永远不会载入。多进程共享极大节省内存系统里运行 100 个 bash代码段物理内存只有一份不是 100 份。虚拟地址空间是 “规划”不是 “占用”虚拟地址大不代表物理内存占用多只有真正访问过、触发过缺页的页面才会占用物理内存。