Shell 重定向实现原理
类 Unix 系统中的重定向实现
在 bash 等典型的 Unix shells 中,program < numbers.txt 命令的行为是从 number.txt 创建文件流,并将 program 进程的标准输入重定向到该文件流中。值得注意的是 numbers.txt 是由 shell 进程打开的。文件打开后,shell 进程暂时将自身的标准输入重定向到 fd,然后调用 fork 创建一个新的子进程。
子进程默认情况下会继承其父进程的文件描述符表,fd 的引用计数增加 1。此时,子进程的标准输入为 fd。创建子进程的同时,父进程可以恢复其标准输入流并关闭 fd,其引用计数减少 1,由于子进程仍然持有 fd,因此 numbers.txt 文件流仍然有效。随后,子进程调用 exec,创建真正的 program 进程。program 进程退出时,所有打开的文件描述符都会关闭。
在下面的示例中,我们首先创建一个包含若干无序数字的 numbers.txt:
echo 3 5 2 4 1 > numbers.txt
然后,编写如下 C 代码,将标准输入重定向到 numbers.txt 并调用 sort 程序对数字进行排序:
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(void) {
int fd = open("numbers.txt", O_RDONLY); // 假定 numbers.txt 已经存在
if (fd < 0) {
fprintf(stderr, "Failed to open numbers.txt, error code: %d.", errno);
return 0;
}
int status = dup2(fd, STDIN_FILENO);
fprintf(stdout, "Child's stdin redirected.\n");
if (status < 0) {
fprintf(stderr, "Failed to redirect stdin, error code: %d.\n", errno);
return 0;
}
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "Failed to fork child process, error code: %d.\n", errno);
return 0;
}
if (pid > 0) { // parent process
close(fd);
int status = dup2(STDIN_FILENO, STDIN_FILENO);
fprintf(stdout, "Parent's stdin restored.\n");
if (status < 0) {
fprintf(stderr, "Failed to restore stdin, error cide: %d\n.", errno);
}
} else { // child process
execlp("/bin/sort", "/bin/sort", (char*)NULL);
fprintf(stderr, "Failed to execute /bin/sort.\n");
}
return 0;
}
Windows 系统中的重定向实现
在 Windows 中,上述过程也是类似的,父进程通过 CreateFile 创建文件句柄后,可以使用 SetStdHandle 函数将标准输入、标准输出或标准错误重定向到该文件句柄上。
与 Unix 所不同的是默认的继承策略,Windows 的句柄继承策略更为严格,需要同时满足两个条件:
- 在打开/创建文件时,
SECURITY_ATTRIBUTES结构体中的bInheritHandles参数设置为TRUE。 - 在创建子进程时,
CreateProcess函数的bInheritHandles参数设置为TRUE。
只有满足上述两个条件,子进程才能继承父进程的文件句柄。
而要实现标准句柄的重定向,还需要在 STARTUPINFO 结构体中的 dwFlags 字段中包含 STARTF_USESTDHANDLES 标志,并设置 hStdInput、hStdOutput、hStdError 三个字段,明确要使用的标准句柄。否则,子进程仍将使用默认的标准句柄。
在下面的示例中,我们同样创建一个包含若干无序数字的 numbers.txt 文件:
echo 5 4 3 2 1 > numbers.txt
#include <Windows.h>
#include <stdio.h>
int main() {
SECURITY_ATTRIBUTES sa = {
.nLength = sizeof(SECURITY_ATTRIBUTES),
.bInheritHandle = TRUE,
.lpSecurityDescriptor = NULL
}; // 文件句柄允许继承
HANDLE hFile = CreateFileW(L"numbers.txt", GENERIC_READ, FILE_SHARE_READ, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
fprintf(stdout, "CreateFileW failed with %lu.\n", GetLastError());
return 0;
}
STARTUPINFOW si = {
.cb = sizeof(STARTUPINFOW),
.dwFlags = STARTF_USESTDHANDLES,
.hStdInput = hFile,
.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE),
.hStdError = GetStdHandle(STD_ERROR_HANDLE)
}; // 指定标准句柄
PROCESS_INFORMATION pi;
wchar_t cmd[] = L"C:/Windows/System32/sort.exe";
BOOL bSuccess = CreateProcessW(
NULL,
cmd,
NULL,
NULL,
TRUE, // 允许子进程继承句柄
0,
NULL,
NULL,
&si,
&pi
);
if (!bSuccess) {
fprintf(stdout, "CreateProcessW failed with %lu.\n", GetLastError());
} else {
fprintf(stdout, "Child process created successfully.\n");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
CloseHandle(hFile); // 关闭文件句柄
}
需要特别注意的是,CreateProcessW 接收的 lpCommandLine 参数必须是可写的字符串缓冲区,不能传入字符串常量,否则程序会因尝试写入只读内存而在运行时直接崩溃。C 语言编译器不会对此进行检查,而 C++ 编译器在编译时会发出警告。