[温故而知新] 《Linux/Unix系统编程手册》——文件I/O

2016-12-02 12:53:03来源:网络收集作者:管理员人点击

第七城市

本文对文件IO这一块做一些梳理,记录思考的一些问题和一些待解决的问题,后续会继续更新。


I hear and I forget,I see and I remember,I do and I understand.


Part 1 :通用IO
/**
相关头文件:


文件IO的几个系统调用
fd = open(pathname, flags, mode)
numRead = read(fd, buffer, count)
numWritten = write(fd, buffer, count)
status = close(fd)
*/

C标准库的函数真是简洁,跟OOP 形成鲜明的对比就是从参数的传递方式,比如open函数,对于flag是通过位运算来进行各种参数的判断,如果是像Java这种比较啰嗦的语言,实现起来估计就会是一个类,然后里面各种方法重载,然后各种参数。当然两种方式各有优缺点。


思考的一些问题:


对于 open函数,返回的是一个文件描述符(file descriptor) , 是一个int型结果,为什么不是返回一个具体的结构体呢?
首先如果让我自己来实现这个系统api,必须有个结构体来记录打开的文件的相关信息,比如当前读取到哪个位置了,文件的路径等。从使用者角度来讲,大多数情况关心的只是如何对文件进行IO,屏蔽掉底层的实现显然是比较合理的。
既然返回的是一个int型的,那么fd可以认为就是个索引而已,内核中必须有个结构来维护进程打开的文件列表。


对于 read函数我们关心的是读了多少数据,这些数据读完放哪,而函数只能有一个返回值,所以buffer作为函数参数传递了。
有个问题待确认,在汇编层面,系统调用中传递的数组参数是如何进行的?//TODO
目前简单的猜测,传递数组实际传的只是个指针,然后在系统调用时切到内核态后,把进程的虚拟地址进行转换为物理地址然后进行IO,而这一步转换是如何进行的?


open调用成功,其返回值为进程未用文件描述符中数值最小者。
原因猜测,一个进程的文件描述符是有限的,所以已经关闭的描述符可以重复利用。


关于 O_CLOEXEC 的flag
//TODO


open函数的O_CREAT 标识,用来在打开文件不存在的时候也进行创建,但是这里有个问题,如果open的时候没有传mode,也就是权限没有传的话,亲测,创建出来的文件的权限是个随机值(书中说的是栈中的随机值,没有具体去考证如何从栈中取值的)。 然而这里为什么不直接返回失败呢?//TODO


open函数的O_CREAT标识,可以用来创建文件,那么为什么不用creat函数呢?
好吧,O_CREAT可以和另一个参数 O_EXCL 配合,达到的效果是,判断文件是否存在,如果存在则调用失败。也就是检查文件存在和创建文件是一个原子操作。
实际上 creat() 等价于
open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode)


系统调用的read(),write() 实际上只是在传递的参数buffer和内核的缓冲区进行数据拷贝而已,并不是实际的通过磁盘IO然后拷贝到buffer中。那么,触发磁盘IO的时机是什么?
对于写操作,如果没有手动刷,内核有个专门的线程干这个事情,检查是否为脏缓冲(超过一定时间,比如30s)是的话就刷缓冲。对于写操作,如果是内核缓冲区满了是不是也刷缓冲?内核的策略是如何的?//TODO


open 的几个参数
O_NOATIME//不修改访问时间,对于一些读操作可以优化IO,因为可以少一次把文件的元数据刷到磁盘的操作
O_NOFOLLOW //对于一些有特权的进程非常有用,不跟随符号链接,保证安全性。
O_ASYNC//TODO
O_NONBLOCK//非阻塞IO,有些类型的文件,open后者后续的读写会造成阻塞,加入这个标志会变为非阻塞,open可能会直接失败返回,而对于读,可能只读了部分数据,对于写呢?//TODO


Part 2 : 文件I/O缓冲
/**
对于标准库的缓冲(stdio的缓冲)
相关的函数有:
fprintf(), fscanf(), fgets(), fputs(), fgetc(), fputc()
这些最终都是通过系统调用read()和write() 进行IO。
设置标准库的缓冲策略相关函数:

int setvbuf(FILE *stream, char *buf, int mode, size_t size)
缓冲策略有三种:
1. 不缓冲_IONBF io no buffer
2. 行缓冲_IOLBFio line buffer 遇到换行符或者缓冲区满就调系统调用
3. 全缓冲_IOFBFio full buffer缓冲区满再调用系统调用
setvbuf两个兄弟
void setbuf(FILE *stream, char *buf) => setvbuf(fp,buf, ( buf!=NULL) ? _IOFBF:_IONBF, BUFSIZ )
也就是缓冲区大小采用stdio.h中定义的默认缓冲区大小,缓冲模式默认为全缓冲
#defnie _BSD_SOURCE
void setbuffer(FILE *stream, char *buf,size_t size);
跟setbuf类似,缓冲模式为全缓冲,缓冲区大小自己配置。
*/

思考的一些问题:


对于文件I/O的内核缓存,对于写缓冲,内核把缓冲刷到磁盘上的策略是什么?
如果程序没有手动调用flush,那么系统内核会有个线程在周期性执行检查然后flush。脏缓冲区能被刷的条件是达到规定的“年龄”(在/proc/sys/vm/dirty_expire_centisecs ,单位为1%秒,一般是30秒),也就是30秒内没有手动刷,系统的一条长期运行的内核线程下次检查到了就会把它刷到磁盘去。


stdio有setvbuf之类的设置缓冲策略的东西,内核呢?如何控制缓冲策略?//TODO


内核用于控制文件IO缓冲的系统调用:
#include
int fsync(int fd);
int fdatasync(int fd);

fsync和fdatasync的区别?
参考:/2014th7cj/d/file/p/20161129/qxzxl2hbrom
简单来说,它们的共同点都是同步操作,需要等磁盘的IO,对于fsync会确保文件的数据和文件的元数据(例如文件的最近访问时间、修改时间等)都同步写完才返回(两次磁盘操作),而fadatasync只保证文件的数据同步写,并不保证文件的元数据同步写(一次磁盘操作)。


针对stdio,强制刷新写缓冲到系统内核的函数: fflush(FILE *stream)


打开一个流同时用于输入和输出,C99两项要求:


一个输出操作不能紧跟一个输入操作,必须在两者之间调用fflush() 函数或者一个文件定位函数(fseek(),fsetpos(),rewind() )。 这里是不是意味着这些定位函数会调用一次fflush()?//TODO
一个输入操作不能紧跟着一个输出操作,必须在二者之间调用一个文件定位函数,除非输入操作已经到了文件结尾。
这两项要求的目的是什么?各种系统的实现如何?(试了下输入后立即输出和 输出后立即输入操作,暂时没发现问题,猜测跟同步问题相关)//TODO

open函数对于缓冲的控制flag
O_SYNC和 O_DSYNC 作用于写操作。
O_SYNC flag, 相当于后续的输出操作,会类似fsync一样,同步写文件的元数据和文件的数据,对性能影响非常大。
O_DSYNC flag, 这个与O_SYNC类似,不过它的语义跟fdatasync类似。
O_RSYNC flag, 作用于读操作,是与O_SYNC和O_DSYNC相结合使用的。具体语义是,如果O_RSYNC|O_DSYNC ,那么在读操作之前,会执行像O_DSYNC一样所有待处理的写操作。
这个标志的使用场景呢?//TODO


I/O缓冲小结,画张图出来//TODO


缓冲有多处,stdio缓冲,内核缓冲,磁盘高速缓冲
对于stdio缓冲,任意时刻可以调用fflush()刷缓冲; 或者在输出之前,通过调用setbuf(stream,NULL)禁用掉stdio的缓冲,然后直接走系统调用。
read,write系统调用,并不是直接进行磁盘IO,而是在读写内核的缓冲区。任意时刻可以调用fsync之类的函数强刷内核缓冲到磁盘。也可以在open的时候设置O_SYNC之类的标志来强刷缓冲。
磁盘的缓冲控制
禁用:hdparam -W0
启用://TODO

裸 I/O: O_DIRECT
O_DIRECT 需定义_GNU_SOURCE
裸I/O看起来好麻烦的样子,看裸I/O的语义,就是可以不经过内核缓冲区,直接经过磁盘DMA进行IO,所以速度应该很慢。但是O_DIRECT和 O_SYNC有什么区别呢?//TODO
参考:/2014th7cj/d/file/p/20161129/dgqsbfk5psy 有这个保证(虽然刷到磁盘上还可能有缓冲)。而O_SYNC是会经过内核缓冲区的,O_DIRECT没有经过内存缓冲区,所以O_DIRECT的使用,需要设置缓冲区,并且有各种内存对齐的要求:


用于数据传递的缓冲区,内存边界必须为block大小(不同环境的block大小不一样)的整数倍。
数据传输的起点,必须是block大小的整数倍。
待传递的数据的长度,必须是block大小整数倍。

posix_fadvise() //TODO
给内核提供建议,优化性能。


Part 3 库函数和系统调用混用
/**
有的函数对于文件传递的是 FILE* 指针,有的是一个int型的文件描述符
#include
int fileno(FILE *stream)
FILE *fdopen(int fd, const char* mode);
两个函数的作用相反。
*/
Part 4 文件操作控制
lseek()函数
off_t lseek(int fd, off_t offset, int whence)
用来定位文件的读写位置,并不是所有类型的文件都支持,比如像 socket,终端就不支持lseek。
lseek() 只是调整内核中与文件相关的文件描述符结构,并没有物理设备访问。
lseek() 的 offset是带符号的,也就是可以从文件最后往前读。

思考的几个问题


为什么是lseek() 而不是 seek()?
返回值是long型。


文件空洞//TODO


lseek() 到文件最后开始写,和open的时候带上O_APPEND的区别?
区别在于O_APPEND能保证原子性的语义,也就是说保证每次写都是从文件的最后开始写。而如果有两个进程同时lseek()到文件最后然后写,有可能导致写覆盖。


intioctl() //TODO 百宝箱
像这种百宝箱类的函数,参数一般都是一个资源、一个cmd、变长的其它参数。


int fcntl(int fd, int cmd, ...)//TODO 又是百宝箱…


读取和修改文件状态标志
能读取的状态标志:
O_SYNC
O_RDONLY
O_WRONLY
O_RDWR
//TODO 还有哪些
O_RDONLY,O_WRONLY,O_RDWR为什么没有与文件状态标志的比特位一一对应,原因很简单,它们有交叉关系……
能修改的标志:
O_APPEND
O_NONBLOCK
O_NOACTIME
O_ASYNC
O_DIRECT
两个进程修改状态标志会相互影响吗?参考下面的文件描述符与文件的关系。

文件描述符与文件的关系
书中一张神图解决所有问题//TODO


如何读写大文件,在32位的机器上,off_t最大是2G
一种推荐做法是定义一个宏,_FILE_OFFSET_BITS 64
然后,之前的IO函数都会变为64位的版本,比如open()->open64()…
所以那些都是宏定义。


创建临时文件的几种种方法:


int mkstemp(char* template)
该调用会加上O_EXCL标志,模版参数类似”/tmp/abcXXXXXX”,内核会替换最后6个X并且保证文件名唯一,如何做到?//TODO
tmpnam(),tempnam(),mktemp() 能用于生成唯一文件名,区别是什么?又为什么说会有安全漏洞?有安全漏洞那么哪些场景下可用?//TODO
FILE* tmpfile(void)
文件流关闭后自动删除该文件,如何做到?//TODO
内部调用unlink()来删除文件名??//TODO
进程退出后自动关闭所有打开的文件描述符,然后就删除临时文件?

一些好用的api //TODO


readv//read vector
writev //write vector
pread//position read
pwrite //position write
第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台