什么是shell?模拟实现shell(深刻理解shell的内建命令)

何为shell?

shell作为命令语言解释器,负责解释我们在Linux命令行中输入的指令,并将解释后的指令传递给Linux内核。简而言之,shell是用户和Linux内核之间交互的桥梁,用户不直接接触Linux内核,而是通过shell程序与内核交互。

shell的本质是一个C语言程序,与我们平常使用的QQ,浏览器一样,区别就是:一旦Linux系统运行起来,shell程序也会运行,而QQ,浏览器这些程序需要我们手动去运行,shell则自动运行

只要用户输入一个指令,就会被shell解释。指令分为两种

1.内建命令,由shell自身执行,如打印当前工作目录(pwd),输出文本消息(echo)

2.其他命令,是一个存储在某个目录下的程序,由shell的子进程执行,如列出目录内容(ls),拷贝文件(cp)

对于用户来说,不用关心该命令是否为内建命令,只要输入命令,就会得到结果。但对于shell来说,需要检查该命令是否为内建命令,如果是,需要自己执行该命令。如果不是,shell会去PATH路径列表下查找是否有该程序,如果有,解释该指令并创建子进程执行该指令,没有的话直接报错。

为了更深刻的理解shell的运行过程,我们模拟实现一个自己的shell程序

shell的模拟实现

提示符打印

在这里插入图片描述
shell被启动后,就会在命令行中等待命令的输入,在输入命令的地方前还有一串提示符

[用户名@主机名 当前所在文件夹] $

实现shell的第一步是打印出这串提示符,用户名,主机名和当前路径都能通过getenv获取环境变量函数得到。需要特别处理的是当前目录,getenv得到的是绝对路径,而提示符显示的是当前文件夹,所以得到绝对路径后要遍历这串字符串,记录最后一个’/'出现的位置,返回该位置的后一个位置,得到的就是当前文件夹

const char* get_dir(const char* path)
{
	int i = 0;
	int index = 0;
	while (path[i] != '\0')
	{
		if (path[i] == '/')
		{
		index = i;
		}
		i++;
	}
	return path + index + 1; 
}
	// 1.显示提示符
    
    printf("[%s@%s %s^]$ ", getenv("USER"), getenv("HOSTNAME"), get_dir(getenv("PWD")));
    fflush(stdout);  
    // 由于缓冲区的存在,不打印'\n'缓冲区是不会立即刷新的,直到缓冲区满了才会刷新出内容
    // 所以这里打印需要手动刷新缓冲区

用户输入消息的获取

比如我要执行ls命令,ls命令后还可以携带选项,ls -a -l,shell需要获取指令与选项,并将它们分割(shell接收到的是一串字符串"ls -a -l",shell需要将字符串分割为更小的字符串"ls" “-a” “-l”)。

定义command_line接收一整行的指令,该数组中的元素保存的是字符,command_args保存分割后的指令,该数组中的元素要保存的是分割后的字符串。

#define NUM 1024
#define SIZE 128

char command_line[NUM];
char* command_args[SIZE];

在存储数据之前先使用memset函数将两个数组置空。

memset(command_line, '\0', sizeof(command_line) * sizeof(char));
memset(command_args, '\0', sizeof(command_args) * sizeof(char));

获取整行数据可以使用fgets
在这里插入图片描述
在这里插入图片描述
fgets将从stream流中获取至多size个字符的字符串并保存到s中,如果遇到了’\0’或者读到了文件结束,fgets将中断。

所以我们可以用fgets从stdin标准输入流中读取NUM个字符到command_line数组中,这样就获取到了命令行的输入

fgets(command_line, NUM, stdin);

但获取到的字符串的’\0’之前还有一个’\n’(因为我们在命令的最后敲了回车,来表示命令输入的结束,这个’\n’会影响命令的识别,需要将其消除::用strlen得到字符串长度(不包括’\0’),字符串最后一个字符’\n’,的下标为长度-1,将该位置置为’\0’,表示字符串的结束,同时也消除了’\n’

command_line[strlen(command_line) - 1] = '\0'; // 消除'\n'

获取到整行字符串后,使用strtok将整行字符串分割成命令与选项
在这里插入图片描述

在这里插入图片描述
strtok使用:第一次调用strtok需要传入要被分割的字符串,之后如果要继续分割,就不用继续传该字符串的地址,只要传NULL,第二个参数是分割字符串的标志,对于分割整行命令,其分割的标志为" "。

当没有字符串要分割时,strtok就返回NULL,否则返回分割的第一个字符串,根据这个特点,可以先对command_line进行第一次分割并保存到command_args中,接着使用while循环分割完所有的子串。

// 获取命令并解析
#define SEP " "
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char* command_args[SIZE];
memset(command_line, '\0', sizeof(command_line) * sizeof(char));
memset(command_args, '\0', sizeof(command_args) * sizeof(char));
fgets(command_line, NUM, stdin);
command_line[strlen(command_line) - 1] = '\0'; // 消除'\n'
command_args[0] = strtok(command_line, SEP); 
int index = 1;
// 当还有字符串要分割时,strtok返回值不为空
// 分割结束,strtok返回空,while循环的条件为假,循环结束
while (command_args[index++] = strtok(NULL, SEP)); 

创建子进程执行程序

分析完指令后,shell会调用fork()函数创建一个子进程,让子进程调用exec()程序替换函数,替换子进程,让子进程执行成分析后的指令,同时父进程阻塞等待子进程并打印退出消息。

int id = fork();
if (id == 0)
{
	// child
	execvp(command_args[0], command_args);
	exit(-1); // 如果程序替换失败,子进程会执行该语句,返回-1
}
else
{
	int status = 0;
	int ret = waitpid(id, &status, 0);
	if (ret == id) // 等待成功
	{
		// 父进程接收子进程的退出码,并打印
		printf("等待成功, code:%d, sig:%d\n", (status >> 8) & 0xFF, status & 0x7F);
	}
}

运行结果

在这里插入图片描述
myshell是编译myshell.c生成的可执行程序,运行shell,为了与Linux自己的shell区分,我写的shell打印出的提示符有个^,上面是打印提示符的效果。
在这里插入图片描述
执行ls -a命令的结果
在这里插入图片描述
但是目前实现的shell有一个问题:执行pwd命令,打印当前的工作目录,然后cd …回到上一级目录,再执行pwd,通过打印的目录我们发现:没有回到上一级目录,要知道其中的原因就需要了解内建命令

内建命令

开头说到,shell的命令分为内部命令和其他命令,何为内建命令? 内建命令是一个需要shell自己执行的命令,即shell不创建子进程,自己亲自执行的命令。

为什么要有内建命令? shell创建子进程执行指令,当子进程执行完命令,子进程就退出了。 有这样一个场景:当子进程执行的指令是cd更改工作路径时,子进程执行cd命令,其工作路径确实发生了改变,但是父进程的工作路径没有变化(进程具有独立性,两个进程不会相互影响),接着子进程退出,此时向shell输入pwd命令,打印当前工作目录,shell又创建子进程执行pwd命令,这个子进程与之前退出的子进程没有关系,是重新创建的,由于父子进程代码共享,它们的工作路径相同,所以打印出的工作路径没有变化。体现给用户的感觉就是cd没有被执行,与上面截图中的问题相同。

所以,shell对于一些命令是必须要亲自执行的:比如cd更改工作路径,将shell的工作路径修改后,由于子进程的工作路径与父进程相同,更改分进程的工作路径后,父进程创建出的子进程的工作路径也是被修改过的,体现给用户的感觉就是当前的工作路径改变了

对于内建命令,shell是怎么实现的? shell中有许多内建命令,当shell接收到指令并解析后,需要判断用户输入的命令是否为内建命令,如果是就执行拦截操作,使子进程不再被创建,自己执行该指令。如果不是内建命令,则创建子进程执行该命令。

接着谈模拟实现:

Linux提供了一个系统接口,可以改变当前工作路径,chdir,将当前进程的工作路径修改为path。
在这里插入图片描述

// 对该系统接口封装
void change_dir(const char* new_path)
{
	chdir(new_path);
}

// 获取命令并解析
#define SEP " "
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char* command_args[SIZE];
memset(command_line, '\0', sizeof(command_line) * sizeof(char));
memset(command_args, '\0', sizeof(command_args) * sizeof(char));
fgets(command_line, NUM, stdin);
command_line[strlen(command_line) - 1] = '\0'; // 消除'\n'
command_args[0] = strtok(command_line, SEP); 
int index = 1;
// 当还有字符串要分割时,strtok返回值不为空
// 分割结束,strtok返回空,while循环的条件为假,循环结束
while (command_args[index++] = strtok(NULL, SEP)); 

// 解析完命令后,判断是否有内建命令需要拦截
if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL) // 要确保第二个参数不为空
{
	change_dir(command_args[1]); // 第二个参数是要进入的工作路径
	continue; 					 // shell被死循环执行,continue跳过后面创建子进程的代码 
}

shell对于内建命令的拦截,是在解析指令后,判断命令是否为内建,如果是,跳转执行对应的函数,并跳过之后创建子进程的过程。

添加内建命令的拦截后再执行cd指令。
在这里插入图片描述

环境变量与内建命令

我们知道,环境变量具有全局属性,具体表现是:一般情况下,子进程的环境变量继承父进程的环境变量,父进程的环境变量改变后,创建的子进程的环境变量也会跟着改变,但不会影响父进程的父进程,也就是说:全局属性体现为向下影响

环境变量的创建或修改需要使用export指令,格式为:export xxx=value,比如创建一个环境变量myval,它的值为123:export myval=123。基于对内建命令的理解,如果让shell创建子进程执行export创建myval这个环境变量,那么shell的子进程就具有了myval,当子进程执行完export指令,子进程退出,由于子进程的环境变量不会向上影响,所以shell的环境变量没有改变,环境变量改变的是已经退出的子进程,体现给用户的感觉是:环境变量没有被创建。我们创建环境变量的目的是希望在shell中创建环境变量,使得shell以下的子进程都具有该变量,所以export也是一个内建命令,需要特别的处理。

接着来说模拟实现:
在这里插入图片描述
在这里插入图片描述
Linux提供了修改环境变量的接口,将要修改或创建的环境变量以xxx=value的字符串方式传入该接口,xxx环境变量就会被创建或者修改。

所以在shell解析完指令后,判断指令是否为export,如果是则进行拦截,调用该接口创建环境变量。

(系统中有一个环境变量列表envrion,类型为char**,存储了当前进程的环境变量字符串char*,当你使用putenv添加环境变量时,实质上是向envrion的列表中添加了一个字符串的地址
在这里插入图片描述

需要注意的是添加环境变量时,环境变量列表不会environ 不会拷贝字符串,所以原字符串的修改会影响添加到环境变量列表中的环境变量)

if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
	// command_args[1]是xxx=value形式的字符串
	add_env(command_args[1]); // 是否有问题?
	continue;
}

由于模拟实现的shell使用command_line获取整行的输入,接着将每个命令与选项的首地址放到数组command_args中,myshell每次读取命令时,都会对command_line进行清空,也就意味着command_args[1]所指向的字符串也会被清空,在myshell添加完环境变量后,command_args[1]被添加到environ中,再次接收其他指令,command_args[1]被清空,环境变量也就不存在,所以向environ传递的字符串必须要是一个不会被随意更改的字符串。

#define NUM 1024
char env_buffer[NUM]; 
// 更规范的做法是动态申请堆上的空间,这里的全局变量只是用来测试,只能添加一个环境变量
void add_env(char* new_env)
{
	putenv(new_env);
}
if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
	// 先拷贝字符串到env_buffer中,再将env_buffer添加到环境变量列表中
	strcpy(env_buffer, command_args[1]);
	add_env(env_buffer);
	continue;
}

在这里插入图片描述
添加完export内建命令的运行结果。我在当前目录下写了个C++程序mycmd,运行mycmd将打印环境变量"lll"所对应的值,然后运行myshell程序,运行mycmd,没有打印信息,说明进程中没有lll环境变量,然后使用export命令创建环境变量lll并赋值123,最后运行mycmd程序,打印出lll变量的值——123,说明环境变量创建成功,export内建命令完成工作。

提示符最后的优化

模拟实现内建命令cd后,可以使用cd进入一个工作目录,但是命令行的提示符却没有发生改变
在这里插入图片描述
有了对环境变量的理解,模拟的shell可以完成最后的一次优化:使提示符打印出的当前工作目录与cd命令同步

提示符打印的当前工作目录是根据环境变量PWD获取的,而我们模拟实现的cd命令只是将当前工作路径修改(chdir函数不会改变PWD环境变量),所以模拟实现的cd命令还应该改变PWD环境变量,使打印的提示也随着cd改变。

还需要根据chdir的返回值判断要进入的工作目录是否存在,当工作目录不存在时,不能修改PWD环境变量,应该要提示用户输入的路径错误。只有输入的工作目录存在,shell才会跳转到该目录下,并且修改PWD环境变量。

char path_buf[NUM];

void change_dir(const char* new_path)
{ 
	strcpy(path_buf, "PWD=" );
	if (chdir(new_path) == -1)
	{
		printf("cd: %s: No such file or directory\n", new_path);
	}
	else // 当更改目录成功,修改环境变量中的PWD变量
	{
		// 进入到新的工作路径下,获取当前工作路径,并连接到path_buf后,作为PWD的value值
		strcat(path_buf, get_current_dir_name()); 
		putenv(path_buf);
	}
}

优化后的shell运行结果
在这里插入图片描述

模拟实现的shell完整代码

当然,到目前为止,myshell与真正的shell之间还是有差距的,还有优化的空间,比如ls对应当前目录下的文件,myshell的打印没有颜色,shell有颜色,这需要在ls后加上–color=auto在这里插入图片描述
实际上我们经常使用的ls,是ls --color=auto的别名(alias是一个取别名操作),在模拟实现的shell中,解析完命令后判断命令是否有ls,如果有,再在解析的参数后加上–color=auto。

但是这些不是模拟实现shell的最终目的,模拟实现shell的最终目的是

理解shell的工作原理,shell的本质:一个C语言程序,shell对于两种命令的处理方式以及shell是怎么创建子进程执行指令的(fork+exec)。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>


#define NUM 1024
#define SIZE 128
#define SEP " "

char command_line[NUM];
char* command_args[SIZE];
char env_buffer[NUM];
char path_buf[NUM];

void change_dir(const char* new_path)
{ 
	strcpy(path_buf, "PWD=" );
	if (chdir(new_path) == -1)
	{
		printf("cd: %s: No such file or directory\n", new_path);
	}
	else // 当更改目录成功,修改环境变量中的PWD变量
	{
		strcat(path_buf, get_current_dir_name());
		putenv(path_buf);
	}
}

void add_env(char* new_env)
{
	putenv(new_env);
}

const char* get_dir(const char* path)
{
	int i = 0;
	int index = 0;
	while (path[i] != '\0')
	{
		if (path[i] == '/')
		{
			index = i;
		}
		i++;
	}
	return path + index + 1; 
}

int main()
{
	while (1)
	{
		// 1.显示提示符
		printf("[%s@%s %s^]$ ", getenv("USER"), getenv("HOSTNAME"), get_dir(getenv("PWD")));
		fflush(stdout);
		
		// 2.获取用户输入
		memset(command_line, '\0', sizeof(command_line) * sizeof(char));
		memset(command_args, '\0', sizeof(command_args) * sizeof(char));
		fgets(command_line, sizeof(command_line) * sizeof(char), stdin);
		if (strlen(command_line) > 1)
		{
			command_line[strlen(command_line) - 1] = '\0'; // 消除'\n'
			command_args[0] = strtok(command_line, SEP); 
			int index = 1;
			if (strcmp(command_args[0], "ls") == 0)
			command_args[index++] = "--color=auto";
			
			while (command_args[index++] = strtok(NULL, SEP));
			
			if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
			{
				change_dir(command_args[1]);
				continue; 
			}
			if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
			{
				strcpy(env_buffer, command_args[1]);
				add_env(env_buffer);
				continue;
			}
			
			//  创建子进程执行其他命令
			int id = fork();
			if (id == 0)
			{
				// child
				execvp(command_args[0], command_args);
				exit(-1);
			}
			else
			{
				int status = 0;
				int ret = waitpid(id, &status, 0);
				if (ret == id) // 等待成功
				{
					printf("等待成功, code:%d, sig:%d\n", (status >> 8) & 0xFF, status & 0x7F);
				}
			}	
		} // end of -- if (strlen(command_line) > 1)
	}  // end of -- while(1)
	return 0;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值