Linux 文件IO: 缓冲与非缓冲

本地文件IO一般都是同步阻塞的

本地普通文件IO一般关注的是缓存,一般都是同步阻塞的。普通文件的file descriptor是block也是POSIX标准。这点不同于网络IO,网络IO要考虑传输两边进程处理等,设计之初就提供了带状态检测的异步操作方式,本地文件IO则必然要求高可靠性的。

利用系统调用, unbuffered I/O

不带缓冲指的是每个readwrite都调用了内核的一个系统调用。这些函数在调用的时候系统调用直接进行了磁盘文件的写入操作。

#include <string.h>
#include <unistd.h>

int main() {
    char* buffer = "hello, world\n";
    write(1, buffer, strlen(buffer));
    return 0;
}

打断点可以看是直接调用的系统调用

(gdb) pt write
type = int ()
(gdb) bt
#0  write () at ../sysdeps/unix/syscall-template.S:81
#1  0x00000000004005ad in main () at main.c:6

利用C标准库, buffered IO

标准IO提供缓存的目的是尽可能减少使用read, write调用的数量。

标准IO库处理很多细节,如缓存分配,以优化长度执行I/O,是在系统调用函数基础上构造的,便于用户使用。标准IO函数fopen返回一个指向FILE对象的指针。该对象管理该流所需要的所有信息:用于实际IO的文件描述符,指向流缓存的指针,缓存的长度,当前在缓存中的字符数,出错标志等。

标准输入、标准输出和标准出错这三个标准IO流通过预定义文件指针 stdin, stdout, stderr 加以引用。这三个文件指针同样定义在头文件<stdio.h>中。

如:函数fputs将一个以null符终止的字符串写到指定的流,终止符null不写出。

#include <stdio.h>

int main() {
    char* buffer = "hello, world!\n";
    fputs(buffer, stdout);
    return 0;
}

打断点看最终还是用的操作系统调用

(gdb) pt write
type = int ()
(gdb) bt
#0  write () at ../sysdeps/unix/syscall-template.S:81
#1  0x00007ffff7a8de53 in _IO_new_file_write (f=0x7ffff7dd4400 <_IO_2_1_stdout_>, data=0x7ffff7ff7000, n=14) at fileops.c:1261
#2  0x00007ffff7a8f32c in new_do_write (to_do=14, data=0x7ffff7ff7000 "hello, world!\n", fp=0x7ffff7dd4400 <_IO_2_1_stdout_>)
    at fileops.c:538
#3  _IO_new_do_write (fp=fp@entry=0x7ffff7dd4400 <_IO_2_1_stdout_>, data=0x7ffff7ff7000 "hello, world!\n", to_do=14)
    at fileops.c:511
#4  0x00007ffff7a8f703 in _IO_new_file_overflow (f=0x7ffff7dd4400 <_IO_2_1_stdout_>, ch=10) at fileops.c:876
#5  0x00007ffff7a9055c in __GI__IO_default_xsputn (f=f@entry=0x7ffff7dd4400 <_IO_2_1_stdout_>, data=data@entry=0x400624,
    n=n@entry=14) at genops.c:480
#6  0x00007ffff7a8e532 in _IO_new_file_xsputn (f=0x7ffff7dd4400 <_IO_2_1_stdout_>, data=<optimized out>, n=14) at fileops.c:1353
#7  0x00007ffff7a83614 in __GI__IO_fputs (str=0x400624 "hello, world!\n", fp=0x7ffff7dd4400 <_IO_2_1_stdout_>) at iofputs.c:40
#8  0x0000000000400593 in main () at main.c:5

其中stdout是标准库定义的一个类型, 可以在标准库源代码看到, 也可以 gdb 看到起类型, 这个结构体中就有一个字段是文件描述符,也是1,可以很容易查看

(gdb) pt stdout
type = struct _IO_FILE {
    int _flags;
    char *_IO_read_ptr;
    char *_IO_read_end;
    char *_IO_read_base;
    char *_IO_write_base;
    char *_IO_write_ptr;
    char *_IO_write_end;
    char *_IO_buf_base;
    char *_IO_buf_end;
    char *_IO_save_base;
    char *_IO_backup_base;
    char *_IO_save_end;
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno;
    int _flags2;
    __off_t _old_offset;
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
    __off64_t _offset;
    void *__pad1;
    void *__pad2;
    void *__pad3;
    void *__pad4;
    size_t __pad5;
    int _mode;
    char _unused2[20];
} *
(gdb) p stdout->_fileno
$1 = 1

一点有趣的地方, cd /usr/include/stdio.h 源码有下面一处注释,very funny, make them happy :)

/* Standard streams.  */
extern struct _IO_FILE *stdin;          /* Standard input stream.  */
extern struct _IO_FILE *stdout;         /* Standard output stream.  */
extern struct _IO_FILE *stderr;         /* Standard error output stream.  */
/* C89/C99 say they're macros.  Make them happy.  */
#define stdin stdin
#define stdout stdout
#define stderr stderr

标准IO三种类型的缓存

  1. 全缓存。在这种情况下,当填满标准IO缓存后才进行实际IO操作。对于驻在磁盘上的文件通常是由标准IO库实施全缓存的。
  2. 行缓存。在这种情况下,当在输入和输出中遇到新行符时,标准IO库执行IO操作。两个限制:第一个是:因为标准IO库用来收集每一行的缓存的长度是固定的,所以只要填满了缓存,那么即使还没有写一个新行符,也进行IO操作。第二个是:任何时候只要通过标准输入输出库要求从(a)一个不带缓存的流,或者(b)一个行缓存的流(它预先要求从内核得到数据)得到输入数据,那么就会造成刷新所有行缓存输出流。
  3. 不带缓存。标准IO库不对字符进行缓存。相当于用write系统调用, 如标准出错流stderr通常是不带缓存的,这就使得出错信息可以尽快显示出来。

ANSI C要求下列缓存特征:
- 当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓存的。
- 标准出错决不会是全缓存的。

参考:UNIX环境高级编程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值