【Linux系统】手搓一个简洁版shell,手都搓烂了

头像
⭐️个人主页:@小羊
⭐️所属专栏:Linux
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述


1、进程创建

fork函数:从已经存在的进程中创建一个新进程。新进程为子进程,原进程为父进程。

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

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝给子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

fork函数返回值:

  • 对子进程返回0
  • 对父进程返回子进程pid

如何理解fork有两个返回值?

  • 同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。

写时拷贝 (懒拷贝,时间换空间)

在这里插入图片描述

数据在默认不修改的情况下是共享的,不各自拷贝一份是因为父子进程间的数据大部分是重复的,一般只有少量数据需要修改,因为各自拷贝一份浪费空间。

更新父进程页表项为只读—子进程继承—子进程写入—触发系统错误—系统触发缺页中断—系统检测—判定是否要写时拷贝—拷贝,修改,恢复权限。

创建出子进程,让子进程执行一些任务:

#include <iostream>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>

using namespace std;

enum 
{
	OK,
	OPEN_FILE_ERROR
};

vector<int> data;

int savebegin()
{
	string name = to_string((unsigned int)time(nullptr));
	name += ".backup";
	FILE *pf = fopen(name.c_str(), "w");
	if (pf == nullptr)
	{
		return OPEN_FILE_ERROR;
	}
	
	string datastr;
	for (auto d : data)
	{
		datastr += to_string(d);
		datastr += " ";
	}
	fputs(datastr.c_str(), pf);//将拿到的数据备份到文件中
	fclose(pf);
	return OK;
}

void save()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程备份数据
		int code = savebegin();
		exit(code);
	}
	
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if (rid > 0)
	{
		int code = WEXITSTATUS(status);//进程退出码
		if (code == 0)
	    	cout << "备份成功, exit code:" << code << endl;
		else
			cout << "备份失败,exit code:" << code << endl;
	}
	else
	{
		perror("waitpid");
	}
}

int main()
{
	int cnt = 1;
	while (true)
	{
		data.push_back(cnt++);
		sleep(1);

		if (cnt % 10 == 0)
		{
			save();
		}
	}
    return 0;
}

上面的代码中子进程每隔10秒备份一份数据。


2、进程终止

main函数的返回值->返回给父进程或系统。

在这里插入图片描述
退出码:

  • 退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0时表示执行成功,没有问题。代码1 或0 以外的任何代码都被视为不成功。
  • Linux shell中的主要退出码:
退出码描述
0命令成功执行
1通用错误码,表示命令未能成功执行(通常由 shell 脚本返回)
2误用 shell 内置命令(如参数不正确)
126命令不可执行(例如,文件不是可执行文件)
127命令未找到(例如,在 PATH 中未找到指定的命令)
128无效的退出参数(通常与信号相关,如 128+n 表示信号 n)
130脚本因收到 SIGINT(通常是 Ctrl+C)而终止
137进程因收到 SIGKILL 信号而被强制终止
255退出码超出范围(通常由 shell 脚本返回,表示错误)

进程终止的方式:

  1. main函数中的return只有main函数中的return才能终止进程
  2. exit(库函数):在代码的任何地方,结束进程

在这里插入图片描述

  1. _exit(系统调用接口):

在这里插入图片描述
这是因为我们所说的缓冲区是语言级别的缓冲区(C/C++),所以_exit(系统调用接口)接触不到。


3、进程等待

  • 等待的时候,如果子进程不退出,父进程就会阻塞在wait函数内部。

在这里插入图片描述
在这里插入图片描述

我们只能通过系统调用获取退出信息。
在这里插入图片描述

  • waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

在这里插入图片描述

进程退出:

  1. 代码跑完,结果对,return 0
  2. 代码跑完,结果不对,return !0
  3. 进程异常,OS提前用信号终止进程,进程退出信息中也会记录退出信号

如果我们想看一个进程结果是否正确,前提这个进程退出信号为0,说明这个进程是正常跑完的,但结果是对还是不对需要看退出码来判断。
在这里插入图片描述
除了上面的位操作获取退出码,还可以使用系统提供的相关宏:

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)

阻塞和非阻塞
非阻塞等待即让父进程在等待子进程的过程中去做一些自己的事。

typedef function<void()> task_t;

void LoadTask(vector<task_t>& tasks)
{
	tasks.push_back(PrintLog);
	tasks.push_back(DownLoad);
	tasks.push_back(BackUp);
}

int main()
{
	vector<task_t> tasks;
	LoadTask(tasks);//加载任务

	pid_t id = fork();
	if (id == 0)
	{
		while (true)
		{
		    cout << "我是子进程,pid:" << getpid() << endl;
			sleep(1);
		}
		exit(0);
	}
	
	while (true)//阻塞循环等待
	{
		sleep(1);
		pid_t rid = waitpid(id, nullptr, WNOHANG);
		if (rid > 0)
		{
			cout << "等待子进程" << rid << "成功" << endl;
			break;
		}
		else if (rid < 0)
		{
			cout << "等待子进程失败" << endl;
			break;
		}
		else
		{
			cout << "子进程尚未退出" << endl;
			//父进程做一些自己的事
			for (auto& task : tasks)
			{
				task();
			}
		}
	}
}

4、进程程序替换

上面的子进程执行的都是父进程的部分代码,如果我们想让子进程执行一个全新的程序呢?
在这里插入图片描述

替换原理:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数来执行另一个程序,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,开始执行新程序。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
如果给上面的代码加上死循环,再让运行时读取指令,不就是一个简单的命令解释器吗?

程序替换不影响命令行参数和环境变量。


5、手写简洁版shell

现在写的shell没有维护自己的环境变量表,是继承自父shell,我们当然也可以维护自己shell的环境变量表。

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

const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;

//全局的命令行参数表
char *gargv[argvnum];//命令行参数表
int gargc = 0;//计数

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

//全局的shell工作路径
char pwd[basesize];
char pwdenv[basesize];

//myshell的环境变量表
char *genv[envnum];

string GetUserName()
{
	string name = getenv("USER");
	return name.empty() ? "None" : name;
}
string GetHostName()
{
	string hostname = getenv("HOSTNAME");
	return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
	//从系统中获取
	if (getcwd(pwd, sizeof(pwd)) == NULL)
	{
		return "None";
	}
	snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
	putenv(pwdenv);//PWD=xxx  更新环境变量
	return pwd;
//	string pwd = getenv("PWD");
//	return pwd.empty() ? "None" : pwd;
}

string LastDir()
{
	//  /home/yjz/code/xxx
	string cur = GetPwd();
	if (cur == "/" || cur == "None")
		return cur;
	size_t pos1 = cur.rfind("/");
	if (pos1 == string::npos)
		return cur;
	size_t pos2 = pos1 == 0 ? pos1 : pos1 - 1;
	size_t pos3 = cur.rfind("/", pos2);
	if (pos3 == string::npos)
		return cur;
	return cur.substr(pos3);
}

string MakeCmdLine()
{
	char cmd_line[basesize];
	snprintf(cmd_line, basesize, "%s@%s:~%s$ ", GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
	return cmd_line;
}

void PrintCmdLine()
{
	printf("%s", MakeCmdLine().c_str());
	fflush(stdout);
}

bool GetCmdLine(char buffer[], int size)
{
	char *result = fgets(buffer, size, stdin);//从标准输入流中读取输入指令
	if (result)
	{
    	buffer[strlen(buffer) - 1] = '\0';//去掉输入指令时的回车键
		if (strlen(buffer) == 0)
		{
			return false;//如果是空串直接返回
		}
		return true;
	}
	return false;
}

void ParseCmdLine(char buffer[], int len)
{
	//初始化
	memset(gargv, 0, sizeof(gargv));
	gargc = 0;

	const char *delim = " ";
	gargv[gargc++] = strtok(buffer, delim);
	while (gargv[gargc++] = strtok(NULL, delim));
	gargc--;
}

//void debug()
//{
//	printf("argc: %d\n", gargc);
//	for (int i = 0; gargv[i]; i++)
//	{
//		printf("agrv[%d}: %s\n", i, gargv[i]);
//	}
//}

//执行解析好的命令,为了防止程序崩溃挂掉,让子进程执行
bool ExeCmd()
{
	pid_t id = fork();
	if (id < 0)
	{
		return false;
	}
	if (id == 0)
	{
		//将myshell的环境变量表传给子进程
		execvpe(gargv[0], gargv, genv); 
	    exit(1);
	}
	
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if (rid > 0)
	{
		if (WIFEXITED(status))
		{
			lastcode = WEXITSTATUS(status);
		}
		else
		{
			lastcode = 100;
		}
		return true;
	}
	return false;
}

void AddEnv(char *item)
{
	int index = 0;
	while (genv[index])
	{
		index++;
	}
	genv[index] = (char*)malloc(strlen(item) + 1);
	strncpy(genv[index], item, strlen(item) + 1);
	genv[++index] = NULL;
}

//在shell中,有些命令,必须由子进程执行
//有些命令,只能shell自己执行——内建命令
//shell自己执行命令,本质是shell调用自己的函数
bool CheckAndExeBuiltCmd()
{
	if (strcmp(gargv[0], "cd") == 0)
	{
		//内建命令
		if (gargc == 2)
		{
			chdir(gargv[1]);
		}
		else
		{
			lastcode = 1;
		}
		return true;
	}
	else if (strcmp(gargv[0], "export") == 0)
	{
		if (gargc == 2)
		{
			AddEnv(gargv[1]);
		}
		else
		{
			lastcode = 2;
		}
		return true;
	}
	else if (strcmp(gargv[0], "env") == 0)
	{
		for (int i = 0; genv[i]; i++)
		{
			printf("%s\n", genv[i]);
		}
		lastcode = 0;
		return true;
	}
	else if (strcmp(gargv[0], "echo") == 0)
	{
		if (gargc == 2)
		{
			if (gargv[1][0] == '$')
			{
				if (gargv[1][1] == '?')
				{
					printf("%d\n", lastcode);
					lastcode = 0;
				}
			}
			else
			{
				printf("%s\n", gargv[1]);
				lastcode = 0;
			}
		}
		else
		{
			lastcode = 3;
		}
		return true;
	}
	return false;
}

//作为一个shell,获取环境变量应该从系I统的配置文件中读取
//这里直接从父shell中拷贝
void InitEnv()
{
    extern char **environ;
	int index = 0;
	while (environ[index])
	{
		genv[index] = (char*)malloc(strlen(environ[index]) + 1);
		strncpy(genv[index], environ[index], strlen(environ[index] + 1));
		index++;
	}
	genv[index] = NULL;
}

int main()
{
	InitEnv();
	char cmd_buffer[basesize];//获取指令缓冲区
	while (true)
	{	
        PrintCmdLine();//1、命令行提示符
		if(!GetCmdLine(cmd_buffer, basesize))//2、获取用户命令
		{
			continue;
		}
		//printf("%s\n", cmd_buffer);
	    ParseCmdLine(cmd_buffer, strlen(cmd_buffer));//3、分析命令
		//debug(); 
		if (CheckAndExeBuiltCmd())
		{
			continue;
		}
		ExeCmd();//4、执行命令
	}
	return 0;
}

本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值