模拟实现Unix/Linux外壳
注:本文中的程序代码来自《深入理解计算机系统》一书,非本人原创。
Unix/Linux中一般工作在文本界面,也就是一个外壳之下,外壳也是一个程序,那么它是怎么实现的呢?本文带你实现一个简单的shell程序,Let’s go.
所谓外壳,实际上就是执行一系列的读/求值(read/evaluate)步骤,然后终止。读步骤取来自用户的一个命令行。求职步骤解析命令行,并代表用户运行程序。
不多说别的,先上代码,大体流程见代码之下的说明。
main.c
#include "csapp.h"
#define MAXARGS 128
/* Function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argc);
int main(){
char cmdline[MAXLINE];
while(1){
/* Read */
printf("> ");
fgets(cmdline,MAXLINE,stdin);
if(feof(stdin)){
exit(0);
}
/* Evaluate */
eval(cmdline);
}
exit(0);
}
/* eval - Evaluate a command line */
void eval(char *cmdline){
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE];
int bg;
pid_t pid;
strcpy(buf,cmdline);
bg = parseline(buf,argv);
/* Ignore empty lines */
if(argv[0] == NULL){
return;
}
if(!builtin_command(argv)){
/* Child runs user jobs */
if((pid = Fork()) == 0){
if(execve(argv[0], argv, environ) < 0){
printf("%s: Command not found.\n",argv[0]);
exit(0);
}
}
/* Parent waits for foreground job to terminate */
if(!bg){
int status;
if(waitpid(pid, &status,0) < 0){
unix_error("waitfg: waitpid error");
}
}else{
printf("%d %s",pid,cmdline);
}
}
return;
}
/* If first arg is a builtin command ,run it and return true */
int builtin_command(char **argv){
/* quit command */
if(!strcmp(argv[0],"quit")){
exit(0);
}
/* Ignore singleton */
if(!strcmp(argv[0],"&")){
return 1;
}
/* Not a builtin command */
return 0;
}
/* parseline - Parse the command line and build the argv array */
int parseline(char *buf, char **argv){
char *delim; /* Points to first space delimiter */
int argc; /* Number of args */
int bg; /* Background jobs? */
buf[strlen(buf) - 1] = ' '; /* Replace trailing '\n' with space */
/*Ignore leading spaces */
while(*buf && (*buf == ' ')){
buf ++;
}
/* Build the argv list */
argc = 0;
while((delim = strchr(buf,' '))){
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
/* Ignore spaces */
while(*buf && (*buf == ' ')){
buf ++;
}
}
argv[argc] = NULL;
/* Ignore blank line */
if(argc == 0){
return 1;
}
/* Should the job run in the background? */
if((bg = (*argv[argc - 1] == '&')) != 0){
argv[--argc] = NULL;
}
return bg;
}
说明:
1.外壳打印一个命令行提示符,等待用户在stdin上输入命令行,然后对这个命令行求值。
2.eval函数的首要任务是调用parseline函数,这个函数解析了以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。第一个参数被假设为要么是一个内置的外壳命令名,马上就会解释这个命令,要么是一个可执行目标文件,会在一个新的子进程的上下文中加载并运行这个文件。
3.如果最后一个参数是“&”字符,那么parseline返回1,表示应该在后台执行该程序(外壳不会等待他完成),否则他返回0,表示应该在前台执行这个程序(外壳会等待他完成)。
4.在解析了命令行之后,eval函数调用builtin_command函数,该函数检查第一个命令行参数是否是一个内置的外壳命令。如果是,它就立即解释这个命令,并返回1,否则返回0.
5.简单的外壳只有一个内置命令——quit命令,该命令会终止外壳。实际使用的外壳有大量的命令,如pwd,jobs,fg等。
6.如果builtin_command返回0,那么外壳创建一个子进程,并在子进程中执行所请求的程序。如果用户要求在后台运行该程序,那么外壳返回到循环的顶部,等待下一个命令行。否则,外壳使用waitpid函数等待作业终止。当作业终止时,外壳就开始下一轮迭代。
7.注意,这个简单的shell是有缺陷的,因为它并不回收它的后台子进程。
需要注意的是,在上面的程序中有csapp.h这个头文件,下面给两个地址可以找到csapp.h以及它的实现csapp.c:
csapp.h : http://csapp.cs.cmu.edu/2e/ics2/code/include/csapp.h
caspp.c : http://csapp.cs.cmu.edu/2e/ics2/code/src/csapp.c
如果某天不幸地资源不在了,可以到我个人的资源页面去下载:
http://download.csdn.net/detail/test1280/9629512
使用头文件的时候需要注意,因为csapp.c中有关于线程的头文件,在用gcc的时候最后要加上-lpthread 如 # gcc -o Ex Ex.c -lpthread,才能顺利通过。
下面给出两个测试的小代码:
code one:
hello.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **argv,char **envp){
int i;
for(i = 0 ; i < argc ; i++){
printf("argv[%2d] is %s\n",i,argv[i]);
}
int n = 0;
while(NULL != envp[n]){
printf("envp[%2d] is %s\n",n,envp[n]);
n++;
}
printf("this is hello.c , hello.c just a simple test!\n");
return 0;
}
hello.c主要的功能是输出参数列表argv以及环境变量数组envp的内容。
code two:
longJob.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char **argv,char **envp){
int i;
i = atoi(argv[1]);
while(i>0){
printf("now i is %d\n",i);
sleep(1);
i--;
}
return 0;
}
longJob.c就是单纯的耗费时间,模拟一个运算量很大的程序。
很明显地看出,在第二次执行 ./longJob 6 &的时候,shell没有等待子进程,而是直接到了主进程中,通过第二个红箭头处指的”>”可以辨别。
第三个红箭头处,并不是没有了提示符,而是提示符早就输出在了stdout中,而子进程与父进程的文件描述符表一样,当他们都向stdout输出时,会混在一起,但这个时候父进程确实已经输出了“>”,只是是在子进程输出之前,看起来不是那么明显。
以上模拟shell的那部分代码以及相关解释来自《深入理解计算机系统》一书,下面的两端代码是本人所写,存在很多不足以及潜在的bug。
上述内容如有错误,敬请指出,谢谢!