在 Linux 和其他 POSIXy 操作系统中,标准输入对应文件描述符 0 (STDIN_FILENO),标准输出对应文件描述符 1 (STDOUT_FILENO),标准错误对应文件描述符 2 (STDERR_FILENO)。
标准文件句柄 stdin、stdout 和 stderr 是标准 C 抽象,在 Linux 中是在这些文件描述符之上实现的。
要重定向标准输入、输出或错误,首先您需要获取一个打开的文件描述符到您想要重定向的任何内容。对于文件,您可以通过open() 函数执行此操作,该函数返回文件描述符编号。然后,您使用dup2() 函数将其复制(复制)到您想要的描述符。
考虑以下example.c:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
/* Duplicate oldfd to newfd, and close oldfd.
In error cases, tries to close both descriptors.
Returns 0 if success, error code (with errno set) otherwise.
*/
static inline int move_descriptor(int oldfd, int newfd)
{
if (oldfd == -1 || newfd == -1) {
if (oldfd != -1)
close(oldfd);
if (newfd != -1)
close(newfd);
return errno = EBADF;
}
if (oldfd == newfd)
return 0;
if (dup2(oldfd, newfd) == -1) {
const int saved_errno = errno;
close(oldfd);
close(newfd);
return errno = saved_errno;
}
if (close(oldfd) == -1) {
const int saved_errno = errno;
close(newfd);
return errno = saved_errno;
}
return 0;
}
/* Write a message to standard error, keeping errno unchanged.
This is async-signal safe.
Returns 0 if success, error code otherwise.
*/
static inline int wrerr(const char *msg)
{
const char *end = (msg) ? msg + strlen(msg) : msg;
if (end == msg)
return 0;
const int saved_errno = errno;
while (msg < end) {
ssize_t n = write(STDERR_FILENO, msg, (size_t)(end - msg));
if (n > 0) {
msg += n;
} else
if (n != -1) {
errno = saved_errno;
return EIO;
} else
if (errno != EINTR) {
const int retval = errno;
errno = saved_errno;
return retval;
}
}
errno = saved_errno;
return 0;
}
static inline void errormessage(const char *name, const char *cause)
{
wrerr(name);
wrerr(": ");
wrerr(cause);
wrerr(".\n");
}
int main(int argc, char *argv[])
{
int fd;
if (argc < 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
wrerr("\n");
wrerr("Usage: "); wrerr(arg0); wrerr(" [ -h | --help ]\n");
wrerr(" "); wrerr(arg0); wrerr(" INPUT OUTPUT COMMAND [ ARGUMENTS ... ]\n");
wrerr("\n");
return EXIT_FAILURE;
}
fd = open(argv[1], O_RDONLY | O_NOCTTY);
if (fd == -1) {
errormessage(argv[1], strerror(errno));
return EXIT_FAILURE;
}
if (move_descriptor(fd, STDIN_FILENO)) {
errormessage(argv[1], strerror(errno));
return EXIT_FAILURE;
}
fd = open(argv[2], O_WRONLY | O_CREAT, 0666);
if (fd == -1) {
errormessage(argv[2], strerror(errno));
return EXIT_FAILURE;
}
if (move_descriptor(fd, STDOUT_FILENO)) {
errormessage(argv[2], strerror(errno));
return EXIT_FAILURE;
}
if (strchr(argv[3], '/'))
execv(argv[3], (char *const *)(argv + 3));
else
execvp(argv[3], (char *const *)(argv + 3));
errormessage(argv[3], strerror(errno));
return EXIT_FAILURE;
}
move_descriptor() 只是 dup2() 和 close() 的包装。我包含它是为了展示如何移动描述符(复制和关闭旧的)安全;有足够的错误检查。
wrerr(msg) 函数类似于fputs(msg, stderr),只是它直接使用文件描述符接口(@987654323@),完全绕过了 C 标准错误流抽象。它也是async-signal safe*,这意味着您可以在信号处理程序中使用它。
*:从技术上讲,人们可能会争论 strlen() 是否是异步信号安全的。在使用 glibc、newlibc 或 avr-libc 的 Linux 中,它是。
许多 Linux/Unix/POSIXy 错误消息使用格式 "filename: Error message." 由于 wrerr() 函数只接受一个参数,我包含了 errormessage(filename, message) 帮助函数来打印此类错误消息。这种将常用任务拆分为辅助函数的做法使代码更易于阅读,也更易于维护。
程序本身至少需要四个命令行参数。 (第一个参数argv[0]是命令本身。第一个参数是argv[1]。例如,如果你编译并运行它为./example arg1 arg2 arg3,那么argv[0]=="./example",@987654351 @,argv[2]=="arg2",和argv[3]=="arg3"。在Linux和POSIXy系统中,argv[argc] == NULL,所以我们可以直接在execv()和execvp()中使用argv数组和相关函数。
如果命令行参数少于四个,或者如果argv[1] 匹配"-h" 或"--h",我们打印usage 并退出。
否则,第一个参数 (argv[1]) 命名我们重定向输入的文件(并且它必须存在),第二个参数 (argv[2]) 命名我们将输出重定向到的文件(如果它,我们将创建它还不存在)。
O_NOCTTY 标志起初可能看起来令人困惑,但我将其包含在内,因为它在重定向来自类文件对象的输入时非常常见。这基本上意味着即使路径名指的是终端,打开它时也不要做任何与终端和会话相关的魔法:“如果它是一个终端,并且我们碰巧没有控制终端,请不要不要让它成为我们的控制终端”。 (它只影响在新会话中运行的程序(通过setsid)或通过 cron、udev 等服务运行的程序,因为您通常从终端运行的程序将该终端作为其控制终端。关于终端和会话的主要内容就是如果控制终端关闭,每个以该终端作为控制终端的进程都会收到挂断(SIGHUP)信号。)
当打开我们将输出重定向到的文件时,我们使用O_CREAT 标志,并添加一个额外的参数,文件访问模式。前导零表示0666是八进制常数,即base-8,指十进制值6·82 + 6·81 + 6·8 0 = 438。这是您看到最常用的标准值。它由当前的 umask(通过运行umask 可以在 shell 中看到其值)修改(由内核)。它是用八进制写的,因为右起第三个数字指定所有者(用户)权限,右起第二个数字指定组权限,最右边是其他所有人的权限; 1 是读访问,2 是写访问,4 是执行(对于文件)或通过/工作(对于目录)。 (每个文件在 Linux 中都有一个所有者(用户)和组,就像在所有 Unix 和 POSIXy 系统中一样。)
只要open() 标志包含O_CREAT,就必须提供额外的访问模式值。它几乎总是0666,除了一些你想使用更严格的值的极少数情况;但一般规则是使用0666,并让用户通过修改umask 使其更具限制性:无论如何,这几乎是所有实用程序所做的。
第三个命令行参数(第四个参数argv[3])包含我们将运行的可执行文件的名称或路径。如果它包含斜杠 (/),我们假设它是路径名引用,并使用 execv()。如果它不包含斜杠,我们假设它是一个名称,并使用execvp(),它使用PATH 环境变量来查找具有该名称的可执行文件。
如果exec成功,则用新程序替换进程;执行在 exec 处结束。
如果 exec 由于任何原因失败,execv() 和 execvp() 都会设置 errno。打印命令(不带任何参数,如果有的话)和错误消息是有意义的,在这种情况下以失败退出。