linu之进程的程序替换与shell基本实现的基本版本

1.概念

一个进程被另一个进程替换,想实现这种功能可以通过OS给的一些函数接口来实现。

例:

复制代码
execl("user/bin/ls","ls","-l","-a",NULL);

使用这个函数后原本进程就被替换为ls了。这种改变进程逻辑的方式就叫程序替换。

2.本质

发生程序替换时,该进程中的数据段与代码段(真实内存)被新进程的数据与代码直接覆盖,然后再调整一下页表的映射关系即可,也就是整个过程中PCB几乎没有进行任何修改。因此程序一但替换成功旧代码和数据就不再存在了。

3.exec*系列的介绍

复制代码
#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 execve(const char *path, char *const argv[], char *const envp[]);

exec*如果替换失败了旧继续运行源代码了,且只有替换失败了才有返回值-1,否则没有返回值。

exec*系列常用于子进程中,以保证父进程的正常运行。

不影响父进程的核心原因是exec*在加载新进程时会对代码和数值都发生写时拷贝。

加载器:广义上讲就是将进程加载进内存的工具,exec*接口可以算的上是加载器一种。

(1)execl

复制代码
int execl(const char*path,const char*arg,...);

path:一个绝对路径,对应一个可执行文件。

arg:一个个命令符的字符串,最后要加一个NULL。

例:

复制代码
execl("usr/bin/ls","ls","-al",NULL);

路径可以是绝对路径,也可以是相对路径,后面的指令格式我们统一用指令的格式即可。

这个可执行文件可以是任何语言的程序,也是我们可以在c语言的环境下调用其他语言的程序

例:

复制代码
execl("usr/bin/python","python","other.py",NULL);
//参数一为该语言的解释器路径
//参数二位该语言名
//三位我们想要执行的可执行文件(路径)

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

p代表PATH,即file可以只写文件名,然后execlp会自动在PATH中存的路径中寻找该指令。其他的写法与execl一样。

例:

复制代码
execlp("ls","ls","-ln",NULL);

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

argv就是类似于命令行参数表的东西

例:

复制代码
char const*argv[] = {"ls","-la",NULL};
execv("usr/bin/ls",argv);

shell传参的原理:

子进程中的mian函数的参数就是父进程通过调用exec*的接口实现的。

(4)execvp(const char*file,char const *argv[])

execv和execlp的结合

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

envp就是环境变量表。

理一下整个调用顺序:父进程给子进程一段代码区间,在里面调用一个exec*接口,之后该子进程就变成新进程,新进程中的main函数参数由exec*的参数提供,其中envp就会把新进程中的环境变量全部覆盖成envp中的,也就是原本的环境变量表直接消失了。

不传envp的话就用原本旧进程中的环境变量表。

添加环境变量的方式:

1.int putenv(char*string)

给当前进程中添加一个环境变量(可以继承给子进程),这个string最好在当前程序(父进程)中定义为全局变量。

2.environ

一个指向当前环境变量表的一个指针,要加extern char** environ声明才能使用。我们可以先putenv新环境变量,然后给exec*传environ为参数即可。

(6)execve

这是OS创建的接口,前面的所有接口都是来至于c语言库。

4.shell底层代码的实现(基本版本)

复习:

(1)fgets(char*arr,int size,FILE*stream)

arr是被输入字符的数组,size是该arr的大小,stream是输入方式

(2)strtok(char*str,const char*delim);

分割字符串,delim是分隔符,str是被分割的字符串,返回每一段的子字符串,要继续切历史字符要传NULL。

(3)string.rfind,从后往前找标识符并返回对应位置的迭代器。

代码:

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<ctype.h>
using namespace std;
//环境变量的提取
string get_user_name()
{
	string name = getenv("USER");
	return name.empty() ? "None" : name;
}
string get_host_name()
{
	string name = getenv("HOSTNAME");
	return name.empty() ? "None" : name;
}
string get_pwd()
{
	string name = getenv("HOSTNAME");
	return name.empty() ? "None" : name;
	return name;
}
//得到路径的最后一个字符
string last_dir()
{
	string curr = get_pwd();
	if (curr == "/")return curr;
	//找到最后一个"/"
	size_t pos = curr.rfind("/");
	//不要"/",因此+1来不添加"/"
	return curr.substr(pos + 1);
}
const int basesize = 1024;
//得到命令行
string make_command_line()
{
	char command_line[basesize];
	//将format中的字符串的前basesize拷贝给command_line
	snprintf(command_line, basesize, "[%s@%s %s]# ",get_user_name().c_str(), get_host_name().c_str(), last_dir().c_str());
	return command_line;
}
//打印命令行
void print_command_line()
{
	printf("%s", make_command_line().c_str());
	fflush(stdout);
}
//得到用户命令
bool get_command_line(char command_buffer[], int size) 
{
	//以行为单位得到字符串
	char* result = fgets(command_buffer, size, stdin);
	if (!result)
	{
		return false;
	}
	//减一用于去除最后的"\n"
	command_buffer[strlen(command_buffer) - 1] = 0;
	//什么都不输入就return false直接写一次循环了
	if (strlen(command_buffer) == 0) return false;
	return true;
}
const int num = 64;
int argc;
char* argv[num];
//分析(拆分)命令格式
void parse_command_line(char command_buffer[], int len) 
{
	(void)len;
	//清0消除上一次的数据
	memset(argv, 0, sizeof(argv));
	argc = 0;
	
	const char* sep = " ";
	argv[argc++] = strtok(command_buffer, sep);
	while ((bool)(argv[argc++] = strtok(nullptr, sep)));
	//strtok在没有标识符后会返回NULL,此时argc依旧会++,因此要--来除去这一次
	argc--;
}
//调用进程执行指令
bool execute_command() 
{
	
	pid_t id = fork();
	if (id < 0) return false;
	if (id == 0)
	{
		execvp(argv[0], argv);
		//exit是用于execvp替换失败的
		exit(1);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
}
int main()
{
	//存命令行的数组
	char command_buffer[basesize];
	while (true)
	{
		//1.打印命令行
		print_command_line();
		//2.输入命令
		if (!get_command_line(command_buffer, basesize)) 
		{
			continue;
		}
		//3.系统剪切命令
		parse_command_line(command_buffer, strlen(command_buffer)); 
		//4.调用子进程实现指令
		execute_command();
	}
	return 0;
}
相关推荐
念恒123062 小时前
进程控制---进程程序替换
linux·c语言
小夏子_riotous2 小时前
Docker学习路径——10、Docker Compose 一站式编排:从入门到生产级部署
linux·运维·服务器·docker·容器·centos·云计算
zhangrelay2 小时前
三分钟云课实践速通--概率统计--python版
linux·开发语言·笔记·python·学习·ubuntu
云栖梦泽2 小时前
Linux内核与驱动:GPIO设备树与SPI设备树的区别
linux·运维·c++·嵌入式硬件
HalvmånEver2 小时前
MySQL表的查询(二)
linux·数据库·学习·mysql
断问天2 小时前
Faq:Fedora44 Kernel升级后WIFI和声卡都不能使用了
linux·运维·服务器
旧故新长2 小时前
部署自动发卡网站的问题和解决方案
linux·docker
万粉变现经纪人2 小时前
如何解决 pip install bitsandbytes 报错 仅支持 Linux+glibc(macOS/Windows 失败)问题
linux·运维·windows·python·scrapy·macos·pip
计算机安禾3 小时前
【Linux从入门到精通】第25篇:循环结构——重复造轮子的终结者
linux·运维·chrome