目录
一、文件描述符
1.文件内核对象
2.文件描述符分配原则
二、文件重定向
1.重定向的现象
输出重定向
输入重定向
dup2
2.重定向的使用
三、标准输出和标准错误
继上篇文章中,我们了解了fd打印的值为文件描述符,那么它还有什么作用呢?
一、文件描述符
1.文件内核对象
我们学习了open函数,知道了文件描述符就是一个整数。Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是标准输入 0, 标准输出 1, 标准错误 2。0、1、2 对应的物理设备一般是:键盘、显示器、显示器。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
输入输出程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>int main()
{char buf[1024];// 定义一个1024字节的缓冲区ssize_t s = read(0, buf, sizeof(buf));//从标准输入(stdin)读取数据到缓冲区bufif(s > 0)// 检查是否成功读取到数据{buf[s] = 0; // 手动添加字符串终止符'\0'write(1, buf, strlen(buf));// 将数据写入标准输出write(2, buf, strlen(buf));// 将数据写入标准错误//write() 是直接写入内核,无需手动刷新缓冲区。}return 0;
}
运行后,输出一个yes,就会再往显示屏打印两个yes
write是如何通过fd找到要打印的文件的呢?
重新理解文件描述符表:
站在用户的角度,对于文件的操作有这些诸如read、write这些系统调用,也有这些系统调用进行了一定的封装后诞生的C语言库函数,这些系统调用的内部会在内存中进行一系列操作,当程序运行的时候,进程里有个叫PCB的结构体,进程的PCB中有这么一块专门的区域名字叫作fd_array[],它的本质是一个指针数组,它会指向一个一个结构体,而每一个结构体中存储的是关于这个文件的信息,这个结构体叫做struct file,而前面所说的文件描述符fd,本质上就是这个指针数组的下标。系统默认会打开三个文件:标准输入、输出和错误。比如你调用read或write的时候,系统会根据fd找到对应的file结构体,然后操作对应文件。不过数据读写不是直接和硬盘打交道的——因为CPU只能和内存玩,所以读写前得把数据先从硬盘加载到内存里的文件缓冲区里,进程才能从中读写。不管读还是写,都是在内核缓冲区和用户空间之间倒腾数据,这也就是为什么系统调用看起来像是在内存里搞事情的原因啦。 结论:在应用层对于数据的读写,本质上是将内核缓冲区的数据进行来回拷贝。
2.文件描述符分配原则
我们知道因为系统会默认打开三个文件,分别占用0,1,2
这三个位置, 那么把前面这几个关掉呢?
关闭0:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>int main()
{close(0);int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
运行结果(关闭2也是一样的效果):
由此可以推测出,文件描述符的分配规则是:寻找最小的,没有被使用的数据的位置,分配给指定的打开文件
二、文件重定向
1.重定向的现象
刚刚其实我们已经细微的发现了重定向了,但是现在我们将要学的更加细:
输出重定向
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>// 重定向
void test()
{close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0){perror("open fail\n");exit(1);}printf("fd-> %d\n", fd);printf("stdout->fd: %d\n", stdout->_fileno);fflush(stdout);close(fd);
}int main()
{test();return 0;
}
运行结果:
运行结果是,在屏幕上没有任何信息,但在log.txt
中居然出现了输出信息。
当程序通过关闭标准输出(fd=1)并重新打开一个文件(如log.txt)时,由于文件描述符遵循"用完即丢"的复用规则,新文件会抢占原本属于标准输出的1号索引位置。此时所有以1为默认目标的输出操作(包括printf)都会被悄悄重定向——它们不再往显示器写数据,而是像对待普通文件一样向log.txt写入内容。这个过程中,Linux内核通过VFS将设备抽象成统一的文件对象,使得无论stdout原本指向的是屏幕还是磁盘文件,最终都表现为对特定文件描述符的操作。这种设计巧妙利用了文件描述符的索引特性,实现了I/O重定向的核心逻辑:系统只认数字标号,不关心具体绑定的物理设备,真正实现了"一切皆文件"的统一管理理念。总结:新打开的文件描述符取代了stdout原先的描述符,大家只认fd==1的文件。
输入重定向
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>int main()
{close(0);int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);char buf[64];fgets(buf, sizeof buf, stdin);printf("%s", buf);return 0;
}
重定向的本质,就是修改特征文件fd的下标内容。上层的fd不变,变化的是底层fd指向的内容,也就是所谓文件描述符级别的数组内容的拷贝。那这样的写法还是太奇怪了,每次都要把一个文件关闭再打开一个新的文件,作为系统理应给操作者提供这样替换文件描述符的系统调用,事实上也确实提供了这样的系统调用。
dup2
SYNOPSIS#include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
简单来说,就是用oldfd
去替换newfd
,保留下来的是oldfd
,那么上面的代码就可以被改良成这样:
void redir2()
{int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0){perror("open fail\n");exit(1);}dup2(fd, 1);printf("fd-> %d\n", fd);printf("stdout->fd: %d\n", stdout->_fileno);fflush(stdout);close(fd);
}
运行结果:
fd->3,fd也没有变化,但是就是没有往显示屏上打印,依旧是往文件里打印的。也就是说,不需要你去关闭一个文件秒速符,而是用一个文件描述符暂时的代替另外一个文件描述符,这样不仅更加准确,而且更加明了。
要输出的文件描述符是 1,而要重定向的目标文件描述符是 fd (echo “hello” > log.txt),dup2 应该怎么传参 —— dup2(1, fd) || dup2(fd, 1) ?
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
很明显依靠函数原型,我们就能认为 dup2(1, fd),因为 1 是先打开的,而 fd 是后打开的.可实际上并不是这样的,文档中说 newfd 是 oldfd 的一份拷贝,这里拷贝的是文件描述符对应数组下标的内容,所以数组内容最终应该和 oldfd 一致。换而言之,这里就是想把让 1 不要指向显示器了,而指向 log.txt,fd 也指向 log.txt。所以这里的 oldfd 对应 fd,newfd 对应 1,所以应该是 dup2(fd, 1)。
oldfd copy to newfd -> 最后要和谁一样? oldfd
假设输出重定向:显示器(1) -> log.txt(3)。应该是 dup2(1, 3);,还是dup2(3, 1); ?
dup2(3, 1);
3 的内容 copy 到 1 里面 -> 最终和 3 一致
2.重定向的使用
重定向其实并不陌生,在之前的学习中已经用过重定向,只是那是还没有建立起来一个基础的概念,先看下面的指令演示:
[root@iZbp1157ft1ib0ydj8jqtzZ test]# echo "hello linux" > log.txt
[root@iZbp1157ft1ib0ydj8jqtzZ test]# cat log.txt
hello linux
这段指令的含义就是,把hello linux重定向到log.txt中,其实这样的操作符还有下面的这几种,一一进行介绍
>:这个操作符表示的是输出重定向,意思就是把内容输出到某个文件中,有些类似于以w的方式打开一个文件并进行写入。
>>:这个操作符表示的是追加重定向,意思就是把内容追加输出到某个文件中,有些类似于append的方式进行写入。
<:这个操作符表示的是输入重定向,表示把原来的内容输入输入到某个文件中,相当于是替换了标准输入流的文件。
三、标准输出和标准错误
前面的知识已经足以理解为什么要有标准输入和标准输出,但是还有一个问题有待解决,标准错误的意义是什么呢?难道标准输出的信息还不够吗?
答案是确实不够,在对于大型项目的时候,会有很多的输出信息,这些输出信息有些是正常信息,有些是异常信息,而对于开发者来说他们需要的是错误信息,因此对于如何获取错误信息就显得至关重要,于是标准错误流信息就诞生了,对于正常来说可能没有太多的感觉,但是实际上,没有感觉的原因是因为标准错误和标准输出的文件对象都是显示器,而实际上这是可以被替换的,基于这个原理可以做出下面的测试代码:
int main()
{printf("this is normal message\n");perror("this is error message\n");return 0;
}
运行和操作结果:
[root@iZbp1157ft1ib0ydj8jqtzZ test]# ./test 1>out.txt 2>error.txt
[root@iZbp1157ft1ib0ydj8jqtzZ test]# cat out.txt
this is normal message
[root@iZbp1157ft1ib0ydj8jqtzZ test]# cat error.txt
this is error message
: Success
利用上面的原理可以写出这样的测试代码,把正确信息存储到一个文件中,把错误信息存储到另外一个文件中,这样就能知道哪里是错误哪里是正确了。
1>&2
意思是把标准输出重定向到标准错误。
2>&1
意思是把标准错误输出重定向到标准输出。