浅墨浓香

想要天亮进城,就得天黑赶路。

导航

第8章 信号(6)_贯穿案例2:mini shell(3)

Posted on 2017-02-16 14:14  浅墨浓香  阅读(282)  评论(0编辑  收藏  举报

4. 贯穿案例2:mini shell(3)

(1)之前存在问题

  ①刚运行时,mshell作为前台进程。运行的其他命令会被加入新的进程组,并且调用tcsetpgrp将这个进程组设置为前台进程组,因此mshell本身所在的进程组就成为后台进程组

  ②SIGTTIN信号表示后台进程组的成员读控制终端时会产生的信号。而SIGTTOU信号表示后台进程组的成员写控制终端是产生的信号这两个信号的默认操作是暂停进程。因此mshell上运行一些命令后,处于后台进程的mshell会试图返回到提示符状态下,这将导致mshell试图读写控制终端,从而造成程序被暂停

(2)解决方案:忽略或捕获SIGTTIN和SIGTTOU信号

【编程实验】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 <signal.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
                 
                 //由于子进程继承了父进程的信号处理方式,因此子进程应恢复
                 //为默认的(如可以被ctrl-c终止,ctrl-z暂停或读写终端等)
                 signal(SIGTTIN, SIG_DFL);
                 signal(SIGTTOU, SIG_DFL);
                 signal(SIGINT,  SIG_DFL);
                 signal(SIGTSTP, SIG_DFL);
                 signal(SIGCHLD, SIG_DFL);

                 //第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
}

//信号处理函数
void sig_handler(int signo)
{
    if(signo == SIGCHLD){
        //回收进程组中所有的子进程
        //每个子进程结束时,父进程都会收到这个信号。
        waitpid(-1, NULL, WNOHANG); //非阻塞
        //将mshell设置为前台进程
        tcsetpgrp(0, getpgid(getpid()));
    }
}

int main(int argc, char* argv[])
{
    //将mshell本身设置成一个进程组
    setpgid(getpid(), getpid());

    signal(SIGTTIN, SIG_IGN); //忽略SIGTTIN信号
    signal(SIGTTOU, SIG_IGN); //忽略SIGTTOU信号
    signal(SIGINT,  SIG_IGN); //忽略ctrl-c信号,防止mshell被ctrl-c杀掉
    signal(SIGTSTP, SIG_IGN); //忽略ctrl-z,防止mshell被ctrl-z暂停
    signal(SIGCHLD, sig_handler);  //捕获信号。回收子进程,同时将mshell设为前台进程

    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;
}