5. 贯穿案例2:mini shell(2)
(1)己经完成的功能:pwd、cd、exit命令
(2)阶段性目标:
①env、export、echo及其他命令
②标准输入、输出重定向">"、"<"、">>"
③设置后台进程
(3)存在问题:当mshell(后台进程)要读写终端时(如执行date时),进程会被暂停。解决方案见下一章的《信号》
【编程实验】mini shell
//job.h
#ifndef __JOB_H__ #define __JOB_H__ #include <sys/types.h> //重定向类型(这里只支持3种,<、>和>>) enum RedirectType{RedirectRead, RedirectWrite, RedirectAppend}; typedef struct { enum RedirectType redirect; //重定向的类型 int fd; //将标准输入、输出重定向到fd这个目标文件 }Redirection; //接收命令行参数 typedef struct { pid_t pid; //进程pid char** args; //对应于主函数中的char* argv[]参数 //每个命令允许使用多个重定向符号,放在以下数组中 //如:echo aaa>s.txt bbb>>s.txt Redirection* redirects; //堆上申请的数组 int redirect_num; }Program; //命令行中可以包含多个程序,如 //#date;ls -l,单个或多个命令通过cmd传入Job结构体中 typedef struct { char* cmd; //单条命令或多条命令(用分号隔开) int progs_num; //作业中包含程序的数量 Program* progs; //各个程序的命令行参数 pid_t pgid; //进程组ID,设置前(后)台进程 }Job; //创建作业 extern Job* create_job(char* cmd); //销毁作业 extern void destroy_job(Job* job); //创建进程(命令) extern Program* create_program(char** arg); //销毁进程(命令) extern void destroy_program(Program* prog); //将命令加入作业中 extern int add_program(Job* job, Program* prog); extern Redirection* create_redirect(int fd, enum RedirectType type); extern void destroy_redirect(Redirection* r); extern void add_redirection(Program* prog, Redirection* r); #endif
//job.c
#include "job.h" #include <malloc.h> #include <assert.h> #include <string.h> //创建作业 Job* create_job(char* cmd) { Job* job = (Job*)malloc(sizeof(Job)); assert( job != NULL); job->cmd = (char*)malloc(sizeof(char) * strlen(cmd)); assert(job->cmd != NULL); strcpy(job->cmd, cmd); job->progs_num = 0; job->progs = NULL; return job; } //销毁作业 void destroy_job(Job* job) { assert(job != NULL); free(job->progs); free(job->cmd); free(job); } //arg格式:command arg0 arg1 ==> 返回3 static int arg_num(char** arg) { int ret = 0; char* start = arg[0]; while(start != NULL){ start = arg[++ret]; } return ret; } //创建进程(命令) Program* create_program(char** arg) { Program* prog = (Program*)malloc(sizeof(Program)); assert(prog != NULL); prog->redirect_num = 0; prog->redirects = NULL; int counter = arg_num(arg); prog->args = (char**)calloc(counter + 1, sizeof(char*)); //以NULL结尾 int i = 0; for(i=0; i< counter; i++){ int len = strlen(arg[i]); prog->args[i] = (char*)malloc(len); assert(prog->args[i] != NULL); strcpy(prog->args[i], arg[i]); } prog->args[i] = NULL; //指针数组,以NULL结尾 return prog; } //销毁进程(命令) void destroy_program(Program* prog) { assert(prog != NULL); int i = 0; while(prog->args[i] != NULL) { free(prog->args[i++]); } free(prog->redirects); free(prog->args); free(prog); } //将命令加入作业中 int add_program(Job* job, Program* prog) { //重新申请一片空间以增加一条命令进来,放入job->progs中 Program* ps = (Program*)malloc(sizeof(Program) * (job->progs_num + 1)); memcpy(ps, job->progs, job->progs_num * sizeof(Program)); ps[job->progs_num++] = *prog;//将新的进程(命令)加入进来 free(job->progs); //释放旧的程序(命令)组 job->progs = ps; return job->progs_num - 1; //返回新命令的索引号 } Redirection* create_redirect(int fd, enum RedirectType type) { Redirection* r = (Redirection*)calloc(1, sizeof(Redirection)); assert( r!= NULL); r->fd = fd; r->redirect = type; return r; } void destroy_redirect(Redirection* r) { assert( r!= NULL); free(r); } void add_redirection(Program* prog, Redirection* r) { Redirection* rs = (Redirection*)calloc(prog->redirect_num + 1, sizeof(Redirection)); assert(rs != NULL); //复制原有数据 if(prog->redirects != NULL){ memcpy(rs, prog->redirects, prog->redirect_num* sizeof(Redirection)); free(prog->redirects); //释放原有的redirects } prog->redirects = rs; //将新的redirection加入数组中 memcpy(&prog->redirects[prog->redirect_num], r, sizeof(Redirection)); prog->redirect_num += 1; }
//mshell.c
#include "job.h" #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <malloc.h> #include <string.h> #include <assert.h> #include <sys/wait.h> char* prompt = "mshell> "; //命令行的提示符 #define MAX_COMMAND_LEN 256 //命令行最多的字符数 extern char** environ; //环境表指针 //前台和后台进程组标志 #define FOREGROUND 0 //前台进程组 #define BACKGROUND 1 //后台进程组 //env命令 void env_fun(void) { int i = 0; char* env = NULL; while((env = environ[i++]) != NULL){ printf("%s\n", env); } } //export命令 void export_fun(Program* prog) { //export格式:export CITY=ShangHai if(prog->args[1] == NULL){ fprintf(stderr, "export: invalid argument\n"); return; } putenv(prog->args[1]); } //echo命令 void echo_fun(Program* prog) { char* s = prog->args[1]; if(s == NULL){ fprintf(stderr, "echo: invalid argument\n"); return; } //echo的格式:echo $PATH //1. echo $PATH 从环境变量中读取 //2. echo PATH 直接输出echo后面的字符,如“PATH” if(s[0] == '$'){ char* v = getenv(s + 1); printf("%s\n", v); }else{ printf("%s\n", s); } } //cd命令 void cd_fun(Program* prog) { if(chdir(prog->args[1]) < 0){ perror("cd error"); } } //pwd命令 void pwd_fun(Program* prog) { char buffer[256]; memset(buffer, 0, sizeof(buffer)); if(getcwd(buffer, sizeof(buffer)) == NULL){ perror("pwd error"); } printf("%s\n", buffer); } //分析命令所带的参数(含进程名本身) void split_cmd(Job* job, char* arguments, int* bg) { char** args = (char**)calloc(MAX_COMMAND_LEN, sizeof(char*)); assert( args != NULL); char* cmd = strtok(arguments, " "); //1. 先取出命令名本身 args[0] = (char*)calloc(strlen(cmd) + 1, sizeof(char)); //命令本身 strcpy(args[0], cmd); Redirection* rs[5]; //一条命令中重定向的符号不会太多。为简单起见,假设为5个。 int redirect_num = 0; int i = 1; char* s = NULL; //2. 剩余的为参数部分 *bg = FOREGROUND; while((s = strtok(NULL, " ")) != NULL){ //将参数分隔出来 if(!strcmp(s, "&")){ //设置后台进程标志 *bg = BACKGROUND; continue; } if(!strcmp(s, "<")){ //如果参数< //格式:cat < s.txt char* file = strtok(NULL, " "); //重定向“<”后面的为文件名 if(file == NULL){ continue; }else{ //打开要重定向到的目标文件,因为后面要用dup2来完成重定向 int fd = open(file, O_RDONLY); //输入重定向,只需以只读打开 rs[redirect_num++] = create_redirect(fd, RedirectRead); } continue; }; if(!strcmp(s, ">")){ //格式:cat > s.txt char* file = strtok(NULL, " "); //重定向“>”后面的为文件名 if(file == NULL){ continue; }else{ //打开要重定向到的目标文件,因为后面要用dup2来完成重定向 //输出重定向,需以可写方式打开 int fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0777); rs[redirect_num++] = create_redirect(fd, RedirectWrite); } continue; } if(!strcmp(s, ">>")){ //格式:cat >> s.txt char* file = strtok(NULL, " "); //重定向“>>”后面的为文件名 if(file == NULL){ continue; }else{ int fd = open(file, O_WRONLY | O_CREAT | O_APPEND, 0777); rs[redirect_num++] = create_redirect(fd, RedirectAppend); } continue; } args[i] = (char*)calloc(strlen(s)+1, sizeof(char)); strcpy(args[i++], s); } //根据args创建一个Program Program* prog = create_program(args); int k = 0; for(; k < redirect_num; k++){ add_redirection(prog, rs[k]);//将所有重定向信息放入prog中 destroy_redirect(rs[k]); } add_program(job, prog); int j = 0; for(j=0; j < i; j++){ free(args[j]); } free(args); } //多条命令的解析 void parse_cmd(Job* job, char* line, int* bg) { char buff [MAX_COMMAND_LEN]={0}; //以“;”号分隔多条命令 char* pos = line; char* start = line; int count = 0; while( start < (line + strlen(line)) ){ //将参数分隔出来 memset(buff, 0, sizeof(buff)); if((pos = strchr(pos, ';')) == NULL) { pos = line + strlen(line); } count = pos-start; if(count > 0 ){ memcpy(buff, start, count); split_cmd(job, buff, bg); } start = ++pos; } } //执行命令 void execute_cmd(Job* job, int bg) { int i = 0; for(i=0; i<job->progs_num; i++) { if(!strcmp(job->progs[i].args[0], "cd")){ //cd命令 cd_fun(&job->progs[i]); }else if(!strcmp(job->progs[i].args[0], "pwd")){ //pwd命令 pwd_fun(&job->progs[i]); }else if(!strcmp(job->progs[i].args[0], "exit")){ //exit命令 exit(0); }else if(!strcmp(job->progs[i].args[0], "env")){ //env命令 env_fun(); }else if(!strcmp(job->progs[i].args[0], "export")){ //export命令 export_fun(&job->progs[i]); }else if(!strcmp(job->progs[i].args[0], "echo")){ //echo命令 echo_fun(&job->progs[i]); }else{ //其他命令用exec函数完成 //创建子进程来执行其它命令 pid_t pid; if((pid = fork()) < 0 ){ perror("fork error"); }else if(pid == 0){ //child process //第1个子进程创建新的进程组,以后的子进程加入到该组当中 if(i==0){ if(setpgid(getpid(), getpid()) < 0){ //组长进程 perror("setpgid error!"); } job->pgid = getpgid(getpid()); //保存组长进程ID }else{ //其余子进程加入到新的进程组中 if(setpgid(getpid(), job->pgid) < 0){ perror("setpgid error"); } } //设置前台进程组 if(bg == FOREGROUND){ tcsetpgrp(0, getpgid(getpid())); } //对标准输入、标准输出和追加输出进行重定向 int k = 0; job->progs[i].pid = getpid(); //每条命令会启动一个子进程,记录其pid for(; k<job->progs[i].redirect_num; k++){ if(job->progs[i].redirects[k].redirect == RedirectRead){ //将标准输入重定向到指定的文件中 if(dup2(job->progs[i].redirects[k].fd, STDIN_FILENO) != STDIN_FILENO){ perror("dup2 error"); } } if((job->progs[i].redirects[k].redirect == RedirectWrite) || (job->progs[i].redirects[k].redirect == RedirectAppend)){ //将标准输出重定向到指定的文件中 if(dup2(job->progs[i].redirects[k].fd, STDOUT_FILENO) != STDOUT_FILENO){ perror("dup2 error"); } } } //end for2 //调用exec函数执行系统中的其它命令 if(execvp(job->progs[i].args[0], job->progs[i].args) < 0){ perror("execvp error"); exit(1); //子进程退出 } }else{ //parent process if(i == 0){ //与子进程执行相同的逻辑,以确保不会因进程调度而出现错误 //由minishell启动的所有子进程默认放到一个新的进程组,组长进程为 //第1个子进程 if ((setpgid(pid, pid)) < 0) { perror("setpgid error"); } job->pgid = pid; }else{ //其余子进程加入到新的进程组中去 if((setpgid(pid, job->pgid)) < 0){ perror("setpgid error"); } } if(bg == FOREGROUND){ tcsetpgrp(0, job->pgid); //等待子进程组结束,含曾被暂停过的(阻塞) waitpid(-job->pgid, NULL, WUNTRACED); } //后台进程 if(bg == BACKGROUND){ waitpid(-job->pgid, NULL, WNOHANG); //非阻塞 } } //end fork } } //end for1 } int main(int argc, char* argv[]) { //将mshell本身设置成一个进程组 setpgid(getpid(), getpid()); char buffer[MAX_COMMAND_LEN]; memset(buffer, 0, MAX_COMMAND_LEN); ssize_t size = strlen(prompt) * sizeof(char); write(STDOUT_FILENO, prompt, size); ssize_t len = 0; int bg; //设置前台和后台进程组标志 while(1){ len = read(STDIN_FILENO, buffer, MAX_COMMAND_LEN); buffer[len -1] = 0; //以NULL结尾 if(strlen(buffer) > 0){ Job* job = create_job(buffer); //解析命令 parse_cmd(job, buffer, &bg); //执行命令 execute_cmd(job, bg); destroy_job(job); } write(STDOUT_FILENO, prompt, size); memset(buffer, 0, MAX_COMMAND_LEN); } return 0; }