文章目录:
进程控制
execl接口介绍
多进程版本程序替换
其他exec接口
接口介绍 替换本地程序
总结
前言:
在Linux系统中,进程程序替换是一种重要的操作,通过进程程序替换,程序可以更新自己的代码和数据,让进程富有动态性和灵活性,话不多说,开始今天的话题。
我们的程序只能执行该程序自己的代码,这是众所周知的,但是今天,我想要创建一个子进程来执行别的文件的代码是否可行呢?
在Linux下是可实现的,因为Linux给我们提供了对应的接口:
这些接口支持我们程序在运行的过程中进行程序替换,从而执行到自己想执行的程序。
int execl(const char* path, const char* arg, ...) :
path :表示带路径文件名的字符串,从而搜索到对应的文件 arg, ...:表示可变参数列表,参数不确定,可传入一个或多个 最后必须以NULL结尾。
首先第一个接口,以下面代码来理解:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
printf("exec begin...\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL); //程序替换,可变参数
printf("exec end ...\n");
return 0;
}
能够清晰观察到,在begin之后,程序被替换为了ls 指令,并且选项为 -al,执行程序,发现运行成功了,但是仔细观察之后,在execl之后的printf并没有起作用。
结论1:
程序在执行完exec* 的接口之后,是不会再执行后续的代码了,因为后续代码已经被替换。
从man手册里有exec* 接口返回值的描述:
结论2:
exec* 只有失败有返回值,为-1。成功就是成功替换了,所以没返回值。
替换完成后是属于创建了新的进程还是旧的进程不变呢?我们不妨做个测试:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
printf("exec begin...\n");
sleep(5);
execl("/usr/bin/top", "top", NULL);
printf("exec end ...\n");
return 0;
}
虽然在替换之后进程的名字变了,但是前后两次的pid并没有变化。
结论3:
进程替换并不会创建新的程序,依旧是原来进程的pid。
通过之前的学习,我们知道进程之间相互独立,那么我们就可以创建一个子进程,让其来执行程序替换,而父进程回收结果:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
sleep(3);
printf("exec begin...\n");
execl("usr/bin/ls", "ls", "-a", "-l", NULL);
printf("exec end ...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if(rid > 0)
{
printf("wait success\n");
}
exit(1);
}
运行之后,就可以让子进程执行程序替换,并且父进程回收子进程的资源。
我们来思考一个问题:程序替换为什么对父进程没有影响?这是因为,进程具有独立性,在程序替换的时候发生写时拷贝。
我们通过man手册查询exec*接口,发现不止一个接口,还有六个接口:
我们需要了解这七个接口的含义以及用法,但是在这里我不会全部一一列举,因为有些接口是类似的,这些类似的接口我只需要说一个就够了。
首先,这些接口中带有 ‘p’ 字符的接口都有 path 这个参数,实际上这个参数的意义是:
PATH: 并不需要告诉系统程序的具体位置,只需要告诉系统程序的名称,系统在进行替换的时候,会自动在PATH环境变量中去查找。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
sleep(3);
printf("exec begin...\n");
execlp("ls", "ls", "-a", "-l", NULL);//使用带有'p'的接口
printf("exec end ...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if(rid > 0)
{
printf("wait success\n");
}
exit(1);
}
使用带 ‘p’ 字符的接口,就不需要带替换程序的路径了,只需要替换程序的名字,在OS中会 依照PATH环境变量来寻找该程序。
下面就是带有 ‘v’ 字符的接口,实际上这个v 在参数里表示的是 const char* argv[],我们在main函数里面是见过的,也就是 命令行参数表。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
char *const argv[] = {
(char*)"ls",
(char*)"-a",
(char*)"-l"
};
sleep(3);
printf("exec begin...\n");
execv("/usr/bin/ls", argv);//带有 'v' 字符的接口
printf("exec end ...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if(rid > 0)
{
printf("wait success\n");
}
exit(1);
}
最开始我们也见过带有 ‘l’ 字符的接口,它表示的是 list,也就是列表,把需要执行的命令和参数全部放在接口内。
那么带 ‘vp’ 的其实就是传 程序名,以及参数列表即可:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
char *const argv[] = {
(char*)"ls",
(char*)"-a",
(char*)"-l"
};
sleep(3);
printf("exec begin...\n");
execvp("ls", argv);//带 'vp' 的接口
printf("exec end ...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if(rid > 0)
{
printf("wait success\n");
}
exit(1);
}
我们前面的程序替换全部都是使用系统提供好的程序,我们使用自己写的程序该当何如?
#include<stdio.h>
#include<stdlib.h>
int main(int argc, const char* argv[])
{
for(int i = 0; argv[i]; ++i)
{
printf("argv[%d]:%s\n", i, argv[i]);
}
printf("I'm test process!\n");
printf("I'm test process!\n");
printf("I'm test process!\n");
printf("I'm test process!\n");
printf("I'm test process!\n");
return 0;
}
此时我们使用之前学习的make语法已经行不通了,因为无论怎样,只能编译过一个,今天我们来看点别的:
.PHONY:all#由依赖关系无依赖方法
all:myprocess mytest
mytest:mytest.c
gcc -o $@ $^ -g -std=c99
mybin:mybin.c
gcc -o $@ $^ -g -std=c99
.PHONY:clean
clean:
rm -f mybin mytest
在需要生成多个文件之前使用 .PHONY,加上依赖关系,但是不需要依赖方法,这样就能 根据依赖关系 从前到后依次 生成可执行文件。
那么现在我mybin.c文件的子进程要替换 mytest 程序,我们可以这么写:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
pid_t id = fork();
if(id == 0){ //child process
sleep(1);
printf("exec begin...\n");
execl("./mytest", "./mytest", "-a", "-b", "-c", NULL);//切换自己写的程序
printf("exec end ...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if(rid > 0)
{
printf("wait success\n");
}
exit(1);
}
当然,这里我是用C语言调用C语言程序,但是我们可以调用其他语言吗?答案是 可以调用 其他语言写的程序。
这是因为:不论什么语言,运行之后都是进程,只要是进程就都能在Linux下运行!
我们修改test文件,让其打印系统环境变量表:
#include<stdio.h>
#include<stdlib.h>
int main(int argc, const char* argv[], const char
{
for(int i = 0; env[i]; ++i)
{
printf("env[%d]:%s\n", i, env[i]);
}
return 0;
}
此时再使用程序替换,让子进程执行这段代码,父进程等待子进程资源回收:
我们也可以使用系统变量 environ,来获取环境变量:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
extern char **environ;
for(int i = 0; environ[i]; ++i)
{
printf("env[%d]:%s\n", i, environ[i]);
}
return 0;
}
我们使用 mybin 文件来执行程序的:
在mybin.c 中,我们并没有传递环境变量表给子进程,但是子进程却能默认拿到环境变量表?
实际上,子进程会默认拿到父进程环境变量表,那么mybin 也是子进程,是bash的子进程,所以mybin能拿到bash的环境变量,而mybin的子进程可以拿到父进程环境变量:
我们导入一个新环境变量在系统里以供猜想:
export VAL=youcanseeme
我们在进程地址空间那一节说过,进程地址空间内在 栈的上方 是 存储命令行参数以及环境变量的地方:
而在本文的最开始,我们也说了,进程替换替换的仅仅是进程的代码和数据,环境变量是不变的。
如果我们想单纯新增环境变量呢?我们可以使用 putenv:
此时我在程序内写入了mytest环境变量,但是当我们在系统中查询时:
此时并没有在系统中出现,但是当我们运行程序之后:
此时进程内就多了一项mytest的环境变量,而这个环境变量的导入位置是mytest 父进程传给子进程的环境变量,而mytest的父进程是bash,也就是说,在这里bash将从 0-24号环境变量传给了进程mytest,而mytest 使用了putenv新增了环境变量给子进程。
而现在我想 设置全新的环境变量给子进程,这个时候我们就需要用到带有 ‘e’ 字符的接口了
接口中还存在带 ‘e’ 字符的接口,e表示的就是 env:const char* env[], 也就需要 环境变量表。
其实这是以 覆盖 的方式来传递环境变量,也就相当于子进程设置了全新的环境变量了。
我在最前面总共列举了七个接口,一个程序替换为什么会有这么多的接口呢?但他们的功能都是进行程序替换,所以他们在功能上没有区别。
他们仅仅是在传参上有区别,其实我们 程序替换的系统调用只有一个,就是 execve 接口,剩下的六个全部都是由这个接口进行封装的。
创作不易,还望三联支持博主呀~~