根据《Unix/Linux系统编程》中关于IO库函数部分内容进行总结
包括系统调用与IO库函数关系,基本IO库函数
fopen
fclose
fgetc getchar getc
fputc putchar putc
fgets gets
fputs puts
fscanf scanf sscanf
fprintf printf sprintf
fread
fwrite
1. Linux中的系统调用与I/O库函数
系统调用是文件操作基础,但是系统调用只支持数据块的读写(BLKSIZE如4KB
)。然而实际用户可能更希望以适合于应用程序的逻辑单元来读写文件,这时候就可以用IO库函数,方便用户使用且效率更高。
IO库函数是建立在系统调用基础上的:
系统调用 | 对应的I/O库函数 |
---|---|
open() | fopen() |
read() | fread() |
write() | fwrite() |
lseek() | fseek() |
close() | fclose() |
可以从上面的表格中看出I/O库函数是建立在系统调用基础上的,即I/O库函数内部会调用系统调用。
系统调用对文件的操作是依靠文件描述符(一个整数)进行操作的,而I/O库函数是依靠文件流指针FILE*
进行操作的。
fopen()库函数会发出open()系统调用。失败返回NULL,成功则在堆分配一个FILE结构体,该结构体包含了一个内部缓冲区char fbuf[BLKSIZE]
,其大小通常与文件系统的数据块BLKSIZE一致。FILE还包含一个整数用于表示文件描述符fd,除此之外还有用于操作缓冲区fbuf的各种指针、计数器和状态变量。
当调用fgetc(fp)
库函数会尝试从文件流FILE*中获取一个字符。如果FILE结构体中的内部缓冲区fbuf为空,则发出read(fd,fbuf,BLKSIZE)
系统调用,从文件描述符fd指向的文件中取出BLKSIZE个字节并存放在fbuf中,然后从fbuf中返回一个字符作为fgetc库函数的返回值。因此只要fbuf中有数据,fgetc就可以直接从fbuf中获取字符作为返回值,直到fbuf为空再发出read系统调用获取BLKSIZE个字节。即read系统调用仅用于重新装填fbuf,且系统调用总是以文件系统数据块大小(4KB)进行数据传输。
因此,在这个例子中对于单个字符的读写,系统调用是非常低效的,因为系统调用以数据块作为传输单位。这也解释了为什么在一些情景下I/O库函数的效率比系统调用效率高。
2. 什么时候使用系统调用,什么时候使用IO库函数
如上所述,如fgetc这样的IO库函数先将数据从内核复制到FILE结构体内部缓冲区,再从内部缓冲区将数据复制到程序的缓冲区,因此IO库函数传输了两次数据。而系统调用直接将数据从内核复制到程序的缓冲区,只传输了一次数据。因此对于以数据块BLKSIZE为单位的读写数据来说,系统调用的效率要更高,因为只用传输一次数据而不是两次。
因此对于以BLKSIZE为单位的数据读写,使用系统调用的效率更高。但是如果不是以BLKSIZE为单位进行数据读写(如上文以单个字节进行读写),那么使用IO库函数效率要更高,虽然IO库函数内部也会发出系统调用,但是这些系统调用仅仅是为了填充或者清除内部缓冲区,并不是每一次IO库函数都会发出系统调用的。并且进程进入内核模式(如发出系统调用使之从用户态陷入内核态)的代价要比停留在用户模式更高。
3. 基本I/O库函数
3.1 fopen打开文件
打开文件,成功返回FILE*指针,失败返回NULL并制定errno值来表示错误。
FILE * fopen(const char * filename, const char * modes)
发出open系统调用,并在堆区创建FILE结构体。该结构体对象包含打开文件的文件描述符fd,内部缓冲区fbuf和操作内部缓冲区的各种指针、计数器和状态变量。
-
第一个参数filename:文件路径
-
第二个参数modes:打开文件方式
参数值 描述 “r” 只读打开文件。读写指针被定位于文件的开始。 “r+” 读写打开文件。读写指针被定位于文件的开始。 “w” 只写打开文件。将文件长度截断为零,读写指针被定位于文件开始 “w+” 读写打开文件。如果文件不存在就创建,否则将截断它。读写指针被定位于文件的开始。 “a” 追加打开文件。如果文件不存在就创建,读写指针被定位于文件的末尾。 “a+” 追加打开文件。如果文件不存在就创建,读文件的初始位置是文件的开始,但是输出总是被追加到文件的末尾。
3.2 fclose关闭文件
关闭文件流。成功返回 0,否则返回 EOF(-1) 并设置全局变量 errno 来表示错误。
int fclose(FILE * stream)
如果该文件流以写的方式打开,fclose函数会把内部缓冲区fbuf内最后剩余的数据写入到内核缓冲区(把还没写入的数据写入文件)。fclose会发出close系统调用来关闭FILE结构体中的文件描述符,然后释放FILE结构体并将FILE*重置为NULL
3.3 fgetc读取单个字符
从文件流中读一个字符,该字符的ASCII值作为函数的返回值。若返回值为EOF(-1),说明文件结束或错误。
int fgetc(FILE *stream)
注意fgetc返回的是整数,因为如果读取到文件末尾或者错误时返回EOF(-1)因此返回值不能是无符号类型。
对于stdin,使用getchar
函数从标准输入流stdin
中读取一个字符,相当于fgetc(stdin)
int getchar()
fgetc和getc的区别:
fgetc和getc都可以从指定的文件流中读取下一个字符。但是fgetc一定是I/O库函数,而getc可以被实现为宏
#define getc(_fp) _IO_getc (_fp)
3.4 fputc写入单个字符
将字符c写入文件流中读写指针指向位置。如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF。
int fputc(int c, FILE *stream)
对于stdout,使用putchar
函数向标准输出流stdout
中写入一个字符,相当于fputc(c,stdout)
int putchar(int c)
fputc和putc的区别:
fputc和putc都可以向指定的文件流中写入一个字符。但是fputc一定是I/O库函数,而putc可以被实现为宏
#define putc(_ch, _fp) _IO_putc (_ch, _fp)
3.5 fgets读取多个字符
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取到 (n-1) 个字符、或者读取到换行符、到达文件末尾时,停止读取。
char * fgets(char * str, int n, FILE * stream)
- 参数str:缓冲区,fgets函数将读取到的数据保存在该缓冲区中
- 参数n:最大读取字符数(包括最后的’\0’),通常传递的是str字符数组长度
- 参数stream:文件流指针
如果成功,该函数返回相同的 str 参数。如果到达文件末尾、没有读取到任何字符或者发生错误,返回一个空指针。
对于stdin,使用gets函数从标准输入流stdin中读取一行,并把它存储在 str 所指向的字符串中
char *gets(char *str)
3.6 fputs写入多个字符
把字符串写入到指定的流 stream 中,但不包括’\0’。
int fputs(const char * str, FILE * stream)
- 参数str :字符数组,包含了要写入的以空字符终止的字符序列
- 参数stream : 文件流
对于stdout,使用puts函数把一个字符串写入到标准输出流stdout,直到’\0’,但不包括’\0’。换行符会被追加到输出中。
int puts(const char *str)
3.7 fscanf格式化输入
从文件流中以格式format读取数据
int fscanf(FILE * stream, const char * format, ...)
对于stdin,使用scanf函数从标准输入流按照格式获取数据
int scanf(const char * format, ...)
3.8 fprintf格式化输出
向文件流以格式format写入数据
int fprintf(FILE * stream, const char * format, ...)
对于stdout,使用printf函数向标准输出流按照格式写入数据
int printf(const char * format, ...)
3.9 内存中的转换函数
注意这里的sscanf()和sprintf()函数不是I/O库函数,仅仅是内存中的数据转换函数。例如atoi()函数可以将字符串转换为int类型数字,相反没有itoA()函数,因为可以通过sprintf函数完成。
3.9.1 sscanf函数
从字符串格式化读取输入
int sscanf(const char * s, const char * format, ...)
3.9.2 sprintf函数
将数据格式化写入到 str 所指向的字符串。
int sprintf(char * str, const char * format, ...)
3.10 fread函数
在第一次调用fread函数时,FILE中的内部缓冲区fbuf是空的,fread发出read(fd,fbuf,BLKSIZE)系统调用填充缓冲区,并设置fbuf的相关指针,计数器和状态变量,以表明fbuf中的数据情况。然后将数据从fbuf中复制到程序(调用处)的缓冲区中以尝试满足fread函数需求。如果FILE内部缓冲区的数据不够,就再发出一个read系统调用填充fbuf,直到满足fread函数或者到达文件末尾则停止。
即每次fread函数调用都会从FILE结构体的fbuf中复制数据,当缓冲区为空时才发出read系统调用重新填充fbuf。因此fread()一方面函数一方面接收来自用户程序的调用,另一方面根据fbuf情况向操作系统内核发出read系统调用。
fread()函数:从给定输入流stream读取数据到 ptr 所指向的数组中。成功返回所读的元素个数(不是字节数),失败如遇到文件结束或出错时可能返回0。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
- ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
- size – 这是要读取的每个元素的大小,以字节为单位。
- nmemb – 这是元素的个数,每个元素的大小为 size 字节。
- stream – 文件流。
例子:
char str[10] = {};
fread(str, 1, 5, stdin);//从标准输入流读取数据并保存到字符数组
3.11 fwrite函数
初始时FILE结构体内fbuf是空的,fwrite函数将数据写入FILE的fbuf中并调整相关指针、计数器和状态变量。当fbuf满时发出write系统调用将整个缓冲区写入操作系统内核。(这也可以解释当以写方式打开文件时,fclose函数会在关闭文件前将缓冲区内的数据写入到内核缓冲区,因为I/O库函数并不保证当即将数据写入文件)
fwrite()函数:向指定的文件中写入若干数据块,如成功执行则返回实际写入的数据块数目。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
- ptr – 这是指向要被写入的元素数组的指针。
- size – 这是要被写入的每个元素的大小,以字节为单位。
- nmemb – 这是元素的个数,每个元素的大小为 size 字节。
- stream – 文件流
例子:
char str[] = "这是个测试";
fwrite(str, sizeof(str), 1, stdout);//将字符数组打印到标准输出流