【linux】进程概念(2)Linux进程的生命密码:从fork到完全独立

在计算机中完成任何操作都是进程,当用终端打开Linux时,会打开大多数发行版Linux的默认解释器-----bash。bash本事也是一个进程,且前文中我所提到的全部进程都是由bash创建的子进程。bash通过什么创建子进程呢?

fork

fork是一个系统调用,用于创建子进程,调用fork的进程叫父进程,被创建的进程叫子进程。

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
        printf("我是一个进程:%d\n",getpid());
        fork();
        printf("我是一个进程:%d\n",getpid());
}

有两个进程pid,说明确实创建了一个子进程。进程=PCB+代码和数据。在fork之后,OS会给子进程分配PCB,子进程会继承拷贝父进程的代码和部分数据继续执行接下来的代码,下面的代码被父进程执行一次后又被子进程执行。

如果子进程创建成功,fork会将子进程的pid返回给父进程(一个父进程可以有很多个子进程,父进程通过pid管理子进程),0会被返回给子进程。

如果要让父进程和子进程执行不同的代码,可以使用if-else结构,子进程继承父进程的代码和部分数据,但是子进程的返回值id是0,父进程的返回值id是子进程的pid大于0。注意:fork之后父子进程就分开了,虽然逻辑上代码相同,但每个进程有独立的程序计数器、寄存器上下文和内核栈。相互独立,互不影响。

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{

        printf("父进程开始运行!%d\n",getpid());
        pid_t id = fork();
        if(id==0)
        {
                while(1)
                {
                        sleep(1);
                        printf("我是子进程:%d,我的父进程是%d\n",getpid(),getppid());
                }
        }
        else if(id > 0)
        {
                while(1)
                {
                        sleep(1);
                        printf("我是父进程:%d,我的父进程是%d\n",getpid(),getppid());
                }
        }
        else
        {
                perror("fork");
                return 1;
        }
}

进程独立

深入理解进程的独立性:fork之后的写时复制机制

在前文中我们了解到,通过**fork()**系统调用创建子进程时,子进程会继承父进程的代码和数据。但这引发了一个关键问题:既然父子进程初始时指向相同的代码和数据,它们又如何保持独立性呢?

这个问题的答案在于Linux内核的一项重要优化技术:写时复制(写时拷贝)(Copy-on-Write )。

父子进程的"共享"真相

当fork()成功创建子进程后,父子进程的关系可以从两个维度理解:

虚拟地址空间相同:

子进程获得与父进程完全相同的虚拟内存布局,相同的代码段地址、数据段地址、堆栈地址;从程序视角看,它们访问相同的内存地址。

物理内存的巧妙共享:

  1. 代码段:由于代码通常是只读的,父子进程真正共享同一份物理内存中的代码页

  2. 数据区域(包括全局数据、堆、栈):

内核将这些页面的权限标记为只读,父子进程的页表项最初指向相同的物理内存页;这创建了一种"共享幻觉"。

写时复制:按需分离的智慧

写时复制的工作原理如下:

初始状态:

父进程页表 → [物理页A]

子进程页表 → [物理页A] ← 相同物理页

当父进程写入时:

  1. 父进程尝试写入只读的物理页A → 触发页错误

  2. 内核为父进程分配新物理页B

  3. 将物理页A的内容复制到物理页B

  4. 父进程页表改为指向[物理页B]

  5. 允许父进程完成写入操作

最终状态:

父进程页表 → [物理页B](已修改)

子进程页表 → [物理页A](保持不变)

写时复制通过按需分离的策略,在默认共享以节省资源的同时,实现了修改时的完全透明隔离。

为什么需要这样的设计?

这种设计主要优化了最常见的fork()使用模式:fork() + exec()

exec

exec 是 Linux/Unix 系统中一组系统调用的统称 (如 execlexecvexecvp 等),核心作用是用全新程序替换当前进程的内存空间------ 简单来说,它不会创建新进程,而是让当前进程 "抛弃" 原有代码和数据,转而执行另一个程序,且进程的 PID、父进程 ID 等核心属性保持不变。

以 Shell 执行新命令的典型场景为例,Shell 进程会先通过fork()创建子进程,随后子进程立即调用exec()加载新程序(如ls),而新程序会替换子进程的全部内存空间;如果fork()时就立即复制父进程的全部内存,就会复制 Shell 进程的几十 MB 内存,可子进程马上就会丢弃这些内存去加载ls仅几 KB 的代码,造成巨大的复制开销,而写时复制则完美解决了这个问题,它让fork()几乎瞬间就能完成,因为这个过程只需要复制页表等元数据,后续如果子进程很快执行exec(),就几乎不会产生实际的内存复制操作,只有当子进程需要修改数据时,内核才会按需为其复制对应的内存页。

父子进程的独立性体现

尽管初始时共享内存,但父子进程仍在多方面保持完全独立:二者的内存空间通过写时复制机制实现修改互不干扰,拥有各自独立的程序计数器与寄存器状态以保障执行流独立,操作系统会对它们进行独立调度,二者甚至可能在不同 CPU 核心上运行;进程属性层面,父子进程不仅拥有不同的进程 ID 与父进程 ID,还具备独立的文件描述符表(尽管子进程会继承父进程的文件描述符,但后续操作相互独立)与独立的信号处理逻辑;此外,二者的 CPU 时间、内存使用等资源统计各自独立,生命周期也互不影响,一方终止不会直接导致另一方终止。

实际编程中的体现
cpp 复制代码
int shared_data = 100;  // 全局变量

pid_t pid = fork();
if (pid == 0) {
    // 子进程
    shared_data = 200;  // 触发写时复制!
    printf("子进程: %d\n", shared_data);  // 输出200
} else {
    // 父进程
    sleep(1);  // 确保子进程先修改
    printf("父进程: %d\n", shared_data);  // 输出100,不受影响!
}
重要注意事项

并非所有资源都会采用写时复制机制:进程控制块(PCB)会被完全复制,而打开的文件描述符则是父子进程共享的,这里需要小心文件偏移量同步的问题;从性能角度考量,fork()本身的开销很小,但如果父子进程都大量修改不同的内存页面,最终还是会产生两份完整的内存拷贝;此外,fork()和早期 Unix 中的vfork()也存在区别,vfork()会让父子进程真正共享内存,且要求子进程必须立即执行exec(),不过在现代 Linux 中fork()已经得到高度优化,vfork()几乎不再需要使用。

总结

fork()与写时复制机制的配合,以高效性、透明性与灵活性的精妙平衡,让Linux在保障进程安全隔离的同时实现高效的进程创建与管理,支撑起复杂的多任务环境。

相关推荐
Trouvaille ~2 小时前
【Linux】库制作与原理(一):静态库与动态库的制作使用
linux·运维·服务器·c语言·汇编·动静态库·编译链接
松涛和鸣2 小时前
DAY37 Getting Started with UDP Network Programming
linux·c语言·网络·单片机·网络协议·udp
深信达沙箱2 小时前
常见数据泄露途径测试用例
服务器·安全·测试用例·源代码
热爱专研AI的学妹2 小时前
Coze-AI 智能体平台:工作流如何成为智能体的 “自动化引擎”?解锁零代码落地新范式
运维·数据结构·人工智能·自动化
梦想的旅途22 小时前
从 0 到 1:构建外部群自动化的全链路监控大屏
运维·自动化
HIT_Weston2 小时前
73、【Ubuntu】【Hugo】搭建私人博客:Hugo&PaperMod 兼容问题
linux·运维·ubuntu
清平乐的技术专栏2 小时前
新电脑验机工具介绍及避坑指南
运维·电脑
想唱rap2 小时前
哈希(C++)
服务器·开发语言·c++·算法·哈希算法
skywalk81632 小时前
为什么Linux系统里用户id和组id不一样?怎么改成一样呢?
linux·服务器