Linux进程控制怎么学?四个核心API详解

 更新时间:2026年09月30日 15:16:13   作者:ephemerals__  
想搞懂Linux进程控制?这篇文章用大白话讲解进程创建、终止、等待和程序替换,带你上手fork、exit、waitpid和exec系列函数,轻松管理进程生命周期,避免僵尸进程,让你的程序更稳定高效

在现代计算中,操作系统负责管理硬件资源并为应用程序提供一个稳定的运行环境。Linux作为一种广泛使用的类Unix操作系统,它的进程管理功能至关重要。

Linux提供了强大的进程控制机制和接口,帮助我们管理进程的生命周期、资源分配和调度。在这篇博客中,我们将深入探讨Linux进程控制的基础知识以及从四个方面(进程创建、进程终止、进程等待、进程程序替换)介绍常用的进程控制相关API接口,帮助大家更好地理解和掌握Linux系统中的进程控制技术。

一、什么是进程控制

进程控制是操作系统中管理和调度进程的关键功能,涉及到进程的创建、终止、等待、程序替换以及进程间通信等多个方面,简而言之,进程控制确保操作系统能够有效地管理并协调不同程序的执行。

对于我们而言,要对进程进行相关操作,就要深入理解操作系统对进程的控制机制,并熟悉Linux下进程控制的相关接口。

二、进程创建

在Linux下,我们通常使用fork函数完成进程的创建。创建后的新进程称之为子进程,而当前进程叫做父进程。

博主已经在以下两篇文章中,对进程创建的底层原理、fork的使用方法、注意事项以及写时拷贝的原理进行了详细的讲解,大家参阅这两篇文章即可,这里就不再复述。

三、进程终止(进程退出)

一个进程在做完相应工作后,需要执行“终止”操作,其本质是释放进程的task_struct及其对应的代码和数据。

进程终止的常见情况有四种:

1.main函数返回

2. 调用_exit()退出

3.调用exit()退出

4. 给进程发送信号(如kill()、ctrl + c)

前三种情况都用于当前正在执行的进程退出,属于进程的正常退出,而最后一种专门用于杀死其他进程,属于异常退出。

本文博主会详细介绍前三种退出方法的使用及其原理,至于最后一种,博主后续会结合信号一起讲解。

退出码

进程退出时,都会返回一个“退出码”,它本质是一个整数,用于表示程序的执行情况。

例如,我们在VSCode终端输入指令时,可以看出它是否执行成功,本质就是通过识别对应程序的退出码来完成的:

不同的退出码,所表示的退出状态也不尽相同。只有退出码为0时,表示成功执行,且执行的结果正确,其他退出码均表示执行失败或结果不符合预期,原因各异。

以下是常见的退出码所表示的执行情况:

注意:

要将“程序成功执行”和“程序执行的结果正确”区分开,退出码非0并不代表程序一定是异常退出的,也有可能成功执行,但结果不符合预期。而异常退出是说程序执行时遇到异常,直接终止了,没有符不符合预期的概念了。

如下指令可以打印最近执行结束的程序的退出码,便于我们判断退出状况:

echo $?

main函数返回

main函数return返回是最常见的进程退出方法,再程序执行完毕后,使用return返回即可。main函数的返回值即是程序的退出码。

_exit()

_exit() 是一个系统调用,当程序执行到调用处时,会直接退出。

它的函数原型如下:

#include <unistd.h>

void _exit(int status);

参数status表示程序的退出码,当出现错误,我们可以使用该接口配合退出码退出程序。

_exit是一种低级的退出操作,退出时不会进行任何其他处理,只会返回退出码,所以一般是用于多进程编程时的子进程错误退出。

注:传入的退出码虽然类型为int,但退出码的取值范围是0~255,所以只会取int的最低8位。

exit()

exit() 是c标准库的函数,包含在<stdlib.h>头文件中,相比_exit更加常用。

其函数原型:

#include <stdlib.h>

void exit(int status);

参数status也表示程序退出码。

实际上,exit的底层调用了_exit,不过在退出之前做了一系列清理工作,例如刷新I/O 缓冲区,确保所有的输出数据都被写入到相应的文件或终端、关闭文件描述符、间接释放动态分配的内存等。

测试

接下来我们做一段测试,写一个简单的程序,调用exit退出,并将50作为退出码,然后在命令行打印退出码:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world\n");
    exit(50);
}

执行程序并打印退出码:

可以看到,程序打印后退出,并且返回了退出码50。

四、进程等待

在之前的文章中提到,一个子进程在退出后,需要父进程回收资源,如果父进程一直不回收,那么子进程的PCB就会一直存在,导致出现僵尸进程。另外进程退出时返回的退出码需要被父进程获取,才能判断子进程的执行情况。

因此,“进程等待”的作用是:

1. 回收子进程,释放子进程的资源(必须完成)

2. 获取子进程的退出信息(可选)

在Linux下,我们通常使用wait和waitpid进行进程等待。

wait

wait是一个函数,在父进程中使用,作用是等待子进程退出后,回收子进程资源,并获取子进程的退出码与其他退出状态。

它的函数原型如下:

#include <sys/wait.h>

pid_t wait(int *status);

参数status:是一个输出型参数,表示子进程的退出信息。具体细节在waitpid之后统一介绍。

返回值:如果回收成功,返回子进程的PID。如果失败,返回 -1。

如果父进程有多个子进程,那么wait就会等待任意一个子进程。

注意:

调用wait等待子进程时,如果此时子进程还在执行自己的程序,并未退出,那么父进程就会一直阻塞在wait的调用处,直到子进程退出,再执行后续代码。

waitpid

相比wait,函数waitpid的功能更加丰富,因此也更加常用。

它的函数原型如下:

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *stat_loc, int options);

参数pid:如果传入-1,会等待任意子进程(有多个子进程的情况下);如果传入一个大于0的数,表示专门等待子进程PID为pid的子进程。

参数stat_loc:输出型参数,表示子进程的退出信息。具体细节稍后介绍。

参数options:通常传入0,表示阻塞等待(即子进程退出之前阻塞)。如果传入宏WNOHANG,则表示:若子进程还未退出,不等待,直接返回0(可以配合循环进行多次的非阻塞轮询,非阻塞轮询期间的空闲时间可以被利用,让父进程完成自己的任务)。

返回值:如果回收成功,返回子进程的PID。如果失败(例如没有对应的pid),返回 -1。如果第三个参数传WNOHANG并且子进程还未退出,返回0。

注意事项

1. 调用wait/waitpid时,如果子进程已经退出而没有被回收,那么函数会立即返回,释放子进程资源,获取退出信息。

2. 调用wait/waitpid时,如果子进程已经被回收,那么就会等待失败,立即返回 -1。

3.如果父进程不调用wait或waitpid,子进程在退出后会变成僵尸进程。

wait和waitpid的输出型参数

wait和waitpid都有一个输出型参数,都用于表示子进程的退出信息。这个信息并不只是单纯的退出码,其中还包含了收到信号导致退出时的信号编号、退出方式等信息。当然,这些东西具体是什么,我们无需关心,后续在讲解信号时博主会进行分析。现在我们终点关注的是:输出型参数只是一个整数,却包含了多种信息,那势必要对其进行划分,我们了解了划分方式,才能从其中提取出退出信息。

它的划分方式如下:

1. 如果子进程正常退出,那么输出型参数的0~7位全为0,8~15位是退出码;

2. 如果子进程异常退出,那么输出型参数0~6位表示信号编号,第7位表示异常退出方式,8~15位的数据没有使用价值。

获取到输出型参数后,我们可以进行相应的位运算处理,得到退出码或者其他信息。当然,有几个宏可以帮助我们直接求出退出码等信息(传入输出型参数的值即可):

  • WIFEXITED() 用于判断子进程是否是正常退出。
  • WEXITSTATUS() 可以求出退出码。
  • WTERMSIG() 可以求出异常退出时的信号编号。

当然,如果我们不关心子进程的退出状况,可以在输出型参数位置传NULL。

父进程回收子进程,并获取子进程退出信息的过程:子进程退出时,其退出信息会存入其PCB中,当被父进程回收时,在子进程PCB中读取,然后释放子进程的PCB。

五、进程程序替换

进程程序替换指的是一个进程在运行过程中,用新的程序代码替换当前执行的代码,然后执行新程序的代码。如果我们创建了一个进程,想要这个进程执行一个全新的程序,就可以使用进程程序替换。

发生进程替换后,PCB的内容不变,也没有创建新的进程,而是当前进程对应物理内存中的代码、数据、堆区栈区全部被目标代码和数据覆盖。

进程替换相关接口

Linux下,实现进程替换最常用的是exec系列函数,一共有六个:

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

这六个函数都是用于替换当前程序的代码和数据,只是传参方式有所不同,功能略有差别。

首先介绍一下它们的返回值:

  • 如果替换成功,直接执行新代码,后续的原代码已经被替换,不再执行,因此没有返回值。
  • 如果替换失败,返回 -1。

所以对于exec系列函数,无需检查其返回值,因为只要返回就失败。

再来谈谈它们的参数:

1. 上述函数中,叫做path的参数,一律传入的是一个字符串,表示目标程序的路径+程序名。

2. 叫做file的参数,一律传入目标程序的程序名(路径会自动到环境变量PATH中查找)。

3. 叫做arg的参数,是一个可变参数,一个一个地传入目标程序的命令行参数字符串(注意程序名算第一个命令行参数;最后一个命令行参数须是NULL)。当然,如果不需要使用其他的命令行参数,只需传一个程序名和一个NULL即可。

4. 叫做argv[]的参数,将指向所有命令行参数字符串的指针数组的数组名传入(注意程序名算第一个命令行参数;指针数组的最后一个元素须是NULL),也就是传命令行参数的另一种方式。

5. 带有参数envp[]的函数,表明替换的进程要使用该函数传入的全新的环境变量。因此,该参数处要传入指向所有环境变量字符串的指针数组的数组名(注意环境变量格式是"环境变量=值";指针数组的最后一个元素须是NULL)。

以上七个函数的名字十分相似,容易混淆,但其实它们也有规律,博主给大家列出来方便记忆:

  • 1. 函数名带"l"的,表示命令行参数要一个个地传入;而带"v"的表示命令行参数要用指针数组的方式传入。
  • 2. 函数名带"p"的表示目标程序不用带路径,会自动在PATH中查找。
  • 3. 函数名带"e"的表示可以给目标程序设置新的环境变量。

当然,除了这六个函数,还有一个系统调用execve,使用方法和execvpe完全相同。原型如下:

#include <unistd.h>

int execve(const char *filename, char *const argv[], char *const envp[]);

写一个简单的调用示例,看看这些函数的调用区别:

#include <unistd.h>

int main()
{
    //假如要将程序替换为/usr/bin路径下的ls指令,并加上选项-l
    //并且对于支持修改环境变量的函数,修改环境变量PATH
    
    char* const argv[] = {"ls", "-l", NULL};
    char* const envp[] = {"PATH=/usr/bin", NULL};

    execl("/usr/bin/ls", "ls", "-l", NULL); // 带l要一个个传入命令行参数

    execlp("ls", "ls", "-l", "NULL"); // 带p不用传路径

    execle("usr/bin/ls", "ls", "-l", NULL, envp); // 带e要传环境变量表

    execv("/usr/bin/ls", argv); // 带v要用指针数组传入命令行参数

    execvp("ls", argv); // 带p不用传路径;带v用指针数组传命令行参数

    execvpe("ls", argv, envp); // 带p不用传路径;带v用指针数组传命令行参数;带e要传环境变量表

    execve("ls", argv, envp);
    return 0;
}

实际上,exec系列接口中,六个标准库函数底层都封装了系统调用execve,只是产生了多种形式使得传参和功能更加多样化。

总结

本篇文章,我们学习了进程控制的概念及进程创建、进程终止、进程等待和进程程序替换的相关Linux常用接口,学习了这些接口,想必大家能够更好地理解和掌握Linux系统中的进程控制技术。

当然,进程控制还包括进程间通信,但这部分的知识涉及文件IO相关的内容,后续博主会给大家详细讲解。接下来博主会和大家一起,结合这些接口的使用方法及进程控制思想,写一个简单的Shell命令行程序。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Linux下忘记mysql密码该如何修改mysql密码

    Linux下忘记mysql密码该如何修改mysql密码

    这篇文章主要介绍了Linux下忘记mysql密码该如何修改mysql密码的相关资料,需要的朋友可以参考下
    2016-02-02
  • 浅谈Linux环境变量文件介绍

    浅谈Linux环境变量文件介绍

    这篇文章主要介绍了浅谈Linux环境变量文件介绍,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04
  • ubuntu 下JDK环境变量配置方法

    ubuntu 下JDK环境变量配置方法

    这篇文章主要介绍了ubuntu 下JDK环境变量配置方法,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2018-03-03
  • Django在Ubuntu14.04的部署方法

    Django在Ubuntu14.04的部署方法

    下面小编就为大家带来一篇Django在Ubuntu14.04的部署方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • 详解Linux CPU负载和CPU使用率

    详解Linux CPU负载和CPU使用率

    在本篇文章里小编给各位分享了关于Linux CPU负载和CPU使用率的相关知识点内容,有需要的朋友们参考下。
    2019-07-07
  • Linux下配置IP的三种实现方式

    Linux下配置IP的三种实现方式

    本文介绍了Linux系统中配置IP的三种方法:使用nmcli命令、ip命令以及通过ifcfg文件,nmcli命令是NetworkManager的命令行工具,配置后不会丢失;ip命令配置后重启系统会丢失;ifcfg文件配置需手动激活
    2024-11-11
  • Linux运维之如何使用ss命令代替netstat

    Linux运维之如何使用ss命令代替netstat

    这篇文章主要给大家介绍了关于Linux运维之如何使用ss命令代替netstat的相关资料,需要的朋友可以参考下
    2019-07-07
  • Centos7下PHP安装gd库的实例讲解

    Centos7下PHP安装gd库的实例讲解

    下面小编就为大家分享一篇Centos7下PHP安装gd库的实例讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-03-03
  • Centos中TCPWrappers访问控制实现

    Centos中TCPWrappers访问控制实现

    这篇文章主要介绍了Centos中TCPWrappers访问控制实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • 一文教你如何监控Kafka Topic的生产者客户端

    一文教你如何监控Kafka Topic的生产者客户端

    Apache Kafka 是现代分布式系统中广泛使用的消息队列和流处理平台,本文将详细介绍如何通过命令行工具、JMX 监控、日志分析等方法,全面掌握 Kafka Topic 的生产者信息,需要的可以参考下
    2025-04-04

最新评论