如果没有终端连接到服务器,标准输入和输出是什么?

What standard input and output would be if there's no terminal connected to server?

昨天我在思考服务器日志记录的方式时,想到了这个问题。

通常,我们打开一个连接到本地计算机或远程服务器的终端,运行一个可执行文件,并在其中打印(printf, cout)一些debug/log信息终端。

但是对于那些processes/executables/scripts 运行在服务器上没有连接到终端的,标准输入和输出是什么?

例如:

  1. 假设我有一个 crontab 任务,运行一天多次在服务器上运行一个程序。如果我在程序中写类似cout << "blablabla" << endl;的东西。会发生什么?这些输出将流向哪里?

  2. 另一个我想知道的例子是,如果我为 Apache Web 服务器编写一个 CGI 程序(使用 CC++),是我的 CGI 程序的标准输入和输出吗? (根据这个C++ CGI tutorial,我猜测CGI程序的标准输入和输出以某种方式重定向到Apache服务器。因为它使用cout输出html 内容,不是按 return. )

我在提问之前读过这个 What is “standard input”?,它告诉我标准输入不必绑定到键盘,而标准输出不必绑定到 terminal/console/screen。

OS 是 Linux.

标准输入和标准输出(和标准错误)流可以指向基本上任何 I/O 设备。这通常是一个终端,但它也可以是一个文件、一个管道、一个网络套接字、一台打印机等。这些流将它们的 I/O 指向的究竟是什么通常由启动您的进程的进程决定,例如shell 或像 cron 或 apache 这样的守护进程,但是进程可以根据需要自行重定向这些流。

我将使用 Linux 作为示例,但大多数其他 OS 中的概念都是相似的。在 Linux 上,标准输入和标准输出流由文件描述符 01 表示。宏 STDIN_FILENOSTDOUT_FILENO 只是为了方便和清晰。文件描述符只是一个数字,与 OS 内核维护的某些文件描述相匹配,告诉它如何写入该设备。这意味着从用户 space 进程的角度来看,您几乎可以用相同的方式写入任何内容:write(some_file_descriptor, some_string, some_string_length)(更高级别的 I/O 函数,如 printfcout 只是对 write) 的一次或多次调用的包装。对于这个过程,some_file_descriptor代表什么类型的设备并不重要。 OS 内核会为您解决这个问题并将您的数据传递给适当的设备驱动程序。


启动新进程的标准方法是调用fork复制父进程,然后调用子进程中的exec函数族之一开始执行一些新程序。在这两者之间,它通常会关闭它从父进程继承的标准流并打开新的流以将子进程的输出重定向到新的地方。例如,要让子项通过管道将其输出返回给父项,您可以在 C++ 中执行如下操作:

int main()
{
    // create a pipe for the child process to use for its
    // standard output stream
    int pipefds[2];
    pipe(pipefds);

    // spawn a child process that's a copy of this process
    pid_t pid = fork();
    if (pid == 0)
    {
        // we're now in the child process

        // we won't be reading from this pipe, so close its read end
        close(pipefds[0]);
        // we won't be reading anything
        close(STDIN_FILENO);
        // close the stdout stream we inherited from our parent
        close(STDOUT_FILENO);
        // make stdout's file descriptor refer to the write end of our pipe
        dup2(pipefds[1], STDOUT_FILENO);
        // we don't need the old file descriptor anymore.
        // stdout points to this pipe now
        close(pipefds[1]);
        // replace this process's code with another program
        execlp("ls", "ls", nullptr);
    } else {
        // we're still in the parent process

        // we won't be writing to this pipe, so close its write end
        close(pipefds[1]);

        // now we can read from the pipe that the
        // child is using for its standard output stream
        std::string read_from_child;
        ssize_t count;
        constexpr size_t BUF_SIZE = 100;
        char buf[BUF_SIZE];
        while((count = read(pipefds[0], buf, BUF_SIZE)) > 0) {
            std::cout << "Read " << count << " bytes from child process\n";
            read_from_child.append(buf, count);
        }

        std::cout << "Read output from child:\n" << read_from_child << '\n';
        return EXIT_SUCCESS;
    }
}

注意:为清楚起见,我省略了错误处理

此示例创建一个子进程并将其输出重定向到管道。子进程 (ls) 中的程序 运行 可以像引用终端一样处理标准输出流(尽管 ls 在检测到它的标准输出时会改变一些行为不是终端)。


这种重定向也可以从终端完成。当您 运行 命令时,您可以使用重定向运算符告诉您 shell 将该命令标准流重定向到终端以外的其他位置。例如,这里有一个复杂的方法,可以使用 sh-like shell:

将文件从一台机器复制到另一台机器
gzip < some_file | ssh some_server 'zcat > some_file'

这会执行以下操作:

  1. 创建管道
  2. 运行 gzip 重定向其标准输入流以从“some_file”读取并重定向其标准输出流以写入管道
  3. 运行 ssh 并重定向其标准输入流以从管道读取
  4. 在服务器上,运行 zcat 其标准输入重定向自从 ssh 连接读取的数据,其标准输出重定向到写入“some_file”[=49] =]