【问题标题】:Background execvp : how to do it properly?后台execvp:如何正确执行?
【发布时间】:2019-06-27 18:22:27
【问题描述】:

像许多其他人一样,我正在尝试模拟一个外壳。我已经在来自用户的字符串上正确使用了execvp。解析字符串并生成字符串数组(每个单词都有自己的数组,在space 字符上拆分),包括最后的NULL

当我发现用户输入的最后一个单词是& 时,我设置了一个标志来通知我的shell 该命令将在后台执行,同时让用户立即输入另一个命令。 “后台执行”命令将其 & 替换为传递给 execvp 的字符串数组中的 NULL 字符。

事实上,我一直在尝试使用pthread 在后台运行该进程,但它的行为有些奇怪:通过线程函数传递给execvp 的命令需要我按两次@ 987654330@发送命令后。

这是我简化的main 函数,它是模拟一个shell:

int main (void) {

    fprintf (stdout, "%% ");

    bool running = true;

    while(running) {

        /* Ask for an instruction and parses it. */
        char** args = query_and_split_input();

        /* Executing the commands. */
        if (args == NULL) {  // error while reading input
            running = false;
        } else {
            printf("shell processing new command\n");

            int count = count_words(args);
            split_line* line = form_split_line(args, count);
            Expression* ast = parse_line(line, 0, line->size - 1);

            if(line->thread_flag) {
                pthread_t cmd_thr;

                /* Setting up the content of the thread. */
                thread_data_t       thr_data;
                thr_data.ast        = *ast;
                thr_data.line       = *line;

                /* Executing the thread. */
                int thr_err;
                if ((thr_err = pthread_create(&cmd_thr, NULL, thr_func, &thr_data))) {
                    fprintf(stderr, "error: pthread_create, rc: %d\n", thr_err);
                    return EXIT_FAILURE;
                }
                printf("thread has been created.\n");

            } else {
                run_shell(args);
            }
            free(line);

            printf("done running shell on one command\n");
        }
    }

    /* We're all done here. See you! */
    printf("Bye!\n");
    exit (0);
}

这是我的线程的功能:

void *thr_func(void *arg) {
    thread_data_t *data = (thread_data_t *)arg;

    data->line.content[data->line.size-1] = NULL;  // to replace the trailing '&' from the command
    run_shell(data->line.content);

    printf("thread should have ran the command\n");
    pthread_exit(NULL);
}

以及运行命令的实际行:

void run_shell(char** args) {

    /* Forking. */
    int status;
    pid_t    pid; /* Right here, the created THREAD somehow awaits a second 'ENTER' before going on and executing the next instruction that forks the process. This is the subject of my first question. */
    pid = fork();

    if (pid < 0) {
        fprintf(stderr, "fork failed");

    } else if (pid == 0) {  // child
        printf("Child executing the command.\n");

        /* Executing the commands. */
        execvp(args[0], args);

        /* Child process failed. */
        printf("execvp didn't finish properly: running exit on child process\n");
        exit(-1);


    } else {  // back in parent
        waitpid(-1, &status, 0);  // wait for child to finish

        if (WIFEXITED(status)) { printf("OK: Child exited with exit status %d.\n", WEXITSTATUS(status)); }
        else { printf("ERROR: Child has not terminated correctly. Status is: %d\n", status); }

        free(args);
        printf("Terminating parent of the child.\n");
    }
}

所以基本上,作为一个例子,run_shell(args) 接收的是["echo","bob","is","great",NULL](在顺序执行的情况下)或["echo","bob","is","great",NULL,NULL](在后台执行的命令的情况下)。

我留下了printf 的痕迹,因为它可能有助于您了解执行流程。

如果我输入echo bob is great,输出(printf traces)是:

shell processing new command
Child executing the command.
bob is great
OK: Child exited with exit status 0.
Terminating parent of the child.
done running shell on one command

但是,如果我输入echo bob is great &amp;,则输出为:

shell processing new command
thread has been created.
done running shell on one command

然后我实际上需要再次按ENTER 才能获得以下输出:

Child executing the command.
bob is great
OK: Child exited with exit status 0.
Terminating parent of the child.
thread should have ran the command

(在最后一次执行中,我还获得了查询和解析用户输入的函数的痕迹,但这似乎无关紧要,因此我将整个部分抽象出来。)

所以我的问题是

  1. 创建的线程怎么会在运行execvp 之前等待第二个ENTER? (thr_func 停止执行 run_shell 并在 pid = fork(); 指令之前等待第二个 ENTER
  2. 我是否有解决手头问题的正确方法? (尝试在后台执行 shell 命令。)

【问题讨论】:

  • @wildplasser 我不确定我理解你的意思。事实上,当我检测到&amp; 时,我会尝试自己调用一个子shell,因为execxxx() 确实不会做这样的事情。这就是为什么我一直在尝试首先创建一个pthread,然后从那里开始fork(),然后在其中创建execvp
  • @wildplasser 我不认为我们可以使用 /bin/sh 来进行赋值,因为它会自动完成赋值本身所需的大部分单词(解析,创建抽象语法树,在需要时重定向输出等)。
  • 不要将线程与 fork 结合使用。您可以在不启动新线程的情况下分叉。分叉线程代码很复杂。
  • @rici 但如果我使用fork 而不是pthread,我不需要使用wait,这正好阻止我的主进程允许用户输入新命令在等待分叉进程完成时?
  • 执行此操作的传统方法是在 SIGCHLD 信号处理程序中进行等待。

标签: c shell posix


【解决方案1】:

您不能使用线程来模拟进程。好吧,严格来说你可以,但这样做没有用。问题是属于一个进程的所有线程共享相同的虚拟地址空间。没有理由创建一个线程,因为您最终需要fork() 来创建一个新进程(您将需要这个,原因如下所述),那么为什么要创建两个执行线程,如果其中一个将被停止所有只是等待子流程完成的时间。此架构没有用处。

历史上需要fork() 系统调用来进行简单调用以创建新进程(具有不同的虚拟内存映射)以允许执行新程序。在调用exec(2)系统调用之前需要创建一个新的、完整的进程,因为进程地址空间会被新程序的文本和数据段覆盖。如果您在线程中执行此操作,您将覆盖整个进程地址空间(这是 shell)并杀死您可以代表该进程运行的所有线程。要遵循的架构是(伪代码):

/* create pipes for redirection here, before fork()ing, so they are available
 * in the parent process and the child process */
int fds[2];
if (pipe(fds) < 0) { /* error */
    ... /* do error treatment */
}
pid_t child_pid = fork();
switch(child_pid) {
case -1: /* fork failed for some reason, no subprocess created */
    ...
    break;
case 0: /* this code is executed in the childd process, do redirections
         * here on pipes acquired ***before*** the fork() call */
        if (dup2(0 /* or 1, or 2... */, fds[0 /* or 1, or 2... */]) < 0) { /* error */
            ... /* do error management, considering you are in a different process now */
        }
        execvpe(argc, argv, envp);
        ... /* do error management, as execvpe failed (exec* is non-returning if ok) */
        break; /* or exit(2) or whatever */ 
    default: /* we are the parent, use the return value to track the child */
        save_child_pid(child_pid);
        ... /* close the unused file descriptors */
        close(fds[1 /* or 0, or 2, ... */]);
        ... /* more bookkeeping */
        /* next depends on if you have to wait for the child or not */
        wait*(...);  /* wait has several flavours */
} /* switch */

Exec 和 fork 系统调用分开的原因有两个:

  • 您需要能够在两个调用之间进行内务处理,以便在 exec() 之前在子进程中执行实际重定向。
  • 曾经有一段时间,unix 没有多任务处理或受保护,而 exec 调用只是将系统中的所有内存替换为要执行的新程序(包括内核代码,以应对未受保护的系统可能被破坏的事实通过执行程序)这在旧操作系统中很常见,我在 CP/M 或 TRS-DOS 等系统上看到过。 unix 中的实现几乎保留了 exec() 调用的所有语义,并且只添加了 fork() 不可用的功能。这很好,因为它允许父进程和子进程在管道时间到来时进行必要的簿记。

只有当您需要不同的线程与每个孩子进行通信时,您才可能使用不同的线程来完成任务。但是认为一个线程与父级共享所有虚拟空间(我们可以讨论线程之间的父/子关系),如果您执行 exec 调用,您将覆盖整个进程的虚拟空间(那里的所有线程)

【讨论】:

  • 我使用了一个线程,因为我想调用exec,这样它就不会阻塞主进程,但仍然能够在子进程上调用wait。那是我的错误。谢谢。
  • @payne,如果您在线程中调用 exec,您将杀死该进程中的所有线程,因为整个虚拟地址空间将被新的程序加载覆盖。 Exec不会创建新的地址空间,因为它不会创建新的进程,这是fork()系统调用的任务。
猜你喜欢
  • 1970-01-01
  • 2016-08-21
  • 2022-01-14
  • 1970-01-01
  • 2019-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多