【Linux】进程控制(上)

🔥铅笔小新z:个人主页

🎬博客专栏:Linux学习

💫滴水不绝,可穿石;步履不休,能至渊。


一、进程终止

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

1.1 进程退出的场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

main()函数的返回值,通常代表程序的执行情况:

  1. 结果正确返回 0
  2. 不正确返回 非0,即 1、2、3、4、......,不同值代表不同的错误信息

在这段代码里,我们可以从打印信息中得知我们的proc程序正常运行了。

1.1.1 echo $?

但是如果将打印信息去掉,我们该如何得知程序是否正常运行呢?

那么我们就要学习一个新指令:echo $?(打印最近一个程序或进程退出时的退出码)

这个退出码也要写到当前进程的 task_struct 结构体中。

1.1.2 所有的错误信息表示

在上面的代码中,我们可以打印出所有的退出码对应的错误信息。

举例子:

可以明确当前目录中没有 hello.txt 这个文本文件,所以打印出 No such file or direcotry,对应着数字 2这个错误信息。

一个程序的执行结果由进程的退出码决定。

1.1.3 退出码无意义

c 复制代码
#include<stdio.h> 
int main()
{
	int a = 10;
	a /= 0;
	return 89;                                                                                                                                                       
}

当我们运行上面的代码,再打印退出码会出现什么?

退出码是 136 ,而我们上面打印返回值信息的时候最多就只有134个,为什么会出现136呢?

因为异常的程序退出码无意义!

1.2 程序常见退出方法

  1. main()函数返回
  2. 调用exit()
  3. _exit

1.2.1 调用exit()

任何地方调用exit()表示程序直接结束,不返回。并返回给父进程子进程的退出码。

c 复制代码
#include<stdio.h>
#include<stdlib.h>

void func()
{
	printf("func begin!\n");
	exit(43);
	printf("func end!\n");
}

int main()
{
	func();
	printf("main end!\n");
	return 0;                                                                                                                                                          
}

程序运行结果:

退出码打印:

从运行结果和退出码两个信息中我们可以知道,程序执行到func()函数中的exit()语句就退出了,没有返回值。

1.2.2 调用_exit()

_exit()的作用是谁调用它就终止谁。

c 复制代码
#include<stdio.h>
#include<unistd.h>

void func()
{
	printf("func begin!\n");
	_exit(4);
	printf("func end!\n");
}

int main()
{
	func();
	printf("main end!\n");
	return 0;                                                                                                                                                          
}

运行结果及退出码:

1.2.3 exit()_exit()的区别

在讲区别之前要了解一个关于缓冲区的知识:

当我们用 printf("hello linux\n")时,hello linux是在缓冲区中的,\n就是将缓冲区中的内容刷新到显示器上,如果不刷新缓冲区,我们就无法看到hello linux这个语句。

我们先来看四组例子:

c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> 
int main()
{
     printf("hello linux!\n");
     sleep(2);
     exit(10);
     return 0;                                                                                                                                                          
}                 

这段代码的运行结果是:

\n刷新缓冲区,所以我们是先看到打印信息然后等待两秒后进程结束。

c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> 
int main()
{
     printf("hello linux!");
     sleep(2);
     exit(10);
     return 0;                                                                                                                                                          
}                 

这段代码的运行结果是:

因为没有\n刷新缓冲区,所以是等待两秒后,打印信息才出现。

c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> 
int main()
{
     printf("hello linux!\n");
     sleep(2);
     _exit(10);
     return 0;                                                                                                                                                          
}                 

这段代码运行结果是:

具体情况同第一组。

c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> 
int main()
{
     printf("hello linux!");
     sleep(2);
     _exit(10);
     return 0;                                                                                                                                                          
}                 

这段代码运行结果是:

为什么跟第二组的结果不一样呢,为什么没有\n刷新缓冲区还没有出现打印信息呢?

这就是exit()_exit()的区别所在:

  1. exit()是库函数,进程调用exit()时,进程退出时会进行缓冲区的回收
  2. _exit()是系统调用,进程调用_exit()时,进程退出时不会进行缓冲区的回收

库函数和系统调用之间的关系是上下级的关系,库函数会调用相关的系统调用来完成对应任务。只有操作系统才有资格终止进程,库函数是没有资格的,所以exit()在底层也要调用_exit()来终止进程。

我们之前谈的缓冲区一定不是操作系统内部的缓冲区,是库缓冲区(C语言提供的缓冲区)。


二、进程等待

2.1 进程等待的必要性

  • 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存
    泄漏。
  • 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的 kill -9也⽆能为⼒,因为谁也
    没有办法杀死⼀个已经死去的进程。
  • 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
    不对,或者是否正常退出。
  • ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息

2.2 进程等待的方法

2.2.1 wait()

简单的说,wait() 函数不仅是为了"收尸"(防止僵尸进程),更是为了让父进程拿到子进程的"遗言"(退出状态)。

wait()返回的是目标僵尸进程的pid

参数传什么?

  • 如果你不关心子进程是怎么死的: 直接传 NULL
    • pid_t rid = wait(NULL);
  • 如果你想知道子进程的退出细节: 传一个 int 变量的地址。
    • int status;
    • pid_t rid = wait(&status);
2.2.1.1 创建一个僵尸进程
c 复制代码
#include<stdio.h>
#include<stdlib.h> 
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("我是一个子进程:pid : %d, ppid : %d\n", getpid(), getppid());                                                                     
            cnt--;
            sleep(1);
        }
        exit(0); 
    }
    
    sleep(100);
    return 0;
}
  • 子进程在 5 秒后执行 exit(0) 变成了尸体。

  • 此时父进程还卡在 sleep(100) 里,根本没有调用 wait() 帮子进程收尸。

所以在上面图片中第五秒时,子进程的状态变成了Z(僵尸进程)。

2.2.1.1 利用wait()解决僵尸进程
c 复制代码
#include<stdio.h>
#include<stdlib.h> 
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("我是一个子进程:pid : %d, ppid : %d\n", getpid(), getppid());                                                                     
            cnt--;
            sleep(1);
        }
        exit(0); 
    }
    pid_t rid = wait(NULL);
    if (rid > 0)
    {
    		printf("wait success, rid : %d\n",rid); 
    }
    sleep(10);
    return 0;
}

我们来详细解释代码逻辑:

  1. 0 - 5秒:

    • **子进程:**在跑 while 循环,每秒打印一次。

    • 父进程: 卡在 wait() 这一行。它像是在车站等人的接机者,子进程不出来(不退出),父进程就哪里也不去,一直等在那里。

  2. 第 5 秒左右:

    • 子进程执行完循环,调用 exit(0)

    • 子进程瞬间变成僵尸状态(Z)

    • 操作系统内核监测到子进程退出了,立刻"捅"了一下正在阻塞等待的父进程,并将子进程的退出信息交给父进程。

  3. 回收瞬间:

    • 父进程的 wait() 拿到了子进程的 PID,并从阻塞状态醒来。

    • 一旦 wait() 返回,子进程的残留信息(PCB)被彻底释放,僵尸状态消失。

  4. 5秒之后:

    • 父进程打印 wait success

    • 父进程开始执行它最后的 sleep(10)

2.2.2 waitpid()

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

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

相比于 wait 只有一个 status 参数,waitpid 多了首尾两个参数。我们一个个看:

2.2.2.1 参数

参数一: pid (你要等谁?)

  • pid > 0(最常用) 只等待进程 ID 等于这个 pid 的子进程。精准打击,别人死活我不管。

  • pid == -1(很常用) 等待任意一个子进程。此时它的行为和 wait 一模一样!实际上,Linux 底层里 wait(&status) 其实就是封装了 waitpid(-1, &status, 0)

  • 补充(稍冷门):pid == 0 等待同一个进程组的任意子进程;pid < -1 等待指定进程组的任意子进程。

参数二: status (拿到遗言)

完全和前面讲的 wait 一样,传入一个整型变量的地址,配合宏(如 WIFEXITED)来提取退出码或信号。

参数三: options (你要怎么等?)

  • 0阻塞等待(死等)。子进程不退出,父进程就卡在这里不走。

  • WNOHANG非阻塞等待(轮询) 。HANG 的意思是挂起(卡住),NO HANG 就是不卡住。如果子进程还没死,waitpid 会立刻返回 0,父进程可以继续往下执行自己的代码,干点别的事,过一会儿再回来问一句:"你死了没?"。

2.2.2.2 返回值 pid_t

结合 WNOHANG,waitpid 的返回值有三种极其重要的状态:

  1. 返回值 > 0 表示收集成功,返回的是死掉的子进程的 PID。

  2. 返回值 == 0 只有在 WNOHANG 模式下才会出现! 表示子进程还在运行,暂时还没死,告诉父进程你可以先去忙别的。

  3. 返回值 < 0 调用出错(比如你传的 pid 根本不是你的子进程,或者根本没有子进程了)。

2.2.2.3 深度解析status
c 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 #include<sys/wait.h>
  5 #include<stdlib.h>
  6 #include<errno.h>
  7 #include<string.h>
  8 int main()
  9 {
 10     pid_t id = fork();
 11     if (id == 0)
 12     {
 13         int cnt = 3;
 14         while (cnt)
 15         {
 16             printf("我是一个子进程:pid : %d, ppid : %d\n", getpid(), get    ppid());
 17             cnt--;
 18             sleep(1);
 19         }
 20         exit(1);
 21     }
 22     int status = 0;
 23     pid_t rid = waitpid(id, &status, 0);
 24     if (rid > 0)                                                         
 25     {
 26         printf("wait success, rid: %d, exit code: %d\n",rid, status);
 27     }
 28     else
 29     {
 30         printf("wait failed: %d: %s\n", errno, strerror(errno));
 31     }
 32     return 0;
 33 }

这段代码最后的退出码(exit code)是256,我们设置子进程退出码不是1吗,这是为什么?

我们来讲讲状态码 status 的底层结构。

虽然 status 是一个 32 位的 int,但通常只使用它的低 16 位(0-15位)。根据子进程是"正常退出"还是"被信号终止",这 16 位的排布意义会有所不同:

  1. 正常退出:
    如果子进程是通过 returnexit() 正常退出的:

    • 高 8 位 (Bits 8-15): 存储子进程的退出码 (Exit Code)(范围 0-255)。

    • 低 8 位 (Bits 0-7): 全部为 0

  2. 被信号终止:
    如果子进程是因为收到某个信号(如 SIGKILL, SIGSEGV)而被强行终止的:

    • 高 8 位 (Bits 8-15): 未使用。

    • 第 7 位 (Bit 7): Core Dump 标志 。如果生成了核心转储文件,该位为 1

    • 低 7 位 (Bits 0-6): 存储导致进程终止的信号编号 (Signal Number)(范围 1-127)。

如果想要得到具体的退出码的话,就要右移8位然后(&0xFF)。

此时我们设置的是exit(1)

此时我们设置的是exit(52)

那么低8位是什么呢,我们来看看Linux中的信号:

当进程异常退出时,低7个比特位保存的是异常时对应的信号编号。(低8位是core dump标志)

  1. 没有异常时,低7个比特位都是0。
  2. 一旦低7个比特位不是0,则是异常退出,退出码无意义。

获取进程退出时的退出信号:
status & 0x7F

通过宏提取信息:

在实际编程中,我们不应该手动进行位运算(如 status >> 8),因为不同 Unix 系统的位排布可能有细微差别。POSIX 标准定义了专门的宏(包含在 <sys/wait.h> 中)来处理这些位运算:

提取整行退出码:

  1. WIFEXITED(status)
    • 作用: 判断子进程是否正常退出。
    • 底层操作: 检查低 7 位是否为 0。如果是,说明没有信号干预,返回 true(非 0)。
  2. WEXITSTATUS(status)
    • 作用: 在确认为正常退出后,提取退出码。
    • 底层操作:status 右移 8 位,并与 0xFF 取交集((status >> 8) & 0xFF),提取出第 8-15 位的值。

提取信号退出码:

  1. WIFSIGNALED(status)
    • 作用: 判断子进程是否因为未捕获的信号而终止。
    • 底层操作: 检查低 7 位是否大于 0 且低 8 位不等于 0x7F(0x7F 是进程暂停的标志)。如果是,返回 true。
  2. WTERMSIG(status)
    • 作用: 在确认为信号终止后,提取信号编号。
    • 底层操作:status0x7F 取交集(status & 0x7F),屏蔽掉高位和第 7 位(core dump 位),只保留低 7 位。
2.2.2.4 阻塞与非阻塞等待

在 Linux 系统编程中,父进程创建子进程后,常常需要知道子进程什么时候结束、结果如何。waitpid 就是用来做这件事的。

它的第三个参数 options 决定了父进程在等待时的行为。最核心的区别就是:父进程是"死等"还是"抽空看一眼"。

2.2.2.4.1 阻塞等待 ------"不见不散"

阻塞等待是 waitpid 的默认行为。当你把第三个参数设置为 0 时,父进程就会进入阻塞状态。

  • 工作原理: 父进程调用 waitpid 后,如果指定的子进程还在运行,操作系统会将父进程挂起(从 CPU 的运行队列移出,放入等待队列),父进程进入睡眠状态。直到子进程退出并变成"僵尸进程(Zombie)",操作系统才会唤醒父进程去回收它。
  • 返回值:
    • > 0:成功,返回退出的子进程的 PID。
    • 0:表示子进程还在运行,尚未退出。
    • -1:出错(如查无此进程)。
  • 优点: 父进程不会被卡住,可以一边执行自身任务,一边兼顾子进程的状态,实现并发。
  • 缺点: 为了确保最终能回收子进程(防止产生孤儿 / 僵尸进程),通常需要结合 轮询 或者信号机制 来使用,代码复杂度较高。

典型代码范例(轮询方式):

c 复制代码
// 父进程不断地用 WNOHANG 去"问"子进程有没有死
while (1) 
{
    pid_t wait_ret = waitpid(child_pid, &status, WNOHANG);

    if (wait_ret == 0) 
    {
        printf("子进程还在跑,父进程先去干点别的事...\n");
        sleep(1); // 模拟父进程执行其他任务,避免 CPU 100% 空转
    } else if (wait_ret == child_pid) 
    {
        printf("子进程 %d 终于退出了,回收完毕!\n", wait_ret);
        break;    // 退出轮询
    } else 
    {
        perror("waitpid error");
        break;
    }
}

总结

进程控制(上)到这里就暂时结束了。


相关推荐
AI周红伟2 小时前
Hermes Agent 工具-周红伟
linux·网络·人工智能·腾讯云·openclaw
大卡片2 小时前
linux和IO常见面试题
linux·运维·服务器
zzzyyy5382 小时前
Linux程序地址空间
linux·运维·服务器
RisunJan2 小时前
Linux命令-newusers(用于批处理的方式一次创建多个命令)
linux·运维·服务器
嵌入式吴彦祖2 小时前
RKNN demo运行
linux
草莓熊Lotso2 小时前
Linux 线程深度剖析:线程 ID 本质、地址空间布局与 pthread 源码全解
android·linux·运维·服务器·数据库·c++
殇者知忧2 小时前
Tmux快速上手
linux·tmux
AcrelGHP2 小时前
安科瑞AIM-T系列工业IT绝缘监测及故障定位解决方案为关键供电场所筑牢安全防线
大数据·运维·数据库
草莓熊Lotso3 小时前
MySQL 从入门到实战:视图特性 + 用户权限管理全解
linux·运维·服务器·数据库·c++·mysql