本文系统介绍了Linux进程管理的核心概念。主要内容包括:1)进程定义与特性:进程是程序的动态执行实例,具有独立性、并发性、动态性和异步性等特征;2)进程组成结构:详细解析了进程控制块(PCB)和虚拟地址空间的内存分布;3)进程状态转换:阐述从创建到终止的生命周期及状态转换关系;4)进程操作命令:介绍top、ps、kill等常用进程管理工具;5)进程创建与控制:深入讲解fork()、exec族函数的使用及区别;6)进程资源回收:分析wait和waitpid函数的应用场景及差异。文章全面覆盖了Linux进程管理的核心知识体系。


一、进程

1.定义:

在Linux系统编程中,进程(Process) 是一个正在执行的程序的实例。它是操作系统进行资源分配和调度的基本单位。进程的出现是为了实现多任务,宏观上看是并行,但是微观上看还是串行的

2.进程 VS 程序:

  • 程序:静态的、存储在磁盘上的可执行文件

  • 进程:动态的、正在执行的程序实例(程序执行的过程),包括进程的创建、调度、消亡,拥有独立的内存空间和系统资源

  • 区别:

1)程序是永存的,进程是暂时的

2)进程有程序状态的变化,程序没有

3)进程可以并发,程序无并发

4)进程于进程会存在竞争计算机资源

5)一个程序可以运行多次,变成多个进程;一个进程可以运行一个或多个程序

3.进程的特性:

- 独立性:每个进程有独立的地址空间
- 并发性:多个进程可以同时运行(并发/并行)
- 动态性:有创建、运行、阻塞、终止等生命周期
- 异步性:进程按各自独立、不可预知的速度前进

4.内存的分布:

二、进程的组成

1.进程控制块(PCB)

是操作系统用来描述进程的各种信息的数据结构,它包含了进程的标识符、状态、优先级、CPU 寄存器信息、内存管理信息等。PCB 是进程存在的唯一标志,操作系统通过 PCB 来感知和控制进程。(Linux中为task_struct)

2.进程地址空间:分为多个段

进程地址空间是“虚拟”的,是现代操作系统为了高效、安全、灵活地管理内存而设计的一种抽象机制。简单来说,虚拟地址空间是操作系统为每个进程提供的“假象”,让进程以为自己独占了整个内存,而实际上物理内存是由操作系统统一管理和分配的。

什么是虚拟地址?

虚拟地址 vs 物理地址

  • 物理地址:内存硬件单元(RAM)的真实地址,从0开始到最大内存容量。

  • 虚拟地址:进程看到的地址,同样从0开始到某个上限(32位系统为4GB)。但虚拟地址需要经过内存管理单元(MMU)页表的转换,才能找到对应的物理内存位置。

为什么需要虚拟地址?

  • 进程隔离和系统安全

如果没有虚拟地址,所有程序直接操作物理内存,一个程序的野指针或缓冲区溢出会直接篡改其他程序甚至操作系统内核的数据,导致系统崩溃或被攻击。虚拟地址让每个进程拥有独立的地址空间,进程之间无法直接访问彼此的物理内存,从根本上保证了系统的稳定性和安全性。

  • 简化内存管理与编程模型

对程序员和编译器来说,虚拟地址空间是连续的、从零开始的。开发者无需关心物理内存哪里有空隙、哪些地址已被占用,也无需处理内存碎片。操作系统通过页表机制,将离散的物理内存页拼凑成连续的虚拟空间呈现给进程,大大简化了程序开发。

  • 高效利用物理内存

虚拟内存机制让操作系统可以更灵活地使用有限的物理内存

1)代码段(text): 

(通常称为 Text Segment),内容直接来源于存储在硬盘上的可执行文件(例如 Linux 下的 ELF 文件或 Windows 下的 PE 文件)可执行指令。 它存放的是经过编译器、汇编器、链接器处理后的机器码。这些机器码就是由 CPU 可以直接识别和执行的二进制指令

2)数据段:
  • 已初始化数据段(.date)
  • 未初始化数据段(.bss)
  • 只读数据段(.rodate)
3)堆(heap):

动态分配的内存,在32位操作系统中,在Linux下,堆区的理论最大尺寸是 3GB,但实际可用的连续堆空间通常远小于此,可能最高能达到2.9GB左右 

4)共享和映射(share/map):

映射(Map):将文件或匿名空间装入地址空间

mmap(memory map)的核心功能是“映射”。它可以在进程的虚拟地址空间中创建一个新的映射区域。这个区域的内容可以来自两部分:

  • 文件映射:将一个磁盘文件映射到内存。之后,进程就可以通过读写这段内存来间接操作文件,无需再调用 read 和 write,效率更高。

  • 匿名映射:不涉及具体文件,映射的区域内容被初始化为0。这通常用于分配大块内存或在有亲缘关系的进程间共享数据。

共享(Share):决定映射的更新策略

mmap 通过 flags 参数决定了这个映射区域的行为,其中最核心的就是“share”:

  • 共享映射 (MAP_SHARED):对内存区域的修改会写回到底层文件(如果是文件映射),并且对其他也以共享方式映射了同一区域的进程可见。这就像所有进程都在看同一块白板,谁写了大家都能看到。

  • 私有映射 (MAP_PRIVATE):对内存区域的修改不会写回文件,也对其他进程不可见。这种机制采用“写时拷贝”(Copy-on-Write)技术:在写入数据时,内核会为当前进程创建一个私有的副本,原文件内容保持不变。这就像每个人拿到了一份文件的复印件,自己在复印件上涂改不影响原件。

5)栈(stack):

在32位操作系统中,空间大小为8M,包括函数调用后的返回地址和局部变量、函数的参数

三、进程的状态

进程的生命周期——从无到有再到无

  • 生命周期:描述进程从创建到消亡的完整过程(宏观视角)

  • 状态:描述进程在某个具体时刻的行为特征(微观视角)

1.进程的五个基本状态:

新建、就绪、执行、阻塞、终止

2.Linux内核中进程状态的具体转换关系

3.ps命令中进程状态码的官方说明

四、进程的相关命令

1.top

动态查看进程的信息:pid号、进程的状态信息、进程名等等

2.ps aux | grep a.out

查看进程快照,可以查看进程的pid号和状态信息。(a.out为可执行文件名,根据实际情况替换)

3.kill

发信号

1) kill -l :查看所有信号

2)kil -命令编号 进程pid号 :向指定进程执行指定指令

18——SIGCONT (signal continue,继续进程)

19——SIGSTOP (signal stop,暂停进程)

9——杀死进程

4.killall

是 Linux/Unix 系统中用于按进程名发送信号的命令。它可以根据进程名称(而不是进程ID)向多个进程发送信号

5.pstree -sp pid号

显示指定进程的完整进程树路径,包括所有父进程

6.ps -eLf | grep a.out

查找名为 a.out 的进程及其所有线程的详细信息。

五、进程的使用

  • 创建好进程之后,做与父进程类似的事情
  • 创建好进程之后,做与父进程不同的事情

1.1进程的创建——fork()

 (1)格式:
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

1)功能:创建进程(通过复制 调用进程(父进程)创建新进程(子进程),子进程从fork之后继续执行,与父进程并发。

2)参数:void

3)返回值:

                    成功:父进程 返回 子进程的pid,子进程返回0

                    失败:返回-1

补充:pid 的最小值为-1

//判断父进程
if (pid > 0)
{ }
//判断子进程
if (pid == 0)
{  }
(2)注意:
  • fork之后父进程和子进程的运行顺序是不确定的,最终顺序取决于操作系统的调度
  • fork之后,父子进程各自拥有自己独立的4G进程空间。因为父子进程的空间独立,所以数据之间没有相互影响,进程安全性可靠性高
  • fork成功后,父子进程都是从fork的下一句开始执行的

(3)练习

1)一次fork生成几个进程?她们之间有什么关系?

一次 fork() 调用会生成 2 个进程:原有的父进程和新创建的子进程

2)如果两次fork同时前后执行,会生成几个进程?

会生成3个新进程:两个子进程,一个孙进程。加上调用进程就有四个进程

3)fork()&&fork()||fork() 总共几个进程?

总共5个进程

4)从键盘输入一个n,要求创建n个子进程

5)创建四个子进程,假设四个子进程分别复制无人机的 do_fiy,do_video,do_transmit,do_store四种工作模式

6)通过fork创建一个子进程,分别尝试杀死父/子进程,观察子/父进程的状态

1.2进程的创建——exec族

exec 族是一组用于在当前进程中执行一个新程序的函数。调用 exec 后,当前进程的代码段、数据段、堆栈等会被新程序的映像替换,但进程的 PID 保持不变。新程序从它的 main 函数开始执行。exec 族函数通常与 fork 配合使用:先 fork 创建一个子进程,然后在子进程中调用 exec 加载并运行另一个程序。

常见的 exec 族函数(均声明在 <unistd.h>):

函数名 参数形式 是否使用 PATH 环境来源
execl 路径 + 可变参数列表(以 NULL 结尾) 继承当前环境
execlp 文件名 + 可变参数列表(以 NULL 结尾) 继承当前环境
execle 路径 + 可变参数列表 + 环境变量数组 由参数指定环境
execv 路径 + 参数数组 继承当前环境
execvp 文件名 + 参数数组 继承当前环境
execvpe 文件名 + 参数数组 + 环境变量数组 由参数指定环境(非 POSIX 标准,但常见)

1.3fork()和exec族的区别

比较维度 fork exec 族
功能 创建一个新的进程(子进程),复制父进程的地址空间。 当前进程内加载并执行一个新程序,替换原有映像。
进程映像 子进程获得父进程的副本(代码、数据、堆栈等)。 当前进程的映像被新程序完全替换。
返回值 父进程返回子进程 PID,子进程返回 0;失败返回 -1。 成功不返回;失败返回 -1。
进程 ID 子进程获得新的 PID。 进程 PID 保持不变。
执行流 父子进程从 fork 调用后的下一条语句继续并发执行。 新程序从 main 开始执行,原程序的代码不再存在。
典型用途 创建新进程以运行不同任务(常与 exec 配合)。 替换当前进程为另一个程序(如 shell 执行外部命令)。
组合关系 可单独使用,也可与 exec 结合(先 fork 再 exec 运行新程序)。 通常与 fork 结合:在子进程中调用 exec 加载新程序。

总结

  • fork 负责复制进程exec 负责变换程序

2.进程的运行

1)pid_t getpid(void)

功能:获得调用该函数进程的pid

参数:void

返回值:进程的pid

2)pid_t getppid(void)

功能:获得该函数进程的父进程的pid号

参数:void
返回值:返回父进程的pid号

3.进程退出和结束

正常退出的情况:

1)从main中退出

2)exit()   //结束调用进程

3)_exit()        

异常退出的情况:

1)发信号结束进程

2)abort  //中止

void exit(int status);

格式:

#include <stdlib.h>
void exit(int status);

功能:使进程正常结束

参数:status:是进程的退出状态码,通常 0 表示成功,非0表示错误(但具体含义由程序定义)

注意:在库函数中exit会先清理缓存再调用退出处理函数

void _exit(int status);

格式:

​
#include <stdlib.h>
void _exit(int status);

​

功能:立即终止进程

参数:status:是进程的退出状态码,通常 0 表示成功,非0表示错误(但具体含义由程序定义)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, const char *argv[])
{
	printf("processing...\n");
//	exit(0);

/*_exit不会清理缓存区
 * 除非flush或者打印内容中出现换行符*/
    _exit(0);

	return 0;
}

特性 exit() _exit()
类型 C 标准库函数 系统调用(unistd.h
执行流程 ① 调用 atexit()/on_exit() 注册的函数
② 刷新所有标准 I/O 缓冲区
③ 关闭流
④ 调用 _exit() 进入内核
直接陷入内核,立即终止进程
缓冲区处理 刷新所有标准 I/O 缓冲区,确保数据写出 不刷新缓冲区,缓冲区数据可能丢失
清理函数 执行用户注册的退出处理函数 不执行任何用户清理函数
使用场景 正常终止程序,需要完整输出和清理 子进程(尤其 exec 失败时)、信号处理函数中(保证异步信号安全)
最终归宿 最终调用 _exit() 自身就是终止系统调用

atexit(void(*function)(void));

格式:

#include <stdlib.h>
int atexit (void(*function))(void);

功能:注册 退出清理函数

参数:function:函数指针(函数名字),指向退出清理函数

返回值:成功返回0,失败返回非0值

注意:清理函数的注册顺序和调用顺序相反;先注册后调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void my_cleanup()
{
	printf("cleanup function called, pid = %d\n",getpid());
}

int main(int argc, const char *argv[])
{
	atexit(my_cleanup);   // 注册退出处理函数

	pid_t pid = fork();
	if (pid < 0)
	{
		perror("fork");
		return 1;
	}
	if (pid > 0)
	{
		while (1)
		{
			printf("father pid = %d\n",getpid());
			exit(0);
		}
	}else if (pid == 0)
	{
		while (1)
		{
			printf("child pid = %d\n",getpid());
			exit(0);
		}
	}

	return 0;
}

4.资源回收

pid_t wait(int *status)

格式:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

功能:阻塞等待任意子进程退出,并回收该进程的退出状态,一般用于父进程回收子进程状态

参数:status:进程退出时候的状态,如果不关心其状态一般用NULL表示,如果要回收进程退出状态,则用WEXITSTATUS回收

返回值:成功返回子进程pid,失败返回-1

注意:如果调用时已有子进程终止(僵尸),wait立即返回并回收;wait只能等待任意一个进程结束,无法指定等待某个特定的子进程;若无子进程终止,则一直等待

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, const char *argv[])
{
	pid_t pid = fork();
	if (pid < 0)
	{
		perror("fork");
		return 1;
	}
	if (pid > 0)
	{
		int status; //接收进程退出状态值
		pid_t child_pid = wait(&status);//wait函数成功时返回回收的子进程pid
		printf("parent:child %d terminated\n",child_pid);
		if (WIFEXITED(status))//判断子进程是否正常退出
		{
			printf("normal exit with status : %d\n",WEXITSTATUS(status));//获取正常退出的状态码
		}
		if (WIFSIGNALED(status))//判断子进程是否因为信号退出
		{
			printf("kill by signal : %d\n",WTERMSIG(status));//获取导致终止的信号编号
		}
	}else
	{
		printf("Child: PID=%d, exiting...\n",getpid());
		exit(42);//子进程正常退出,状态码为42
	}
	
	return 0;
}

pid_t waitpid(int *status)

格式:

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

功能:等待指定的子进程状态发生变化(终止、暂停或恢复),并回收该进程的资源。相比 waitwaitpid 提供了更灵活的控制能力。

参数:

  • pid指定要等待的子进程:

    • pid > 0:等待进程 ID 等于 pid 的子进程。

    • pid = -1:等待任意子进程(等同于 wait)。

    • pid = 0:等待与调用进程同进程组的任意子进程。

  • status与 wait 相同,用于接收子进程的退出状态。若不关心可设为 NULL

  • options:控制等待行为的选项,常用值:

    • 0:阻塞等待,直到指定子进程状态改变。

    • WNOHANG:非阻塞模式,若没有子进程状态改变则立即返回 0。

    • WUNTRACED:也返回因信号而停止的子进程状态。

返回值:成功:返回状态发生变化的子进程 PID。失败:返回 -1,并设置 errno(如指定 PID 不存在或不是调用者的子进程,或调用被信号中断)

注意:waitpid 可以等待特定子进程;通过 options 可实现非阻塞等待,避免进程挂起;与 wait 一样,回收子进程资源可防止僵尸进程产生。解析 status 的宏与 wait 完全相同

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, const char *argv[])
{
	pid_t pid = fork();
	if (pid < 0)
	{
		perror("fork");
		return 1;
	}
	if (pid > 0)
	{
		//// 父进程:使用 WNOHANG 非阻塞等待子进程,每秒检查一次
		int status; //接收进程退出状态值
		while (1)
		{// 循环轮询子进程状态
			pid_t ret = waitpid(pid,&status,WNOHANG);
			if (ret == -1)
			{
				perror("waitpid");
				break;
			}
			if (ret == 0)//非阻塞模式,若没有子进程状态改变则立即返回 0
			{
				printf("child still running...\n");
				sleep(1);//轮询间隔
			}else//指定的进程状态发生变化,返回该子进程 PID,并回收该进程资源
			{
				printf("child terminated!\n");
				break;
			}
		}

	}
	if (pid == 0)
	{
		//子进程睡眠2秒后退出
		sleep(2);
		exit(0);
	}

	return 0;
}
特性 wait() waitpid()
函数原型 pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
等待的子进程 任意一个子进程(等价于 waitpid(-1, status, 0) 可根据 pid 参数精确控制:
• pid > 0:等待指定 PID 的子进程
• pid = -1:等待任意子进程(同 wait
• pid = 0:等待同进程组的任意子进程
• pid < -1:等待指定进程组(|pid|)的任意子进程
阻塞行为 始终阻塞直到有子进程状态改变 可通过 options 中的 WNOHANG 实现非阻塞(立即返回 0 表示尚无状态改变)
支持的等待事件 仅等待子进程终止 除终止外,还可通过选项等待:
• WUNTRACED:暂停的子进程
• WCONTINUED:恢复执行的子进程(需 Linux 2.6.10+)
灵活性 简单但有限 非常灵活,可满足复杂场景需求
错误处理 无子进程时返回 -1,errno=ECHILD 同左,但若指定 PID 不存在或不是调用者的子进程,也会返回 -1 并设置 ECHILD
典型应用场景 简单的同步等待子进程退出 需要控制特定子进程、非阻塞轮询、作业控制(如 shell 任务管理)

核心关系wait(&status) 完全等价于 waitpid(-1, &status, 0)

退出状态宏(用于解析 status)

宏解析需要两个宏搭配使用,通常需要先判断条件宏,再提取具体信息,不能单独使用提取宏。

用途
WIFEXITED(status) 子进程是否正常退出(调用 exit 或 _exit 或从 main 返回)。
WEXITSTATUS(status) 获取正常退出的退出码(低 8 位)。需配合 WIFEXITED 使用。
WIFSIGNALED(status) 子进程是否因信号而终止。
WTERMSIG(status) 获取导致终止的信号编号。需配合 WIFSIGNALED 使用。
WCOREDUMP(status) 是否产生了 core 文件(非标准,但常用)。
WIFSTOPPED(status) 子进程是否处于停止状态(仅当使用 WUNTRACED 时)。
WSTOPSIG(status) 获取导致停止的信号编号。
WIFCONTINUED(status) 子进程是否因 SIGCONT 恢复(需 WCONTINUED 选项)。
if (WIFEXITED(status)) 
{
    // 子进程正常退出,获取退出码
    int exit_code = WEXITSTATUS(status);
} else if (WIFSIGNALED(status))
 {
    // 子进程被信号终止,获取信号编号
    int sig_num = WTERMSIG(status);
} else if (WIFSTOPPED(status)) 
{
    // 子进程已停止(作业控制),获取停止信号
    int stop_sig = WSTOPSIG(status);
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐