de风——【从零开始学习Linux】(九):进程基础 —— 从冯诺依曼体系到 fork 创建进程

环境说明

  • 本地工具:Xshell 7
  • 服务器:腾讯云 Ubuntu 22.04(64 位)
  • 编译器:gcc(Ubuntu 默认自带)
  • 本篇所有命令均可在 Xshell 连接的 Ubuntu 终端里直接敲

前言

前八篇我们把"怎么写代码、怎么编译、怎么调试"这条链路走通了。从这一篇开始,我们要进入 Linux 学习里真正最核心、也最劝退新手的模块------进程。

很多同学一上来就背"进程是程序的一次执行",背完还是懵:进程到底是什么?操作系统凭什么管得住它?我写的那几行代码到底是怎么变成进程的?

这一篇不讲状态、不讲优先级、不讲调度算法(那是下一篇的事),我们只做三件事:

  1. 认识冯诺依曼体系结构 ------ 搞清楚计算机硬件是怎么配合干活的
  2. 搞懂操作系统是什么、有什么作用 ------ 理解"管理"这两个字
  3. 掌握进程的基本概念与基本操作 ------ 会看进程、会拿 PID、会用 fork 创建进程

把这三件事搞明白,后面学进程状态、僵尸进程、进程调度都会顺很多。

本章内容思维导图:


一、认识冯诺依曼体系结构

1.1 语法形式(结构表达)

冯诺依曼体系不是一个命令,它是一台计算机的硬件结构图。我们用一张结构图来"写"出它:

bash 复制代码
        ┌────────────────────┐
        │      输入设备       │   键盘 / 鼠标 / 扫描仪 / 网卡
        └─────────┬──────────┘
                  │  只能写入内存
                  ▼
┌──────────────────────────────────────────┐
│              存储器(内存)                │
└─────────┬──────────────────────────▲─────┘
          │                          │
          │  读写                     │  读写
          ▼                          │
┌───────────────────┐        ┌───────┴────────────┐
│   中央处理器 CPU    │        │      输出设备        │
│  运算器 + 控制器    │        │  显示器 / 打印机     │
└───────────────────┘        └────────────────────┘

一句话记忆 :输入 → 内存 ← → CPU → 内存 → 输出。

1.2 功能

我们常见的笔记本、不常见的服务器,大部分都遵守冯诺依曼体系。它由这几类硬件组成:

|---------------|-----------------|-------------------|
| 部件 | 具体是什么 | 作用 |
| 输入单元 | 键盘、鼠标、扫描仪、写板、网卡 | 把外部数据送进计算机 |
| 输出单元 | 显示器、打印机、网卡 | 把计算结果送出去 |
| 中央处理器 CPU | 运算器 + 控制器 | 做算术运算、逻辑运算、控制指令执行 |
| 存储器 | 内存(RAM) | 存放程序和数据 |

关于冯诺依曼,有 三条必须刻进脑子 的结论:

  1. 这里说的"存储器"指的是内存,不是硬盘!
  2. 不考虑缓存的情况下,CPU 能且只能对内存进行读写,不能直接访问外设(输入/输出设备)。
  3. 外设要输入或输出数据,也只能写入内存或者从内存读取。

合起来就是课件里那句最经典的话:

所有设备都只能直接和内存打交道。

1.3 例子

例子一:用代码直观感受"数据都在内存里"

写一个最普通的加法程序,顺便把变量的地址打印出来:

cpp 复制代码
// von_neumann.c ------ 验证:程序运行时的所有数据都在内存里
#include <stdio.h>

int main()
{
    int a = 10;
    int b = 20;

    printf("a = %d, a 的地址 = %p\n", a, &a);
    printf("b = %d, b 的地址 = %p\n", b, &b);

    int sum = a + b;                 // CPU 从内存读 a、b → 运算 → 写回内存
    printf("%d + %d = %d\n", a, b, sum);

    return 0;
}

在 Xshell 里编译运行:

cpp 复制代码
gcc -o von_neumann von_neumann.c
./von_neumann

运行结果(地址每次可能不同):

cpp 复制代码
a = 10, a 的地址 = 0x7ffd3c8a4a48
b = 20, b 的地址 = 0x7ffd3c8a4a4c
10 + 20 = 30

这个例子说明了什么 :a、b、sum 这些变量全都有内存地址,说明数据全部住在内存里 ;CPU 做加法时,必须先从内存把 a、b 读进寄存器,算完再把结果写回内存。CPU 从来不会"直接"去找键盘、找屏幕要数据。

例子二:QQ 聊天的数据流动过程

这是课件里专门留的思考题,也是面试常问的题。我们从"你登录 QQ 给朋友发一句'在吗'"开始拆解:

发送方(你):

cpp 复制代码
① 键盘输入"在吗"
        ↓  输入设备
② 数据被写进内存(QQ 程序的接收缓冲区)
        ↓
③ CPU 从内存取出数据,做处理(加密、封装协议、加上好友ID)
        ↓  写回内存
④ 内存 → 网卡(输出设备),数据发到网络

接收方(你朋友):

cpp 复制代码
⑤ 朋友的网卡(输入设备)收到数据 → 写入他机器的内存
        ↓
⑥ CPU 从内存取出数据,解包、解密、还原成文字 → 写回内存
        ↓
⑦ 内存 → 显示器(输出设备),朋友看到"在吗"

如果是在 QQ 上发送文件呢? 流程完全一样,只是数据来自磁盘(输入设备)而不是键盘:

cpp 复制代码
磁盘(文件)→ 内存 → CPU 处理(分块、编号、封装)→ 内存 → 网卡 → 网络

新手要点 :不管是打字还是传文件,内存永远是中转站。CPU 不碰磁盘、不碰网卡、不碰键盘,它只认内存。

1.4 本节思维导图


二、操作系统(Operator System)

2.1 概念

bash 复制代码
操作系统(OS) = 内核 + 其他程序
  • 内核:进程管理、内存管理、文件管理、驱动管理
  • 其他程序:函数库(如 libc)、shell 程序(如 bash)等等
功能

任何计算机系统都包含一个基本的程序集合,这个集合就叫操作系统。笼统地说,它就是夹在"硬件"和"你写的应用"之间的那一大坨软件。

例子:看看你的系统里 OS 都由什么组成
cpp 复制代码
# 查看内核版本(内核就是最核心的那个程序)
uname -r

# 看看你现在用的 shell(其他程序之一)
echo $SHELL

# 看看 C 语言函数库在哪(其他程序之一)
ls /lib/x86_64-linux-gnu/libc.so.6

输出示例:

bash 复制代码
5.15.0-91-generic
/bin/bash
/lib/x86_64-linux-gnu/libc.so.6

这个例子说明了什么 :5.15.0-91-generic 就是内核;/bin/bash 就是你敲命令的那个 shell;libc.so.6 就是函数库。这三者加起来,才是完整的操作系统。

2.2 设计 OS 的目的与核心功能

cpp 复制代码
对下:与硬件交互,管理所有的软硬件资源
对上:为用户程序(应用程序)提供一个良好的执行环境
功能

在整个计算机软硬件架构中,操作系统的定位是:

一款纯正的"搞管理"的软件。

这句话是这一章的灵魂。操作系统既不生产数据,也不直接帮你算 1+1,它干的事只有一件:管理。

下面这张图直观地展示了操作系统的"承上启下"作用:

例子:用代码看 OS 是怎么"分配资源"的
cpp 复制代码
// os_manage.c ------ 体会操作系统如何替你管理内存、文件、CPU
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 1. 内存管理:我只是"申请",真正分配的是 OS
    int *arr = (int *)malloc(10 * sizeof(int));
    if (arr == NULL) {
        perror("malloc");
        return 1;
    }

    // 2. CPU 管理:我这个进程能被调度上 CPU 跑,是 OS 安排的
    for (int i = 0; i < 10; i++) {
        arr[i] = i * i;
    }

    // 3. 文件管理:写文件也是 OS 帮我把数据落到磁盘
    FILE *fp = fopen("data.txt", "w");
    for (int i = 0; i < 10; i++) {
        fprintf(fp, "%d ", arr[i]);
    }
    fclose(fp);

    printf("写入完成:");
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);   // 释放内存,把资源还给 OS
    return 0;
}

输出:

cpp 复制代码
gcc -o os_manage os_manage.c
./os_manage
cat data.txt

这个例子说明了什么 :你只写了 malloc、fopen、printf,一次都没碰过内存条和磁盘。所有脏活累活都是 OS 干的------这就是"对下管理硬件,对上提供良好执行环境"的具体体现。

2.3 如何理解"管理"

cpp 复制代码
管理 = 先描述(用 struct 结构体) + 再组织(用链表或其他高效数据结构)
功能

课件里用了个特别好懂的三角色例子:

|---------|----------------------------|
| 角色 | 在"管理"里对应什么 |
| 学生 | 被管理的对象 |
| 辅导员 | 执行者 ------ 收集学生信息、落实决策 |
| 校长 | 管理者 ------ 做决策,但未必亲自见每一个学生 |

关键问题来了:校长从来没见过你,他凭什么管理你? 答案是:因为你的信息(学号、姓名、成绩)被记录在表格里,表格被组织进了学校的系统。校长只要查表就能管理你。

操作系统管理硬件,用的是一模一样的思路:

  1. 描述起来 ,用 struct 结构体
  2. 组织起来,用链表或其他高效的数据结构

这就是计算机世界里最著名的六字真言:先描述,再组织。

例子:用 C 代码模拟"先描述、再组织"

这段代码就是操作系统管理进程的核心套路的迷你版:

cpp 复制代码
// manage.c ------ 模拟 OS 的"先描述,再组织"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 第一步:描述被管理对象 ------ 用 struct
typedef struct Student {
    int  id;              // 学号:相当于进程的 PID
    char name[32];        // 姓名:相当于进程名
    int  score;           // 成绩:相当于进程状态 / 优先级
    struct Student *next; // 第二步:组织被管理对象 ------ 用链表
} Student;

// 创建一个"被管理者"
Student *create_student(int id, const char *name, int score)
{
    Student *s = (Student *)malloc(sizeof(Student));
    s->id    = id;
    strcpy(s->name, name);
    s->score = score;
    s->next  = NULL;
    return s;
}

// 校长查表:遍历链表就能管理所有学生
void show_all(Student *head)
{
    Student *cur = head;
    while (cur != NULL) {
        printf("学号:%-4d 姓名:%-8s 成绩:%d\n", cur->id, cur->name, cur->score);
        cur = cur->next;
    }
}

int main()
{
    Student *head = create_student(1, "zhangsan", 90);
    head->next = create_student(2, "lisi",     85);
    head->next->next = create_student(3, "wangwu", 92);

    printf("=== 校长查表(不用见学生本人)===\n");
    show_all(head);

    return 0;
}
cpp 复制代码
gcc -o manage manage.c
./manage

输出:

cpp 复制代码
=== 校长查表(不用见学生本人)===
学号:1    姓名:zhangsan 成绩:90
学号:2    姓名:lisi     成绩:85
学号:3    姓名:wangwu   成绩:92

这个例子说明了什么 :OS 管理进程也是这样------它不需要"认识"你的程序,只需要把每个进程用一个 struct 描述好,再用链表串起来。以后要调度、要杀掉、要查看,全都是对链表做增删改查。

承上启下:还没学进程之前就能猜到------操作系统怎么管理进程?很简单,先把进程描述起来,再把进程组织起来!

2.4 系统调用和库函数

bash 复制代码
操作系统暴露出来的部分接口 = 系统调用(system call)
对系统调用做适度封装 = 库函数(library function)
功能
  • 从开发角度看,操作系统对外表现为一个整体 ,但会暴露自己的部分接口 供上层使用,这部分接口就叫系统调用。
  • 系统调用功能比较基础、对用户要求比较高,于是有心的开发者把部分系统调用封装成库。有了库,上层开发者做二次开发就轻松多了。

一句话区分:

printf 是库函数(库),write 是系统调用(OS 接口),printf 内部调用了 write。

例子:分清库函数和系统调用
bash 复制代码
// call.c ------ printf 是库函数,write 是系统调用
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    printf("这是库函数 printf 输出的\n");              // 库函数:带缓冲区、能格式化
    write(1, "这是系统调用 write 输出的\n", strlen("这是系统调用 write 输出的\n")); // 系统调用:最原始
    return 0;
}

再用 man 手册验证一下它俩的身份:

bash 复制代码
man 2 write     # 第 2 章 = 系统调用
man 3 printf    # 第 3 章 = 库函数
man 2 fork      # fork 也是系统调用

新手避坑 :man 后面的数字很关键。man fork 默认打开第 2 章(系统调用);如果你想查 shell 命令,要用 man 1 xxx;查库函数用 man 3 xxx。

2.5 本节思维导图


三、进程(3-1 基本概念与基本操作)

3.1 基本概念

语法形式(定义)
bash 复制代码
课本概念:程序的一个执行实例,正在执行的程序
内核观点:担当分配系统资源(CPU 时间、内存)的实体
当前理解:进程 = 内核数据结构(task_struct) + 自己的程序代码和数据
功能

这三个定义是从三个角度描述同一件事,建议三个都背下来:

|--------|-----------------------|---------------------------------|
| 角度 | 定义 | 大白话 |
| 课本 | 程序的一个执行实例 | 程序是躺在磁盘上的文件,跑起来的那个才叫进程 |
| 内核 | 分配系统资源的实体 | OS 是按"进程"为单位发 CPU 时间、发内存的 |
| 现在 | task_struct + 代码和数据 | 进程 = OS 给你的档案袋 + 你自己的东西 |

新手最容易混的点 :程序 ≠ 进程 。 程序是静态的文件(比如 /bin/ls),进程是动态的运行过程。一个程序可以同时跑出多个进程(你开三个 Xshell 窗口,就是三个 bash 进程)。

例子:程序 vs 进程
bash 复制代码
# 程序:躺在磁盘上的文件,一直在那儿
ls -l /bin/ls

# 进程:跑起来才有,跑完就没了
ls            # 这一瞬间产生了一个 ls 进程
ps aux | grep sleep | grep -v grep   # 没在跑就查不到

3.2 描述进程 ------ PCB

cpp 复制代码
进程信息被放在一个叫做「进程控制块」的数据结构中 = PCB(process control block)
Linux 下的 PCB 具体叫:task_struct
功能
  • PCB 可以理解为进程属性的集合 ,是操作系统描述进程的那个 struct。
  • task_struct 是 Linux 内核的一种数据结构类型,会被装载到内存(RAM)里,并且包含着进程的所有信息。

对应关系回顾一下:

cpp 复制代码
学生(被管理对象)  →  struct Student(描述)
进程(被管理对象)  →  struct task_struct(描述)  ← 这就是 PCB

3.3 task_struct 的内容分类

语法形式(成员分类)

|--------------|-------------------------------------|----------|
| 分类 | 含义 | 类比 |
| 标示符 | 描述本进程的唯一标示符,用来区别其他进程 | 学号 / PID |
| 状态 | 任务状态、退出代码、退出信号等 | 在上课 / 请假 |
| 优先级 | 相对于其他进程的优先级 | 奖学金优先级 |
| 程序计数器 | 程序中即将被执行的下一条指令的地址 | 读到第几页了 |
| 内存指针 | 程序代码和进程相关数据的指针,以及共享内存块的指针 | 你的书包放哪 |
| 上下文数据 | 进程执行时处理器的寄存器中的数据 | 你做题时的草稿纸 |
| I/O 状态信息 | 显示的 I/O 请求、分配给进程的 I/O 设备、被进程使用的文件列表 | 你借了哪些书 |
| 记账信息 | 处理器时间总和、使用的时钟数总和、时间限制、记账号等 | 你的考勤记录 |
| 其他信息 | ------ | ------ |

功能

task_struct 里面的东西非常多,上面这些只是主要分类 。所有运行在系统里的进程,都以 task_struct 双链表的形式存在内核里 ------注意,是双链表,不是数组。

例子:用代码模拟一个"迷你 PCB"
cpp 复制代码
// mini_pcb.c ------ 模拟内核里的 task_struct
#include <stdio.h>

// 模拟 PCB(极度简化版,真实 task_struct 有几百个字段)
typedef struct task_struct {
    int  pid;          // 标示符
    int  state;        // 状态:0-运行 1-睡眠 2-停止
    int  priority;     // 优先级
    unsigned long pc;  // 程序计数器:下一条指令地址
    void *mm;          // 内存指针:指向自己的代码和数据
    struct task_struct *next;  // 组织:双链表(这里简化成单向)
    struct task_struct *prev;
} task_struct;

void show_task(task_struct *t)
{
    const char *state_str[] = {"R (running)", "S (sleeping)", "T (stopped)"};
    printf("PID=%-5d 状态=%-12s 优先级=%-3d PC=%p\n",
           t->pid, state_str[t->state], t->priority, (void *)t->pc);
}

int main()
{
    task_struct p1 = {1001, 0, 80, 0x400000, NULL, NULL, NULL};
    task_struct p2 = {1002, 1, 90, 0x401000, NULL, NULL, NULL};
    task_struct p3 = {1003, 0, 75, 0x402000, NULL, NULL, NULL};

    // 组织起来:串成链表
    p1.next = &p2;  p2.prev = &p1;
    p2.next = &p3;  p3.prev = &p2;

    printf("=== 内核里的进程链表 ===\n");
    task_struct *cur = &p1;
    while (cur != NULL) {
        show_task(cur);
        cur = cur->next;
    }
    return 0;
}

输出:

cpp 复制代码
=== 内核里的进程链表 ===
PID=1001  状态=R (running)   优先级=80  PC=0x400000
PID=1002  状态=S (sleeping)  优先级=90  PC=0x401000
PID=1003  状态=R (running)   优先级=75  PC=0x402000

这个例子说明了什么 :内核里"进程"并不是一团混沌,而是一个个 task_struct 结构体被链表串起来的。OS 要管理进程,就是遍历这条链表。

3.4 查看进程

cpp 复制代码
ls /proc            # 方式一:直接看 /proc 系统文件夹
ps aux              # 方式二:用户级工具 ps
ps axj              # 方式二:ps 的另一种常用组合
top                 # 方式三:动态查看(类似 Windows 任务管理器)

|-----|--------------------------------------------|
| 选项 | 含义 |
| a | 显示一个终端所有的进程,包括其他用户的进程 |
| x | 显示没有控制终端的进程,例如后台运行的守护进程 |
| j | 显示进程归属的进程组 ID、会话 ID、父进程 ID,以及与作业控制相关的信息 |
| u | 以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU 和内存使用情况等 |

功能
  • /proc 是内存级的文件系统 (不是真的存在磁盘上)。要获取 PID 为 1 的进程信息,就去看 /proc/1 这个文件夹。
  • top 和 ps 是最常用的两个用户级工具:ps 看的是某一瞬间的快照 ,top 是动态刷新的。
例子:跑一个死循环进程,然后把它揪出来
cpp 复制代码
// myproc.c ------ 一个一直活着的进程,方便我们观察
#include <stdio.h>
#include <unistd.h>

int main()
{
    while (1) {
        sleep(1);
    }
    return 0;
}

终端 1(运行进程):

cpp 复制代码
gcc -o myproc myproc.c
./myproc &          # & 表示放到后台运行,终端还能继续敲命令
echo "我的进程 PID 是 $!"

输出:

cpp 复制代码
[1] 25431
我的进程 PID 是 25431

终端 2(观察进程)------再开一个 Xshell 会话:

cpp 复制代码
# 方式一:从 /proc 里看
ls /proc/25431
cat /proc/25431/status | head -5

# 方式二:用 ps 看
ps aux | grep myproc | grep -v grep
ps axj | grep myproc | grep -v grep

输出示例:

cpp 复制代码
$ cat /proc/25431/status | head -5
Name:   myproc
State:  S (sleeping)
Pid:    25431
PPid:   25380
...

$ ps aux | grep myproc | grep -v grep
user     25431  0.0  0.0   2496   844 pts/0    S+   21:15   0:00 ./myproc

$ ps axj | grep myproc | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25380 25431 25431 25380 pts/0    25456 S+    1000   0:00 ./myproc

新手要点:

  • /proc 里每个数字目录就是一个进程,目录名 = PID。
  • ps axj 会多出 PPID(父进程 ID)、PGID(进程组 ID)、SID(会话 ID)这几列,后面学进程组、会话时还会用到。
  • 想看 PID 为 1 的进程(Ubuntu 上是 systemd),执行 ls /proc/1 或 cat /proc/1/status。

3.5 通过系统调用获取进程标识符

cpp 复制代码
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);     // 获取当前进程 id(PID)
pid_t getppid(void);    // 获取父进程 id(PPID)
  • 返回值 :pid_t(本质就是整型),不会失败,没有失败返回值。
  • 头文件 :<sys/types.h>(pid_t 类型)+ <unistd.h>(函数声明)
功能

|-----------------------------|----------------|
| 名称 | 含义 |
| PID(Process ID) | 进程 id,本进程的唯一标识 |
| PPID(Parent Process ID) | 父进程 id,即"谁生了我" |

例子:打印自己的 PID 和爸爸的 PID
cpp 复制代码
// getid.c ------ 获取进程标识符
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("pid: %d\n",  getpid());
    printf("ppid: %d\n", getppid());
    return 0;
}

输出:

cpp 复制代码
pid: 25488
ppid: 25380

再跑一次:

cpp 复制代码
pid: 25491
ppid: 25380

这个例子说明了什么:

  1. PID 每次都不一样 ------ 因为每次运行都是创建了一个新的进程,PID 是 OS 动态分配的。
  1. PPID 一直不变 ------ 因为你的父进程就是你这个 Xshell 会话里的 bash,只要不退出这个终端,它就不会变。

验证一下爸爸到底是谁:

cpp 复制代码
ps axj | grep 25380 | grep -v grep
# 或者直接看
cat /proc/25380/comm
# 输出:bash

果然是 bash! 你在命令行里敲的每一个命令,都是 bash 帮你 fork 出来的子进程。

3.6 通过系统调用创建进程 ------ fork 初识

cpp 复制代码
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
  • 返回值 (重点):fork 有两个返回值

    • 在父进程 中,返回子进程的 PID(大于 0)
    • 在子进程 中,返回 0
    • 创建失败,返回 -1
功能

fork() 用来创建一个新的子进程。调用一次,返回两次。

课件原文的两条关键结论:

  1. 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
  1. fork 之后通常要用 if 进行分流。

先看看手册:

cpp 复制代码
man 2 fork
例子一:观察 fork 的两个返回值
cpp 复制代码
// fork1.c ------ 验证 fork 有两个返回值
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int ret = fork();
    printf("hello proc : %d!, ret: %d\n", getpid(), ret);
    sleep(1);
    return 0;
}

输出:

cpp 复制代码
hello proc : 25495!, ret: 25496
hello proc : 25496!, ret: 0

这个例子说明了什么:

  • 明明只写了一句 printf,却打印了两行 ------ 因为 fork() 之后变成了两个进程,各自往下执行了一遍。
  • 第一行 ret: 25496 是父进程打印的(返回的是子进程 PID)。
  • 第二行 ret: 0 是子进程打印的(PID 25496,正好是父进程拿到的返回值)。
例子二:fork 之后用 if 分流(正确写法)

实际开发中,必须 用 if 把父子进程分开,否则代码会被执行两遍:

cpp 复制代码
// fork2.c ------ fork 后用 if 分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int ret = fork();

    if (ret < 0) {              // 创建失败
        perror("fork");
        return 1;
    } else if (ret == 0) {      // 子进程
        printf("I am child  : pid=%d, ret=%d\n", getpid(), ret);
    } else {                    // 父进程
        printf("I am father : pid=%d, ret=%d\n", getpid(), ret);
    }

    sleep(1);
    return 0;
}

输出:

cpp 复制代码
I am father : pid=25510, ret=25511
I am child  : pid=25511, ret=0

这个例子说明了什么:

  • ret == 0 → 子进程独有的分支
  • ret > 0 → 父进程独有的分支(ret 就是子进程 PID)
  • 注意打印顺序父子谁先谁后是不确定的,由调度器决定,别以为一定是父进程先打印。
⚠️ 三个必须知道的疑问

学到这里,至少要能说出这几个问题:

  1. fork 为什么会有两个返回值? 因为 fork() 内部在 return 之前就已经把子进程创建出来了,此时父子两个进程都会从 fork 返回,所以返回两次。
  2. 两个返回值各自给父子如何返回? 父进程拿到子进程 PID (方便以后管理它),子进程拿到 0 (子进程可以通过 getppid() 找爸爸,不需要知道自己的 PID ------ 想要就用 getpid())。
  3. 一个变量 ret 怎么能让 if 和 else if 同时成立? 严格说不是同时成立 ,而是两个进程各有一份 ret 。父子进程数据私有(写时拷贝),父进程里的 ret 是 25511,子进程里的 ret 是 0。这个问题需要在后面讲虚拟地址空间时才能彻底解释清楚,这里先记住结论就行。

3.7 本节思维导图


四、速查表

4.1 查看进程常用命令

|------------------------|---------------------------|
| 命令 | 功能 |
| ls /proc | 列出所有进程的目录(目录名即 PID) |
| cat /proc/PID/status | 查看某个进程的详细状态 |
| ls -l /proc/PID/cwd | 查看进程的当前工作目录 |
| ls -l /proc/PID/exe | 查看进程对应的可执行程序 |
| ps aux | 以用户为中心查看所有进程 |
| ps axj | 查看进程并显示 PPID / PGID / SID |
| ps aux | grep xxx | 按名字过滤进程 |
| top | 动态查看进程(按 q 退出) |

4.2 常用函数

|-------------|--------------|------------|---------------------|
| 函数 | 头文件 | 功能 | 返回值 |
| getpid() | <unistd.h> | 获取当前进程 PID | 当前 PID |
| getppid() | <unistd.h> | 获取父进程 PPID | 父进程 PID |
| fork() | <unistd.h> | 创建子进程 | 父:子进程 PID;子:0;失败:-1 |

4.3 本篇核心结论(背下来)

  1. 冯诺依曼 :所有设备都只能直接和内存打交道。
  2. 操作系统 :一款纯正的**"搞管理"**的软件;对下管硬件,对上提供环境。
  3. 管理的本质 :先描述(struct),再组织(链表)。
  4. 进程 = 内核数据结构(task_struct) + 自己的程序代码和数据。
  1. PCB 就是描述进程的 struct,Linux 下叫 task_struct,内核里用双链表组织。
  1. fork 调用一次,返回两次 :父拿子进程 PID,子拿 0,所以必须用 if 分流。

五、总结

这一篇我们从最底层的硬件结构出发,一路走到了"用代码创建一个进程":

  • 第一章 讲了冯诺依曼体系,核心就一句话------CPU 只认内存,所有外设都只能直接和内存打交道。理解了这一点,后面学 IO、学缓冲区都会轻松很多。
  • 第二章 讲了操作系统,核心是"管理 "两个字。记住先描述、再组织 这六字真言,以及系统调用是 OS 暴露的接口、库函数是对系统调用的封装。
  • 第三章 进入进程本身:知道了进程 = task_struct + 代码和数据 ,学会了用 /proc、ps、top 查看进程,用 getpid/getppid 拿标识符,用 fork 创建进程并用 if 分流。

下一篇 我们会继续在"进程"这个话题上深入:讲清楚进程状态(R/S/D/T/Z) 、僵尸进程与孤儿进程 、进程优先级(PRI/NI) ,以及进程的竞争性与独立性、并行与并发。


六、专栏目录

  • [第一篇:Linux 环境搭建与 Xshell 连接腾讯云服务器](#第一篇:Linux 环境搭建与 Xshell 连接腾讯云服务器)
  • [第二篇:Linux 常用基础指令](#第二篇:Linux 常用基础指令)
  • [第三篇:Linux 权限管理](#第三篇:Linux 权限管理)
  • [第四篇:Vim 编辑器的基本使用](#第四篇:Vim 编辑器的基本使用)
  • [第五篇:GCC 编译器的基本使用](#第五篇:GCC 编译器的基本使用)
  • [第六篇:自动化构建 make/Makefile](#第六篇:自动化构建 make/Makefile)
  • [第七篇:Git 版本控制与 Gitee 代码托管](#第七篇:Git 版本控制与 Gitee 代码托管)
  • [第八篇:调试器 gdb/cgdb 的使用](#第八篇:调试器 gdb/cgdb 的使用)
  • 第九篇:进程基础 ------ 从冯诺依曼到 fork 创建进程(本篇)
  • 第十篇:进程状态与优先级(待更新)

如果这篇文章对你有帮助,欢迎点赞 + 收藏 + 关注,你的支持是我持续更新「从零开始学习Linux」系列的最大动力! 有问题欢迎在评论区留言,我看到都会回复。

相关推荐
李某学编程1 小时前
基于ARM的微机原理学习笔记(一)
arm开发·笔记·学习
Song_RsHt2 小时前
Redis 与微服务,什么场景下无可替代
java·redis·后端·学习·微服务
晚风吹北巷3 小时前
Linux复习篇(自用)
linux·运维·服务器·1024程序员节
by209993 小时前
Linux 压缩传输、进程查看与权限管理
linux·经验分享·1024程序员节
无足鸟ICT4 小时前
【RHCA+】reboot命令(重启)
linux·运维·服务器·1024程序员节
~kiss~5 小时前
PRO-LONG: Programmatic Memory Enables Long-Horizon Reasoning 程序式记忆赋能长程推理
学习
程序猿编码5 小时前
不依赖 vLLM/llama.cpp:一套面向学习的 DeepSeek-R1 CUDA 推理实现
c++·学习·语言模型·llama·vllm·大模型推理
DYWorker0015 小时前
第1课:基于正点原子RK3568开发板读懂Linux内核
linux·运维·网络