一个基于C的shell实验

CSAPP | Shell Lab 深入解析

实验概览

Shell Lab 要求实现一个带有作业控制的 Unix Shell 程序,需要考虑基础的并发,进程控制以及信号和信号处理。做这个实验之前一定要将 CSAPP 的第八章(异常控制流)仔细研读。

需要我们实现的函数:

  • eval:解析命令行 [约 70 行]
  • builtin_cmd:检测是否为内置命令quitfgbgjobs [约 25 行]
  • do_bgfg:实现内置命令bgfg [约 50 行]
  • waitfg:等待前台作业执行完成 [约 20 行]
  • sigchld_handler:处理SIGCHLD信号,即子进程停止或者终止 [约 80 行]
  • sigint_handler:处理SIGINT信号,即来自键盘的中断ctrl-c [约 15 行]
  • sigtstp_handler:处理SIGTSTP信号,即来自终端的停止信号 [约 15 行]

作者提供的帮助函数及功能:

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);  //解析命令行参数,如果后台运行则返回 1
void sigquit_handler(int sig);

void clearjob(struct job_t *job);               //清除job结构体
void initjobs(struct job_t *jobs);              //初始化jobs列表
int maxjid(struct job_t *jobs);                 //返回jobs列表中jid最大值
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);    //在jobs列表中添加job
int deletejob(struct job_t *jobs, pid_t pid);   //在jobs列表中删除pid对应的job
pid_t fgpid(struct job_t *jobs);                //返回前台运行的job的pid
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); //返回对应pid的job
struct job_t *getjobjid(struct job_t *jobs, int jid);   //返回对应jid的job
int pid2jid(pid_t pid);     //返回对应pid的job的jid
void listjobs(struct job_t *jobs);  //打印jobs列表

void usage(void);   //帮助信息
void unix_error(char *msg); //错误信息
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);

CMU 文档的一些说明与要求:

  • Shell 的提示标记符为 "tsh> "

  • 用户键入的命令行应包括命令名称和 0 个或多个参数,所有参数以一个或多个空格分隔。如果 name 是一个内置命令,则 tsh 应该立即处理它并等待下一个命令行。否则,tsh 假定该名称是一个可执行程序的路径,并在在一个初始子进程的上下文中加载并运行

  • tsh 不需要支持管道(|)或 I/O 重定向(< 和 >)

  • 键入 ctrl-c(ctrl-z)应该会发送一个 SIGINT(SIGTSTP)信号给当前的前台作业以及它的子进程。如果没有前台作业,则该信号应该没有任何作用

  • 如果命令行以 & 结束,则 tsh 应该在后台运行该作业。否则,它应该在前台运行

  • 每个作业都可以由一个进程 ID(PID)或一个作业 ID(JID)标识,该 ID 是一个由 tsh 分配的正整数。 JID应该在命令行上以前缀 “%” 表示。例如,“%5” 表示 JID 5,“ 5” 表示 PID 5

  • tsh 应该支持以下内置命令:

    • quit:终止 tsh 程序。
    • jobs:列出所有后台作业
    • bg :向作业发送 SIGCONT 信号来重新启动 ,然后在后台运行
    • fg :向作业发送 SIGCONT 信号来重新启动 ,然后在前台运行
  • tsh 必须回收所有的僵死进程

实验前

在正式实验之前,我们还是回顾一下相关知识

回顾:回收子进程

一个终止了但是还未被回收的进程称为僵死进程。对于一个长时间运行的程序(比如 Shell)来说,内核不会安排init进程去回收僵死进程,而它虽不运行却仍然消耗系统资源,因此实验要求我们回收所有的僵死进程。

waitpid是一个非常重要的函数,一个进程可以调用waitpid函数来等待它的子进程终止或停止,从而回收子进程,在本实验大量用到,我们必须学习它的用法:

这个函数用来挂起调用进程的执行,直到pid对应的等待集合的一个子进程的改变才返回,包括三种状态的改变:

  • 子进程终止
  • 子进程收到信号停止
  • 子进程收到信号重新执行

如果一个子进程在调用之前就已经终止了,那么函数就会立即返回,否则,就会阻塞,直到一个子进程改变状态。

等待集合以及监测那些状态都是用函数的参数确定的,函数定义如下:

pid_t waitpid(pid_t pid, int *wstatus, int options);

各参数含义及使用

  • pid:判定等待集合成员

    • pid > 0 : 等待集合为 pid 对应的单独子进程
    • pid = -1: 等待集合为所有的子进程
    • pid < -1: 等待集合为一个进程组,ID 为 pid 的绝对值
    • pid = 0 : 等待集合为一个进程组,ID 为调用进程的 pid
  • options:修改默认行为

    • WNOHANG:集合中任何子进程都未终止,立即返回 0
    • WUNTRACED:阻塞,直到一个进程终止或停止,返回 PID
    • WCONTINUED:阻塞,直到一个停止的进程收到 SIGCONT 信号重新开始执行
    • 也可以用或运算把 options 的选项组合起来。例如 WNOHANG | WUNTRACED 表示:立即返回,如果等待集合中的子进程都没有被停职或终止,则返回值为 0;如果有一个停止或终止,则返回值为该子进程的 PID
  • statusp:检查已回收子进程的退出状态

    • waitpid 会在 status 中放上关于导致返回的子进程的状态信息,很容易理解,这里就不再翻译了

回顾:并发编程原则

这里仅列出在本实验中用到的原则,后面的解析也会大量提及

  1. 注意保存和恢复 errno。很多函数会在出错返回式设置 errno,在处理程序中调用这样的函数可能会干扰主程序中其他依赖于 errno 的部分,解决办法是在进入处理函数时用局部变量保存它,运行完成后再将其恢复
  2. 访问全局数据时,阻塞所有信号。这里很容易理解,不再解释了
  3. 不可以用信号来对其它进程中发生的事情计数。未处理的信号是不排队的,即每种类型的信号最多只能有一个待处理信号。举例:如果父进程将要接受三个相同的信号,当处理程序还在处理一个信号时,第二个信号就会加入待处理信号集合,如果此时第三个信号到达,那么它就会被简单地丢弃,从而出现问题
  4. 注意考虑同步错误:竞争。我们在下面单独开一节说明

回顾:竞争

看如下例子:
在这里插入图片描述
这是一个 Unix Shell 的框架,父进程在一个全局列表中记录子进程,并设置了一个 SIGCHLD 处理程序来回收子进程,乍一看没问题,但是考虑如下可能的事件序列:

  • 第 29 行,创建子进程运行
  • 假设子进程在父进程运行到 32 行,即运行addjob函数之前就结束了,并发送一个 SIGCHLD 信号
  • 父进程接收到信号,运行信号处理程序,调用deletejob函数,而这个job本来就没有添加入列表
  • 返回父进程,调用addjob函数,而这个子进程已经终止并回收了,job早就不存在了

也就是说,在这里,deletejob函数的调用发生在了addjos之前,导致错误。我们称addjobdeletejob存在竞争。

解决办法也就很容易想到了,即在父进程folk之前就阻塞 SIGCHLD 信号:

在这里插入图片描述

这样,父进程在fork后,addjob前一定不会处理 SIGCHLD 信号,保证了addjob一定在deletejob之前执行

错误处理包装函数

当 Unix 系统及函数遇到错误时,它们通常会返回 -1,并设置全局整型变量errno来表示什么出错了。为了能让程序检查错误的同时代码不那么臃肿,按照书本推荐的方式,我编写了一套后面将要用到的错误处理包装函数

/* Error wrapper function */
pid_t Fork(void);
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);
void Sigaddset(sigset_t *set, int signum);
void Execve(const char *filename, char *const argv[], char *const envp[]);
void Setpgid(pid_t pid, pid_t pgid);
void Kill(pid_t pid, int sig);
pid_t Fork(void){
    pid_t pid;
    if((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset){
    if(sigprocmask(how, set, oldset) < 0)
        unix_error("Sigprocmask error");
}
void Sigemptyset(sigset_t *set){
    if(sigemptyset(set) < 0)
        unix_error("Sigprocmask error");
}
void Sigfillset(sigset_t *set){
    if(sigfillset(set) < 0)
        unix_error("Sigfillset error");
}
void Sigaddset(sigset_t *set, int signum){
    if(sigaddset(set, signum) < 0)
        unix_error("Sigaddset error");
}
void Execve(const char *filename, char *const argv[], char *const envp[]){
    if(execve(filename, argv, envp) < 0){
        printf("%s: Command not found.\n", argv[0]);
    }
}
void Setpgid(pid_t pid, pid_t pgid){
    if(setpgid(pid, pgid) < 0){
        unix_error("Setpid error");
    }
}
void Kill(pid_t pid, int sig){
    if(kill(pid, sig) < 0){
        unix_error("Kill error");
    }
}

eval

这个函数功能是解析命令行后判断为内置命令还是程序路径,分别执行。如果是前台作业,则要等待其完成,如果是后台作业,则要输出其相应信息

代码

/* 
 * eval - Evaluate the command line that the user has just typed in
 * 
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard.  
*/
void eval(char *cmdline) 
{
    char *argv[MAXARGS];        //存放解析的参数
    char buf[MAXLINE];          //解析cmdline
    int bg;                     //判断程序是前台还是后台执行
    int state;                  //指示前台还是后台运行状态
    pid_t pid;                  //执行程序的子进程的pid

    strcpy(buf, cmdline);   
    bg = parseline(buf, argv);  //解析参数
    state = bg? BG:FG;          
    if(argv[0] == NULL)         //空行,直接返回
        return;
    sigset_t mask_all, mask_one, prev_one;
    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    if(!builtin_cmd(argv)) {                            //判断是否为内置命令
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);       //fork前阻塞SIGCHLD信号
        if((pid = Fork()) == 0) {                           //创建子进程
            Sigprocmask(SIG_SETMASK, &prev_one, NULL);      //解除子进程的阻塞
            Setpgid(0, 0);                                  //创建新进程组,ID设置为进程PID
            Execve(argv[0], argv, environ);                 //执行
            exit(0);                                        //子线程执行完毕后一定要退出
        }
        if(state==FG){
            Sigprocmask(SIG_BLOCK, &mask_all, NULL);            //添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);                  //添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL);
            waitfg(pid);                                        //等待前台进程执行完毕
        }         
        else{
            Sigprocmask(SIG_BLOCK, &mask_all, NULL);            //添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);                  //添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL);                                 
            printf("[%d] (%d) %s",pid2jid(pid), pid, cmdline);  //打印后台进程信息
        }
        Sigprocmask(SIG_SETMASK, &prev_one, NULL);          //解除阻塞 
    }
    return;
}

说明

注释已经写得很详细了,这里用到了消除竞争的方法。我要解释两点:

  • 为什么调用printf时也阻塞信号?

    • 因为这里打印了全局变量,而printf函数是线程不安全的,比如可能会出现读内存的同时另一个线程修改它的情况
  • 为什么要在执行前创建新线程组?

    • 因为这里主要是为了将子进程组与 tsh 进程组分开,防止发信号终止子进程组时也将 tsh 进程组终止了

参见 Write UP 中的提示:

After the fork, but before the execve, the child process should call setpgid(0, 0), which puts the child in a new process group whose group ID is identical to the child’s PID. This ensures that there will be only one process, your shell, in the foreground process group.

builtin_cmd

这个函数就是简简单单判断是否为内置命令

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
    if (!strcmp(argv[0], "quit"))
        exit(0);
    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
        do_bgfg(argv);
        return 1;
    }
    if (!strcmp(argv[0], "jobs")) {
        listjobs(jobs);
        return 1;
    }
    if (!strcmp(argv[0], "&"))
        return 1;
    return 0;     /* not a builtin command */
}

do_bgfg

这个函数要实现内置命令bgfg,这两个命令的功能如下:

  • bg <job>:通过向<job>对应的作业发送SIGCONT信号来使它重启并放在后台运行
  • fg <job>:通过向 <job>对应的作业发送SIGCONT信号来使它重启并放在前台运行
  • 输入时后面的参数有%则代表jid,没有则代表pid

trace14主要就是用来测试这个命令,查看标准程序的输出:

代码

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    struct job_t *job = NULL;        //要处理的job
    int state;                      //输入的命令
    int id;                         //存储jid或pid
    if(!strcmp(argv[0], "bg")) state = BG;
        else state = FG;  
    if(argv[1]==NULL){               //没带参数
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    if(argv[1][0]=='%'){             //说明是jid
       if(sscanf(&argv[1][1], "%d", &id) > 0){
            job = getjobjid(jobs, id);  //获得job
            if(job==NULL){
                printf("%%%d: No such job\n", id);
                return;
            }
        }
    }
    else if(!isdigit(argv[1][0])) {  //其它符号,非法输入
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }
    else{                       //pid
        id = atoi(argv[1]);
        job = getjobpid(jobs, id);
        if(job==NULL){
            printf("(%d): No such process\n", id);
            return;
        }

    }
    Kill(-(job->pid), SIGCONT);       //重启进程, 这里发送到进程组
    job->state = state;
    if(state==BG)
        printf("[%d] (%d) %s",job->jid, job->pid, job->cmdline);
    else 
        waitfg(job->pid);
    return;
}

说明

这个代码的难点主要在于命令行参数的判断,要考虑健壮性。注意原来的后台进程可能会变成前台进程,所以应修改job结构体的相应信息

waitfg

这个函数从要求实现阻塞父进程,直到当前的前台进程不再是前台进程了。这里显然要显示地等待信号,我们先回顾一下相关知识

回顾:显式地等待信号

考虑如下代码:

while(fgpid(jobs) != 0)
    pause();

这里会有一个竞争,并且它可能引起致命的错误!考虑如下事件序列:

  • 父进程调用fgpid函数,此时有一个子进程仍然在前台运行,所以判断条件为真,进入循环
  • 假定父进程在进入循环后,而执行pause前,子进程终止
  • 父进程接收到 SIGCHLD 信号,并处理结束后才调用pause

由于pause仅在捕捉到信号后返回,而之后不会再有任何信号抵达,那么父进程就会永远休眠!

解决办法是用sleep函数:因为它不依赖信号来返回,通过每次循环来监测子进程状态

也可以用sigsuspend函数,这个函数相当于如下代码:

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

在调用sigsuspend之前阻塞 SIGCHLD 信号,调用时又通过sigprocmask函数,在执行pause函数之前解除对信号的阻塞,从而正常休眠。有同学可能会问了:这里并没有消除竞争啊?如果在第 1 行和第 2 行之间子进程终止不还是会发生永久休眠吗?

这就是sigsuspend与上述代码的不同之处了,它相当于上述代码的原子版本,即第 1 行和第 2 行总是一起发生的,不会被中断!

代码

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    sigset_t mask;
    Sigemptyset(&mask);   
    while (fgpid(jobs) != 0){
        sigsuspend(&mask);      //暂停时取消阻塞,见sigsuspend用法
    }
    return;
}

经过上面的回顾,这段代码就很容易理解了。

信号处理函数实现

除了前面的 4 个函数,还要实现 3 个信号处理函数。由于他们非常相像,我放在了一起

sigchld_handler

实现一个 SIGCHLD 信号处理函数,能够回收所有僵死进程

/* 
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate.  
 */
void sigchld_handler(int sig) 
{
    int olderrno = errno;   //由于errno是全局变量,注意保存和恢复errno
    int status;
    pid_t pid;
    struct job_t *job;
    sigset_t mask, prev;
    sigfillset(&mask);
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){      //立即返回该子进程的pid
        sigprocmask(SIG_BLOCK, &mask, &prev);   //阻塞所有信号
        if (WIFEXITED(status)){                 //正常终止
            deletejob(jobs, pid);
        }
        else if (WIFSIGNALED(status)){          //因为信号而终止, 打印
            printf ("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
            deletejob(jobs, pid);
        }
        else if (WIFSTOPPED(status)){           //因为信号而停止, 打印
            printf ("Job [%d] (%d) stoped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
            job = getjobpid(jobs, pid);
            job->state = ST;
        }
        sigprocmask(SIG_SETMASK, &prev, NULL);          
    }
    errno = olderrno;
    return;
}

这里用到了并发编程原则 1,后面就是利用waitpid函数(前面讲解很清楚了),检测子进程的退出状态来实现相应操作,如回收子进程,打印相关信息等等。

sigint_handler

实现一个 SIGINT 信号处理函数,将信号传送给前台进程

/* 
 * sigint_handler - The kernel sends a SIGINT to the shell whenver the
 *    user types ctrl-c at the keyboard.  Catch it and send it along
 *    to the foreground job.  
 */
void sigint_handler(int sig) 
{
    int olderrno = errno;
    int pid;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);
    Sigprocmask(SIG_BLOCK, &mask_all, &prev);   //jobs为全局变量
    if((pid = fgpid(jobs)) != 0){
        Sigprocmask(SIG_SETMASK, &prev, NULL);
        Kill(-pid, SIGINT);
    }
    errno = olderrno;
    return;
}

为什么要在调用fpgid函数之前阻塞信号,我在waitfg实现中已经讲解了,这里不再赘述。

sigstp_handler

实现一个 SIGSTOP 信号处理函数,将信号传送给前台进程

/*
 * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
 *     the user types ctrl-z at the keyboard. Catch it and suspend the
 *     foreground job by sending it a SIGTSTP.  
 */
void sigtstp_handler(int sig) 
{
    int olderrno = errno;
    int pid;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);
    Sigprocmask(SIG_BLOCK, &mask_all, &prev);
    if((pid = fgpid(jobs)) > 0){
        Sigprocmask(SIG_SETMASK, &prev, NULL);
        Kill(-pid, SIGSTOP);
    }
    errno = olderrno;
    return;
}

代码与上个函数几乎完全相同

tsh 测试

先给文件附加权限

chmod +x sdriver.pl
chmod +x tshref
gcc -o tsh tsh.c
chmod +x myspin
chmod +x mysplit
chmod +x mystop

实验文件还包括了作者制作的参考 tsh,以及 16 个测试用例,通过跑用例查看结果与参考 tsh 是否相同,就能判断我们写的 tsh 有无问题,我这里仅放出其中几个用例的测试截图:

trace09:

在这里插入图片描述

trace10:

在这里插入图片描述

trace11:

在这里插入图片描述

trace12:
在这里插入图片描述

trace13:

在这里插入图片描述

trace14:

在这里插入图片描述

trace15:

在这里插入图片描述

扩展管道,重定向,历史回溯功能

历史回溯的实现

int main() {
    initscr();
    cbreak();
    noecho();

    deleteFirstNLines(FIindexAME, 7);
    char history[MAX_HISTORY][MAXARGS];
    int history_index = 0;
    int current_history = 0;
    int history_count = 0;
    char *delim;

    FILE *file = fopen(FIindexAME, "a+");
    if (file == NULL) {
        printw("Error opening file.\n");
        refresh();
        endwin();
        return 1;
    }

    // Read history from file
    char line[MAXARGS];
    while (fgets(line, sizeof(line), file) != NULL) {
        line[strcspn(line, "\n")] = '\0'; // Remove newline character
        strcpy(history[history_count], line);
        history_count++;
    }
    current_history = history_count;


    while (1) {
        char buffer[MAXARGS];
        int index = 0;
        int in_history_mode = 0;
        move(getcury(stdscr), 0);
        clrtoeol();
        printw("Enter a string (Ctrl+L to clear, Enter to submit):\n$");
        refresh();

        while (1) {
            char ch = getch();

            if (ch == '\n') {
                break;
            } else if (ch == 127) { // Backspace key
                if (index > 0) {
                    index--;
                    buffer[index] = '\0';
                    move(getcury(stdscr), getcurx(stdscr) - 1);
                    delch();
                    if(getcurx(stdscr) == 0){
                        addch('$');
                    }
                }
            } else if (ch == 27) { // Escape key (arrow keys)
                char next1 = getch();
                char next2 = getch();
                if (next1 == '[' && next2 == 'A') { // Up arrow key
                    if (history_count > 0 && current_history > 0) {
                        current_history--;
                        strcpy(buffer, history[current_history]);
                        index = strlen(buffer);
                        move(getcury(stdscr), 0);
                        clrtoeol();
                        printw("$%s", buffer);
                        refresh();
                        in_history_mode = 1;
                    }
                } else if (next1 == '[' && next2 == 'B') { // Down arrow key
                    if (history_count > 0 && current_history < history_count - 1) {
                        current_history++;
                        strcpy(buffer, history[current_history]);
                        index = strlen(buffer);
                        move(getcury(stdscr), 0);
                        clrtoeol();
                        printw("$%s", buffer);
                        refresh();
                        in_history_mode = 1;
                    }
                }
            } else if (ch == 12) { // Ctrl+L key
                index = 0;
                buffer[0] = '\0';
                clear();
                printw("Enter a string (Ctrl+L to clear, Enter to submit):\n$");
                refresh();
            } else {
                buffer[index] = ch;
                index++;
                buffer[index] = '\0';
                addch(ch);
            }
        }


        fprintf(file, "%s\n", buffer);
        fflush(file);

        if (!in_history_mode) {
            if (history_count < MAX_HISTORY) {
                strcpy(history[history_count], buffer);
                history_count++;
            } else {
                for (int i = 0; i < MAX_HISTORY - 1; i++) {
                    strcpy(history[i], history[i + 1]);
                }
                strcpy(history[MAX_HISTORY - 1], buffer);
            }
            current_history = history_count;
        }

        printw("\nYou entered: %s\n\n", buffer);
        refresh();
        
        if (strcmp(buffer, "quit") == 0) {
            endwin();
            exit(0);
        }
        /* Evaluate */
        if((delim = strchr(buffer, '|')) == NULL){
                eval(buffer);
        }else{
            execveCommand(buffer);
        }
        
 
    }

    endwin();
    fclose(file);

    return 0;
}

管道功能的实现

void execveCommand(char *cmdline){
    int fd[2];
    pid_t pid;
    char *delim;
    int saved_fd;

    saved_fd = dup(STDIN_FILENO); // 保存标准输入

    delim = strchr(cmdline, '|');
    *delim = '\0'; // 将'|'替换为字符串结束符'\0'
    
    if (pipe(fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid = fork();

    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process
        close(fd[0]); // Close reading end of pipe 

        // Redirect stdout to the writing end of the pipe
        dup2(fd[1], STDOUT_FILENO);
        close(fd[1]);

        // Execute the first command
        eval(cmdline);
    } else {
        // Parent process
        close(fd[1]); // Close writing end of pipe

        // Redirect stdin to the reading end of the pipe
        dup2(fd[0], STDIN_FILENO);
        close(fd[0]);

        // Execute the second command
        eval(delim + 1); // 执行第二个命令,跳过'|'符号
    }
   
    int status;
    if (waitpid(pid, &status, 0) < 0) {
        perror("waitpid error");
        exit(EXIT_FAILURE);
    }

    dup2(saved_fd, STDIN_FILENO);
    close(saved_fd);
    return;
}

重定向功能的实现

void eval(char *cmdline) {
    char *argv[MAXARGS];
    char *redirection[3] = { NULL, NULL, NULL }; // Array to hold input and output redirection paths
    char buf[MAXARGS];
    int bg;
    pid_t pid;
    int fd_in, fd_out;

    strcpy(buf, cmdline);
    bg = parseline(buf, argv, redirection);

    if (argv[0] == NULL)
        return; /* Ignore empty line */

    // Check for redirection symbols
    char *input_redir = redirection[0];
    char *output_redir = redirection[1];

    int original_stdin = dup(STDIN_FILENO);
    int original_stdout = dup(STDOUT_FILENO);

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {
            if (input_redir || output_redir) {
                if (input_redir) {
                    fd_in = open(input_redir, O_RDONLY);
                    if (fd_in < 0) {
                        printf("Error opening input file: %s\n", input_redir);
                        return;
                    }
                    dup2(fd_in, STDIN_FILENO);
                    close(fd_in);
                }
                if (output_redir) {
                    fd_out = open(output_redir, O_WRONLY | O_CREAT | O_TRUNC, 0644);
                    if (fd_out < 0) {
                        printf("Error opening output file: %s\n", output_redir);
                        return;
                    }
                    dup2(fd_out, STDOUT_FILENO);
                    close(fd_out);
                }
            }
            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("waitpid error");
    } else {
        printf("%d %s", pid, cmdline);
    }
    /* Restore original stdin and stdout */
    dup2(original_stdin, STDIN_FILENO);
    close(original_stdin);
    dup2(original_stdout, STDOUT_FILENO);
    close(original_stdout); 
}  

增加扩展功能的全部代码

#include <ncurses.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
// gcc -o shell_pipe shell_pipe.c -l ncurses
#define MAXARGS 128
#define MAX_HISTORY 10
#define FIindexAME ".bash_history_"
extern char **environ;

void unix_error(const char *msg) {
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
}

pid_t Fork(void) {
    pid_t pid;

    if ((pid = fork()) < 0) {
        unix_error("Fork error");
    }
    return pid;
}

void eval(char *cmdline);
int parseline(char *buf, char **argv, char **redirection);
int builtin_command(char **argv);
void deleteFirstNLines(const char *filename, int n);
void execveCommand(char *cmdline);

int main() {
    initscr();
    cbreak();
    noecho();

    deleteFirstNLines(FIindexAME, 7);
    char history[MAX_HISTORY][MAXARGS];
    int history_index = 0;
    int current_history = 0;
    int history_count = 0;
    char *delim;

    FILE *file = fopen(FIindexAME, "a+");
    if (file == NULL) {
        printw("Error opening file.\n");
        refresh();
        endwin();
        return 1;
    }

    // Read history from file
    char line[MAXARGS];
    while (fgets(line, sizeof(line), file) != NULL) {
        line[strcspn(line, "\n")] = '\0'; // Remove newline character
        strcpy(history[history_count], line);
        history_count++;
    }
    current_history = history_count;


    while (1) {
        char buffer[MAXARGS];
        int index = 0;
        int in_history_mode = 0;
        move(getcury(stdscr), 0);
        clrtoeol();
        printw("Enter a string (Ctrl+L to clear, Enter to submit):\n$");
        refresh();

        while (1) {
            char ch = getch();

            if (ch == '\n') {
                break;
            } else if (ch == 127) { // Backspace key
                if (index > 0) {
                    index--;
                    buffer[index] = '\0';
                    move(getcury(stdscr), getcurx(stdscr) - 1);
                    delch();
                    if(getcurx(stdscr) == 0){
                        addch('$');
                    }
                }
            } else if (ch == 27) { // Escape key (arrow keys)
                char next1 = getch();
                char next2 = getch();
                if (next1 == '[' && next2 == 'A') { // Up arrow key
                    if (history_count > 0 && current_history > 0) {
                        current_history--;
                        strcpy(buffer, history[current_history]);
                        index = strlen(buffer);
                        move(getcury(stdscr), 0);
                        clrtoeol();
                        printw("$%s", buffer);
                        refresh();
                        in_history_mode = 1;
                    }
                } else if (next1 == '[' && next2 == 'B') { // Down arrow key
                    if (history_count > 0 && current_history < history_count - 1) {
                        current_history++;
                        strcpy(buffer, history[current_history]);
                        index = strlen(buffer);
                        move(getcury(stdscr), 0);
                        clrtoeol();
                        printw("$%s", buffer);
                        refresh();
                        in_history_mode = 1;
                    }
                }
            } else if (ch == 12) { // Ctrl+L key
                index = 0;
                buffer[0] = '\0';
                clear();
                printw("Enter a string (Ctrl+L to clear, Enter to submit):\n$");
                refresh();
            } else {
                buffer[index] = ch;
                index++;
                buffer[index] = '\0';
                addch(ch);
            }
        }


        fprintf(file, "%s\n", buffer);
        fflush(file);

        if (!in_history_mode) {
            if (history_count < MAX_HISTORY) {
                strcpy(history[history_count], buffer);
                history_count++;
            } else {
                for (int i = 0; i < MAX_HISTORY - 1; i++) {
                    strcpy(history[i], history[i + 1]);
                }
                strcpy(history[MAX_HISTORY - 1], buffer);
            }
            current_history = history_count;
        }

        printw("\nYou entered: %s\n\n", buffer);
        refresh();
        
        if (strcmp(buffer, "quit") == 0) {
            endwin();
            exit(0);
        }
        /* Evaluate */
        if((delim = strchr(buffer, '|')) == NULL){
                eval(buffer);
        }else{
            execveCommand(buffer);
        }
        
 
    }

    endwin();
    fclose(file);

    return 0;
}


// 执行命令
void execveCommand(char *cmdline){
    int fd[2];
    pid_t pid;
    char *delim;
    int saved_fd;

    saved_fd = dup(STDIN_FILENO); // 保存标准输入

    delim = strchr(cmdline, '|');
    *delim = '\0'; // 将'|'替换为字符串结束符'\0'
    
    if (pipe(fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid = fork();

    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // Child process
        close(fd[0]); // Close reading end of pipe 

        // Redirect stdout to the writing end of the pipe
        dup2(fd[1], STDOUT_FILENO);
        close(fd[1]);

        // Execute the first command
        eval(cmdline);
    } else {
        // Parent process
        close(fd[1]); // Close writing end of pipe

        // Redirect stdin to the reading end of the pipe
        dup2(fd[0], STDIN_FILENO);
        close(fd[0]);

        // Execute the second command
        eval(delim + 1); // 执行第二个命令,跳过'|'符号
    }

    int status;
    if (waitpid(pid, &status, 0) < 0) {
        perror("waitpid error");
        exit(EXIT_FAILURE);
    }

    dup2(saved_fd, STDIN_FILENO);
    close(saved_fd);
    return;
}




void deleteFirstNLines(const char *filename, int n) {
    FILE *file = fopen(filename, "r+");
    if (file == NULL) {
        printf("Error opening file.\n");
        return;
    }

    int lines = 0;
    int c;
    while ((c = fgetc(file)) != EOF) {
        if (c == '\n') {
            lines++;
            if (lines >= n) {
                break;
            }
        }
    }

    long offset = ftell(file);
    fseek(file, 0, SEEK_SET);

    FILE *temp = fopen("temp.txt", "w");
    if (temp == NULL) {
        printf("Error creating temporary file.\n");
        fclose(file);
        return;
    }

    fseek(file, offset, SEEK_SET);
    while ((c = fgetc(file)) != EOF) {
        fputc(c, temp);
    }

    fclose(file);
    fclose(temp);

    remove(filename);
    rename("temp.txt", filename);
}



void eval(char *cmdline) {
    char *argv[MAXARGS];
    char *redirection[3] = { NULL, NULL, NULL }; // Array to hold input and output redirection paths
    char buf[MAXARGS];
    int bg;
    pid_t pid;
    int fd_in, fd_out;

    strcpy(buf, cmdline);
    bg = parseline(buf, argv, redirection);

    if (argv[0] == NULL)
        return; /* Ignore empty line */

    // Check for redirection symbols
    char *input_redir = redirection[0];
    char *output_redir = redirection[1];

    int original_stdin = dup(STDIN_FILENO);
    int original_stdout = dup(STDOUT_FILENO);

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {
            if (input_redir || output_redir) {
                if (input_redir) {
                    fd_in = open(input_redir, O_RDONLY);
                    if (fd_in < 0) {
                        printf("Error opening input file: %s\n", input_redir);
                        return;
                    }
                    dup2(fd_in, STDIN_FILENO);
                    close(fd_in);
                }
                if (output_redir) {
                    fd_out = open(output_redir, O_WRONLY | O_CREAT | O_TRUNC, 0644);
                    if (fd_out < 0) {
                        printf("Error opening output file: %s\n", output_redir);
                        return;
                    }
                    dup2(fd_out, STDOUT_FILENO);
                    close(fd_out);
                }
            }
            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("waitpid error");
    } else {
        printf("%d %s", pid, cmdline);
    }
    /* Restore original stdin and stdout */
    dup2(original_stdin, STDIN_FILENO);
    close(original_stdin);
    dup2(original_stdout, STDOUT_FILENO);
    close(original_stdout); 
}  


int builtin_command(char **argv) {
    if (!strcmp(argv[0], "quit"))
        exit(0);
    if (!strcmp(argv[0], "&"))
        return 1;
    return 0;
}

int parseline(char *buf, char **argv, char **redirection) {
    char *delim;
    int argc;
    int bg = 0; // Initialize bg to 0 for foreground execution
    int param_flag = 2; // Initialize param_flag to indicate normal arguments

    buf[strlen(buf)] = ' '; /* replace trailing '\n' with space */
    while (*buf && (*buf == ' '))
        buf++;

    /* Build the argv list */
    argc = 0;
    while ((delim = strchr(buf, ' '))) {
        if (param_flag == 2) {
            argv[argc++] = buf;
            *delim = '\0';
        } else {
            redirection[param_flag] = buf;
            *delim = '\0';
            param_flag = 2; // Reset param_flag after handling redirection
        }

        buf = delim + 1;
        while (*buf && (*buf == ' '))
            buf++;

        // Check for redirection symbols
        if (*buf == '<') {
            param_flag = 0;
            buf++;
            while (*buf && (*buf == ' '))
                buf++;
        } else if (*buf == '>') {
            param_flag = 1;
            buf++;
            while (*buf && (*buf == ' '))
                buf++;
        }
    }

    argv[argc] = NULL;
    if (argc == 0)
        return 1;

    // Check for background execution
    if ((bg = (*argv[argc - 1] == '&')) != 0)
        argv[--argc] = NULL;

    return bg;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值