0%

Linux系统编程-缓冲I/O


专注于用户空间的系统级编程–即内核之上的所有内容。

注意标准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为高层接口。