文件IO与标准IO库

1 不带缓冲IO

所谓不带缓存指的是每个read和write都调用内核中的一个系统调用。这些不带缓存的I/O函数不是ANSI C的组成部分,但是是POSIX.1和XPG3的组成部分。(这unbuffered I/O,是与后面说明的标准I/O函数相对)。

2 文件描述符、opencreatlseek

对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。

按照惯例,UNIX shell使文件描述符0与进程的标准输入相结合,文件描述符1与标准输出相结合,文件描述符2与标准出错输出相结合。在POSIX.1应用程序中,幻数0、1、2应被代换成符号常数STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。这些常数都定义在头文件< unistd.h>中。

调用open函数可以打开或创建一个文件。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(const char *pathname, int oflag,.../* mode_t mode * / ) ;

返回:若成功为文件描述符,若出错为- 1

pathname是要打开或创建的文件的名字。oflag参数可用来说明此函数的多个选择项。用下列一个或多个常数进行或运算构成oflag参数(这些常数定义在<fcntl.h>头文件中):O_RDONLY 只读打开、O_WRONLY 只写打开、O_RDWR 读、写打开、O_APPEND 每次写时都加到文件的尾端、O_CREAT 若此文件不存在则创建它,使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位、O_EXCL 如果同时指定了OCREAT,而文件已经存在,则出错。这可测试一个文件是否存在,如果不存在则创建此文件成为一个原子操作、O_TRUNC 如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0、O_SYNC 使每次write都等到物理I/O操作完成。

也可用create函数创建一个新文件。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int creat(const char *pathname, mode_t mode) ;

返回:若成功为只写打开的文件描述符,若出错为- 1

注意,此函数等效于:open(pathname, O_WRONLY|OCREAT|O_TRUNC, mode) ;

每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。按系统默认,当打开一个文件时,除非指定OAPPEND选择项,否则该位移量被设置为0。

可以调用lseek显式地定位一个打开文件。

#include <sys/types.h>

#include <unistd.h>

off_t lseek(int filedes, off_t offset, int whence) ;

返回:若成功为新的文件位移,若出错为- 1

对参数offset 的解释与参数whence的值有关。

若是SEEK_SET,则将该文件的位移量设置为距文件开始处offset 个字节。

若是SEEK_CUR,则将该文件的位移量设置为其当前值加offset。

若是SEEK_END,则将该文件的位移量设置为文件长度加offset。

若lseek成功执行,则返回新的文件位移量,为此可以用下列方式确定一个打开文件的当前位移量:

off_t currpos;

currpos = lseek(fd, 0, SEEK_CUR);

3 文件共享、dupdup2

内核使用了三种数据结构,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

(1) 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,每个描述符占用一项。与每个文件描述符相关联的是:

(a) 文件描述符标志。(b) 指向一个文件表项的指针。

(2) 内核为所有打开文件维持一张文件表。每个文件表项包含:

(a) 文件状态标志(读、写、增写、同步、非阻塞等)。(b) 当前文件位移量。(c) 指向该文件v节点表项的指针。

(3) 每个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针信息。对于大多数文件,v节点还包含了该文件的i节点(索引节点)。在linux中,v节点就是inode。

下图显示了进程的三张表之间的关系。该进程有两个不同的打开文件——一个文件打开为标准输入(文件描述符0),另一个打开为标准输出(文件描述符为1)。

如果两个独立进程各自打开了同一文件,则有下图中所示的安排。我们假定第一个进程使该文件在文件描述符3上打开,而另一个进程则使此文件在文件描述符4上打开。打开此文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前位移量

通过以上可以看出,这里的进程表项(内含文件描述符表)是进程的属性,而文件表项其实就是文件的打开结构file结构,v节点表就是文件的inode。前面open中的文件状态标志以及文件的当前位移量都是打开file结构的属性,而打开的file结构在进程中则是由文件描述符fd对应。需要注意的是,一个fd只能对应一个file(即一个fd就代表了一个打开的file);而一个file却可对应多个fd(即一个file可以被多个fd同时打开共享)。然后,文件的长度和文件权限等则是inode结构本身的属性,若一个文件的偏移量超过了当前的文件长度则它也要保存到inode中的。同一文件的多个file结构都对应一个inode。

上面的这种结构也就是UNIX实现文件共享,实现多用户的本质所在。

下面进一步说明了这个观点。

在完成每个write后,在文件表项中的当前文件位移量即增加所写的字节数。如果这使当前文件位移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件位移量(也就是该文件加长了)。

如果用OAPPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件位移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。

若一个文件用lseek被定位到文件当前的尾端,则文件表项中的当前文件位移量被设置为i节点表项中的当前文件长度。lseek函数只修改文件表项中的当前文件位移量,没有进行任何I/O操作。

下面两个函数都可用来复制一个现存的文件描述符:

#include <unistd.h>

int dup(int filedes) ;

int dup2(int filedes, int filedes2) ;

两函数的返回:若成功为新的文件描述符,若出错为- 1

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如若filedes等于filedes2,则dup2返回filedes2,而不关闭它。这些函数返回的新文件描述符与参数filedes共享同一个文件表项。

可能有多个文件描述符项指向同一文件表项,如dup函数,这印证了上述所说。这一点在fork后也发生同样的情况,此时父、子进程对于每一个打开的文件描述符共享同一个文件表项。前者(dup)用于在一个进程内共享一个file结构,后者(fork)用于在进程间共享file结构。

注意,文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则适用于指向该给定文件表项的任何进程中的所有描述符。上述的一切对于多个进程读同一文件都能正确工作。每个进程都有它自己的文件表项,其中也有它自己的当前文件位移量。但是,当多个进程写同一文件时,则可能产生预期不到的结果。为了说明如何避免这种情况,需要理解原子操作的概念。

4 syncfsyncfcntl

传统的UNIX在内核中设有缓冲区告诉缓存或页面高速缓存。当将数据写入文件时,内核先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其它磁盘块时,再将该缓冲区排入输出队列,然后待其到达队首时,才进行实际的IO操作。这种方式成为延迟写。

UNIX提供了sync、fsync和fdatasync三个函数来刷回数据。

#include<unistd.h>

int fsync(int filedes);

int fdatasync(int filedes);

void sync(void);

sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。

fsync函数只对由文件描述符fd指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。

fdatasync类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。

fcntl函数可以改变已经打开文件的性质。

#include <sys/types.h>

#include <unistd.h>

#include <fcntl.h>

int fcntl(int filedes, int cmd,.../* int arg * / ) ;

返回:若成功则依赖于cmd(见下),若出错为- 1

fcntl函数有五种功能:

复制一个现存的描述符(cmd=F_DUPFD)。

获得/设置文件描述符标记(cmd = F_GETFD或F_SETFD)。

获得/设置文件状态标志(cmd = F_GETFL或F_SETFL)。

获得/设置异步I/Os=所有权(cmd = F_GETOWN或F_SETOWN)。

获得/设置记录锁(cmd = F_GETLK , F_SETLK或F_SETLKW)。

具体用法参见高级编程清单3-4。

5 流和缓冲

前面所讲的所有I/O函数都是针对文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后读的I/O操作。而对于标准I/O库,它们的操作则是围绕流(stream)进行的)。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相结合。当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了I/O库为管理该流所需要的所有信息:用于实际I/O的文件描述符,指向流缓存的指针,缓存的长度,当前在缓存中的字符数,出错标志等等。

对一个进程预定义了三个流,它们自动地可为进程使用:标准输入、标准输出和标准出错。前面曾用文件描述符STDIN_FILENO, STDOUT_FILENO和STDERR_FILENO分别表示它们。这三个标准I/O流通过预定义文件指针stdin , stdout和stderr加以引用。这三个文件指针同样定义在头文件< stdio.h >中。

标准I/O提供缓存的目的是尽可能减少使用read和write调用的数量。它也对每个I/O流自动地进行缓存管理,避免了应用程序需要考虑这一点所带来的麻烦。不幸的是,标准I/O库令人最感迷惑的也是它的缓存。

标准I/O提供了三种类型的缓存:

(1) 全缓存。在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。对于驻在磁盘上的文件通常是由标准I/O库实施全缓存的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓存。

(2) 行缓存。在这种情况下,当在输入和输出中遇到新行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O fputc函数),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓存。

(3) 不带缓存。标准I/O库不对字符进行缓存。如果用标准I/O函数写若干字符到不带缓存的流中,则相当于用write系统调用函数将这些字符写至相关联的打开文件上。标准出错流stderr通常是不带缓存的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新行字符。

6 打开流、读写流

6.1字符IO

下列三个函数可用于打开一个标准I / O流。

#include <stdio.h>

FILE *fopen(const char *pathname, const char *type) ;

FILE *freopen(const char *pathname, const char *type, FILE *fp) ;

FILE *fdopen(int filedes, const char *type) ;

三个函数的返回:若成功则为文件指针,若出错则为NULL

这三个函数的区别是:

(1) fopen打开路径名由pathname 指示的一个文件。

(2) freopen在一个特定的流上(由fp指示)打开一个指定的文件(其路径名由pathname 指示),如若该流已经打开,则先关闭该流。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准出错。

(3) fdopen取一个现存的文件描述符(我们可能从open , dup , dup2 ,fcntl或pipe函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数获得的描述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。

调用fclose关闭一个打开的流。

#include <stdio.h>

int fclose(FILE *fp) ;

返回:若成功则为0,若出错则为EOF

在该文件被关闭之前,刷新缓存中的输出数据。缓存中的输入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓存,则释放此缓存。当一个进程正常终止时(直接调用exit函数,或从ma n函数返回),则所有带未写缓存数据的标准I/O流都被刷新,所有打开的标准I/O流都被关闭。

一旦打开了流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作。

(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓存的,则标准I/O函数处理所有缓存。

(2) 每次一行的I O。使用fgets和fputs一次读或写一行。每行都以一个新行符终止。

(3) 直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构。

以下三个函数可用于一次读一个字符。

#include <stdio.h>

int getc(FILE *fp) ;

int fgetc(FILE *fp) ;

int getchar(void);

三个函数的返回:若成功则为下一个字符,若已处文件尾端或出错则为EOF

函数getchar等同于getc(stdin)。前两个函数的区别是getc可被实现为宏,而fgetc则不能实现为宏。

这三个函数以unsigned char 类型转换为int的方式返回下一个字符。说明为不带符号的理由是,如果最高位为1也不会使返回值为负。要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已发生错误或已到达文件尾端的指示值。在<stdio.h>中的常数EOF被要求是一个负值,其值经常是-1。这就意味着不能将这三个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常数EOF相比较。

注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferror或feof。

#include <stdio.h>

int ferror(FILE *fp) ;

int feof(FILE *fp) ;

两个函数返回:若条件为真则为非0(真),否则为0(假)

void clearerr(FILE *fp);

在大多数实现的FILE对象中,为每个流保持了两个标志:出错标志、文件结束标志。调用clearerr则清除这两个标志。

从一个流读之后,可以调用ungetc将字符再送回流中。

#include <stdio.h>

int ungetc(intc, FILE *f p) ;

返回:若成功则为C,若出错则为EOF

送回到流中的字符以后又可从流中读出,但读出字符的顺序与送回的顺序相反。回送的字符,不一定必须是上一次读到的字符。EOF不能回送。但是当已经到达文件尾端时,仍可以回送一字符。下次读将返回该字符,再次读则返回EOF。之所以能这样做的原因是一次成功的ungetc调用会清除该流的文件结束指示。

当正在读一个输入流,并进行某种形式的分字或分记号操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的字符送回,以便下一次调用getc时返回该字符。如果标准I/O库不提供回送能力,就需将该字符存放到一个我们自己的变量中,并设置一个标志以便判别在下一次需要一个字符时是调用getc,还是从我们自己的变量中取用。

对应于上面所述的每个输入函数都有一个输出函数。

#include <stdio.h>

int putc(intc , FILE *fp) ;

int fputc(int c, FILE *fp);

int putchar(intc) ;

三个函数返回:若成功则为C,若出错则为EOF

与输入函数一样,putchar(c) 等同于putc(c, stdout),putc 可被实现为宏,而fputc 则不能实现为宏。

6.2IO

下面两个函数提供每次输入一行的功能。

#include <stdio.h>

char *fgets(char *buf, int n,FILE *fp) ;

char *gets(char *buf) ;

两个函数返回:若成功则为buf,若已处文件尾端或出错则为NULL

这两个函数都指定了缓存地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。

对于fgets,必须指定缓存的长度n。此函数一直读到下一个新行符为止,但是不超过n-1个字符,读入的字符被送入缓存。该缓存以null字符结尾。如若该行,包括最后一个新行符的字符数超过n-1,则只返回一个不完整的行,而且缓存总是以null字符结尾。对fgets的下一次调用会继续读该行。

gets是一个不推荐使用的函数。问题是调用者在使用gets时不能指定缓存的长度。这样就可能造成缓存越界(如若该行长于缓存长度),写到缓存之后的存储空间中,从而产生不可预料的后果。这种缺陷曾被利用,造成1988年的因特网蠕虫事件。gets与fgets的另一个区别是, gets并不将新行符存入缓存中。

fputs和puts提供每次输出一行的功能。

#include <stdio.h>

int fputs(const char *str,FILE *fp) ;

int puts(const char *str) ;

两个函数返回:若成功则为非负值,若出错则为EOF

函数fputs将一个以null符终止的字符串写到指定的流,终止符null不写出。注意,这并不一定是每次输出一行,因为它并不要求在null符之前一定是新行符。通常,在null符之前是一个新行符,但并不要求总是如此。

puts将一个以null符终止的字符串写到标准输出,终止符不写出。但是, puts然后又将一个新行符写到标准输出。

6.3二进制IO

前面的函数以一次一个字符或一次一行的方式进行操作。如果为二进制I/O,那么我们更愿意一次读或写整个结构。为了使用getc或putc做到这一点,必须循环通过整个结构,一次读或写一个字节。因为fputs在遇到null字节时就停止,而在结构中可能含有null字节,所以不能使用每次一行函数实现这种要求。相类似,如果输入数据中包含有null字节或新行符,则fgets也不能正确工作。因此,提供了下列两个函数以执行二进制I/O操作。

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nobj, FILE *fp) ;

size_t fwrite(const void * ptr,size_t size, size_t nobj, FILE *fp) ;

两个函数的返回:读或写的对象数

这些函数有两个常见的用法:

(1) 读或写一个二进制数组。例如,将一个浮点数组的第2至第5个元素写至一个文件上,可以写作:

float data[10];

if (fwrite(&data[2], sizeof(float), 4, fp) != 4)

err_sys("fwrite error");

其中,指定size为每个数组元素的长度,nobj为欲写的元素数。

(2) 读或写一个结构。例如,可以写作:

struct {

short count;

long total;

char name[NAMESIZE];

} item;

if (fwrite(&item, sizeof(item), 1, fp) != 1)

err_sys("fwrite error");

其中,指定size为结构的长度,nobj为1(要写的对象数)。

将这两个例子结合起来就可读或写一个结构数组。为了做到这一点,size应当是该结构的sizeof ,nobj应是该数组中的元素数。

fread和fwrite返回读或写的对象数。对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况,应调用ferror或feof以判断究竟是那一种情况。对于写,如果返回值少于所要求的nobj,则出错。

正如前述,在UNIX中,标准I/O库最终都要调用前面说明的不带缓冲的I/O例程。每个I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno以获得其描述符。

#include <stdio.h>

int fileno(FILE *fp) ;

返回:与该流相关联的文件描述符

如果要调用dup或fcntl等函数,则需要此函数。具体用法参考高级编程清单5-3.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值