2. 进程的基本概念和初步使用 fork 函数 📅 2026/7/15 4:42:47 目录一、基本概念二、进程控制块 - PCB三、通过命令行和代码来查看进程相关信息3.1. 命令行形式3.2. 代码形式四、初步介绍 fork 函数4.1. 使用 fork 函数4.2. 解释4.3. 返回值介绍4.4. 解释一、基本概念在多道程序环境下允许多个程序并发执行但并发性破坏了程序原本的封闭性使其执行过程呈现间断性并可能导致结果不可再现。为有效描述和控制程序的并发行为操作系统引入了进程Process这一核心概念以支撑操作系统的两大基本特性并发性与共享性。说的真高大上但是其实就是内核数据结构对象 自己的代码和数据二、进程控制块 - PCB进程创建时操作系统为其分配一个 PCB该结构常驻内存可随时被系统访问并在进程终止时被回收。PCB 是进程存在的唯一标志系统唯有通过它才能感知并管理进程。在Linux 中 PCB 是 task_struct他的里面相关属性标示符: 描述本进程的唯一标示符用来区别其他进程。状态: 任务状态退出代码退出信号等。优先级: 相对于其他进程的优先级。程序计数器: 程序中即将被执行的下一条指令的地址。内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针。上下文数据: 进程执行时处理器的寄存器中的数据[休学例子要加图CPU寄存器]。I/O状态信息: 包括显示的I/O请求分配给进程的I/O设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。其他信息组织形式双向链表组成三、通过命令行和代码来查看进程相关信息3.1. 命令行形式/proc 中是各种进程的信息输入 即可ls /proc/我们也可以查看已经运行的进程写一个简单的代码#include iostream #include cstdio #include unistd.h using namespace std; int main() { while (true) { sleep(1); } return 0; }运行之后输入ps -aux | grep test其中第一条信息是 刚启动的进程而第一条信息则是 grep test 查询语句按照上述的命令行操作不管有没有查询到 对应的进程都一定有 这个 grep xxx 查询语句。3.2. 代码形式#include sys/types.h #include unistd.h pid_t getpid(void); pid_t getppid(void);getpid() 获得是 当前进程的 id 信息getppid() 获得是 父进程的id信息#include iostream #include cstdio #include unistd.h #include sys/types.h using namespace std; int main() { while (true) { sleep(1); std::cout 父进程id: getppid() 当前进程id: getpid() std::endl; } return 0; }这次我们输入这个命令ps aux | head -1 ps -aux | grep test可以清楚每个数值的含义我们可以发现 命令行的 pid 和 代码运行出来的 pid 值是一样的没有问题。当然上面的命令我们也可以使用ps ajx | head -1 ps -ajx | grep test来进行可以更好的观察到 pid 等相关信息四、初步介绍 fork 函数4.1. 使用 fork 函数命令行输入man 2 fork#include sys/types.h #include unistd.h pid_t fork(void);官方介绍fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.fork通过复制调用进程来创建一个新进程。新进程被称为子进程调用进程则被称为父进程。先写个简单的代码#include iostream #include cstdio #include unistd.h using namespace std; int main() { cout 父进程: pid: getpid() 开始创建子进程 endl; pid_t id fork(); cout 子进程: pid: getpid() 开始执行 endl; return 0; }结果可能会和大家想的不一样为什么会打印出来两个 子进程并且 子进程的 pid 不一样4.2. 解释咱们主要是利用现有的知识进行反推。1. pid 是不会骗人的它是系统分配的 调用了系统接口一定是正确的通过对应的数值我们就可以看出来 打印出来的 两个“子进程” 本质上其实是一个 父进程pid441397 一个是 子进程 (pid 441398)。2. 是怎么造成这个结果的我们看一下上面的官方的介绍fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.fork通过复制调用进程来创建一个新进程。新进程被称为子进程调用进程则被称为父进程。这个“复制” 非常有意思怎么有意思呀复制的本质是拷贝在计算机中有两种拷贝方式深拷贝和浅拷贝根据上述的 1 的分析我们其实都可以知道这个是浅拷贝。父进程 和 子进程 公用一套 代码和数据这就是为什么 会打印出 两个 “子进程” 的原因子进程这个是 不可修改的而 pid 是各自的、不同的因此我们才会发现上述的内容。3. 为什么会这样子设计呢以及什么时候子进程会有属于自己专属的代码和数据呢以后再说。4.3. 返回值介绍这个 返回值也大有讲究RETURN VALUEOn success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.翻译返回值成功时父进程中返回子进程的 PID子进程中返回 0。失败时父进程中返回 -1此时不会创建子进程并且会适当地设置 errno。在写一个对应的代码#include iostream #include cstdio #include unistd.h using namespace std; int main() { cout 父进程: pid: getpid() 开始创建子进程 endl; pid_t id fork(); if (id 0) { cerr 创建子进程失败 endl; return 1; } else if (id 0) { // child while (true) { sleep(1); cout 我是子进程 当前 pid: getpid() , 父进程id: getppid() endl; } } else { // father while (true) { sleep(1); cout 我是父进程 当前 pid: getpid() , 父进程id: getppid() endl; } } cout main函数进程 id: getpid() endl; return 0; }看完这个代码其实 很多人 都已经云里雾里了我们会先抛出下面几个问题以及一些名词不可能同时讲完但是会引出一些观点和知识。问题为什么 fork 给父进程和子进程返回各自不同的返回值为什么一个函数会返回两次为什么一个变量既等于 0 又 大于 0 导致 if else 同时成立4.4. 解释1. 为什么 fork 给父进程和子进程返回各自不同的返回值换而言之就是系统是怎么区分 子进程 和 父进程的答案就是不同的返回值。在父进程中fork() 返回子进程的 PID是大于0的父进程需要知道 子进程 的 pid对 子进程进行管理例如杀死进程等待进程等操作而 在子进程中fork() 返回0也比较好理解fork 返回值是整数0 的是 子进程的 pid 0 表示失败只剩下 0所以子进程拿到的返回值就是 0.2. 为什么一个函数会返回两次。这个现象和上面的很像上面的 4.1. 中的例子我们已经可以发现 fork() 创建的本质其实是浅拷贝在一个堆区中执行了相同的打印代码。而这次的代码而是更好的揭露里面的一个内容fork在内核中只被调用了一次但返回动作发生了两次发生在两个不同的进程中。具体的过程就是父进程执行到 fork()进入到内核中内核开始复制父进程的 PCB、内存数据、文件描述符等创建出一个几乎一模一样的子进程。内核创建完子进程后父进程和子进程都会从内核退出到当前的main中并执行“系统调用返回”指令其中父进程返回内核将子进程的 PID写入父进程的 id 变量所在的内存位置。子进程返回内核将 0 写入子进程的 id 变量所在的内存位置其中父子进程的 id 变量位于各自的独立内存空间互不干扰。3. 为什么一个变量既等于 0 又 大于 0 导致 if else 同时成立一个变量变量的地址指向与一个物理地址中此地址中存储具体的二进制。已知同一个物理地址中是无法等于两个数值的只能满足一个 if else 条件因此我们可以得出一个结论这一定是两个物理地址并且父进程一个物理地址子进程另一个物理地址只不过他们的变量名字都是 fork() 的返回值id我先用代码来证明这一点#include iostream #include cstdio #include unistd.h using namespace std; int gval 100; int main() { printf(父进程开始执行,pid:%d\n, getpid()); pid_t id fork(); if (id 0) { perror(fork failure); return 1; } else if (id 0) { // child printf(我是一个子进程我的pid: %d, 我的父进程id: %d,gval: %d\n, getpid(), getppid(), gval); sleep(5); while (1) { sleep(1); printf(子进程修改变量: %d-%d\n, gval, gval 10); gval 10; printf(我是一个子进程我的pid: %d, 我的父进程id: %d\n, getpid(), getppid()); } } else { // father while (1) { sleep(1); printf(我是一个父进程我的pid: %d, 我的父进程id: %d,gval: %d\n, getpid(), getppid(), gval); } } printf(进程开始执行,pid:%d\n, getpid()); return 0; }我们可以发现这个 gval 全局变量在不同进程的中值是可以被任意修改的且不同进程不一样这个是怎么回事说明每个进程都有他独有的代码和数据也就是此时 拷贝由原来的浅拷贝改变为了深拷贝我们知道代码是不变的 数据是可以发生变化的其中 父子进程中有一个数据发生了改变那么此时OS 会把 被修改的数据在底层中拷贝一份让目标进程去进行修改此时这个过程就叫做写时拷贝。完