8.4.6 利用 forkexecve 运行程序

8.4.6 利用 forkexecve 运行程序

像 Unix shell 和 Web 服务器这样的程序大量使用了 forkexecve 函数。shell 是一个交互型的应用级程序,它代表用户运行其他程序。最早的 shell 是 sh 程序,后面出现了一些变种,比如 csh、tcsh、ksh 和 bash。shell 执行一系列的读/求值(read/evaluate)步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

图 8-23 展示了一个简单 shell 的 main 例程。shell 打印一个命令行提示符,等待用户在 stdin 上输入命令行,然后对这个命令行求值。

code/ecf/shellex.c

#include "csapp.h"
#define MAXARGS 128

/* Function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);

int main()
{
    char cmdline[MAXLINE]; /* Command line */

    while (1) {
        /* Read */
        printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
            exit(0);

        /* Evaluate */
        eval(cmdline);
    }
}

图 8-23 一个简单的 shell 程序的 main 例程

图 8-24 展示了对命令行求值的代码。它的首要任务是调用 parseline 函数(见图 8-25),这个函数解析了以空格分隔的命令行参数,并构造最终会传递给 execveargv 向量。第一个参数被假设为要么是一个内置的 shell 命令名,马上就会解释这个命令,要么是一个可执行目标文件,会在一个新的子进程的上下文中加载并运行这个文件。

如果最后一个参数是一个“&”字符,那么 parseline 返回 1,表示应该在后台执行该程序(shell 不会等待它完成)。否则,它返回 0,表示应该在前台执行这个程序(shell 会等待它完成)。

在解析了命令行之后,eval 函数调用 builtin_command 函数,该函数检查第一个命令行参数是否是一个内置的 shell 命令。如果是,它就立即解释这个命令,并返回值 1。否则返回 0。简单的 shell 只有一个内置命令——quit 命令,该命令会终止 shell。实际使用的 shell 有大量的命令,比如 pwdjobsfg

如果 builtin_command 返回 0,那么 shell 创建一个子进程,并在子进程中执行所请求的程序。如果用户要求在后台运行该程序,那么 shell 返回到循环的顶部,等待下一个命令行。否则,shell 使用 waitpid 函数等待作业终止。当作业终止时,shell 就开始下一轮迭代。

code/ecf/shellex.c

/* eval - Evaluate a command line */
void eval(char *cmdline)
{
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? */
    pid_t pid;           /* Process id */

    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;   /* Ignore empty lines */

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            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)
{
    if (!strcmp(argv[0], "quit")) /* quit command */
        exit(0);
    if (!strcmp(argv[0], "&"))    /* Ignore singleton & */
        return 1;
    return 0;                     /* Not a builtin command */
}

图 8-24 eval 对 shell 命令行求值

code/ecf/shellex.c

/* 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 job? */

    buf[strlen(buf)-1] = ' ';  /* Replace trailing '\n' with space */
    while (*buf && (*buf == ' ')) /* Ignore leading spaces */
        buf++;

    /* Build the argv list */
    argc = 0;
    while ((delim = strchr(buf, ' '))) {
        argv[argc++] = buf;
        *delim = '\0';
        buf = delim + 1;
        while (*buf && (*buf == ' ')) /* Ignore spaces */
            buf++;
    }
    argv[argc] = NULL;

    if (argc == 0) /* Ignore blank line */
        return 1;

    /* Should the job run in the background? */
    if ((bg = (*argv[argc-1] == '&')) != 0)
        argv[--argc] = NULL;

    return bg;
}

图 8-25 parseline 解析 shell 的一个输入行

注意,这个简单的 shell 是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷就要求使用信号,我们将在下一节中讲述信号。