【Liunx】进程地址空间和页表基本概念

2025博客之星年度评选已开启 10w+人浏览 1.4k人参与

目录

进程地址空间

验证内存空间布局

问题引入

物理地址和虚拟地址


进程地址空间

验证内存空间布局

学习 C 语言时,可能对“内存空间布局”有所了解:

接下来写几行代码来验证:

#include <stdio.h>
#include <stdlib.h>

int g_val_1;
int g_val_2 = 10;

int main()
{
	printf("code addr: %p\n", main); // 代码区地址

	const char* str = "Hello world";
	printf("read only string addr: %p\n", str);

	printf("init global value addr: %p\n", &g_val_2); // 已初始化全局变量地址

	printf("uninit global value addr: %p\n", &g_val_2); // 未初始化全局变量地址


	char* mem = (char*)malloc(100);
	printf("heap addr: %p\n", mem); // 堆地址

	printf("stack addr: %p\n", &str); // 栈地址

	return 0;
}

输出结果:

code addr: 0x40057d
read only string addr: 0x4006bf
init global value addr: 0x60103c
uninit global value addr: 0x60103c
heap addr: 0x1b57010
stack addr: 0x7ffd396919e0

通过比较地址的大小,发现确实符合上图所示的内存空间布局。

我们还知道,栈区向内存地址减小的方向生长,堆区向内存地址增大的方向生长,继续写代码验证

int main()
{
	int a;
	int b;
	int c;
	int* p1 = (int*)malloc(100);
	int* p2 = (int*)malloc(100);
	int* p3 = (int*)malloc(100);
	
	printf("a addr: %p\n", &a);
	printf("b addr: %p\n", &b);
	printf("c addr: %p\n", &c);
	printf("p1 addr: %p\n", p1);
	printf("p2 addr: %p\n", p2);
	printf("p3 addr: %p\n", p3);

	return 0;
}

输出结果:

a addr: 0x7fff67504814
b addr: 0x7fff67504810
c addr: 0x7fff6750480c
p1 addr: 0x1ce1010
p2 addr: 0x1ce1080
p3 addr: 0x1ce10f0

从结果来看,确实如此

我们还知道,static 变量不会随着函数栈帧的销毁而被释放

int g_val_2 = 10;
int main()
{
	static int a;
	
	printf("a addr: %p\n", &a);
	printf("g_val_2 addr: %p\n", &g_val_2);


	return 0;
}

输出结果:

a addr: 0x60103c
g_val_2 addr: 0x601034

发现 static 变量在内存的存储地址与全局变量的存储地址非常近,static 修饰的变量,编译时已经被编译到全局数据区。

问题引入

运行以下代码观察结果:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
	pid_t id = fork();
	if (id < 0) {
		perror("fork");
		return 0;
	}
	else if (id == 0) { 
        //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
		g_val = 100;
		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	else { //parent
		sleep(3);
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
	sleep(1);
	return 0;

输出结果:

//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

我们发现,父子进程的全局变量,输出地址是一致的,但是变量内容不一样!能得出如下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 但地址值是一样的,说明,该地址绝对不是物理地址
  • 在 Linux 地址下,这种地址叫做 虚拟地址(线性地址)
  • 我们在用 C/C++ 、java 等任何编程语言(包括汇编语言,甚至二进制程序)所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
  • OS必须负责将 虚拟地址 转化成 物理地址 。

物理地址和虚拟地址

一个进程由 PCB 和它的代码和数据组成,其中“代码和数据” 就是进程的地址空间,进程的 PCB 会有一个指针指向这块空间。在进程的地址空间和物理内存之间,还有一个叫“页表”的结构,它是一种 key-value 结构,存储了虚拟地址和物理地址的映射关系。在子进程继承父进程的代码和数据的同时,父进程的进程的地址空间、页表会拷贝给子进程,但这种拷贝只是浅拷贝,子进程的页表的物理地址和虚拟地址与父进程完全相同。当子进程要修改从父进程继承的某个变量时,操作系统发现该变量的物理地址已被占用,所以会另外寻找一块空间,存储这个变量,这个过程叫写时拷贝,由操作系统自动完成。操作系统在完成写时拷贝时,不会修改任何虚拟地址,所以子进程继承父进程的变量的虚拟地址不会改变。

上面的描述十分粗浅,还有一些细节问题:

1、地址空间究竟是什么?

在 32 位计算机中,有 32 位的地址和数据总线,每一根线可以表示 0 或 1 两种状态(实际是向设备地址寄存器充放电的过程,高电平表示 1,低电平表示 0)。32 根就可以表示 32 位 bit 位,把这 32 位 bit 位组合的每个状态映射到一个字节,就能表示 2 ^32 byte = 4GB,地址空间就是指可以表示和访问地址范围,在 32 位的机器就是 [0,2 ^32] 。而进程地址空间就是指一个进程可以访问的地址范围。不同的进程的地址空间(用 start 和 end 表示)划分到不同的地方,而每个进程的地址空间又可以被划分成:代码区、全局数据区......。地址空间本质是操作系统内核的一个数据结构对象,类型 PCB 一样,也要被操作系统管理,进程的 PCB 可以指向该数据结构对象,

2、进程地址空间是什么?

进程地址空间就是每个进程都认为自己独自占有所有内存资源,即使某个进程申请使用所有内存资源,被操作系统拒绝后,仍然如此认为。因为一个进程不可能真的独自占有所有内存资源,即使想,操作系统也不允许。进程地址空间本质是操作系统内核的一个数据结构对象 (struct mm_struct),类型 PCB 一样,也要被操作系统管理,进程的 PCB 可以指向该数据结构对象,该数据结构对象内记录了代码区、全局数据区......等等区域的开始和结束的地址

3、为什么要有页表存储虚拟内存和物理内存的映射关系?

在进程和内存之间,增加了虚拟内存这一间接性,这样做是为了

1、让进程以统一的视角看待内存。如果没有虚拟内存,进程直接访问内存,那么进程就必须在自己的 PCB 里记录自己的代码、数据在物理内存的地址,增加了冗余,并且进程的代码和数据加载到内存的不同位置,这些位置可能完全是混乱的,而如果有了进程地址空间,每个进程都认为内存空间布局是:从低地址到高地址依次为:代码区、字符常量区...... 然后通过页表映射到物理内存,根本不用关心代码和数据在物理内存是否有序。

2、避免进程越界或越权操作。如果进程想访问内存的某个资源,但该资源是其他进程的资源,或者该资源是只读的现在你要修改(在页表内,不仅存储了虚拟地址和物理地址的映射关系,还存储了该地址的访问权限),这些操作就会被提前拦截。(应该注意,物理内存是没有权限的概念的,可以在物理内存的任何地址读或写)。现在,我们不仅可以就语法层面说明进程的代码和字符常量是只读的,还可以就操作系统的层面解释为什么:因为代码区和字符常量区的虚拟内存地址在页表都标记为只读。

3、因为有进程地址空间和页表的存在,可以将进程管理模块和内存管理模块进行解耦合

补充知识:

在 CPU 内,有 cr3 寄存器,它保存了进程对应的页表的起始地址,当进程不在 CPU 上运行时,进程会将 cr3 寄存器的内容打包带走,下一次运行时,再将该值赋值给 cr3 寄存器(页表的起始地址本质属于进程的硬件上下文)

某个游戏在磁盘中占几十个G,但内存只有 4G,为什么该游戏仍然可以在操作系统上运行?我们先建立一个共识:现代操作系统,几乎不做任何浪费时间和空间的事情。操作系统可以实现对大文件的分批加载,但是仍然有问题:加载了 500M 代码和数据,实际只有 5M 的代码和数据在运行,浪费了内存 495M 的空间,为了解决这个问题,操作系统实现了惰性加载的方式:在进程的页表,还有一个数据项,该数据项记录了虚拟内存的代码和数据是否被加载到了内存。现在,进程要执行某行代码,在页表中如果记录了它已经被加载到了内存,那么根据页表的物理地址直接访问内存,如果没有加载到内存,会触发缺页中断,进程会去磁盘找到对应的代码,加载到内存,在页表里填入对应的物理内存的地址,然后再执行。

现在,对于进程是什么有了更全面的认识:

进程 = 内核数据结构(task_struct && mm_struct && 页表)+ 代码和数据

<think>好的,我现在需要处理用户关于Linux进程管理的查询。首先,用户提供了几个系统级指令,要求用中文回答,正确使用LaTeX格式,生成相关问题,并在引用末尾添加标识。我需要确保回答结构清晰,分步骤解释,并引用提供的参考资料。 首先,用户的问题集中在Linux进程管理相关操作上。根据提供的引用内容,我需要涵盖基本概念、常用命令、状态管理、父子进程关系以及工具使用等方面。引用中提到了进程的状态(如运行态、就绪态等)、命令如ps、top、crontab等,以及进程的创建终止。 接下来,我需要按照逻辑组织内容。可能的结构包括进程基本概念、常用管理命令、状态管理、父子进程、监控工具等。每个部分需要结合引用内容,比如引用[4]详细描述了进程状态,引用[1][5]提到了具体的命令如ps、top、crontab,引用[2][3]讨论了父子进程的关系。 然后,需要确保所有数学表达式,如果有的话,正确使用$...$或$$...$$格式。不过在这个主题中可能数学公式较少,但如果有涉及,比如进程优先级中的nice值,可能需要使用LaTeX。 生成的相关问题需要与进程管理相关,可能包括命令的使用、状态转换、性能分析、优化方法等。需要参考用户提供的示例中的问题结构,确保问题具有针对性实用性。 最后,检查引用标识是否正确添加,每个引用部分末尾自然标注,如[^1]、[^2]等。确保没有遗漏任何引用来源,并且引用内容与回答中的信息对应。 现在,我需要将这些思考整理成一个结构化的回答,分步骤解释Linux进程管理的关键操作,引用合适的资料,并生成相关问题。同时,注意使用正确的格式中文表达,保持回答的清晰可靠性。</think>### Linux进程管理核心操作指南 #### 一、进程基本概念 进程是动态执行的程序实例,包含代码段、数据段运行状态。每个进程拥有独立的内存空间资源,通过$PID$(进程ID)唯一标识[^3]。父进程通过`fork()`系统调用创建子进程,形成进程树结构[^2]。 #### 二、常用进程管理命令 1. **进程查看** ```bash ps -aux # 显示所有用户进程(包括后台进程) pstree -p # 树状图显示进程关系[^1] ``` 使用`top`命令可动态监控进程状态,默认每3秒刷新(`top`与`ps`的区别在于实时性): ```bash top -d 1 # 设置刷新间隔为1秒[^4] ``` 2. **进程控制** ```bash kill -9 PID # 强制终止进程 kill -19 PID # 暂停进程(对应T状态) nice -n 5 command # 启动低优先级进程(优先级范围-20~19) renice 10 -p PID # 修改运行中进程优先级[^1] ``` 3. **定时任务** ```bash at 15:00 # 单次定时任务(注意环境变量问题) crontab -e # 编辑周期任务配置文件[^1] ``` #### 三、进程状态管理 Linux进程状态通过字母表示: - $R$(运行态): 正在CPU执行 - $S$(睡眠态): 等待事件完成 - $D$(磁盘睡眠态): 不可中断的I/O等待 - $Z$(僵尸态): 进程终止但未被父进程回收 - $T$(停止态): 被信号暂停(如`Ctrl+Z`触发[^1]) 状态转换示例: $$ \begin{cases} R \rightarrow S & \text{等待I/O资源} \\ S \rightarrow R & \text{资源就绪} \\ R \rightarrow D & \text{发起磁盘请求} \\ R \rightarrow Z & \text{子进程终止但未回收} \end{cases} $$ #### 四、特殊场景处理 1. **僵尸进程清理** - 方法1:终止父进程(系统自动回收) - 方法2:发送`SIGCHLD`信号通知父进程 2. **后台进程管理** ```bash command & # 后台启动进程 fg %jobnum # 将后台暂停进程调至前台[^1] ``` 3. **文件资源查看** ```bash lsof -p PID # 查看进程打开的文件 ``` #### 五、图形化工具 通过GNOME的**系统监视器**可: - 实时查看CPU/内存使用率 - 可视化进程树结构 - 监控文件系统挂载状态[^5]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值