IO是一个比较大的概念,它所涉及到的内容也比较多,比较繁杂,下面就从文件的IO开始说起,因为unix下,一切皆文件。UNIX系统中的大多数文件I/O只需用到5个函数:open、read、write、lseek以及close。 当然,有一些基本概念必须先解释清楚。
一 系统IO
1 文件描述符
首先要了解一下文件描述符的概念,文件描述符是一切的基础,有文件描述符操作系统才能准确找到需要发生IO的文件,之后才会有各种IO事件的发生。
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。注意:文件描述符是针对进程的,脱离了进程则毫无意义。
注意文件描述符0,1,2这三个数字已经被占用了。在符合POSIX.1的应用程序中,幻数0、1、2为标准输入,标准输出,标准错误,常用符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO表示以提高可读性,都在头文件<unistd.h>中定义。
2 系统调用I/O函数
① open
int open(const char *path, int oflag,... /* mode_t mode */);
打开一个已有的文件,返回该文件的文件描述符。
② creat
int creat(const char *path, mode_t mode);
// 等效于以新建的方式open一个不存在的文件 成功返回描述符,失败返回-1
open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
返回文件描述符。
creat的一个不足之处是它以只写方式打开所创建的文件。在提供open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat、close,然后再调用open。现在则可用下列方式调用open实现:open(path, O_RDWR|O_CREAT|O_TRUNC, mode);
③ close
int close (int fd);
成功返回0,失败返回-1
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。注意:当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。
④ lseek
一般来说,操作系统读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。调用lseek显式地为一个打开文件设置偏移量。
off_t lseek(int fd, off_t offset, int whence);
出错返回 -1
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
总结一下:lseek如果设为大于当前文件的长度,在文件被读时,这些空洞被识别为0,但是却不占用磁盘空间。
⑤ read
ssize_t read(int fd, void *buf, size_t nbytes);
如read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。
⑥ write
ssize_t write(int fd, const void *buf, size_t nbytes);
若成功,返回已写的字节数;若出错,返回-1
⑦ 原子操作
考虑下面的这种操作:A,B两个进程,对同一个日志文件进行写,A先用lseek定位到文件末尾,假设这个值是100,但是这个时候内核切换了,B开始对同一个日志文件进行写入,lseek定位到文件末尾100,然后开始进行写入,写了10个字节,此时的文件末尾偏移量为1600。然后内核切到A进程,A进程从1500的偏移量开始写,就会把B进程的内容覆盖。
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
返回值:若成功,返回已写的字节数;若出错,返回−1
操作系统提供了两个原子操作的函数。
pwrite相当于先调用lseek接着调用write。但又不完全是这样:(1) pwrite是原子操作,期间不可中断;(2) 不更新当前文件偏移量。
记住:上述所有操作均:为不带缓冲的系统调用,将与后面的库函数形成鲜明对比。
二 标准IO库
1. 流
之前说的操作系统对IO的操作都是通过文件来进行的,通过对文件的读和写还有重定向。但是标准库不同,它实现了一套基于流的IO操作。
对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节(“宽”)字符集。流的定向(stream's orientation)决定了所读、写的字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节 I/O 函数(见<wchar.h>),则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设为字节定向的。只有两个函数可改变流的定向。freopen函数(稍后讨论)清除一个流的定向;fwide函数可用于设置流的定向。
当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。
2 缓冲
(1) 基础概念
这边直接复制unix高级编程的内容:
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是,标准I/O库最令人迷惑的也是它的缓冲。
标准I/O提供了以下3种类型的缓冲。
(1)全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在 UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面(例如,在第18章中所述的tcflush函数),flush(刷清)表示丢弃已存储在缓冲区中的数据。
(2)行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。
对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O 库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
(3)不带缓冲。标准I/O库不对字符进行缓冲存储。例如,若用标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符能立即输出,很可能使用3.8节的write函数将这些字符写到相关联的打开文件中。
标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
ISO C要求下列缓冲特征。
- 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
- 标准错误决不会是全缓冲的。
但是,这并没有告诉我们如果标准输入和标准输出指向交互式设备时,它们是不带缓冲的还是行缓冲的;以及标准错误是不带缓冲的还是行缓冲的。很多系统默认使用下列类型的缓冲: - 标准错误是不带缓冲的。
- 若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。
一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
(2) 打开流
以下三个函数可以打开一个标准IO流:
#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);
3个函数的返回值:若成功,返回文件指针;若出错,返回NULL
这3个函数的区别如下。
- (1)fopen函数打开路径名为pathname的一个指定的文件。
- (2)freopen 函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用 freopen 清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
- (3)fdopen函数取一个已有的文件描述符(我们可能从open、dup、dup2、fcntl、pipe、socket、socketpair或accept函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。
(3) 读写流
一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作。
(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
(2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。
(3)直接 I/O。fread和fwrite函数支持这种类型的I/O。每次 I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。
3 实现
每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数以获得其描述符。
系统的默认是:当标准输入、输出连至终端时,它们是行缓冲的。行缓冲的长度是 1 024 字节。注意,这并没有将输入、输出的行长限制为 1 024 字节,这只是缓冲区的长度。如果要将 2 048 字节的行写到标准输出,则要进行两次 write 系统调用。当将这两个流重新定向到普通文件时,它们就变成是全缓冲的,其缓冲区长度是该文件系统优先选用的 I/O 长度(从 stat 结构中得到的 st_blksize 值)。从中也可看到,标准错误如它所应该的那样是不带缓冲的,而普通文件按系统默认是全缓冲的。
4 文件系统
每个进程进程维护一张表,记录着打开的文件描述符(同一个文件打开两次会产生两个文件描述符),系统内核有一张打开文件表,记录着文件本身的状态,同时有一张inode表,记录着当前lseek位置等信息。
5 总结
标准IO库通过流这一手段,通过引入缓冲区的概念,(行缓冲,全缓冲,无缓冲),减少调用系统read, write的次数,对系统IO操作进行优化,但是要理解的是,无论标准库做了哪些优化,一定都是离不开文件描述符的,这是一切IO的基础,同时也离不开底层open, write, read等函数的支持。
缓冲:
- 行缓冲遇到\n再写文件。
- 全缓冲在缓冲区写满后写文件。
- 无缓冲直接写文件。
-
可以通过flush直接强制触发读写。