(17)Linux的进程阻塞&&进程程序替换 && exec 函数簇
前言:本章我们讲解它的 options 参数。在讲解之前我们需要理解进程阻塞,然后我们重点讲解二进程程序替换,这是本章的重点,然后介绍一个进程替换函数 execl,通过介绍这个函数来打开突破口,引入进程创建的知识点。最后,我们在学习进程创建的 exec 函数簇。
一、进程阻塞(Process Blocking)
1、继续讲解 waitpid
?我们先来简单回顾一下上一章的内容:?
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
?上一章介绍了?status?参数,知道了如何通过位操作来截 status?获取进程错误码与错误信号:
status&0x7F // 获取错误信号
(status>>8)&0xFF // 获取错误码
但是我们还是不建议这么做,因为直接用操作系统提供的宏就好了,我们可以通过 WIFEXITED?宏来检测子进程是否正常退出(检测进程退出时信号是否为 0),在用 WEXITSTAUS?宏还获取进程的退出码:
if (WIFEEXITED(status))
{
printf("等待成功: exit code: %d\n", WIFEEXITED(status));
}
这些都是上一章讲解 status?参数的内容了,我们下面要讲的是 waitpid?另一个参数 options。?
当 options?为 0,则标识为 阻塞等待?
比如:如果子进程不退出,父进程在等,等的时候子进程是卡在那等的。
2、理解进程阻塞
思考:如何理解父进程进程阻塞??
首先,进程状态我们说过:如果一个进程在系统层面上要等待某件事情发生,
但这件事情还没发生,那么当前进程的代码还没法向后运行,只能让该进程处于阻塞状态。
就是让父进程的 task_struct 状态由 ,从运行队列投入到等待队列,等待子进程退出。
子进程退出的本质是条件就绪,如果子进程退出条件一旦就绪,操作系统会逆向地做上述工作。
将父进程的从等待队列再搬回运行队列,并将状态,此时父进程就会继续运行。
?3、轮询检测(Polling)
所谓的阻塞,其实就是挂起。在上层表现来看,就是进程卡住了?
而非阻塞式等待是会做些自己的事,而不是傻等!
多次调用非阻塞接口,这个过程我们称之为 轮询检测 (Polling)。
我们上一章中讲解 waitpid?时,举的例子都是 阻塞式 的等待。
如果我们想 非阻塞式 的等,我们可以设置 options?选项为?WNOHANG
?(With No Hang)。
这个选项通过字面很好理解,就是等待的时候不要给我挂 (Hang) 住,其实就是非阻塞!
现在我们正式介绍一下 waitpid?的返回值:
- 如果此时等待成功,返回值是子进程的退出码。
- 如果你是非阻塞等待 (
WNOHANG
),等待的子进程没有退出,返回值为 0。
4、 基于非阻塞的轮询等待(waitpid)
?如果我们想把我们上一章节,演示 waitpid?使用方式的代码,改为非阻塞等待。
我们只需要将 waitpid?的 options?参数加上。
代码演示:基于非阻塞的轮询等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t id = fork();
if (id == 0) {
// 子进程
while (1) {
printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());
sleep(5); // 先睡眠 5s,5s后退出
break;
}
exit(233);
}
else if (id > 0) {
// 父进程
/* 基于非阻塞的轮询等待方案 */
int status = 0;
while (1) {
pid_t ret = waitpid(-1, &status, WNOHANG);
if (ret > 0) { // 等待成功
printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);
}
else if (ret == 0) { // 等待成功,但是子进程没有退出
printf("父进程:子进程还没好,那我先做其他事情\n");
sleep(1);
}
else {
// 出错了,暂时不作处理
}
}
}
else {
// 什么也不做
}
}
说明:我们只需要将 waitpid 中的 options 参数带上 WHOHANG 就可以了。返回值 ret>0 就是等待成功,我们这里新增一个等于 0 的判断,作为 "等待成功但是子进程还没有退出" 的情况,因为等待的子进程没有退出,返回值为 0 。运行后,就会问子进程好没好,如果没有好父进程就可以做自己的事情了,这就是非阻塞式轮询等待。
运行结果如下:
我们可以让父进程在非阻塞等待时真正做点事?
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>
typedef void (* handler_t)(); // 函数指针类型
// 方法集
std::vector<handler_t> handlers;
void func1() {
printf("Hello,我是方法1\n");
}
void func2() {
printf("Hello,我是方法2\n");
}
void Load() {
// 加载方法
handlers.push_back(func1);
handlers.push_back(func2);
}
int main(void)
{
pid_t id = fork();
if (id == 0) {
// 子进程
while (1) {
printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());
sleep(3);
}
exit(233);
}
else if (id > 0) {
// 父进程
/* 基于非阻塞的轮训等待方案 */
int status = 0;
while (1) {
pid_t ret = waitpid(-1, &status, WNOHANG);
if (ret > 0) { // 等待成功
printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);
}
else if (ret == 0) { // 等待成功,但是子进程没有退出
printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n");
if (handlers.empty()) {
Load();
}
for (auto f : handlers) {
f(); // 回调处理对应的任务
}
sleep(1);
}
else {
// 出错了,暂时不作处理
}
}
}
else {
// 什么也不做
}
}
如果你想要你的程序直接父进程做更多的事情,把方法加到 Load?里就可以了。
写下 Makefile:
mytest:mytest.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mytest
运行结果如下:
二、 进程程序替换(Process Substitution)
1、 让子进程执行一个新的程序
我们之前做的所有代码演示,子进程执行的都是父进程的代码片段。
如果我们想让创建出来的子进程,执行全新的程序呢??
之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换。
那为什么要让子进程执行新的程序呢?
我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:
- ??? 让子进程执行父进程的代码片段(服务器代码…)
- ??? 想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)
2、程序替换原理?
程序替换的原理:
- 将磁盘中的内存,加载入内存结构。
- 重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。
- 效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序!
程序替换的本质
本质上就是去替换一个进程pcb在内存中的对应的代码和数据(加载新程序到内存——>更新页表信息——>初始化虚拟地址空间),然后这个进程pcb重新开始执行这个新的程序
这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!
因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。
?内核数据结构没有发生任何变化!?包括子进程的 , 都不变,说明压根没有创建新进程。
?以可变参数列表的接收参数的 execl 接口
我们要调用接口,让操作系统去完成这个工作 —— 系统调用。
如何进行程序替换?我们先见见猪跑 —— 从 execl?这个接口讲,看看它怎么跑的。
int execl(const char* path, const char& arg, ...);
如果我们想执行一个全新的程序,我们需要做几件事情:
- 第一件事情:先找到这个程序在哪里。
- 第二件事情:程序可能携带选项进行执行(也可以不携带)。
?简单来说就是:① 程序在哪?? ② 怎么执行?
所以,execl 这个接口就必须得把这两个功能都体现出来!
- ??? 它的第一个参数是 path,属于路径。
- ??? 参数? const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。
代码演示:exec()?
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("我是一个进程,我的PID是:%d\n", getpid());
// ls -a -l
execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 带选项
printf("我执行完毕了,我的PID是:%d\n", getpid());
return 0;
}
运行结果:
?运行最后就会执行ls命令execl("/usr/bin/ls","ls","-l","-a",NULL);
刚才是带选项的,现在我们再来演示一下不带选项的:?
execl("/usr/bin/top", "top", NULL); // 不带选项
运行结果如下:
这样我们的程序就直接能执行 top?命令了,除此之外,我们曾经学的大部分命令其实都可以通过 execl?执行起来。这就叫做 程序替换。
printf("我执行完毕了,我的PID是:%d\n", getpid());
这句话为什么没有打印出来??
为什么我们最后的代码并没有被打印出来?
因为 一旦替换成功,是会将当前进程的代码和数据全部替换的!
所以自然后面的 printf 代码早就被替换了,这意味着该代码不复存在了,荡然无存!
因为在程序替换的时候,就已经把对应进程的代码和数据替换掉了!
而第一个 printf 执行了的原因自然是因为程序还没有执行替换,
所以,这里的程序替换函数用不用判断返回值?为什么?
int ret = execl(...);
一旦替换成功,还会执行返回语句吗?返回值有意义吗? 没有意义的!
?程序替换不用判断返回值!因为只要成功了,就不会有返回值。 而失败的时候,必然会继续向后执行。通过返回值最多能得到是什么原因导致替换失败的。只要执行了后面的代码,看都不用看,一定是替换失败了;只要有返回值,就一定是替换失败了。
我们来模拟一下失败的情况,我们来执行一个不存在的指令 :
execl("/usr/bin/lssssss", "ls", "-l", "-a", NULL); // 带选项
execl?替换失败,就会继续向后执行。但是,一旦 execl?成功后就会跟着新程序的逻辑走,就不会再 return?了,再也不回来了,所以返回值加不加无所谓了。
3、引入进程创建?
以前我们的示例都是让子进程执行父进程的代码,我们今天想让子进程执行自己的程序。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(void)
{
printf("我是父进程,我的PID是: %d\n", getpid());
pid_t id = fork();
if (id == 0) {
/* child
我们想让子进程执行全新的程序 */
printf("我是子进程,我的PID是:%d\n", getpid());
execl("/usr/bin/ls", "ls", "-a", "-l", NULL); /* 让子进程执行替换 */
exit(1); /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/
}
/* 一定是父进程 */
int status = 0;
int ret = waitpid(id, &status, 0);
if (ret == id) {
/* 等待成功 */
sleep(2);
printf("父进程等待成功!\n");
}
return( 0);
}
?运行结果:
成功执行代码,父进程也等待成功了。这里的子进程没有执行父进程的代码,而是执行了自己的程序。
子进程执行程序替换,会不会影响父进程呢?不会!因为进程具有独立性。
当程序替换的时候,我们可以理解成 —— 代码和数据都发生了写时拷贝,完成了父子分离。
三、exec 函数簇(Sheaf of functions exec)
1、以指针数组接收参数的 execv 接口
刚才我们学会了 execl?接口,我们下面开始学习更多的 exec?接口!它们都是用来替换的。
下面我们先来讲解一下和?execl?很近似的 execv:
int execv(const char* path, char* const argv[]);
path 参数和 execl 一样,关注的都是 "如何找到"
argv[] 参数关注的是 "如何执行",是个指针数组,放 char* 类型,指向一个个字符串。
大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。
所以 execv 和 execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。
值得注意的是,在构建 argv[] 的时,结尾仍然是要加上 NULL!
代码:execv()
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#define NUM 16
int main()
{
pid_t id=fork();
if(id==0){
//子进程
//ls -a -l
printf("子进程开始运行,pid: %d\n",getpid());
sleep(3);
char* const _argv[NUM]={
(char*)"ls",
(char*)"-l",
(char*)"-a",
NULL
};
//execl("/usr/bin/ls","ls","-a","-l",NULL);
execv("/usr/bin/ls",_argv);
exit(1);
}
else{
//父进程
int status=0;
printf("父进程开始运行,pid: %d\n",getpid());
pid_t id=waitpid(-1,&status,0);//阻塞等待
if(id>0){
printf("wait success,exit code: %d\n",WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
2、无需带路径就能直接执行的 execlp 接口(可变参数列表)
int execlp(const char* file, const char* arg, ...);
execlp,它的作用和 execv、execl?是一样的,它的作用也是执行一个新的程序。
仍然是需要两步:① 找到这个程序? ?② 告诉我怎么执行
第一个参数 file?也是 "你想执行什么程序",第二个参数 arg?是 "如何去执行它"。
所以这一块的参数传递,和 execl?是一样的,唯一的区别是比 execl?多了一个 p!
我们执行指令的时候,默认的搜索路径在环境变量??中,所以这个 p?的意思是环境变量。
这意味着:执行 execlp?时,会直接在环境变量中找,不用去输路径了,只要程序名即可。
execlp("ls", "ls", "-a", "-l", "NULL"); // 路径都不用,直接扔
?这里出现的两个 ls?含义是不一样的,不可以省略
代码演示:execlp()
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#define NUM 16
int main()
{
pid_t id=fork();
if(id==0){
//子进程
//ls -a -l
printf("子进程开始运行,pid: %d\n",getpid());
sleep(3);
char* const _argv[NUM]={
(char*)"ls",
(char*)"-l",
(char*)"-a",
NULL
};
//execl("/usr/bin/ls","ls","-a","-l",NULL);
//execv("/usr/bin/ls",_argv);
execlp("ls","ls","-l","-a",NULL);
exit(1);
}
else{
//父进程
int status=0;
printf("父进程开始运行,pid: %d\n",getpid());
pid_t id=waitpid(-1,&status,0);//阻塞等待
if(id>0){
printf("wait success,exit code: %d\n",WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
?3、无需带路径的 execvp 接口(指针数组)
int execvp(const char* file, char* const argv[]);
execvp?也是带 p?的,执行 execvp?时,会直接在环境变量中找,只要程序名即可。
代码
char* const _argv[NUM]={
(char*)"ls",
(char*)"-l",
(char*)"-a",
NULL
};
execvp("ls", _argv);
运行结果:
?目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的 C/C++ 程序呢?
4、利用 exec?调各种程序
假设有两个可执行程序:mycmd.c & exec.c,我们期望用 exec.c?调用 mycmp.c:
//mycmd.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char *argv[])
{
if(argc!=2){
printf("can not execute!\n");
exit(1);
}
if(strcmp(argv[1],"-a")==0){
printf("helllo a!\n");
}
else if(strcmp(argv[1],"-b")==0){
printf("hello b!\n");
}
else{
printf("default!\n");
}
return 0;
}
也就是 C 语言的可执行程序调用 C++ 的可执行程序,我们先来设计一下 Makefile。
我们需要在前面添加 .PHONY:all ,让伪目标 all 依赖 exec 和 mycmd。
如果不这样做,直接写,默认生成的是 mycmd,轮不到后面的 exec,属于 "先到先得"。
且 Makefile 默认也只能形成一个可执行程序,想要形成多个就需要用到 all 了。
?
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#define NUM 16
const char *myfile="/home/amx/lesson3/mycmd";
int main()
{
pid_t id=fork();
if(id==0){
//子进程
//ls -a -l
printf("子进程开始运行,pid: %d\n",getpid());
sleep(3);
char* const _argv[NUM]={
(char*)"ls",
(char*)"-l",
(char*)"-a",
NULL
};
execl(myfile,"mycmd","-a",NULL);
//execv("/usr/bin/ls",_argv);
//execlp("ls","ls","-l","-a",NULL);
exit(1);
}
else{
//父进程
int status=0;
printf("父进程开始运行,pid: %d\n",getpid());
pid_t id=waitpid(-1,&status,0);//阻塞等待
if(id>0){
printf("wait success,exit code: %d\n",WEXITSTATUS(status));
}
}
return 0;
}
?
?运行结果:
这是用的是绝对路径,那我们使用相对路径可以吗?
结果:
依然可以!!!!
那么如何 执行其它语言的程序呢??
我们创建两个文件
准备工作:先给两个文件写一些内容
试试能否运行:
都没问题:
那么我们修改我们的exec.c文件
运行结果:
完美!!!
运行test.sh也是一样!!!
运行结果:
还有一种方法:
修改:exec.c
一样可以!!
5、添加环境变量给目标进程的?execle 接口
?
int execle(const char* path, const char* arg, ..., char* const envp[]);
我们可以使用 execle?接口传递环境变量,相当于自己把环境变量导进去。
打开 mycmd?文件,我们加上几句环境变量:
?我们自己在exec.c中定义一个
传进去:
?我们来试一下:
?超级缝合怪 execvpe 接口
v - 数组,p - 文件名,e - 可自定义环境变量:
int execvpe(const char* file, char* const argv[], char* const envp[]);
这也没什么好说的,execle、execve、execvpe 都是 "环境变量" 一伙的。
6、为什么会有这么多 exec 接口?
唯一的差别就是传参的方式不一样,有的带路径,有的不带路径,有的是列表传参,有的是数组传参,有的可带环境变量,有的不带环境变量。
因为要适配各种各样的应用场景,使用的场景不一样,有些人就喜欢列表传参,有些人喜欢数组传参。所以就配备了这么多接口,这就好比我们 C++ 函数重载的思想。
那为什么 execve?是单独的呢?
int execve(const char* file, char* const argv[], char* const envp[]);
?它处于 man 2 号手册,execve?才属于是真正意义上的系统调用接口。
?总结一下它们的命名规律,通过这个来记忆对应接口的功能会好很多:
- l (list) :表示参数采用列表形式
- v (vector) :表示参数采用数组形式
- p (path):有 p 自动收缩环境变量 PATH
- e (env) :表示自己维护环境变量
感谢观看!!!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!