Linux进程控制

进程控制

本文已收录至《Linux知识与编程》专栏!
作者:ARMCSKGT
演示环境:CentOS 7

在这里插入图片描述



前言

进程并非只能创建,创建后进行合理的管理才能更好的利用进程,仅仅依靠操作系统的管理是不够的,所以本节将介绍关于进程控制的相关知识,让我们更合理的使用进程!
在这里插入图片描述


正文

本文将重温fork进程创建,学习进程终止和进程等待,进程替换相关知识!

进程创建


fork函数

进程的创建需要fork函数,关于此函数:

#include <unistd.h> //系统库文件
pid_t fork(void); //函数声明格式
//fork函数无参数
//返回值(pid_t类型):子进程中返回0,父进程返回子进程pid,出错返回-1

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程!

关于fork的返回值:

  • 父进程中fork返回子进程的PID
  • 子进程中fork返回0
  • 如果父进程中创建子进程出错则给父进程返回-1

fork创建失败的原因:

  • 系统资源紧张
  • 系统中的进程过多
  • 用户所需进程超过系统限制

fork使用场景:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。(例如:父进程等待客户端请求,生成子进程来处理请求。)
  • 一个进程要执行一个不同的程序。(例如:子进程从fork返回后,需要使用exec进程替换函数执行控制台清空程序。)

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

  • 分配新的内存块和内核数据结构(PCB)给子进程
  • 将父进程部分数据结构内容拷贝至子进程(包括环境变量表)
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度进程运行

fork函数
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们的运行状态处于相同的地方。但每个进程都将可以开始它们自己的旅程!

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
 cout<<"父进程启动,PID:"<<getpid()<<endl;
 pid_t id = fork();
 if(id == -1) exit(-1); //如果子进程创建失败则父进程直接退出
 if(id == 0) //子进程开始执行处
 {
   cout<<"子进程启动,PID:"<<getpid()<<endl;
 }
 sleep(1); //等待1秒
 cout<<"Hello Word!"<<" 执行进程PID:"<<getpid()<<endl;
 return 0;
}

执行结果
我们可以发现,父子进程在运行时都打印了自己的PID,但是有一条语句被执行了两次(父子进程各自执行了一次)!
执行流
父进程中由于id是子进程的pid所以if语句直接跳过转而执行后面的语句,在子进程中,由于id为0会执行if语句中的语句,然后执行后面的语句,所以打印 “Hello Word!” 语句会被执行两次;同时也说明了父子进程是独立的!

所以fork之前父进程独立执行,fork之后父子两个执行流分别执行。注意:fork之后,谁先执行完全由调度器决定
父子进程执行流



写时拷贝

谈到进程创建,肯定会涉及写时拷贝,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本!
写时拷贝
我们在<Linux进程地址空间-写时拷贝>中介绍了写时拷贝机制,实现原理就是通过页表+MMU机制,对不同的进程进行空间寻址,达到出现改写行为时,父子进程使用同一个地址而不同的真实空间的效果!

写实拷贝的验证:

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
 int num = 2;
 pid_t id = fork();
 if(id == -1) exit(-1); //如果子进程创建失败则父进程直接退出
 if(id == 0) //子进程开始执行处
 {
   --num; //子进程对num--
   cout<<"子进程num地址:"<<&num<<"|num="<<num<<endl;
   exit(0); //子进程执行到此为止
 }
 cout<<"父进程num地址:"<<&num<<"|num="<<num<<endl;
 return 0;
}

写时拷贝
可以发现同一个地址的num变量值却不同;这是因为同一个虚拟地址通过页表和MMU处理映射后的物理地址不同!

注意:

  • 写时拷贝不仅可以发生在常规栈区,堆区,还可以发生在静态区,代码区(例如exec进程替换)等等!
  • 写时拷贝后,生成的是副本在不同的物理空间,修改不会对原数据造成影响

进程终止


进程退出码

某些情况下,我们希望进程提前退出,例如进程申请某些资源失败,进程出现野指针问题等等…!

进程退出场景:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

进程常见退出方法:

  • 正常终止
    – 从main返回(return)
    – 调用exit或_exit函数
  • 异常终止
    –信号终止(CTRL+C,野指针导致进程异常的信号等等)

当进程退出,我们可以通过进程返回的退出码判断进程的运行状态!

查看进程退出码:

echo $?

return
main函数最后的return语句代表main函数正常退出时的退出码,这些是供用户进程退出健康状态的判定,不同的退出码表示不同的状态,一般为0,我们可以通过 echo $? 指令查看进程最近一次运行的退出码!

对于fork创建子进程时,子进程也可以有退出码,父进程通过子进程退出码判断子进程是否运行正常!
当一个进程退出后,表示操作系统内少了一个进程,操作系统会释放该进程的 内核数据结构以及代码和数据(内存) ;main函数退出代表整父进程退出,程序中其他函数退出仅代表该函数退出了!



退出码描述

关于进程退出码,在C语言中,虽然进程退出码是int类型但是实际可用的仅有低8位可以被父进程所用其范围大小是 0~255的无符号整型数 ,每一个退出码都代表一种错误!

C语言有对这些退出码有自己的解释,借助string.h库函数中的strerror函数可以打印出该退出码对应的C语言而言的异常是什么!

//#include <string.h>
#include <cstring> //C++中声明C库函数的写法
char* strerror(size_t pos); //strerror函数声明

进程退出码对应的异常描述字符串在C语言中是以指针数组存储的,每一个字符串对应一个指针;所以strerror是错误码函数返回函数退出码以及退出码所反映的结果(在C/C++语言上对退出码的描述)!

#include <iostream>
#include <cstring>
using namespace std;

int main()
{
 for(int i = 0;i<256;++i)
 {
   cout<<i<<":"<<strerror(i)<<endl; //打印所有退出码描述
 }
 return 0;
}

退出码描述
在C/C++中,对退出码的有效描述范围是0-133,后面的我们可以自己使用,也可以不管!
当我们需要对某些进程的退出码进行转换描述时可以使用strerror进行输出!



进程退出函数

当一个进程正在运行时,可以从外部终止(kill向进程发送终止信号,CTRL+C终止进程),也可以从内部终止(return,exit函数等等)!

进程外部终止:
进程外部终止方式

进程内部终止:

//#include <stdlib.h> //C语言声明
#include <cstdlib> //C++声明C库-所需头文件
void exit(int status); 
#include <unistd.h> //系统库文件
void _eixt(int status); 

参数:status 定义了进程的终止状态,父进程通过wait来获取该值
进程代码中,任意位置调用此函数都会终止该进程;对于exit函数和_exit函数,两者本质都是退出,但是exit函数是对_exit函数的封装!

exit最后也会调用_exit, 但在调用exit之前,还做了其他工作:

  • 执行用户通过 atexit或on_exit定义的清理函数
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用_exit

对于这两个函数,我们更推荐使用exit();我们通过代码展示区别:

#include <iostream>
#include <cstdlib>
#include <unistd.h>
using namespace std;

int main()
{
 cout<<"CSDN";
 exit(0); //先调用exit屏蔽_exit进行程序编译
 //_exit(0); //再调用_exit屏蔽exit进行编译
 return 0;
}

对比
第一次调用exit函数我们发现程序运行退出后打印了CSDN,但是_exit却没有!

这里再次说明: _exit()就只是单纯的退出程序,exit()是对_exit()的封装,exit()函数在内部调用_exit()之前会做一些其他事情,例如冲刷缓冲区等等,要不然我们调用exit()也不会看到CSDN被打印出来!
底层关系

关于return退出: return是一种更常见的退出进程方法。执行 return n 等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit()的参数!


进程等待


为什么等待?

我们在创建子进程时,如果子进程先结束会处于僵尸状态,这是一个比较麻烦的问题,僵尸进程越来越多会导致内存泄漏和标识符占用问题!
僵尸进程
出现僵尸进程的原因是因为子进程运行结束后,父进程没有等待并接收其退出码和退出状态,操作系统无法擅自释放其子进程所占用的相关资源,导致子进程处于僵尸状态!

此时就需要父进程等待子进程退出并回收!这就是为什么需要进程等待,准确说是父进程等待子进程结束回收子进程的过程!



等待函数

系统通过两个接口供父进程等待子进程:

#include <sys/types.h> //所需头文件
#include <sys/wait.h>
pid_t wait(int *status); //函数声明格式
pid_t waitpid(pid_t pid, int *status, int options);

wait():

  • 功能:等待任意一个子进程,并带回子进程的退出码
  • 返回值:成功等待返回被等待子进程pid,失败返回-1
  • 参数
    –status:获取子进程退出码的输出型参数,我们在外部创建一个变量传入变量的地址,如果函数等待成功则变量被设置为子进程的退出码(如果不需要可以设置为NULL/nullptr)

waitpid():

  • 功能:等待指定子进程,通过pid指定,同时waitpid可以设置等待方式
  • 返回值:
    –当正常返回的时候waitpid返回收集到的子进程的进程ID
    –如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    –如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
  • 参数
    – status:获取子进程退出码的输出型参数,我们在外部创建一个变量传入变量的地址,如果函数等待成功则变量被设置为子进程的退出码(如果不需要可以设置为NULL/nullptr)

    – pid:等待指定子进程的pid;当pid=-1,等待任一个子进程,与wait等效

    – options:设置等待方式:
    ——如果设置为0则子进程没有退出时父进程在此函数处阻塞,子进程退出时父进程才被唤醒
    ——如果设置为WNOHANG,当子进程没有退出时waitpid()函数返回0,不予以等待,若正常结束,则返回该子进程的ID

说明:

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
  • 如果不存在该子进程,则立即出错返回

wait/waitpid



获取子进程status

说明:

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  • 对于status参数,如果传递NULL,表示不关心子进程的退出状态信息
  • 如果传递status参数,则操作系统会根据该参数,将子进程的退出信息反馈给父进程

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
status

注意:

  • 当进程正常退出时,我们只关注8-15位(次低8位)的退出码
  • 当进程异常退出时,我们只关注0-7位终止信号
    –在进程的 PCB 中,包含了 int _exit_code 和 int _exit_signal 这两个信息,可以通过对 status 的位操作间接获取其中的值

提取status
-如果我们需要提取子进程退出码只需要使用位操作:(status >> 8) & 0xFF 提取退出码即可
-如果我们需要提取子进程终止信号只需要使用位操作:(status & 0x7F) 提取

-库中也给我们提供了宏来获取退出码WEXITSTATUS(status)
-通过 WIFEXITED(status) 宏判断进程退出情况,当宏为真时,表示进程正常退出



进程等待操作示例

wait等待
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

int main()
{
   cout<<"父进程启动..."<<endl;
   pid_t ret = fork();
   if(ret == 0)
   {
       for(int i = 0;i<5;++i) //子进程打印五秒退出
       {
           printf("我是子进程,pid:%d ppid%d\n",getpid(),getppid());
           sleep(1);
       }
       exit(0);
   } 
   wait(nullptr); //这里我们不关心返回值 父进程等待
   cout<<"子进程退出成功,父进程退出!"<<endl;
   return 0;
}

wait
这里可以发现父进程启动创建子进程后等待子进程五秒钟后才退出!


waitpid阻塞等待

这里我们使用waitpid阻塞等待,并使用位操作获取子进程退出码!

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

int main()
{
   cout<<"父进程启动..."<<endl;
   pid_t ret = fork();
   if(ret == 0)
   {
       for(int i = 0;i<5;++i)
       {
           printf("我是子进程,pid:%d ppid%d\n",getpid(),getppid());
           sleep(1);
       }
       exit(1);
   } 
   int status = 0; //定义变量获取退出码
   pid_t id = waitpid(ret,&status,0);
   if(id == ret) //如果相等表示等待成功 打印status相关信息
   {
     printf("我是父进程,pid:%d ppid:%d,子进程pid:%d 子进程状态码:%d 子进程终止信号:%d\n",getpid(),getppid(),id,(status>>8)&0xFF,status&0x7F);
   }
   return 0;
}

正常退出:
正常退出
收到异常信号退出:
异常信号退出


waitpid轮询式等待

这里我们使用waitpid轮询式等待(每次查看子进程是否退出,没有退出继续执行其他代码),并使用宏操作获取子进程退出码和判断是否正常退出!

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

int main()
{
   pid_t ret = fork();
   if(ret == 0)
   {
       for(int i = 0;i<10;++i)
       {
           printf("我是子进程,pid:%d ppid%d\n",getpid(),getppid());
           sleep(1);
       }
       exit(1);
   } 
   while(1)
   {
       int status = 0;
       pid_t id = waitpid(ret,&status,WNOHANG); //设置WNOHANG
       if(id == ret) //如果子进程正常退出
       {
         if(WIFEXITED(status)) //判断退出状态
         {
           printf("我是父进程:子进程正常退出,pid:%d ppid:%d,子进程pid:%d 子进程状态码:%d 子进程终止信号:%d\n",getpid(),getppid(),id,WEXITSTATUS(status),status&0x7F);
         }
         else
         {
           printf("我是父进程:子进程异常退出,pid:%d ppid:%d,子进程pid:%d 子进程状态码:%d 子进程终止信号:%d\n",getpid(),getppid(),id,WEXITSTATUS(status),status&0x7F);
         }
         break;
       }
       cout<<"父进程运行中..."<<endl;
       sleep(1);
   }
   return 0;
}

正常退出:
正常退出
异常退出:
异常退出
进程等待函数和宏一起搭配使用才能完整的进行进程等待!

父进程并非需要一直等待子进程运行结束(阻塞等待),可以通过设置 options 参数,进程解除状态,父进程变成轮询等待状态,不断获取子进程状态(是否退出),如果没退出,就执行其他任务了


关于waitpid等待方式的使用,当父进程没有其他任务时建议设置为阻塞式等待,当父进程需要执行其他任务时可以设置为轮询等待!


进程替换


什么是进程替换?

进程替换是改变进程原有的执行代码,转而执行另一套进程代码的过程!

为什么要进程替换:

  • 将进程看作一个任务处理单元
  • 我们写出指令,进程依次执行命令
  • 当我们需求比较多,但代码又无法改变时,就可以使用进程替换
  • 父进程可以创建子进程,用另一个程序的代码数据替换子进程代码数据转而让子进程执行新程序的代码数据

所以,进程替换的目的是让子进程帮我们执行特定任务,以应对不同场景!(例如当我们在某些时刻需要clear清屏时可以使用进程替换调用clear指令)


进程替换原理:

进程替换在Linux中时时刻刻都在出现,例如bash中,我们输入指令,指令是使用C语言写的可执行程序,bash会创建一个子进程然后进程替换执行我们输入的指令程序,所以bash是依靠创建子进程去运行我们的指令和可执行程序的,这样可以避免恶意程序导致bash和内核崩溃!

原理:用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
进程替换

说明:

  • 进程替换:让一个进程去执行一个在硬盘中的新的程序(从进程的角度),但并没有创建新的进程;进程替换只是新程序被加载了(从程序的角度)
  • 我们自己写的代码可以加载新的程序,操作系统创建进程数据结构,然后进程替换,新的代码和数据就被加载了,那么原进程的代码直接被替换了,没机会执行了
  • 程序替换是整体替换(数据和代码),不能局部替换
  • 程序替换只会影响调用的进程,子进程调用不影响父进程,因为进程具有独立性,子进程替换数据时会发生写时拷贝,与父进程的数据进行区分(这就是前面提到的,写时拷贝不一定只会发生在栈区或者数据区等)
  • 如果exec函数替换成功不会有返回值(被替换了原数据代码逻辑就没了),如果替换失败一定有返回值,所以不需要对程序替换的成功进行判断,如果成功就执行其他程序去了,如果失败直接执行异常终止就行了


进程替换函数

进程替换函数有七个,但实际上只有一个,即execve,其余6个是对execve的封装!

#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[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

int execve(const char *path, char *const argv[], char *const envp[]); //系统调用

因为都是exec开头,所以统称exec函数!

关于exec函数:

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回(因为代码数据已经被替换,是不可逆的)
  • exec函数只有出错的返回值而没有成功的返回值,如果调用出错则返回-1

关于参数和函数命名:

  • 函数名带l(list) : 表示参数采用列表
  • 函数名带v(vector) : 参数用数组
  • 函数名带p(path) : 自动搜索环境变量PATH(不需要输入精确路径)
  • 函数名带e(env) : 表示自己维护环境变量(环境变量需要自己传递)
    概览

exec函数相互之间区别

exec调用简单举例如下:

#include <stdio.h>
#include <unistd.h> //替换函数库文件
using namespace std;
int main()
{
    //替换程序的路径位置,以及调用选项
	char *const argv[] = {"ps", "-ef", NULL};
	//环境变量数组,最后为NULL结尾
	char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; 

	//不带p,需要写全替换程序的路径以及所有的参数选项	
	execl("/bin/ps", "ps", "-ef", NULL);

	// 带p的,可以使用环境变量PATH,无需写全路径
	execlp("ps", "ps", "-ef", NULL);

	// 带e的,需要自己组装环境变量
	execle("ps", "ps", "-ef", NULL, envp);

	execv("/bin/ps", argv);

	// 带p的,可以使用环境变量PATH,无需写全路径
	execvp("ps", argv);

	// 带e的,需要自己组装环境变量
	execve("/bin/ps", argv, envp);

	execvpe("ps",argv,envp);	

	return 0;
}

接下来我们逐一介绍这些函数的使用!(因为接口是C语言实现的,与C++编译器的要求有所不同,我们演示参数C编译器演示!)



execl替换函数

execl采用链表形式传递参数,后面待 l 的都是如此!

int execl(const char* path, const char* arg, ...);

execl参数解析:

  • path:待替换程序的准确路径(例如:/usr/bin/ls)
  • arg:待替换程序名,例如ls
  • :可变参数列表,传递可变数目的程序选项(例如-a和-a -l等等)

execl参数
注意,无论是否有选项,最后一定要传递NULL/nullptr!

#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动...\n");
	int ret = execl("/usr/bin/ls","ls","-a","-l",NULL);
	if(ret == -1) printf("%s%d\n","ret=",ret); 
	printf("进程替换失败!"); //如果进程替换失败则一定会这些这条语句
	return 0;
}

execl示例
如果路径有问题则:

//将上述该语句修改为
int ret = execl("/usr/","ls","-a","-l",NULL); //path路径错误

错误示范

可以发现execl的程序替换参数链表形式:
execl的参数传递


execv替换函数

execv函数采用vector传递参数(数组的方式),后面带 v 的都是如此!

int execv(const char* path, char* const argv[]);

函数参数解析:

  • path:待替换程序的准确路径(例如:/usr/bin/ls)
  • argv:待替换程序名和程序选项构成的参数表(指针数组)

execv参数

注意: 虽然execv只需传递两个参数,但在创建argv表时,最后一个元素仍然要设置为nullptr/NULL!

因为C++编译器对类型转换检查严格,而函数是早年C语言所实现的,所需参数argv是char* cosnt类型但里面的字符串是const char*类型,编译失败,所以我们退一步使用C语言进行演示!

#include <stdio.h>
#include <unistd.h>

int main()
{
 char* const argv[] = {"ls","-a","-l",NULL}; //数组接收程序名和选项
 printf("进程启动...\n");
 int ret = execv("/usr/bin/ls",argv);
 if(ret == -1) printf("%s%d\n","ret=",ret); 
 printf("进程替换失败!"); //如果进程替换失败则一定会这些这条语句
 return 0;
}

正常运行
同样的修改为错误路径测试,execv参数路径有问题也会出错:

int ret = execv("/usr/",argv); //path路径错误

错误示范

这里与execl的区别在于,execv的待替换程序名和选项参数是以数组的形式传递的!
argv数组


execlp替换函数

execlp是execl的变种,与execl不同的是execlp会自动去PATH环境变量中寻址待替换程序路径,我们只需要传递待替换程序名即可

int execlp(const char* file, const char* arg, ...);

execlp参数解析:

  • file:PATH中待替换程序名(不是路径,例如ls,clear等等)
  • arg:待替换程序名
  • : 程序选项,以可变参数列表传递(与execl保持一致)

注意: 只能在环境变量表中的 PATH 变量中搜索,如果待程序路径没有在 PATH 变量中,是无法进行替换的!

所以execlp和execl各有优劣,应对不同场景!
这里我们结合上面进程创建等待的知识使用子进程替换演示:

#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   printf("子进程创建成功\n");
   execlp("ls", "ls", "-a", "-l", NULL); //程序替换
   printf("子进程替换失败!\n");
   exit(-1);
 }

 int status = 0;
 waitpid(id, &status, 0);  //阻塞方式等待
 if(WEXITSTATUS(status) != 255) //判断退出码是否异常
   printf("父进程:子进程替换成功!\n");
 else
   printf("父进程:子进程替换失败!\n");

 return 0;
}

execlp演示
如果我们指定的待替换程序不在PATH环境变量中则会报错:

execlp("exe", NULL); //不在PATH路径中的可执行程序

在这里插入图片描述
execlp只要待替换程序在PATH路径中,就可以替换!


execvp替换函数

同样的,execvp与execv大部分相同,只不过execvp是从PATH路径中寻址待替换程序!

int execvp(const char* file, char* const argv[]);

execvp参数解析:

  • file:PATH中待替换程序名(不是路径,例如ls,clear等等)
  • argv:参数数组,包括待替换程序名和参数选项构成的指针数组
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   char* const argv[] = {"ls","-a","-l",NULL};
   printf("子进程创建成功\n");
   execvp("ls", argv);
   printf("子进程替换失败!\n");
   exit(-1);
 }

 int status = 0;
 waitpid(id, &status, 0);  //阻塞方式等待
 if(WEXITSTATUS(status) != 255) //判断退出码是否异常
   printf("父进程:子进程替换成功!\n");
 else
   printf("父进程:子进程替换失败!\n");

 return 0;
}

execvp演示
同样的,如果PATH中不存在待替换程序路径则替换失败:

execvp("exe", argv);

路径错误
如果要使用该替换函数调用自己写的程序,只需要将自己写的程序路径添加到环境变量PATH即可!


execle替换函数

execle替换函数在execl的基础上,支持自定义环境变量表!(前面的替换默认继承系统传入的环境变量表)

int execl(const char* path, const char* arg, ..., char* const envp[]);

execle参数解析:

  • path:待替换程序的准确路径
  • arg:待替换程序名
  • :待替换程序名和程序选项参数,以可变参数列表传递(与execl保持一致)
  • envp:自定义环境变量表

我们自己实现一个打印前五个环境变量参数的程序,使用进程替换调用!

#include <stdio.h>
int main(int argc,char* argv[],char* envp[])
{
   //默认打印前五条环境变量
   for(int i = 0;i<5 && envp[i];++i) printf("%d:%s\n",i,envp[i]); 
   return 0;
} //程序命名为Print

自定义环境变量让程序打印:

#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   char* const myenv[] = {"myval1=668","myval1=688","myval1=888" ,NULL};
   printf("子进程创建成功\n");
   execle("./Print","Print",NULL,myenv);
   printf("子进程替换失败!\n");
   exit(-1);
 }

 int status = 0;
 waitpid(id, &status, 0);  //阻塞方式等待
 if(WEXITSTATUS(status) != 255) //判断退出码是否异常
   printf("父进程:子进程execle替换成功!\n");
 else
   printf("父进程:子进程execle替换失败!\n");

 return 0;
}

execle
由于我们传递的环境变量只有三条所以只打印了三条!

我们可以传递系统环境变量给execle,这样功能就与execl相同了!

extern char** environ;	//声明环境变量表
execle("./Print","Print",NULL,environ);

传递系统环境变量
通过这个我们可以发现,如果主动传入环境变量后,待替换程序中的原环境变量表将被覆盖!

所以我们自己写的程序之所以可以继承环境变量表是因为bash在开启子进程后进程替换传递的是bash环境变量表,所以我们可以继承下去!

其他没有在exec后带e的替换函数都是默认传递bash环境变量表!


execve替换函数

execve是真正可以执行进程替换的系统调用,其他函数最终都会调用execve!
其他函数将参数处理后,传递给execve!

int execve(const char* filename, char* const argv[], char* const envp[]);

execve参数解析:

  • filename:待替换程序的精确路径
  • argv:待替换程序名和程序选项,以数组形式传递
  • envp:自定义环境变量表

使用execve打印我们自己的环境变量表:

#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   char* const myenv[] = {"myval1=668","myval1=688","myval1=888" ,NULL};
   char* const argv[] = {"Print",NULL};
   printf("子进程创建成功\n");
   execve("./Print",argv,myenv); //调用自己实现的Print打印环境变量程序
   printf("子进程替换失败!\n");
   exit(-1);
 }

 int status = 0;
 waitpid(id, &status, 0);  //阻塞方式等待
 if(WEXITSTATUS(status) != 255) //判断退出码是否异常
   printf("父进程:子进程execve替换成功!\n");
 else
   printf("父进程:子进程execve替换失败!\n");

 return 0;
} 

execve
注意: 替换函数除了能替换为C/C++编写的程序外,还能替换为其他语言编写的程序,如 Java、Python、GO、Swift等等,虽然它们在语法上各不相同,但在操作系统看来都属于二进制可执行程序,数据位于代码段和数据段,使用统一的系统调用替换即可!


execvpe替换函数

execvpe是对execvp的进一步封装,使用方法与execvp一致,不过最后一个参数可以传递自定义环境变量表!

int execvpe(const char* file, char* const argv[], char* const envp[]);

execvpe参数解析:

  • file:待替换程序名(程序在PATH环境变量中)
  • argv:待替换程序名和程序选项,以数组形式传递
  • envp:自定义环境变量表
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   char* const myenv[] = {"myval1=668","myval1=688","myval1=888" ,NULL};
   char* const argv[] = {"env",NULL};
   printf("子进程创建成功\n");
   execvpe("env",argv,myenv); //使用系统命令env打印我们自己的环境变量
   printf("子进程替换失败!\n");
   exit(-1);
 }

 int status = 0;
 waitpid(id, &status, 0);  //阻塞方式等待
 if(WEXITSTATUS(status) != 255) //判断退出码是否异常
   printf("父进程:子进程execvpe替换成功!\n");
 else
   printf("父进程:子进程execvpe替换失败!\n");

 return 0;
}

execvpe


最后

Linux进程控制到这里就介绍的差不多了,相信学习了进程控制大家一定对进程有了新的认识,原来我们平时开辟新进程时很多细节没有注意,这些细节都是非常致命的,例如父进程等待子进程退出和回收避免僵尸进程,以及开辟子进程进行进程替换,高效利用多进程等等;其实了解完了进程控制,结合前面所介绍的知识已经可以对bash的实现有一定的了解,这将有助于更合理的使用Linux系统!

本次 <Linux进程控制> 就先介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
结尾

🌟其他文章阅读推荐🌟
Linux<进程地址空间> -CSDN博客
Linux<环境变量> -CSDN博客
Linux<进程初识> -CSDN博客
Linux<进程状态及优先级> -CSDN博客
🌹欢迎读者多多浏览多多支持!🌹

  • 60
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 119
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 119
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ARMCSKGT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值