Standard I/O
1. Background
在UNIX出现前, 程序必须明确指出链接到哪些输入和输出, 因此, 执行任何I/O操作前, 程序需先获取一系列环境配置和硬件配置, 且每个系统的配置大不相同, 导致程序开发十分困难. UNIX的一切皆文件概念让程序执行I/O操作的成本大大降低, 程序可通过UNIX filesystem访问一切资源, 包括机器服务和外部设备; 借助简化的文件模型, 程序可从一个有序字节序列中读取数据, 直到文件结尾, 无需知道各个设备的底层信息.
UNIX并不是一个操作系统的名称, 而表示一系列操作系统, 当UNIX衍生出不同系统实现时, UNIX变体之间的代码移植变得愈发困难, 因此诞生了POSIX标准: 若程序遵循POSIX标准定义的接口, 则程序可在支持POSIX标准的不同UNIX变体之间移植.
C语言也需要自己的可移植性: ANSI C. 该标准规范了C语言的语法和语义, 还有其标准库, 让C语言适配不同操作系统, 当然包括UNIX.
2. Introduction of Standard
C语言并没有将I/O操作构建到语言中, 而是让其作为外部库. ANSI C将I/O函数统一放置在stdio.h
, 称为Standard I/O; 与之相对, UNIX的I/O操作称为File I/O. 以下是两者的差别:
- File I/O由POSIX Standard制定; Standard I/O由ANSI C制定
- FIle I/O兼容遵循POSIX的UNIX系统实现; Standard I/O兼容主流操作系统, 包含UNIX之外的操作系统
- File I/O的所有操作都需要kernel参与, 因此涉及到user mode与kernel mode的切换; Standard I/O作为一个函数库, 在user mode执行, 必要时需调用File I/O
- Standard I/O引入了许多新概念和特性: Stream, line-by-line input, formatted output, buffered I/O, safe writing
大部分情况下, Standard I/O更适合开发程序, 但开发者仍需学习File I/O:
- 了解File I/O可帮助理解系统概念, 如进程如何管理文件
- 有时Standard I/O无法实现需求, 如获取文件的metadata
3. I/O Stream and FILE Objects
File I/O中, 程序通过file descriptor读取, 修改, 或删除文件; Standard I/O中, 程序通过stream操作文件. Stream不是一个设备或文件, 而是一个线性队列, 可从stream读取一个block的数据, 也可以向stream写入一个block的数据.
Standard I/O支持wide character(宽字符), 也就是说, 每个字符由一个或多byte组成, 而不是ASCII的单byte. 处理单字符的函数称为normal character function(如fread
, fwrite
, fgetc
等), 处理宽字符的函数称为wide character function(如fgetwc
, fgetws
, fputwc
等). normal character function不能与wide character function混用, 否则会发生编码错误.
stream orientation表示stream的字符类型, Standard I/O通过stream orientation判断当前stream的字符类型为normal character或wide character. stream一旦确定stream orientation就无法更改, 以下是指定orientation的三种方法:
- 若stream使用任意normal character function, 该stream确定为not wide oriented
- 若stream使用任意wide character function, 该stream确定为wide oriented
- 使用
fwide
设置orientation
/** |
4. Buffered I/O
对于File I/O, 读取数据的流程如下:
- 用户进程通过
read()
向Kernel发送system call, 从user mode切换到kernel mode - CPU利用DMA(Direct Memory Access) controller将数据从硬盘读取到kernel mode的read buffer
- CPU将数据从read buffer拷贝到user mode的user buffer
- 切回user mode,
read()
执行完毕
若进程频繁调用read()
, 需进行多次磁盘I/O操作, 根据局部性原理, 操作系统引入了Page Cache(页缓存)来减少磁盘访问: 当进程调用read()
时, 会首先检查数据是否在page cache中, 若存在, 则直接从page cache读取; 若不在, 则从磁盘读取, 并将后面的几个页面一并读取到页缓存. 当进程调用write()
时, 会先写入page cache, 由操作系统决定何时刷入磁盘.
Standard I/O的操作函数有自己的stdio buffer, 该buffer处于user mode, 这样避免在user mode和kernel mode之间频繁切换.
stdio buffer分为三种:
- Block buffered: buffer空间耗尽, 进程调用
fflush()
, 或进程退出时执行I/O操作. - Line buffered: buffer空间耗尽, 遇到newline(
\n
), 进程调用fflush()
, 或进程退出时执行I/O操作. - Unbuffered: 不缓存任何数据, 每次调用I/O函数都会执行I/O操作.
stdio buffer具有以下属性:
- stdin和stdout都必须使用fully buffered, 除非他们对应的不是interactive device.
- stderr永不使用fully buffered.
/** |
注意: buffer所能使用的空间小于size
, 因为其中一部分空间要用于存放操作记录. 通常情况下不需要自己申请buffer, 关闭stream时会自动释放buffer. 任何时刻进程都可执行flush:
/** |
setvbuf()
只有在打开stream后, 执行I/O操作前使用.
5. Open a Stream
/** |
以下是不同的mode
参数:
若进程以可读写的方式打开文件, 由于stream使用buffer来缓存数据, 且input与output的切换会导致buffer内容被清空. 为避免数据丢失, 需遵守以下规则:
- 执行output操作(如
fwrite()
)后若进行input操作(如fread()
), 需在两个操作之间执行fflush
,fseek
,fsetpos
, 或rewind
- 执行
input
操作后若进行output操作, 需在两个操作之间运行fseek
,fsetpos
,rewind
. 若input操作已经抵达EOF, 则无需该步骤.
6. Read and Write a Stream
stream有三种读写方式:
- Character-at-a-time I/O: 一次读取或写入一个字符
- Line-at-a-time I/O: 一次读取或写入一行
- Direct I/O: 一次读取或写入固定长度的字符
6.1 Character-at-a-time
读取stream:
/** |
写入stream:
/** |
6.2 Line-at-a-Time I/O
读取stream:
/** |
写入stream:
/** |
7. Binary I/O
有时要读取的数据太长, 导致getc()
读取效率过低; 数据中含有大量null或newline, 导致gets()
无法正确运行.
/** |
8. Position a Stream
Standard I/O提供了ftell()
和fseek()
设置文件位置.
/** |
若文件大小超过long最大值, 则ftell()
和fseek()
无法正确处理, 应避免使用ftell()
和fseek()
, 而使用ftello()
和fseeko()
.
off_t ftello(FILE* stream); |
有些系统的off_t
和long
都为32-bit, 但可通过设置_FILE_OFFSET_BITS = 64
将off_t
改为64-bit.ftello()
和fseeko()
只适合处理ASCII编码的文件, 若文件使用其他编码, 需使用fgetpos()
和fsetpos()
.
/** |
9. Formatted I/O
9.1 Formatted Output
/** |
以下5个函数与上述函数功能相同, 但使用va_list
替代...
int vprintf(const char *format, va_list ap); |
所有printf函数都有一个format
参数, 以%
开始来描述输出的character的格式. 共有四个可选项
%[flags][fldwidth][precision][lenmodifier]convtype |
9.2 Formatted Input
/** |
以下3个函数与上述函数功能相同, 但使用va_list
替代...
int vscanf(const char *format, va_list ap); |
Formatted Input也以%
为起点来描述整个format参数. 共有三个可选项
%[*][fldwidth][m][lenmodifier]convtype |
10. Temporary Files
临时文件有三个用途:
- 内存不足时, 使用临时文件放置
- 写入数据大于系统的地址空间时, 使用临时文件放置
- 用于进程间的数据通信
/** |
需要注意的是, 如果我们使用tmpnam()
后, 使用其返回的文件名调用open()
创建文件, 可能有其他进程创建同名文件, 因此需设置为open(filename, O_CREAT | O_EXCL | O_NOFOLLOW)
.
/** |
mkdtemp()
创建的文件夹的权限为0700
mkstemp
创建的文件的权限为0600
11. Memory Streams
Standard I/O会将数据缓存在buffer来加快处理速度, 且进程可创建自己的buffer. Single UNIX Specification Version 4后提供了memory streams来避免使用底层存储设备, 对于FILE stream的所有操作都在内存发生. 从而更快的处理数据.
/** |
相对于file-based standard I/O streams, memrory stream有几点不同:
- Memory stream的type设置为append后, file position会设置为buffer中的第一个NULL byte的位置. 若buffer中没有NULL byte, 则以buffer最后一个字节为file position.
- 若buf为NULL, 则type不应设为只读或只写. 因为fmemopen()得到的stream无法获取buffer的地址, 所以写入的数据永远无法读取. 同理, buffer读取的内容永远无法修改或添加.
- 当调用fclose(), fflush(), fseek(), fseeko()或fsetpos()时会在当前file position处添加一个NULL byte.
/** |