专注于用户空间的系统级编程–即内核之上的所有内容。
注意标准I/O调用与系统调用的区别
针对频繁轻量级I/O请求通常会使用“用户缓冲I/O”(user-buffered I/O)来提高执行效率。
标准I/O
C标准库中提供了标准I/O库(简称stdio),实现了跨平台的用户缓冲解决方案。
stdio不是直接操作文件描述符,而是通过唯一标识符,即文件指针(file pointer)来操作。
在标准C库中,文件指针和文件描述符一一映射。
文件指针是有指向类型定义FILE的指针表示,在<stdio.h>中。
在标准I/O中,打开的文件称为“流”(stream)。流可以被打开来读(输入流)、写(输出流)或者两者兼有。
打开文件
文件通过fopen()打开以供读写操作:
#include <stdio.h>
FILE * fopen(const char * path,const char * mode);
该函数根据mode参数,按照指定模式打开path指向的文件,并给它关联上新的流。
- r 以只读方式打开文件,该文件必须存在。
- r+ 以可读写方式打开文件,该文件必须存在。
- rb+ 读写打开一个二进制文件,允许读数据。
- rt+ 读写打开一个文本文件,允许读和写。
- w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
- w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
- a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
- a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)
- wb 只写打开或新建一个二进制文件;只允许写数据。
- wb+ 读写打开或建立一个二进制文件,允许读和写。
- wt+ 读写打开或着建立一个文本文件;允许读写。
- at+ 读写打开一个文本文件,允许读或在文本末追加数据。
- ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。
如果成功的打开一个文件, fopen()函数返回文件指针,
否则返回空指针(NULL)。由此可判断文件打开是否成功:
FILE *stream;
stream = fopen("/etc/text.c", "r");
if(!stream)
/*error*/
通过文件描述符打开流
fdopen()函数用于在一个已经打开的文件描述符上打开一个流:
#include <stdio.h>
FILE fdopen(int fd, const char *mode);
其第1个参数表示一个已经打开的文件描述符,
第2个参数mode的意义和fopen()函数的第2个参数一样。
只有一点不同的是,由于文件已经被打开,所以fdopen函数不会创建文件,
而且也不会将文件截短为0,这一点要特别注意。
这两步操作在打开该文件描述符的时候已经完成。
关闭流
fclose()函数用来关闭一个由fopen()函数打开的文件 , 其调用格式为:
int fclose(FILE *stream);
该函数返回一个整型数。当文件关闭成功时, 返回0, 否则返回一个非零值。
可以根据函数的返回值判断文件是否关闭成功。
从流中读取数据
为了成功调用读写函数,必须保证流以适当的模式打开。
字节操作:
#include <stdio.h>
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
int ungetc(int c, FILE *stream); //将字符放回到流中
行操作:
#include <stdio.h>
char * fgets(char *str, int size, FILE *stream);
int fputs(const char *str, FILE *stream);
二进制操作,用于读写复杂的二进制数据,例如C语言的结构体:
#include <stdio.h>
size_t fread(void *buf, size_t size, size_t nr, FILE *stream);
size_t fwrite(void *buf, size_t size, size_t nr, FILE *stream);
缓冲I/O示例
#include <stdio.h>
int main(void)
{
FILE *in, *out;
struct pirate {
char name[100];
unsigned long booty;
unsigned int beard_len;
} p, blackbeard = {"Edward Teach", 950, 48};
out = fopen("data", "w");
if(!out) {
perror("fopen");
return 1;
}
if(!fwrite(&blackbeard, sizeof(struct pirate), 1, out)) {
perror("fwrite");
return 1;
}
if(fclose(out)) {
perror("fclose");
return 1;
}
in = fopen("data", "r");
if(!in) {
perror("fopen");
return 1;
}
if(!fread(&p, sizeof(struct pirate), 1, in)) {
perror("fwrite");
return 1;
}
if(fclose(out)) {
perror("fclose");
return 1;
}
printf("name = %s booty = %lu beard_len = %u", p.name, p.booty, p.beard_len);
}
定位流
int fseek(FILE *stream, long offset, int whence);
第一个参数stream为文件指针
第二个参数offset为偏移量,整数表示正向偏移,负数表示负向偏移
第三个参数whence设定从文件的哪里开始偏移,可能取值为:SEEK_CUR、 SEEK_END 或 SEEK_SET
* SEEK_SET: 文件开头
* SEEK_CUR: 当前位置
* SEEK_END: 文件结尾
其他函数:
int fsetpos(FILE *stream, fpos_t *pos); //fseek(stream, pos, SEEK_SET);
void rewind(FILE *stream); //fseek(stream, 0, SEEK_SET);
获取当前流位置的函数:
long ftell(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);
刷新流
#include <stdio.h>
int fflush(FILE *stream);
调用该函数时,stream指向的流中所有未写入的数据会被flush到内核中。
如果stream为NULL,进程中所有的stream会被flush。
C函数库维持的缓冲区和内核本身的缓冲区之间的区别:
C函数库维持的缓冲器存在于用户空间,调用的性能提升来自于用户空间,运行的是用话代码,而不是系统调用。
只有当需要访问磁盘介质时,才会发起系统调用。
fflush()函数的功能只是把用户缓冲区的数据写入到内核缓冲中,
但并不保证数据最终会写到物理介质上。如果需要这个功能,应该使用fsync()等函数。
为了确保数据最终会备份存储,应该先保证用户缓冲区数据写到内核缓冲,然后保证内核缓冲写到磁盘介质上。
控制缓冲
int setvbuf( FILE *stream, char *buffer, int mode, size_t size );
函数setvbuf()设置用于stream(流)的缓冲区到buffer(缓冲区),其大小为size(大小). mode(方式)可以是:
- _IOFBF, 表示完全缓冲
- _IOLBF, 表示线缓冲
- _IONBF, 表示无缓存
在关闭流时,其使用的缓冲区必须存在,要注意自定义的缓冲区位于局部作用域时的有效性。例如:
#include <stdio.h>
int main(void)
{
char buf[BUFSIZ];
setvbuf(stdout, buf, _IOFBF, BUFSIZ);
return 0;
/* 'buf' exits scope and is freed, but stdout isn't closed until later */
}
结束语
标准I/O最大的诟病是两次拷贝带来的性能开销。
当读数据时,标准I/O会向内核发起read()系统调用,把数据从内核中复制到标准I/O缓冲区。
当应用通过标准I/O如fgets()发起读请求时,又会从标准I/O缓冲区拷贝到指定缓冲区。
写入请求时:数据先从指定缓冲区拷贝到标准I/O缓冲区,然后执行系统调用从标准I/O缓冲区写入内核。
与底层的系统调用相比,标准I/O为高层接口。