【Linux】基础IO
一、C语言中文件IO操作
1.C语言中的开关读写文件
在学习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(void* ptr, size_t size, size_t nmemb, FILE* stream);
- 函数参数
ptr
:写入文件的内容size
:往文件中写入的块的大小,单位为字节nmemb
:预期写入的块数stream
:预期写入文件的文件指针
- 函数返回值
- 成功:写入文件中的块数
- 常见用法
- 定义块的大小为1个字节,
nmemb
为向写入的字节数量,返回值为成功写入的字节数
- 定义块的大小为1个字节,
#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;
}
运行结果:
注意:fopen()
中的path
不是执行程序的所处的路径,而是进程运行时做出的路径。
举例:
1.4.fread()
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
- 函数参数
ptr
:将从文件读取的内容保存在ptr
所指向的空间中size
:定义读文件时块的大小,单位为字节nmemb
:期望从文件中读的块数stream
:预期读取文件的文件指针
- 函数返回值
- 成功从文件中读取的块的个数
- 常见用法
- 定义块的大小为1个字节,
nmemb
为向写入的字节数量,返回值为成功读取的字节数
- 定义块的大小为1个字节,
#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;
}
运行结果:
二、系统文件IO
其实除了C语言之外,很多语言都是自己的IO接口函数,但是下面我们要谈论的就是系统给我们提供的IO接口,也就是系统级别的IO接口。
1.系统级别的开关读写文件
1.1.open()
// 在打开的文件已经存在的时候
int open(const char* pathname, int flags);
// 在打开的文件不存在的时候
int open(const char* pathname, int flags, mode_t mode);
- 函数参数
pathname
:需要打开的文件flags
:打开文件的方式- 必选项
O_RDONLY
:只读方式O_WRONLY
:只写方式O_RDWR
:读写方式
- 可选项
O_TRUNC
:截断文件(清空文件内容)O_CREAT
:文件不存在则创建文件O_APPEND
:追加方式O_EXXL | O_CREAT
:如果文件存在,则打开文件失败
- 原理
- 可以使用按位或的方式进行组合:如打开并创建只写文件
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 fc, 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标准库中的函数,我们统称为库函数。
而open
、close
、read
、write
都是系统提供的接口,我们称之为系统调用接口。
如下图:
右下图可知,系统调用接口在
lib
库函数之下,所以库函数中其实求出对系统调用接口的二次封装。
因为库函数是系统函数的一层封装,因此库函数对文件操作的时候,必然会使用系统调用接口。每打开一个文件所获得的文件指针FILE
都有一个文件描述符fd
与之对应。
为什么不适用系统调用接口,而是只使用库函数的IO调用接口?
1.虽然库函数有函数调用的开销,但是系统调用比库函数调用还要慢,因为它需要把上下文环境切换成为内核模式。
2.系统调用与操作系统是相关的,所以系统调用接口没有跨平台的可移植性。
3.一般读写文件都是要操作大量的数据,而库函数调用要大大减少系统调用的次数。这是因为缓冲区的技术,内核缓冲区是全缓冲,只有当缓冲区写完之后或者结束之后,才会将缓冲区中的内容写入文件中。
3.文件描述符fd
在上面open
的接口中,我们提到了fd
,这个也是open
接口的返回值。而write
和read
接口也是通过fd
这个参数使得文件可以读写,可以说fd
是整个系统IO的灵魂,所以接下来,我们需要好好地理解一下fd
。
3.1.什么是文件描述符
在Linux下一切皆文件,而大量的文件需要被高效的组织和管理,因此就诞生了文件描述符fd
(file descriptor)。
文件描述符是内核为高效的管理已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的。
进程和文件之间的对应关系是如何建立的?
由图可知:文件描述符就是从0开始的正整数。但我们打开一个文件的时候,操作系统都需要创建一个数据结构来描述这个文件。所以struct file
结构体就应运而生了,它就是表示打开的一个文件对象。
当进程执行open
函数的时候,必须要让进程和文件关联起来。所以在每一个进程的PCB
中都是一个struct files_struct* files
指针,它指向一张表files_struct
,这个表中有一个指针数组fd_array[]
,其中指针数组的每一个元素都是一个指向struct file
结构的struct file*
指针,而这个文件指针就指向打开的文件。
注意:向文件写入数据后,数据其实先写入对应文件的缓冲区当中,只有当将缓冲区中的内容刷新到磁盘当中时才算真正地写入到文件当中。
小总结:
- 所以本质上文件描述符就是
struct 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
数组的过程就叫做默认打开了标准输入流,标准输出流和标准错误流。
补充:磁盘文件和内存文件的区别?
上面说的都是在操作进程打开的文件,正是因为操作系统中有大量的进程打开了大量的文件,所以需要使用struct file
和struct files_struct
这样的结构体去管理这些文件。而这些文件都是在内存中加载的文件,所以我们称之为**「内存文件」**。
如果一个文件储存在磁盘当中,我们就称之为「磁盘文件」。这两种文件的关系就是当一个磁盘文件被加载到内存当中的话,就成为了内存文件。
磁盘文件由两部分构成:「文件内容」和「文件属性」。文件内容就是文件中的数据内容,而文件属性(元信息)就是一个文件的基本信息。这就像是去超市买一盒牛奶,其中的牛奶就是文件内容,而牛奶的包装盒上的牛奶成分分析表就是文件属性。(在后面的文件系统中,还会详细地介绍磁盘文件)
文件加载到内存时,一般先加载文件的属性信息,然后将文件内容放入缓冲区中,延后式的慢慢加载内存。
3.2.如何创建文件描述符
进程通过文件描述符最常见的方式就是通过系统调用接口open
或者是从父进程继承过来的。
虽然文件描述符对于每一个进程的PID
都是唯一的,但是每一个进程都是一个进程描述表struct files_struct
,用于管理进程描述符,当使用fork
创建子进程的时候,子进程会获得父进程进程描述表的一个副本,所以子进程可以拿到父进程的进程描述符,因此就可以打开父进程所有的文件。
3.3. 文件描述符的分配规则
我们先上结论, 文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
如果再打开一个新的文件的话,就分配一个最小的没有使用的文件描述符fd。因为默认打开了0, 1, 2,所以新的文件描述符就应该从3开始的。
**思考:因为文件描述符也是可以使用close
关闭掉的,所以如果我们先将stdin
对应的0关闭掉的话,然后在此时打开一个新的文件的话,则这个文件对应struct file
的文件描述符就应该是0,此时这个文件就变成了标准输入。**如果我们向标准输入中输入一些内容的话,其实就输入到了这个文件当中。这个原理就和重定向的原理很像,只不过重定向要比这个原来还要复杂一点,但是这个可以帮助我们学习重定向。
#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;
}
运行结果:
3.3. 文件描述符与打开文件之间的关系
每一个文件描述符都对应着一个打开的文件,同时,不同的文件描述符也可以指向同一个文件。
同一个文件可以被同一个进程打开多次,也可以被不同的进程打开。
系统为每一个级进程都创建了一个文件描述表。内核中维护了三种文件描述表。
1.进程级别的文件描述表
进程级别的文件描述符表的每一条目都记录了单个文件描述符的相关信息。
struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};
2.系统级别的文件描述表
内核对所有打开的文件有一个系统级别的文件描述符表。有时也称为打开文件描述符表,并将表格中各条目称为打开文件句柄。一个打开文件句柄存储了这个打开文件的全部相关信息。
struct file
{
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /*打开文件时所指定的标志*/
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及
预读的页面数*/
int f_owner; /* 通过信号进行异步I/O数据的传送*/
unsigned int f_uid, f_gid; /*用户的UID和GID*/
int f_error; /*网络写操作的错误码*/
unsigned long f_version; /*版本号*/
void *private_data; /* tty驱动程序所需 */
};
3.文件系统的inode
表
作用:保护了文件系统的相关信息。
不同级别的文件表述表的关系:
3.4.文件描述符fd与文件指针FILE的区别
在linux系统中打开文件就会获得文件描述符,它是一个数组的下标。每个进程控制块(PCB)中保存着一份文件描述符表,文件描述符就是文件描述符表的索引,每个表项都有一个指向打开文件的文件指针,这个文件指针指向进程用户区中的一个被称为FILE
的数据结构。FILE
结构包含一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引。
4.重定向
理解了文件描述符后,就可以讲一讲重定向的原理了。
4.1.重定向原理
**简单来说:重定向的原理就是修改了文件描述符和打开文件的对应关系。**接下来的三个例子会进一步的帮助你理解这句话。
4.1.1.输入重定向原理
原本文件描述符指向标准输入流文件,而现在我们先将标准输入流文件关闭,然后再打开一个文件,这时文件描述符0就分给了新打开的文件了。
举例:
scanf默认是从标准输入中获取内容,如果打开的文件的文件符为0的话,那么就从打开的文件中获取内容。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
int main()
{
close(0);
int fd = open("file.txt", O_RDONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
char str[1024];
while (scanf("%s", str) != EOF)
printf("%s", str);
close(fd);
return 0;
}
运行结果:
4.1.2.输出重定向
原本文件描述符指向标准输出流文件,而现在我们先将标准输出流文件关闭,然后再打开一个文件,这时文件描述符1就分给了新打开的文件了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bIkuVqc4-1635688996871)(D:\github\gitee\linux-study\【Linux】基础IO.assets\1635427605224.png)]
举例:
printf
标准输出默认是往显示器上打印内容,如果打开的文件的文件符为1的话,那么就往打开的文件中打印内容。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
int main()
{
close(1);
int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
printf("hello world");
fflush(stdout); // 将缓冲区中的内容提前刷新出来
close(fd);
return 0;
}
运行结果:
4.1.3.追加重定向
追加重定向和输出重定向几乎一模一样,只不过open
的方式需要多添加一个选项O_APPEND
而已。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
int main()
{
close(1);
int fd = open("file.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
printf("hello world\n");
fflush(stdout); // 将缓冲区中的内容提前刷新出来
close(fd);
return 0;
}
运行结果:
标准输出流和标准错误流有什么区别?
标准输入和标准输出对应的设备都是显示屏,但是它们的区别是它们的文件描述符是不一样的,一个对应1,一个对应2。
接下来我们用一段代码来解释一下:
#include <stdio.h>
int main()
{
printf("hello printf\n");
perror("hello perror");
return 0;
}
运行结果:
我们可以发现当我们起一个进程的时候,进程会通过标准输入和标准错误的文件描述符找到对应的文件,然后向文件中输入内容。
观察下面的操作,思考和上面的不同之处:
但是如果我们将test.cpp
中的内容利用>
重定向到file.txt
文件中的话,会发现file.txt
中的内容只有hellp printf
,也就是将stdout
中的内容重定向到file.txt
文件当中,但是stderr
直接运行打印到了显示器上并没有重定向到file.txt
当中。
总结:当我们使用>
重定向的时候,实际上修改的是文件描述符1
,并不会对其他的文件描述符造成影响。而stdout
和stderr
最大的差别就在于它们的文件描述符不同。
4.2.dup2
在Linux系统中提供了系统接口dup2
,这样函数是专门完成文件重定向的。
int dup2(int oldfd, int newfd);
- 函数参数
oldfd
为需要重定向的文件newfd
为被重定向文件替换的文件
- 函数返回值
dup2
如果调用成功,返回newfd
,否则返回-1
。
原理:dup2
会将fd_array[oldfd]
的内容拷贝到fd_array[newfd]
当中(如果有必要的话,我们需要先将关闭文件描述符为newfd
的文件。)这样当我们在通过fd_array[newfd]
使用文件的时候,其实我们使用的是fd_array[oldfd]
文件。
注意事项:
1.如果oldfd
不是有效的文件描述符,则调用dup2
接口失败,并且此时的文件描述符fd_array[newfd]
对应的文件没有关闭。
2.如果oldfd
是有效的文件描述符,但是oldfd == newfd
,则该接口不会发生任何的操作,而是直接返回oldfd
文件描述符。
举例:
我们可以使用dup2
将1
重定向为fd
,这样就让打印在显示器上的内容,打印在fd
对应的文件中。
#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;
}
运行结果:
使用printf
打印的内容,打印到了file.txt
文件当中。
4.3.添加重定向的简易的shell
例如ls -a -l > file.txt
。
1.我们可以先判断是否命令是否存在重定向。并判断重定向的类型>
,>>
,<
。
2.将重定向的位置设置为‘\0’
,这样就将命令分成了两块,左边是输入的内容,右边是重定向的文件。
3.将左右的命令分解放在myargv
数组中,方便后续的进程创建fork
和进程替换execvp
执行命令。
4.去除重定向符号后的空格,使得start
为重定向文件的首地址,即*start
为文件名。
5.重定向dup2(fd, )
,打开右边需要重定向的文件。
6.进程替换执行左边的命令。
7.进程等待,父进程回收使用完的子进程。
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define LEN 1024
#define MAX 32
int main()
{
char cmd[LEN];
char* myargv[MAX];
while (1) {
printf("[zhy@my_machine dir]$ ");
// 获取命令行参数
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1]= '\0';
// 实现重定向
int type = 0; // 0 >, 1 >>, 2 <
char * start = cmd;
while (*start != '\0') {
if (*start == '>') { // 输出重定向
type = 0;
*start = '\0';
start ++;
if (*start == '>') { // 追加重定向
type = 1;
start ++;
}
break;
}
if (*start == '<') { // 输入重定向
type = 2;
*start = '\0';
start ++;
break;
}
start ++;
}
// 判断是否有重定向并将重定向的文件准备好
if (*start != '\0') {
while (isspace(*start)) start ++;
} else {
start = NULL;
}
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " "))
i ++;
pid_t pid = fork();
if (pid == 0) {
//child process
if (start != NULL) {
if (type == 0) { // 输出重定向
// start就表示文件
int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
close(1);
dup2(fd, 1);
} else if (type == 1) { // 追加重定向
int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) {
perror("open");
exit(2);
}
close(1);
dup2(fd, 1);
} else if (type == 2) { // 输入重定向
int fd = open(start, O_RDONLY);
close(0);
dup2(fd, 0);
}
}
execvp(myargv[0], myargv);
exit(1);
} else {
// father process
int status = 0;
// 父进程阻塞式等待子进程
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
}
return 0;
}
5.FILE
5.1.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究竟在底层做什么?
1.给调用的用户申请struct FILE
结构体变量,并返回地址(FILE*
)。
2.在底层通过open
打开文件,并返回fd
,把fd
填充进FILE
变量中的_fileno
。
综上:在C语言中,fread(), fwrite(), fputs(), fgets()
,都是通过FILE*
指针找到FILE
结构体,然后再在FILE
结构体中找到_fileno
从而知道fd
,最后通过fd
操作内存中的文件。
5.2.文件缓冲区
我们通过一个例子来引出文件的缓冲区:
#include <stdio.h>
#include <unistd.h>
int main()
{
// C库函数
printf("hello printf\n");
// 系统接口
write(1, "hello write\n", 12);
fork();
return 0;
}
运行1:直接运行,打印出两段文字。
当程序的结构重定向到file.txt
中的时候。
运行2:我们发现C库函数打印的东西在file.txt
中出现了两份。
接下来我们要来学习三种缓冲区:
1.无缓冲
2.行缓冲(常用于对显示器的刷新)
以行为单位,每满一行就将内容刷新到显示器上,可以使用fflush()
或者\n
等,将缓冲区中的内容提前刷洗出来。
3.全缓冲(常用于对文件IO的刷新)
以全部的文字为单位,等到所有的内容都写入完成才将缓冲区刷新出来。
解释上述的程序:
当我们直接执行可程序的时候,将数据打印到显示器时所采用的就是行缓冲,因为代码后面都有\n
,所以当我们执行完对应代码后就立即将缓冲区中的数据刷新到了显示器上。
而当我们将运行结果重定向到file.txt
文件时,数据的刷新策略就变为了「全缓冲」,当printf
之后hello printf
就被放入的缓冲区中,当fork()
出子进程后,由于进程的独立性,所以在刷新缓冲区的时候,会发生进程间的「写时拷贝」。所以在刷新缓冲区的时候,会有两份hello printf
。但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。
缓冲区是谁提供的?
这个缓冲区是C语言自带的。
这个缓冲区在哪里?
FILE
结构体中记录了缓冲区的相关信息。也就是说FILE
不仅保存了文件描述符fd
,而且维护了缓冲区的信息。
操作系统有缓冲区吗?
操作系统其实也是有缓冲区的,当我们在刷新缓冲区的时候,并不是直接将用户缓冲区中的数据直接刷新到磁盘或者显示器上,而是先将数据刷新到操作系统的缓冲区中,然后再从操作系统的缓冲区将数据刷新到磁盘或者显示器上。