Linux下的I/O主要分为两种:文件I/O和标准I/O。文件I/O直接使用系统调用,遵循POSIX标准(不遵循ANSI C标准),是无缓冲的I/O;标准I/O对文件I/O进行了封装,遵循ANSI C标准(不遵循POSIX标准),是有缓冲的I/O。
理解有/无缓冲
这里的有无缓存是指函数在用户空间是否有缓冲。不管用什么I/O,最后都需要文件I/O来和内核进行通信。在内核中,有专门的缓冲来储存文件I/O输入的信息。比如write()
会直接把数据写入内核的相应缓冲中;比如行缓冲的函数,需要接收\n
后才会将缓冲中的数据写入内核中的缓冲。注意1,缓冲并不是指函数参数中的buf
;注意2,每个流在进程中都有与其对应的缓冲,对于每个流,不同的标准I/O函数作用于相同的缓冲。
有一点我不清楚,到底读写各有一个缓冲还是一共只有一个缓冲?这里暂且认为一共只有一个缓冲吧。不管有几个缓冲,读写都需要使用缓冲,但读操作和写操作不一样。读是从内核缓冲中读一大块到I/O的缓冲,然后用户程序慢慢地从I/O缓冲中读取数据;而写就是只有I/O缓冲满了才会写入内核的缓冲(如果是全缓冲的话)。
文件I/O
open和close
open()
和close()
用于打开、关闭文件,其函数原型如下:
1 | // 错误返回-1,成功打开返回文件描述符 |
open()
函数的flags
必须要有O_RDONLY
, O_WRONLY
或 O_RDWR
;如果需要创建文件,还需要添加O_CREAT
并且给出相应的mode值(比如0777
)。open()
函数返回当前可用的最小文件描述符。close()
函数可以被直接调用,也可以通过结束进程的方式来间接调用。
write和read
write()
和read()
函数原型如下:
1 | // 函数成功返回读/写的字节数 |
这两个函数成功的话都返回读/写的字节数,这个字节数可能小于第三个参数设置的count
,比如从socket读或者读到了EOF
都会导致读的数量少于count
。我们这里看个例子:
1 |
|
从这个例子我们可以看出,
write()
和read()
函数遇到\0
都会自动结束读写(遇到EOF
也会);write()
函数可以不加\n
输出到STDOUT_FILENO
,但printf()
却不行;write()
文件后需要close()
或者调整偏移量,否则之后不能正确地读取文件内容。
上面的第2点牵涉行缓存的内容,这个会在下面进行讨论。我们这里先来看看文件描述符以及文件的偏移量偏移量。
lseek和文件描述符
Linux中每一个文件, socket, 设备等都是“文件”,都可以用文件I/O的函数对它们进行操作。其中fd0
是标准输入,fd1
是标准输出,fd2
是标准错误输出,其它的文件描述符都从fd3
开始。
在每个进程的进程表中都有一个记录文件表的项,每一个项对应一个文件表项。文件表项有文件的标志位、偏移量、打开文件的数量以及指向v-node表的指针等。图1是一个进程中的文件表项示意图:
1 | // 如果成功,返回当前相对于文件开始的偏移量;失败返回-1 |
我们可以改变whence
来调整偏移量修改的位置,可以是当前的位置(pcur + offset
)、也可是文件开始的位置(0 + offset
),也可以是文件结束的位置(file_size + offset
)。
标准I/O
文件I/O直接操作文件描述符,而标准I/O操作流,流将标准I/O和文件描述符联系在了一起。标准I/O和文件I/O的一个不同之处在于,标准I/O有缓冲而文件I/O没有缓冲,我们先来看看缓冲。
缓冲
缓冲分为3种,1. 全缓冲;2. 行缓冲;3. 无缓冲。
在一般情况下,行缓冲的I/O函数只有在遇到\n
时才会将缓冲中的数据写入内核(如果I/O的缓冲写满了,也会进行同样的操作);而全缓冲必须在I/O的缓冲写满时才会将数据写入内核。
标准I/O对stdout/stdin
的操作是行缓冲,对stderr
的操作是无缓存,而对文件的操作是全缓冲。我们可以通过下面函数修改流的缓冲类型:
1 | void setbuf(FILE *stream, char *buf); |
如果我们希望在缓冲没有写满的情况下将缓冲内容写入文件,那么我们要用fflush()
函数(或者关闭文件、终止进程)。fflush()
作用于输出流时,会清空(而不是写入磁盘)缓冲中的数据。除了fflush()
,fseek()
也可以实现相同的作用。
流的打开/关闭
标准I/O有3个函数用于打开流:
1 | // 成功返回FILE类型指针,否则返回NULL |
fopen()
函数用于打开/新建文件,mode
可以是w
, r
, a
, w
+, r+
和a+
,具体意义请参考APUE 5.5节。注意,在文件I/O中,我们同样可以设置mode
,但那个mode
和这里的mode
不一样。fopen()
创建的文件没有设置访问权限,我们需要额外使用umask()
函数来设置访问权限。
fdopen()
是POSIX标准下的函数,它可以将文件描述符转换成相应的FILE
指针。由于fd
已经存在,因此一些mode
的意义和fopen()
不同,具体说明参考APUE 5.5节。
freopen()
函数用于将stream
绑定到pathname
这个文件处,然后关闭stream
;或者改变stream
的mode
,具体细节参考本链接。
标准I/O用于关闭流的函数是fclose()
,该函数原型如下:
1 | // 成功返回0,否则返回EOF(-1) |
文件关闭后,该文件缓冲中所有数据被冲洗;进程正常终止时,所有标准I/O会被关闭,并且所有I/O中的缓冲会被冲洗。
流的读写(一次一个字符)
标准I/O中,流的读写有很多函数,它们可以分为3类:一次一个字符,一次一行和二进制/直接读写。这里先看一次一个字符的函数。
1 | // 成功返回下一个读取的字符;失败或者读到文件尾,返回EOF |
getc()
和fgetc()
作用一样,不过getc()
可以是宏,因此getc()
的参数不能有副作用。一般情况下,getc()
要比fgetc()
快一些。getchar()
和getc(stdin)
等效。因为读取失败和读到EOF
都返回EOF
,我们需要使用如下函数测试具体是失败还是读到EOF
:
1 | // 不是EOF返回0,否则返回非0 |
putc()
、fputc()
和putchar()
和读取函数一样,这里就不详细介绍了。
流的读写(一次一行)
一次一行的标准I/O读写函数有3个:
1 | // 成功返回s,错误或者读到EOF返回NULL |
gets()
函数已经被弃用,因此一共只有3个一次读一行的标准I/O函数。fgets()
用于读取一行,读到\n
停止,或者读了size - 1
个字符。如果第size - 1
个字符不是\n
,那么函数返回不完整的一行,再次调用fgets()
会继续读剩下的部分。不管是否读完,s
的最后一个字符一定是\0
。
fputs()
和puts()
都将一个以\0
结尾的字符串写出去,fputs()
和puts()
的输出都不包括\0
,但fputs()
可以没有\n
,但puts()
会添加一个\n
。puts()
将字符串输出到stdout
。和write()
一样,如果要输出的字符串中有\0
,那么只输出\0
前的字符串。
下面给出一个fputs()
和fgets()
的函数例子:
1 | int fgets_fputs_test(void) |
流的读写(二进制/直接读写)
二进制读写函数主要是为了解决上面函数的一个问题:输出时,遇到\0
就会停止。我们可以用fputc()
函数一个一个的字符/字节输出,但效率比较慢;如果用fputs()
的话,又不能处理有\0
的字符串;因此这种情况就需要使用二进制读写函数了。
1 | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); |
下面直接引用手册说明:
1 | The function fread() reads nmemb items of data, each size bytes long, from the stream pointed to by stream, storing them at the location given by ptr. |
格式化I/O
什么是格式化I/O?格式化I/O就是printf()
, sprintf()
, snprintf()
, scanf()
等函数,这类函数如果是对stdin
和stdout
进行操作,那么还是满足行缓存;如果对文件流进行操作,那么就是全缓存;如果只是对本地变量进行操作(比如sprintf()
),那么是没有缓冲的。比如下面这段代码:
1 | int main() |
即使"The total is %d"
没有换行符,字符串也会写入buffer
;但如果"%d characters stored\n"
不加换行符,那么这段字符串是不会输出到stdout
的。
总结
这部分内容比较繁杂,但只要搞懂了文件和标准I/O的区别,不同I/O缓冲的区别,那么还是比较好理解的。下面是一些比较重要的知识点,需要好好理解记忆:
- 文件I/O都不带缓冲;
- 标准I/O默认情况下基本都带缓冲,但它们缓冲类型不同;
- 当缓冲写满、文件关闭、进程结束、调用
fflush()
,fseek()
等函数时,缓冲中数据写入内核; - 对于行缓冲,遇到
\n
也要将缓冲中数据写入内核; - 上面3 - 4针对的是写操作;对于读操作,先从内核读取适量数据到缓冲,然后再慢慢读取;
- 不管有无用户空间的缓存,内核空间都是有缓冲的。