【写在前面】:这是操作系统的实验,主要参考了这位大佬的csdn的代码,https://blog.csdn.net/qq_36172505/article/details/80372592。但是可能Ubuntu版本不同,所以他的代码在我这里出现了bug。改了很久的bug,也用gdb调试了许久,bug原因问了老师,老师现在还没答复,有了答复之后再更新。
实验目的
学习如何编写一个Unix Shell程序,使得有机会了解如何创建子进程来执行一项专门的工作以及父进程如何继续进行子进程的工作。熟悉进程概念,了解fork,execve,wait等系统调用。本实验的目的主要在于学会如何在Unix系统下创建进程和管理进程。
实验类型
综合型实验
预习要求
完成第三章进程的学习,了解进程的基本概念及进程的创建与管理。
实验设备与环境
PII以上电脑一台, 已安装Linux操作系统, VC++、GCC或其他C语言编译环境
实验原理
操作系统控制整个硬件与管理系统的活动监测,它不能被用户随意操作,若用户使用不当,可能会造成整个系统崩溃。但我们总是需要让用户操作系统的,这样就有了在操作系统上发展应用程序。用户可以通过应用程序来指挥内核,让内核来完成任务。在整个操作系统中,应用程序在最外层,就如同鸡蛋的外壳一样,这就是shell的由来。
在实现过程中,首先解析用户提交的命令行,通过fork()系统调用产生一进程,调用execvp()函数来完成命令所要求的操作。使用信号(signals)来通知进程事件的发生,并调用信号处理函数来完成处理工作。
实验任务
编写一个C语言程序作为Linux内核的Shell命令行解释程序,实现以下功能:
(1)解析用户提交的命令行;按照环境变量搜索目录系统;执行命令。
(2)提供ls、mkdir rmdir、pwd、ps等内部命令。
(3)提供历史查询功能。如用户按下Ctr1+C,信号处理器将输出最近的10个命令列表。
实验步骤和方法
- setup()函数读取用户的下一条命令(最多80个字符),然后将之分析为独立的标记,这些标记被用来填充命令的参数向量(如果将要在后台运行命令,它将以“&”结尾,setup()将会更新参数background,以使main()函数相应地执行)。当用户按快捷键Ctrl+D后,setup()调用exit(),此程序将被终止。
main()函数打印提示符COMMAND-> ,然后调用setup() ,它等待用户输入命令。用户输入命令的内容被装入一个args 数组。例如,如果用户在COMMAND-> 提示符处输入ls -1 , args[0]等同于字符串ls 和args[1 ]被设置为字符串 –l (这里的字符串指的是以0结束的C字符串变量)。
#include <stdio.h>
#include <unistd.h>
#define MAX LINE 80
void setup(char inputBuffer[], char *args[],int *background)
{
//用于解析命令行的
}
int main(void)
{
char inputBuffer[MAXLINE]; /* buffer to hold command entered */
int background; /* equals I if a command is followed by ,&' */
char *args[MAXLINE/2 + I]; /* command line arguments */
while(1)
{
background = 0;
printf(" COMMAND->");
/* setup() calls exit() when Control-D is entered */
setup(inputBuffer,,args, &background);
/* the steps are:
(1) fork a child process using fork()
(2) the child process will invoke execvp()
(3) if background = 1,the parent will wait,
otherwise it will invoke the setup() function again. */
}
}
1.创建子进程
修改main()函数,以使从setup()返回时,创建一个子进程,并执行用户的命令。如前面所指出的,setup()函数用用户指定命令装载args 数组的内容,args 数组将被传递给execvp()函数,该函数具有如下接口:
execvp(char *command, char *params[]);
其中command表示要执行的命令,params保存命令的参数。对于该项目,execvp()函数应作为execvp(args[0],args)来调用;需要保证检测background的值,以决定父进程是否需要等待子进程退出。
2.创建历史特性
信号处理函数应在main()之前声明,并且由于控制可在任意点传递给该函数,没有参数可以传递给它。因此,在程序中,它所访问的任意数据必须定义为全局,即在源文件的顶部、函数声明之前。在从信号处理函数返回之前,它应重发指令提示。
如果用户按下快捷键Ctr1+C, ,信号处理器将输出最近的10 个命令列表。根据该列表,用户通过输入"rx" 可以运行之前10 个命令中的任何一个,其中"x" 为该命令的第一个字母。如果有个命令以"x" 开头,则执行最近的一个。同样,用户可以通过仅输入"r"来再次运行最近的命令。可以假定只有一个空格来将"r" 和第一个字母分开,并且该字母后面跟着”\n” 。而且,如果希望执行最近的命令,单独的"r" 将紧跟\n。
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#define BUFFER SIZE 50
char buffer[BUFFER_SIZE];
/*信号处理函数*/
void handle_SIGINTO
{ //信号处理函数
write(STDOUT_FILENO, buffer, strlen(buffer));
exit (0);
}
int main(int argc, char *argv[])
{
/*创建信号处理器*/
struct sigaction hander;
handler.sa_handler = handle_SIGINT;
handler.sa_handler = handle_SIGINT;
sigaction(SIGINT, &handler, NULL);
/*生成输出消息*/
strcpy(buffer, "Caught Control C\n");
/*循环运行,直至接收到<Ctrl+C>*/
while(1)
;
return 0;
}
【话不多说,先贴代码,然后再做解释】
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#include <sys/shm.h>
#include <ctype.h>
#define HISTORYNUM 10
#define MAX_LINE 80
struct Queue{
char history[HISTORYNUM][MAX_LINE];
int front;
int rear;
}queue;
void add(char *str){ //添加命令进history,这里用循环队列实现
if(queue.front==-1)
{
strcpy(queue.history[queue.rear],str);
queue.front=queue.rear;
return;
}
queue.rear =(queue.rear+1)%HISTORYNUM;
strcpy(queue.history[queue.rear],str);
if(queue.front==queue.rear) queue.front=(queue.front+1)%HISTORYNUM;
}
//打印历史命令
void print(){
if(queue.front==-1){//空队列,无历史命令
printf("No command!");
return;
}
int index= queue.front;
printf("\n");
fflush(stdout);
printf("%s\n",queue.history[index]); //先输出第一个历史命令,否则循环队列将无意义,当超出历史命令条数时,循环无法进入
fflush(stdout);
index=(index+1)%HISTORYNUM;
while(index!=(queue.rear+1)&&(!(index==0&&queue.rear==9))){ //后面一个条件是防止这种情况进入死循环,因为,index永远不可能等于10
printf("%s\n",queue.history[index]);
fflush(stdout);
index=(index+1)%HISTORYNUM;
}
}
char *gethistory(char c){ //得到以‘x'开头的命令
int temp=queue.front;
while(temp!=queue.rear+1){
if(queue.history[temp][0]==c)return queue.history[temp];//找到就退出
temp=(temp+1)%HISTORYNUM;
}
if(temp==queue.rear+1) return NULL; //搜索完一遍还是没有,就返回NULL
}
char *getnearhistory(){ //返回最近的历史命令
if(queue.front==-1) return NULL; //无历史命令
else return queue.history[queue.rear];
}
int getNum(){ //得到历史命令的数目
return (queue.rear+HISTORYNUM-queue.front+1)%HISTORYNUM;
}
void setup(char *inputBuffer,char *args[],int *background)
{
int length;//命令的字符数目
int start;//命令的第一个字符位置
int ct; //下一个参数存入args[]的位置
ct = 0;
//读入命令行字符串,存入inputBuffer
length=read(STDIN_FILENO,inputBuffer,MAX_LINE);
start = -1;
if(length==0)exit(0);
else if(length<0) { //ctrl +c
perror("error reading the command");
args[0]=NULL;
return;
}
/*用户通过输入"rx" 可以运行之前10 个命令中的任何一个,
其中"x" 为该命令的第一个字母。如果有个命令以"x" 开头,则执行最近的一个。同样,
用户可以通过仅输入"r"来再次运行最近的命令*/
char *str;
if(inputBuffer[0]=='r')
{
if(inputBuffer[0]=='r'&&inputBuffer[1]=='\n') { //当只有'r'
str=getnearhistory();
if(str==NULL)
{ //取最近的一条命令,如果没有命令则返回NULL
perror("no nearhistory input command!");
return;
}
else strcpy(inputBuffer,str);
}
else if(inputBuffer[3]=='\n'||inputBuffer[3]=='\t')
{
str = gethistory(inputBuffer[2]);
//if((str=gethistory(inputBuffer[2])==NULL))
if(str==NULL)
{ //如果没有以x开头的命令,会返回null
perror("No input command!");
return;
} else strcpy(inputBuffer,str); //放入inputBuffer来执行
}
inputBuffer[strlen(str)]='\n';//以便下面代码统一读取
inputBuffer[strlen(str)+1]='\0';//结束符
length = strlen(str)+1;
}
int i;
//检查inputBuffer中每一个字符
for(i=0; i<length; i++) {
switch(inputBuffer[i]) {
case ' ':
case '\t'://字符为参数的空格或者tab
if(start!=-1) {
args[ct]=&inputBuffer[start];
ct++;
}
inputBuffer[i]='\0';//设置c string的结束符
start=-1;
break;
case '\n'://命令行结束符
if(start!=-1) {
args[ct]=&inputBuffer[start];
ct++;
}
inputBuffer[i]='\0';
args[ct]=NULL;
break;
case '&':
*background =1;
inputBuffer[i]='\0';
break;
default://其他字符
if(start==-1) start=i;
}
}
add(inputBuffer);//增加历史记录
args[ct]=NULL;//命令字符数>80
}
void handle_SIGINT();
int main(void){
char inputBuffer[MAX_LINE];//这个缓存用来存放输入的命令
int background;// == 1时,表示在后台运行命令
char *args[MAX_LINE/2+1];//命令最多40个参数
pid_t pid;
struct sigaction hander;
queue.front = -1;
queue.rear = 0;
while(1){//程序在setup中正常结束
background = 0;
printf("COMMAND->");
fflush(stdout);//输出
setup(inputBuffer, args, &background);
//这一步要创建子进程
if((pid = fork()) == -1) { printf("Fork Error.\n"); }//创建失败
if(pid == 0) { execvp(args[0], args); exit(0); }//子进程
if(background == 0) { wait(0); } //如果background为1,表示在后台运行,则父进程不用等子进程执行完,否则要wait
/**
*创建信号处理器,处理ctrl+c,+z等信号
*/
hander.sa_handler = (void (*)(int))handle_SIGINT;
sigaction(SIGINT, &hander, NULL);
/**
*循环运行,直到收到ctrl+c获ctrl+z等信号
*/
}
return 0;
}
void handle_SIGINT(){
printf("\nCOMMAND HISTORY:");
print(); //打印历史命令
signal(SIGINT, SIG_IGN);
}
本次shell要解决的两个大问题(这里后台运行的机制还不太清楚,感兴趣的小伙伴可以自行百度):
1.ctrl c输出历史指令
2.r 和rx指令
1问题的解决方案是
hander.sa_handler = (void (*)(int))handle_SIGINT;
sigaction(SIGINT, &hander, NULL);
这里hander的机制可以自行百度。然后代码是用了一个循环队列实现10个历史指令。因为输出历史指令最多10个,所以结构体就设置为:
struct Queue{
char history[HISTORYNUM][MAX_LINE];
int front;
int rear;
}queue;
这里HISTORYNUM为10,MAX_LINE为80,因为题目已经说了每一条指令不超过80个字符。然后front相当于头指针,rear相当于尾指针。
2.r 和rx指令的解决方案:
这块就比较坑了,源代码的r和rx在我的ubuntu上运行有bug。要么就直接不能执行要不就报空指针错误,gdb调试了半天之后发现了好几处错误,这里我贴一个错误来(还有几个忘掉了,感兴趣的朋友可以阅读原博主的代码做个对比即可)
这里调用了getnearhistory函数,用gdb调试之后发现,他的返回值并没有装到str里面:
可以看到红圈圈出来的是空指针,但是按道理来讲应该是会把函数的返回结果赋值给str,可偏偏就没有,就很气。然后如果把它单独放在一行里算,编译就不会报警告,然后运行也不会有空指针错误,就像下图那样:
这里不太清楚为什么,还希望有大神可以解答。
这里讲一下为什么是 else if(inputBuffer[3]=='\n'||inputBuffer[3]=='\t')
因为我这里是将rx指令处理成了r x,r和x中间会有空格,所以是inputBuffer[3]=='\n'||inputBuffer[3]=='\t'。
相信如果看代码注释的话能看懂的,不懂的地方可以在下方评论。