进程控制(详解)

一.进程创建

1.fork函数

在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进 程为⽗进程。

  • #include <unistd.h>
  • pid_t fork(void);
  • 返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1

进程调⽤fork,当控制转移到内核中的fork代码后,内核做:

  1.  分配新的内存块和内核数据结构给⼦进程
  2.  将⽗进程部分数据结构内容拷⻉⾄⼦进程
  3.  添加⼦进程到系统进程列表当中
  4. fork返回,开始调度器调度
  5. fork之后,谁先执⾏完 全由调度器决定

2.写时拷贝

当父进程创建子进程时fork,父进程首先要做的是,直接将父进程代码和数据对应页表项的权限,全部改成只读权限,然后子进程继承下来的页表也全部是只读权限,当子进程尝试通过代码对某些数据进行修改的时候,那么页表就立马识别到当前正在对只读区域进行写入操作,就会触发系统错误,触发系统错误的时候,系统就要进行判断,是真的错了,还是要进行写时拷贝,因为毕竟也会有野指针异常访问空间,系统错误后,就会触发缺页中断(后面学习会讲解,这里只需要了解就行),让系统去做检测,如果发现写入的区域是代码区,直接进行杀进程,但一旦发现写入的区域是数据区,就判定成发生写时拷贝,然后系统向物理内存申请空间,进行拷贝,修改页表映射,恢复权限。

要是写入时,这个内存空间不在物理内存里呢?

也是触发错误,发生缺页中断,系统检测,发生页表置换,从磁盘调入物理内存 

 写时拷贝就是时间换空间的做法!!!
为什么还要做一次拷贝呢?不能直接开辟空间吗?

因为你的写入操作!=对目标区域进行覆盖,也有可能会用到原有数据,比如:count++。

二.进程终止

main函数的返回值,是返回给父进程或者是系统,最终表示程序对还是不对!!

回顾一下查看上一次进程退出码的指令:echo $?查看上次进程的退出信息,命令行中,最近一次程序的退出码。退出码表示错误原因。

一般0表示成功,非0 表示错误,用不同的数字,约定或表明出错的原因,系统提供了一批错误码,也可以自己约定错误码。

errno表示获取错误码,而strerror则是将该错误码转换成字符串

数字是给系统看的,字符串是给用户看到

看下面代码,如果不存在该文件,打开一定是失败的,则会返回错误码errno,然后通过strerror将数字转换成字符串。

#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.

在两种系统分别执行下面代码查看:

#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>,但系统级头文件不行。

观察下面代码结果:

int main()
{printf("进程运行结束!");sleep(2);exit(23);sleep(2);return 0;
}

然后对比_exit,看看区别:

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,则直接会杀死进程,数据还在缓冲区内,没机会刷新。

三.进程等待

对于我们创建出来的子进程,作为父进程,必须等待这个子进程,因为是父进程创建的,就必须对子进程负责,对子进程进行回收,如果不回收,根据进程状态那一节内容,子进程就是变成僵尸进程,如果忘了可以进行回顾:进程的状态。

我们看下面代码,如果不进行回收,子进程就会变成僵尸:

#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.作用:等待任意一个子进程退出

#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:

#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:

#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:

#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:

#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就是用来提取这个返回值的。

#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.我们想让子进程帮我们去完成某种任务,举一个例子:让子进程进行备份操作:

这里我们就使用了自己约定的退出码,用枚举列出来,下面是阻塞式的备份操作:

#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表示不挂起)

父进程一边做自己的事情,一边等待子进程退出:

#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

#pragma once
#include <iostream>//打印日志任务
void PrintLog();//下载任务
void DownLoag();//备份任务
void BackUp();

task.cc

#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;
}

main.cc

#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,他的作用就是执行指定路径下的程序,执行方法为在命令行上输入的形式,也就是命令行怎么写,参数怎么传!!!

这种特性就叫做,进程的程序替换 

#include <iostream>
#include <cstdio>
#include <unistd.h>int main()
{execl("/usr/bin/top","top",nullptr);return 0;
}

就可以直接执行top指令!!!

1.程序替换是创建了新的进程吗?

不是!替换知识替换了代码和数据,然后改改页表的映射就可以了

验证问题:

1.没有创建新的进程,观看如下代码,其执行结果

other.c

#include <stdio.h>
#include <unistd.h>int main()
{printf("我是other进程,pid: %d\n",getpid());return 0;
}

main.c c

#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.

如下代码演示了失败的情况:

#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*系列进行加载,这些接口的本质就相当于把可执行程序加载进内存

如果替换自己本身,就会无线循环递归:

#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*系列的函数,就可以执行任何命令,这样既可以完成一个仿命令行解释器,原理就是这样:

#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);// childexecl("/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: 

#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,虽然内容一样,但是表达的含有不同

#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就是不用带路径

如下代码使用方式: 

#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;
}

 下面一种传参形式更加优雅:

#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来拿到的 

不传环境变量:

main.cc

#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 

#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;
}

程序替换不影响环境变量,因为具有全局性,可以被所以人看到。

下面我们看看来手动传环境变量:

#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;
}
#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,把父进程的环境变量传过去,这样子进程就能拿到追加后的环境变量。

#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!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/477110.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RFSOC 49dr 开发板,支持12收5发

硬件支持: 1,12收5发 2.4X25G光模块 3.J30J扩展接口 4.支持多板同步&#xff0c;多TILE同步

生产制造领域的多元化模式探索

在当今全球化和信息化的时代背景下&#xff0c;生产制造领域正经历着前所未有的变革。随着消费者需求的多样化、市场竞争的加剧以及技术的不断进步&#xff0c;传统的生产制造模式已经难以满足现代企业的需求。因此&#xff0c;多种生产制造模式应运而生&#xff0c;以适应不同…

基于YOLOv8深度学习的智慧农业果园果树苹果类果实目标检测系统(PyQt5界面+数据集+训练代码)

随着智慧农业技术的快速发展&#xff0c;果园管理逐渐向自动化和智能化方向迈进&#xff0c;传统的果园管理方式面临着高成本、效率低以及人工依赖程度大的挑战。在这种背景下&#xff0c;基于人工智能的目标检测技术为果园管理提供了一种全新的解决方案。本研究设计并实现了一…

【泥石流;风险;脆弱性;风险评估;川藏公路北线|论文解读4】川藏高速公路北线泥石流风险评估

【泥石流&#xff1b;风险&#xff1b;脆弱性&#xff1b;风险评估&#xff1b;川藏公路北线|论文解读4】川藏高速公路北线泥石流风险评估 【泥石流&#xff1b;风险&#xff1b;脆弱性&#xff1b;风险评估&#xff1b;川藏公路北线|论文解读4】川藏高速公路北线泥石流风险评…

mysql的优化

1、概念 在应用开发的初期&#xff0c;由于数据量较小&#xff0c;开发人员更重视功能上的实现&#xff0c;随着应用系统上线后&#xff0c;数据量急剧增长&#xff0c;很多性能问题逐渐显现&#xff0c;对使用的影响也越来越大&#xff0c;此时这些问题语句就称为整个系统的性…

栈的应用,力扣394.字符串解码力扣946.验证栈序列力扣429.N叉树的层序遍历力扣103.二叉树的锯齿形层序遍历

目录 力扣394.字符串解码 力扣946.验证栈序列 力扣429.N叉树的层序遍历 力扣103.二叉树的锯齿形层序遍历 力扣394.字符串解码 看见括号&#xff0c;由内而外&#xff0c;转向用栈解决。使用两个栈处理&#xff0c;一个用String,一个用Integer 遇到数字:提取数字放入到数字栈…

【Python系列】 Base64 编码:使用`base64`模块

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Mac 修改默认jdk版本

当前会话生效 这里演示将 Java 17 版本降低到 Java 8 查看已安装的 Java 版本&#xff1a; 在终端&#xff08;Terminal&#xff09;中运行以下命令&#xff0c;查看已安装的 Java 版本列表 /usr/libexec/java_home -V设置默认 Java 版本&#xff1a; 找到 Java 8 的安装路…

C++ STL - vector/list讲解及迭代器失效

vector 使用 vector 是一个动态数组. 构造/拷贝构造/赋值重载函数 int main() {// 是一个模板, 在实例化的时候, 需要指明类型std::vector<int> first; // 一个空的数组std::vector<int> second (4,100); // 设置初始空间大小为 4 个int, 全部初始化为 100std::v…

libphone desktop编译

linphone-desktop 在ubuntu20.04 下编译 linphone 介绍 Linphone是一款遵循GPL的开源网络视频电话系统&#xff0c;支持多种平台如Windows、Linux、Android等。它基于SIP协议&#xff0c;提供语音、视频通话及即时文本消息功能。核心功能包括SIP用户代理、音频视频Codec支持、…

根据已知站点寻找路网的最短路径

背景 接上期&#xff0c;基于MATSim的交通仿真&#xff0c;其中有一块非常重要的就是公交的仿真&#xff0c;这也是当初选择MATSim技术路线的一个重要原因&#xff0c;现在业务给出的场景是上传一些有序站点及其经纬度&#xff0c;需要通过算法来适配对应的路网&#xff0c;由…

Jenkins + gitee 自动触发项目拉取部署(Webhook配置)

目录 前言 Generic Webhook Trigger 插件 下载插件 ​编辑 配置WebHook 生成tocken 总结 前言 前文简单介绍了Jenkins环境搭建&#xff0c;本文主要来介绍一下如何使用 WebHook 触发自动拉取构建项目&#xff1b; Generic Webhook Trigger 插件 实现代码推送后&#xff0c;触…

leetcode 919.完全二叉树插入器

1.题目要求: 完全二叉树 是每一层&#xff08;除最后一层外&#xff09;都是完全填充&#xff08;即&#xff0c;节点数达到最大&#xff09;的&#xff0c;并且所有的节点都尽可能地集中在左侧。设计一种算法&#xff0c;将一个新节点插入到一棵完全二叉树中&#xff0c;并在…

SSL协议

文章目录 1. 前言2. 基础概念3. SSL协议结构3.1 概述3.2 SSL握手协议3.3 修改密码说明协议3.4 报警协议3.5 SSL记录协议 4. SSL安全性4.1 安全机制分析4.2 脆弱性分析 5. SSL证书 1. 前言 参考《应用系统安全基础》 2. 基础概念 安全套接字层协议&#xff08;Security Socke…

Flink-Source的使用

Data Sources 是什么呢&#xff1f;就字面意思其实就可以知道&#xff1a;数据来源。 Flink 做为一款流式计算框架&#xff0c;它可用来做批处理&#xff0c;也可以用来做流处理&#xff0c;这个 Data Sources 就是数据的来源地。 flink在批/流处理中常见的source主要有两大类…

Linux线程_线程控制_线程库

一.线程控制 在Linux操作系统的视角&#xff0c;Linux下没有真正意义上的线程&#xff0c;而是用进程模拟的线程&#xff08;LWP&#xff09;。所以&#xff0c;Linux不会提供直接创建线程的系统调用&#xff0c;而是提供创建轻量级进程的接口。但是由于用户只认线程&#xff0…

计算机网络:运输层 —— TCP 的超时重传机制

文章目录 TCP 的超时重传超时重传时间的选择重传策略与拥塞控制的关联 TCP 的超时重传 TCP 的超时重传是保证数据可靠传输的重要机制之一 保证数据可靠性&#xff1a;通过超时重传机制&#xff0c;即使在网络状况不佳&#xff0c;出现数据包丢失等情况时&#xff0c;也能够确保…

C嘎嘎探索篇:和stack,queue的相遇

C嘎嘎探索篇&#xff1a;和stack&#xff0c;queue的再次相遇 前言&#xff1a; 小编在前几日刚完成了关于list容器的介绍&#xff0c;中间由于我牙齿出现了问题所以断更了不少天&#xff0c;如今我牙齿已经恢复&#xff0c;我也要开始继续新内容的讲解了&#xff0c;各位读者…

GPTZero:高效识别AI生成文本,保障学术诚信与内容原创性

产品描述 GPTZero 是一款先进的AI文本检测工具&#xff0c;专为识别由大型语言模型&#xff08;如ChatGPT、GPT-4、Bard等&#xff09;生成的文本而设计。它通过分析文本的复杂性和一致性&#xff0c;判断文本是否可能由人类编写。GPTZero 已经得到了超过100家媒体机构的报道&…

MyBatis Plus 项目的创建和使用

1. 快速上手 1.1. 项目的创建和配置 首先&#xff0c;创建一个 Spring Boot 工程&#xff0c;添加 MyBatis Plus 和 MySQL 对应的依赖&#xff0c;然后&#xff0c;和 MyBatis 一样&#xff0c;需要在 yml 文件中配置数据库连接信息 <dependency><groupId>com.b…