文件描述符
文件描述符的概念和原理
通过上述内容,我们知道使用 open
系统调用打开文件时,系统会返回一个文件描述符。这个描述符用于后续的文件操作。
在C语言中默认会打开三个输入输出流,分别是stdin,stdout,stderr
仔细观察会发现,这三个流的类型都是FILE*,fopen返回值类型是文件指针,C语言库函数式对系统接口的封装。故FILE中必然存在一个保存描述符的变量间,即_fileno
#include <stdio.h>
int main()
{
printf("stdin->%d\n", stdin->_fileno);
printf("stdout->%d\n", stdout->_fileno);
printf("stderr->%d\n", stderr->_fileno);
return 0;
}
文件描述符 | 描述 | 对应设备 |
---|---|---|
0 | 标准输入 | 键盘 |
1 | 标准输出 | 显示器 |
2 | 错误输出 | 显示器 |
文件描述符fd的本质是:内核的进程:文件映射关系的数组的下标
下面,我们使用系统接口从0号描述符读入内容,并将读入内容写入1号及2号描述符
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(){
char buffer[1024];
read(0, buffer, sizeof(buffer));
printf("write to 1st fd: \n");
write(1,buffer,strlen(buffer));
printf("write to 2st fd: \n");
write(2,buffer,strlen(buffer));
return 0;
}
-
向1号和2号描述符中打印都是输出到显示器,那它们有什么区别呢?
◉ 用途上的区别:
标准输出(stdout):通常用于程序的正常输出,比如程序的运行结果。
标准错误(stderr):用于程序的错误输出,包括运行时产生的警告和错误信息。
◉ 重定向行为上的区别:
标准输出(stdout)和标准错误(stderr)都可以被重定向到文件或其他目的地,但是它们的行为有所不同。
如果标准输出被重定向,而标准错误没有,那么标准错误仍然会默认输出到显示器(如果之前没有重定向过)。
如果标准错误被重定向,而标准输出没有,那么标准输出仍然会默认输出到显示器(如果之前没有重定向过)。
◉ 错误信息显示:
标准错误(stderr)默认情况下总是显示错误信息,即使它们被重定向。这有助于用户快速识别和解决问题。
标准输出(stdout)则不总是显示错误信息,除非它也被重定向。
◉ 重定向的文件描述符:
标准输出(stdout)默认情况下被重定向到显示器,文件描述符为 1。
标准错误(stderr)默认情况下也被重定向到显示器,文件描述符为 2。
◉ 信号处理上的区别:
标准输出(stdout)和标准错误(stderr)在某些情况下(如接收到特定信号时)可能会被特殊处理。例如,当接收到
SIGPIPE
信号时,标准输出(stdout)可能会被关闭,而标准错误(stderr)则不受影响。
标准输出被重定向,而标准错误没有,那么标准错误仍然会默认输出到显示器(如果之前没有重定向过),但是标准输出的内容被打印到指定文件当中
-
那么如何标准错误被重定向呢?
在 UNIX 和类 UNIX 系统中,标准错误(stderr)可以通过不同的方法重定向。这里有几种常见的方法:
使用
2>
符号:./mytest 2> log1.txt这会将
mytest
程序的标准错误输出重定向到log1.txt
文件,而标准输出仍然会发送到屏幕。使用
2>>
符号:./mytest 2>> log1.txt这会将
mytest
程序的标准错误输出追加到log1.txt
文件,而不是覆盖已有的内容。如果文件不存在,它将创建该文件。使用
&>
符号(双竖线):./mytest &> log1.txt这会将
mytest
程序的标准输出和标准错误输出都重定向到log1.txt
文件。如果文件不存在,它将创建该文件。使用
2>|
符号:./mytest 2>| command这会将
mytest
程序的标准错误输出通过管道传递给command
命令。使用
2>&
符号:./mytest 2>&1这会将
mytest
程序的标准错误输出重定向到标准输出(stdout),这意味着错误信息将与正常输出一起显示在屏幕上。请注意,重定向操作符
>
和>>
必须位于命令的最后一个参数之前,并且它们不能与&
操作符同时使用。如果您需要同时重定向标准输出和标准错误,请使用&>
操作符。
将打印到2号描述符的内容打印到err.txt,打印到1号描述符的内容打印到显示器
将写入到1号和2号文件描述符的内容输入到 all.txt,则需要执行 ./mytest > all.txt 2>&1
或./mytest > all.txt 1>&2
二者输出结果相同
代码解释:
这个命令行
./mytest > all.txt 2>&1
执行了以下操作:
标准输出重定向:首先,
mytest
程序的标准输出(stdout)被重定向到文件all.txt
。这意味着mytest
程序的输出(除了错误信息)不会显示在屏幕上,而是会被写入到all.txt
文件中。标准错误重定向:然后,
mytest
程序的标准错误输出(stderr)被重定向到标准输出(stdout)。由于标准输出已经被重定向到all.txt
文件,因此mytest
程序的错误信息也会被写入到同一个文件中。文件描述符 2:在 UNIX 系统中,文件描述符 2 通常用于标准错误输出。当使用
2>&1
时,实际上是将标准错误输出重定向到文件描述符 1,即先将标准错误输出重定向到标准输出,然后再将标准输出重定向到指定的文件。综上所述,这个命令行会将
mytest
程序的所有输出(包括正常输出和错误信息)写入到all.txt
文件中,而不会在屏幕上显示任何输出。
在 Linux 内核中,文件系统(Filesystem)是操作系统的一部分,用于管理和存储文件和目录。每个进程在内存中维护一个打开的文件列表,这个列表通过 struct files_struct
结构体来管理。每个打开的文件在 struct files_struct
中都有一个对应的 struct file
结构体,用于保存该文件的信息。
以下是 struct file
和 struct files_struct
结构体的简要概述:
struct file 结构体
struct file
结构体包含了关于打开的文件的信息,包括文件描述符、文件操作指针、文件状态、文件大小等。每个打开的文件都对应一个 struct file
实例。
struct files_struct 结构体
struct files_struct
结构体用于保存当前进程打开的文件列表。每个进程都有一个自己的 struct files_struct
实例。它包含了指向 struct file
实例的指针数组,以及一些管理打开文件的信息,如文件描述符表等。
当一个进程打开一个文件时,内核会创建一个 struct file
实例来保存该文件的信息,并将其添加到该进程的 struct files_struct
中的文件描述符表中。当进程关闭文件时,内核会从文件描述符表中移除对应的 struct file
实例,并释放与之相关的资源。
通过这种方式,Linux 内核能够有效地管理每个进程打开的文件,确保资源的正确分配和释放,从而避免资源泄露和文件描述符冲突等问题。
所以,本质上,文件描述符就是数组下标(即当前进程的文件描述符表下标)。只要取得对应的文件描述符,就能找到对应的文件。
文件描述符分配规则
系统会默认打开三个标准输入输出流stdin(0号)、stdout(1号)、stderr(2号),此时我们再打开一个文件时会默认分配3号描述符,再打开一个文件,那就会被自动分配4号描述符;但是如果我们关闭2号,再打开一个新的文件时,该文件的描述符为2。由此可知,文件描述符分配规则是:分配当前最小的未使用的文件描述符
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
int main()
{
printf("stdin->%d\n", stdin->_fileno);
printf("stdout->%d\n", stdout->_fileno);
printf("stderr->%d\n", stderr->_fileno);
int fd1 = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd1->%d\n", fd1);
//关闭标准输入
close(0);
int fd2 = open("./log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd2->%d\n", fd2);
//关闭标准错误
close(2);
int fd3 = open("./log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd3->%d\n", fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
访问文件的本质
当进程打开一个文件的时候,操作系统会创造对应的数据结构;在类Unix操作系统中,struct file
是内核中的一个数据结构,用于表示打开的文件。它通常包含文件位置(当前读写操作的文件偏移量)、文件操作(指向一组函数指针,这些指针定义了对文件执行各种操作的方法,如读写、打开、关闭等。)、文件状态标志(如读写模式、同步等。)、引用计数(表示有多少进程或文件描述符引用了这个struct file
)、文件信息(如文件大小、最后修改时间等)等
文件访问的流程:
打开文件:
-
当进程调用
open
系统调用时,操作系统会创建一个struct file
结构体,并将其地址放入进程的文件描述符表中。 -
如果文件已经被其他进程打开,则操作系统会增加现有
struct file
的引用计数,而不是创建一个新的。
文件操作:
-
进程通过文件描述符执行读写操作时,操作系统会根据文件描述符找到对应的
struct file
,并执行相应的文件操作。
关闭文件:
-
当进程调用
close
系统调用时,操作系统会减少struct file
的引用计数。 -
如果引用计数降到0,操作系统会释放
struct file
结构体,并可能执行清理操作,如关闭文件描述符、释放内存等。
当多个进程打开同一个文件时,它们共享同一个struct file
结构体,但每个进程都有自己的文件描述符,指向这个共享的struct file
。这意味着即使一个进程关闭了文件,只要其他进程还在使用该文件,struct file
就不会被释放
重定向原理
我们前面已经使用了重定向,这里我们系统的学习一下重定向
在计算机科学中,特别是操作系统的上下文中,重定向指的是改变输入输出流的方向,使其从一个默认的源或目的地转移到另一个源或目的地。我们之前就使用过输出重定向(>
和 >>
)输入重定向(<
)错误输出重定向 (2>
) 合并重定向 (&>
) 管道(|
)当然还有其他的就不介绍了
dup2系统调用
在类Unix操作系统中,dup2
是一个系统调用,用于复制一个现有的文件描述符到另一个指定的文件描述符。
dup2的本质:dup2() makes newfd be the copy of oldfd, closing newfd first if necessary)(文件描述符下标所对应的内容拷贝)
如果我们希望将 fd 描述符被1号替换则可以使用 dup2(fd , 1)
【示例1】
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
int main()
{
// close(0);
// close(2);
// close(1);
umask(0);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd == -1)
{
perror("open");
exit(1);
}
//重定向
dup2(fd,1);
printf("fd:%d\n",fd);
//这里必须刷新一下,不然log.txt里面没有内容
fflush(stdout);
close(fd);
return 0;
}
可以发现我们执行可执行程序的时候,并未在命令行打印,反而打印到文件当中;
如果我们把dup2(fd,1)
注释了,那么重新执行的结果;即:printf默认输出到stdout上(显示器)
而文件内部不会打印
所以说重定向的本质:是在内核中改变文件描述符表特定下表的内容,与上层无关、
缓冲区的理解
在C语言里相关IO函数与系统调用是对应的,并且相关库函数是对系统调用的封装;所以本质上,C语言访问文件就是通过fd(文件描述符访问的)。所以在C语言内FILE结构体,必定有fd(文件描述符)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
const char* msg1 = "printf\n";
const char* msg2 = "fprintf\n";
const char* msg3 = "fwrite\n";
const char* msg4 = "write\n";
printf("%s", msg1);
fprintf(stdout, "%s", msg2);
fwrite(msg3, strlen(msg3), sizeof(char), stdout);
write(1, msg4, strlen(msg4));
fork();
}
执行结果:
如果将输出重定向到log.txt呢?
可以看出,如果向屏幕输出,则每个语句打印一次;如果将结果重定向到指定文件中,除了操作系统提供的write打印一次,其余的C语言接口均打印两次,这是为什么?并且重定向后为什么write先打印出来呢?
这个现象的原因在于 fork()
系统调用后的进程行为以及标准输出的缓冲机制。
1️⃣ 缓冲区是什么?本质就是一块连续的空间!
2️⃣ 为什么要有缓冲区?给上层提供高效的IO体验,间接提高整体的效率
3️⃣ 缓冲区的刷新策略:
◉ 立即刷新(无缓冲):fflush(stdout);ffsync(int fd)
◉ 行缓冲(显示器):照顾用户的查看习惯
◉ 全缓冲(缓冲区写满才刷新):常用于普通文件的写入,该缓冲区在进程退出时也会刷新
4️⃣缓冲区分为用户级缓冲区和内核级缓冲区
【解答上面问题】:
首先我们已经了解到./test
默认是向显示器文件进行写入,显示器文件默认是行刷新
./test > log.txt
这个是向普通文件进行写入,刷新策略也会发生变化,变为全缓冲
在fork()前 write是系统调用,直接写到操作系统内核,甚至已经写到硬件中
但printf等是stdout对应的缓冲区是用户级别的缓冲区,用户级别是全缓冲,此时缓冲区没有写满,不会加载到操作系统内核,而调用fork()后,进程结束时,父子进程会都把缓冲区刷新一遍,刷新就是读取,就是写时拷贝,所以出现了父进程打印一遍内容,子进程打印一遍内容。
C语言中的缓冲区 语言中的缓冲区
都在这个struct _IO _FILE中,所以每一个文件都有自己的缓冲区(printf,scanf)