资讯详情

06.【Linux系统编程】命令行参数(给main传参)、环境变量(概念+使用)、进程的虚拟地址空间(用户实际访问的空间)

📅 2026/10/6 1:53:55 | 华诺云谱 👁 阅读
06.【Linux系统编程】命令行参数(给main传参)、环境变量(概念+使用)、进程的虚拟地址空间(用户实际访问的空间)
目录1. 命令行参数 int main(int argc, char* argv[])1.1 命令行参数的使用2. 环境变量2.1 环境变量概念2.2 常见环境变量2.3 环境变量相关命令增删查改只对当前bash生效2.4 环境变量的组织方式、存放位置2.5 通过代码获取环境变量main函数第三个参数-环境变量表2.6 环境变量的全局属性、本地变量、内建命令3. 程序地址空间-进程的虚拟地址空间3.1 进程的虚拟地址空间 概念3.2 虚拟内存机制证明3.3 一个进程有一个虚拟地址空间页表写时拷贝3.4 虚拟地址空间物理地址空间-理解区域划分概括举例 [重重重点]3.5 进程运行时虚拟地址空间的开辟 及 与物理地址空间的映射3.6 为什么要有虚拟地址空间3.7 澄清一些问题4. 理解几个概念1. mm_struct 是什么2. 页表是什么3. 虚拟地址怎么映射到物理地址4. 为什么要用多级页表5. 什么是缺页中断6. 缺页不一定表示程序出错7. 把几个概念串起来8. 页表权限和缺页中断的联系1. 命令行参数 int main(int argc, char* argv[])1.1 命令行参数的使用我们写的main函数也有参数其中argc是输入的以空格为分隔符的字符串个数系统识别后自动赋值。argv是以空格为分隔符的字符串数组。用户输入。main函数的命令行参数是实现程序不同子功能的方法指令选项的实现原理。总结进程拥有一张表argv表用来支持实现选项功能。举例1argv是字符指针数组接收命令行参数#includestdio.h int main(int argc, char* argv[]) { for(int i 0; i argc; i) { printf(argv[%d]:%s\n,i,argv[i]); } return 0; }./code # argv[0]:./code ./code a b c d # ./code也算字符串 # argv[0]:./code # argv[1]:a # argv[2]:b # argv[3]:c # argv[4]:d像我们的ls,cp等命令行指令也是通过这种方法实现的。举例2解释ls,cp等指令的参数功能实现#includestdio.h #includestring.h int main(int argc, char* argv[]) { if(argc ! 2) { printf(Usage: %s [-a|-b|-c]\n,argv[0]); return 1; } const char *arg argv[1]; if(strcmp(arg,-a) 0) printf(这是功能a\n); else if(strcmp(arg,-b) 0) printf(这是功能b\n); else if(strcmp(arg,-c) 0) printf(这是功能c\n); else printf(Usage: %s [-a|-b|-c]\n,argv[0]); return 0; }./code # Usage: ./code [-a|-b|-c] ./code -a # 这是功能a ./code -b # 这是功能b ./code -c # 这是功能c ./code -d # Usage: ./code [-a|-b|-c]2. 环境变量2.1 环境变量概念引言为什么系统的指令不需要带“./”而我们自己的程序需要./才能执行答系统中存在环境变量要执行一个程序必须先找到他。系统的PATH 环境变量来帮助系统找到二进制文件。当然系统中还有其他各种环境变量环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。参数被bash使用也就是间接被用户使用了环境变量通常具有某些特殊用途还有在系统当中通常具有全局特性。举例问题1我们在编写C/C代码的时候在链接的时候从来不知道我们的所链接的动态静态库在哪里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。2.2 常见环境变量•HOSTNAME: 主机名•SHELL: 当前Shell它的值通常是/bin/bash。•HISTSIZE: bash记录历史命令条数的最大值history可以查看历史命令•SSH_CLIENT: 公网IP•OLD_PWD: 前一个工作目录cd -•SSH_TTY: 当前设备 /dev/pts/0•USER: 当前用户•PATH: 指定命令的搜索路径比如我们写的程序执行要使用./code而系统命令比如ls不需要“ls”因为ls的路径在PATH的环境变量中•PWD: 当前工作目录•HOME: 指定用户的主工作目录用户家目录cd ~可以直接进入家目录• LOGNAME: 当前登录的用户su不改变USER和LOGNAMEsu -改变即su -是退出重新登陆了2.3 环境变量相关命令增删查改只对当前bash生效每当我们打开一个终端终端也是一个进程终端会在系统中继承一份环境变量表。终端进程启动bashbash在终端进程中继承了环境变量表并且读取.bashrc和.bash_profile等配置文件中的内容修改并整合刚刚从终端继承的环境变量表从而形成自己最终要使用的“完整环境变量表”。而以下我们可以在bash终端中执行的对环境变量的操作都是对bash环境变量表的修改。当新开一个bash进程时bash还是重新继承环境变量表所以我们在bash中对环境变量表的操作都是临时性的。如果要永久性修改环境变量需要修改./bashrc或./bash_profile文件指令功能举例查看环境变量env显示所有环境变量envecho $NAME显示某个环境变量值echo $PATHset显示本地定义的shell变量和环境变量新增环境变量给bashexport NAME内容设置一个新的环境变量当前bash生效export MYENV1111env查看删除环境变量unset NAME清除环境变量当前bash生效unset MYENV1env查看修改环境变量NAME内容修改环境变量内容为指定内容NAME$NAME:内容添加指定内容到环境变量中当前bash生效见下面代码# 修改PATH环境变量为自己的路径 PATH$PATH:/home/gyy/linux-system-programming/lesson15 echo $PATH # 给PATH环境变量添加自己的路径 PATH$PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/gyy/.local/bin:/home/gyy/bin:/home/gyy/linux-system-programming/lesson152.4 环境变量的组织方式、存放位置1. bash为什么能根据环境变量执行指令PATH环境变量每当我们打开一个终端终端也是一个进程终端会在系统中继承一份环境变量表。终端进程启动bashbash在终端进程中继承了环境变量表并且读取.bashrc和.bash_profile等配置文件中的内容修改并整合刚刚从终端继承的环境变量表从而形成自己最终要使用的“完整环境变量表”。环境表量表也是一个字符指针数组存放每个环境变量的字符串指令执行流程举例ls -a -l1. 命令行参数表拆分命令2. bash根据命令在环境变量表找到PATH从PATH中获取ls程序的路径执行ls -a -l指令。2. 环境变量的存放位置、存放方式系统的相关配置文件.bash_profile.bashrc在用户家目录里面的隐藏文件bash程序启动时继承终端环境变量表并整合.bash_profile.bash_profile配置文件的内容形成完整的环境变量表。bash中环境变量的存放方式环境变量表是一个字符指针数组每个指针指向一个以’\0’结尾的环境字符串。3. 如果Linux系统有10个用户登录呢存在10个bash每个bash都要从系统的相关配置文件中读取环境变量形成环境变量表和命令行参数表。2.5 通过代码获取环境变量main函数第三个参数-环境变量表1. main函数参数获取环境变量表main函数的第三个参数envenv是获取的父进程的环境变量当前程序的父进程就是bash#includestdio.h #includestring.h // main最多有几个参数3个 // env参数是父进程传给我们的 int main(int argc, char *argv[], char *env[]) { (void)argc; (void)argv; // 打印环境变量表 for(int i 0; env[i]; i) { printf(env[%d]- %s\n, i, env[i]); } return 0; }环境变量可以被子进程继承只要是bash的子孙进程都可以拿到bash的环境变量即环境变量在系统中通常具有全局特性。为什么要被子孙进程继承环境变量子孙进程拿到bash的环境变量后可以结合环境变量做个性化操作比如定制一个只能让指定用户执行的程序。getenv获取USER指定的USER才能执行对应的程序2. getenv函数获取环境变量getenv头文件stdlib.hgetenv(NAME); //返回对应名称环境变量的内容字符串#includestdio.h #includestring.h #includestdlib.h // main最多有几个参数3个 // env参数是父进程传给我们的 int main(int argc, char *argv[], char *env[]) { (void)argc; (void)argv; (void)env; char *value getenv(PATH); if(value NULL) return 1; printf(PATH-%s\n,value); return 0; }3. environ获取环境变量表environ的头文件unistd.h使用时需要声明全局变量。#includestdio.h #includestring.h #includestdlib.h #includeunistd.h extern char **environ; // main最多有几个参数3个 // env参数是父进程传给我们的 int main(int argc, char *argv[]) { (void)argc; (void)argv; // 打印环境变量表 for(int i 0; environ[i]; i) { printf(environ[%d]- %s\n, i, environ[i]); } return 0; }注意推荐使用第二种方法只获取某个环境变量的内容。2.6 环境变量的全局属性、本地变量、内建命令• 环境变量通常具有全局属性可以被子进程继承下去• 本地变量不会被子进程继承只能在bash内部被调用。• 内建命令(built-in command)不需要创建子进程而是让bash自己亲自执行。bash自己调用函数或者系统调用。例如export举例当使用以下命令之后修改当前bash的PATH环境变量时还有一些指令可以使用这些就是内建命令。PATH/home/gyy/linux-system-programming/lesson153. 程序地址空间-进程的虚拟地址空间3.1 进程的虚拟地址空间 概念我们之前所说的程序地址空间其实并不应该叫做程序地址空间应该叫做进程地址空间 或 进程的虚拟地址空间这是一个操作系统层级的概念不是语言层的概念。之前讲程序地址空间只是为了好理解C语言中变量的存储位置实际中不止我们的程序代码和变量存在这个位置还包含了系统注入的信息如环境变量、命令行参数以及动态链接库等。为什么又叫做虚拟地址空间呢它之所以是‘虚拟’的是因为操作系统通过虚拟内存机制为每个进程提供了一个独立、统一的地址空间幻觉并将其地址映射到物理内存甚至硬盘上实现了内存管理和保护。由下列代码可见环境变量environ和命令行参数argv, argc就存放在栈区的最高地址区域。注意堆区地址由低地址向高地址增长栈区由高地址向低地址增长#include stdio.h #include unistd.h #include stdlib.h int g_unval; int g_val 100; int main(int argc, char* argv[], char* env[]) { const char* str helloworld; printf(code addr: %p\n, main); printf(init global addr: %p\n, g_val); printf(uninit global addr: %p\n, g_unval); static int test 10; char* heap_mem (char*)malloc(10); char* heap_mem1 (char*)malloc(10); char* heap_mem2 (char*)malloc(10); char* heap_mem3 (char*)malloc(10); printf(heap addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1) printf(test static addr: %p\n, test); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1) printf(read only string addr: %p\n, str); for (int i 0; i argc; i) { printf(argv[%d]: %p\n, i, argv[i]); } for (int i 0; env[i]; i) { printf(env[%d]: %p\n, i, env[i]); } return 0; }code addr: 0x40057d init global addr: 0x60103c uninit global addr: 0x601048 heap addr: 0x148a010 heap addr: 0x148a030 heap addr: 0x148a050 heap addr: 0x148a070 test static addr: 0x601040 stack addr: 0x7fffa66d3cb8 stack addr: 0x7fffa66d3cb0 stack addr: 0x7fffa66d3ca8 stack addr: 0x7fffa66d3ca0 read only string addr: 0x400820 argv[0]: 0x7fffa66d47e5 env[0]: 0x7fffa66d47ed env[1]: 0x7fffa66d4801 env[2]: 0x7fffa66d480f env[3]: 0x7fffa66d481a env[4]: 0x7fffa66d482a env[5]: 0x7fffa66d4838 env[6]: 0x7fffa66d485c env[7]: 0x7fffa66d486f env[8]: 0x7fffa66d4878 env[9]: 0x7fffa66d48bb env[10]: 0x7fffa66d4e57 env[11]: 0x7fffa66d4e70 env[12]: 0x7fffa66d4eca env[13]: 0x7fffa66d4efa env[14]: 0x7fffa66d4f0b env[15]: 0x7fffa66d4f22 env[16]: 0x7fffa66d4f2a env[17]: 0x7fffa66d4f39 env[18]: 0x7fffa66d4f45 env[19]: 0x7fffa66d4f7a env[20]: 0x7fffa66d4f9d env[21]: 0x7fffa66d4fbc env[22]: 0x7fffa66d4fc63.2 虚拟内存机制证明• 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。• 但地址值是一样的说明该地址绝对不是物理地址在Linux地址下这种地址叫做虚拟地址。• 我们在用C/C语言所看到的地址全部都是虚拟地址物理地址用户一概看不到由OS统一管理。OS必须负责将虚拟地址转化成物理地址。#include stdio.h #include unistd.h int gval 100; int main() { pid_t id fork(); if(id 0) { while(1) { printf(子: gval: %d, gval: %p, pid: %d, ppid: %d\n, gval, gval, getpid(), getppid()); sleep(1); gval; } } else { while(1) { printf(父: gval: %d, gval: %p, pid: %d, ppid: %d\n, gval, gval, getpid(), getppid()); sleep(1); } } return 0; }子: gval: 100, gval: 0x601054, pid: 9907, ppid: 9906 父: gval: 100, gval: 0x601054, pid: 9906, ppid: 9013 子: gval: 101, gval: 0x601054, pid: 9907, ppid: 9906 父: gval: 100, gval: 0x601054, pid: 9906, ppid: 9013 子: gval: 102, gval: 0x601054, pid: 9907, ppid: 9906 父: gval: 100, gval: 0x601054, pid: 9906, ppid: 9013 子: gval: 103, gval: 0x601054, pid: 9907, ppid: 9906 父: gval: 100, gval: 0x601054, pid: 9906, ppid: 9013 子: gval: 104, gval: 0x601054, pid: 9907, ppid: 9906 父: gval: 100, gval: 0x601054, pid: 9906, ppid: 90133.3 一个进程有一个虚拟地址空间页表写时拷贝虚拟地址空间总共4GB[0,3]GB为用户空间[3,4]GB为内核空间。1. 代码中取地址如何找到完整变量虚拟地址空间的宽度是1字节而例如一个整型g_val占四个字节有四个虚拟地址我们取地址时拿到的是起始虚拟地址地址数值最小的那个然后根据变量类型的偏移量就能找到完整的变量。2. 页表概念每个进程有一套“页表”用来做虚拟地址空间和物理地址空间的映射关系一个虚拟地址对应一个物理地址。3. 写时拷贝因为一个进程有一个虚拟地址空间所以我们使用fork创建的子进程也有自己的虚拟地址空间。子进程的task_struct(PCB)、虚拟地址空间、页表都是拷贝的父进程的。所以子进程的初始化数据区也有全局变量g_val地址。因为子进程拷贝了父进程的页表页表中存放的虚拟地址和物理地址空间的映射关系也被拷贝了(浅拷贝)所以子进程和父进程中g_val的虚拟地址和物理地址都相同。所以在2.2中父子进程打印的g_val的地址是相同的(父子进程中g_val的虚拟地址相同)因为浅拷贝所以代码也是共享的因为我们的进程具有独立性所以在子进程对g_val修改时操作系统会进行判断如果要修改g_val则会给子进程的g_val重新分配物理地址空间并修改页表(深拷贝)。到此父子进程中g_val的虚拟地址相同而各自映射的物理地址不同所以父子进程对g_val变量的修改互不影响这也就是写时拷贝不修改则先浅拷贝修改则再深拷贝再回看第五篇博客的3.1.6fork的两个问题id和gval都有写时拷贝机制3.4 虚拟地址空间物理地址空间-理解区域划分概括举例 [重重重点]物理内存中完全没有代码区、堆区、栈区这些概念物理内存只会给每个进程进行简单的分块。代码区、堆区、栈区都是虚拟地址空间的概念具体每个虚拟地址空间的布局是由操作系统和编译器在编译器决定的。进程只是在运行时在这个已经规划好的布局内动态地使用堆和栈等区域。1. 理解-虚拟地址空间物理地址空间大富翁一个富翁有10亿$有四个私生子富翁给每个私生子承诺会把10亿$遗产继承给他们私生子互不认识所以每个人都会认为自己能继承到10亿$的遗产画大饼富翁操作系统10亿$遗产物理内存私生子不同的进程大饼虚拟地址空间即让每个进程都认为自己有4G的物理内存以32位操作系统为例或者每一个进程都认为自己独占物理内存。怎么管理大饼→怎么管理虚拟地址空间先描述再组织虚拟地址空间本质时一个数据结构struct mm_struct{}每个进程的struct mm_struct{}包含各自虚拟地址空间的各种信息。2. 虚拟地址空间的区域划分38线小时候同桌a,b之间画38线把课桌平分将课桌的100cm每个厘米都标注出来(对桌子进行编址其实桌子的划分就成了0-100的数字所以可以用int整数来保存地址)a占0-50cmb占50-100cm。即区域划分只需要确认区域的开始和结束即可桌子地址空间桌子的100cm地址空间的2^32各个字节对应的地址桌子上的刻度地址空间上的地址每个小朋友有各自对应的区域正文代码区、初始化数据区、未初始化数据区、堆区等等。根据每个区域的开始和结束地址划分地址空间而struct mm_struct{}里面就保存了各个区域的起始和结束地址long code_start; long code_end; long init_start, init_end……但是有一天a惹b生气了b把a打了一顿然后把38线调整a:b30:70。即为地址空间的区域调整例area.b_start - 20; area.a_end - 20;a占0-30cmb占30-100cm。3.5 进程运行时虚拟地址空间的开辟 及 与物理地址空间的映射流程1.【编译链接】规划蓝图编译器与链接器在可执行文件中规定好虚拟地址空间的布局如.text在0x08048000。此时虚拟地址空间是“蓝图”。2.【加载】创建虚拟空间并建立文件映射执行程序时OS为新进程创建mm_struct和页表。OS按“蓝图”在虚拟地址空间中划出区域代码区、数据区等并设置权限。OS将页表项设置为无效并将其映射到磁盘上的可执行文件而非物理内存。至此仍未分配任何物理内存。3.【运行时】按需分配物理内存由缺页中断驱动4.【动态管理】堆与栈的分配malloc和函数调用最初也只在虚拟空间上操作移动brk指针或扩展栈指针。只有当真正读写这些新区域时才会触发第3步的缺页中断流程从而分配物理内存。对于堆分配的是普通物理页对于栈可能映射到零页Zero Page。虚拟地址空间-物理地址空间 通过页表映射(物理地址转化为虚拟地址)将虚拟地址空间提供给上层用户使用。我们的进程控制块 task_struct 中的 mm_struct 结构体是用来管理进程虚拟地址空间的包括虚拟地址空间中的代码段、数据段、堆、栈、共享库映射区等核心区域第五章3.1进程的执行task_struct还有其他变量保存进程的其他信息虚拟地址空间是 mm_struct 结构体所管理和描述的那个“对象”3.6 为什么要有虚拟地址空间用户访问的程序中的地址是连续的。而物理内存加载代码和数据的位置已经不重要了无论代码和数据加载到物理内存的哪个位置分散的还是聚集的通过虚拟地址空间的映射在我们用户看来他们都是顺序存放的1.虚拟地址空间将地址从“无序”→“有序”。2. 虚拟地址转换成物理地址。转换过程中对地址和操作进行合法性、权限等判定进而保护物理内存。因为页表中不止存放了虚拟地址空间和物理地址空间的映射关系还存放了物理地址空间权限的数据用来保护物理内存a. 野指针堆区没有分配内存但是要访问野指针指向的地址野指针指向的是虚拟内存地址虚拟内存地址要映射到物理内存地址去访问但由于没有开辟物理内存所以就没有权限访问物理地址进而保护物理内存。野指针访问失败程序就可能崩溃。b. char *str helloworld; str H; 修改常量字符串导致程序崩溃原因字符串常量存放在字符常量区字符常量区在正文代码和初始化数据区之间跟正文代码编在一块的则字符常量区的页表权限是只读的所以当我们对字符串常量进行修改的时候程序会崩溃。为什么在字符常量区写入会崩溃查找页表的时候权限拦截了这是操作系统的概念并不只针对某种编程语言。3. 让进程管理 和 内存管理进行一定程度的解耦合。为了实现安全性、可靠性、高效性和灵活性。例如: 进程使用realloc时只需要对mm_struct的对象修改不需要对PCB修改方便内存管理补缺页中断后面讲3.7 澄清一些问题1. 我们可以不加在代码和数据只有tast_structmm_struct页表2. 创建进程先有task_structmm_struct等还是先加载 代码和数据先有数据结构3. 如何理解进程挂起比如阻塞挂起找到进程页表将物理内存换出到磁盘并将页表标记为无效。此时PCB和页表仍然存在。4.虚拟内存管理在虚拟地址空间中堆区通常表现为一个连续的大区域由一个主要的vm_area_struct描述。malloc等内存分配器在这个连续的虚拟地址空间内进行管理。然而支撑这个虚拟堆区的物理内存页框是不连续的、分散的。mm_struct通过管理一个由vm_area_struct组成的链表/红黑树来管理整个虚拟地址空间中的所有区域包括代码段、数据段、堆、栈以及每个通过mmap分配的独立内存块4. 理解几个概念可以这几个概念放在一起理解会更顺。1.mm_struct是什么mm_struct是 Linux 内核中用来描述一个进程虚拟地址空间的重要结构体。大白话理解task_struct是进程的“总档案”而mm_struct是其中专门负责记录“这个进程的内存怎么分布”的档案。它里面会记录很多和内存有关的信息比如代码段在哪里 数据段在哪里 堆在哪里 栈在哪里 页表在哪里 虚拟地址空间有哪些区域可以粗略理解成task_struct | --- mm_struct | |-- 代码段 |-- 数据段 |-- 堆 |-- 栈 |-- 页表 |-- 内存映射区域所以一句话mm_struct就是 Linux 用来管理一个进程虚拟地址空间的核心结构。2. 页表是什么页表可以理解成虚拟地址到物理地址的“地址翻译表”。程序使用的是虚拟地址例如0x400000但内存条真正访问的是物理地址因此 CPU 需要知道虚拟地址 ↓ 对应哪一块物理内存这个映射关系就记录在页表中。更准确地说页表中的每一项叫PTE Page Table Entry 页表项一个页表项不仅保存这个虚拟页对应哪个物理页还会保存权限信息和状态信息。可以理解成页表项 PTE │ ├── 地址信息 │ └── 对应哪个物理页 │ ├── 权限信息 │ ├── 能不能写 │ ├── 用户态能不能访问 │ └── 能不能执行 │ └── 状态信息 ├── 这一页是否在物理内存中 ├── 有没有被访问过 └── 有没有被修改过常见的信息例如信息作用Present这一页当前是否在物理内存中Read/Write是否允许写User/Supervisor用户态是否可以访问NX是否禁止执行代码Accessed这一页是否被访问过Dirty这一页是否被修改过比如一个代码页可能是可读是 可写否 可执行是一个数据页可能是可读是 可写是 可执行否所以可以记页表项 物理页地址 权限信息 状态信息。3. 虚拟地址怎么映射到物理地址现代操作系统不是一个字节一个字节地映射而是按“页”进行管理。常见页大小例如4KB假设程序访问虚拟地址0x12345678CPU 会把它拆成虚拟页号 页内偏移然后虚拟地址 ↓ 找到属于哪个虚拟页 ↓ 查页表 ↓ 找到对应的物理页 ↓ 加上页内偏移 ↓ 得到最终物理地址例如虚拟地址 虚拟页 3 偏移 100页表告诉 CPU虚拟页 3 → 物理页 8最终访问物理页 8 偏移 100真正负责地址转换的是 CPU 中的MMUMemory Management Unit内存管理单元整体就是程序访问虚拟地址 ↓ MMU ↓ 查页表 ↓ 检查地址和权限 ↓ 找到对应物理页 ↓ 访问物理内存4. 为什么要用多级页表如果所有虚拟地址都放在一张巨大的页表中会非常占内存。所以现代 Linux 一般采用多级页表。可以理解成查目录一级目录 ↓ 二级目录 ↓ 三级目录 ↓ …… ↓ 找到最终页表项 PTE ↓ 找到物理页这样不用一次创建一张特别大的完整页表可以节省大量内存。5. 什么是缺页中断如果程序访问一个虚拟地址CPU 查页表时发现这个虚拟页当前没有有效的物理页映射CPU 就无法继续完成访问于是会触发缺页异常 / 缺页中断Page Fault大白话就是“程序要访问这块内存但是现在页表处理不了先交给操作系统看看怎么办。”例如int* p malloc(4096);malloc()之后系统可能只是先给进程安排好了虚拟地址空间并没有立刻分配真实物理页。当真正执行*p 10;可能出现访问虚拟地址 ↓ MMU 查页表 ↓ 发现没有对应物理页 ↓ 触发 Page Fault ↓ 进入内核 ↓ 内核判断地址是否合法 ↓ 合法 ↓ 分配物理页 ↓ 修改页表 ↓ 重新执行刚才的指令程序就可以继续运行。6. 缺页不一定表示程序出错缺页异常本身不一定是错误。比如下面这些情况都可能正常触发 Page Faultmalloc 后第一次真正访问 mmap 文件后第一次访问 写时拷贝 COW内核处理完以后程序继续运行。但如果访问的是非法地址int* p NULL; *p 10;流程可能变成触发 Page Fault ↓ 内核检查 ↓ 发现地址非法 ↓ 发送 SIGSEGV ↓ Segmentation fault7. 把几个概念串起来可以直接记这一条task_struct ↓ mm_struct ↓ 描述整个进程的虚拟地址空间 ↓ 页表记录虚拟页 → 物理页的映射 并保存权限、状态等信息 ↓ MMU 根据页表进行地址转换和权限检查 ↓ 如果映射不存在或权限不允许 ↓ 可能触发 Page Fault ↓ 进入内核处理一句话总结mm_struct管整个进程的虚拟内存布局页表负责记录虚拟页到物理页的映射同时保存权限和状态信息MMU 根据页表完成地址转换和权限检查如果映射不存在或者访问不符合页表权限就可能触发缺页异常由内核进一步处理。8. 页表权限和缺页中断的联系有而且联系非常直接。可以这样理解CPU 查页表时不仅看“这个虚拟页映射到哪个物理页”还会顺便检查“你有没有权限这样访问”。如果权限不允许也可能触发 Page Fault。比如某个页表项规定可读是 可写否这时候程序去写*p 10;CPU 会发现这个地址虽然有物理页映射 但是当前不允许写于是触发Page Fault进入内核处理。这正是写时拷贝 COW 能实现的关键之一。父子进程刚fork()后共享的数据页会暂时设成只读父进程 ─┐ ├── 同一个物理页只读 子进程 ─┘如果子进程要写CPU检查页表权限 ↓ 发现不可写 ↓ 触发 Page Fault ↓ 内核发现这是 COW ↓ 复制一份物理页 ↓ 修改子进程页表 ↓ 把新页设成可写 ↓ 继续执行所以你可以记成缺页异常不只是“页不在内存”才会发生访问权限不符合页表要求时也会触发。一句话总结页表里的权限位是 CPU 判断访问是否合法的重要依据权限不匹配时会触发 Page Fault再由内核判断这是正常情况如 COW还是非法访问如段错误。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑