【Linux】exec族函数·execve详解

execve函数详解及应用

目录

1.  概述

2.  执行简单的ls命令

拓展:查看当前环境变量

3.  程序跳转

拓展:C标准中的main函数声明

4.  execve和fork混合使用


1.  概述

        在 Linux 进程控制中,exec 函数族用于替换当前进程的映像,即用新的程序替换当前正在运行的程序,简单来说就是使进程执行另一段程序。exec家族一共有6个函数分别是execl()、execlp()、execle()、execv()、execvp()、execvpe(),他们包含在unistd.h当中,函数声明如下:

/* 替换当前进程,使用参数 ARGV 和环境变量 ENVP 执行 PATH。
   ARGV 和 ENVP 以 NULL 指针结束。*/
extern int execve (const char *__path, char *const __argv[],
		   char *const __envp[]) __THROW __nonnull ((1, 2));

#ifdef __USE_XOPEN2K8
/* 执行 FD 引用的文件,覆盖正在运行的程序映像。
   ARGV 和 ENVP 会传递给新程序,如同 `execve`。*/
extern int fexecve (int __fd, char *const __argv[], char *const __envp[])
     __THROW __nonnull ((2));
#endif


/* 使用参数 ARGV 和来自 `environ` 的环境变量执行 PATH。*/
extern int execv (const char *__path, char *const __argv[])
     __THROW __nonnull ((1, 2));

/* 执行 PATH,使用 PATH 后的所有参数直到 NULL 指针,
   以及该指针后的参数作为环境变量。*/
extern int execle (const char *__path, const char *__arg, ...)
     __THROW __nonnull ((1, 2));

/* 执行 PATH,使用 PATH 后的所有参数直到 NULL 指针,
   以及来自 `environ` 的环境变量。*/
extern int execl (const char *__path, const char *__arg, ...)
     __THROW __nonnull ((1, 2));

/* 执行 FILE,如果文件名不包含斜杠则在 `PATH` 环境变量中搜索,
   使用参数 ARGV 和来自 `environ` 的环境变量。*/
extern int execvp (const char *__file, char *const __argv[])
     __THROW __nonnull ((1, 2));

/* 执行 FILE,如果文件名不包含斜杠则在 `PATH` 环境变量中搜索,
   使用 FILE 后的所有参数直到 NULL 指针和来自 `environ` 的环境变量。*/
extern int execlp (const char *__file, const char *__arg, ...)
     __THROW __nonnull ((1, 2));

#ifdef __USE_GNU
/* 执行 FILE,如果文件名不包含斜杠则在 `PATH` 环境变量中搜索,
   使用参数 ARGV 和来自 `environ` 的环境变量。*/
extern int execvpe (const char *__file, char *const __argv[],
		    char *const __envp[])
     __THROW __nonnull ((1, 2));
#endif

参数:

  • path和file分别表示要执行的程序所包含的路径和文件名。
  • arg为参数序列,中间用逗号分隔。
  • argv为参数列表。
  • envp为环境变量列表。

        这里我们需要知道一点,上述所说的exec家族实际上都是execve函数的封装,其依赖关系可以参考下图:

        exec函数执行的特点:

  • exec把当前进程映像替换成新的程序文件,该进程完全由新程序代换,而且新程序从其main函数开始执行
  • 因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段
  • 如果exec函数执行成功,则原程序中的exec之后的代码都不会执行
  • exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性

        只有execve函数是真正意义上的系统调用,其他都是在此基础上经过包装的库函数,exec函数族的作用是根据指定的文件名找到可执行文件,并且它来取代调用进程内容,即exec系列不创建进程,被调用者成为当前进程,同时清除了调用进程,下面我们来使用一下。

2.  执行简单的ls命令

        开始先简单使用一下,我们重新创建一个文件命名为execve_example,在此文件夹下创建.c文件,如execve_ls.c,然后编写代码:

#include <unistd.h>
#include <stdio.h>

int main() 
{
    /**
     * exec系列函数  父进程跳转进入一个新进程
     * int execve (const char *__path, char *const __argv[], char *const __envp[])
     * 
     * char *__path: 需要执行程序的完整路径名
     * char *const __argv[]: 指向字符串数组的指针 需要传入多个参数
     *      (1) 需要执行的程序命令(同*__path)
     *      (2) 执行程序需要传入的参数
     *      (3) 最后一个参数必须是NULL
     * char *const __envp[]: 指向字符串数组的指针 需要传入多个环境变量参数
     *      (1) 环境变量参数 固定格式 key=value
     *      (2) 最后一个参数必须是NULL
     * return: 成功就回不来了 下面的代码都没有意义
            失败返回-1
     */
    char *argv[] = {"ls", "-l", "-a", NULL};
    char *envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/dky:/snap/bin:/opt/linux-devkit/bin:/home/dky", NULL};
    
    printf("准备执行 ls -l -a...\n");
    
    execve("/bin/ls", argv, envp);
    
    // 如果执行到这里,说明 execve 失败了
    perror("execve 失败");
    return 1;
}

        对于envp需要获取的环境变量,实际上如下即可:

char *envp[] = {"PATH=/bin:/usr/bin", "TERM=xterm", NULL};

        至于为什么我们写这么长,主要是,如果你不知道需要哪些环境变量,直接终端输入echo $PATH查询系统PATH环境变量,将所有的都输入:

        对于大多数程序,只需要这些基本环境变量:

char *minimal_envp[] = {
    "PATH=/bin:/usr/bin",      // 命令搜索路径
    "TERM=xterm",              // 终端类型
    "HOME=/home/username",     // 家目录
    "USER=username",           // 用户名
    "PWD=/current/directory",  // 当前目录
    "SHELL=/bin/bash",         // 默认shell
    NULL
};

        然后编写Makefile方便运行代码:

cc := gcc

execve_ls : execve_ls.c
	-$(cc) -o $@ $^
	-./$@
	-rm ./$@

        运行一下看看:

        来终端看一下,注意终端没有打印出execve_ls文件,那是因为我们在Makefile运行ls -al的时候还没调用rm删除execve_ls,而我们运行完去终端在输入命令查看时,此时已经调用rm删除过了:


拓展:查看当前环境变量

        如果想查看全部环境变量,可以通过env命令查看:

        如果想查看某一个或者几个,可以进行如下操作:

env | grep -E '^(PATH|HOME)'

对于Linux常用命令的使用这里不在一一介绍,想要详细了解可以参考:

【Linux应用开发·入门指南】常用命令大全(含完整命令演示过程)_linux命令解读-优快云博客

        这里解释一下功能,我们上面也演示了,通过 env 打印当前系统的所有环境变量,然后通过管道 | 将 env 的输出作为 grep 的输入,grep 进行文本搜索,通过 ^ 保证值匹配行开头,避免变量值中包含这些词导致误匹配,进行匹配括号里的内容,这里需要注意加上 单引号‘’ 或者双引号“”括起来,否则Shell 会优先解析括号 (),但 HOME|PATH 不是合法的 Shell 命令,Shell 会直接报错,不进行下面操作,如:


3.  程序跳转

        我们可以创建两个程序,通过 execve 函数实现程序一执行程序二,注意这里不是创建新进程,而是用新程序替换当前进程,也就是说进程不会改变,我们来验证一下,首先我们来编写一个用来接受传递参数的代码:

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

int main(int argc, char *argv[])
{
    //argv至少有想要传递的路径,以及需要传递的参数
    //因此argc不能小于2
    if (argc < 2) 
    {
        printf("传递参数不足,请重新修改参数\n");
        return 1;
    }
    else
    {
        printf("接收到的参数为:%s\n当前进程号为:%d\n", argv[1], getpid());
    }
    
    return 0;
}

拓展:C标准中的main函数声明

        先来看一下无参形式的,通常情况下无参的形式如下两种:

int main();

int main(void);

        这两种形式都表示main函数不接收命令行参数。在C99标准之前,main函数没有参数的形式被写为int main(),这在某些情况下可能导致与int main(void)行为不完全相同的问题,因为int main()在老式的C语言标准中不明确指出函数是否接受参数。

        从C99标准开始,推荐使用int main(void)明确指明main函数不接受任何参数,以提高代码的可读性和一致性。

        对于有参形式,如我们上面编写的代码:

int main(int argc, char *argv[]);
  • argc:传递给程序的命令行参数的数量
  • argv:指向字符串数组的指针,存储了命令行参数  

        对于argv通常情况下,argv[0]通常是程序的名称,argv[1]到argv[argc-1]是实际的命令行参数。

    char *argv[] = {
        "我是自定义程序名",  // argv[0] - 可以任意设置
        "参数1",
        "参数2", 
        NULL
    };

        下面编写,调用execve跳转程序传递参数的代码,为了便于观察我们先使用printf打印一下需要传递的参数和当前进程:

    char *name = "canshu";
    printf("\n将要传递的参数:%s\n当前的进程号:%d\n\n",name,getpid());

        然后编写需要传递的参数,我们上面了解到execve跳转,需要跳转路径,然后执行需要执行的程序命令以及程序需要传递的参数:

    // 准备跳转到第二个程序
    char *argv[] = {
        "/home/dky/Linux_task/exec_example/execve_skip",  // 要执行的程序路径
        name,                                             // 传递的参数
        NULL                                              // 参数结束标志
    };
    
    char *envs[] = {
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/dky:/snap/bin:/opt/linux-devkit/bin:/home/dky",
        NULL
    };

        然后将数据写入,加一层判断,如果数据为传输出去打印错误:

    int re = execve(argv[0], argv, envs);
    
    // 如果execve成功,下面的代码不会执行
    if (re == -1) {
        printf("未将数据传递出去\n");
        return -1;
    }

        完整代码:

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

int main(void)
{
    char *name = "canshu";
    printf("\n将要传递的参数:%s\n当前的进程号:%d\n\n",name,getpid());

    /**
     * exec系列函数  父进程跳转进入一个新进程
     * int execve (const char *__path, char *const __argv[], char *const __envp[])
     * 
     * char *__path: 需要执行程序的完整路径名
     * char *const __argv[]: 指向字符串数组的指针 需要传入多个参数
     *      (1) 需要执行的程序命令(同*__path)
     *      (2) 执行程序需要传入的参数
     *      (3) 最后一个参数必须是NULL
     * char *const __envp[]: 指向字符串数组的指针 需要传入多个环境变量参数
     *      (1) 环境变量参数 固定格式 key=value
     *      (2) 最后一个参数必须是NULL
     * return: 成功就回不来了 下面的代码都没有意义
            失败返回-1
     */

    // 准备跳转到第二个程序
    char *argv[] = {
        "/home/dky/Linux_task/exec_example/execve_skip",  // 要执行的程序路径
        name,                                             // 传递的参数
        NULL                                              // 参数结束标志
    };
    
    char *envs[] = {
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/dky:/snap/bin:/opt/linux-devkit/bin:/home/dky",
        NULL
    };
    
    // 关键操作:进程跳转
    int re = execve(argv[0], argv, envs);
    
    // 如果execve成功,下面的代码不会执行
    if (re == -1) {
        printf("未将数据传递出去\n");
        return -1;
    }
    
    return 0;
}

        编写Makefile代码:

execve_fath: execve_fath.c execve_skip
	-$(CC) -o $@ $<
	-./$@
	-rm ./$@ ./execve_skip

        编写完成,可以看到参数发生传递但是进程号并没有发生改变:

        除此之外,我们还可以发下一点就是,我们并没有在make当中编写编译execve_skip.c的代码,但是为什么图上会出现编译其的代码呢?

        那是因为make又一个非常强大的功能:自动推导。简单来说,我们编写:

execve_fath: execve_fath.c execve_skip

        表示 execve_fath 依赖于两个文件:execve_fath.c - 源代码文件和execve_skip - 可执行文件,但是 make 发现 execve_skip 文件不存在时,它会自动寻找生成这个文件的规则。

make不熟悉的可以查看:

【Linux应用开发·入门指南】Makefile菜鸟教程-优快云博客

        当然为了代码的可读性,我们可以添加上:

execve_skip: execve_skip.c
	$(CC) -o $@ $^

        到这里我们先思考一个问题,我们知道如果exec函数执行成功,则原程序中的exec之后的代码都不会执行,那么如果这样我们直接写在一起不就好了,为什么要这样用,这里就需要拿出我们之前提到的fork函数,其可以创建一个子进程,这样我们fork() 负责复制进程,execve() 负责替换程序,两者结合是不是就可以实现了灵活的进程管理和程序执行。

fork不熟悉的可以参考:

【Linux】fork函数详解-优快云博客

4.  execve和fork混合使用

        这样就可以解决 fork 后无法执行新程序的问题:

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

int main(void)
{
    char *name = "father";
    printf("\n父进程当前参数:%s\n当前的进程号:%d\n\n",name,getpid());

    __pid_t pid = fork();

    if(pid == -1)
    {
        printf("进程创建失败\n");
    }
    else if(pid == 0)
    {
        char *name = "son";
        printf("\n子进程将要传递的参数:%s\n当前的进程号:%d\n\n",name,getpid());

        // 准备跳转到第二个程序
        char *argv[] = {
            "/home/dky/Linux_task/exec_example/execve_skip",  // 要执行的程序路径
            name,                                             // 传递的参数
            NULL                                              // 参数结束标志
        };
        
        char *envs[] = {
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/dky:/snap/bin:/opt/linux-devkit/bin:/home/dky",
            NULL
        };
        
        // 关键操作:进程跳转
        int re = execve(argv[0], argv, envs);
        
        // 如果execve成功,下面的代码不会执行
        if (re == -1) {
            printf("未将数据传递出去\n");
            return -1;
        }
    }
    else
    {
        printf("父进程当前参数:%d",getpid());
    }
   
    return 0;
}

嵌入式Linux_时光の尘的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值