内存中的数据都是暂时的,当程序结束时,它们都将丢失。为了永久性的保存大量的数据,C语言提供了对文件的操作。
1、文件和流
C将每个文件简单地作为顺序字节流(如下图)。每个文件用文件结束符结束,或者在特定字节数的地方结束,这个特定的字节数可以存储在系统维护的管理数据结构中。当打开文件时,就建立了和文件的关系。在开始执行程序的时候,将自动打开3个文件和相关的流:标准输入流、标准输出流和标准错误。流提供了文件和程序的通信通道。例如,标准输入流使得程序可以从键盘读取数据,而标准输出流使得程序可以在屏幕上输出数据。打开一个文件将返回指向FILE结构(在stdio.h中定义)的指针,它包含用于处理文件的信息,也就是说,这个结构包含文件描述符。文件描述符是操作系统数组(打开文件列表的索引)。每个数组元素包含一个文件控制块(FCB, File Control Block),操作系统用它来管理特定的文件。标准输入、标准输出和标准错误是用文件指针stdin、stdout和stderr来处理的。
2、C语言文件操作的底层实现简介
2.1 FILE结构体
C语言的stdio.h头文件中,定义了用于文件操作的结构体FILE。这样,我们通过fopen返回一个文件指针(指向FILE结构体的指针)来进行文件操作。可以在stdio.h(位于visual studio安装目录下的include文件夹下)头文件中查看FILE结构体的定义,如下:
- struct _iobuf {
- char *_ptr;
- int _cnt;
- char *_base;
- int _flag;
- int _file;
- int _charbuf;
- int _bufsiz;
- char *_tmpfname;
- };
- typedef struct _iobuf FILE;
2.2 C语言文件管理的实现
C程序用不同的FILE结构管理每个文件。程序员可以使用文件,但是不需要知道FILE结构的细节。实际上,FILE结构是间接地操作系统的文件控制块 (FCB)来实现对文件的操作的,如下图:
上面图中的_file实际上是一个描述符,作为进入打开文件表索引的整数。
2.3 操作系统文件管理简介
从2.2中的图可以看出,C语言通过FILE结构可以间接操作文件控制块(FCB)。为了加深对这些的理解,这里科普下操作系统对打开文件的管理。
文件是存放在物理磁盘上的,包括文件控制块(FCB)和数据块。文件控制块通常包括文件权限、日期(创建、读取、修改)、拥有者、文件大小、数据块信息。数据块用来存储实际的内容。对于打开的文件,操作系统是这样管理的:系统维护了两张表,一张是系统级打开文件表【指向i节点】,一张是进程级打开文件表(每个进程有一个)【指向FILE结构体】。系统级打开文件表复制了文件控制块的信息等;进程级打开文件表保存了指向系统级文件表的指针及其他信息。 系统级文件表每一项都保存一个计数器,即该文件打开的次数。我们初次打开一个文件时,系统首先查看该文件是否已在系统级文件表中,如果不在,则创建该项信息,否则,计数器加1。当我们关闭一个文件时,相应的计数也会减1,当减到0时,系统将系统级文件表中的项删除。 进程打开一个文件时,会在进程级文件表中添加一项。每项的信息包括当前文件偏移量(读写文件的位置)、存取权限、和一个指向系统级文件表中对应文件项的指针。系统级文件表中的每一项通过文件描述符(一个非负整数)来标识。
联系2.2和2.3上面的内容,可以发现,应该是这样的:FILE结构体中的_file成员应该是指向进程级打开文件表,然后,通过进程级打开文件表可以找到系统级打开文件表,进而可以通过FCB操作物理磁盘上面的文件。
2.4 文件操作的例子
- #include <stdio.h>
- void main() {
- FILE * fp1;
- FILE * fp2;
- fp1 = fopen("filetest.cpp","r");
- fp2 = fopen("filetest.cpp","r");
- char buffer[256];
- fscanf(fp1,"%s",buffer);
- printf("%s\n",buffer);
- fscanf(fp2,"%s",buffer);
- printf("%s\n",buffer);
- printf("fp1(filetest.cpp):%d\n",fp1->_file);
- printf("fp2(filetest.cpp):%d\n",fp2->_file);
- printf("stdin:%d\n",stdin->_file);
- printf("stdout:%d\n",stdout->_file);
- printf("stderr:%d\n",stderr->_file);
- }
filetest.cpp中的内容如下:
- #include<stdio.h>
- int main()
- {
- printf("Hello World!\n");
- return 0;
- }
运行结果如下:
通过这个程序可以看出,应该是每打开一次文件,哪怕多次打开的都是同一个文件,进程级打开文件表中应该都会添加一个记录。如果是打开的是同一个文件,这多条记录对应着同一个物理磁盘文件。由于每一次打开文件所进行的操作都是通过进程级打开文件表中不同的记录来实现的,这样,相当于每次打开文件的操作是相对独立的,这就是上面的程序的运行结果中,两次读取文件的结果是一样的(而不是第二次读取从第一次结束的位置进行)。
另外,还可以看出,程序运行的时候,默认三个流是打开的stdin,stdout和stderr,它们的_file描述符分别是0、1和2。也可以看出,该程序打开的文件描述符依次从3开始递增。
3、顺序访问文件
3.1 顺序写入文件
先看一个例子:
- #include <stdio.h>
- int main()
- {
- int account;//账号
- char name[30];//账号名
- double balance;//余额
- FILE *cfPtr;
- if ((cfPtr=fopen("clients.dat","w"))==NULL)
- {
- printf("File could not be opened.\n");
- }
- else
- {
- printf("Enter the account, name and the balance:\n");
- printf("Enter EOF to end input.\n");
- printf("? ");
- scanf("%d%s%lf",&account,name,&balance);
- while(!feof(stdin))
- {
- fprintf(cfPtr,"%d %s %.2f\n",account,name,balance);
- printf("? ");
- scanf("%d%s%lf",&account,name,&balance);
- }
- fclose(cfPtr);
- }
- return 0;
- }
运行结果:
从上面的例子中可以看出,写入文件大致需两步:定义文件指针和打开文件。函数fopen有两个参数:文件名和文件打开模式。文件打开模式‘w’说明文件时用于写入的。如果以写入模式打开的文件不存在,则fopen将创建该文件。如果打开现有的文件来写入,则将抛弃文件原有的内容而没有任何警告。在程序中,if语句用于确定文件指针cfPtr是否是NULL(没有成功打开文件时fopen的返回值)。如果是NULL,则将输出错误消息,然后程序终止。否则,处理输入并写入到文件中。
foef(stdin)用来确定用户是否从标准输入输入了文件结束符。文件结束符通知程序没有其他数据可以处理了。foef的参数是指向测试是否为文件结束符的FILE指针。一旦输入了文件结束符,函数将返回一个非零值;否则,函数返回0。当没有输入文件结束符时,程序继续执行while循环。
fprintf(cfPtr,"%d %s %.2f\n",account,name,balance);向文件clients.dat中写入数据。稍后通过用于读取文件的程序,就可以提取数据。函数fprintf和printf等价,只是fprintf还需要一个指向文件的指针,所有数据都写入到这个文件中。
在用户输入文件结束之后,程序用fclose关闭clients.dat文件,并结束运行。函数fclose也接收文件指针作为参数。如果没有明确地调用函数fclose,则操作系统通常在程序执行结束的稍后关闭文件。这是操作系统“内务管理”的一个示例,但是,这样可能会带来一些难以预料的问题,所以一定要注意在使用结束之后关闭文件。
3.2 文件打开模式
模式 | 说明 |
r | 打开文件,进行读取。 |
w | 创建文件,以进行写入。如果文件已经存在,则删除当前内容。 |
a | 追加,打开或创建文件以在文件尾部写入。 |
r+ | 打开文件以进行更新(读取和写入)。 |
w+ | 创建文件以进行更新。如果文件已经存在,则删除当前内容。 |
a+ | 追加,打开或者创建文件以进行更新,在文件尾部写入。 |
3.3 顺序读取文件
下面的例子读取的是上一个例子中写入数据生成的文件。
- #include <stdio.h>
- int main()
- {
- int account;//账号
- char name[30];//账号名
- double balance;//余额
- FILE *cfPtr;
- if ((cfPtr=fopen("clients.dat","r"))==NULL)
- {
- printf("File could not be opened.\n");
- }
- else
- {
- printf("%-10s%-13s%s\n","Account","Name","Balance");
- fscanf(cfPtr,"%d%s%lf",&account,name,&balance);
- while(!feof(cfPtr))
- {
- printf("%-10d%-13s%lf\n",account,name,balance);
- fscanf(cfPtr,"%d%s%lf",&account,&name,&balance);
- }
- fclose(cfPtr);
- }
- return 0;
- }
运行结果:
上面的例子中,只需将第一个例子中的文件打开模式从w变为r,就可以打开文件读取数据。
同样地,fscanf(cfPtr,"%d%s%lf",&account,name,&balance);函数从文件中读取一条记录。函数fscanf和函数scanf等价看,只是fscanf接收将从中读取数据的文件指针作为参数。在第一次执行前面的语句时,account的值为100,name的值是Jones,而balance等于24.98。每次执行第二条fscanf语句时,将从文件中读取另一条记录,而account,name和balance将有新值。当到达文件结束位置时,关闭文件,而程序终止。
要从文件中顺序检索数据,程序通常从文件的开始来读取,而且连续读取所有数据,直至找到期望的数据。在程序执行过程中,有可能会多次处理文件中的数据(重新从文件的开头处理数据)。这时候就要用到函数rewind(cfPtr);,它可以使程序的文件位置指针(表示文件中将要读取或者写入的下一个字节的位置)重新设置到文件的开头(也就是偏移量为0的字节)。注意,文件位置指针并不是指针,它是指定文件中将进行下一次读取或者写入的位置的整数值,有时候也称其为文件偏移量,它是FILE结构的成员。
4、随机访问文件
文件中用格式化输入函数fprintf所创建的记录的长度并不是完全一致的。然而,在随机访问文件中,单个记录的长度通常是固定的,而且可以直接访问(这样速度更快)而无需通过其他记录来查找。这使得随机文件访问适合飞机订票系统,银行系统,销售点系统和其他需要快速访问特定数据的事务处理系统。我们可以有很多方法来实现随机访问文件,但是这里我们将把讨论的范围限制在使用固定长度记录的简单方法上。
函数fwrite把从内存中特定位置开始的指定数量的字节写入到文件位置指针指定的文件位置,函数fread从文件位置指针指定的文件位置处把指定数量的字节复制到指定的内存位置。fwrite和fread可以从磁盘上读取数据数组,以及向磁盘上写入数据数组。fread和fwrite的第三个参数是从磁盘中读取或者写入到磁盘上的数组元素的个数。
文件处理程序很少向文件中写入字段。通常情况下,它们一次写入一个struct。
4.1 创建随机访问的文件
- #include<stdio.h>
- struct clientData
- {
- int acctNum;
- char lastName[15];
- char firstName[10];
- double balance;
- };
- int main()
- {
- int i;
- struct clientData blankClient={0,"","",0.0};
- FILE *cfPtr;
- if ((cfPtr = fopen("credit.dat","wb"))== NULL)
- {
- printf("File could not be opened.\n");
- }
- else
- {
- for (i=1;i<=100;i++)
- {
- fwrite(&blankClient,sizeof(struct clientData),1,cfPtr);
- }
- fclose(cfPtr);
- }
- return 0;
- }
fwrite(&blankClient,sizeof(struct clientData),1,cfPtr);用于向文件中写入一个数据块,其会在cfPtr指向的文件中写入大小为sizeof(struct clientData)的结构blankClient。当然,也可以写入对象数组的多个元素,只需把数组名传给第一个参数,把要写入的元素个数写入第三个参数即可。
4.2 随机向随机访问文件中写入数据
- #include<stdio.h>
- struct clientData
- {
- int acctNum;
- char lastName[15];
- char firstName[10];
- double balance;
- };
- int main()
- {
- int i;
- struct clientData client={0,"","",0.0};
- FILE *cfPtr;
- if ((cfPtr = fopen("credit.dat","rb+"))== NULL)
- {
- printf("File could not be opened.\n");
- }
- else
- {
- printf("Enter account number(1 to 100, 0 to end input\):\n");
- scanf("%d",&client.acctNum);
- while (client.acctNum!=0)
- {
- printf("Enter lastname, firstname, balance\n");
- fscanf(stdin,"%s%s%lf",client.lastName,client.firstName,&client.balance);
- //在文件中定位用户指定的记录
- fseek(cfPtr,(client.acctNum-1)*sizeof(struct clientData),SEEK_SET);
- //将用户指定的信息写入文件
- fwrite(&client,sizeof(struct clientData),1,cfPtr);
- //输入下一个账号
- printf("Enter account number:\n");
- scanf("%d",&client.acctNum);
- }
- fclose(cfPtr);
- }
- return 0;
- }
运行结果:
fseek(cfPtr,(client.acctNum-1)*sizeof(struct clientData),SEEK_SET);将cfPtr所引用文件的位置指针移动到由(client.acctNum-1)*sizeof(struct clientData)计算所得到的字节位置处,这个表达式的值称为偏移量或者位移。负号常量SEEK_SET说明,文件位置指针指向的位置是相对于文件开头的偏移量。
ANSI标准制定了fseek的函数原型为int fseek(FILE *stream, long int offset, int whence);其中offset是stream指向的文件中从位置whence开始的字节数。参数whence可以有三个值:SEEK_SET, SEEKCUR或者SEEK_END,分别对应文件的开头当前位置和结尾。 4.2 从随机访问文件中读取数据
- #include<stdio.h>
- struct clientData
- {
- int acctNum;
- char lastName[15];
- char firstName[10];
- double balance;
- };
- int main()
- {
- struct clientData client={0,"","",0.0};
- FILE *cfPtr;
- if ((cfPtr = fopen("credit.dat","rb"))== NULL)
- {
- printf("File could not be opened.\n");
- }
- else
- {
- printf("%-6s%-16s%-11s%10s\n","Acct","Last name","First name","Balance");
- while(!feof(cfPtr))
- {
- fread(&client,sizeof(struct clientData),1,cfPtr);
- if (client.acctNum!=0)
- {
- printf("%-6d%-16s%-11s%10.2f\n",client.acctNum,client.lastName,client.firstName,client.balance);
- }
- }
- fclose(cfPtr);
- }
- return 0;
- }
运行结果:
好了,终于完了。照样和原来一样,类似于笔记的东西。whatever,晚上开会,求轻虐。
参考:
http://blog.csdn.net/xia7139/article/details/17142619
(1) 《C语言程序设计经典教程》
(2) http://hi.baidu.com/sangwf/item/4023100cf126af3af2eafc26
==分割线===============================================================================================
文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
附:文件系统
VFS
Linux支持各种各样的文件系统格式,如ext2、ext3、reiserfs、FAT、NTFS、iso9660等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以mount到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用ls命令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢?Linux内核在各种不同的文件系统格式之上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来用起来都一样,这个抽象层称为虚拟文件系统(VFS,Virtual Filesystem)。上一节我们介绍了一种典型的文件
系统在磁盘上的存储布局,这一节我们介绍运行时文件系统在内核中的表示。
内核数据结构
Linux内核的VFS子系统可以图示如下:
在第 28 章 文件与I/O中讲过,每个进程在PCB(Process Control Block)中都保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
在file结构体中维护File Status Flag(file结构体的成员f_flags)和当前读写位置(file结构体的成员f_pos)。在上图中,进程1和进程2都打开同一文件,但是对应不同的file结构体,因此可以有不同的File Status Flag和读写位置。file结构体中比较重要的成员还有f_count,表示引用计数(Reference Count),后面我们会讲到,dup、fork等系统调用会导致多个文件描述符指向同一个file结构体,例如有fd1和fd2都引用同一个file结构体,那么它的引用计数就是2,当close(fd1)时并不会释放file结构体,而只是把引用计数减到1,如果再close(fd2),引用计数就会减到0同时释放file结构体,这才真的关闭了文件。
每个file结构体都指向一个file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中read一个文件描述符,read通过系统调用进入内核,然后找到这个文件描述符所指向的file结构体,找到file结构体所指向的file_operations结构体,调用它的read成员所指向的内核函数以完成用户请求。在用户程序中调用lseek、read、write、ioctl、open等函数,最终都由内核调用file_operations的各成员所指向的内核函数完成用户请求。file_operations结构体中的release成员用于完成用户程序的close请求,之所以叫release而不叫close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到0才关闭文件。对于同一个文件系统上打开的常规文件来说,read、write等文件操作的步骤和方法应该是一样的,调用的函数应该是相同的,所以图中的三个打开文件的file结构体指向同一个file_operations结构体。如果打开一个字符设备文件,那么它的read、write操作肯定和常规文件不一样,不是读写磁盘的数据块而是读写硬件设备,所以file结构体应该指向不同的file_operations结构体,其中的各种文件操作函数由该设备的驱动程序实现。
每个file结构体都有一个指向dentry结构体的指针,“dentry”是directory entry(目录项)的缩写。我们传给open、stat等函数的参数的是一个路径,例如/home/akaedu/a,需要根据路径找到文件的inode。为了减少读盘次数,内核缓存了目录的树状结构,称为dentry cache,其中每个节点是一个dentry结构体,只要沿着路径各部分的dentry搜索即可,从根目录/找到home目录,然后找到akaedu目录,然后找到文件a。dentry cache只保存最近访问过的目录项,如果要找的目录项在cache中没有,就要从磁盘读到内存中。
每个dentry结构体都有一个指针指向inode结构体。inode结构体保存着从磁盘inode读上来的信息。在上图的例子中,有两个dentry,分别表示/home/akaedu/a和/home/akaedu/b,它们都指向同一个inode,说明这两个文件互为硬链接。inode结构体中保存着从磁盘分区的inode读上来信息,例如所有者、文件大小、文件类型和权限位等。每个inode结构体都有一个指向inode_operations结构体的指针,后者也是一组函数指针指向一些完成文件目录操作的内核函数。和file_operations不同,inode_operations所指向的不是针对某一个文件进行操作的函数,而是影响文件和目录布局的函数,例如添加删除文件和目录、跟踪符号链接等等,属于同一文件系统的各inode结构体可以指向同一个inode_operations结构体。
inode结构体有一个指向super_block结构体的指针。super_block结构体保存着从磁盘分区的超级块读上来的信息,例如文件系统类型、块大小等。super_block结构体的s_root成员是一个指向dentry的指针,表示这个文件系统的根目录被mount到哪里,在上图的例子中这个分区被mount到/home目录下。
file、dentry、inode、super_block这几个结构体组成了VFS的核心概念。对于ext2文件系统来说,在磁盘存储布局上也有inode和超级块的概念,所以很容易和VFS中的概念建立对应关系。而另外一些文件系统格式来自非UNIX系统(例如Windows的FAT32、NTFS),可能没有inode或超级块这样的概念,但为了能mount到Linux系统,也只好在驱动程序中硬凑一下,在Linux下看FAT32和NTFS分区会发现权限位是错的,所有文件都是rwxrwxrwx,因为它们本来就没有inode和权限位的概念,这是硬凑出来的。
参考: