彻底理解 fork 之写时复制 《一》

本文探讨了操作系统中fork调用所运用的写时复制技术,解释了该技术如何在不影响性能的前提下节省资源,特别是在父子进程间共享内存时的作用。通过具体的代码示例和深入分析,展示了写时复制的工作原理及其在Linux系统中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

彻底理解 fork 之写时复制 《一》


一直以来都对操作系统都比较感兴趣,这篇文章呢就主要研究一下当我们调用fork系统掉用所用到的写时复制技术(copy-on-write)。

下图是fork系列函数的调用过程

在这里插入图片描述

<摘自网络 侵删>


写时复制,其实在很多地方都会用到,我们先来看看关于字符串使用写时复制的例子吧。

	写时拷贝故名思意:是在写的时候(即改变字符串的时候)才会真正的开辟空间拷贝(深拷贝),如果只是对数据的读时,只会对数据进行浅拷贝。
	写时拷贝:引用计数器的浅拷贝,又称延时拷贝
	:写时拷贝技术是通过"引用计数"实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真的释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时引用计数的变化,旧的空间的引用计数减一,新分配的空间引用计数加一)。

基于此 我们来再看看调用 fork 时需要使用的写时复制技术吧!

其实这块比较有意思,系统需要处理的事情太多,处理任务一般都采用最懒惰的策略,在网上也看了几个证明写时复制的例子,但感觉并不严谨,并不能来证明。比如这个高票回答

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
 
void main()
{
    char str[6]="hello";
 
    pid_t pid=fork();
 
    if(pid==0)
	{
        str[0]='b';
        printf("子进程中str=%s\n",str);
        printf("子进程中str指向的首地址:%x\n",(unsigned int)str);
    }
    else
	{
        sleep(1);
        printf("父进程中str=%s\n",str);
        printf("父进程中str指向的首地址:%x\n",(unsigned int)str);
    }
}


/**结果**/
子进程中str=bello
子进程中str指向的首地址:bfdbfc06
父进程中str=hello
父进程中str指向的首地址:bfdbfc06
/******/

/先解释一下程序的两个比较容易让人困惑的点/

  1. 通常情况下,进程都会有独立的地址空间,为了提高系统的资源利用率,我们所使用的地址都是虚拟地址,(具体内容不在此处讨论),控制台打印出来的地址是虚拟地址(我们用户能看到的),真正的物理地址是给cpu看的,虚拟地址与真实物理地址之间是有一个对应关系的。
    每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。

  2. 为什么会打印两次
    这明明是一个互斥的选择语句,哦天哪,难道这个fork返回了两个值,这不科学啊,这颠覆了函数只有一个返回值的真理啊,哈哈哈,不用担心,原因是fork之后产生了两个进程,屏幕上的两句话,是fork在两个进程分别返回,所以打印两次。


首先明确一点,因为str的数据改变了,str所在的页面操作系统会给子进程重新分配,为什么打印出来的地址是一模一样的,请记住,fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。 这也就是我所说的开头的代码是无法证明写时复制的,别急,往下看!


为什么需要写时复制呢?

直接给进程分配独立的地址空间不就更省事了嘛,效率也高,但是请记住一点,商业的东西都是需要成本控制的,内存在电脑体系中是非常宝贵的资源(手动狗头三星),所以Linux等人,要想方设法节省资源,提高资源利用率,好明白了这一点,重点就来了,假如父子进程对原有的所有页面是无任何改变,也就是说对数据是只读的,没有写过,那么懒懒的操作系统是不是根本没有必要为了子进程再开辟一块物理空间(页面),所以说子进程里的页表是和父进程的页表是一模一样的即其中的逻辑地址对应的物理地址是一模一样的,也就是所谓的页面共享。这时我们如果改变策略,改变了某一个共享页面的某一项数据,那么此时这个页面已经无法被共享了,会发生什么呢?

具体过程是这样的:

fork子进程完全复制父进程的栈空间,也复制了它的内存分配页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”(类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面,直到其中任意一个进程要对共享的页面进行“写操作”,这时内核会 分配+复制 一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。画重点,操作性同在内存分配这块非常懒,仅仅只分配"新的页面" 并在页表里边改变相关属性

这就是所谓的“写时复制”。正因为fork采用了这种写时复制的机制,所以fork出来子进程之后,父子进程哪个先调度呢?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,会清空栈、堆。。这些和父进程共享的空间,加载新的代码段。这就避免了“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以在这种情况下,一般是子进程先调度滴(欢迎指正),这个关于父子进程谁先执行是一个比较复杂的问题不是本篇重点,需要考虑很多因素,这个我们之后再细细研究。


所以我们如何来证明写是复制呢,没错,寻找进程页面对应的物理地址,如果在更改字符串之后,那么会有一个新的物理页面产生,所以我们来搞一搞!!!

关于这个问题有什么方法呢,先抛出一个引子,进程在磁盘里的实体就在/proc/pid.

限于篇幅,这个实验请关注

彻底理解fork之写时复制<二>

1. 进程控制 1. 参照参考程序编程序,其中父进程无限循环。在后台执行该程序。 [root@localhost lab2]# vi fork_1.c [root@localhost lab2]# gcc fork_1.c-o def [root@localhost lab2]# ./def & [1] 3620 def fork_1.c 子进程用exec( ) 装入命令ls,exec( ) 后,子进程的代码被ls的代码取代,这子 进程的PC指向ls的第1条语句,开始执行ls的命令代码。 如果是下面这种: [root@localhost lab2]# ./def 则只有用∧c强行终止父进程,则在第2步结果就不同了,父子进程都成为僵死进 程。 2. 显示当前终端上启动的所有进程。 [root@localhost lab2]# ps-af UID PID PPID C STIME TTY TIME CMD root 3620 3126 0 20:11 pts/1 00:00:00 ./def root 3621 3620 0 20:11 pts/1 00:00:00 [ls <defunct>] root 3623 3126 0 20:12 pts/1 00:00:00 ps-af 10 湘潭大学 操作系统实验指导书 其中,”<defunct>”说明进程”ls”是个僵死进程。父进程创建个子进程后,可 以用wait() 等待回收其子进程的资源,也可以正常终止后由系统回收其子进程的资源。” ./def ”就是”ls”的父进程。”./def ”进程中,既没有等待回收其子进程的资源,也没 有正常终止,因此造成”ls”成为个僵死进程。 3. 用 kill 命令直接杀死该子进程,看看是否能够成功? [root@localhost lab2]# kill-9 3621 [root@localhost lab2]# ps-af 为什么不能杀死?因为父进程还没结束。用kill命令杀死父进程之后呢?解释原因。 [root@localhost lab2]# kill-9 3620 [root@localhost lab2]# ps-af 4. 修改程序 fork_1.c,在父进程执行”while(1)”之前,添加代码”wait(0);”。 在后台执行该程序。显示当前终端上启动的所有进程。解释原因。 5. 修改以上程序,在子进程执行”printf(“Isson:\n”);”之前添加代码“sleep(1);”。 观察多进程的执行序列,解释原因。
06-05
<think>嗯,用户问的是Linux进程控制相关的问题,包括父进程和子进程的关系,exec、wait、sleep函数的作用,以及僵死进程的处理。这个问题涉及操作系统进程管理的核心概念,需要结合多个知识模块系统回答。用户似乎正在学习Linux系统编程,可能遇到了进程控制的实践问题。从引用内容看,用户已经了解孤儿进程、僵尸进程的基本定义,以及wait函数的阻塞特性,但需要更深入的行为分析。用户特别强调"行为分析",说明ta更关注实际操作中进程间的交互和状态变化。核心需要拆解五个技术点:首先是fork-exec模型的行为特征,这里要强调父子进程的代码段差异;第二是僵尸进程的产生机制,必须明确只有父进程未回收才会出现;第三是wait的阻塞/非阻塞模式对进程调度的影响;第四是sleep如何通过调度器改变执行序列;最后是kill信号处理与进程终止的关联。特别要注意用户引用的[3]提到阻塞等待的示例,回答需要扩展这种情况的优化方案。而[4]中wait的三功能总结很到位,可以融入回答框架。僵尸进程的解决方案要突出waitpid非阻塞模式的实用性,这正好回应用户引用的"什么是非阻塞等待"的疑问。技术难点在于说清楚exec如何保持"进程外壳替换程序内芯"的特性——需要解释PCB、内存映射的变化,但又要避免过于底层。sleep和kill部分可以共用调度器的逻辑主线,体现信号与进程状态的关联。用户应该需要示例代码辅助理解,但当前问题偏理论,所以采用文字描述典型场景即可。最后补充kill处理僵尸进程的方案,要暗示这是次优解,回收资源才是根本,避免用户形成错误习惯。</think> 在Linux进程控制中,父进程与子进程的行为涉及创建、执行控制、资源回收和状态同步等核心机制。以下是关键技术的深度分析及示例: --- ### **1. 父进程与子进程的行为** - **创建关系** 父进程通过 `fork()` 复制自身创建子进程(引用[2]原理): ```c pid_t pid = fork(); if (pid == 0) { // 子进程代码 } else if (pid > 0) { // 父进程代码 } ``` 子进程拥有独立的PCB,内核平等调度父子进程(引用[2])。 - **执行差异** 子进程常通过 `exec` 系列函数加载新程序: ```c execlp("/bin/ls", "ls", "-l", NULL); // 子进程替换为ls命令 ``` 此子进程的**代码段被完全覆盖**,但PCB、文件描述符等资源保留。 --- ### **2. 僵死进程的产生与解决** - **产生原因** 子进程终止后,若父进程**未调用 `wait/waitpid` 回收资源**(引用[1][4]),子进程成为僵尸进程(Zombie),占用系统进程表资源。 - **解决方案** - **阻塞回收**(引用[3][4]): ```c wait(&status); // 父进程阻塞等待任意子进程终止 ``` - **非阻塞回收**: ```c waitpid(-1, &status, WNOHANG); // 无等待子进程立即返回 ``` --- ### **3. 关键函数行为分析** | **函数** | **作用** | **对进程行为的影响** | |-------------|-------------------------------------------------------------------------|-----------------------------------------------| | `exec` | 替换当前进程的代码和数据段 | 子进程执行新程序,但PID和资源保持不变 | | `wait` | 阻塞父进程直至子进程终止(引用[3][4]) | 避免僵尸进程,同步父子进程执行顺序 | | `sleep(n)` | 暂停当前进程执行至少n秒 | 主动让出CPU,触发进程调度器选择其他进程运行 | | `kill(pid)` | 向指定PID发送信号(如SIGTERM终止进程) | 可终止僵尸进程的父进程,使init接管其资源(引用[1]) | --- ### **4. Sleep与进程调度** 当进程调用 `sleep(5)`: 1. 进程状态从 **RUNNING → SLEEPING** 2. 内核将其移出就绪队列,设置5秒后唤醒 3. 调度器选择其他进程运行 4. 唤醒后状态变为 **READY**,等待下次调度 → **父子进程的代码段因`sleep`出现交错执行**,实际执行顺序由调度策略决定。 --- ### **5. 核心问题应对策略** - **僵尸进程预防** - 父进程必须调用 `wait/waitpid`(引用[4]) - 若父进程不回收,终止父进程可使子进程由init接管并自动回收(引用[1]) - **孤儿进程处理** 父进程先终止 → 子进程被init领养 → init自动调用wait回收(无害) - **非阻塞等待应用** ```c while (waitpid(pid, &status, WNOHANG) == 0) { // 父进程可继续处理其他任务 } ``` --- **典型场景模拟**: 父进程创建子进程 → 子进程sleep(5)后退出 → 父进程阻塞在wait处 → 5秒后回收子进程资源。 若父进程**跳过wait直接结束**,则子进程成为僵尸进程。 > **关键结论**:僵尸进程的根源是父进程**未履行回收职责**,通过显式调用 `wait` 系列函数或利用init进程兜底机制可彻底避免资源泄露[^1][^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值