一、从程序到进程:操作系统究竟在管理什么?

1.1 程序和进程不是一回事

在前面的学习中,我们接触了冯·诺依曼体系结构,也了解了程序运行时需要进入内存。

先从一个熟悉的场景开始。

我们在 Linux 中编写一个 C/C++ 程序,经过编译后,磁盘上会生成一个可执行文件。例如:

g++ main.cpp -o main

此时,磁盘中的 main 是一个可执行文件。我们执行:

./main

程序开始运行。

这里需要区分两个概念:

  • 程序(Program): 通常指磁盘上的可执行文件,是一组指令和相关数据。
  • 进程(Process): 程序运行时形成的执行实体,操作系统需要跟踪它的状态、资源和执行情况。

可以把程序理解为一份菜谱,而进程是按照菜谱正在进行的一次烹饪活动。菜谱本身不会告诉你当前做到哪一步、使用了哪些工具;但要管理正在进行的烹饪活动,就必须记录这些状态。

同样,操作系统如果只知道磁盘上存在一个可执行文件,是不够的。它还必须知道:

  • 这个程序是否正在运行?
  • 当前运行到哪里了?
  • 它使用了哪些内存和文件?
  • 它什么时候能够获得 CPU?
  • 如果存在多个运行任务,应该怎样区分它们?

这就引出了进程的核心问题。

1.2 进程的本质:内核数据结构 + 程序代码和数据

教材通常会把进程概括为“程序的一次执行过程”,或者“正在执行的程序”。

这些说法没有错,但还不够具体。

从操作系统的角度看,进程不仅包含程序代码和数据,还需要一份专门的管理信息。

可以先建立一个重要的理解模型:

进程 = 描述进程的内核数据结构 + 与该进程相关的程序代码和数据。

为什么需要额外的数据结构?

假设现在有几十个程序同时运行。操作系统需要分别记录它们的标识、状态、调度信息和资源使用情况。如果没有统一的数据结构来描述每个进程,操作系统就很难有效管理这些任务。

因此,操作系统会为进程维护相应的内核管理信息。

这里要记住一句话:

操作系统管理进程,不是只盯着程序代码,而是通过描述进程的数据结构来管理它。

1.3 PCB 与 Linux 中的 task_struct

在操作系统理论中,用于描述进程的管理结构通常称为 PCB(Process Control Block,进程控制块)。

在 Linux 内核中,进程的主要管理信息由 struct task_struct 等内核数据结构描述,其中 task_struct 是我们学习进程时最常接触的结构。

可以把 PCB 理解成一份任务档案:它不等于任务本身的全部代码,而是记录这个任务需要被管理的信息。

task_struct 中包含大量字段,下面先认识其中几类重要信息。

信息类别作用
标识符区分不同的进程,例如进程 ID
状态信息描述进程当前的状态及相关退出信息
优先级与调度信息帮助内核决定如何安排进程运行
程序计数器等上下文信息与进程执行时的处理器状态有关
内存管理信息关联进程的地址空间及相关内存管理结构
上下文数据保存或关联进程执行所需的处理器寄存器状态
I/O 与文件信息记录进程使用的文件、文件描述符等相关信息
记账信息统计处理器时间等资源使用情况

这些信息共同帮助操作系统回答一个问题:这个进程是谁,它现在是什么状态,内核应该如何管理它?

需要注意,task_struct 并不是把进程的全部代码和数据都直接塞进一个结构体。它更多是记录和关联进程管理所需的信息,其中一些字段指向其他内核数据结构。

1.4 操作系统如何管理大量进程?

现在我们已经知道,操作系统需要为每个进程维护管理信息。

但如果系统中存在成百上千个进程,内核又如何找到它们并进行管理呢?

这就涉及操作系统管理的另一个重要思想:

先描述,再组织。

  • 先描述: 使用 task_struct 等数据结构描述进程。
  • 再组织: 通过内核中的组织方式,将这些进程管理起来,便于查找、调度和维护。

初学时,可以把进程管理想象成一套图书馆的借阅系统。

每一本书都有自己的档案,档案记录书名、编号、借阅状态等信息。图书馆不会为了查一本书,就把整个图书馆的所有书都重新整理一遍;它会通过索引、目录等组织方式找到对应的记录。

在操作系统中,进程的管理信息同样需要被组织起来。内核可以根据这些数据结构查找和操作进程,并完成创建、调度、退出等管理工作。

原理上,操作系统管理进程的重点不是不断直接修改源代码,而是通过进程的管理信息及相关内核机制进行管理。

这里需要补充一个准确性说明:Linux 内核并不是简单地把所有进程只放进一条普通链表。不同版本和不同功能会使用不同的数据结构,例如链表、树和其他索引结构。对于初学者来说,先理解“描述进程,再组织进程”这一核心思想即可。
在这里插入图片描述

1.5 理解进程的两个结论

到这里,可以归纳出两个基础结论。

结论一:进程不等于可执行文件。

磁盘上的可执行文件是程序;程序运行后,操作系统还需要维护对应的进程管理信息,并为执行提供所需的运行环境。

结论二:进程管理依赖内核数据结构。

操作系统通过维护进程的管理信息及其组织关系,完成进程的创建、调度、状态跟踪和退出等操作。

理解这两点后,后面学习 PID、父子进程以及 fork() 时,就不容易把“代码”和“进程”混为一谈。


二、通过系统调用认识正在运行的进程

前面讲的是操作系统内部如何描述和管理进程。接下来,我们从 Linux 用户空间出发,观察真实的进程。

2.1 系统调用是什么?

我们编写的普通应用程序运行在用户空间,而许多关键的系统资源由操作系统内核负责管理。

例如:

  • 获取当前进程的 ID;
  • 创建新进程;
  • 打开文件;
  • 读取文件;
  • 修改进程的工作目录。

这些操作可能需要内核提供服务。

系统调用(System Call)是用户程序请求操作系统内核提供服务的重要接口。

在 Linux 中,可以通过 man 手册查看相关接口。手册的不同章节分别介绍系统调用、库函数等内容。

例如:

man 2 getpid

通常用于查看 getpid 系统调用相关的手册信息。

而:

man 3 printf

通常用于查看 printf 这样的库函数接口。

这里的数字表示手册章节。它们帮助我们区分同名条目属于哪一类内容。

还需要注意:应用程序调用的 C 库函数不一定都直接对应一个系统调用。某些库函数会在内部调用系统调用,另一些则完全在用户空间完成工作。

2.2 获取当前进程的 PID

Linux 为进程分配进程标识符,称为 PID(Process ID)。

PID 用于识别进程。我们可以通过 getpid() 获取当前进程的 PID,通过 getppid() 获取当前进程的父进程 PID。

先编写一个简单的程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        sleep(1);
        printf("当前进程的 PID:%d\n", (int)getpid());
    }

    return 0;
}

保存为 process.c,编译并运行:

gcc process.c -o process
./process

程序每隔一秒打印一次当前进程的 PID。

代码中的几个部分分别是:

  • unistd.h:声明了 getpid()、sleep() 等接口。
  • sys/types.h:提供了 pid_t 等相关类型。
  • sleep(1):让当前进程暂停约一秒。
  • getpid():获取调用进程自身的 PID。

这里可以观察到一个现象:只要这个程序仍然是同一个进程,它每次打印的 PID 通常就保持不变。

如果停止程序,再重新运行,通常会得到另一个 PID。

不过,PID 并不是永远唯一且永不重复的编号。它在系统中用于标识当前进程,进程结束后,相关编号以后可能被重新分配。

2.3 获取父进程的 PPID

除了 PID,我们还需要认识另一个概念:PPID(Parent Process ID,父进程 ID)。

每个普通进程在创建时通常都来自某个父进程。getppid() 可以帮助我们查看当前进程的父进程 ID。

例如:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        sleep(1);
        printf("当前进程的 PPID:%d\n", (int)getppid());
    }

    return 0;
}

编译运行:

gcc process.c -o process
./process

程序会反复打印当前进程的父进程 ID。

这时需要区分:

  • getpid():我是谁?
  • getppid():创建我的父进程是谁?

这个区别在理解 fork() 时非常重要,因为父进程和子进程拥有不同的 PID,而子进程的 PPID 通常指向父进程的 PID。


三、在 Linux 中查看进程

3.1 使用 ps 查看进程

程序运行起来后,我们可以通过 Linux 命令查看它。

常见命令是:

ps axj

它可以列出系统中较全面的进程信息,包括进程 ID、父进程 ID、终端信息和进程状态等。

如果输出内容太多,可以配合管道和 grep 筛选。

例如:

ps axj | grep process

这条命令会筛选出包含 process 字样的行。

不过,grep process 自己的命令行中也包含 process,所以结果可能会把筛选命令本身也显示出来。可以使用:

ps axj | grep process | grep -v grep

其中:

  • grep process:筛选包含指定字符串的行。
  • grep -v grep:排除包含 grep 的行。

如果想同时保留 ps 输出的标题行,可以这样写:

ps axj | head -1
ps axj | grep process | grep -v grep

第一条命令显示列名,第二条命令显示筛选后的进程。

3.2 实时观察进程

如果希望每隔一秒刷新一次进程信息,可以使用循环:

while :
do
    ps axj | head -1
    ps axj | grep process | grep -v grep
    sleep 1
done

这里的 while : 表示循环条件始终成立,因此循环会持续运行。

每一轮循环都会:

  1. 输出 ps axj 的标题行;
  2. 找出名称中包含 process 的进程;
  3. 暂停一秒;
  4. 再次查询。

要停止循环,可以按 Ctrl+C。

这是一种简单的命令行监控方式。它并不是真正意义上的专用进程监控工具,但很适合用来观察实验程序。

3.3 为什么每次启动看到的 PID 不一样?

停止程序后再次启动,通常会发现 PID 发生变化。

这是因为每次启动程序,操作系统都会创建新的进程实例,并为其分配相应的 PID。

磁盘上的可执行文件可能完全相同,但每次运行都可以形成一个新的进程。

例如:

第一次运行:PID = 5100
停止程序
第二次运行:PID = 5234

上面的数字只是示意,真实 PID 由系统分配。

还可以观察到,启动程序后,终端本身依然可以继续接收命令。这是因为终端背后的 Shell 也是一个进程。

3.4 Shell 本身也是一个进程

当我们登录 Linux 并打开命令行时,通常会通过 Shell 与操作系统交互,例如 Bash。

Shell 的主要工作包括:

  • 显示命令提示符;
  • 等待用户输入命令;
  • 解析命令;
  • 根据需要启动其他程序;
  • 管理命令执行过程。

例如,我们输入:

ls

Shell 会负责解释这条命令,并通过相应的进程创建和程序执行机制,让 ls 程序运行起来。

因此,Shell 不是一个脱离操作系统运行的特殊界面,它本身也是一个进程。

当我们运行一个持续打印 PID 的程序时,Shell 通常会等待前台程序结束后,再显示新的命令提示符。

如果从进程列表中观察,就可以看到 Shell 与它启动的程序之间的父子关系。

需要注意,不是每个用户都严格只对应一个 Bash 进程。一个用户可以打开多个终端、启动多个 Shell,也可以使用其他 Shell 程序。

3.5 通过 /proc 查看进程

除了 ps 命令,Linux 还提供了 /proc 文件系统,用来暴露许多内核和进程相关的信息。

可以查看:

ls /proc

其中通常会看到很多数字名称的目录,例如:

/proc/1
/proc/420
/proc/5234

这些数字目录通常对应相应 PID 的进程信息。

例如:

ls /proc/$$

这里的 $$ 是 Shell 提供的特殊参数,表示当前 Shell 的 PID。因此,这条命令会查看当前 Shell 对应的 /proc 目录。

还可以直接查看:

ls /proc/$$/fd

fd 目录用于展示该进程打开的文件描述符。

/proc 中的内容并不是普通磁盘文件的简单集合,而是由内核提供的虚拟文件系统接口。许多内容会根据当前内核状态动态生成。

因此,当一个进程退出后,对应的进程目录通常也会消失。


四、进程不仅有 PID,还有自己的运行环境

4.1 exe 与 cwd 分别是什么?

在 /proc/[pid] 目录中,我们可以看到一些具有特殊含义的链接,例如:

/proc/5234/exe
/proc/5234/cwd

其中:

  • exe:指向该进程所执行程序对应的可执行文件。
  • cwd:指向该进程当前的工作目录。

可以使用下面的命令查看:

readlink /proc/$$/exe
readlink /proc/$$/cwd

第一条命令查看当前 Shell 对应的可执行文件路径;第二条查看当前 Shell 的工作目录。

需要注意,exe 表示正在执行的程序文件相关信息,并不代表进程的全部代码和数据都只存放在这个磁盘路径中。

即使原来的可执行文件后来被删除,已经运行的进程通常也不会因此立刻停止,因为运行中的代码映射和磁盘目录项是不同的概念。

4.2 cwd 为什么会影响文件创建位置?

cwd 是 Current Working Directory,即当前工作目录。

假设程序中有这样一行代码:

FILE *fp = fopen("hello.txt", "w");

这里使用的是相对路径 hello.txt,并没有写出完整的目录路径。

那么文件应该创建在哪里?

通常情况下,系统会根据进程当前的工作目录来解析相对路径,而不是简单地根据可执行文件所在目录决定。

例如,假设进程的工作目录是:

/home/ubuntu/project

那么程序使用相对路径打开 hello.txt 时,目标路径通常就是:

/home/ubuntu/project/hello.txt

这也是为什么从不同目录运行同一个程序,可能会在不同位置创建文件。

4.3 如何修改进程的工作目录?

在 Linux 中,可以使用 chdir() 修改当前进程的工作目录。

例如:

#include <stdio.h>
#include <unistd.h>

int main()
{
    if (chdir("/home/ubuntu") == -1)
    {
        perror("chdir");
        return 1;
    }

    printf("工作目录切换完成\n");
    return 0;
}

调用成功后,当前进程的工作目录会切换到 /home/ubuntu。

如果程序之后通过相对路径访问文件,就会以新的工作目录为基准。

需要注意:chdir() 只改变调用它的进程的工作目录,不会自动改变父进程的工作目录。

4.4 为什么 Shell 的 cd 能改变命令行所在目录?

我们平时使用的命令是:

cd /home/ubuntu

执行后,终端的工作目录就发生了变化。

但如果把 cd 写成一个普通的外部程序,运行它时,子进程的 chdir() 只会改变子进程自己的工作目录。子进程结束后,父进程 Shell 的目录不会因此改变。

所以,Shell 通常把 cd 实现为内建命令,由当前 Shell 进程自己调用相应的目录切换机制。

这样,Shell 自身的工作目录才会真正改变,后续执行的命令也会继承相应的工作目录。

这个例子很好地说明了:进程不仅有 PID,也有自己的运行环境;某个进程的操作,不一定会改变另一个进程的状态。


五、进程之间的父子关系:从进程树到 fork

5.1 进程树是什么?

Linux 中的进程通常存在父子关系。

一个进程可以创建多个子进程,而这些子进程又可能继续创建自己的子进程。

因此,进程之间的关系可以用树形结构来理解:

systemd
  ├── service A
  │     ├── worker 1
  │     └── worker 2
  └── terminal
        └── bash
              ├── ls
              └── process

上图只是示意结构,真实系统中的进程关系会根据发行版、服务和启动方式有所不同。

在许多现代 Linux 系统中,PID 1 通常由 systemd 或其他初始化程序承担。某个终端中的命令通常由 Shell 启动,因此 Shell 常常是这些命令进程的父进程。

父子关系描述的是进程的创建关系,而不是说子进程必须始终与父进程一起运行。

5.2 fork:创建子进程的系统调用

Linux 提供了 fork(),用于创建一个新的子进程。

它的声明可以通过下面的头文件获得:

#include <unistd.h>

调用 fork() 后,系统会创建一个新的进程。新进程继承父进程许多方面的运行环境,并从 fork() 返回后的执行位置继续运行。

可以先记住三个要点:

  1. fork() 创建子进程。
  2. 父进程和子进程都可以继续执行后面的代码。
  3. 父子进程具有各自的进程标识和独立的执行流。

这里说“继承”并不意味着把父进程的所有资源都无条件复制成完全独立的物理副本。Linux 会采用相应的资源管理机制,其中包括后面要讲的写时拷贝。

5.3 先写一个 fork 实验

下面用一个完整例子观察父子进程的行为:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>

using namespace std;

int main()
{
    pid_t id = fork();

    if (id < 0)
    {
        cerr << "fork failed" << endl;
        return 1;
    }
    else if (id == 0)
    {
        while (true)
        {
            cout << "子进程 PID: " << getpid()
                 << ", PPID: " << getppid() << endl;
            sleep(1);
        }
    }
    else
    {
        while (true)
        {
            cout << "父进程 PID: " << getpid()
                 << ", PPID: " << getppid() << endl;
            sleep(1);
        }
    }

    return 0;
}

保存为 fork_demo.cpp,然后编译:

g++ fork_demo.cpp -o fork_demo

运行:

./fork_demo

程序会持续输出父进程和子进程的信息。要停止实验,可以在终端按 Ctrl+C。

这个实验中最值得观察的是:fork() 调用之后,父进程和子进程都会执行后面的代码,但它们拿到的 id 值不同。

下面分三个问题来解释这个现象。

5.4 为什么 fork 的返回值在父子进程中不同?

fork() 的返回值有三种情况:

返回情况含义
小于 0创建失败,没有成功创建子进程
等于 0当前执行流是子进程
大于 0当前执行流是父进程,返回值是新创建的子进程 PID

那么问题来了:为什么一次函数调用,父子进程会得到不同的返回值?

关键在于:fork() 返回时,系统已经成功创建了两个进程执行流。

可以把这个过程理解为:

  1. 父进程调用 fork()。
  2. 内核创建子进程,并为其建立相应的进程管理信息。
  3. 父进程和子进程都从 fork() 返回后的执行位置继续执行。
  4. 内核让父进程观察到子进程的 PID,让子进程观察到返回值 0。

这里并不是普通函数在一次执行中凭空产生两个返回值,而是两个进程各自继续执行,并分别获得了与自身身份相对应的返回结果。

为什么父进程要得到子进程的 PID?

因为父进程需要知道自己创建了哪个子进程,才能进一步管理它,例如等待子进程退出或向其发送信号。

为什么子进程得到 0?

因为子进程可以用这个返回值判断自己当前是子进程。如果它需要知道父进程的 PID,可以调用 getppid()。

5.5 为什么 if 和 else 的两个分支都能执行?

我们平时学习 C/C++ 时,知道 if ... else 的两个分支在一次正常的控制流判断中是互斥的。

例如:

if (x > 0)
{
    cout << "positive" << endl;
}
else
{
    cout << "not positive" << endl;
}

一次执行只会选择其中一个分支。

但在 fork() 的例子里,父进程和子进程都可能输出内容,看起来就像两个分支都执行了。

其实,不是一个进程同时执行了两个互斥分支,而是两个进程分别执行了自己的分支。

可以按下面的方式理解:

父进程调用 fork()
        │
        ▼
    创建子进程
        │
        ├── 父进程得到 id = 子进程 PID
        │       └── 进入父进程分支
        │
        └── 子进程得到 id = 0
                └── 进入子进程分支

父进程判断 id > 0,条件成立,因此执行父进程分支。

子进程判断 id == 0,条件成立,因此执行子进程分支。

两个进程拥有各自的执行流,所以它们可以分别走进不同的分支。

此外,父进程和子进程的调度顺序并不固定。哪一个先输出,取决于当时的调度情况。因此,终端上的输出可能交替出现,也可能短时间内连续出现某一个进程的输出。

5.6 同一个局部变量为什么会有两个不同的值?

这是理解 fork() 时最关键的问题之一。

代码中有:

pid_t id = fork();

父进程看到的 id 是子进程的 PID;子进程看到的 id 却是 0。

但 id 明明是在同一行代码中定义的,为什么会出现两个值?

原因是:父进程和子进程拥有各自独立的进程地址空间视图。

fork() 创建子进程后,子进程会获得与父进程相应的地址空间内容。对程序来说,父子进程看起来拥有相同的代码和初始数据,但它们并不是永远共用一份可以随意互相修改的普通变量。

Linux 通常使用写时拷贝(Copy-On-Write,COW)优化这一过程。

5.7 写时拷贝究竟是什么?

写时拷贝的基本思想是:

在没有发生写入时,尽量共享原有的物理内存;当某一方需要修改共享内容时,再为修改建立独立的副本。

可以把它理解成两个人同时查看同一份只读资料:

  • 在没有修改之前,两个人可以共同查看同一份内容。
  • 当其中一个人需要修改内容时,系统为其准备一份独立副本。
  • 一个人对副本所做的修改,不会直接改变另一个人看到的内容。

在 Linux 的 fork() 场景中,可以从以下几个阶段理解。

第一阶段:创建子进程。

父进程和子进程拥有各自的虚拟地址空间。为了避免无意义地复制所有物理内存,内核可以让两边的页表暂时映射到相同的物理页面,并通过相应的内存保护机制控制写入。

第二阶段:父子进程分别继续执行。

父进程和子进程在逻辑上拥有独立的变量和地址空间视图。即使它们最初映射到相同的物理页面,也不意味着双方可以随意修改同一份普通变量并互相看到变化。

第三阶段:某一方需要写入。

当某个进程尝试写入受写时拷贝保护的页面时,处理器会触发相应的异常,内核再根据页面状态处理写入请求。需要时,内核为写入方建立独立的物理页面,并调整相应映射。

这样,父进程和子进程就能够在保持独立性的同时,避免在创建时立即复制所有内存。

不过,有一点必须说清楚:fork() 的两个返回值并不是因为父子进程先共享同一个 id,再由写时拷贝把 id 改成两个值。

更准确地说,内核在创建父子进程并安排系统调用返回时,会让两个执行流获得不同的返回结果。写时拷贝解释的是父子进程如何在内存层面保持逻辑独立、减少不必要的复制,而不是直接决定 fork() 的返回值。

5.8 父子进程的独立性

现在可以进一步理解进程独立性。

父子进程有各自的 PID、进程管理信息和执行流。它们在 fork() 后可以继续执行各自的代码,并且对普通私有数据的修改不会自动影响对方。

例如:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>

using namespace std;

int main()
{
    int value = 10;

    pid_t id = fork();

    if (id == 0)
    {
        value = 20;
        cout << "子进程 value = " << value << endl;
    }
    else if (id > 0)
    {
        sleep(1);
        cout << "父进程 value = " << value << endl;
    }
    else
    {
        cerr << "fork failed" << endl;
        return 1;
    }

    return 0;
}

在正常创建成功的情况下,子进程修改自己的 value 后,父进程的普通私有变量仍然保持原来的值。

原因是父子进程拥有逻辑上独立的地址空间。写时拷贝是实现这种独立性的一种重要优化机制。

这里也要区分代码和数据:父子进程可以共享只读的程序代码页面,但普通私有数据在写入时会受到写时拷贝等机制的保护。对于显式配置的共享内存等情况,规则则不同。


六、补充:守护进程是什么?

6.1 守护进程与精灵进程

Linux 中有一种常见的进程类型,叫作守护进程(Daemon)。有时也会被翻译成“精灵进程”。

守护进程通常用于提供持续性的后台服务。例如,系统服务、日志管理服务和网络服务,都可能以守护进程或类似的后台服务形式运行。

它的常见特点包括:

  1. 后台运行: 通常不直接依赖某个交互式终端来完成工作。
  2. 生命周期较长: 很多守护进程会在系统启动时运行,并在系统关闭或服务停止时结束。
  3. 等待事件或持续提供服务: 例如等待网络请求、处理系统事件,或者按一定周期执行任务。

需要注意,后台运行不等于一定是守护进程。一个普通程序也可以在后台运行,但守护进程通常还涉及服务管理、终端关联和生命周期管理等问题。

6.2 守护进程有什么用?

以一个网络服务为例。

当服务器启动后,网络服务需要持续等待客户端连接。如果每次有人访问服务器,都必须由用户手动打开终端、运行程序,那么服务就无法稳定地持续工作。

因此,系统可以在后台启动相应的服务进程,让它持续等待请求,并在收到请求后执行处理逻辑。

这就是守护进程在操作系统和服务器开发中的重要用途之一。


七、本篇总结

本篇从进程的本质开始,逐步建立了对 Linux 进程管理和父子进程关系的理解。

需要掌握的核心知识如下:

  1. 程序与进程不同: 程序通常是磁盘上的可执行文件,进程则是程序运行时由操作系统管理的执行实体。
  2. 进程需要内核数据结构描述: PCB 是进程控制块的抽象概念,Linux 中的 task_struct 是重要的进程管理结构。
  3. 操作系统采用“先描述,再组织”的思路管理进程。
  4. PID 与 PPID: getpid() 获取当前进程的 PID,getppid() 获取父进程 PID。
  5. 查看进程: 可以使用 ps axj、grep 和 /proc 观察进程信息。
  6. 进程有自己的工作目录: /proc/[pid]/cwd 可以查看工作目录,chdir() 可以修改当前进程的工作目录。
  7. 进程树: 一个进程可以创建多个子进程,父子关系构成树形结构。
  8. fork() 的返回值: 失败时返回小于零的值;子进程得到 0;父进程得到新创建的子进程 PID。
  9. 父子进程有独立的执行流: 因此,它们可以分别进入不同的条件分支。
  10. 写时拷贝: Linux 可以在创建子进程时暂时共享物理内存页面,在需要写入时再建立独立副本。
  11. 守护进程: 常用于长期运行的后台服务。

理解这些知识之后,fork() 就不再是一个看起来违反 C/C++ 语言规则的特殊函数,而是一个创建新进程、建立独立执行流,并通过不同返回值区分父子进程的系统调用。

后续学习进程地址空间、程序替换和进程控制时,这些概念会继续发挥作用。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐