
一、什么是进程
只要是计算机科班出身,肯定对《操作系统》中的“进程”概念有所了解。但若想真正掌握并说清楚其本质,却并不容易。我们通常说,进程是计算机操作系统中资源分配和独立运行的基本单位。但你是否思考过:为什么它是资源分配的基本单位?又为何能称为独立运行的基本单位呢?这些问题的答案,或许并不像教科书上定义的那般清晰。
二、程序、进程、线程和协程
我们先从最基础的概念厘清关系。程序,最简单的理解是开发者编写的代码及其相关依赖。更准确地说,它是编译后存放在硬盘上的二进制代码和数据集合,是静态的。而进程,则是程序被加载到内存中运行起来的实体,是动态的。程序没有生命周期,只是一堆文件;而进程则有明确的创建、运行和消亡过程。
理解了这一点,再来区分线程和协程就更容易了。我们可以用一个比喻来说明:
- 进程:好比一个独立的工人,拥有全套工具(资源)去完成一项完整的任务。在计算机中,一个进程拥有独立的地址空间和系统资源。
- 线程:如同这个工人可以在一个任务中,交替或同时进行多项子操作(比如一只手测量,另一只手记录)。线程是进程内的执行单元,共享进程的资源,但拥有独立的执行流。
- 协程:则像是把任务流程设计好,交给自动化设备(多个机械手)来协作完成,工人(开发者)无需时刻关注切换细节。协程是一种更轻量级的用户态线程,由程序自行调度切换。
由此可见,多线程需要开发者处理复杂的同步与资源竞争问题,而协程旨在简化并发编程的复杂度。不过,目前系统层面对协程的原生支持仍在发展中,其应用复杂度因语言和库而异。
三、操作系统中的进程及其本质
要掌握进程的本质,不妨从其历史演进来看。早期的计算机一次只能执行一个任务,无所谓“进程”。随着硬件能力提升,出现了多道程序技术,为了管理和描述这些同时存在于内存中的“作业”,进程的概念应运而生。
再以工厂为例:一个手工作坊(单道程序)只需一人。当引入机器(多道程序)后,就需要多个工人,每人负责一个环节,拥有自己的工具(资源)。如果工人不足,机器产能就会浪费。现代化大生产(现代操作系统)中,产品(任务)海量,工人(进程)众多,这时就需要一套精密的管理体系(操作系统内核)来协调。
那么,操作系统如何管理这些“工人”呢?答案是进程控制块(PCB, Process Control Block)。操作系统维护着一张PCB表,每个PCB对应一个进程,记录了管理该进程所需的一切信息,可以看作是进程的“身份证”和“档案袋”。一个进程的完整实体,就是由 “PCB + 加载到内存的程序段 + 该程序操作的数据结构集合” 构成。
下面以Linux内核中描述进程的核心结构体 task_struct(可视为PCB的具体实现)为例,看看它都包含了哪些信息:
struct task_struct {
// 进程状态 --是否在工作
volatile long state; // TASK_RUNNING, TASK_INTERRUPTIBLE 等
// 调度信息
struct sched_entity se; // CFS 调度实体
int prio; // 动态优先级
int static_prio; // 静态优先级
// 进程标识 --身份标识
pid_t pid; // 进程 ID
pid_t tgid; // 线程组 ID(主线程 pid)
struct task_struct __rcu *parent; // 父进程
struct list_head children; // 子进程链表
struct list_head sibling; // 兄弟进程(同父)
// 内存管理 --最典型的资源
struct mm_struct *mm; // 用户地址空间(堆、栈、代码段等)
struct mm_struct *active_mm; // 内核线程使用
// 文件系统
struct fs_struct *fs; // 根目录、当前工作目录
// 打开的文件
struct files_struct *files; // 文件描述符表(fd table)
// 信号处理
struct signal_struct *signal; // 共享信号处理(线程组共享)
sigset_t blocked; // 被阻塞的信号集
// CPU 上下文--任务切换时需要保存
struct thread_struct thread; // CPU 寄存器、内核栈指针等
// 内核栈
void *stack; // 指向内核栈(通常 16KB)
// 命名空间--容器的支持
struct nsproxy *nsproxy; // UTS, PID, network, mount 等 namespace
// ... 其它省略
};
可以看到,PCB主要涵盖四大类信息:标识信息、资源信息、状态与调度信息、以及CPU上下文。操作系统正是通过这些信息,对一个进程进行全面的控制和管理。这也是学习 计算机基础 ,尤其是 操作系统 原理时,必须深入理解的核心。
综上所述,进程的本质是一个正在执行的程序实例,是系统进行资源分配和调度的独立单元,它是一种动态的、由内核管理的抽象实体。 理解了这个本质,就明白了为什么开发中需要关注进程ID、上下文切换以及数据传递。这也回答了文章开头提出的问题:正因进程拥有独立的资源集合(通过PCB管理),所以它是资源分配的基本单位;正因它拥有独立的执行上下文和调度状态,所以它是独立运行的基本单位。
视角决定认知:对开发者而言,进程是一个执行单元;对内核而言,进程是一个被管理的任务对象;对用户而言,进程就是一个正在运行的软件,比如浏览器或文档编辑器。
四、Linux系统中如何创建进程
在Linux系统中,我们可以使用 ps 命令查看进程,或到 /proc/[PID] 目录下查阅更详细的信息。但更重要的是理解一个进程是如何被创建并运行的,其核心步骤如下:
- 程序存储:程序的二进制代码静态存储在硬盘上。
- 加载入内存:通过命令或系统调用启动程序,内核将其代码和所需数据加载到内存中。
- 创建PCB:内核为该程序创建一个新的PCB(
task_struct),并填充PID、状态、资源指针等信息。如果是通过 fork() 创建,则会复制父进程的许多资源。
- 加入调度队列:将新创建的PCB加入到操作系统的进程调度队列(PCB列表)中。
- 调度执行:操作系统调度器根据策略,选择合适的时机分配CPU时间片,开始执行该进程。
在代码层面,这通常通过 fork() 和 exec() 系列函数来实现。fork() 用于创建一个作为当前进程副本的子进程;exec() 则用于将当前进程的内存空间替换为新的程序。以下是一个经典的示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("create process error!");
return 1;
}
else if (pid == 0) {
// child process
printf("child PID: %d, parent PID: %d\n", getpid(), getppid());
// 使用 df -h 命令替换当前进程镜像
execl("/bin/df", "df", "-h", NULL);
perror("execl start error!");
return 1;
}
else {
//parent process
printf("parent PID: %d, child PID: %d\n", getpid(), pid);
int status = -1;
waitpid(pid, &status, 0); // 等待子进程结束
printf("child exited, status %d\n", WEXITSTATUS(status));
}
return 0;
}
这段代码清晰地展示了进程创建、替换和等待的过程,是理解 Linux 下 进程控制 的绝佳示例。除了 fork/exec,Linux 还提供了 clone() 系统调用,可以更精细地控制资源共享程度,常被用于实现线程。
五、总结
本文并非对进程概念的基础复述,而是旨在引导有一定经验的开发者,从更底层的视角重新审视这个核心概念。进程作为操作系统最基础的抽象之一,其重要性常常被上层应用开发者忽视。然而,许多线程并发问题的根源,最终都需要回到进程模型和资源管理的层面来寻找答案。
唯有透彻理解进程作为“资源容器”和“执行实体”的本质,理解PCB如何作为其与内核沟通的桥梁,我们才能在处理更复杂的并发、 内存管理 或系统编程问题时游刃有余。希望本文能为你深入理解 C语言 系统编程及操作系统原理打开一扇新的窗户。欢迎在 云栈社区 继续交流探讨。