UNIX中所有的I/O操作都是通过读文件或写文件来完成的,UNIX把所有的外围设备,包括键盘和显示器都看成是文件系统中的文件。
内核结构
UNIX系统支持在不同进程间共享打开文件。
内核使用3中数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
每个进程在其进程表中都有一个记录项,该记录项指向一张属于该进程的打开文件描述符表。该文件描述符表的每一行记录都由两个字段组成:
文件描述符
只想文件表项的指针
内核为所有打开文件维护一张文件表。文件表中的每个文件表项包含:
文件状态标志
当前文件偏移量
指向该文件V节点(Virtual Node,虚拟节点)表项的指针
每个打开文件(或设备)都有一个V节点结构。V节点包含了文件类型和对此文件进行操作的各种函数的指针。对于大多数文件,v节点还包含了该文件的i节点信息(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的。
文件描述符(File Description)
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。
当打开一个文件系统的现有文件时或者在文件系统中创建一个新的文件时,内核向进程返回一个文件描述符。
当进程对一个打开的文件执行读、写操作时,需将相应的文件描述符作为参数传递给内核。复制文件描述符-dup
下面两个函数都可用来复制一个现有的文件描述符
#includeint dup(int fd);int dup2(int fd_from,int fd_to);两函数的返回值:若成功,返回新的文件描述符;若失败,返回-1
由dup()返回的新文件描述符一定是当前可用文件描述符中的最小值
对于dup2()(读作dup to)可以用fd_to参数指定新文件描述符的值。
如果fd_to已经打开,则先将其关闭
如果fd_from等于fd_to,则dup2返回fd_to,而不关闭它
如果fd_from不等于fd_to,则fd2的FD_CLOEXEC文件描述符标志就会被清除
标准输入、标准输出、标准错误输出
按照惯例,UNIX系统的Shell会把由其创建的进程的文件描述符0与进程的标准输入关联(即与控制终端相关联),文件描述符1与标准输出关联(即与控制终端相关联)、文件描述符2与标准错误关联(即与控制终端相关联)。
注意:这是个中Shell以及很多应用程序的惯例,与UNIX内核无关。尽管如此,如果不遵循着中惯例,很多UNIX系统应用程序就不能正常工作。
在符合POSIX.1的应用程序中,数字0、1、2虽然已经被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO以提高代码的可读性。这些常量都在头文件<unistd.h>中定义
打开文件-open
#includeint open(const char * path,int openFlag,... /* mode_t mode */);int openat(int fd,const char * path,int openFlag,... /* mode_t mode */);返回值:成功返回文件描述符(非负),失败返回-1
path参数是要打开或创建文件的名字
openFlag是用来指明打开该文件的意图和其它选项
O_RDONLY 只读操作
O_WRONLY 只写操作
O_RDWR 读、写操作
O_APPEND 每次写操作都将内容追加到文件的尾端(即便当前文件偏移量的值小于文件长度,也会写到文件尾端)
#include
#include #include int main(){ int fd = open("example2.txt",O_RDWR|O_CREAT|O_APPEND); lseek(fd,4,SEEK_SET); printf("position = %d\n",lseek(fd,0,SEEK_CUR)); char *name = "Kakawater"; write(fd,name,strlen(name));} O_CREAT 若文件不存在,则创建。使用此选项时,open函数需要同时说明mode
O_NOCTTY 如果path引用的是终端设备,则不将该设备分配作为进程的控制终端
O_SYNC 每次write操作都等待物理I/O操作完成之后再返回
O_TRUNC 如果此文件存在,而且以只写或读写操作打开,则将其长度截断为0,即清空文件
关闭文件-close
#includeint close(int fd);返回值:成功返回0,失败返回-1
关闭一个文件时会释放该进程加载该文件上的所有记录锁
当一个进程终止时,内核自动关闭它所有的打开文件。
当前文件偏移量(Current File Offset)
每个打开文件都有一个与其相关联的"当前文件偏移量",它通常是一个非负数,用以度量从文件开始处计算的字节数。文件的读、写操作都是基于一个文件表项的当前文件偏移量来进行操作的。
当打开一个文件时,系统默认将当前文件偏移量设置为0,如果指定了O_APPEND选项,则该当前文件偏移量被设置为文件的长度。
我们可以调用lseek()来操作一个文件的当前文件偏移量
#includeoff_t lseek(int fd,off_t offset,int whence);返回值:成功返回新的文件偏移量;失败返回-1
参数offset的解释与whence(从何处,即参考点)的值有关
若whence是SEEK_SET,则将该文件的当前文件偏移量设置为距文件开始处offset个字节。
若whence是SEEK_CUR,则将该文件的当前文件偏移量设置为当前文件偏移量+offset,offset可为正或负
若whence是SEEK_END,则将该文件的当前文件偏移量设置为文件长度+offset,offset可为正或负
由于lseek执行成功会返回新的文件偏移量,因此我们可以使用如下方式来获得一个打开文件的当前偏移量
off_t currentOffset;currentOffset = lseek(fd,0,SEEK_CUR);
当前文件偏移量与I/O操作
lseek仅将当前的文件偏移量记录在内核中,它并不会引起任何I/O操作。然后,该偏移量用于下一次读写操作
文件偏移量可以大于文件的当前长度,这种情况下,对该文件的下一次写将会加长该文件,由于中间并没有数据,因此会在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
注意:文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分不需要分配磁盘块。
空洞文件示例:
[root@www ~]# cat fileHole.c #includechar buf1[] = "abcdefghij";char buf2[] = "ABCDEFGHIJ";int main(void){ int fd; if ((fd = open("file.hole",O_WRONLY|O_CREAT,0421)) < 0)return -1; if(write(fd,buf1,10) != 10)return -1; if(lseek(fd,16384,SEEK_SET) == -1)return -1; if(write(fd,buf2,10) != 10)return -1; return 0;}[root@www ~]#
[root@www ~]# gcc -o filehole fileHole.c [root@www ~]# ./filehole [root@www ~]# echo $?0[root@www ~]# ls -l file.hole -r-------x 1 root root 16394 5月 13 10:33 file.hole[root@www ~]# od -c file.hole 0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \00000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0*0040000 A B C D E F G H I J0040012[root@www ~]#
#includechar buf1[] = "abcdefghij";char buf2[] = "ABCDEFGHIJ";int main(void){ int fd; if ((fd = open("file.nohole",O_WRONLY|O_CREAT,0421)) < 0)return -1; if(write(fd,buf1,10) != 10)return -1; //if(lseek(fd,16384,SEEK_SET) == -1)return -1; if(write(fd,buf2,10) != 10)return -1; return 0;}
无空洞的文件占用了4个磁盘块,而具有空洞的文件占用了8个磁盘块
[root@www ~]# ls -ls file.nohole file.hole 8 -r-------x 1 root root 16394 5月 13 10:33 file.hole4 -r-------x 1 root root 20 5月 13 10:35 file.nohole[root@www ~]#
创建一个同等长度的文件
[root@www ~]# dd if=/dev/zero of=file.tmp count=1 bs=16394记录了1+0 的读入记录了1+0 的写出16394字节(16 kB)已复制,0.000230042 秒,71.3 MB/秒
与长度相同的文件相比,有空洞的文件只占用8个磁盘块,而无空洞的文件占用20个磁盘块
[root@www ~]# ls -ls file.nohole file.hole file.tmp 8 -r-------x 1 root root 16394 5月 13 10:33 file.hole 4 -r-------x 1 root root 20 5月 13 10:35 file.nohole20 -rw-r--r-- 1 root root 16394 5月 13 10:42 file.tmp[root@www ~]#
读、写操作与当前文件偏移量
读操作从文件的当前文件偏移量开始。在成功返回之前,该文件的当前文件偏移量将增加实际读取的字节数
对于普通文件,写操作从文件的当前文件偏移量开始。在一次成功写返沪之前,该文件的当前文件偏移量增加实际写的字节数
读操作-read
#includessize_t read(int fd,void * buf,size_t nbytes);返回值:实际读取到的字节数。若已到文件末尾,则返回0;若出错,则返回-1
有多种情况可使实际读取到的字节数少于要求读的字节数:
读普通文件时,在读到要求字节数之前已经到达了文件末尾
当从终端设备读取是,通常一次最多读取一行(可以通过改变终端的工作表示来达到)
当从网络读取时,网络的缓冲机制可能造成返回值小于所要读取的字节数
当从管道或FIFO读时,如若管道包含的字节少于所需要的数量,那么read将至返回实际可用的字节数
当从某些面向记录的设备(例如磁带)读时,一次最多返回一个记录
当一个信号造成读操作的中断,则返回已经读取的部分数据量
写操作-write
#includessize_t write(int fd,const void * buf,size_t nbytes);返回值:成功,则返回已写的字节数;失败,则返回-1
注意:wirte()提供的写出方式是一种"覆盖写",即若该位置存在数据则覆盖,不存在则直接写入。
#include#include #include int main(){ int fd = open("example2.txt",O_RDWR|O_CREAT); lseek(fd,4,SEEK_CUR); char *name = "Kakawater"; write(fd,name,strlen(name));}
[root@www ~]# echo 1234567890ABCDEFGHIJKLMN > example2.txt[root@www ~]# gcc -o finsert finsert.c[root@www ~]# ./finsert [root@www ~]# cat example2.txt 1234KakawaterDEFGHIJKLMN[root@www ~]#
I/O效率和内核缓冲
#include#include #include #define BUFFERSIZE 4096int main(void){ int readBytes = 0; int writeBytes = 0; char buffer[BUFFERSIZE]; int count = 0; clock_t start,end; start = clock(); while((readBytes = read(STDIN_FILENO,buffer,BUFFERSIZE)) > 0){ while(writeBytes < readBytes){ writeBytes += write(STDERR_FILENO,&buffer[writeBytes],readBytes - writeBytes); } writeBytes = 0; ++count; } time(&end); int second = localltime(&end).tm_sec - localltime(&start)->tm_sec; printf("循环次数 = %d\n",count); printf("耗时%f秒\n",(double)end-start)/CLK_TCK); return 0; }
次测试文件所用的文件系统为Linux的`文件系统,其磁盘块长度为4096`字节。
大多数文件系统为了改善性能都采用某种预读(read ahead)技术。当检测到正在进行顺序读取时,文件系统就会试图读入比进程所要求的更多数据,并假想进程很快就会读取这些数据。
标准I/O库
前面所有I/O函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符用于后续的I/O操作。
对于标准I/O库,它们的操作是围绕流(Stream)进行的。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
标准I/O库stdio.h为标准输入、标准输出和标准错误预定义了文件指针stdin、stdout和stderr加以引用:
stdin 标准输入的文件指针
stdout 标准输出的文件指针
stderr 标准错误的文件指针
打开流
标准库stdio提供了三个函数用于打开一个标准I/O流
#includeFILE * fopen(const char * pathname,const char * type);FILE * freopen(const char * pathname,const char * type,FILE * fp);FILE * fdopen(int fd,const char * type);//返回值:若成功,返回FILE文件指针;失败,返回NULL
fopen函数打开路径名为pathname的一个指定的文件
freopen函数在一个指定的流fp上打开一个指定的文件。
一般用于将一个指定的文件打开为一个预定义的流:标准输入(stdin)、标准输出(stdout)或标准错误(stderr)如果该流fp已经打开,则会先关闭该流
如果该流已经设置了定向,则会清除该定向
fdopen函数打开一个和指定文件描述符fd(我们可能从open、pipe、socket、accept等得到文件描述符)关联的标准I/O流。
此函数常用于由管道和网络通道函数返回的文件描述符。因为这些特殊类型的文件不能用标准I/O函数fopen函数,所以我们必须调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该文件描述符相结合。
type参数指定对该I/O流的读、写方式,ISO C规定type参数可由有15种不同的值
type | 说明 | open()函数中的相应标志 |
---|---|---|
r或rb | 以只读方式打开 | O_RDONLY |
w或wb | 把文件截断至0长,并以只写方式打开 | O_WRONLY|O_CREAT|O_TRUNC |
a或ab | 追加.将文件流游标定位到文件末尾,并以只写方式打开 | O_WRONLY|O_CREAT|O_APPEND |
r+或r+b或rb+ | 以读和写方式打开 | RDWR |
w+或w+b或wb+ | 把文件截断至0长,并以读和写方式打开 | RDWR|O_CREATE|O_TRUNC |
a+或a+b或ab+ | 追加,将文件流游标定位到文件末尾,并以读和写方式打开 | O_RDWR|O_CREATE|O_APPEND |
注意:r+和w+不完全相同,w+会将文件截断至0长
使用字符b作为type的一部分,使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不区分这两天文件,对于UNIX内核来说都是二进制文件。
文件描述符
在UNIX中,标准I/O库最终都要调用前面说明的read()和write()例程,因此每个标准I/O流都有一个与其相关联的文件描述符。我们可以使用fileno()函数来获得其文件描述符
#includeint fileno(FILE* fp);
缓冲
系统调用会使得程序陷入内核空间执行,因此为了尽可能少地减少read()和write()的调用,stdio.h库为文件读、写操作提供了缓冲区,从而加速了对文件的读、写操作。
当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针,该对象通常是一个结构,它包含了stdio.h库为管理该流所需要的所有信息:用于实际I/O的文件描述符,指向与该流关联的缓冲区的指针,缓冲区的长度,当前在缓冲区中的字符数,出错标志等等。
标准I/O提供的I/O操作函数大多数时候都是在对I/O缓冲区进行操作,只有少数时候会通过系统调用把读写请求交给内核。
以fgetc/fputc为例,当用户程序第一次调用fgetc读一个字节时,fgetc函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指向I/O缓冲区中的第二个字符,以后用户再调fgetc,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用fgetc时,fgetc函数会再次进入内核读1K字节到I/O缓冲区中。标准I/O库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,标准I/O库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作
标准I/O库提供了三种类型的缓冲:
不缓冲
行缓冲
全缓冲
在一个流上执行第一次I/O操作时,相关标准I/O函数通常会调用malloc()函数来获得需要使用的缓冲区。
不缓冲
在这种情况下,标准I/O库并不对字符进行缓冲存储。
例如,若用标准I/O函数fputs()将15个字符写到不带缓冲的流中,我们就期望这15个字符能立即输出,很可能使用write()函数将这些字符写到相关联的打开文件中。
根据惯例,标准错误流stderr通常是不带缓冲的。这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
行缓冲
在这种情况下,当在输入流或输出流中遇到换行符时,标准I/O库执行实际I/O操作(即调用read()或write())。这允许我们使用标准I/O函数fputc()一次输出一个字符,但只有在写了一行之后才进行实际I/O。这种方式能够显著地提高应用程序的I/O性能。
根据惯例,当输入流或输出流指向一个交互式设备(譬如终端)时,通常使用行缓冲。
全缓冲
在这种情况下,仅当填满标准I/O缓冲区后才进行实际I/O操作(即调用read()或write())。
对于驻留在磁盘上的文件通常是由标准I/O实施全缓冲。
ISO C规定:
当且仅当标准输入和标准输出并不指向交互式设备(譬如终端)时,它们才是全缓冲
标准错误决不会是全缓冲
设置缓冲区类型
对于一个已经打开的流,我们可以调用下面的函数来更改其缓冲类型:
#includevoid setbuf(FILE * fp,char * buf);void setvbuf(FILE * fp,char * buf,int mode,size_t size);//返回值 若成功,返回0;出错返回非0
setbuf
#includevoid setbuf(FILE * fp,char * buf);//返回值 若成功,返回0;出错返回非0
mode 缓冲区类型
buf 指向一个长度为BUFSIZE(该常量定义在stdio.h中)的用户缓冲区(即由用户自己提供,而不是由标准I/O提供)
通常在此之后,该流就是全缓冲的,但是如果该流指向一个终端设备,那么某些系统也可将其设置为行缓冲。
我们可以通过将参数buf设置为NULL,来关闭一个流的缓冲,即不带缓冲
setvbuf
#includevoid setvbuf(FILE * fp,char * buf,int mode,size_t size);//返回值 若成功,返回0;出错返回非0
mode 缓冲区类型
buf 指向一个长度为参数size的用户缓冲区(即由用户自己提供,而不是由标准I/O提供)
size 缓冲区大小
我们可以通过设置参数mode来指明缓冲类型:
**_IOFBF** 全缓冲(FBF,FULL BUFF)
**_IOLBF** 行缓冲(LBF,LINE BUFF)
**_IONBF** 不缓冲(NBF,NO BUFF)
如果要指定一个不带缓冲的流,则忽略buf和size参数。
如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲区及其长度:
如果该流是带缓冲的(全缓冲或行缓冲),而buf为NULL,则标准I/O库将自动地为该流分配适当长度的缓冲区。适当长度指的就是由常量BUFSIZE所指定的值。
注意:如果在一个函数内分配一个自动变量来作为标准I/O缓冲区,则在该函数返回之前,我们必须关闭该流。(因为在该函数返回之后,自动变量的内存空间将被用作其它用途,如果继续使用该流,则会出错)
缓冲区操作-flush
我们使用术语"冲洗(flush)"来说明对标准I/O缓冲区的写操作。
标准I/O缓冲区可由标准I/O例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用标准I/O库stdio.h提供的函数fflush()来冲洗一个流。
#includeint fflush(FILE * fp);返回值:若成功,返回0;否则返回EOF
fflush()会使得该流的输出缓冲区中的所有数据都被传递给内核,进行实际I/O。
作为一种特殊情况,若fp未NULL,则会该进程的所有输出流都会被冲洗
关闭流
标准I/O库提供了fclose()函数来关闭流
#includeint fclose( FILE *stream );//返回值:若成功,返回0;若出错,返回EOF
在流被关闭之前,会先冲洗flush缓冲区中的输出数据,而缓冲区中的任何输入数据都会被丢弃。
如果缓冲区是由标准I/O库自动分配的,则会自动释放此缓冲区;对于用户分配的缓冲区(通过setbuf或setvbuf)并不会自动释放,需要用户自己释放。
缓冲区游标
在UNIX中,文件的I/O操作是基于文件的"当前文件偏移量量"进行的。而标准I/O库stdio的I/O操作是基于"缓冲区游标"执行的。缓冲区游标的位置与文件的当前文件偏移量都是参考文件起始位置。
缓冲区采用的是懒加载机制,当进行第一次I/O操作时,标准I/O库才会将文件中的数据加载到缓冲区中.
#include#include #include #define BUFFSIZE 512int main (){ char buffer[BUFFSIZE]; char tmp[10]; memset(buffer,0,sizeof(char)*BUFFSIZE); memset(tmp,0,sizeof(char)*10); FILE * pFile; pFile = fopen ( "example.txt" , "r+b" ); int fd = fileno(pFile); setvbuf(pFile,buffer,_IOFBF,BUFFSIZE); int realPosition = lseek(fd,0,SEEK_CUR); int position = ftell(pFile); printf("> realPosition = %d position = %d\n",realPosition,position); printf("> 未进行I/O操作之前,缓冲区中的内容:%s\n",buffer); fgets(tmp,10,pFile); printf("> 进行读取操作之后,缓冲区中的内容:%s\n",buffer); realPosition = lseek(fd,0,SEEK_CUR); position = ftell(pFile); printf("> realPosition = %d position = %d\n",realPosition,position); fclose ( pFile ); return 0;}
执行结果:
kakawater$ ./fseekTestDemo > realPosition = 0 position = 0> 未进行I/O操作之前,缓冲区中的内容:> 进行读取操作之后,缓冲区中的内容:#字节顺序-大端存储与小端存储[](http://blog.erratasec.com/2016/11/how-to-teach-endian.html#.WM8g2xhY6Rs)[](http://www.ruanyifeng.com/blog/2016/11/byte-order.html)我们知道,在计算机内部数据是由一连串的零和一组成的,当我们使用一连串的零和一来表示一个数字时,就涉及到哪一边是高位,哪一边是低位的问题?我们称这个问题为"字节顺序"。字节顺序有两种,分别是**大端模式**和**小端模式**。![](media/14892364?zY?> realPosition = 512 position = 9kakawater$
kakawater$ mv example.txt example.txt.bakkakawater$ echo HelloWorld > example.txtkakawater$ ./fseekTestDemo > realPosition = 0 position = 0> 未进行I/O操作之前,缓冲区中的内容:> 进行读取操作之后,缓冲区中的内容:HelloWorld> realPosition = 11 position = 9kakawater$
并且我们可以从当前文件偏移量发现,标准I/O会尝试填满缓冲区
获取缓冲区游标位置
标准I/O库为我们提供了ftell()函数来获取缓冲区游标的位置
#includelong ftell(FILE *fp);//返回值:若成功返回当前文件偏移量;出错则返回-1L
当我们以只读方式(r或rb)打开一个文件流时,缓冲区游标位置为0
当我们以写方式(w或wb)打开一个文件流时,缓冲区游标位置为0
当我们以追加方式(a或ab)打开一个文件流时,当前缓冲区游标位置为文件的末尾(即文件的长度)
当我们以读写方式(r+或``r+b或w+或w+b)打开一个文件流时,缓冲区游标位置为0
当我们以追加并读的方式(a+或a+b)打开一个文件流时,缓冲区游标位置为文件的末尾
fseek()
将移动文件光标移动到一个指定位置
#includeint fseek( FILE *stream, long offset, int origin );
返回值
成功则返回0
失败返回非0
stream
指向FILE结构offset
距离origin的字节数origin
参考位置,其取值和lseek中的一样:SEEk_SET表示从文件的起始位置开始;SEEK_CUR表示从当前文件位置指示器开始、SEEK_END表示从文件的尾端开始
备注:
fseek()函数将与流相关的文件指针(如果有的话)移动到指定的位置(origin+offset)。对该流进行的下一个操组将会在这个新位置执行。
我们可以使用fseek()函数将文件指示移动到任何位置,和lseek()一样,fseek()函数可以将文件指示器移动超出文件长度的位置,会造成文件空洞。fseek()会清除EOF的标志,并且消除之前的ungetc()函数对流的影响。
#include#include #include #define BUFFSIZE 512int main (){ char buffer[BUFFSIZE]; char tmp[10]; memset(buffer,0,sizeof(char)*BUFFSIZE); memset(tmp,0,sizeof(char)*10); FILE * pFile; pFile = fopen ( "example.txt" , "r+b" ); int fd = fileno(pFile); setvbuf(pFile,buffer,_IOFBF,BUFFSIZE); int realPosition = lseek(fd,0,SEEK_CUR); int position = ftell(pFile); printf("> realPosition = %d position = %d\n",realPosition,position); printf("> 未进行I/O操作之前,缓冲区中的内容:%s\n",buffer); fgets(tmp,10,pFile); printf("> 进行读取操作之后,缓冲区中的内容:%s\n",buffer); realPosition = lseek(fd,0,SEEK_CUR); position = ftell(pFile); printf("> realPosition = %d position = %d\n",realPosition,position); printf("> 移动缓冲区游标到距离文件起始位置1000的位置\n"); fseek(pFile,1000,SEEK_SET); realPosition = lseek(fd,0,SEEK_CUR); position = ftell(pFile); printf("> 移动之后 realPosition = %d position = %d\n",realPosition,position); printf("> 此时缓冲区中的内容:%s\n",buffer); fgets(tmp,10,pFile); printf("> 进行读取操作之后,缓冲区中的内容:%s\n",buffer); printf("> 此时缓冲区中的内容:%s\n",buffer); printf("> 将Kakawater写入文件"); fputs("Kakawater",pFile); printf("> 此时缓冲区中的内容:%s\n",buffer); fclose ( pFile ); return 0;}
kakawater$ gcc -o fseekTestDemo fseekTest.c fseekTest.c:19:22: warning: implicit declaration of function 'lseek' is invalid in C99 [-Wimplicit-function-declaration] int realPosition = lseek(fd,0,SEEK_CUR); ^1 warning generated.kakawater$ echo HelloWorld > example.txtkakawater$ ./fseekTestDemo > realPosition = 0 position = 0> 未进行I/O操作之前,缓冲区中的内容:> 进行读取操作之后,缓冲区中的内容:HelloWorld> realPosition = 11 position = 9> 移动缓冲区游标到距离文件起始位置1000的位置> 移动之后 realPosition = 1000 position = 1000> 此时缓冲区中的内容:HelloWorld> 进行读取操作之后,缓冲区中的内容:HelloWorld> 此时缓冲区中的内容:HelloWorld> 将Kakawater写入文件> 此时缓冲区中的内容:Kakawaterdkakawater$
可见,当缓冲区游标移动超出文件长度之外的位置后,文件的偏移量也会移动到该文件。但此时缓冲区中的内容并不为空,而是为缓冲区中原先的值。当我们写入数据之后,缓冲区中的值才会变化。但是并不会将Kakawaterd写入文件,而是将Kakawater写到文件的1000位置处。
获取缓冲区游标在缓冲区中的偏移位置
我们可以通过下列公式来获取缓冲区游标在缓冲区中的偏移位置
int realPosition = lseek(fileno(file),0,SEEK_CUR); int position = ftell(file); int offset = realPosition == position ? 0 :buffSize - (realPosition - position);
rewind()
将一个缓冲区游标到文件的起始位置。相当于fseek(fp,0,SEEK_SET);
#includevoid rewind(FILE *fp);
覆盖写
标准I/O库提供的写出方式是一种"覆盖写",即若该位置存在数据则覆盖,不存在则直接写入。
#include#include #include #define BUFFSIZE 4void showPointerOffset(FILE * file,int buffSize){ int realPosition = lseek(fileno(file),0,SEEK_CUR); int position = ftell(file); int offset = realPosition == position ? 0 :buffSize - (realPosition - position); printf("> realPosition = %d position = %d offset = %d \n\n",realPosition,position,offset);}int main (){ char buffer[BUFFSIZE]; char tmp[6]; memset(buffer,0,sizeof(char)*BUFFSIZE); memset(tmp,0,sizeof(char)*6); FILE * pFile; pFile = fopen ( "example.txt" , "r+b" ); int fd = fileno(pFile); setvbuf(pFile,buffer,_IOFBF,BUFFSIZE); printf("> 未进行I/O操作之前,缓冲区中的内容:%s\n",buffer); showPointerOffset(pFile,BUFFSIZE); fgets(tmp,6,pFile); printf("> 进行读取操作之后,缓冲区中的内容:%s",buffer); showPointerOffset(pFile,BUFFSIZE); printf("> 移动缓冲区游标到距离文件起始位置10的位置\n"); fseek(pFile,10,SEEK_SET); printf("> 移动之后,缓冲区中的内容:%s",buffer); showPointerOffset(pFile,BUFFSIZE); fgetc(pFile); printf("> 再次进行读取操作之后,缓冲区中的内容:%s\n",buffer); showPointerOffset(pFile,BUFFSIZE); printf("> 此时缓冲区中的内容:%s\n",buffer); printf("> 将Kakawater写入文件"); fputs("Kakawater凌宇",pFile); printf("> 此时缓冲区中的内容:%s\n",buffer); showPointerOffset(pFile,BUFFSIZE); fclose ( pFile ); return 0;}
文本I/O
流的定向
不同的字符编码方案,一个字符所占用的字节数不同的。标准I/O文件流可以设置流的字符编码方案。流的定向(Stream's Orientation)决定了字符的编码方案。
当一个流最初被创建时,它并没有定向,我们可以通过fwide()函数来设置流的定向
#include#include int fwide(FILE * fp,int mode);返回值:若流是宽定向的,返回正直;若流是字节定向的,返回负值;若流是未定向的,返回0
如果mode参数值为负,则fwide试图使指定的流是字节定向的
如果mode参数值为正,则fwide试图使指定的流是宽定向的
如果mode参数值为0,则fwide将不会设置流的定向,但会返回该流定向的值。因而,我们可以使用0作为mode的值来获取流的定向
注意:fwide并不改变已定向流的定向
如果在未定向的流上使用一个多字节I/O函数(见<wchar.h>),则将流的定向设置为宽定向
如果在未定向的流上使用一个单字节I/O函数,则将流的定向设置为字节定向
读操作
一旦打开了流,则可以使用如下两种方式进行文本模式读取操作:
每次一个字符读取
#includeint getc(FILE *fp);int fgetc(FILE * fp);int getchar(void);//返回值: 成功,则返回读取到的字符;已到达文件尾端或出错,则返回EOF
其中getchar()函数等同于getc(stdin)。
前两个函数的区别是:getc()可被实现为宏,而fgetc不能实现为宏,这意味着:
getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次
fgetc一定函数,所以可以得到该函数的地址,这就允许将fgetc的地址作为一个参数传递给另一个函数
调用fgetc所需时间可能比调用getc要长,因为调用函数所需的时间通常长于宏
这3个函数在返回下一个字符时,将其unsigned char类型转换为int类型。说明为符号的理由是,如果最高位为1也不会使返回值为负。要求整型返回值的理由是:这样就可以返回所有可能的字符值再加上一个已经出错或已到达文件尾端的地址值(在stdio.h中的常量EOF被要求是一个负值,通常是-1)。这就意味着不能将这3个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回与常量EOF比较。
每次一行读取
#includechar * fgets(char * buf,int n,FILE * fp);char * gets(char * buf);//返回值:若成功,则返回buf;若已到达文件末尾或出错,则返回NULL
这两个函数都需要用户提供一个缓冲区用于接收读入的行。
和getchar()一样,gets()等同于fgets(stdin)。
对于fgets()必须指定缓冲区的长度,此函数会一直读到下一个换行符为止,但是不超过n-1个字符,读入的字符被送入给定的缓冲区buf。该缓冲区以null字符结尾。如果该行包括最后一个换行符的字符数超过了n-1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字符结尾。对fgets的下一次调用会继续使用该行
注意:gets不推荐使用,因为不能指定缓冲区的长度,可能造成缓冲区溢出(1988年的因特网蠕虫事件)
错误处理
由于不管是出错还是到达文件尾端,上述3个函数都返回EOF,因此无法准确判断是出错还是到达文件尾端。为此标准I/O库stdio提供了ferror()和feof()函数来进行判断
判断是否出错
#includeint ferror(FILE * fp);//返回值:若为出错,则返回非0(即True),否者返回0(即False)
判断是否到达文件末尾
#includeint feof(FILE * fp);//返回值:若到达文件末尾,则返回非0(即True),否则返回0(即False)
标准I/O的大多数实现中,为每个流在FILE对象中维护了两个标志:
出错标志
文件结束标志
我们可以调用clearerr()函数来清除这两个标志
#includevoid clearerr(FILE *fp);
写操作
一旦打开了流,则可以使用如下两种方式进行文本模式写出操作:
每次一个字符写出
#includeint putc(int c,FILE * fp);int fputc(int c,FILE * fp);int putchar(int c);//返回值:若成功,返回c;若出错,返回EOF
与读取操作一样,putchar()等同于fputc(c,stdout)。putc()可被实现为宏,fputc()不能实现为宏
每次一行写出
#includeint fputs(const char * str,FILE * fp);int puts(const char * fp);//返回值:成功返回非负值,出错返回EOF
函数fputs()会将一个以null字符终止的字符串写到指定的流,末尾的终止符null并不会写到流中,因为该null字符仅用来表示这是一个字符串的结尾,没有任何意义。
puts并不是完全等价于fputs(str,stdout),因为终端一般采用的行规程模式,因此必须在将字符串写入到流中之后(null字符并不会写入),再写入一个换行符\n以让终端显示该行字符串。
int puts(const char * str){ int state = fputs(str,stdout); fputs('\n',stdout); return state;}
二进制I/O(直接I/O)
二进制I/O又称为"直接I/O"。
如果使用fgetc()或fputc()写一个结构,那么必须通过循环整个结构,每次循环处理一个字节,一次读取或写一个字节,这会非常麻烦。
如果使用fgets()或fputs(),由于fputs()或fgets()再遇到一个null字节时就停止,而在结构中可能包含有null字节,所以不能使用它实现读写结构的要求。
此外如果进行二进制I/O操作,那么我们更愿意一次读或写一个完整的结构。因此标准I/O库stdio提供了下面两个函数进行二进制I/O操作:
#includesize_t fread(void * ptr,size_t objectSize size_t objectCount,FILE * fp);size_t fwrite(const void * ptr,size_t objectSize,size_t objectCount,FILE * fp);//返回值:实际读或写的对象数
fp 存放对象的空间
objectSize 为每个对象的大小
objectCount 为欲读入或写出的对象个数
对于读,如果出错或到达文件末尾,则其返回值可以少于objectCount。这种情况下,应调用ferror()函数或feof()函数来判断究竟是哪一种情况。
对于写,如果返回值小于objectCount,则出错二进制I/O的局限
二进制I/O只能用于读在同一系统上已经写入的数据。
现在,很多异构系统通过网络互联起来。常常,在一个系统上写的数据,要在另一个系统上进行处理。在这种情况下,这两个函数可能就不能正常工作。其原因是:
在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同(由于不同的字节对齐要求)。
的确,某些编译器有一个选项,选择它的不同值,或者使结构中的各成员紧密对齐(这可以节省空间,而运行性能可能有所下降),或者准确对齐(以便在运行时方便存取结构中的各成员)。这意味着,即使在同一个系统上,一个结构的二进制存放方式也可能因为编译程序选项的不同而不同用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。解决方案是统一字节序
标准I/O的缺陷
标准I/O库的一个不足之处就是"效率不高",这与它需要复制的数据有关。
当每使用一次函数fgets()和fputs()时,通常需要复制两次数据:一次是在内核和标准I/O缓冲区之前(当调用read和write时);第二次是在标准I/O缓冲区和用户程序的缓冲区之间。