【进程控制二】进程替换和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;
}
相关推荐
小学生的信奥之路几秒前
力扣1991:找到数组的中间位置(前缀和)
数据结构·算法·leetcode·前缀和·数组
ha20428941944 分钟前
c++学习之--- list
c语言·c++·学习·list
এ᭄画画的北北5 分钟前
力扣-102.二叉树的层序遍历
数据结构·算法·leetcode
ccLianLian6 分钟前
数据结构·字典树
数据结构·算法
Lu Yao_18 分钟前
用golang实现二叉搜索树(BST)
开发语言·数据结构·golang
君鼎35 分钟前
muduo库TcpServer模块详解
linux·网络·c++
沐土Arvin41 分钟前
前端图片上传组件实战:从动态销毁Input到全屏预览的全功能实现
开发语言·前端·javascript
找不到、了44 分钟前
Spring-Beans的生命周期的介绍
java·开发语言·spring
卡戎-caryon1 小时前
【MySQL】02.数据库基础
linux·网络·数据库·mysql·存储引擎
(・Д・)ノ1 小时前
python打卡day29
开发语言·python