什么是进程替换?
我们的可执行程序,在运行起来的时候就上一个进程
一个进程就会有他的内核数据结构+代码和数据
把一个已经成型的进程的代码和数据替换掉,这就叫进程替换
也就是可以通过系统调用把当前进程替换位我们需要的进程
那么替换后,会创建一个新进程吗?
不会,只是在旧进程的壳子执行新进程;替换进程后,之前的代码不会执行,因为已经被替换了
进程怎样替换?
进程替换是需要接口函数的,总不能什么都没有直接替换吧
来看看都有什么:
上图的【int execl(const char *pfin,“const char *arg, ...);】这种后面的省略号,是指参数可变
来举个栗子捏:
test1.c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("test1.c ... begin!\n");
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
printf("test1.c ... end!\n");
return 0;
}
makefile:
test1.out:test1.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test1.out
解释一下:就是把我们本来的代码通过execl来执行execl引用的程序,成为替换的新进程
替换的过程?
而execl的返回值我们并不关心,因为一旦替换成功,就不会向后继续运行旧进程了
而只要继续运行旧进程,那就一定是替换失败了
所以我们的替换接口函数只有失败返回值,没有成功返回值
替换的过程本质上是把这个新程序加载到内存上
一个程序怎样加载到内存上呢?
别忘了exec*!它类似一种Linux上的加载函数,做的是从外设拷贝到内存是操作系统的活
多进程版的进程替换
将代码改成多进程版就是fork创建子进程,让子进程自己替换,父进程只需要等待就好了
也就是说这里被替换的是子进程捏,父进程还在等待
test1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("exec begin\n");
//进程替换
execl("/usr/bin/ls", "ls", "-al", NULL);
printf("exec end\n");
exit(0);
}
int rid = waitpid(id, NULL, WUNTRACED);
if(rid > 0)
{
printf("wait success\n");
}
return 0;
}
makefile:
test1.out:test1.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test1.out
解释一下:我们在if(rid==0)分支里,先打印了一下,然后进入我们的接口,接口里是我们的ls命令,我们ls命令也是文件,也是C语言写的,我们平时执行命令也是执行该程序,所以可以在接口里替换为ls命令
这样就是替换成功了
我们刚刚说了替换失败会继续执行原进程,来看看吧:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("test1.c ... begin!\n");
pid_t id = fork();
if (id == 0)
{
sleep(2);
execl("/usr/bin/lsss", "lsss", "-l", "-a", NULL);
printf("替换失败捏");
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father wait success,child exit code:%d\n",WEXITSTATUS(status));
}
printf("test1.c ... end!\n");
return 0;
}
你看,没执行execl里面的程序
以前创建子进程让它完成任务,是让子进程执行父进程代码的一部分,现如今可以让子进程执行一个全新的程序
进程有对应的数据和代码,创建子进程会有新的地址空间和页表;替换本身就是覆盖,子进程在数据部分进行写时拷贝,当执行新程序的时候新程序也要进行写时拷贝,所以此后父子间几乎完全独立(本来数据和代码是继承父进程的,现在是全新的了)
来仔细介绍一下我们的替换接口函数:
这些函数的前四个字母都是exec,后面跟着的:
p: 表示自动搜索路径;e: 表示自己维护环境变量;l:表示采用列表传参;v:表示采用数组传参;
execl
int execl(const char *path, const char *arg, ...);
l就是list(列表),传参数列表
剩下的...带选项,在命令行中怎样写命令的参数,就怎么写(下图有例子捏)结束时传NULL
路径表明我们想要执行谁,后面的选项表示我们想要怎样执行
execv
v是vector的意思,动态数组
int execv(const char *path, char *const argv[]);
前面的参数是路径,后面的参数是指针数组,execl的是可变参数,execv的传参需要传指针数组指针数组是自己写的
execvp
带p的意思就是用户可以不用传要执行文件的路径(但要传文件名)
char *const argv[] = {"ps", "-ef", NULL};
execvp("ps", argv);
那它是怎么做到的呢?
别忘了p是可以自己搜路径,在环境变量里搜路径
execlp
就是采用列表传参,但是不用输路径
execlp("ps", "ps", "-ef", NULL);
execvpe
execvpe
带e的需要自己组装环境变量
int execvp(const char *file, char *const argv[], char *const envp[]);
以替换 "ls -al" 为例,示例一下剩余接口:
//l代表传参数列表,分成一个个字符串传
execl("/usr/bin/ls", "ls", "-al", NULL);
//p表示系统会去PATH环境变量中找
execlp("ls", "ls", "-al", NULL);
//v代表传数组,把命令分成字符串,放进字符串数组里,一起传
char* argv[] = {"ls", "-al", NULL};
execv("/usr/bin/ls", argv);
替换自己写的进程
刚刚我们引用了系统的进程,我们也可以引用我们自己写的程序
下面就来替换段C++代码(文件后缀: .cpp , .cc ,.cxx)
来先写一个自己的程序:
#include<iostream>
using namespace std;
int main()
{
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
return 0;
}
然后再也一个父进程:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("test1.c ...begin!\n");
pid_t id = fork();
if (id == 0)
{
sleep(2);
execl("./mypragma", "mypragma", NULL);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father wait success,child exit code:%d\n", WEXITSTATUS(status));
}
printf("test1.c ... end!\n");
return 0;
}
tips:别忘了makefile默认是从上到下形成可执行程序,所以第一个要是主程序,其他的依赖程序依次往下排哦
.PHONY:all
all:testexec mypragma
mypragma:mypragma.cc
g++ -o $@ $^ -std=c++11
testexec:testexec.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f testexec mypragma
来看看:
程序替换并未形成新进程,我们来验证一下:
test1.c:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("test1.c ...begin!\n");
pid_t id = fork();
if (id == 0)
{ printf("i am a process,pid:%d\n",getpid());
sleep(2);
execl("./mypragma", "mypragma", NULL);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father wait success,child exit code:%d\n", WEXITSTATUS(status));
}
printf("test1.c ... end!\n");
return 0;
}
mypragma.cpp
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
printf("i am a new process,pid:%d\n",getpid());
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
return 0;
}
看,这两个进程pid是一样的
自己组装环境变量
我们刚刚说环境变量可以自己组装,来引用
看看吧:
test1.c:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("test1.c ...begin!\n");
pid_t id = fork();
if (id == 0)
{
sleep(2);
char *const argv[] = { (char*)"mypragma",NULL };
char* const envp[] = { (char*)"LIKE=521",(char*)"LOVE=1314",NULL };
execvpe("./mypragma", argv, envp);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father wait success,child exit code:%d\n", WEXITSTATUS(status));
}
printf("test1.c ... end!\n"); return 0;
}
mypragma.cpp:
#include<iostream>
using namespace std;
int main(int argc,char *argv[],char *env[])
{
int i = 0;
for (; argv[i]; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
printf("-------------------------------\n");
for (i = 0; env[i]; i++)
{
printf("env[%d]: %s\n", i, env[i]);
}
printf("-------------------------------\n");
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
cout << "hello C++!" << endl;
return 0;
}
哦?
父进程本身自己的环境变量,来自于父进程的父进程:bash
你也可以用全新的环境变量,也可以用修改后的环境变量:使用putenv()
往进程加环境变量: putenv() ,程序替换不会替换环境变量,我们可以通过带e的接口函数(例如execpe())设置新的环境变量,这会覆盖原本的环境变量。
用法:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
char *p;
if((p = getenv("USER")))
printf("USER =%s\n", p);
putenv("USER=test");
printf("USER+5s\n", getenv("USER"));
}
这些都是:
#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[]);
tips:只有execve是真正的系统调用,其它五个函数最终都调用 execve(execve在man手册第2节,其它函数在man手册第3节)