学习文件描述符fd和FILE结构体前,我们先了解一下系统I/O
(1)系统文件I/O
我们学习C语言的时候,通过fopen(),fclose(),fread(),fwrite()等 I/O函数来操作文件,同样的,我们也可以采用系统接口open、close、write、read等来进行文件访问。
往文件里写
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);
int fd = open("myfile",O_WRONLY|O_CREAT,0644);
if(fd < 0)
{
perror("open");
return 1;
}
int count = 5;
const char* msg = "hello world\n";
int len = strlen(msg);
while(count--)
{
write(fd,msg,len);
}
close(fd);
return 0;
}
读文件
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("myfile",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char buf[1024];
char *msg = "hello world\n";
int len = strlen(msg);
while(1)
{
ssize_t s = read(fd,buf,len);
if(s > 0)
{
printf("%s",buf);
}
else
break;
}
close(fd);
return 0;
}
通过上图我们可以很明显的看出系统调用接口和库函数的关系,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
(2)文件描述符fd
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,即file结构体,来表示已经打开的文件。进程执行open系统调用时必须让进程和文件关联起来。每个进程都有一个*file指针,指向一张表files_struct,这张表包含一个文件指针数组,每个元素都是一个指向打开文件的指针。所以,文件描述符本质上是文件指针数组的下标。只要拿着fd,就可以找到对应的文件。
从上面这张图可以看出文件描述符fd是一个从0开始的小整数。
那当我们关闭标准输入0时,重新打开一个文件,它的fd是多少呢?
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
close(0);
int fd = open("myfile",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n",fd);
close(fd);
return 0;
}
结果:
我们可以得出文件描述符的分配规则:在files_struct数组当中,找到没有被使用的最小一个下标,作为新的文件描述符。而当关闭标准输出1时,会发生重定向,他将本来应该显示在屏幕上的内容显示到了一个打开的文件中。因为底层访问文件时,找的还是fd:1,而此时fd:1下标所表示的内容已经变成了一个新打开文件的地址,而不再是显示器的地址,所以会发生重定向。
(3)FILE结构体
从上面介绍我们可以得出访问文件都是通过fd访问的,所以file结构体里面一定封装了fd。那file里面还封装了什么?
先来看段代码:
#include<stdio.h>
#include<string.h>
int main()
{
const char* msg1 = "hello fprintf\n";
const char* msg2 = "hello fwrite\n";
const char* msg3 = "hello writre\n";
printf("%s",msg1);
fwrite(msg2,strlen(msg2),1,stdout);
write(1,msg3,strlen(msg3));
fork();
return 0;
}
结果:
但是对进程实现输出重定向后,发现结果变成:
除了write其他都输出了两次,这是为什么?
其实,这就牵扯到了缓冲区的概念。缓冲数据有三种方式:无缓冲、行缓冲和全缓冲。当数据写入显示器时是行缓冲,遇到 “\n”就输出,所以打印了三条语句。而往文件里面写入时是全缓冲,fork后,父子数据写时拷贝,当父进程准备刷新的时候,子进程有了相同的一份数据,随机产生两份数据,等进程退出后统一刷新,这两份数据都写入了文件中。
从上述例子可以看出,fwrite,printf库函数自带缓冲区,而write系统调用没有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C库提供。
所以,C库当中的FILE结构体内部一定也封装了缓冲区。