Linux的基础IO内容补充-FILE
目录
FILE当中的文件描述符
再谈缓冲区
1. 无缓冲(Unbuffered)
2. 行缓冲(Line Buffered)
3. 全缓冲(Fully Buffered)
FILE当中的文件描述符
- 因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的。
- 所以C库当中的FILE结构体内部必定封装了文件描述符fd。
- 对于 f 系列的函数底层其实都是调用了系统的接口。
首先,我们在/usr/include/stdio.h
头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE
结构体的一个别名。
typedef struct _IO_FILE FILE;
而我们在/usr/include/libio.h
头文件中可以找到struct _IO_FILE
结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno
的成员,这个成员实际上就是封装的文件描述符。
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
};
当然我们也可以通过代码验证一下,
代码展示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
FILE *fp = fopen("log.txt", "r");
if (fp == NULL)
{
perror("Error opening file");
}
else
{
int fd1 = fileno(fp); // 获取底层的文件描述符
printf("File descriptor: %d\n", fd1); // 打印文件描述符
}
fclose(fp);
return 0;
}
运行后,可以看到其fd1为3,也是符合我们刚才所说的预期结果。
当然仅仅知道fopen底层其实是调用了open是不够的。那么我们再来理解一下C语言当中的fopen函数究竟在做什么?
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
那么我们研究一下下面的一段代码:
我们先不运行,我们先按照我们以前的知识,对这段代码进行解析。
我们先定义了三个字符串,然后分别调用printf,fwrite,write这三个函数,对于printf知识简单的打印输出到到显示器,fwrite是向fd为1的文件内写入一个mgs1,对于write也是如此。
但是我们刚才也说了,fwrite其实底层是调用了系统接口write。这是一点。
但我们在学习Linux的时候就听说过一句话,Linux下一切皆为文件,那么对于显示器,他对于Linux来说也是一个文件,那么对于printf本质就是向显示器文件内写内容,那么对于C语言的printf其实底层也是调用了write函数。
我们还在C语言的文件操作中听说过缓冲区的概念,其实我们向文件中写内容,其实一开始是向文件缓冲区内写内容,然后在适当的时候再刷新缓冲区,使得缓冲区的内容刷新到文件中。
代码的最后我们再fork,根据以前我们对fork的理解,那么fork出来的子进程它其实是不执行任何代码的,但是对于fork出来的子进程,它也会有着与父进程相同的数据与代码,那么父子进程的缓冲区内容会被 复制。但是有一点
- 父进程和子进程的缓冲区是独立的:
- 父子进程各自拥有缓冲区的副本,缓冲区中的数据状态(是否刷新、是否有未写入数据)也被复制。
- 这意味着父子进程的缓冲区互不影响。
根据上面的分析,我们可以得出一下结论:
- printf,fwrite底层全部调用了write接口。
- 向文件中写内容,本质是向文件缓冲区内写内容,然后再适当的时候再刷新缓冲区,使得缓冲区的内容刷新到文件中。
- 父子进程的缓冲区内容会被 复制。但是父进程和子进程的缓冲区是独立的。
#include <stdio.h>
#include <string.h>
int main()
{
const char *mgs0 = "hello printf\n";
const char *mgs1 = "hello fwrite\n";
const char *mgs2 = "hello write\n";
printf("%s",mgs0);
fwrite(mgs1, strlen(mgs1), 1, stdout);
write(1, mgs2, strlen(mgs2));
fork();
return 0;
}
那么我们运行,看看效果:
我们可以看到printf、fwrite和write函数都成功将对应内容输出到了显示器上。
但是,当我们将程序的结果重定向到log.txt文件当中后,我们发现文件当中的内容与我们直接打印输出到显示器的内容是不一样的。
那为什么C库函数打印的内容重定向到文件后就变成了两份,而系统接口打印的内容还是原来的一份呢?
再谈缓冲区
首先解释为什么之前我们要对缓冲区再有一个跟新的认识。
对于缓冲区的刷新方式一般分为三种,分别是:
- 无缓冲。
- 行缓冲。(常见的对显示器进行刷新数据)
- 全缓冲。(常见的对磁盘文件写入数据)
这里简单介绍一下三种缓冲刷新方式
1. 无缓冲(Unbuffered)
- 特点:
- 数据不经过缓冲区,直接写入目标设备或文件。
- 每次输出操作都会立即执行。
- 通常用于对速度和性能要求不高,但对数据实时性要求高的场景(例如日志记录、错误信息输出)。
- 适用场景:
- 标准错误输出(
stderr
)通常是无缓冲模式。2. 行缓冲(Line Buffered)
- 特点:
- 缓冲区在遇到换行符(
\n
)时会自动刷新。- 缓冲区满或显式调用刷新函数(如
fflush()
)也会触发刷新。- 常用于交互式输出,例如向终端打印信息。
- 适用场景:
- 标准输出(
stdout
)连接到终端时通常是行缓冲模式。3. 全缓冲(Fully Buffered)
- 特点:
- 数据先存入缓冲区,当缓冲区满时才写入目标设备或文件。
- 缓冲区写入操作较少,因此效率更高。
- 如果程序结束或显式调用刷新函数(如
fflush()
),缓冲区中的数据也会被写入。- 适用场景:
- 对文件等非交互式设备进行写操作时通常采用全缓冲模式。
当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。那么这时候父进程的缓冲区内也没有任何内容,当fork后的子进程的缓冲区内也没有任何数据。
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fwrite函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite函数打印的数据就有两份。但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。
那么这时候就会有疑问了。这个缓冲区是谁提供的?
首先肯定的是这个缓冲区肯定不是系统内部提供的,如果是系统提供的,那么那么printf、fwrite和write函数打印的数据重定向到文件后都应该打印两次。但实际上write只是对应一次。
所以这个缓冲区其实就是C语言提供的缓冲区。
那么再联系我们开始说的 fwrite ,printf 函数底层其实是调用了write函数,那么再与刚才说的这个缓冲区是C语言提供的。此时我们就可以知道,实际上fwrite函数是先将数据放入C语言提供的缓冲区内,然后触发缓冲区刷新,最后再在适当的时候调用write。
这个缓冲区在哪里?
我们常说printf是将数据打印到stdout里面,而stdout就是一个FILE*
的指针,在FILE结构体当中还有一大部分成员是用于记录缓冲区相关的信息的。
//缓冲区相关
/* 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. */
也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。
我们向fd为1的写,那么我们所说的缓冲区就在stdout的FILE*指针内。
操作系统有缓冲区吗?
操作系统实际上也维护着自己的缓冲区。当用户程序中的缓冲区被刷新时,数据并不会直接写入磁盘或显示器,而是先被传递到操作系统的缓冲区。随后,操作系统会根据其自身的刷新机制,将这些数据写入磁盘或显示器。由于操作系统的刷新规则由系统管理,程序开发者通常无需过多关心其细节。
因为操作系统是进行软硬件资源管理的软件,根据下面的层状结构图,用户区的数据要刷新到具体外设必须经过操作系统。
那么对于上面的过程就可以用一个图进行简化