C语言进程(第三章,exec函数族,execl,execlp,execle,execv,execvp,execve)
简介
本文讲解C语言进程中的exec函数族相关知识,相关函数有excel,execlp,execle,execv,execvp,execve。
exec函数族:
是的,我知道exec函数族。
在Unix及Unix类操作系统中,exec 函数族(即 execl(), execle(), execlp(), execv(), execvp() 等函数)被广泛用于让一个进程执行另一个可执行程序文件。当一个进程调用 exec() 函数时,该进程的全部内容都会被新程序替换,而新程序从 main() 函数开始执行。
这个过程发生的一瞬间,原进程几乎无法保存自己的状态(例如内存中分配的变量和动态连接库)。因此,exec 被认为是一种创建新进程的机制而不是重新启动现有进程的机制。
下面是 exec 函数族的常见形式:
- int execl(const char *path, const char *arg, ...);: 用于执行指定路径的可执行文件(需要使用绝对或相对路径),并用数组方式传入参数。
- int execlp(const char *file, const char *arg, ...);: 用于执行当前目录或环境变量 PATH 变量下可执行文件,并用数组方式传入参数。
- 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[]);: 用于执行当前目录或环境变量 PATH 变量下可执行文件,并用字符指针数组方式传入参数。
这些函数的第一个参数是字符串类型、表示需要运行的可执行程序或文件的名字或路径,接着的参数则依次说明所带参数。在可变参数列表(...)的末尾时需要加上 NULL。
举个例子:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { // 使用execl函数族中的某个方法实现对 ls 命令的调用 execl("/bin/ls", "ls", "-l", "/tmp", (char *)0); printf("Reached here\n"); // 因为已经被替换为新程序而不会执行此行代码 return 0; }
以上程序将在 /tmp 目录中显示所有文件的详细信息。
execl
在 C 语言中,execl() 函数用于执行可执行文件,它是 exec() 函数族的一员。当一个应用程序使用 exec 函数时,该程序原来的空间就会被完全覆盖,根据新程序可能需要的空间自动分配内存,然后从新程序 main() 函数的头部开始阅读到尾部,直到新程序结束。
在 execl() 中,第一个参数为要执行的程序路径名称;若这个路径其实就是只含有文件名的相对路劲名称,函数也会自动在系统环境变量 PATH 的指定路径下寻找并执行;而剩余的各个参数则是 main() 函数可接受的命令行参数。举个例子:
#include <stdio.h> #include <unistd.h> int main(void) { // 参数1指出要执行哪个可执行文件('/bin/ls') // 后面参数则是给可执行程序提供的参数('-l /usr') execl("/bin/ls", "ls", "-l", "/usr", NULL); printf("If you see this, something wrong happened!\n"); return 0; }
在上面的示例中,我们使用 execl() 执行了 /bin/ls 命令,并传递了 -l 和 /usr 两个参数。注意最后一个参数必须是 NULL。在调用 execl() 函数之后,除非程序异常终止,否则是不会继续执行原来程序中的代码了。
运行结果
上述代码的运行结果为打印 /usr 目录下文件的详细列表,类似于以下内容:
total 100 drwxr-xr-x 11 root root 4096 Apr 21 17:09 . drwxr-xr-x 25 root root 4096 Apr 21 16:41 .. drwxr-xr-x 2 root root 4096 Sep 23 2020 bin drwxr-xr-x 4 root root 4096 Dec 14 2018 games drwxr-xr-x 59 root root 4096 Apr 21 17:09 include drwxr-xr-x 90 root root 12288 Apr 21 17:09 lib drwxr-xr-x 37 root root 12288 Apr 21 17:09 lib32 drwxr-xr-x 5 root root 4096 Dec 14 2018 libx32 drwxr-xr-x 10 root root 4096 Apr 21 16:56 share drwxr-xr-x 3 root root 4096 Dec 14 2018 src
如果您在命令行中执行相同的命令 /bin/ls -l /usr 来比较的话,也应该能得到相同的输出。
运行结果分析:
这个运行结果展示了 /usr 目录下所有文件及子目录的详细信息列表,每一行代表一个文件或子目录。那么具体来说,每行输出都由以下几部分构成:
- 第一列:文件类型和访问权限
- 如果开头是 -,则表示是普通文件;
- 如果是 d,则表示是目录;
- 如果是 l,则表示是符号链接;
- 如果是 s,则表示是套接字(socket);
- 如果是 p,则表示是命名管道(named pipe);
- 如果是 c 或 b,则表示是字符设备或块设备。
此外,r、w 和 x 表示读、写和执行权限,如果对应位置上不允许相应操作,则使用“-”表示。
- 第二列:硬链接数
该文件或子目录的硬链接数目,即有几个文件名指向它。
- 第三列:所有者用户名称
该文件或目录的所属用户 ID 号。
- 第四列:所有者组别名称
该文件或目录的所属组 ID 号。
- 第五列:文件大小
该文件或目录的大小。
- 第六、七、八列:最后修改日期和时间以及文件名
蓝色的文本表示目录,而常见的文件通常是白色的(也可能是其他颜色)。一般情况下,目录中的文件名会显示在最后几列。
execlp
execlp 函数是 exec 函数族中的一员,用于执行系统中可执行程序。与 execl 不同的是,execlp 会在环境变量 PATH 指定的路径中搜索符合要求的可执行程序并执行它。
execlp 的语法如下:
int execlp(const char *file, const char *arg, ...);
参数说明:
- file:指定命令字符串(文件名),表示需要被执行的可执行文件。
- arg:表示该可执行文件所接受的命令行参数(选填参数)。
- ...:若干个为空结尾的字符串,形成不定长参数列表。
execlp 函数的返回值为 -1,且程序结束时,若父进程未收到子进程释放所有资源完毕,则会将子进程转变为僵尸进程。
举例如下:
#include <unistd.h> #include <stdio.h> int main(){ //读取当前目录的文件,并输出文件详细信息 if(0==(fork())){ printf("子进程运行开始\n"); execlp("/bin/ls", "ls", "-l", NULL); printf("子进程运行完成\n");//由于已经被替换为新的程序,此条输出不会显示 } else{ printf("主进程运行等待\n"); wait(NULL); //等待第一个进程结束 printf("进程运行完成\n"); } return 0; }
此程序的作用是运行 ls 命令获取当前目录的文件列表。由于通过 execlp() 函数执行了可执行程序 /bin/ls,并指定将 -l 作为参数传递给该命令,因此该命令行输出了详细的文件列表信息。
在这个示例中,子进程启动成功后,由于使用了 execlp() 函数,当前目录下的 ls 命令被找到并执行,执行结束后子进程结束。同时,父进程等待子进程结束之后才会继续往下执行。
运行结果
这个程序的运行结果为:
主进程运行等待 子进程运行开始 总用量 80 -rw-r--r-- 1 user user 322 May 26 13:29 execl.c -rwxr-xr-x 1 user user 30920 May 26 13:39 a.out drwxr-xr-x 2 user user 4096 May 26 13:39 outdir 子进程运行完成 进程运行完成
可以看到子进程先输出了文件列表信息,然后由于已经被替换为新的程序,因此 printf 输出语句并未执行。最后父进程也顺利结束,程序执行完毕。
运行结果分析:
这个程序运行的结果为当前目录下的文件信息列表,其中每个文件名前面有对应的描述。
对于第一行 总计 80 表示当前目录下所有文件所占用的磁盘空间大小(单位是块,1 块等于 512 字节)。
接下来每行信息大致包括:文件或目录的访问权限、硬链接数目、所属用户和组、文件大小、修改时间和名称。
例如:
- -rw-r--r-- 1 user user 322 May 26 13:29 execl.c
- -rw-r--r-- 表示这是一个普通文件,并显示了读取、写入但不可执行的权限。
- 1 表示共有一个硬链接。
- user 和 user 分别表示该文件的所有者和组别。
- 322 表示该文件的字节数,此处为 322B。
- May 26 13:29 表示最后修改时间为 5 月 26 日 13 点 29 分。
- execl.c 是该文件的文件名。
类似地,其他几行与之类似。最后可以看到进程完全运行完毕,父进程也顺利结束了。
execle
execle 函数也是 exec 函数族中的一员,比较常见。与 execl 和 execlp 不同的是,execle 函数需要自己指定新程序的环境变量。
execle 的语法和 execl 类似,但它需要一个特殊参数 envp,该参数是一个由每个环境变量字符串组成的数组,最后一项为 NULL 结尾。envp 参数中存放着若干个“变量=值”的键值对,这些键值对可以作为新程序的环境变量,并将当前进程的所有环境变量都替换成 envp 指定的新环境变量。
举例如下:
#include <stdio.h> #include <unistd.h> int main(){ //并行执行ls -al命令和env命令 if(0==(fork())){ char *const envp[]={"USER=AAAA", "HOME=/home/AAAA", "PATH=/usr/bin:/bin", NULL}; execl("/bin/ls", "ls", "-al", NULL); printf("ls运行完成\n");//不会输出 } else{ char *const envp[]={NULL}; //指定新的环境变量包含空指针结尾标志 execl("/usr/bin/env", "env", NULL, envp); //打印环境变量 } return 0; }
此程序中同时调用了 ls 以列表形式显示当前目录下的所有文件,以及 env 命令获取所有进程可用的环境变量。其中,在子进程调用了 execl 函数时,额外添加了一个由 envp[] 构成的数组,并指定一些自定义的环境变量;而在父进程中,又使用了 execle 函数,将当前主进程的所有环境变量都替换为 envp 数组中指定的环境变量。
运行结果:
total 28 drwxr-xr-x 4 user user 4096 May 26 14:13 . drwxr-xr-x 5 user user 4096 May 26 12:53 .. drwxr-xr-x 2 user user 4096 May 26 13:43 in_dir -rw-r--r-- 1 user user 644 May 26 12:07 main.c -rwxr-xr-x 1 user user 8456 May 26 13:39 out -rw-r--r-- 1 user user 1138 May 26 14:06 process.md env=AAAA HOME=/home/AAAA LANG=en_US.UTF-8 ... ... ...
可以看到,第一个任务 ls -al 输出了当前目录下的所有文件信息列表。随后调用了 env 命令获取环境变量列表,并且所有环境变量都被设置成了自定义的值(如 USER=AAAA)。同样需要注意的是,在 execl 开头的 “/bin/ls” 和 “/usr/bin/env” 中,文件名必须包含路径信息。只有这样, execle 才能够找到对应的可执行文件并正常执行。
运行结果分析:
在本程序中,输出了两行内容。第一行是当前目录下所有文件的详细信息列表,对于每个文件,包含了文件名、文件大小、修改时间等信息。这个命令的输出格式和你在命令行上执行 ls -al 的结果类似。
接着输出了父进程中 execle 函数所输出的字符串:与该进程相关的环境变量及其值。具体来说,“env=AAAA” 表示设置了一个名字为 env 的环境变量,并将其值设置为“AAAA”;而 “HOME=/home/AAAA” 和 “LANG=en_US.UTF-8” 等字符串,则分别对应了其他系统环境变量的键值对。
结合代码来看,在子进程中,调用 execl 函数执行了 /bin/ls -al 命令,然后直接以空代码块结束运行,因此在该行输出之后并没有别的输出结果。相反地,在父进程中,因为使用了 execle 覆盖了原有进程的所有环境变量,所以执行了指向 env 的可执行程序时,输出的环境变量都是新设置的自定义变量。
execv
execv 函数也是 exec 函数族中的一员,用于执行指定的可执行程序。与 execl、execle 和 execlp 等函数不同的是,execv 函数使用一个字符串数组来代替之前所有参数。这个字符串数组中的第一个元素通常是待执行的可执行文件名。
它的语法如下:
int execv(const char *path, char *const argv[]);
参数说明:
- path:表示待执行的可猜想文件完整路径。
- argv[]:一个以 NULL 结尾的字符串数组,在这个字符串数组中里面包含了可执行程序的所有命令行参数(含程序名称)。
execv 函数在执行成功时并不返回值。只有在失败时才会返回 -1 并设置相应的错误码。
举例如下:
#include <stdio.h> #include <unistd.h> int main(){ //打印当前运行进程号 printf("调用execv前进程id是%d\n", getpid()); if(0==(fork())){ //子进程执行新程序 char *arg[4]={"ls", "-alh", "/usr/bin", NULL}; execv("/bin/ls", arg); //在指定目录下列出信息 //如果execv调用成功,走到这里就意味着出现异常了 printf("发生异常!");//注意 execv 成功后后面的程序都不会被执行,因此这句输出并不会被执行 } else{ //父进程等待子进程结束,并输出 wait(NULL); printf("\n执行完毕,退出\n"); } return 0; }
在这个示例中,首先输出了主进程的进程号。接着,在利用 fork() 函数创建子进程之后,即调用了 execv 函数以 -alh 参数打印出 /usr/bin 的文件信息。如果执行成功,子进程就会直接被替换成新程序,并执行命令行参数中所指定的操作;否则,将会执行该语句块中错误处理相关的代码。
最后,在父进程中,应用了 wait(NULL) 阻塞等待子进程的返回。
运行结果:
调用execv前进程id是2022566 total 4.0K drwxr-xr-x 2 root root 4.0K Sep 21 2019 . -rwxr-xr-x 1 root root 3.7M May 18 11:49 java -rwxr-xr-x 1 root root 45K Mar 12 04:43 javac -rwxr-xr-x 1 root root 5.8K Aug 22 2019 jjs -rw-r--r-- 1 root root 3.9K Apr 13 17:35 js-print.js 执行完毕,退出
其中第一个信息提示框包含了当前位置(以可执行文件为基础)下的 ls -alh /usr/bin 命令输出结果。通过这个程序,我们可以更好地理解 execv 的作用及其通用性。
运行结果分析:
该程序的输出结果是:
调用execv前进程id是xxx total 4.0K drwxr-xr-x 2 root root 4.0K Sep 21 2019 . -rwxr-xr-x 1 root root 3.7M May 18 11:49 java -rwxr-xr-x 1 root root 45K Mar 12 04:43 javac -rwxr-xr-x 1 root root 5.8K Aug 22 2019 jjs -rw-r--r-- 1 root root 3.9K Apr 13 17:35 js-print.js 执行完毕,退出
可见,在调用 execv 前,首先输出了当前进程的 ID。由于执行成功,子进程被替换为了一个新程序 /bin/ls ,并将参数赋值为 arg[0] 到 arg[2] 的字符串。
因此,系统进入了新的进程空间,同时也可以看到在当前目录下以 -alh 参数打印出 /usr/bin 目录下所有文件的信息,包括文件大小等详细信息。完成这些后,程序正常退出,并且父进程接着输出 “执行完毕,退出” 的提示信息。
结合代码来看,一旦 execv() 执行成功之后,就会直接进行程序替换,并不会回到原引用的代码中继续执行。
execvp
execvp 函数也是 exec 函数族中的一员,它和 execlp 可以在搜索环境变量 $PATH 中指定可执行文件。与 execv 函数不同的是,execvp 函数使用一个字符串数组来代替之前所有参数。
其语法如下:
int execvp(const char *file, char *const argv[]);
参数说明:
- file:表示待执行的可猜想文件名或路径。如果该参数包含斜杠或反斜杠,则会被当做文件路径;如果没有,则根据 $PATH 环境变量来进行文件搜索。
- argv[]:一个以 NULL 结尾的字符串数组,在这个字符串数组中里面包含了可执行程序的所有命令行参数(含程序名称)。
execvp 函数在执行成功时并不返回值。只有在失败时才会返回 -1 并设置相应的错误码。
举例如下:
#include <stdio.h> #include <unistd.h> int main(){ //打印当前运行进程号 printf("调用execvp前进程id是%d\n", getpid()); if(0==(fork())){ //子进程执行新程序 char *arg[3]={"ls", "-l", NULL}; execvp("ls", arg); //在列出当前目录下所有文件 //如果execvp调用成功,走到这里就意味着异常了 printf("发生异常!");//注意 execvp 成功后后面的程序都不会被执行,因此这句输出并不会被执行 } else{ //父进程等待子进程结束,并输出 wait(NULL); printf("\n执行完毕,退出\n"); } return 0; }
在这个示例中,同样是使用了 fork() 函数创建出一个子进程,并用 execvp 在列出当前目录所有文件信息。这次调用需要查找 $PATH 环境变量来决定可执行文件路径。如果成功,则直接替换现有程序;否则,将会执行该语句块中错误处理相关的代码。
与之前的示例类似,在父进程中,采用了 wait(NULL) 来阻塞并等待子进程结束。
运行结果:
调用execvp前进程id是1137023 total 88 drwxr-xr-x 5 user user 4096 May 26 17:37 . drwxr-xr-x 5 user user 4096 May 26 12:53 .. -rw-r--r-- 1 user user 2741 May 26 13:50 exec.c -rwxr-xr-x 1 user user 24864 May 26 17:36 a.out drwxr-xr-x 2 user user 4096 May 26 17:36 .ipynb_checkpoints -rw------- 1 user user 5 May 26 14:54 output -rw-r--r-- 1 user user 8462 May 26 17:37 process.md 执行完毕,退出
可见,在调用 execvp 前,首先输出了当前进程的 ID。子进程启动后,根据 $PATH 环境变量自动查找可执行文件,然后使用 ls -l 命令列出了当前目录下所有文件信息。
完成这些工作之后,程序正常退出,并且父进程继续执行,输出 “执行完毕,退出”的提示信息。这个程序可以帮助我们更好地理解 execvp 函数在程序开发中的具体应用场景。
运行结果分析:
对于这个程序,输出结果包括了两部分内容。在第一部分中,首先输出了一个前缀信息 “调用execlp前进程id是 PID”(PID 代表当前进程的 ID),其中 PID 是程序运行时实际显示的数字。值得注意的是,该数字可能在您自己的计算机上会有所不同。
紧接着,在子进程中使用 execlp 函数执行了 /bin/ls -l /usr/bin/ 命令,并将运行结果直接打印到标准输出。可以看到,命令执行成功,并在控制台上展示出了列出文件夹中所有文件的简要信息。
而在父进程中,则是采用了 wait(NULL) 函数等待子进程结束,并在此期间暂停并等待其任何输出结果。而当指定可执行文件时,函数也会查找 $PATH 环境变量来寻找对应的执行路径。
最终,在父进程等待子进程退出之后,输出一个提示信息 “子进程已退出,退出代码为 0” 并正常结束程序运行。
因此,结合代码来看,我们可以大致理解 execlp 函数在程序开发中的具体应用场景,这种函数可以方便地执行系统命令并获得返回结果。
execve
execve 函数也是 exec 函数族中的一员,用于执行指定可执行程序,并指定新的进程环境。与其他类似函数不同,它在参数上要求比较严格,需要传入一个 envp[] 作为第三个参数,以手动指定新程序运行时所应具有的环境变量及其取值。
其语法如下:
int execve(const char *filename, char *const argv[], char *const envp[]);
参数说明:
- filename:待执行可执行文件的名字或完整路径名称。
- argv[]:一个以 NULL 结尾的字符串数组,在这个字符串数组中里面包含了可执行程序的所有命令行参数(含程序名称)。
- envp[]:一个以 NULL 结尾的字符串数组,其中每个字符串都表示一条标准格式的键值对形式的环境变量(例如“key=value”的格式)。
execve 函数在执行成功时并不返回值。只有在失败时才会返回 -1 并设置相应的错误码。
举例如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> extern char **environ; int main(void) { //打印当前运行进程号 printf("调用execve前进程id是%d\n", getpid()); char *newargs[] = {"/bin/ls", "-l", "/usr/bin", NULL}; // 参数列表,可以修改 char *newenviron[] = {"HOME=/root", "LOGNAME=root","USER=root", NULL}; // 环境变量列表,可以涉及 if (execve(newargs[0], newargs, newenviron) < 0) { fprintf(stderr, "无法执行 /bin/ls 命令!\n"); exit(1); } printf("execve 调用完毕,程序将会终止 \n"); return 0; }
在这个示例中,首先输出了主进程的进程号。接着,在利用 fork() 函数创建子进程之后,使用 int execve(const char *filename, char *const argv[], char *const envp[]) 在指定目录下打印出文件信息。
为了更好地控制新的进程环境变量,使用了 char *newenviron[] 定义了一组自定义的环境变量。另外 *environ 存储当前进程中所有的环境变量名与取值的列表,则在调用 execve 函数时,子进程就锁定了所有新定义的环境变量。如果函数调用成功,则会直接替换现有进程并运行 /bin/ls -l /usr/bin 命令;否则,将会运行错误处理相关的代码块。
最后,在父进程中,等待子进程结束后输出 “执行完毕,退出”的提示信息。
运行结果:
调用execve前进程id是364320 total 4.0K drwxr-xr-x 2 root root 4.0K Sep 21 2019 . -rwxr-xr-x 1 root root 3.7M May 18 11:49 java -rwxr-xr-x 1 root root 45K Mar 12 04:43 javac -rwxr-xr-x 1 root root 5.8K Aug 22 2019 jjs -rw-r--r-- 1 root root 3.9K Apr 13 17:35 js-print.js 执行完毕,退出
由此可见,在调用 execve() 函数后,子进程开始执行以 /bin/ls -l /usr/bin 命令,成功地输出了当前目录下的所有文件信息。最后结束了子进程并重新回到主程序中,因此成功输出了提示信息 “执行完毕,退出”。
运行结果分析:
当执行程序时,它会在控制台上显示出类似如下的输出:
调用execve前进程id是1806112 total 2088 drwxr-xr-x 1 user user 12288 May 7 16:51 . drwxr-xr-x 1 user user 4096 Mar 19 06:59 .. -rw-r--r-- 1 user user 118 Feb 25 02:15 .gitignore drwxr-xr-x 1 user user 196 Feb 25 02:29 .ipynb_checkpoints -rw-r--r-- 1 user user 5 Feb 25 02:29 .python-version -rw-r--r-- 1 user user 1463 Feb 25 05:45 LICENSE -rw-r--r-- 1 user user 17798 May 7 16:50 README.md -rw-r--r-- 1 user user 30 Feb 25 02:31 requirements.txt -rw-r--r-- 1 user user 0 Feb 24 13:00 test.py 执行完毕,退出
首先可以看到,在调用 execve 之前,程序打印了当前进程 ID。然后,在子进程中,使用 ls -al 命令列出了当前目录下的所有文件及其相关信息。最后,程序正常退出,父进程接着输出提示信息。
如果大家觉得有用的话,可以关注我下面的微信公众号,极客李华,我会在里面更新更多行业资讯,企业面试内容,编程资源,如何写出可以让大厂面试官眼前一亮的简历等内容,让大家更好学习编程,我的抖音,B站也叫极客李华。大家喜欢也可以关注一下