在 mini-shell 中使用 setpgid 会中断交互式命令

Using setpgid in a mini-shell breaks interactive commands

我正在尝试使用 this template 在 C 中编写 mini-shell。但是每当我尝试使用 lessvi 等交互式命令时,shell 就会卡在 waitpid 上(WUNTRACED 立即启用这些命令 return 因为它们由 ps) 指示的作业控制信号停止。其他不需要输入的命令,如 ls 都可以。根本原因是 setpgid,它似乎将分叉的 child 进程(例如 lessvi)放入不再共享终端的不同进程组中。 child 进程因此被作业控制信号停止。删除 setpgid 将使 mini-shell 再次工作,但不能将其删除,因为 mini-shell 需要将其前台进程作为一个组来控制(例如,如果前台进程 P 分叉其他进程P1 和 P2,即 shell,在收到用户的 SIGTSTP 后,应该停止 P、P1 和 P2。如果 P、P1、P2 在同一个进程组中,这可以很方便地完成pgid和P的pid是一样的,我们直接给整个进程组发送SIGTSTP即可)。

我曾尝试使用 tcsetpgrp 修复我的 shell。虽然它会让 vi 等命令再次起作用,但是 mini-shell 会在分叉的 child 完成后自动退出,大概是因为 parent shell 错误地将分叉 child 的完成视为 mini-shell.

的完成

是否有修复程序可以让我保留 setpgid?

// full code is in the provided link
if (!builtin_command(argv)) {
    if ((pid = Fork()) == 0) {   /* Child runs user job */
        if (execve(argv[0], argv, environ) < 0) {
            execvp(argv[0], argv);
            printf("%s: Command not found.\n", argv[0]);
            exit(0);
        }
    }
    // call wrapper function for error handling
    // set process group id of child to the pid of child
    Setpgid(pid, 0);
    if (!bg) {
        // foreground process, should wait for completion
        // tcsetpgrp does make vi and less work, 
        // but their completion also terminates the mini-shell
        // tcsetpgrp(STDERR_FILENO, pid);
        int status;
        if (waitpid(pid, &status, 0) < 0) {
            unix_error("waitfg: waitpid error");
        }
    } else {
        // background process
        printf("%d %s", pid, cmdline);
    }
}

解决方案是使用 tcsetpgrp 将 tty 的控制权交给其他进程组,当子进程完成后,再次使用 tcsetpgrp 收回对 tty 的控制权。请注意,tcsetpgrpSIGTTOU 发送给其调用者 if the calling process belongs to a background process group,因此必须阻止 SIGTTOUSIGTTIN

// error handling is omitted for brevity
// these two signal functions can be anywhere before tcsetpgrp
// alternatively, they can be masked during tcsetpgrp
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);

if (!builtin_command(argv)) {
    if ((pid = Fork()) == 0) {
        if (execve(argv[0], argv, environ) < 0) {
            execvp(argv[0], argv);
            printf("%s: Command not found.\n", argv[0]);
            exit(0);
        }
    }
    if (!bg) {
        setpgid(pid, 0);
        tcsetpgrp(STDERR_FILENO, pid);
        int status;
        if (waitpid(pid, &status, 0) < 0) {
            unix_error("waitfg: waitpid error");
        }
        tcsetpgrp(STDERR_FILENO, getpgrp());
    } else {
        printf("%d %s", pid, cmdline);
    }
}

这是一个相当粗糙的实现。请参阅 Craig 对这个问题的评论,了解在哪里可以找到 bash 的实现。