在学习了进程控制的基础上实现一个小型的miningshell,加深对进程控制的掌握,这里实现的仅仅是一个简易版,只能完成一些基础简单的功能!
目录
我们在使用shell,输入完一个命令后它会继续弹出提示符等待我们继续输入,所以这应该有一个死循环等待用户输入,所以应该先设置一个死循环输入,因为我们要执行完指令后还能继续输入,因此,执行命令的应该是创建一个子进程去执行,而不是父进程执行,否则执行完毕该程序就结束了,无法继续输入,但是有些命令是内建命令,就需要在父进程中执行,如cd,export,echo......
显示提示符
首先是显示提示符,如下:
就需要我们获取当前用户的用户名,主机号和当前目录,调用系统接口并对输出的格式进行转化即可,具体代码如下:
//获取用户名
uid_t uid = getuid();
struct passwd *pw = getpwuid(uid);
printf("[%s@", pw->pw_name);
//获取主机号
char hostname[256];
if (gethostname(hostname, sizeof(hostname)) != 0) {
perror("gethostname error");
return 1;
}
printf("%s:", hostname);
//获取当前目录
char cwd[4096];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s]$ ", cwd);
} else {
perror("getcwd error");
return 1;
}
//刷新缓冲区
fflush(stdout);
运行该代码则会显示提示符,由于输出已设置好格式,就会像上图一样,不过会处于死循环的打印该行在没有进行死循环的输出情况如下:
(注:我们会发现Linux系统的实现对于家目录到用户目录那一块是~,但这里实现的不是,需要对获取当前目录的输出进行修改输出进行转换)
这样显示提示符就完成了
获取用户输入
shell若要执行我们输入的命令就需要获取到我们的输入,由于我们是使用一个字符数组获取的输入,因此我们每次在读入用户输入时就需要将数组先清空,这里使用的memset函数,其次使用的是fgets函数获取用户输入,使用gets函数也是一样的,fgets函数的声明如下:
需要我们给他传存储获取到的输入,接收的数据大小,以及输入流
memset(command_line, '\0', sizeof command_line);
fgets(command_line, NUM, stdin);
command_line[strlen(command_line)-1] = '\0';//清空输入的\n
这里是第三行代码存在的意义是我们在输入完成后会敲一下回车,而这个回车就会被fgets作为\n读入到字符数组command_line中,但我们在执行完命令后本来就是会换行的,因此会出现多一个换行符,所以需要将读入的\n去掉,我们无法改变系统函数!
切分字符串
读完用户的输入的字符串就需要将读入的字符串进行切分,以"ls -a -l -i"为例,我们需要将其分开分别将"ls" "-a" "-l" "-i"切出来然后传给进程替换函数进行程序替换,切分字符串这里使用的strtok函数,其声明如下:
参数分别是需要被切割的字符串和分隔符,返回的就是被切割的字符串
command_args[0] = strtok(command_line, SEP);
int index = 1;
//给ls加上颜色
if(strcmp(command_args[0], "ls") == 0)
command_args[index++] = (char*)"--color=auto";
//strtok截取成功返回字符串起始地址,截取失败返回NULL,若command_line已经截取过想继续截取该字符串则传NULL
while(command_args[index++] = strtok(NULL, SEP));
这里使用command_args数组来存储被切割下来的字符串,第一个位置就是我们输入的命令,然后再将剩下的字符串继续切割下来往command_args数组下标为1的开始往后存储,直至全部切完后切割不了strtok返回NULL,while中的strtok传的NULL是因为我们要继续从已经被切割剩余的字符串切割,不能传command_line,否则就是一直切割第一个字符串,死循环了就
中间插入的两行代码的意义:我们在执行ls时,发现文件是有颜色的,就是因为我们使用的ls其实应该是下图这样:
alias是取别名的意思,因此其实我们使用的ls并不是原生的ls,而是带了参数的ls --color=auto,被取别名成ls,所以我们使用系统的shell时有些文件是有颜色的。
因此,我们也可以像他这样,当传入的命令是ls时,就给存储拆分字符串数组command_args中先放上一个参数--color=auto即可,就可以和系统的ls一样了
TODO——内建命令
由于是miningshell,因此这里只实现了cd和export命令。
//内建命令cd,因为cd要改变父进程而不是子进程
if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL )
{
chdir(command_args[1]);
continue;
}
//内建命令export导入环境变量,将环境变量变成全局的
if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
strcpy(env_array, command_args[1]);
putenv(env_array);
continue;
}
cd命令
由于改变路径我们想要的是改变父进程的路径而不是子进程路径,子进程执行完命令就die了,所以cd要被作为内建命令实现,当我们输入cd路径时,需要判断输入是否合法,合法则调用系统接口chdir函数来改变我们父进程的路径,chdir函数的声明如下:
将新的路径传入即可,即我们切割字符串存储数组command_args的第二个位置就是路径即command_args[1],由于是内建命令,因此执行完毕后就continue开始下一轮命令的读入
export命令
在学习环境变量时我们知道环境变量是全局属性的,从当前进程往下的每一个进程都会继承当前进程的环境变量,而我们知道export就可以将我们自建的环境变量导入到系统的环境变量中(shell层面),所有导入的环境变量最后最会变成shell的内容,即变成shell中的环境变量。
所以这里有个问题,如果是在子进程中导入我们自建的环境变量则无法影响到shell,只能影响到从子进程往下的进程,此时在shell中就无法查询到该自建的环境变量。
因此我们需要将export作为自建命令来实现,不能交由子进程来执行该命令。
使用putenv函数将自建环境变量导入到系统环境变量,声明如下:
即将command_args数组第二个位置的字符串路径传入给putenv函数。
但是这里还有个问题,由于我们执行完内建内建命令后会进行下一轮更新等待用户输入,因此command_args数组会被覆盖,那之前存储的环境变量就不复存在了,再打印该环境变量就会变成null即不存在该环境变量,所有我们需要一个全局变量来存储我们的环境变量,因为环境变量需要具有全局性,能够被子进程继承,所以这里为了测试设置了一个字符串数组env_array(正确做法应该是开辟一块空间给它,不能随意被释放)用来存储环境变量,将需要导入的环境变量拷贝到env_array中存储下来即可
以上的两个问题可以创建一个程序打印自建的环境变量,在我们自己实现miningshell中使用export导入自建的环境变量,然后执行打印自建的环境变量查看输出结果来验证
创建进程执行命令and程序替换
不是内建命令交给子进程去执行,让子进程执行完毕即可die,让子进程进行程序替换,执行命令
pid_t id = fork();
if(id == 0)
{
//6. 程序替换
execvp(command_args[0], command_args);//选择该函数进行替换,因为用户输入的是系统命令在PATH中
exit(1);//替换失败
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("wait successfully! singlecode : %d, exitcode : %d\n", status&0x7F, (status>>8)&0xFF);
这里创建进程后设置的是阻塞状态下等待子进程结束,也可以设置成非阻塞状态等待,这里的程序替换函数选择的是execvp函数,因为我们替换时,是将存储被切割成多个字符串的command_args数组传给替换函数,因此选择execvp函数更方便
总结and代码实现
至此,一个简单的miningshell就完成了,实现miningshell是为了加深对进程控制这块的理解,以下是代码实现:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <pwd.h>
#define NUM 1024
#define SIZE 128
#define SEP " "
char command_line[NUM];//存储输入的字符串
char *command_args[SIZE];//存储切割完的字符串
char *env_array[NUM];//用来存储全局的环境变量
int main()
{
while(1)
{
//1. 显示提示符
//获取用户名
uid_t uid = getuid();
struct passwd *pw = getpwuid(uid);
printf("[%s@", pw->pw_name);
//获取主机号
char hostname[256];
if (gethostname(hostname, sizeof(hostname)) != 0) {
perror("gethostname error");
return 1;
}
printf("%s:", hostname);
//获取当前目录
char cwd[4096];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s]$ ", cwd);
} else {
perror("getcwd error");
return 1;
}
//刷新缓冲区
fflush(stdout);
//2. 获取用户输入
memset(command_line, '\0', sizeof command_line);
fgets(command_line, NUM, stdin);
command_line[strlen(command_line)-1] = '\0';//清空输入的\n
//3. 切分字符串
command_args[0] = strtok(command_line, SEP);
int index = 1;
//给ls加上颜色
if(strcmp(command_args[0], "ls") == 0)
command_args[index++] = (char*)"--color=auto";
//strtok截取成功返回字符串起始地址,截取失败返回NULL,若command_line已经截取过想继续截取该字符串则传NULL
while(command_args[index++] = strtok(NULL, SEP));
//4. TODO
//内建命令cd,因为cd要改变父进程而不是子进程
if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL )
{
chdir(command_args[1]);
continue;
}
//内建命令export导入环境变量,将环境变量变成全局的
if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
strcpy(env_array, command_args[1]);
putenv(env_array);
continue;
}
//5. 创建进程执行命令
pid_t id = fork();
if(id == 0)
{
//6. 程序替换
execvp(command_args[0], command_args);//选择该函数进行替换,因为用户输入的是系统命令在PATH中
exit(1);//替换失败
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("wait successfully! singlecode : %d, exitcode : %d\n", status&0x7F, (status>>8)&0xFF);
}
return 0;
}