【进程控制二】进程替换和bash解释器

【进程控制二】进程替换

通过fork创建的子进程,会继承父进程的代码和数据,因此本质上还是在执行父进程的代码
进程替换可以将别的进程的代码替换到自己的代码区,让自己去执行别人的代码
进程替换是通过exec系列系统调用接口实现的

1.exec系列接口

先看看man手册中的exec接口:

这些接口健壮度很高,就算错误地使用了接口,结果也不容易出错

2.execl系列

execl隶属于exec系列,加上l代表list,表示参数采用列表

2.1execl接口

cpp 复制代码
int execl(const char *pathname, const char *arg, ...);
  • pathname:指定用于替换的进程的路径
  • arg:以何种方式运行进程
  • ...:以何种方式运行该进程
  • NULL:当参数列表list结束,必须以NULL结尾
  • 返回值:如果调用成功,该函数不会返回,因为当前进程的映像被替换

我们现在要替换ls指令到自己的进程中,ls指令在/usr/bin/ls中

我们希望以ls -l -a的形式来调用这个进程,因此我们的三个参数 "ls", "-l", "-a"就是这个指令拆分出来的三个字符串

最后以NULL结尾

cpp 复制代码
#include<unistd.h>
#include<stdio.h>

int main()
{
    printf("程序替换前\n");

    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    //执行ls -l并替代当前进程
	printf("程序替换后\n");     
    return 0;
}

输出结果:

我们成功在当前进程中替换成了ls指令,并以ls -l -a的形式调用

但没有打印"程序替换后",因为进程替换是用别的进程的代码区覆盖掉自己原先的代码区,所以execl一旦执行,整个进程的代码都被替换了,那么printf("程序替换后\n");就会被覆盖掉,最后不输出

2.2execlp接口

cpp 复制代码
int execlp(const char* file, const char* arg, ... );
  • file:指定替换的进程名称(不用指明路径,会自动去环境变量PATH指定的路径中查找)
  • arg:以何种方式运行进程
  • ...: 运行该进程的选项
  • 最后以NULL结尾
  • 返回值:如果调用成功,该函数不会返回,因为当前进程的映像被替换
cpp 复制代码
int main()    
{    
    printf("程序替换前\n");    
    
    execlp("ls","-ls","-l","-a",NULL);  
                                                                                                                                       
    printf("程序替换后\n");        
    return 0;    
} 

2.3execle

cpp 复制代码
int execle(const char *pathname, const char *arg, ... ,char *const envp[] );
  • pathname:指定用于替换的进程的路径
  • arg:以何种方式运行进程
  • ...:以何种方式运行该进程
  • NULL:当参数列表list结束,必须以NULL结尾
  • envp:指针数组存储环境变量,用于设置新程序的环境变量,数组必须以 NULL 结束
  • 返回值:如果调用成功,该函数不会返回,因为当前进程的映像被替换
cpp 复制代码
int main()    
{    
	const char* _env[] = {"My_env = 666666666666666666666",NULL};
    printf("程序替换前\n");    
    
    execlp("/usr/bin/ls","-ls","-l","-a",NULL,_env);  
                                                                                                                                       
    printf("程序替换后\n");        
    return 0;    
} 

execle可以给替换后的进程指定环境变量表

3.execv系列

v就是vector,以数组的形式,把选项都存在数组中,将整个数组传入

3.1execv

cpp 复制代码
int execv(const char *pathname, char *const argv[]):
  • pathname:指定用于替换的进程的路径
  • argv:指定以何种方式调用进程,将这些选项存储在一个数组中
cpp 复制代码
int main()    
{    
	char* set[] = {"ls","-a","-l",NULL};
    printf("程序替换前\n");    
    
    execv("/usr/bin/ls",set);  
                                                                                                                                       
    printf("程序替换后\n");        
    return 0;    
} 

将我们要执行程序的方法用数组存起来再把数组传过去

3.2总结


其他接口就不一一演示了

健壮度演示:

cpp 复制代码
int main()    
{    
	char* set[] = {"ls","-a","-l",NULL};
    printf("程序替换前\n");    
    
    execvp("/usr/bin/ls",set);  //自动查找可执行文件并执行,但我们主动传递了文件路径也不会出错
                                                                                                                                       
    printf("程序替换后\n");        
    return 0;    
} 

虽然使用的是execvp,但我们主动传递了文件路径也不会出错

4.实现一个bash解释器

接下来要把字符串以空格为分割进行打散,strtok函数可以帮助我们实现

代码如下:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024//输入命令行字符串
#define SIZE 64 //打散后的命令行字符串
#define SEP " " //字符串分隔符

int lastcode = 0;//上个进程的退出码

const char* getUsername()
{
	const char* name = getenv("USER");
	if(name) return name;
	else return "none";
}

const char* getHostname()
{
	const char* hostname = getenv("HOSTNAME");
	if(hostname) return hostname;
	else return "none";
}

const char* getCwd()
{
	const char* cwd = getenv("PWD");
	if(cwd) return cwd;
	else return "none";
}

int GetUserCommand(char* command,int num)
{
	printf("[%s@%s %s]#",getUsername(),getHostname(),getCwd());
	fgets(command,num,stdin);//在fgets()函数的眼里,换行符'\n'也是它要读取的一个普通字符而已。在读取键盘输入的时候会把最后输入的回车符也存进数组里面,即会把'\n'也存进数组里面
	command[strlen(command) - 1] = '\0';//将输入的\n清除掉
	return strlen(command);
}

void CommandSplit(char* in,char* out[])
{
	int argc = 0;
	out[argc++] = strtok(in,SEP);
	while(out[argc++] = strtok(NULL,SEP));
}

int execute(char* argv[])//执行命令
{
	pid_t id = fork();
	if(id < 0) return -1;
	else if(id == 0)//child process
	{
		execvp(argv[0],argv);//程序替换
	}
	else//father process
	{
		int status = 0;
		pid_t rid = waitpid(id,&status,0);
		if(rid > 0)
		{
			lastcode = WEXITSTATUS(status);//刷新退出码
		}
	}
	return 0;
}

int main()
{	
	while(1)
	{
		char UserCommand[NUM];//用于保存即将输入的命令行字符串
		char* argv[SIZE];//保存将会被打散的字符串
						 //
		GetUserCommand(UserCommand,sizeof(UserCommand));//打印提示符&&获取用户命令字符串

		CommandSplit(UserCommand,argv);//分割字符串

		execute(argv);//执行命令
	}
	return 0;
}

4.1内建命令

我们实现bash后,可能会遇见一个问题:cd指令进入某个文件夹似乎没用

因为指令cd是进入某个文件夹,而进入此文件夹当然是由当前的父进程 进入

如果由子进程去执行,由于写时拷贝的原因父进程并不会进去

对于像cd这样的指令我们称为内建命令,也就是不能让子进程来完成的命令,只能父进程亲自执行

我们需要主动添加内建命令的判断

cpp 复制代码
char cwd[1024];//父进程要进入的文件路径

char* homepath()
{
	char* home = getenv("HOME");
	if(home) return home;
	else return (char*)".";
}
void cd(const char* path)
{
	chdir(path);//切换当前的工作目录
	char tmp[1024];
	getcwd(tmp,sizeof(tmp));
	sprintf(cwd,"PWD=%s",tmp);
	putenv(cwd);
}
int doBuildin(char* argv[])
{
	if(strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if(argv[1] == NULL) path = homepath();
        else path = argv[1];
        cd(path);
        return 1;
    }
	return 0;
}

内建命令不止cd,像export,kill和history等等也是内建命令

完整代码

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024//输入命令行字符串
#define SIZE 64 //打散后的命令行字符串
#define SEP " " //字符串分隔符

int lastcode = 0;//上个进程的退出码
char cwd[1024];//父进程要进入的文件路径

const char* getUsername()
{
	const char* name = getenv("USER");
	if(name) return name;
	else return "none";
}

const char* getHostname()
{
	const char* hostname = getenv("HOSTNAME");
	if(hostname) return hostname;
	else return "none";
}

const char* getCwd()
{
	const char* cwd = getenv("PWD");
	if(cwd) return cwd;
	else return "none";
}

int GetUserCommand(char* command,int num)
{
	printf("[%s@%s %s]#",getUsername(),getHostname(),getCwd());
	fgets(command,num,stdin);//在fgets()函数的眼里,换行符'\n'也是它要读取的一个普通字符而已。在读取键盘输入的时候会把最后输入的回车符也存进数组里面,即会把'\n'也存进数组里面
	command[strlen(command) - 1] = '\0';//将输入的\n清除掉
	return strlen(command);
}

void CommandSplit(char* in,char* out[])
{
	int argc = 0;
	out[argc++] = strtok(in,SEP);
	while(out[argc++] = strtok(NULL,SEP));
}

char* homepath()
{
	char* home = getenv("HOME");
	if(home) return home;
	else return (char*)".";
}
void cd(const char* path)
{
	chdir(path);//切换当前的工作目录
	char tmp[1024];
	getcwd(tmp,sizeof(tmp));
	sprintf(cwd,"PWD=%s",tmp);
	putenv(cwd);
}
int doBuildin(char* argv[])
{
	if(strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if(argv[1] == NULL) path = homepath();
        else path = argv[1];
        cd(path);
        return 1;
    }
	return 0;
}

int execute(char* argv[])//执行命令
{
	pid_t id = fork();
	if(id < 0) return -1;
	else if(id == 0)//child process
	{
		execvp(argv[0],argv);//程序替换
	}
	else//father process
	{
		int status = 0;
		pid_t rid = waitpid(id,&status,0);
		if(rid > 0)
		{
			lastcode = WEXITSTATUS(status);//刷新退出码
		}
	}
	return 0;
}

int main()
{	
	while(1)
	{
		char UserCommand[NUM];//用于保存即将输入的命令行字符串
		char* argv[SIZE];//保存将会被打散的字符串
						 //
		GetUserCommand(UserCommand,sizeof(UserCommand));//打印提示符&&获取用户命令字符串

		CommandSplit(UserCommand,argv);//分割字符串

		int n = doBuildin(argv);//判断是否是内建命令并执行
		if(n) continue;

		execute(argv);//执行命令
	}
	return 0;
}
相关推荐
Lucas55555555几秒前
现代C++四十不惑:AI时代系统软件的基石与新征程
开发语言·c++·人工智能
源代码•宸1 分钟前
goframe框架签到系统项目(BITFIELD 命令详解、Redis Key 设计、goframe 框架教程、安装MySQL)
开发语言·数据库·经验分享·redis·后端·mysql·golang
吃喝不愁霸王餐APP开发者17 分钟前
Java后端系统对接第三方外卖API时的幂等性设计与重试策略实践
java·开发语言
写代码的【黑咖啡】22 分钟前
深入理解 Python 中的模块(Module)
开发语言·python
耗同学一米八30 分钟前
2026年河北省职业院校技能大赛中职组“网络建设与运维”赛项答案解析 1.系统安装
linux·服务器·centos
ytttr87344 分钟前
MATLAB基于LDA的人脸识别算法实现(ORL数据库)
数据库·算法·matlab
wuk9981 小时前
matlab为地图进行四色着色
开发语言·matlab
_MyFavorite_1 小时前
cl报错+安装 Microsoft Visual C++ Build Tools
开发语言·c++·microsoft
charlie1145141911 小时前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++
zmzb01031 小时前
C++课后习题训练记录Day55
开发语言·c++