shell实现要求
Shell主体为一个while循环,对用户键盘输入命令进行操作并给出反馈。
该实验中,我们主要实现以下几个命令:
- cd:shell也是一个程序,启动时迷你型会给它分配一个当前工作目录,利用chdir系统调用可以移动Shell的工作目录。
- history n:保存每次输入的命令,打印最近n条命令。
- exit:退出Shell的while循环,结束Shell的main函数。
- mytop:参考Minix终端输入top命令的输出信息,最终输出总体内存大小、空闲内存大小、缓存大小和总体CPU使用占比。
- 后台运行:对末尾包含&参数的命令,通过将子进程的标准输入、输出映射成 /dev/null来屏蔽键盘和控制台,并调用signal(SIGCHLD,SIG_IGN),使minix接管此进程,shell不等待进程结束,直接返回。
- 重定向:利用dup2函数,将某个打开文件的文件描述符fd映射到标准输入or输出,实现覆盖写“>”、追加写“>>”和文件输入“<”。
- 管道:利用fork及dup2实现进程间同步。
shell实现代码
对输入命令,Shell先将其分解成单词序列,再根据命令名称分为两类分别处理。
一类是shell内置命令(如cd,history,exit),识别后,执行对应操作。
另一类是program命令(如/bin/ 目录下的ls,grep ),识别后,创建一个新进程执行命令,并等待进程结束。
参数设置
#define M 256
#define SIZE 256
char path[M], input[M], *command[M], *temp, history[M][M]; //save path, input command, processes command, a temporary char, historical command
int i, j, n_com, n_his = 0; //n_com: the number of blocks of a command, n_his:the number of all commands
int k, h, flag_back, flag_out, flag_add, flag_in, flag_pipe, pid, pid2, status, status2, fd1, fd2; //flag: 分别标识后台运行、覆盖写、追加写、输入、管道状态
char file_out[M], file_in[M], *command2[M];
char buffer[SIZE] = "", *pagesize, *total, *freepage, *cached, *temp1; //*pagesize不要赋为pagesize[SIZE],否则后期无法用 atoi()转为数字
int count = 0, fd;
Shell主体
int main(int argc, char *argv[]) {
while (1) {
//get path
getcwd(path, sizeof(path) - 1);
printf("shell:%s%% ", path);
//initialize parameter
memset(input, 0, sizeof(input));
memset(command, 0, sizeof(command));
n_com = 0;
flag_back = 0;
flag_out = 0;
flag_add = 0;
flag_in = 0;
flag_pipe = 0;
memset(command2, 0, sizeof(command2));
gets(input); //get command
/* save command
* ——!!! The code should follow "gets(input)", or we can only get command[0] (it was cut by "strtok").
*/
for (i = 0; i < M; ++i) {
history[n_his][i] = input[i]; //notice:history[0] can't be empty
}
++n_his;
//break command to get parameter
temp = strtok(input, " "); //char *strtok(char *s, char *delim):以delim中的字符为分界符,将s切分成一个个子串
while (temp != NULL) {
command[n_com] = temp;
n_com++;
temp = strtok(NULL, " "); //将strtok第一个参数赋为空值NULL,继续分解字符串
}
/* 命令相关操作 */
}
return 0;
}
该部分是在进行解析命令之前做的准备工作,将输入的字符串命令分割成块并依次存入command中。在此之前,注意要将输入的命令存入history数组中作为命令的记录。
三个较简单命令
//parse the command
if (!strcmp(command[0], "cd")) {
if (n_com != 1) {
chdir(command[1]);
}
} else if (!strcmp(command[0], "exit")) {
if (n_com == 1) {
break;
}
} else if (!strcmp(input, "\000")) {
continue;
}
如上实现的是对cd、exit和无输入情况的实现,其中对n_com != 1进行判断以确保命令只有1块。
history n
else if (!strcmp(command[0], "history")) {
if (n_com != 1) {
int t = atoi(command[1]); // notice: command[1] is a string, not a char!
for (j = n_his - t; j < n_his; ++j) {
printf("%d ", j + 1);
puts(history[j]);
}
}
}
该命令接上一部分的if-else语句,因为command[1]是字符串,故需用atoi()函数来确定需要的最近n条命令,并用for循环将其打印到屏幕。
mytop
else if (!strcmp(command[0], "mytop")) {
if (n_com == 1) {
// method 1: 内存
int memory_sum=0,memory_free=0,cache_size=0;
fd = open("/proc/meminfo",O_RDONLY);
count=read(fd,buffer,SIZE);
temp1=strtok(buffer," ");
pagesize=temp1;
temp1=strtok(NULL," ");
total=temp1;
temp1=strtok(NULL," ");
freepage=temp1;
temp1=strtok(NULL," ");
temp1=strtok(NULL," ");
cached=temp1;
close(fd);
memory_sum=atoi(pagesize)*atoi(total)*1.0/1024;
memory_free=atoi(pagesize)*atoi(freepage)/1024;
cache_size=atoi(pagesize)*atoi(cached)/1024;
printf("main memory: %dK total, %dK free, %dK cached\n",memory_sum,memory_free,cache_size);
}
}
这是一种输出总体内存大小、空闲内存大小和缓存大小的方法。
在/proc/meminfo中,可查看内存信息。文件中的每个参数对应含义依次是页面大小pagesize, 总页数量total , 空闲页数量free ,最大页数量largest ,缓存页数量cached 。
计算内存大小公式: (pagesize * total)/1024
输出结果为:
*
接下来第二种方法,是参考top源码的代码实现的,有兴趣的话可以在此处https://github.com/0xffea/MINIX3/blob/master/usr.bin/top/top.c查看top源码
else if (!strcmp(command[0], "mytop")) {
if (n_com == 1) {
//method 2 ——reference source of top
int r, c, s = 0;
int cputimemode = 1; /* bitmap. */
if (chdir(_PATH_PROC) != 0) {
perror("chdir to " _PATH_PROC);
return 1;
}
system_hz = (u32_t) sysconf(_SC_CLK_TCK);
getkinfo();
while ((c = getopt(argc, argv, "s:B")) != EOF) {
switch (c) {
case 's':
s = atoi(optarg);
break;
case 'B':
blockedverbose = 1;
break;
default:
fprintf(stderr,
"Usage: %s [-s<secdelay>] [-B]\n",
argv[0]);
return 1;
}
}
if (s < 1)
s = 2;
/* Catch window size changes so display is updated properly
* right away.
*/
signal(SIGWINCH, sigwinch);
/* while循环控制无限动态刷新,直到 Ctrl-C
* 或用for循环控制刷新次数,便于退出回到shell
*/
for (j=0;j<10;j++){
// while (1) {
slot = 0; // notice: slot should be reset before each dynamic refresh ——注意每次动态刷新都要重置为0
//控制动态更新间隔所需参数
fd_set fds;
int ns;
struct timeval tv;
showtop(cputimemode, r); //打印结果
tv.tv_sec = s;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
if ((ns = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv)) < 0 //设置动态更新间隔sleep
&& errno != EINTR) {
perror("select");
sleep(1);
}
if (ns > 0 && FD_ISSET(STDIN_FILENO, &fds)) {
char c;
if (read(STDIN_FILENO, &c, 1) == 1) {
switch (c) {
case 'q':
putchar('\r');
return 0;
break;
case 'o':
order++;
if (order > ORDER_HIGHEST)
order = 0;
break;
case TIMECYCLEKEY:
cputimemode++;
if (cputimemode >= (1L << CPUTIMENAMES))
cputimemode = 1;
break;
}
}
}
}
}
其中,用while或for循环控制其动态更新,而此时若不注意重置slot的话,则会导致slot最终大于nr_total,使屏幕一直输出:“top: unreasonable endpoint number xxx”。
后台运行&
if (!strcmp(command[n_com - 1], "&")) {
flag_back = 1;
}
if ((pid = fork()) < 0) {
printf("fork error\n");
return -1;
}
if (pid == 0) {
if (flag_back) {
// fd1=open("/dev/null",O_RDWR | O_CREAT | O_TRUNC, 0644);
fd1 = open("/dev/null", O_RDONLY);
dup2(fd1, 0);
dup2(fd1, 1);
dup2(fd1, 2);
signal(SIGCHLD, SIG_IGN);
}
}
else {
if (flag_back) {
printf("[process id %d]\n", pid); //若为后台程序,则输出进程号
continue;
} else {
if (waitpid(pid, &status, 0) == -1) {
}
}
}
在该实验中,若为&后台运行命令,则&必定出现在输入命令的最后一部分,故检验command[n_com-1]位来确定是否为&命令,以标识flag_back的状态。
之后,需fork进程,并在子进程中将标准输入、输出、错误都重定向到“/dev/null”,最后用 signal(SIGCHLD, SIG_IGN)来使Minix接管此进程。
最后,在父进程中附加代码printf("[process id %d]\n", pid),使得有后台进程时,输出其进程号。
重定向、管道
状态确定:
for (k = 0; k < n_com; k++) {
if (!strcmp(command[k], ">")) {
flag_out = 1;
strcpy(file_out, command[k + 1]); //">" 后面即使重定向指向的文件
// for (h = k; h < n_com - 2; h++) { //在该实验中不需要
// command[h] = command[h + 2];
// }
command[n_com - 2] = NULL;
n_com -= 2;
k--;
} else if (!strcmp(command[k], ">>")) {
flag_add = 1;
strcpy(file_out, command[k + 1]);
command[n_com - 2] = NULL;
n_com -= 2;
k--;
} else if (!strcmp(command[k], "<")) {
flag_in = 1;
strcpy(file_in, command[k + 1]);
command[n_com - 2] = NULL;
n_com -= 2;
k--;
} else if (!strcmp(command[k], "|")) {
flag_pipe = 1;
for (h = 0; h < k; h++) {
command2[h] = command[h]; //将 "|" 之前的命令移至command2中
}
command2[k] = NULL;
for (h = 0; h < n_com - k - 1; h++) {
command[h] = command[h + k + 1]; //重置command保存 "|" 之后的命令
}
command[n_com - k - 1] = NULL;
break;
}
此处,由于这些命令的标识符可能出现在输入命令中的任意一处,故需用一个for循环来确认其位置,而后设置它们的状态flag_xxx为1。而标识符“>”、“>>”、“<”之后一位则是重定向指向的文件,故将它们赋为file_out、file_in的值,便于之后重定向操作处理。
对管道的操作稍复杂,要将“|”之前的命令移到command[2]中保存备用,再更新command数组存“|”之后的命令备用。
命令执行:
if ((pid = fork()) < 0) {
printf("fork error\n");
return -1;
}
if (pid == 0) {
if (flag_out) // >
{
fd1 = open(file_out, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 < 0) {
printf("open error!\n");
return 0;
}
dup2(fd1, 1);
}
if (flag_add) // >> : O_APPEND
{
fd1 = open(file_out, O_RDWR | O_CREAT | O_APPEND, 0644);
if (fd1 < 0) {
printf("open error!\n");
return 0;
}
dup2(fd1, 1);
}
if (flag_in) // <
{
fd1 = open(file_in, O_RDONLY);
// fd1 = open(file_in,O_WRONLY|O_CREAT|O_TRUNC, 0644);
if (fd1 < 0) {
printf("open error!\n");
return 0;
}
dup2(fd1, 0);
}
if (flag_pipe) // |
{
if ((pid2 = fork()) < 0) {
printf("fork2 error\n");
return -1;
} else if (pid2 == 0) //子子进程中以写方式打开pipe
{
fd2 = open("/tmp/tempfile", O_WRONLY | O_CREAT | O_TRUNC, 0644); //新建一个临时文件,后删除
dup2(fd2, 1);
execvp(command2[0], command2);
exit(0);
}
if (waitpid(pid2, &status2, 0) == -1) //等待子子进程结束以回收
{
}
fd2 = open("/tmp/tempfile", O_RDONLY); //子进程中以读方式打开管道
dup2(fd2, 0);
}
execvp(command[0], command); //执行命令
remove("/tmp/tempfile");
exit(0);
} else {
if (waitpid(pid, &status, 0) == -1) {
}
}
同&命令一样,fork一个进程(它们和&在同一个fork的进程中,只不过此处省略了&的相关操作)。根据各flag_xxx的状态来确定要执行哪一个操作。
在重定向中,用fd1 = open(file_xxx, XXX);打开所需文件,在“>”中需用到可读可写O_RDWR | O_CREAT | O_TRUNC, 0644 ,在“>>”中需将O_TRUNC 改为O_APPEND 追加写模式,而“<”则用只读O_RDONLY。之后用dup2()函数分别重定向它们的标准输入、输出即可。
在管道中,由于涉及到“|”前后两个文件的协调控制问题,故需在子进程中再fork一个进程,在子子进程中重定向前一个文件的标准输出,让其写入一个新建的临时文件“/tempfile”中。而后,等待该子子进程结束并将其回收后,在重定向后一个文件的标注输入,将前一个文件写入临时文件“/tempfile”中的内容传给后一个文件,以达到管道的控制目的。
在对以上操作定义完后,用execvp(command[0], command)将命令执行,且对管道命令,还要讲中途新建的文件“/tempfile”删除,执行完成后退出子进程。最后在父进程中等待子进程结束并将其回收。
至此,shell的全部内容已完成。
实验结果
- cd
- ls –a –l
- ls –a –l > result.txt
- vi result.txt
- grep a < text.txt (事先ls –a –l > text.txt)
- ls –a –l | grep a
- vi result.txt &
- history 9
- mytop (实际运行时是动态刷新的)
- exit