(17)Linux的进程阻塞&&进程程序替换 && exec 函数簇

2024-01-07 19:14:41

前言:本章我们讲解它的 options 参数。在讲解之前我们需要理解进程阻塞,然后我们重点讲解二进程程序替换,这是本章的重点,然后介绍一个进程替换函数 execl,通过介绍这个函数来打开突破口,引入进程创建的知识点。最后,我们在学习进程创建的 exec 函数簇。

一、进程阻塞(Process Blocking)

1、继续讲解 waitpid

?我们先来简单回顾一下上一章的内容:?

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

?上一章介绍了?status?参数,知道了如何通过位操作来截 status?获取进程错误码与错误信号:

status&0x7F        // 获取错误信号
(status>>8)&0xFF   // 获取错误码

但是我们还是不建议这么做,因为直接用操作系统提供的宏就好了,我们可以通过 WIFEXITED?宏来检测子进程是否正常退出(检测进程退出时信号是否为 0),在用 WEXITSTAUS?宏还获取进程的退出码:

if (WIFEEXITED(status)) 
{
    printf("等待成功: exit code: %d\n", WIFEEXITED(status));
}

这些都是上一章讲解 status?参数的内容了,我们下面要讲的是 waitpid?另一个参数 options。?

options?为 0,则标识为 阻塞等待?

比如:如果子进程不退出,父进程在等,等的时候子进程是卡在那等的。

2、理解进程阻塞

思考:如何理解父进程进程阻塞??

首先,进程状态我们说过:如果一个进程在系统层面上要等待某件事情发生,

但这件事情还没发生,那么当前进程的代码还没法向后运行,只能让该进程处于阻塞状态。

就是让父进程的 task_struct 状态由 R\rightarrow S,从运行队列投入到等待队列,等待子进程退出。

子进程退出的本质是条件就绪,如果子进程退出条件一旦就绪,操作系统会逆向地做上述工作。

将父进程的\textrm{pcb}从等待队列再搬回运行队列,并将状态S\rightarrow R,此时父进程就会继续运行。

?3、轮询检测(Polling)

所谓的阻塞,其实就是挂起。在上层表现来看,就是进程卡住了?

而非阻塞式等待是会做些自己的事,而不是傻等!

多次调用非阻塞接口,这个过程我们称之为 轮询检测 (Polling)。

我们上一章中讲解 waitpid?时,举的例子都是 阻塞式 的等待。

如果我们想 非阻塞式 的等,我们可以设置 options?选项为?WNOHANG?(With No Hang)。

这个选项通过字面很好理解,就是等待的时候不要给我挂 (Hang) 住,其实就是非阻塞!

现在我们正式介绍一下 waitpid?的返回值:

  • 如果此时等待成功,返回值是子进程的退出码。
  • 如果你是非阻塞等待 (WNOHANG),等待的子进程没有退出,返回值为 0。

4、 基于非阻塞的轮询等待(waitpid)

?如果我们想把我们上一章节,演示 waitpid?使用方式的代码,改为非阻塞等待。

我们只需要将 waitpid?的 options?参数加上。

代码演示:基于非阻塞的轮询等待

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
 
int main(void)
{
    pid_t id = fork();
    if (id == 0) {
        // 子进程
        while (1) {
            printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());
            sleep(5);  // 先睡眠 5s,5s后退出
            break;
        }
        exit(233);
    }
    else if (id > 0) {
        // 父进程
 
        /* 基于非阻塞的轮询等待方案 */
        int status = 0;
        while (1) {
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if (ret > 0) {          // 等待成功
                printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);
            }
            else if (ret == 0) {    // 等待成功,但是子进程没有退出
                printf("父进程:子进程还没好,那我先做其他事情\n");
                sleep(1);
            }
            else {
                // 出错了,暂时不作处理
            }
        }
    }
    else {
        // 什么也不做
    }
}

说明:我们只需要将 waitpid 中的 options 参数带上 WHOHANG 就可以了。返回值 ret>0 就是等待成功,我们这里新增一个等于 0 的判断,作为 "等待成功但是子进程还没有退出" 的情况,因为等待的子进程没有退出,返回值为 0 。运行后,就会问子进程好没好,如果没有好父进程就可以做自己的事情了,这就是非阻塞式轮询等待。
运行结果如下:

我们可以让父进程在非阻塞等待时真正做点事?

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>
 
typedef void (* handler_t)();  // 函数指针类型
 
// 方法集
std::vector<handler_t> handlers;
 
void func1() {
    printf("Hello,我是方法1\n");
}
void func2() {
    printf("Hello,我是方法2\n");
}
 
void Load() {
    // 加载方法
    handlers.push_back(func1);
    handlers.push_back(func2);
}
 
int main(void)
{
    pid_t id = fork();
    if (id == 0) {
        // 子进程
        while (1) {
            printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());
            sleep(3);
        }
        exit(233);
    }
    else if (id > 0) {
        // 父进程
 
        /* 基于非阻塞的轮训等待方案 */
        int status = 0;
        while (1) {
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if (ret > 0) {          // 等待成功
                printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);
            }
            else if (ret == 0) {    // 等待成功,但是子进程没有退出
                printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n");
                if (handlers.empty()) {
                    Load();
                }
                for (auto f : handlers) {
                    f();  // 回调处理对应的任务
                }
 
                sleep(1);
            }
            else {
                // 出错了,暂时不作处理
            }
        }
    }
    else {
        // 什么也不做
    }
}

如果你想要你的程序直接父进程做更多的事情,把方法加到 Load?里就可以了。

写下 Makefile:

mytest:mytest.cpp
    g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
 rm -f mytest

运行结果如下:

二、 进程程序替换(Process Substitution)

1、 让子进程执行一个新的程序

我们之前做的所有代码演示,子进程执行的都是父进程的代码片段。

如果我们想让创建出来的子进程,执行全新的程序呢??

之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换

那为什么要让子进程执行新的程序呢?

我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:

  • ??? 让子进程执行父进程的代码片段(服务器代码…)
  • ??? 想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)

2、程序替换原理?

程序替换的原理:

  • 将磁盘中的内存,加载入内存结构。
  • 重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。
  • 效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序!

程序替换的本质

本质上就是去替换一个进程pcb在内存中的对应的代码和数据(加载新程序到内存——>更新页表信息——>初始化虚拟地址空间),然后这个进程pcb重新开始执行这个新的程序

这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!

因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。

?内核数据结构没有发生任何变化!?包括子进程的 \textrm{pid}\textrm{pid} 都不变,说明压根没有创建新进程。

?以可变参数列表的接收参数的 execl 接口

我们要调用接口,让操作系统去完成这个工作 —— 系统调用。

如何进行程序替换?我们先见见猪跑 —— 从 execl?这个接口讲,看看它怎么跑的。

int execl(const char* path, const char& arg, ...);

如果我们想执行一个全新的程序,我们需要做几件事情:

  • 第一件事情:先找到这个程序在哪里。
  • 第二件事情:程序可能携带选项进行执行(也可以不携带)。

?简单来说就是:① 程序在哪?? ② 怎么执行?

所以,execl 这个接口就必须得把这两个功能都体现出来!

  • ??? 它的第一个参数是 path,属于路径。
  • ??? 参数? const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。

代码演示:exec()?

#include <stdio.h>
#include <unistd.h>
 
int main(void)
{
    printf("我是一个进程,我的PID是:%d\n", getpid());
    // ls -a -l
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);  // 带选项
 
    printf("我执行完毕了,我的PID是:%d\n", getpid());
 
    return 0;
}

运行结果:

?运行最后就会执行ls命令execl("/usr/bin/ls","ls","-l","-a",NULL);

刚才是带选项的,现在我们再来演示一下不带选项的:?

 execl("/usr/bin/top", "top", NULL);  // 不带选项

运行结果如下:

这样我们的程序就直接能执行 top?命令了,除此之外,我们曾经学的大部分命令其实都可以通过 execl?执行起来。这就叫做 程序替换

printf("我执行完毕了,我的PID是:%d\n", getpid());

这句话为什么没有打印出来??

为什么我们最后的代码并没有被打印出来?

因为 一旦替换成功,是会将当前进程的代码和数据全部替换的!

所以自然后面的 printf 代码早就被替换了,这意味着该代码不复存在了,荡然无存!

因为在程序替换的时候,就已经把对应进程的代码和数据替换掉了!

而第一个 printf 执行了的原因自然是因为程序还没有执行替换,
所以,这里的程序替换函数用不用判断返回值?为什么?

int ret = execl(...);

一旦替换成功,还会执行返回语句吗?返回值有意义吗? 没有意义的!

?程序替换不用判断返回值!因为只要成功了,就不会有返回值。 而失败的时候,必然会继续向后执行。通过返回值最多能得到是什么原因导致替换失败的。只要执行了后面的代码,看都不用看,一定是替换失败了;只要有返回值,就一定是替换失败了。

我们来模拟一下失败的情况,我们来执行一个不存在的指令 :

execl("/usr/bin/lssssss", "ls", "-l", "-a", NULL);  // 带选项

execl?替换失败,就会继续向后执行。但是,一旦 execl?成功后就会跟着新程序的逻辑走,就不会再 return?了,再也不回来了,所以返回值加不加无所谓了。

3、引入进程创建?

以前我们的示例都是让子进程执行父进程的代码,我们今天想让子进程执行自己的程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
 
int main(void) 
{
    printf("我是父进程,我的PID是: %d\n", getpid());
    pid_t id = fork();
    if (id == 0) {
        /* child 
           我们想让子进程执行全新的程序 */
        printf("我是子进程,我的PID是:%d\n", getpid());
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);  /* 让子进程执行替换 */
 
        exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/
    }
 
    /* 一定是父进程 */
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if (ret == id) {
        /* 等待成功 */
        sleep(2);  
        printf("父进程等待成功!\n");
    }
 
    return( 0);
}

?运行结果:

成功执行代码,父进程也等待成功了。这里的子进程没有执行父进程的代码,而是执行了自己的程序。

子进程执行程序替换,会不会影响父进程呢?不会!因为进程具有独立性。

当程序替换的时候,我们可以理解成 —— 代码和数据都发生了写时拷贝,完成了父子分离。

三、exec 函数簇(Sheaf of functions exec)

1、以指针数组接收参数的 execv 接口

刚才我们学会了 execl?接口,我们下面开始学习更多的 exec?接口!它们都是用来替换的。

下面我们先来讲解一下和?execl?很近似的 execv:

int execv(const char* path, char* const argv[]);

path 参数和 execl 一样,关注的都是 "如何找到"

argv[] 参数关注的是 "如何执行",是个指针数组,放 char* 类型,指向一个个字符串。

大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。

所以 execv 和 execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。

值得注意的是,在构建 argv[] 的时,结尾仍然是要加上 NULL!
代码:execv()

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
 #define NUM 16
    int main()
{
pid_t id=fork();
if(id==0){
    //子进程
    //ls -a -l
  printf("子进程开始运行,pid: %d\n",getpid());
    sleep(3);
    char* const _argv[NUM]={
      (char*)"ls",
      (char*)"-l",
      (char*)"-a",
      NULL
    };
   //execl("/usr/bin/ls","ls","-a","-l",NULL);
   execv("/usr/bin/ls",_argv);
    exit(1);
  
    }
    else{
    //父进程
      int status=0;
      printf("父进程开始运行,pid: %d\n",getpid());
      pid_t id=waitpid(-1,&status,0);//阻塞等待                                                                                                                      
      if(id>0){
        printf("wait success,exit code: %d\n",WEXITSTATUS(status));
      }             
    }               
                      
    return 0;
  }

运行结果:

2、无需带路径就能直接执行的 execlp 接口(可变参数列表)

int execlp(const char* file, const char* arg, ...);

execlp,它的作用和 execv、execl?是一样的,它的作用也是执行一个新的程序。

仍然是需要两步:① 找到这个程序? ?② 告诉我怎么执行

第一个参数 file?也是 "你想执行什么程序",第二个参数 arg?是 "如何去执行它"。

所以这一块的参数传递,和 execl?是一样的,唯一的区别是比 execl?多了一个 p

我们执行指令的时候,默认的搜索路径在环境变量?\textrm{PATH}?中,所以这个 p?的意思是环境变量。

这意味着:执行 execlp?时,会直接在环境变量中找,不用去输路径了,只要程序名即可。

execlp("ls", "ls", "-a", "-l", "NULL");   // 路径都不用,直接扔

?这里出现的两个 ls?含义是不一样的,不可以省略

代码演示:execlp()

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
 #define NUM 16
    int main()
{
pid_t id=fork();
if(id==0){
    //子进程
    //ls -a -l
  printf("子进程开始运行,pid: %d\n",getpid());
    sleep(3);
    char* const _argv[NUM]={
      (char*)"ls",
      (char*)"-l",
      (char*)"-a",
      NULL
    };
   //execl("/usr/bin/ls","ls","-a","-l",NULL);
    //execv("/usr/bin/ls",_argv);
    execlp("ls","ls","-l","-a",NULL);
    exit(1);
  
    }
    else{
    //父进程
      int status=0;
      printf("父进程开始运行,pid: %d\n",getpid());
      pid_t id=waitpid(-1,&status,0);//阻塞等待                                                                                                                      
      if(id>0){
        printf("wait success,exit code: %d\n",WEXITSTATUS(status));
      }             
    }               
                      
    return 0;
  }

运行结果:

?3、无需带路径的 execvp 接口(指针数组)

int execvp(const char* file, char* const argv[]);

execvp?也是带 p?的,执行 execvp?时,会直接在环境变量中找,只要程序名即可。

代码

   char* const _argv[NUM]={
      (char*)"ls",
      (char*)"-l",
      (char*)"-a",
      NULL
    };
      execvp("ls", _argv); 

运行结果:

?目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的 C/C++ 程序呢?

4、利用 exec?调各种程序

假设有两个可执行程序:mycmd.c & exec.c,我们期望用 exec.c?调用 mycmp.c:

//mycmd.c
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
   
int main(int argc,char *argv[])  
{  
  if(argc!=2){  
    printf("can not execute!\n");  
   exit(1);  
  }  
  if(strcmp(argv[1],"-a")==0){  
    printf("helllo a!\n");
  }
  else if(strcmp(argv[1],"-b")==0){
    printf("hello b!\n");
  }
  else{
    printf("default!\n");
  }

  return 0;                                                                                                                                                        
                                                                                         
}   

也就是 C 语言的可执行程序调用 C++ 的可执行程序,我们先来设计一下 Makefile。

我们需要在前面添加 .PHONY:all ,让伪目标 all 依赖 exec 和 mycmd。

如果不这样做,直接写,默认生成的是 mycmd,轮不到后面的 exec,属于 "先到先得"。

且 Makefile 默认也只能形成一个可执行程序,想要形成多个就需要用到 all 了。

?

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
 #define NUM 16
const char *myfile="/home/amx/lesson3/mycmd";
    int main()
{
pid_t id=fork();
if(id==0){
    //子进程
    //ls -a -l
  printf("子进程开始运行,pid: %d\n",getpid());
    sleep(3);
    char* const _argv[NUM]={
      (char*)"ls",
      (char*)"-l",
      (char*)"-a",
      NULL
    };
   execl(myfile,"mycmd","-a",NULL);
    //execv("/usr/bin/ls",_argv);
    //execlp("ls","ls","-l","-a",NULL);
    exit(1);
  
    }
    else{
    //父进程
      int status=0;
      printf("父进程开始运行,pid: %d\n",getpid());
      pid_t id=waitpid(-1,&status,0);//阻塞等待                                                                                                                      
      if(id>0){
        printf("wait success,exit code: %d\n",WEXITSTATUS(status));
      }             
    }               
                      
    return 0;
  }

?

?运行结果:

这是用的是绝对路径,那我们使用相对路径可以吗?

结果:

依然可以!!!!

那么如何 执行其它语言的程序呢??

我们创建两个文件

准备工作:先给两个文件写一些内容

试试能否运行:

都没问题:

那么我们修改我们的exec.c文件

运行结果:

完美!!!

运行test.sh也是一样!!!

运行结果:

还有一种方法:

修改:exec.c

一样可以!!

5、添加环境变量给目标进程的?execle 接口

?

int execle(const char* path, const char* arg, ..., char* const envp[]);

我们可以使用 execle?接口传递环境变量,相当于自己把环境变量导进去。

打开 mycmd?文件,我们加上几句环境变量:

?我们自己在exec.c中定义一个

传进去:

?我们来试一下:

?超级缝合怪 execvpe 接口

v - 数组,p - 文件名,e - 可自定义环境变量:

int execvpe(const char* file, char* const argv[], char* const envp[]);

这也没什么好说的,execle、execve、execvpe 都是 "环境变量" 一伙的。

6、为什么会有这么多 exec 接口?

唯一的差别就是传参的方式不一样,有的带路径,有的不带路径,有的是列表传参,有的是数组传参,有的可带环境变量,有的不带环境变量。

因为要适配各种各样的应用场景,使用的场景不一样,有些人就喜欢列表传参,有些人喜欢数组传参。所以就配备了这么多接口,这就好比我们 C++ 函数重载的思想。
那为什么 execve?是单独的呢?

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

?它处于 man 2 号手册,execve?才属于是真正意义上的系统调用接口。

?总结一下它们的命名规律,通过这个来记忆对应接口的功能会好很多:

  • l (list) :表示参数采用列表形式
  • v (vector) :表示参数采用数组形式
  • p (path):有 p 自动收缩环境变量 PATH
  • e (env) :表示自己维护环境变量

感谢观看!!!

文章来源:https://blog.csdn.net/ayf9999/article/details/135370845
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。