一.进程创建
1.fork函数
在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进 程为⽗进程。
- #include <unistd.h>
- pid_t fork(void);
- 返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调⽤fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给⼦进程
- 将⽗进程部分数据结构内容拷⻉⾄⼦进程
- 添加⼦进程到系统进程列表当中
- fork返回,开始调度器调度
- fork之后,谁先执⾏完 全由调度器决定
2.写时拷贝
当父进程创建子进程时fork,父进程首先要做的是,直接将父进程代码和数据对应页表项的权限,全部改成只读权限,然后子进程继承下来的页表也全部是只读权限,当子进程尝试通过代码对某些数据进行修改的时候,那么页表就立马识别到当前正在对只读区域进行写入操作,就会触发系统错误,触发系统错误的时候,系统就要进行判断,是真的错了,还是要进行写时拷贝,因为毕竟也会有野指针异常访问空间,系统错误后,就会触发缺页中断(后面学习会讲解,这里只需要了解就行),让系统去做检测,如果发现写入的区域是代码区,直接进行杀进程,但一旦发现写入的区域是数据区,就判定成发生写时拷贝,然后系统向物理内存申请空间,进行拷贝,修改页表映射,恢复权限。
要是写入时,这个内存空间不在物理内存里呢?
也是触发错误,发生缺页中断,系统检测,发生页表置换,从磁盘调入物理内存
写时拷贝就是时间换空间的做法!!!
为什么还要做一次拷贝呢?不能直接开辟空间吗?
因为你的写入操作!=对目标区域进行覆盖,也有可能会用到原有数据,比如:count++。
二.进程终止
main函数的返回值,是返回给父进程或者是系统,最终表示程序对还是不对!!
回顾一下查看上一次进程退出码的指令:echo $? 。 查看上次进程的退出信息,命令行中,最近一次程序的退出码。退出码表示错误原因。
一般0表示成功,非0 表示错误,用不同的数字,约定或表明出错的原因,系统提供了一批错误码,也可以自己约定错误码。
errno表示获取错误码,而strerror则是将该错误码转换成字符串
数字是给系统看的,字符串是给用户看到
看下面代码,如果不存在该文件,打开一定是失败的,则会返回错误码errno,然后通过strerror将数字转换成字符串。
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
int main()
{
printf("before: errno: %d,errstring: %s\n", errno, strerror(errno));
FILE *fp = fopen("./log.txt", "r");
if (fp == nullptr)
{
printf("after: errno: %d,errstring: %s\n", errno, strerror(errno));
return errno;
}
return 0;
}
所有再用echo $?查看也是与errno一样。
在Linux下系统错误码有0-133,Windows下有0-140.
在两种系统分别执行下面代码查看:
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
int main()
{
for(int i = 0;i<200;i++)
{
std::cout<<"code: "<<i<<", errstring: "<<strerror(i)<<std::endl;
}
return 0;
}
1.进程退出的时候,main函数结束代表进程退出,mian函数返回值代表进程所对应的退出码。
2.进程退出码可以由系统默认的错误码来提供,也可以约定自己的退出码。
1.进程终止的方式
1.main函数return
2.进程调用exit,exit在代码任何地方,表示进程结束,非mian函数的return,只表示函数结束,exit表进程结束
3._exit,系统接口,也是终止进程
补充:系统级头文件都是**.h**.语言级头文件,比如<stdio.h>,在c++中可以写成<cstdio>,但系统级头文件不行。
观察下面代码结果:
cpp
int main()
{
printf("进程运行结束!");
sleep(2);
exit(23);
sleep(2);
return 0;
}
然后对比_exit,看看区别:
cpp
int main()
{
printf("进程运行结束!");
sleep(2);
_exit(23);
sleep(2);
return 0;
}
结论:语言级exit会把打印从缓冲区刷新出来,再结束进程,而系统级_exit则不会把打印从缓冲区刷新出来,直接结束进程。
1.刷新缓冲区的区别
2.上下层的区别
我们知道语言级函数,往往是封装系统调用接口,更接近上层,
我们试想一下我们之前认为的缓冲区在哪个位置?
这个缓冲区一定不在OS内部,因为如果在OS内部那么printf打印的信息也一定在OS中,所不管调用哪个函数,都会刷新缓冲区,所有这个缓冲区一定不在OS中。
结论:这个缓冲区是语言级缓冲区,由C/C++提供的!!!
调用exit,fflush从语言层把缓冲区内容刷新到OS中,在刷新到屏幕上。
调用_exit,则直接会杀死进程,数据还在缓冲区内,没机会刷新。
三.进程等待
对于我们创建出来的子进程,作为父进程,必须等待这个子进程,因为是父进程创建的,就必须对子进程负责,对子进程进行回收,如果不回收,根据进程状态那一节内容,子进程就是变成僵尸进程,如果忘了可以进行回顾:进程的状态。
我们看下面代码,如果不进行回收,子进程就会变成僵尸:
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 10;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
1.wait
学习两个回收子进程函数:
1.先看wait,一般而言,父进程创建子进程就要等待子进程,直到结束
wait
1.回收子进程的僵尸状态
2.等待的时候子进程,如果不退,父进程就要一直阻塞在wait内部,类似scanf。
3.返回值,大于0,表示成功回收子进程,小于0,表示回收失败
4.等待期间,父进程阻塞式等待,等待成功一般返回子进程pid
5.作用:等待任意一个子进程退出
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
sleep(10);
pid_t rid = wait(nullptr);
if(rid > 0)
{
printf("wait sub processon,rid: %d\n",rid);
}
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
如图结果,直接回收僵尸状态。
2.waitpid
子进程退出了,想不想知道,子进程把任务完成的怎么样,运行的怎么样,你怎么知道子进程把任务完成的怎么样呢?
父进程要知道子进程的退出信息,想办法获取子进程的退出信息,比如:退出码。父进程不光要回收子进程,还要知道子进程运行结果对还是不对。
1.如果pid > 0,指定一个进程,pid == -1,表示任意一个进程
2.status表明子进程的退出信息,帮助父进程获取子进程的退出信息,OS把子进程PCB中退出信息写入这里,是一个输出型参数
3.如果options == 0,则阻塞式等待,options == WNOHANG,表示非阻塞式等待
补充:输入型参数,是数据进入函数的通道,输出型参数,是函数将结果传出来的通道。
等待错误的子进程,就是传存在进程的id:
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
sleep(10);
pid_t rid = waitpid(id+1,nullptr,0);// ==wait(nullptr)
if(rid > 0)
{
printf("wait sub processon,rid: %d\n",rid);
}
else
{
perror("waitpid");
}
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
子进程一直会是僵尸状态。
正常等待进程,传正确的pid:
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
else
{
sleep(10);
int status = 0;
pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
if(rid > 0)
{
printf("wait sub processon,rid: %d,status: %d\n",rid,status);
}
else
{
perror("waitpid");
}
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
这里我们会发现我们用的exir(1),不应该是错退出码是1吗,为什么是256?
因为status里面不仅包含了程序的退出码还包含退出信号。
因为进程不光会正常结束,还会异常结束,异常结束,OS就会提前使用信号杀死进程,这时进程退出信息中就会记录退出信号,是因为什么导致的退出。
status,不是一个完整的整数,他是一个位图,32个比特位,只考虑低16位,次8位是退出码,地位是退出信号。
如果想获取退出码,就要 右移8位,然后&上0xFF:
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
else
{
sleep(10);
int status = 0;
pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
if(rid > 0)
{
printf("wait sub processon,rid: %d,status: %d\n",rid,(status>>8)&0xFF);
}
else
{
perror("waitpid");
}
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
这样就可以获取到正确的退出码。
如果想要知道退出信号呢?我们信号有哪些?
我们可以看到为什么没有0号信号,因为0代表成功退出,不是异常退出。
如果想要看知道退出型号,status&0x7F:
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
else
{
sleep(10);
int status = 0;
pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
if(rid > 0)
{
printf("wait sub processon,rid: %d,status: %d, exit signal: %d\n",rid,(status>>8)&0xFF,status&0x7F);
}
else
{
perror("waitpid");
}
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
1.子进程退出,可不可以使用全局变量,来获取子进程的退出码呢?
不行,因为全局数据一修改,就会发生写时拷贝,父进程看不见,因为进程具有独立性,地址一样,内容不同。
这就是为什么我们只能通过系统调用接口来获取退出信息,系统调用waitpid的时候,他是OS提供的接口,OS帮我们拿到子进程的PCB中的退出信息,通过status给我们返回。
重新谈进程退出:
1.代码跑完,结果对,return 0;
2.代码跑完,结果不对,return !0;
3.进程异常退出,OS提前使用信号终止了你的进程,进程PCB退出信息中会记录退出信号
status不光会获取退出码,又会获取退出信号,一般想看到进程结果是否正确,前提是这个进程退出信号为0,没有收到信号,证明这个代码是正常跑完的,结果是对还是不对,我们通过退出码来进一步判断。
1.创建子进程,不关注子进程退出码退出结果,调用waitpid,传nullptr给status
2.关注退出码退出结果,调用waitpid,通过status得知子进程退出信息
我们来看看Linux内核中,进程PCB是否有退出码和退出信号:
这里我们认识两个宏:WIFEXITED,WEXITSTATUS。
WIFEXITED:用于判断子进程是否正常终止。如果子进程是通过调用exit函数或从main函数中正常返回而终止的,那么WIFEXITED返回非零值(通常为 1);否则,如果子进程是由于收到信号等异常原因而终止的,WIFEXITED返回 0 。
WEXITSTATUS:当WIDEXITED返回非零值,即确定子进程是正常终止时,WEXITSTATUS用于获取子进程的返回值。子进程在正常退出时可以通过exit函数传递一个返回值给父进程,WEXITSTATUS就是用来提取这个返回值的。
cpp
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(1);
}
else
{
sleep(10);
int status = 0;
pid_t rid = waitpid(id,&status,0);// ==wait(nullptr)
if(rid > 0)
{
if(WIFEXITED(status))//如果成功退出
{
printf("wait sub processon,rid: %d,status: %d\n",rid,WEXITSTATUS(status));
}
else//异常退出
{
printf("child process quit error!\n");
}
}
else
{
perror("waitpid");
}
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
1.我们想让子进程帮我们去完成某种任务,举一个例子:让子进程进行备份操作:
这里我们就使用了自己约定的退出码,用枚举列出来,下面是阻塞式的备份操作:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
enum{
OPEN_FILE_ERROR = 1,
OK = 0,
};
const std::string gsep = " ";
std::vector<int> data;
int SaveBegin()
{
std::string name = std::to_string(time(nullptr));
name += ".backup";
//打开文件
FILE *fp = fopen(name.c_str(),"w");
//判断文件打开是否失败
if(fp == nullptr) return OPEN_FILE_ERROR;
//文件打开成功 ,备份
std::string dataStr;//储存vector里面值
for(auto d: data)
{
dataStr += std::to_string(d);
dataStr += gsep;//加空格符
}
//把dataStr数据,放入fp文件中
fputs(dataStr.c_str(),fp);
//关闭文件
fclose(fp);
return OK;
}
void Save()
{
pid_t id = fork();
if(id == 0)//child
{
//备份任务
int code = SaveBegin();
exit(code);
}
//父进程
int status = 0;
//等待回收子进程
pid_t rid = waitpid(id,&status,0);
if(rid > 0)//成功退出
{
int code = WEXITSTATUS(status);//获取退出码
if(code == 0) printf("备份成功!,exit code: %d\n",code);
else printf("备份失败!,exit code: %d\n",code);
}
else//异常退出
{
perror("waitpid");
}
}
int main()
{
int cnt = 1;
while(true)
{
data.push_back(cnt++);
sleep(1);
if(cnt % 10 == 0)
{
Save();
}
}
return 0;
}
非阻塞轮询调度,这里给waitpid第三个参数传WNOHANG(W表示wait,NO HANG表示不挂起)
父进程一边做自己的事情,一边等待子进程退出:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0) // 子进程
{
while (true)
{
printf("我是子进程,pid: %d\n", getpid());
sleep(1);
}
exit(0);
}
// 父进程
while (true)
{
sleep(1);
pid_t rid = waitpid(id, nullptr, WNOHANG);
if(rid > 0)
{
printf("等待子进程%d成功\n",rid);
break;
}
else if (rid < 0)
{
printf("等待子进程失败\n");
break;
}
else
{
printf("子进程尚未退出\n");
//父进程做自己的事情
printf("我是父进程!\n");
}
}
}
waitpid返回值:
1.如果大于0,等待成功,返回目标子进程的pid
2.如果等于0,阻塞等待一般不会返回这,在非阻塞等待中,表示等待成功,但子进程还没有退
3.如果小于0,等待失败
非阻塞式等待,让父进程进行完成任务:
task.h
cpp
#pragma once
#include <iostream>
//打印日志任务
void PrintLog();
//下载任务
void DownLoag();
//备份任务
void BackUp();
cpp
#include "task.h"
//打印日志任务
void PrintLog()
{
std::cout << "Print Log task" << std::endl;
}
//下载任务
void DownLoag()
{
std::cout << "DownLoad task" << std::endl;
}
//备份任务
void BackUp()
{
std::cout << "BackUp task" << std::endl;
}
cpp
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "task.h"
typedef std::function<void()> task_t;
// using task_t = std::function<void()>;
void LoadTask(std::vector<task_t> &tasks)
{
tasks.push_back(PrintLog);
tasks.push_back(DownLoag);
tasks.push_back(BackUp);
}
int main()
{
// 任务列表
std::vector<task_t> tasks;
LoadTask(tasks); // 加载任务
pid_t id = fork();
if (id == 0) // 子进程
{
while (true)
{
printf("我是子进程,pid: %d\n", getpid());
sleep(1);
}
exit(0);
}
// 父进程
while (true)
{
sleep(1);
pid_t rid = waitpid(id, nullptr, WNOHANG);
if (rid > 0)
{
printf("等待子进程%d成功\n", rid);
break;
}
else if (rid < 0)
{
printf("等待子进程失败\n");
break;
}
else
{
printf("子进程尚未退出\n");
// 父进程做自己的事情
for (auto &task : tasks)
{
task();
}
}
}
}
四.进程程序替换
1.快速见一见
在这章内容前面写的代码,子进程执行的永远是父进程代码的一部分,如果想执行新的程序呢?该怎么办呢?
使用exec系列函数。
1.execl
先看execl,他的作用就是执行指定路径下的程序,执行方法为在命令行上输入的形式,也就是命令行怎么写,参数怎么传!!!
这种特性就叫做,进程的程序替换
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
int main()
{
execl("/usr/bin/top","top",nullptr);
return 0;
}
就可以直接执行top指令!!!
1.程序替换是创建了新的进程吗?
不是!替换知识替换了代码和数据,然后改改页表的映射就可以了
验证问题:
1.没有创建新的进程,观看如下代码,其执行结果
other.c
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是other进程,pid: %d\n",getpid());
return 0;
}
main.c c
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
int main()
{
printf("我是myexec,pid: %d\n",getpid());
execl("./other","other",nullptr);
//execl("/bin/ls","ls","-1","-a","-n",nullptr);
//execl("/usr/bin/top","top",nullptr);
printf("hello");
return 0;
}
如果没有创建新的进程,二者打印的pid相同,否则pid不同,如上结果,所有程序替换没有创建新的进程,但执行了不同的程序。
细心的人会观察到,为什么没有打印hello呢?
因为上面也说了,程序替换的本质就是覆盖原来代码和数据,原来代码数据被覆盖,自然而然就没法打印。
2.execl返回值
成功就没有返回值,因为后面代码数据都被覆盖了所以不可能有返回值,失败了就没覆盖成功,返回-1.
如下代码演示了失败的情况:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
int main()
{
printf("我是myexec,pid: %d\n",getpid());
//execl("./other","other",nullptr);
int n = execl("/bin/lsssssss","ls","-1","-a","-n","--color",nullptr);
printf("execl return val: %d\n",n);
//execl("/usr/bin/top","top",nullptr);
printf("hello");
return 0;
}
这意味着,只要返回,就是失败,exit也有返回值,但因为直接进程终止,不需要考虑。
OS把程序加载进内存中,所有OS肯定会给留系统调用,所以可以通过exec*系列进行加载,这些接口的本质就相当于把可执行程序加载进内存。
如果替换自己本身,就会无线循环递归:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
int main()
{
printf("我是myexec,pid: %d\n",getpid());
//execl("./other","other",nullptr);
//int n = execl("/bin/ls","ls","-1","-a","-n","--color",nullptr);
int n = execl("./myexec","myexec",nullptr);
printf("execl return val: %d\n",n);
//execl("/usr/bin/top","top",nullptr);
printf("hello");
return 0;
}
再来看下面代码,给下面代码套上一个死循环,每次获取命令行上输入的指令,然后fork,让子进程执行输入的字符串,灵活调用exec*系列的函数,就可以执行任何命令,这样既可以完成一个仿命令行解释器,原理就是这样:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(3);
// child
execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
在Linux中,所有的进程都是由父进程创建的,比如登陆Linux系统,在命令行上执行命令,全部都是shell的子进程,所有系统是怎么把我们程序跑起来的呢?先fork,然后做程序替换,不就可以把我们程序跑起来了吗。
所以之前的问题,是先有数据结构还是先有代码数据?
因为你所有的程序都是子进程,得先是一个子进程,所以先创建PCB,怎么创建PCB,fork,
fork的时候, 不就没有代码没有数据吗,是父进程代码数据,fork之后,先把PCB创建出来,想运行你自己的程序,直接使用exec*系列的函数,不久有了一个新的进程吗,
所以Linux下,所有的软件,都是fork,然后exec*系列跑起来的,
所有有这个思想,既然能跑起来我们的程序,那么创建python,java程序一样可以调用起来。
父进程fork,子进程调用exec*系列函数替换程序,就要发生写时拷贝,所以独立开了,进程就可以彻底独立
堆和栈,进程程序替换,如果历史上用过,系统会,重新把堆栈初始化,恢复到最开始,如果没使用,就不会和父进程干扰。
2.execv
execv与execl区别,把第二个往后的参数放入指针数组里面,argv是不是很熟悉,就是之前命令行参数里面讲的参数列表,命令行参数。
看看下面代码,看看如何使用execv:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
char *const argv[] = {
(char *)"ls",//因为是字符串常量,强转一下避免警告
(char *)"--color",
(char *)"-a",
(char *)"-l",
nullptr
};
execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
execv与execl中,l代表list,v代表vector
我们程序必须从新的程序的mian函数开始执行,都要从main函数开始传递参数,所以命令行参数是怎么传递给main函数的呢,可以通过调用execv,自己的程序是先fork,然后exec*创建出来的,命令行给我们构建出对应的数组,然后可以通过execv把函数传递给我们自己程序的main函数,execv不是系统调用吗,系统可以帮我们找到main函数,把参数传递过去。
execl函数后面的可变参数,这个函数内部会自动把这些参数转换成argv这个表,顺便可以把个数也统计出来
3.execlp
根据上面俩个函数,可以知道,有l说明是可变参数,那么p又是什么呢?
p代表的是,不用带路径,他会自动根据环境变量PATH中的路径中去找对应的命令
第一个参数永远是你想执行谁,后面参数永远是你想怎么执行!!如下代码,重复两个ls,虽然内容一样,但是表达的含有不同。
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
execlp("ls","ls","--color","-a","-n","-l",nullptr);
//execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
4.execvp
根据上面讲的,v就是传参数列表,p就是不用带路径
如下代码使用方式:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
char *const argv[] = {
(char *)"ls",//因为是字符串常量,强转一下避免警告
(char *)"--color",
(char *)"-a",
(char *)"-l",
nullptr
};
execvp("ls",argv);
//execlp("ls","ls","--color","-a","-n","-l",nullptr);
//execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
下面一种传参形式更加优雅:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
char *const argv[] = {
(char *)"ls",//因为是字符串常量,强转一下避免警告
(char *)"--color",
(char *)"-a",
(char *)"-l",
nullptr
};
execvp(argv[0],argv);
//execlp("ls","ls","--color","-a","-n","-l",nullptr);
//execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
5.execvpe
这里的e又是什么意思呢?
这里的e表示传环境变量env。环境变量
子进程在创建的时候即使不跟我们传环境变量,也是会被子进程拿到,通过全局指针environ来拿到的
不传环境变量:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
execl("./other","other",nullptr);
//execvp(argv[0],argv);
//execlp("ls","ls","--color","-a","-n","-l",nullptr);
//execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
other.c
cpp
#include <stdio.h>
#include <unistd.h>
extern char**environ;
int main()
{
for(int i = 0;environ[i];i++)
{
printf("evn[%d]: %s\n",i,environ[i]);
}
return 0;
}
程序替换不影响环境变量,因为具有全局性,可以被所以人看到。
下面我们看看来手动传环境变量:
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//由子进程执行程序
int main()
{
pid_t id = fork();
if (id == 0)
{
char *const argv[] = {
(char *)"other",
// (char *)"ls",//因为是字符串常量,强转一下避免警告
// (char *)"--color",
// (char *)"-a",
// (char *)"-l",
nullptr
};
char * const env[] = {
(char*)"HELLO=bite",
(char*)"HELLO1=bite1",
(char*)"HELLO2=bite2",
(char*)"HELLO3=bite3",
nullptr
};
execvpe("./other",argv,env);
//execl("./other","other",nullptr);
//execvp(argv[0],argv);
//execlp("ls","ls","--color","-a","-n","-l",nullptr);
//execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
cpp
#include <stdio.h>
#include <unistd.h>
extern char**environ;
int main()
{
for(int i = 0;environ[i];i++)
{
printf("evn[%d]: %s\n",i,environ[i]);
}
return 0;
}
使用execvp进行手动去传环境变量 ,他的意思是使用全新的环境变量,传递给目标程序,而不是追加传递
关于环境变量:
1.让子进程继承父进程全部的环境变量(默认)
2.如果要使用全新的环境变量(自己定义传递)
3.如果要追加传递呢?
我们回顾一下获取环境变量有一个方式是getenv, 还有一个增加环境变量的方式putenv。
如下代码,在全局自定义一个环境变量myenv,然后定义一个全局指针environ,使用putenv把该变量追加到环境变量中,然后使用execvpe,通过传environ,把父进程的环境变量传过去,这样子进程就能拿到追加后的环境变量。
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
const std::string myenv = "HELLO=AAAAAAAAAAAAAAA";
extern char **environ;
//由子进程执行程序
int main()
{
putenv((char*)myenv.c_str());
pid_t id = fork();
if (id == 0)
{
char *const argv[] = {
(char *)"other",
// (char *)"ls",//因为是字符串常量,强转一下避免警告
// (char *)"--color",
// (char *)"-a",
// (char *)"-l",
nullptr
};
char * const env[] = {
(char*)"HELLO=bite",
(char*)"HELLO1=bite1",
(char*)"HELLO2=bite2",
(char*)"HELLO3=bite3",
nullptr
};
execvpe("./other",argv,environ);
//execl("./other","other",nullptr);
//execvp(argv[0],argv);
//execlp("ls","ls","--color","-a","-n","-l",nullptr);
//execv("/usr/bin/ls",argv);
// child
//execl("/bin/ls", "ls","-1","-a","-n","--color", nullptr);
//走到这一定是失败的
exit(1);
}
pid_t rid = waitpid(id,nullptr,0);
if(rid > 0)
{
printf("等待子进程成功!\n");
}
return 0;
}
putenv返回值,如果成功返回0,如果失败返回非0值,并设置错误码来提示
结论:程序替换不影响环境变量和命令行参数
6.execle和execve
认识上面知识后,就可以轻易知道这两个函数怎么使用。
execle,传可变参数,要传路径,要传环境变量。
execve,传参数列表,传路径,传环境变量。
补充:
除了了execve是系统调用接口,其他的都是语言级接口,底层都是封装的execve!!!