【Linux】文件IO深度解析:文件描述符与重定向的奥秘
Zfox_ 2024-10-24 09:07:01 阅读 96
🌈 个人主页:Zfox_
🔥 系列专栏:Linux
目录
一:🔥 C语言中文件IO操作 🥝 1.C语言中的开关读写文件🦋 1.1 fopen()🦋 1.2 fclose()🦋 1.3 fwrite()🦋 1.4 fread()
🥝 2.stdin && stdout && stderr🥝 3.三个标准流和IO接口
二:🔥 系统文件IO 🥝 1.系统级别的开关读写文件🦋 1.1 open()🦋 1.2.close()🦋 1.3.write()🦋 1.4 read()
🥝 2.系统文件IO VS C文件IO
三:🔥 文件描述符fd 🥝 3.1.什么是文件描述符🥝 3.2 如何创建文件描述符🥝 3.3 文件描述符的分配规则
四:🔥 重定向 🦋 dup2 系统调用
五:🔥 FILE 🦋 文件缓冲区
六:🔥 共勉
一:🔥 C语言中文件IO操作
🥝 1.C语言中的开关读写文件
🦁 在学习 <code>Linux 中的 IO
操作之前,我们先来简单的回顾一下,C语言中我们常用的一些 IO
操作的接口。
🦋 1.1 fopen()
FILE* fopen(const char* path, const char* mode);
🍊 函数参数
path
:要打开的文件mode
:打开文件的方式
r
:可读方式r+
:可读可写方式w
:可写方式,如果文件不存在,就创建一个文件。如果文件已经存在,就截断一个文件(清空文件内容)w+
:可读可写方式,如果文件不存在,就创建一个文件。如果文件已经存在,就截断一个文件(清空文件内容)a
:追加写,但是不可以读取内容。如果文件不存在,就创建一个文件。如果文件已经存在,就在文件的末尾开始追加写a+
:追加写,可以读取内容。如果文件不存在,就创建一个文件。如果文件已经存在,就在文件的末尾开始追加写
🍊 函数返回值
成功:返回一个文件流指针FILE失败:返回NULL作用:以某种方式打开一个文件,并返回一个指向该文件的文件流指针。
🦋 1.2 fclose()
int fclose(FILE* fp);
作用:关闭传入的文件流指针指向的文件。
🦋 1.3 fwrite()
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
🍊 函数参数
ptr
:写入文件的内容size
:往文件中写入的块的大小,单位为字节count
:预期写入的块数stream
:预期写入文件的文件指针
🍊 函数返回值
成功:写入文件中的块数
常见用法
#include <cstdio>
#include <cstring>
#include <cstdlib>
int main()
{
FILE* fp = fopen("myfile.txt", "w");
if (!fp) {
perror("fopen");
exit(-1);
}
const char* msg = "hello Linux file\n";
fwrite(msg, sizeof(char), strlen(msg), fp);
fclose(fp);
return 0;
}
🦋 1.4 fread()
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
🍊 函数参数
ptr
:将从文件读取的内容保存在ptr所指向的空间中size
:定义读文件时块的大小,单位为字节count
:期望从文件中读的块数stream
:预期读取文件的文件指针
🍊 函数返回值
成功从文件中读取的块的个数
常见用法
#include <cstdio>
#include <cstring>
#include <cstdlib>
int main()
{
FILE* fp = fopen("myfile.txt", "r");
if (!fp) {
perror("fopen");
exit(-1);
}
char buff[64];
fread(buff, sizeof(char), sizeof(buff) / sizeof(char), fp);
printf("%s", buff);
fclose(fp);
return 0;
}
🥝 2.stdin && stdout && stderr
🦁 默认情况下,C语言会自动打开两个输入输出流,分别是 stdin
,stdout
,stderr
。
这三个流的类型都是 FILE,也就是文件指针类型。
既然是文件指针,所以这三个指针分别指向键盘,显示器,显示器。后面的系统IO会再详细的讲解这三个输入输出流。
🥝 3.三个标准流和IO接口
💦 可以利用上面这三个标准流和C语言的IO接口,将字符串直接打印到显示器上。
#include <cstdio>
#include <cstring>
#include <cstdlib>
int main()
{
FILE* fp = fopen("myfile.txt", "w+");
if (!fp) {
perror("fopen");
exit(-1);
}
char buff[64];
fread(buff, sizeof(char), 12, stdin);// 从键盘中输入到buff中
fwrite(buff, sizeof(char), strlen(buff), stdout); // 从buff中写入到显示器上
fclose(fp);
return 0;
}
运行结果:
[lisi@hcss-ecs-a9ee work]$ ./a.out
hello linux
hello linux
二:🔥 系统文件IO
其实除了C语言之外,很多语言都是自己的IO接口函数,但是下面我们要谈论的就是 Linux 系统给我们提供的IO接口,也就是系统级别的IO接口。
🥝 1.系统级别的开关读写文件
🦋 1.1 open()
// 在打开的文件已经存在的时候
int open(const char* Path, int flags);
// 在打开的文件不存在的时候
int open(const char* Path, int flags, mode_t mode);
🍊 函数参数
Path
:需要打开的文件flags
:打开文件的方式
🎯 必选项
O_RDONLY
:只读方式O_WRONLY
:只写方式O_RDWR
:读写方式 🎯 可选项
O_TRUNC
:截断文件(清空文件内容)O_CREAT
:文件不存在则创建文件O_APPEND
:追加方式
🍊 原理
可以使用按位或的方式进行组合:如打开并创建只写文件 O_WRONLY | O_CREAT
本质是利用了 位图
的方式来表示每一种的方式
mode
:当打开一个新开的文件的时候,需要给一个文件设置权限,需要设置一个8进制的数字。这个和umask
也会有关系
🍊 函数返回值
成功:返回一个文件描述符(后面介绍)失败:返回 -1作用:打开一个文件
🦋 1.2.close()
int close(int fd);
🍊 函数参数
fd
:文件描述符作用
:关闭一个文件
🦋 1.3.write()
ssize_t write(int fd, const void* buf, size_t count);
🍊 函数参数
fd
:文件描述符buf
:将buf中的内容写到文件中count
:期望写入的字节数
🍊 返回值
返回的字节数
代码示例:
#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>
int main()
{
// 创建一个权限为666的权限
umask(0);
int fd = open("file.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0) {
perror("open");
exit(1);
}
// 将msg写入file.txt中
const char* msg = "I am studing Linux IO\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
🦋 1.4 read()
ssize_t read(int fd, void* buf, size_t count);
🍊 函数参数
fd
:文件描述符buf
:将文件中的内容读到buf中count
:期望写入的字节数
🍊 返回值
返回的字节数
#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>
int main()
{
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
char buff[64];
read(fd, buff, sizeof(buff));
printf("%s", buff);
close(fd);
return 0;
}
🥝 2.系统文件IO VS C文件IO
认识一下两个概念: 系统调用
和 库函数
上面的
fopen fclose fread fwrite
都是C标准库当中的函数,我们称之为库函数(libc)
。而,open close read write lseek
都属于系统提供的接口,称之为系统调用接口
。
可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
做一个小实验:
<code>#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open fail");
return 1;
}
int cnt = 5;
char outBuffer[64];
while(cnt)
{
sprintf(outBuffer,"%s:%d\n","hello world",cnt--);
write(fd,outBuffer,strlen(outBuffer));
}
close(fd);
return 0;
}
特别注意:
🐮 我们在线文件中写入 string 的时候以、0 作为字符串的结尾,它是由 C语言规定的,但是这里是文件写入是,结束时是与 \0 无关的。所以在 strlen() 不需要 +1。
运行结果:
[lisi@hcss-ecs-a9ee work]$ cat log.txt
hello world:5
hello world:4
hello world:3
hello world:2
hello world:1
🦁 通过上面测试我们不难发现C语言中的库函数接口是通过封装了系统调用接口实现的
三:🔥 文件描述符fd
🐲 在上面open的接口中,我们提到了fd,这个也是open接口的返回值。而write和read接口也是通过fd这个参数使得文件可以读写,可以说fd是整个系统IO的灵魂,所以接下来,我们需要好好地理解一下fd
🥝 3.1.什么是文件描述符
🦁 <code>在Linux下一切皆文件,而大量的文件需要被高效的组织和管理,因此就诞生了文件描述符fd(file descriptor)。
🐮 文件描述符是内核为高效的管理已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的。
进程和文件之间的对应关系是如何建立的?
📖 由图可知:文件描述符就是从0开始的正整数。但我们打开一个文件的时候,操作系统都需要创建一个数据结构来描述这个文件。所以 <code>struct file 结构体就应运而生了,它就是表示打开的一个文件对象。
当进程执行 open
函数的时候,必须要让进程和文件关联起来。所以在每一个进程的 PCB
中都是一个 struct files_struct* files
指针,它指向一张表 files_struct
,这个表中有一个指针数组 fd_array[]
,其中指针数组的每一个元素都是一个指向 struct file
结构体的 struct file*
指针,而这个文件指针就指向打开的文件。
🦁 注意:向文件写入数据后,数据其实先写入对应文件的缓冲区当中,只有当将缓冲区中的内容刷新到磁盘当中时才算真正地写入到文件当中。
💦 小总结:
所以本质上文件描述符就是 file_struct
结构体中 fd_array
数组的下标。而只要拿到了这个文件描述符,就可以找到对应的文件。
什么是进程创建会默认打开文件的 0,1,2 ?
在Linux中,进程是通过 进程描述符fd
来访问文件的,文件描述符实际上是一个整数。在程序刚启动的时候,默认有三个文件描述符,分别是:0(代表标准输入stdin)
,1(代表标准输出stdout)
,2(代表标准错误stderr)
。对应的物理设备就是:键盘,显示器,显示器。
这三个文件设备都有自己对应的 struct file
系统会默认的生成这三个结构体,并使用双链表将他们连接起来,并且将 struct file
的地址放入到 struct file\* fd_array[]
数组的对应在 0, 1, 2 位置上。这个默认生成结构体并将地址放在 fd_array
数组的过程就叫做默认打开了标准输入流,标准输出流和标准错误流。
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0)
{
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
补充:磁盘文件和内存文件的区别 ?
上面说的都是在操作进程打开的文件,正是因为操作系统中有大量的进程打开了大量的文件,所以需要使用 struct file 和 struct files_struct 这样的结构体去管理这些文件。而这些文件都是在内存中加载的文件,所以我们称之为 「内存文件」。
如果一个文件储存在磁盘当中,我们就称之为 「磁盘文件」。这两种文件的关系就是当一个磁盘文件被加载到内存当中的话,就成为了内存文件。
磁盘文件由两部分构成:「文件内容」和「文件属性」。文件内容就是文件中的数据内容,而文件属性(元信息)就是一个文件的基本信息。
🥝 3.2 如何创建文件描述符
进程获取文件描述符最常见的方式就是通过系统调用接口open或者是从父进程继承过来的。
虽然文件描述符对于每一个进程的PID都是唯一的,但是每一个进程都是一个进程描述表struct files_struct,用于管理进程描述符,当使用fork创建子进程的时候,子进程会获得父进程进程描述表的一个副本,所以子进程可以拿到父进程的进程描述符,因此就可以打开父进程所有的文件。
🥝 3.3 文件描述符的分配规则
🌿 我们先上结论, 文件描述符的分配规则:在 files_struct
数组当中,找到当前没有被使用的最小的一个下标,作为 新的文件描述符
。
如果再打开一个新的文件的话,就分配一个最小的没有使用的 文件描述符fd。因为默认打开了0, 1, 2,所以新的文件描述符就应该从3开始的。
#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>
int main()
{
close(0); // 关闭stdin
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
printf("%d\n", fd);
close(fd);
return 0;
}
运行结果:
[lisi@hcss-ecs-a9ee work]$ ./a.out
0
四:🔥 重定向
那如果关闭1呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢 ?
每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件。
🦋 dup2 系统调用
在 Linux 系统中提供了系统接口dup2,这样函数是专门完成文件重定向的。
<code>#include <unistd.h>
int dup2(int oldfd, int newfd);
🍊 函数参数
oldfd
为需要重定向的文件newfd
为被重定向文件替换的文件
🍊 函数返回值
dup2
如果调用成功,返回 newfd
,否则返回-1。
🍊 注意事项:
如果 oldfd
不是有效的文件描述符,则调用 dup2
接口失败,并且此时的文件描述符 fd_array[newfd]
对应的文件没有关闭。
如果 oldfd
是有效的文件描述符,但是 oldfd == newfd
,则该接口不会发生任何的操作,而是直接返回oldfd
文件描述符。
举例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
int main()
{
umask(0);
int fd = open("file.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0) {
perror("open");
exit(1);
}
close(1);
dup2(fd, 1);
printf("hello Linux IO\n");
return 0;
}
运行结果:
[lisi@hcss-ecs-a9ee work]$ cat file.txt
hello Linux IO
🐮 使用printf打印的内容,打印到了file.txt文件当中。
五:🔥 FILE
🎯 因为库函数是对系统调用接口的封装,即fopen()中有open(),所以本质上来说访问文件都是通过文件描述符fd来访问的。所以我们可以在FILE结构体中有fd的存在。
为了搞清楚是不是这样的情况,我们可以通过源码来一探究竟。
我们可以在stdio.h的头文件下看到FILE。
typedef struct _IO_FILE FILE;
也就是说 FILE
其实是 struct _IO_FILE
。我们也可以在 libio.h
头文件下找到 struct _IO_FILE
结构体的定义,其中有一个叫 _fileno
的成员,这个变量中其实就是文件描述符 fd
。
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装了文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
现在重新理解一下,fopen究竟在底层做什么?
给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)。
在底层通过 open 打开文件,并返回 fd,把 fd 填充进 FILE 变量中的 _fileno。
综上:在C语言中,fread(), fwrite(), fputs(), fgets(),都是通过 FILE* 指针找到 FILE结构体,然后再在FILE 结构体中找到 _fileno 从而知道 fd,最后通过 fd 操作内存中的文件。
🦋 文件缓冲区
为什么我重定向以后它并没有直接给我写到指定文件中,而fflush(stdout)后就写进去了呢?
首先我们要知道,在C语言中我们使用的printf、scanf、fprintf、fscanf、fwrite、fread等都要求有一个FILE*的指针。所以,在调用这些函数进行操作时,它并没有直接调用系统调用read、write直接拷贝到文件的内核缓冲区,因为频繁的调用系统调用的成本太高了,效率低。
🍊 所以怎么能提高效率呢?
通过用户级缓冲区!! 你printf、fprintf等只需要将内容拷贝到用户级缓冲区中任务就完成了,无非就是在拷贝的过程中进行一下格式化;等用户级缓冲区攒了足够多的数量,在统一调用系统调用写入到文件的内核缓冲区,提高了效率。
该缓冲区在FILE结构体中,刷新的本质就是从用户级缓冲区拷贝到内核的文件缓冲区。
🍊 用户级缓冲区有以下几种刷新方案:
显示器文件:行刷新普通文件:缓冲区写满再刷新不缓冲(语言级无需刷新)
我们将最开始的代码修改一下会发现,如果我不调用任何的close或者调用fclose,内容可以正常打印出来,这是为什么呢?
因为,当一个进程退出的时候,会自动刷新自己的缓冲区(所有的FILE对象内部,包括stdin、stdout、stderr);fclose是C语言级的,调用它关闭FILE时,也会自动刷新。
那close(fd)后,为什么不会刷新呢?
此时尽管“表面上”是向显示器中打,应该是行刷新,那么我不自己刷新应该也可以显示出来呀? - - 此时不是行刷新,因为显示器文件早就关闭了,1中放的是普通文件,应执行写满刷新的策略。
🌿 那操作系统是什么时候将文件内核缓冲区的内容刷新到外设中的呢?我能不能控制呢?
可以通过系统调用fsync可以做到
一个简单的题目:
如果在调用函数时不加 \n,即使不重定向,也是上图所示的打印效果。
六:🔥 共勉
以上就是我对 <code>【Linux】IO深度解析:文件描述符与重定向的奥秘 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
上一篇: 【linux 多进程并发】0301 Linux创建后台服务进程,daemon进程,自己的进程可以被一号进程接管啦
下一篇: Docker-OSX 在 Ubuntu 环境下的安装与使用指南
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。