【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;
    }
}

总结

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


相关推荐
A小辣椒17 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒21 小时前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式