Linux--进程核心概念
目录
一、体系结构基础:冯·诺依曼模型
二、操作系统的定位与管理
三、进程的核心概念
1. 什么是进程?
2. 进程控制块(PCB)
3. 查看进程
4. 进程标识符(PID & PPID)
5. 创建进程(fork)
四、进程状态
僵尸进程(Zombie)
孤儿进程(Orphan)
五、进程优先级
六、环境变量
七、进程地址空间(虚拟内存)
八、进程调度
进程是Linux系统的灵魂,是系统进行资源分配和调度的基本单位。深入理解进程是掌握Linux系统编程和性能优化的基石。本文将对Linux进程的核心概念进行总结。
一、体系结构基础:冯·诺依曼模型
理解进程前,必须先了解计算机的底层工作模型。
-
核心思想:计算机由输入设备、存储器、运算器、控制器、输出设备五大部分组成。
-
关键强调:
-
这里的存储器特指内存。
-
CPU只能直接读写内存,无法直接访问外设。
-
所有外设的输入/输出数据都必须经过内存。
-
-
以QQ聊天时数据流为例来理解一下冯诺依曼体系架构:
-
输入:键盘敲击消息 → 输入设备 → 内存
-
处理:CPU 从内存读取数据并进行处理
-
输出:内存 → 输出设备 → 网卡发送 / 显示器显示
-
二、操作系统的定位与管理
操作系统(OS)是计算机系统的“大管家”。
-
定位:一款纯粹的管理软件,向下管理硬件,向上为用户程序提供执行环境。
-
管理内容:进程管理、内存管理、文件管理、驱动管理。
-
如何管理?——“先描述,再组织”
-
描述:用
struct
结构体(如task_struct
)将进程的属性信息描述起来。 -
组织:用链表等高效的数据结构将多个
struct
组织起来,方便进行增删改查。
-
-
系统调用 vs 库函数:操作系统将部分管理功能以系统调用的接口形式暴露,功能基础但对于了解程度要求高。开发者对其封装后形成库函数(如C库),更方便使用。
三、进程的核心概念
1. 什么是进程?
-
基本概念:程序的一个执行实例,正在执行的程序。
-
内核观点:担当分配系统资源(CPU时间、内存)的实体。
2. 进程控制块(PCB)
进程的所有信息都被放在一个称为进程控制块(PCB)的数据结构中。在Linux中,PCB的具体实现是 task_struct
。
-
task_struct
内容分类:-
标识符(PID):唯一标识进程的ID。
-
状态:进程状态(运行、睡眠、停止、僵尸等)。
-
优先级:相对于其他进程的优先级。
-
程序计数器:即将执行的下一条指令的地址。
-
内存指针:指向程序代码、数据及共享内存的指针。
-
上下文数据:进程运行时CPU寄存器中的数据。
-
I/O状态信息:进程使用的I/O设备和文件列表。
-
记账信息:处理器时间、时间限制等。
-
-
组织方式:内核中所有
task_struct
通过链表的形式组织起来。
3. 查看进程
-
命令行工具:
ps aux
、ps axj
、top
-
文件系统:
/proc/[PID]/
目录下包含了进程的详细信息。
4. 进程标识符(PID & PPID)
-
PID:当前进程的唯一ID。
-
PPID:父进程的ID。
-
系统调用:
getpid()
(获取自身PID),getppid()
(获取父进程PID)
5. 创建进程(fork)
-
系统调用:
fork()
-
关键特性:
-
调用一次,返回两次:在父进程中返回子进程的PID,在子进程中返回0(失败则返回-1)
-
写时拷贝:子进程共享父进程的代码段,数据段在初始时也共享。只有当任何一方尝试修改数据时,才会为该数据分配独立的物理空间。这是一种高效的内存管理策略
-
#include <stdio.h>
#include <unistd.h>int main() {pid_t id = fork();if (id < 0) {// fork失败perror("fork");return 1;} else if (id == 0) {// 子进程代码区printf("I am child, PID: %d\n", getpid());} else {// 父进程代码区printf("I am parent, PID: %d, Child PID: %d\n", getpid(), id);}sleep(1);return 0;
}
-
fork
后,代码共享,数据看似共享。 -
只有在任何一方尝试修改数据时,操作系统才会真正复制一份数据给修改方。
+-----------------------+
| 父进程 |
| +-----------------+ |
| | 代码段 | | (共享)
| +-----------------+ |
| | 数据段 | |<----------+
| | g_val = 0 | | (共享) | 子进程尝试修改
| +-----------------+ | | 触发写时拷贝
| | 堆、栈 | | |
| +-----------------+ | |
+-----------------------+ |
|
+-----------------------+ |
| 子进程 | |
| +-----------------+ | |
| | 代码段 | | (共享) |
| +-----------------+ | |
| | 数据段 | | (共享) |
| | g_val = 0 | |-----------+
| +-----------------+ |
| | 堆、栈 | |
| +-----------------+ |
+-----------------------+ |
|
+---------------v----+
| 内核为新数据 |
| 分配物理页面 |
| g_val = 100 |
+-------------------+
四、进程状态
Linux内核中定义的进程状态主要包括(在kernel源代码中定义):
-
R (TASK_RUNNING):运行或就绪态。进程正在CPU上运行或在运行队列中等待被调度。
-
S (TASK_INTERRUPTIBLE):可中断睡眠。进程在等待某个事件完成(如等待输入),可以被信号中断唤醒。
-
D (TASK_UNINTERRUPTIBLE):不可中断睡眠。进程通常在等待I/O操作结束,不可被信号中断。这是一种深层睡眠状态。
-
T (TASK_STOPPED):停止状态。进程被信号(如
SIGSTOP
)暂停运行,可通过信号(如SIGCONT
)继续运行。 -
X (TASK_DEAD):死亡状态。进程结束运行,资源被回收。这是一个瞬时状态,用户无法捕捉。
-
Z (TASK_ZOMBIE):僵尸状态。这是一个非常重要且需要警惕的状态。
僵尸进程(Zombie)
-
成因:子进程先于父进程退出,但父进程没有调用
wait()
或waitpid()
来读取子进程的退出状态信息。 -
危害:
-
维护退出状态的
PCB
无法被释放,导致内存泄漏。 -
如果父进程创建大量子进程且从不回收,将耗尽系统的进程资源(PID)。
-
-
如何避免:父进程需要通过
wait
系列系统调用来回收子进程资源。
孤儿进程(Orphan)
-
成因:父进程先于子进程退出。
-
处理:孤儿进程会被1号init进程(或systemd进程)领养,并由该进程负责其退出后的回收工作。这避免了僵尸进程的产生。

-
R状态是中心,是获得CPU的唯一入口。
-
S和D都是等待,但
S
可被信号唤醒,D
不行。 -
Zombie是子进程给父进程留的“遗嘱”,父进程不读(
wait
),遗嘱就一直留着,占着资源。 -
Orphan会被“福利院院长”init进程收养,保证其正常结束。
五、进程优先级
-
基本概念:CPU资源分配的先后顺序。
-
查看命令:
ps -l
主要关注 PRI 和 NI 列。-
PRI (Priority):进程的优先级,值越小优先级越高,越容易被调度。
-
NI (Nice):进程优先级修正值。其关系为:
PRI(new) = PRI(old) + NI
。 -
NI范围:-20 到 19。普通用户只能调高NI值(降低优先级)。
-
-
调整NI值:使用
top
命令,按r
后输入进程PID和新的nice值。
六、环境变量
-
基本概念:用于指定操作系统运行环境参数的变量,具有全局属性。
-
常见环境变量:
-
PATH
:指定命令的搜索路径。echo $PATH
-
HOME
:指定用户的主工作目录。echo $HOME
-
SHELL
:指定当前使用的Shell。
-
-
相关命令:
-
env
:查看所有环境变量。 -
export
:设置一个新的环境变量。export MY_VAR=Hello
-
unset
:清除一个环境变量。unset MY_VAR
-
-
在程序中访问:
-
命令行参数:
main(int argc, char *argv[], char *env[])
-
全局变量:
extern char **environ;
-
系统调用:
getenv()
(获取),setenv()
(设置)。
-
#include <stdio.h>
#include <stdlib.h>
int main() {printf("PATH: %s\n", getenv("PATH"));return 0;
}
七、进程地址空间(虚拟内存)
这是理解进程隔离和内存管理的核心。
-
现象:在 fork 后的父子进程中,修改同一个全局变量,变量的地址值相同,但内容不同。
-
结论:
-
我们在C/C++中看到的地址(
&变量
)都是虚拟地址/线性地址,而非物理地址。 -
操作系统负责将虚拟地址通过页表映射到物理地址。
-
父子进程的虚拟地址相同,但被OS映射到了不同的物理页上,从而实现了进程间的独立性和写时拷贝技术。
-
-
意义:
-
保护:一个进程的错误操作(如非法内存访问)不会影响其他进程。
-
简化:让每个进程都认为自己独占整个内存空间,简化了程序员的编程模型。
-
高效:配合“写时拷贝”等技术,可以高效地利用内存。
-
+------------------------+ +------------------------------+
| 进程A | | 进程B |
| 虚拟地址空间 | | 虚拟地址空间 |
| | | |
| 0x8049768: g_val | | 0x8049768: g_val |
| ... | | ... |
+-------------------------+ +------------------------------+
↓ ↓
+---------+ +---------+
| 页表A | | 页表B |
+---------+ +---------+
↓ ↓
+-------------------------------------------------+
| 物理内存 |
| +-----------+ +---------------+ |
| | 页面1 | | 页面2 | |
| | g_val=0 | | g_val=100 | |
| +-----------+ +----------------+ |
| |
+-------------------------------------------------+
-
每个进程都有自己的虚拟地址空间,感觉自己是独占内存的。
-
通过每个进程独有的页表,将相同的虚拟地址映射到不同的物理地址上。
-
这就是进程独立性和写时拷贝得以实现的底层基础。
八、进程调度
Linux 2.6内核采用了高效的 O(1)调度算法。
-
核心数据结构:每个CPU有一个运行队列(runqueue),包含活动队列(active) 和过期队列(expired)。
-
活动队列:存放时间片未用完的所有进程,按优先级(0-139)组织成
140
个队列。 -
过期队列:存放时间片已耗尽的进程,结构同活动队列。
-
调度过程:
-
调度器从活动队列的优先级最高的非空队列中选取第一个进程运行。
-
使用
bitmap
位图来快速查找非空队列,时间复杂度为O(1)。 -
当活动队列为空时,交换
active
和expired
两个指针的值,过期队列即刻变为新的活动队列。
-
-
优点:调度过程非常高效,与系统中进程数量无关。