Linux--进程控制

目录

一,引言

二,进程结束

1,写时拷贝

2,进程终止

三,进程等待

1,进程等待方法

2,阻塞等待和非阻塞等待

四,进程程序替换


一,引言

在前几篇的讲解中,已经初步了解了进程的相关概念,进程是什么,进程的相关操作,以及命令行参数,环境变量等等。本节主要讲解,从开始创建一个进程,到结束一个进程 ,以及前几篇出现僵尸 进程的解决办法--进程等待 ,和最后进程替换等内容。本节详细讲解以上内容。

二,进程结束

在讲解进程终止之前,稍微说一下进程创建。在Linux中提供fork()函数用来创建子进程。fork函数的头文件为<unistd.h>中。其中fork函数的返回类型为pid_t 。这个返回类型可以当作整形来理解。fork函数在创建成功的前提下有两种返回值,子进程返回0;父进程返回子进程的pid。在创建子进程之后会发生写时拷贝的概念:下面详细了解一下写时拷贝。

1,写时拷贝

当fork函数创建了子进程之后。就触发了写时拷贝。也就是子进程也会拷贝一份和父进程几乎一致的PCB,虚拟地址空间,页表等等如图:

当创建一个子进程之后,子进程会创建task_struct,以及虚拟地址,页表等等,此时完成的是实时拷贝的初始化阶段,也就是子进程并没有修改数据。

当子进程修改数据时,操作系统会发现原本的页表项为只读状态,此时就会触发写时拷贝的拷贝功能,将修改部分的页进行拷贝,如下黄色的区域,子进程页表指向新的物理物理内存页。

上图中,就是修改后的父子进程。两者task_struct独立,虚拟空间也是独立,在不修改时,物理内是共享的。一旦数据修改。物理空间就也是独立的。这进而表现出进程独立性的特性。

2,进程终止

在了解的父子进程的拷贝逻辑之后。通常来说一个程序会出现三种情况:

1,代码运行结束并且运行结果正确。

2,代码运行结束但是运行结果并不正确

3,代码没有正常运行结束

这时候如何得知一个代码运行的情况呢,就引出了进程的退出码。

退出码:可以告诉我们最后一次执行命令的状态。可以根据这个退出码来得知这个指令是正确结束,还是错误结束。常见的退出码如下:

在Linux操作系统中:使用命令 echo $?来查看最后一次执行命令的退出码。如图:

在一个进程结束时--也就是代码运行结束。子进程会返回子进程的退出码给父进程。通常来说这个这个退出码会写入在PCB中。在讲解僵尸进程时讲到。子进程退出并不会立即销毁。只有当父进程接收到子进程的退出信息之后。子进程的PCB才会回收。之后父进程会将退出码写入环境变量之中。这也就是为什么echo命令可以查看最近一个指令的退出码。

3,exit和_exit

上文中讲解到退出码的概念,除了系统给出的退出码。在自己写的程序中,比如当编写子进程代码时,遇到某种情况,就会引起进程退出等等。因此引入exit和_exit的概念。

cpp 复制代码
#include <unistd.h>
void exit(int status);
cpp 复制代码
#include <unistd.h>
void _exit(int status);

在Linux中有这两种结束进程并且返回退出码的函数。这两者的差异最后说明。首先先讲解一下函数用法:

exit 函数执行进程退出工作。当代码运行到这个函数时。该进程就会结束。其中输入的status参数就是退出码 该变量虽然是整形,但是父进程获取子进程的退出码时,只会截取该变量的前八个比特位。因此范围0,255 。例如:

cpp 复制代码
int main()
    7 {
    8   pid_t id = fork();
    9   if(id == 0)
   10   {
   11     //chlid
   12     printf("我是一个子进程\n");
   13     exit(10);
   14     printf("我的id是******\n");
   15   }
   16   int i = 0;
W> 17   pid_t add = wait(&i);
   18   printf("子进程的退出码%d",WEXITSTATUS(i));
   19   return 0;
   20 }          

运行结果如下:

此时会发现子进程的第二个printf并没有执行。表明exit结束该子进程。

上文中讲到,退出码要截取前八位bit位。WEXITSTATUS宏函数就是实现这个功能。


最后讲一下:_exit和exit的区别

exit本质来说是c语言提供的库函数,_exit是系统调用。它们两者是上下级的关系。也就是说exit内部封装了_exit。在功能上来说exit在执行时不仅会结束进程,在结束进程之前会清理所有的缓冲区,确保数据都被写入。_exit则不会清理缓冲区。

举个例子:C语言的\n不仅 有换行的作用还要清理缓冲区的作用。因此当代码没有\n时,_exit系统调用并不会讲数据打印出来。大家可以去尝试一下。

三,进程等待

在进程状态时讲到,有一种进程状态叫做僵尸进程,这种进程不仅仅会浪费空间,还会造成内存泄漏。自此,一个进程的结束,往往来说父进程来知道子进程的运行状态--是否正常的结束该进程。因此,引入进程等待的概念。

进程等待就是解决僵尸进程的问题(回收子进程) ,并且获取子进程所需要的信息。

1,进程等待方法

在进行进程等待方法讲解之前,先介绍一部分宏函数

1,WIFEXITED(status)--表示判断子进程是否正常退出

2,WEXITSTATUS(status)--表示若1中判断为真,也就是正常退出。这个结果为退出码

3,WIFSIGNALED(status)-- 表示判断子进程是否被信号终止

4,WTERMSIG(status)--表示若3为真,结果为信号编号

wait方法

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);

等待子进程成功返回子进程pid,失败返回-1 .下面还有一个方法,两者的status参数含义一致。

waitpid方法

cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);

waitpid和wait中status的参数功能是一致的。所有这里讲解waitpid。

第一个参数pid:可以指定回收某个进程,当这个pid为-1,就相当于wait()。当这个pid为0表示回收同组进程。当小于-1,则对该值取绝对值。

第二个参数status:这个为输出型参数,通过宏函数进行修改,来得出不同的结果。

第三个参数options:这个表示该方法时候阻塞等待。当该值为0时,阻塞等待,一直等待子进程结束。当该值为WNOHANG。这时是非阻塞等待,直接返回零。

2,阻塞等待和非阻塞等待

在上文中讲解了相关函数的方法,在这一节就实际的例子来了解一下这两者的区别:

阻塞等待:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid1 = fork();
    pid_t pid2 = fork(); // 创建两个子进程

    // 子进程1:退出码 10
    if (pid1 == 0 && pid2 > 0) {
        printf("子进程1(PID:%d)退出,退出码10\n", getpid());
        exit(10);
    }
    // 子进程2:退出码 20
    if (pid2 == 0 && pid1 > 0) {
        printf("子进程2(PID:%d)退出,退出码20\n", getpid());
        exit(20);
    }

    // 父进程:只等待子进程1(假设pid1是第一个子进程的PID)
    if (pid1 > 0 && pid2 > 0) {
        int status;
        // 精准等待 PID 为 pid1 的子进程,阻塞模式
        pid_t ret = waitpid(pid1, &status, 0);
        if (ret > 0 && WIFEXITED(status)) {
            printf("回收子进程%d,退出码:%d\n", ret, WEXITSTATUS(status));
        }
    }

    return 0;
}

非阻塞等待:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

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

    // 父进程:非阻塞等待子进程
    int status;
    while (1) {
        pid_t ret = waitpid(pid, &status, WNOHANG);
        if (ret == 0) {
            // 子进程还未退出,父进程可做其他事
            printf("子进程还在运行,父进程继续执行其他逻辑...\n");
            sleep(1);
        } else if (ret > 0) {
            // 成功回收子进程
            printf("子进程%d退出,退出码:%d\n", ret, WEXITSTATUS(status));
            break;
        } else {
            // 等待失败
            perror("waitpid 失败");
            break;
        }
    }

    return 0;
}

相较于阻塞等待,非阻塞等待,可以等子进程执行的区间,父进程完成其余的代码。只需要循环访查看子进程是否结束,来保证子进程不会出现僵尸进程的问题。因此非阻塞等待可以提高运行效率。

四,进程程序替换

在讲述用fork函数来创建子进程时,有时子进程往往运行并不是自己的程序,当子进程想要执行其他程序时,就需要用到进程替换。

通常来说,当进程执行程序替换函数之后,进程所对应的代码数据部分会被完全覆盖,相对于的PCB部分关于内存方面的也会被重构。但像一些pid等待并不会改变,也就是说当执行exec类函数时并不创建新进程,而是替代该进程执行所替代的函数。

程序替换的函数一共有6种:

cpp 复制代码
#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 execve(const char *path, char *const argv[], char *const envp[]);

本质上来说,是否传入绝对路径,命令行参数表,环境变量表等选项。

第一个参数代码传入的程序的路径,第一个参数在前三个都是第一个参数后跟的选项,第三个参数是环境变量,通常来说继承父进程的,但是也可以传入自己设定的环境变量。

相关推荐
雯宝1 小时前
mnt_init 函数
linux
律宏阔7 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔7 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴7 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千7 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei377 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei377 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei377 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh7 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW7 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux