:基础IO——系统调用、文件描述符与重定向)
一、 系统调用 vs 库函数我们常用 C 语言的fopen、printf这属于 C 标准库函数用户态。而 Linux 系统底层真正干活的是系统调用接口open、read、write、close。库函数本质上是对系统调用的封装底层还是通过文件描述符fd来操作。二、 深入理解文件描述符fd使用open打开文件后返回值是一个整数这就是 fd。它是怎么产生的在操作系统的进程控制块PCB中有一个指针指向files_struct结构体里面包含一个指针数组。这个数组的下标就是 fd0标准输入键盘1标准输出显示器2标准错误显示器分配规则系统分配 fd 时永远找当前未被使用的最小下标。所以我们新打开的文件默认是 3如果关掉 0新打开的文件就会变成 0。三、 重定向的本质我们平时用的,符号本质上是改变了 fd 的指向。如果标准输出1号原本指向显示器我们通过代码让 1 号指向一个磁盘文件那么printf输出的内容就会跑到文件里。实现这个魔法的核心系统调用是dup2(oldfd, newfd)。它会将oldfd复制给newfd让newfd也指向同一个文件。四、 实战代码体验重定向int fd open(log.txt, O_CREAT | O_WRONLY | O_TRUNC, 0644); // 此时 fd 是 3指向 log.txt // 让 1 号标准输出也指向 log.txt dup2(fd, 1); // 接下来 printf 的内容全都会写入 log.txt不再显示在屏幕上 printf(这句话被重定向到文件了\n); close(fd);总结“一切皆文件”是 Linux 的核心哲学。通过文件描述符Linux 把硬件键盘、显示器和磁盘文件统一成了同一套操作接口。掌握dup2就能理解各种重定向和管道底层的运作机制。自我实战操作cd ~/linux_study mkdir io_test cd io_test实战 1体验系统调用 I/Ovim sysio.c(输入以下代码)#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h int main() { // 打开文件如果没有则创建权限设为 0644 int fd open(test.txt, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd 0) { perror(open); return 1; } printf(成功打开文件得到的 fd 是: %d\n, fd); // 大概率是 3 const char *msg Hello System IO!\n; // 用系统调用 write 写入 write(fd, msg, strlen(msg)); close(fd); return 0; }编译运行gcc sysio.c -o sysio ./sysio cat test.txt 你会发现 fd 是 3。因为 0、1、2 被系统占用了。实战 2验证 fd 的分配规则vim fd_rule.c(输入以下代码)#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h int main() { close(0); // 关掉标准输入 int fd open(test.txt, O_RDONLY); printf(关掉 0 号后新打开的 fd 是: %d\n, fd); // 你会发现变成了 0 close(fd); return 0; }(现象系统永远找最小的空位。这证明了 fd 就是数组下标。)实战 3用dup2完成输出重定向vim redir.c(输入以下代码)#include stdio.h #include unistd.h #include fcntl.h int main() { // 1. 打开一个文件此时 fd 是 3 int fd open(log.txt, O_CREAT | O_WRONLY | O_TRUNC, 0644); // 2. 将 fd3复制给 1标准输出 // 原本 1 指向显示器现在 1 和 3 指向同一个文件 dup2(fd, 1); // 3. 此时用 printf 输出原本该上屏幕的字全进 log.txt 了 printf(这句话会被写进文件里\n); printf(标准输出已经被重定向啦\n); close(fd); return 0; }编译运行gcc redir.c -o redir ./redir # 屏幕上什么都不会显示 cat log.txt # 打开文件看字全在里面。实战 4进阶——给之前写的微型 Shell 加上重定向选做第一步创建新的 Shell 文件进入目录新建一个文件cd ~/linux_study/io_test vim myshell_redir.c第二步复制并保存以下代码#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/wait.h #include sys/types.h #include sys/stat.h #include fcntl.h #define MAX_ARGS 10 // 定义重定向类型 #define NoneRedir 0 #define OutputRedir 1 // #define AppRedir 2 // int main() { char buf[1024]; char *argv[MAX_ARGS]; int redir NoneRedir; char *filename NULL; while (1) { // 每次循环重置状态 memset(argv, 0, sizeof(argv)); redir NoneRedir; filename NULL; printf(myshell ); fflush(stdout); // 1. 获取用户输入 if (fgets(buf, sizeof(buf), stdin) NULL) continue; buf[strcspn(buf, \n)] 0; // 去除换行符 if (strcmp(buf, exit) 0) break; if (strlen(buf) 0) continue; // 2. 解析重定向非常简单粗暴的解析找 或 char *ptr strstr(buf, ); if (ptr ! NULL) { redir AppRedir; *ptr \0; // 把 变成字符串结束符切断前面命令 filename ptr 2; // 文件名在 的后面 while (*filename ) filename; // 跳过空格 } else { ptr strchr(buf, ); if (ptr ! NULL) { redir OutputRedir; *ptr \0; // 把 变成字符串结束符 filename ptr 1; // 文件名在 的后面 while (*filename ) filename; // 跳过空格 } } // 3. 解析命令行按空格切分注意此时 buf 里只剩下命令部分了 int argc 0; argv[argc] strtok(buf, ); while (argv[argc] ! NULL) { argc; argv[argc] strtok(NULL, ); } if (argc 0) continue; // 用户只敲了回车或重定向符号 // 4. Fork 创建子进程执行 pid_t pid fork(); if (pid 0) { // --- 子进程内部 --- // 核心处理重定向 if (redir ! NoneRedir filename ! NULL) { int fd -1; if (redir OutputRedir) { // 以写方式打开不存在则创建存在则清空 fd open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644); } else if (redir AppRedir) { // 以追加方式打开不存在则创建 fd open(filename, O_WRONLY | O_CREAT | O_APPEND, 0644); } if (fd 0) { perror(open error); exit(1); } // 将标准输出1指向我们打开的文件 dup2(fd, 1); close(fd); // 复制完就可以关掉原来的 fd 了 } // 执行命令 execvp(argv[0], argv); perror(execvp error); // 走到这说明命令没找到 exit(1); } else if (pid 0) { // 父进程等待回收 wait(NULL); } } return 0; }第三步编译并运行gcc myshell_redir.c -o myshell_redir ./myshell_redir第四步见证奇迹的时刻在myshell提示符下依次输入以下命令体验测试普通命令myshell ls -l(应该正常在屏幕上打印出当前目录的文件列表)测试输出重定向myshell ls -l out.txt(屏幕上什么都不打印退回到myshell提示符。此时你输入cat out.txt或退出 shell 后看内容全在里面)测试追加重定向myshell echo hello out.txt myshell echo world out.txt(再用cat out.txt查看你会发现 hello 和 world 都追加进去了没有被覆盖)测试错误命令myshell abcdefg(会提示execvp error: No such file or directory因为系统找不到这个命令)