Linux编程入门-文件I/O
Linux编程入门-文件I/O
碎碎念
获取glibc版本:
1 |
|
错误处理:
1 | void perror(const char *s); //打印错误信息 |
通用I/O模型
C语言标准中的文件操作这里略。
open
打开一个已存在的文件或创建并打开一个新文件:
1 |
|
新建文件的访问权限可能还受进程umask和父目录默认访问控制列表的影响。常用文件访问标志如下,其中前三个只能指定一种。
标志 | 含义 |
---|---|
O_RDONLY | 只读方式打开 |
O_WRONLY | 只写方式打开 |
O_RDWR | 读写方式打开 |
O_CLOEXEC | 设置close-on-exec标志 |
O_CREAT | 文件不存在则创建 |
O_DIRECTORY | 若不是目录则失败 |
O_EXCL | 当与O_CREAT结合使用时,若文件已存在则不打开文件并返回错误 |
O_NOCTTY | 若打开的文件为中断设备,则使其不要称为控制终端 |
O_NOFOLLOW | 不对符号链接进行解引用 |
O_TRUNC | 清空文件 |
O_APPEND | 文件尾追加数据 |
O_ASYNC | I/O操作可行时产生信号通知进程,略 |
O_DIRECT | 无系统缓冲 |
O_DSYNC | 提供同步的I/O数据完整性 |
O_NOATIME | read 时不修改文件最近访问时间 |
O_NONBLOCK | 非阻塞方式打开 |
O_SYNC | 同步方式写文件 |
O_PATH | 略 |
常见errno错误如下:
错误码 | 含义 |
---|---|
EACCES | 无法访问文件 |
EISDIR | 打开目录进行写操作 |
EMFILE | 进程已打开的文件描述符数达到进程资源限制上限 |
ENFILE | 文件打开数量达到系统允许的上限 |
ENOENT | 文件不存在且未指定创建标志,或指定路径目录不存在,或为空符号链接 |
EROFS | 文件隶属于只读文件系统,企图写打开文件 |
ETXTBSY | 所指定文件为正在运行的可执行文件,终止后即可 |
大文件支持(LFS)指对2GB以上文件进行读写,此时open
应添加选项O_LARGEFILE标志,否则返回错误。
creat
创建并打开一个新文件,已被open
替代。
1 | int creat( //注意没有“e” |
read
从文件描述符指代的打开文件中读取数据:
1 |
|
write
将数据写入一个已打开的文件中:
1 | ssize_t write( |
close
关闭一个打开的文件描述符,释放回调用进程,供该进程继续使用。进程终止时自动关闭已打开的所有文件描述符。
1 | int close( |
lseek
调整文件偏移量。若某文件包含N字节数据,从0到N-1,则SEEK_SET为0,SEEK_END为N。
1 | off_t lseek( |
常用参照基点如下:
基点 | 含义 |
---|---|
SEEK_SET | 文件头部 |
SEEK_CUR | 当前偏移量 |
SEEK_END | 文件尾部 |
当whence为SEEK_SET时offset必须为非负数,其他时可正可负。常见errno错误码有ESPIPE,表示lseek
不能应用于管道、FIFO、套接字或终端。
当往文件结尾后一段距离处写入数据时,中间的空间称为文件空洞,读取时返回空字节。
ioctl
用于通用模型以外的操作:
1 |
|
文件描述符与文件控制
fcntl
对一个打开的文件描述符执行一系列控制操作:
1 | int fcntl( |
例如获取或修改打开文件的访问模式和状态标志,即在open
时设置的。
1 | int flags=fcntl(fd,F_GETFL),accessMode; |
dup/dup2/dup3
复制一个打开的文件句柄。其中0为标准输入流,1为标准输出流,2为标准错误流。系统保证返回新描述符一定是编号值最低的未用文件描述符。对于dup2
,当目标文件描述符已被占用,则自动先关闭再复制。当dup2
的fd无效,则返回EBADF,不关闭fd2。dup3
的flags常用值有O_CLOEXEC,含义为为新文件描述符设置close-on-exec标志。
1 | int dup( |
例如:
1 | //法一 |
特殊I/O
pread/pwrite
再指定偏移量位置进行文件I/O操作,不改变文件当前偏移量:
1 | ssize_t pread( |
readv/writev
分别实现分散输入和集中输出,这俩都原子操作。iov数组成员个数上限通过sysconf
的_SC_IOV_MAX获取。
1 |
|
preadv/pwritev
在指定文件偏移量处进行分散-集中I/O:
1 | ssize_t preadv( |
truncate/ftruncate
将文件大小设为指定值,不修改文件偏移量。若文件当前长度大于length,则丢弃超出部分,小于则在文件尾部添加一系列空字节或称一个文件空洞。其中truncate
需要路径名字符串,要求组成路径名的各目录拥有可执行权限。ftruncate
需要文件描述符,只需文件写权限。
1 | int truncate( |
临时文件
mkstemp
用mkstemp
生成一个唯一文件名并使用O_EXCL标志,保证调用者以独占方式访问文件。文件拥有者对该函数建立的文件有读写权限,其他用户没有任何权限。
1 | int mkstemp( |
例子有:
1 | int fd; |
tmpfile
创建一个名称唯一的临时文件,用O_EXCL标志以读写方式打开。返回的文件流在关闭后自动删除临时文件。
1 | FILE *tmpfile(void); //成功返回文件流 |
缓冲
setvbuf/setbuf/setbuffer
setvbuf
控制stdio库使用缓冲的形式。当buf不为NULL,,则其指向size大小的内存块作为stream的缓冲区,其中缓冲区需要用malloc
等函数以动态或静态方式在堆中分配空间。当buf为NULL时忽略size参数,stdio库为stream自动分配一个缓冲区,除非选择非缓冲I/O。
1 |
|
type参数指定缓冲类型,常用值如下:
值 | 含义 |
---|---|
_IONBF | 不缓冲。每个stdio库函数立即调用write 或read ,忽略buf和size参数。stderr默认该类型。 |
_IOLBF | 行缓冲。对于输出流,在输出一个换行符前缓冲数据,除非缓冲区已满。对于输入流,每次读一行数据。终端设备流默认该类型。 |
_IOFBF | 全缓冲。单次读写数据大小与缓冲区相同。磁盘流默认该类型。 |
setbuf
和setbuffer
基于setvbuf
实现,分别相当于:
1 | setvbuf(stream,buf,(buf!=NULL)?_IOFBF:_IONBF,BUFSIZ); //BUFSIZ在stdio中一般定义为8192 |
fflush
对于输出流,强制将某stdio输出流中的数据刷新到内核缓冲区中。当stream参数为NULL,则该函数刷新与输出流相关的所有stdio缓冲区。对于输入流,则丢弃已缓冲的输入数据。
1 | int fflush( |
例如当stdin和stdout指向一终端,则从stdin读入输入时,都将先隐含调用一次fflush
。
fsync/fdatasync
同步I/O完成指的是某一I/O操作要么已完成到磁盘的数据传递,要么不成功。同步I/O完成分为同步I/O数据完整性完成和同步I/O文件完整性完成。前者对于读操作而言意味着被请求的文件数据已被从磁盘传递给进程,写操作而言意味着写请求的所有数据和获取数据所需的所有文件元数据属性已传递至磁盘完毕。后者区别在于将所有发生更新的文件元数据都传递到磁盘上,无论在后续读操作中是否需要。
fsync
将使缓冲数据和与打开的文件描述符fd相关的所有元数据刷新到磁盘上,强制使文件处于同步I/O文件完整性完成状态,并只在传递完成后返回。fdatasync
运作类似fsync
,但强制处于同步I/O文件数据完整性完成状态。sync_file_range
当刷新文件时还可指定待刷新的文件区域和标志,以及控制在写磁盘时是否阻塞,这里不讲。
1 |
|
sync
sync
使包含更新文件信息的所有内核缓冲区刷新到磁盘上,包含数据块、指针块和元数据等,仅在所有数据已传递到磁盘或高速缓存时返回。
1 | void sync(void); |
若内容发生变化的内核缓冲区30秒内未经显式方式同步到磁盘上,则一条长期运行的内核线程会确保将其刷新到磁盘上。下面该文件规定了该内核线程刷新前脏缓冲区必须达到的年龄,单位为1%秒。
1 | $ cat /proc/sys/vm/dirty_expire_centisecs |
若open
时指定O_SYNC标志,则使所有后续输出同步,写操作遵从同步I/O文件完整性完成,类似fsync
,即每个write
会自动将文件数据和元数据刷新到磁盘上,此时性能影响极大。不过现代磁盘驱动器内置大型高速缓存,若用以下命令禁用磁盘高速缓存,则性能影响更为极端。
1 | hdparm -W0 |
使用O_DSYNC标志时,写操作遵从同步I/O数据完整性完成,类似fdatasync
。O_RSYNC标志与O_DSYNC或O_SYNC标志配合使用,表示将对写操作的作用同样应用到读操作上。
posix_fadvise
允许进程就自身访问文件数据时可能采取的模式通知内核,内核可根据该函数提供的信息优化对缓冲区高速缓存的使用,提高进程和整个系统的性能。
1 |
|
advice参数的常用值有:
值 | 含义 |
---|---|
POSIX_FADV_NORMAL | 无建议,默认行为。文件预读窗口大小设为默认值128KB。 |
POSIX_FADV_SEQUENTIAL | 进程预计从低偏移量到高偏移量读取数据。将文件预读窗口置为默认值两倍。 |
POSIX_FADV_RANDOM | 进程预计以随机顺序访问数据。禁用文件预读。 |
POSIX_FADV_WILLNEED | 进程预计在不久将来访问指定文件区域。将指定文件区域文件数据预先填充到缓冲区高速缓存中。效果同readahead 。 |
POSIX_FADV_DONTNEED | 进程预计在不久将来不访问指定文件区域。若底层设备未挤满排队的写操作请求,则内核对指定区域中已修改页面刷新并释放该区域高速缓存页面。建议该操作前用sync 或fdatasync 以防设备拥塞而不成功。 |
POSIX_FADV_NOREUSE | 进程预计一次性访问指定文件区域,不复用。内核对指定区域访问一次后即释放,Linux貌似没实现该功能。 |
缓冲I/O总结
在一般情况下,输出时首先通过stdio库函数调用(如printf
、fputc
等)将用户数据传递到stdio缓冲区(位于用户态内存区)。缓冲区填满时stdio库调用write
将数据传递到内核高速缓冲区(位于内核她内存区)。最终内核发起磁盘操作,将数据传递到磁盘。
使用setbuf(stream,NULL);
时,每个stdio库函数自动刷新到用write
。使用fflush();
时,强制将stdio缓冲区内容刷新到用write
。使用open(path,flags|O_SYNC,mode);
时,使每次write
都直接由内核发起写操作。使用fsync
、fdatasync
或sync
时,强制将内核缓冲区高速缓存内容刷新到内核发起写操作。
直接I/O
执行磁盘I/O时操作缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备,称为直接I/O或裸I/O。直接I/O使用方法是在open
时指定O_DIRECT选项。有些非UNIX文件系统可能不支持,如VFAT等。
直接I/O执行时必须遵循一些规则,否则导致EINVAL错误,其中物理块大小通常为512B:
- 文件系统支持直接I/O。
- 缓冲区内存边界必须对齐为块大小整数倍。
- 数据传输的开始点,即文件或设备的偏移量,必须是块大小整数倍。
- 待传输数据长度必须是块大小整数倍。
例如:
1 | int fd; |
fileno/fdopen
系统调用和标准C语言库函数可混合使用。fileno
通过一个文件流返回相应文件描述符,随后可在read
、write
、dup
、fcntl
等系统调用中正常使用该文件描述符。fdopen
通过一个文件描述符创建一个使用该描述符进行文件I/O的流。fdopen
的mode参数含义与fopen
的mode参数相同,且当该参数与文件描述符的访问模式不一致时失败。
1 | int fileno( |
时刻注意I/O系统调用直接将数据传递到内核缓冲区高速缓存,而stdio库函数等到用户空间流缓冲区填满后才传递。