本地文件IO一般都是同步阻塞的
本地普通文件IO一般关注的是缓存,一般都是同步阻塞的。普通文件的file descriptor是block也是POSIX标准。这点不同于网络IO,网络IO要考虑传输两边进程处理等,设计之初就提供了带状态检测的异步操作方式,本地文件IO则必然要求高可靠性的。
利用系统调用, unbuffered I/O
不带缓冲指的是每个read
和write
都调用了内核的一个系统调用。这些函数在调用的时候系统调用直接进行了磁盘文件的写入操作。
#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三种类型的缓存
- 全缓存。在这种情况下,当填满标准IO缓存后才进行实际IO操作。对于驻在磁盘上的文件通常是由标准IO库实施全缓存的。
- 行缓存。在这种情况下,当在输入和输出中遇到新行符时,标准IO库执行IO操作。两个限制:第一个是:因为标准IO库用来收集每一行的缓存的长度是固定的,所以只要填满了缓存,那么即使还没有写一个新行符,也进行IO操作。第二个是:任何时候只要通过标准输入输出库要求从(a)一个不带缓存的流,或者(b)一个行缓存的流(它预先要求从内核得到数据)得到输入数据,那么就会造成刷新所有行缓存输出流。
- 不带缓存。标准IO库不对字符进行缓存。相当于用
write
系统调用, 如标准出错流stderr
通常是不带缓存的,这就使得出错信息可以尽快显示出来。
ANSI C要求下列缓存特征:
- 当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓存的。
- 标准出错决不会是全缓存的。
参考:UNIX环境高级编程