引入
在学习了进程之后,我自己写了一个非常简单的自定义shell 用来巩固一下自己学到的知识 。
先来看一看shell:
显式地信息有用户名,主机名,冒号和当前的路径加上$符号,那么就可以在显示的时候先显示这部分内容:
void ShellShowMess()
{
getcwd(path,sizeof(path));
char* p = nullptr;
p = FindLastPath(path);
gethostname(hostname,sizeof(hostname));
std::printf("%s@%s:%s$ ",getenv("USER"),hostname,p);
}
这里,getcwd是直接获取当前进程的工作路径。gethostname是用来得到主机名,它是一个系统调用,用man手册是可以在第二章查到:
FindLastPath是我自己写的函数,用来找到该路径的最后一个目录:
//找到最后一个目录
char* FindLastPath(char* pa)
{
char* p = pa;
p = p + strlen(p) - 1;
while(1)
{
if(*p != '/')
{
p--;
}
else
{
p+=1;
break;
}
}
return p;
}
该段代码编译运行之后的效果是这样的:
现在已经初见雏形了,然而这还不够,我们是需要他能像真正的shell一样可以执行命令的。
于是,现在就需要得到和解析命令。
void GetCmd()
{
argc = 0;
fgets(cmd,sizeof(cmd),stdin);
cmd[strlen(cmd)-1] = '\0';
cmdbuff[argc++] = strtok(cmd," ");
while((bool)(cmdbuff[argc++] = strtok(nullptr," ")));
argc--;
int i = 0;
}
这里使用fgets来从标准输入中的到字符串,为什么不用scanf或者std::cin呢?原因是这两个函数是把空格作为输入的两个字符串的分隔符,而我们的命令是带有空格的,所以我们直接从标准输入拿字符串。
由于直接从标准输入中拿取字符串,所以这个字符串的最后一个字符是回车,所以需要进行一次特殊处理。
strtok是C标准库的字符串处理函数,用于将指定的字符替换为'\0',第一次调用的时候先替换,然后再返回替换后的字符的前边的字符串,例如“hello world”,,我要把空格进行替换,那么第一次就会把空格替换为'\0',再返回Hello的指针,第二次只需把NULL传入转换的字符处,就会自动处理上一次的字符串。当没有处理的处理的分隔符(在这里是空格),就会直接返回最后一个字符指针,当没有字符串进行处理的时候(这里是Hello World),就会返回空指针。
我在这里将空格替换为'\0',用来分割命令的参数。其中第一个肯定是命令,后续的肯定是参数,于是我们用另外一个数组cmdbuff去存储每个字符串的指针。这样就拿到了命令和它的选项参数。
接下来就是命令的执行了:
void Excute()
{
//创建子进程并执行命令
pid_t id = fork();
if(id == 0)
{
execvp(cmdbuff[0],cmdbuff);
_exit(1);
}
pid_t ret = waitpid(id,NULL,0);
if(ret < 0)
std::perror("waitpid");
}
因为我们不能影响到我们的父进程(这里使用了execvp来进行程序的替换,程序的替换是为了将该程序替换为其他程序来执行,并且程序的替换并不会创建新进程),所以在这里使用创建子进程来执行我们输入的命令,这里的程序替换的函数我们使用execvp,原因是第一个参数就是一个字符串(这里是命令),第二个参数可以传入字符的指针数组(这里是命令选项)。而exec系列的函数用同样可以在man手册里查到(以下是库函数):
而execve是系统调用:
可见库函数是在系统调用的基础上进行了封装。
当子进程执行完毕后,为了防止执行和父进程一样的代码,这里需要使用_exit(1)来终止子进程(不用exit(1)的原因是exit(1)会刷新缓冲区,这样增加了一些不确定性,例如刷新出不必要的内容)。最后使用waitpid()来获取进程的结束状态,避免僵尸进程的产生。
以下来看代码效果:
1.使用ls命令:
2.使用pwd命令:
3.使用cd命令:
哎?为什么cd命令不起作用呢?
在linux里,cd是属于内建命令(上边的pwd也是),因为pwd指数打印当前路径(子进程继承父进程的环境变量),而cd是改变当前进程的工作路径,但是我们的命令是在子进程里执行的,那么改变的也是子进程的路径,但是我们显示的是父进程路径,所以是需要改变父进程的路径,于是乎,这里可以写一个函数用来执行内建命令:
void BuiltInCmd()
{
std::string s = cmdbuff[0];
if(s == "cd")
{
chdir(cmdbuff[1]);
}
}
这个函数就是用来检查是否是内建命令。而if后还可以增加需要的内建命令,在这里我只写一个cd的命令。
内建命令的判断应该执行在非内建命令之前,保证逻辑的清晰: