环境说明
- 本地工具:Xshell 7
- 服务器:腾讯云 Ubuntu 22.04(64 位)
- 编译器:gcc(Ubuntu 默认自带)
- 本篇所有命令均可在 Xshell 连接的 Ubuntu 终端里直接敲
前言
前八篇我们把"怎么写代码、怎么编译、怎么调试"这条链路走通了。从这一篇开始,我们要进入 Linux 学习里真正最核心、也最劝退新手的模块------进程。
很多同学一上来就背"进程是程序的一次执行",背完还是懵:进程到底是什么?操作系统凭什么管得住它?我写的那几行代码到底是怎么变成进程的?
这一篇不讲状态、不讲优先级、不讲调度算法(那是下一篇的事),我们只做三件事:
- 认识冯诺依曼体系结构 ------ 搞清楚计算机硬件是怎么配合干活的
- 搞懂操作系统是什么、有什么作用 ------ 理解"管理"这两个字
- 掌握进程的基本概念与基本操作 ------ 会看进程、会拿 PID、会用 fork 创建进程
把这三件事搞明白,后面学进程状态、僵尸进程、进程调度都会顺很多。
本章内容思维导图:

一、认识冯诺依曼体系结构
1.1 语法形式(结构表达)
冯诺依曼体系不是一个命令,它是一台计算机的硬件结构图。我们用一张结构图来"写"出它:
bash
┌────────────────────┐
│ 输入设备 │ 键盘 / 鼠标 / 扫描仪 / 网卡
└─────────┬──────────┘
│ 只能写入内存
▼
┌──────────────────────────────────────────┐
│ 存储器(内存) │
└─────────┬──────────────────────────▲─────┘
│ │
│ 读写 │ 读写
▼ │
┌───────────────────┐ ┌───────┴────────────┐
│ 中央处理器 CPU │ │ 输出设备 │
│ 运算器 + 控制器 │ │ 显示器 / 打印机 │
└───────────────────┘ └────────────────────┘
一句话记忆 :输入 → 内存 ← → CPU → 内存 → 输出。
1.2 功能
我们常见的笔记本、不常见的服务器,大部分都遵守冯诺依曼体系。它由这几类硬件组成:
|---------------|-----------------|-------------------|
| 部件 | 具体是什么 | 作用 |
| 输入单元 | 键盘、鼠标、扫描仪、写板、网卡 | 把外部数据送进计算机 |
| 输出单元 | 显示器、打印机、网卡 | 把计算结果送出去 |
| 中央处理器 CPU | 运算器 + 控制器 | 做算术运算、逻辑运算、控制指令执行 |
| 存储器 | 内存(RAM) | 存放程序和数据 |
关于冯诺依曼,有 三条必须刻进脑子 的结论:
- 这里说的"存储器"指的是内存,不是硬盘!
- 不考虑缓存的情况下,CPU 能且只能对内存进行读写,不能直接访问外设(输入/输出设备)。
- 外设要输入或输出数据,也只能写入内存或者从内存读取。
合起来就是课件里那句最经典的话:
所有设备都只能直接和内存打交道。
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 结构体) + 再组织(用链表或其他高效数据结构)
功能
课件里用了个特别好懂的三角色例子:
|---------|----------------------------|
| 角色 | 在"管理"里对应什么 |
| 学生 | 被管理的对象 |
| 辅导员 | 执行者 ------ 收集学生信息、落实决策 |
| 校长 | 管理者 ------ 做决策,但未必亲自见每一个学生 |
关键问题来了:校长从来没见过你,他凭什么管理你? 答案是:因为你的信息(学号、姓名、成绩)被记录在表格里,表格被组织进了学校的系统。校长只要查表就能管理你。
操作系统管理硬件,用的是一模一样的思路:
- 描述起来 ,用
struct结构体- 组织起来,用链表或其他高效的数据结构
这就是计算机世界里最著名的六字真言:先描述,再组织。
例子:用 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
这个例子说明了什么:
- PID 每次都不一样 ------ 因为每次运行都是创建了一个新的进程,PID 是 OS 动态分配的。
- 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() 用来创建一个新的子进程。调用一次,返回两次。
课件原文的两条关键结论:
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
- 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)
- 注意打印顺序父子谁先谁后是不确定的,由调度器决定,别以为一定是父进程先打印。
⚠️ 三个必须知道的疑问
学到这里,至少要能说出这几个问题:
- fork 为什么会有两个返回值? 因为
fork()内部在return之前就已经把子进程创建出来了,此时父子两个进程都会从fork返回,所以返回两次。 - 两个返回值各自给父子如何返回? 父进程拿到子进程 PID (方便以后管理它),子进程拿到 0 (子进程可以通过
getppid()找爸爸,不需要知道自己的 PID ------ 想要就用getpid())。 - 一个变量
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 本篇核心结论(背下来)
- 冯诺依曼 :所有设备都只能直接和内存打交道。
- 操作系统 :一款纯正的**"搞管理"**的软件;对下管硬件,对上提供环境。
- 管理的本质 :先描述(struct),再组织(链表)。
- 进程 = 内核数据结构(task_struct) + 自己的程序代码和数据。
- PCB 就是描述进程的 struct,Linux 下叫
task_struct,内核里用双链表组织。
- 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」系列的最大动力! 有问题欢迎在评论区留言,我看到都会回复。
