【Linux入门】从0开始认识进程
本文主要内容包括:
- 认识冯诺依曼体系结构,理解现代计算机的基本工作原理
- 学习操作系统的概念与定位,理解管理的本质思想
- 深入理解进程概念,认识 PCB(进程控制块)及其作用
- 学习进程状态,掌握进程创建过程,理解僵尸进程和孤儿进程的形成原因及危害
- 了解 Linux 进程调度机制,掌握进程优先级相关概念
- 理解进程的竞争性与独立性,区分并发与并行
- 理解进程切换过程,认识 Linux 2.6 Kernel 中 O(1) 调度算法架构
- 学习环境变量相关知识,掌握常见环境变量及相关命令
- 熟悉 getenv、setenv 等环境变量操作函数
- 理解 C 语言内存空间分布规律
- 了解进程内存映像与应用程序之间的区别
- 认识虚拟地址空间及其存在意义
一、冯诺依曼体系结构
冯诺依曼体系结构是现代计算机最常见的组织方式。我们平时接触的笔记本、服务器,大体都符合这套模型。它把计算机划分成几个基本部分:

- 输入单元,比如键盘、鼠标、扫描仪
- 中央处理器 CPU,负责运算和控制
- 输出单元,比如显示器、打印机
- 存储器,也就是内存
这里要特别注意:这里说的存储器指的是内存,不是硬盘。
冯诺依曼体系最核心的规则是:
- CPU 只能直接和内存打交道
- 外设也只能先把数据写入内存,或者从内存取数据
- 所有设备的数据传输,本质上都绕不开内存
也就是说,所有设备都只能直接和内存交互。

注意,理解冯诺依曼不能只停留在硬件名词上,还要能把它和软件数据流联系起来。比如 QQ 聊天时:
- 你输入消息,先进入内存
- CPU 处理消息内容
- 消息通过网络协议发出去
- 对方收到后,数据先落到内存,再交给对应程序读取
如果发送文件,过程也是一样的,只是传输的数据量更大,路径更长。
二、操作系统(Operator System)
2-1 概念
操作系统是整个计算机系统中最基础的一类软件。广义上说,它包含两部分:
- 内核,比如进程管理、内存管理、文件管理、驱动管理
- 其他系统级程序,比如函数库、shell 等
操作系统对外看起来像一个整体,但内部其实分工明确。它最重要的任务不是替代应用程序,而是给应用程序提供运行环境。

2-2 设计OS的目的
操作系统存在的意义可以分成两个方向:
- 对下:和硬件交互,管理所有软硬件资源
- 对上:给用户程序提供一个稳定、统一、好用的执行环境

这意味着操作系统既要管硬件,又要服务上层程序。
2-3 核心功能
操作系统在整个软硬件架构中的定位,可以概括成一句话:
它是一款专门负责管理的软件
它要管理的对象很多,比如:
- CPU
- 内存
- 硬盘
- 文件
- 进程
- 设备
所以理解操作系统,最重要的不是把它想成一个神秘的大黑盒,而是把它看成一个管理者。
2-4 如何理解 管理
管理这件事,最容易理解的方法,就是拿现实中的组织结构做类比。
比如学校里:
- 学生是被管理对象
- 辅导员负责日常管理
- 校长负责更高层次的组织和决策
管理一个对象,通常分两步:
- 先描述它
- 再组织它
操作系统管理硬件和进程,也遵循同样的思路:
- 用结构体描述对象属性
- 用链表、树等数据结构组织这些对象
描述起来用结构体,组织起来用链表或其他高效数据结构

2-5 系统调用和库函数概念
操作系统不会把所有能力都直接暴露给用户程序。它会提供一部分基础接口给上层使用,这些接口叫做 系统调用。
系统调用的特点是:
- 功能比较底层
- 使用门槛高
- 接口偏基础
为了方便开发,很多系统调用会被进一步封装成更好用的 库函数。
简单理解:
- 系统调用负责和内核直接打交道
- 库函数负责把复杂接口包装得更容易用
三、进程
3-1 基本概念与基本操作
教材对进程的定义:程序的一个执行实例,也就是正在执行的程序。
从内核的角度看,进程是:
分配系统资源的实体
现在可以先把进程理解成:
内核数据结构 task_struct + 程序代码和数据
也就是说,进程不是单纯的一段代码,而是代码和内核管理信息的组合。
3-1-2 描述进程-PCB
进程的信息不会散落在内核中,而是统一放在一个专门的数据结构里,这个结构叫做 进程控制块 PCB。
在 Linux 中,PCB 对应的就是 task_struct。
task_struct 里会包含进程的很多属性
如:
- 进程标识符
- 状态
- 优先级
- 程序计数器
- 内存指针
- 上下文数据
- I/O 状态信息
- 记账信息
可以把它理解成进程的档案袋,进程要被管理,先得有档案。
3-1-3 task_struct
Linux 内核中,进程就是靠 task_struct 描述的。它会被装载到内存里,里面保存了进程的完整信息。
常见字段为:
- 标识符:区分不同进程
- 状态:表示当前处于运行、睡眠、停止等哪种状态
- 优先级:决定调度先后
- 程序计数器:下一条将要执行的指令地址
- 内存指针:代码段、数据段以及共享内存的相关信息
- 上下文数据:寄存器现场
- I/O 状态信息:文件列表、I/O 请求等
- 记账信息:CPU 时间、执行时间限制等
所有进程在内核里还会以双链表的形式组织起来,方便统一管理。

3-1-4 查看进程
查看进程信息有两种常见方式:
- 直接看
/proc文件系统 - 使用
top、ps这类用户态工具
比如 PID 为 1 的进程信息,可以查看 /proc/1 目录。
3-1-5 通过系统调用获取进程标识符
进程最重要的两个标识符是:
- PID:进程号
- PPID:父进程号
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
这两个接口非常常用,尤其在父子进程、守护进程、进程树分析里。
3-1-6 通过系统调用创建进程-fork初识
创建子进程要用 fork。
基本概念
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;
}
通常会结合 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 : %d!, ret: %d\n", getpid(), ret);
}
else // 父进程
{
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
注意:
- 为什么
fork会有两个返回值 - 返回值如何分别给父进程和子进程
这两个问题后面会深入讲进程复制机制。
3-2 进程状态
3-2-1 Linux内核源代码
Linux 内核里,进程状态不是单一的,而是有多种状态。
常见状态包括:
- R:运行状态
- S:睡眠状态
- D:磁盘休眠状态
- T:停止状态
- X:死亡状态
- Z:僵尸状态

3-2-2 进程状态查看
查看进程状态常用命令是:
ps auxps axj
其中:
a:显示终端上的所有进程x:显示没有控制终端的进程j:显示进程组 ID、会话 ID、父进程 ID 等作业控制信息u:以用户为中心显示详细信息

3-2-3 Z(zombie)-僵尸进程
它的形成过程是:
- 子进程已经退出
- 父进程还活着
- 父进程没有调用
wait或waitpid回收子进程状态 - 子进程就会进入 Z 状态
僵尸进程本身不再运行,但它的退出信息还留在进程表中,等待父进程读取。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id > 0)
{
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else
{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
僵尸进程危害
僵尸进程会一直占着进程表中的一部分信息,因为它的退出状态还要保留给父进程查看。
这意味着:
- PCB 相关信息不能立刻释放
- 如果父进程长期不回收,资源会被浪费
- 大量僵尸进程会造成系统压力
注意,这类泄漏不是内存块忘了释放,而是进程状态和控制块长期无法回收。
3-2-5 孤儿进程
如果父进程先退出,子进程还在运行,那么这个子进程就叫做 孤儿进程。
孤儿进程不会一直无人管理,它会被 1 号进程,也就是 init 或 systemd 进程领养,后续由它负责回收。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else
{
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
孤儿进程本身不是错误,但要理解它的归宿是被系统接管。
3-3 进程优先级
3-3-1 基本概念
CPU 资源分配的先后顺序,就是进程的优先级。
优先级高的进程,通常更早获得执行机会。
这在多任务系统里非常重要,因为 CPU 资源有限,进程很多,必须通过优先级来协调竞争。
3-3-2 查看系统进程
在 ps -l 的输出中,常见字段有:
- UID:执行者身份
- PID:进程号
- PPID:父进程号
- PRI:实际执行优先级
- NI:nice 值
3-3-3 PRI and NI
PRI 可以理解为真正的优先级,值越小越优先执行。
NI 是 nice 值,可以理解成优先级修正值。
二者关系可以写成:
PRI(new)=PRI(old)+nice
所以:
- nice 值越小,优先级越高
- nice 值越大,优先级越低
Linux 中 nice 的取值范围通常是 -20 到 19。
3-3-4 PRI vs NI
区分:
PRI是进程优先级NI不是优先级本身NI是影响优先级的修正值
也就是说,nice 值只是调整项,不是最终值。
3-3-5 查看进程优先级的命令
可以用 top 修改已有进程的 nice 值:
- 先输入
top - 再按
r - 输入进程 PID
- 再输入新的 nice 值
其他命令还有:
nicerenice
相关系统函数:
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
3-3-6 竞争、独立、并行、并发
| 概念 | 含义 |
|---|---|
| 竞争性 | 多个进程争抢有限 CPU 资源 |
| 独立性 | 多个进程互不干扰,各自运行 |
| 并行 | 多个进程在多个 CPU 上同时运行 |
| 并发 | 多个进程在一个 CPU 上通过切换推进执行 |
并行强调真正同时执行,并发强调一段时间内都能推进。
3-4 进程切换
进程切换的本质是 CPU 上下文切换,也就是:
- 保存当前任务的寄存器现场
- 恢复下一个任务的寄存器现场
- CPU 转去执行新的任务

注意,时间片是分时系统里非常重要的概念。每个进程都会被分配一定的 CPU 时间片,时间片到了,就会被系统切走。
3-4 Linux2.6内核进程O(1)调度队列
Linux 2.6 时代的调度器采用过 O(1) 调度思想,目标是:查找最合适进程的时间复杂度尽量保持常数级。
基本结构

每个 CPU 对应一个 runqueue。
如果有多个 CPU,就要考虑负载均衡问题。
调度队列分为两个核心数组:
- active:活动队列
- expired:过期队列
优先级划分
- 普通优先级:100 到 139
- 实时优先级:0 到 99
普通进程最常见。
活动队列
时间片没有用完的进程都放在活动队列中。

nr_active:活动进程数queue[140]:每个优先级一个队列- 同优先级按 FIFO 排队
查找过程大致是:
- 从低下标开始遍历
- 找到第一个非空队列
- 取出队首进程执行
为了提高效率,还会配合 bitmap 记录哪些队列非空。
过期队列
时间片用完的进程会进入过期队列。
当活动队列处理完后,再把过期队列中的进程重新计算时间片,交换 active 和 expired 指针,继续下一轮调度。
所以整个查找过程可以保持接近 O(1) 的效率。
四、命令行参数和环境变量
4-1 基本概念
环境变量是操作系统中用来描述运行环境的一组参数。
它有几个典型特点:
- 和系统配置有关
- 具有全局属性
- 会被子进程继承
比如编译和链接时,程序能找到动态库或静态库,很大程度上就和环境变量有关。
4-2 常见环境变量
常见环境变量包括:
PATH:命令搜索路径HOME:当前用户主目录SHELL:当前使用的 shell,一般是/bin/bash
4-3 查看环境变量方法
查看某个环境变量:
echo $NAME
比如:
echo $PATHecho $HOME
关于 PATH 的理解
如果一个程序不在系统默认路径里,通常要写完整路径才能执行。
把程序所在目录加入 PATH 后,就可以直接运行。
4-4 和环境变量相关的命令
常见命令有:
echo:查看变量值export:导出环境变量env:显示所有环境变量unset:清除环境变量set:显示 shell 变量和环境变量
4-5 环境变量的组织方式

每个程序启动时,都会收到一张环境表。
这张环境表本质上是一个字符指针数组:
- 每个元素指向一个环境字符串
- 每个字符串都以
\0结尾
4-6 通过代码如何获取环境变量
方法一:通过命令行参数传入的 env
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++)
{
printf("%s\n", env[i]);
}
return 0;
}
方法二:通过全局变量 environ
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
environ 没有包含在常规头文件中,所以使用前要 extern 声明。
4-7 通过系统调用获取或设置环境变量
常用函数是:
getenvputenv
示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
getenv 用来获取指定环境变量的值。
4-8 环境变量通常是具有全局属性的
环境变量可以被子进程继承。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv("MYENV");
if(env)
{
printf("%s\n", env);
}
return 0;
}
如果只设置:
MYENV="helloworld"
但不 export,程序通常拿不到。
如果执行:
export MYENV="hello world"
再运行程序,就能读取到。
这说明:导出的环境变量会进入环境表,并被子进程继承。
五、程序地址空间
核心:
- 程序地址空间
- 进程地址空间
- 虚拟地址
- 物理地址
5-1 研究平台
这里讨论的环境是:
- kernel 2.6.32
- 32 位平台
地址空间布局和内核行为会受平台影响。
5-2 程序地址空间查看
验证不同区域的地址分布:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem);
printf("heap addr: %p\n", heap_mem1);
printf("heap addr: %p\n", heap_mem2);
printf("heap addr: %p\n", heap_mem3);
printf("test static addr: %p\n", &test);
printf("stack addr: %p\n", &heap_mem);
printf("stack addr: %p\n", &heap_mem1);
printf("stack addr: %p\n", &heap_mem2);
printf("stack addr: %p\n", &heap_mem3);
printf("read only string addr: %p\n", str);
for(int i = 0; i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
通过打印可以看出:
- 代码段有自己的地址
- 全局变量有自己的地址
- 堆和栈分布不同
- 字符串常量在只读区
- argv 和 env 也都有自己的位置
这说明进程内存布局是有规律的。
5-3 虚拟地址
如:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出会出现一个很关键的现象:
- 父子进程变量地址一样
- 但变量值不一样
这说明:
- 这个地址不是物理地址
- 它是 虚拟地址
所以我们在 C 和 C++ 里看到的地址,全部都是虚拟地址。物理地址对用户是不可见的,由操作系统统一管理。
5-4 进程地址空间
以前常说程序地址空间,但更准确的说法其实是 进程地址空间。

原因:
- 每个进程都有自己独立的地址空间
- 同一个虚拟地址,在不同进程里可以映射到不同物理地址
- 进程之间互不干扰
进程地址空间由操作系统通过页表进行映射和管理。
描述进程地址空间的核心结构体是:
mm_struct:内存描述符
在 task_struct 中,有一个指向 mm_struct 的指针。
5-5 虚拟内存管理
task_struct 和 mm_struct 的关系,可以理解为:
task_struct管进程整体mm_struct管进程地址空间
mm_struct 描述的是整个用户空间。每个进程都有自己的 mm_struct,这样才能拥有独立地址空间。

常见字段包括:
mmap:指向虚拟区间链表mm_rb:红黑树根task_size:进程虚拟地址空间大小start_code、end_code:代码段范围start_data、end_data:数据段范围start_brk、brk:堆区范围start_stack:栈区起始arg_start、arg_end:命令行参数区域env_start、env_end:环境变量区域
vm_area_struct
Linux 用 vm_area_struct 表示一个独立的虚拟内存区域,也叫 VMA。
一个进程通常会有多个 VMA,分别表示:
- 代码区
- 数据区
- 堆区
- 栈区
- 文件映射区
- 共享内存区


vm_area_struct 的一些典型字段:
vm_start:起始地址vm_end:结束地址vm_next、vm_prev:链表前后指针vm_rb:红黑树节点vm_mm:所属的 mm_structvm_flags:标志位vm_file:映射文件vm_private_data:私有数据
5-6 为什么要有虚拟地址空间
如果程序直接操作物理内存,会有什么问题?
1. 安全风险
如果每个进程都能直接访问物理内存,那么:
- 程序可以随便读写系统内存
- 木马和病毒更容易破坏系统
- 进程之间也更容易互相干扰
显然不安全。
2. 地址不确定
程序编译好后存放在磁盘上,运行时才被加载到内存。
如果直接用物理地址,那么每次装载的位置都可能不同:
- 第一次运行,内存可能很空
- 第二次运行,内存已经有很多程序了
- 地址每次都可能变化
这样程序就很难稳定运行。
3. 效率低下
如果直接使用物理内存,进程会以整体块的形式管理。
当内存不足时,想把不常用的程序换出到磁盘,就得整块搬移,效率很低。
虚拟地址空间的好处
有了虚拟地址空间和页表机制之后,问题就解决了很多:
- 进程看起来都有独立的地址空间
- 进程之间互不干扰
- 物理内存可以被统一调度
- 内存管理和进程管理可以解耦
malloc和new时,未必立刻分配物理内存- 需要访问时才真正触发分配,这叫延迟分配
虚拟地址空间的本质,是让进程看到一个稳定、连续、安全的内存空间
总结
- 计算机所有设备最终都要和内存打交道
- 操作系统的核心任务是管理
- 进程是操作系统管理资源的基本单位
- task_struct 是进程的核心描述结构
- fork 可以创建子进程
- 进程有运行、睡眠、僵尸、孤儿等状态
- 优先级和调度决定进程执行顺序
- 环境变量会被进程继承
- 程序看到的是虚拟地址,不是物理地址
- 虚拟地址空间让进程更安全、更稳定、更高效
完
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)