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

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

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

Part 1 :通用IO
/**
相关头文件:
<fcntl.h>
<unistd.h>

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

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

思考的一些问题:

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

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

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

  4. 关于 O_CLOEXEC 的flag
    //TODO

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

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

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

  8. 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。
设置标准库的缓冲策略相关函数:
<stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
缓冲策略有三种:
1. 不缓冲  _IONBF     io no buffer
2. 行缓冲  _IOLBF      io line buffer 遇到换行符或者缓冲区满就调系统调用
3. 全缓冲  _IOFBF      io 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类似,缓冲模式为全缓冲,缓冲区大小自己配置。
*/

思考的一些问题:

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

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

内核用于控制文件IO缓冲的系统调用:
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
  1. fsync和fdatasync的区别?
    参考:http://blog.csdn.net/zbszhangbosen/article/details/7956558
    简单来说,它们的共同点都是同步操作,需要等磁盘的IO,对于fsync会确保文件的数据和文件的元数据(例如文件的最近访问时间、修改时间等)都同步写完才返回(两次磁盘操作),而fadatasync只保证文件的数据同步写,并不保证文件的元数据同步写(一次磁盘操作)。

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

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

    • 一个输出操作不能紧跟一个输入操作,必须在两者之间调用fflush() 函数或者一个文件定位函数(fseek(),fsetpos(),rewind() )。 这里是不是意味着这些定位函数会调用一次fflush()?//TODO
    • 一个输入操作不能紧跟着一个输出操作,必须在二者之间调用一个文件定位函数,除非输入操作已经到了文件结尾。
      这两项要求的目的是什么?各种系统的实现如何?(试了下输入后立即输出和 输出后立即输入操作,暂时没发现问题,猜测跟同步问题相关)//TODO
  4. open函数对于缓冲的控制flag
    O_SYNC和 O_DSYNC 作用于写操作。
    O_SYNC flag, 相当于后续的输出操作,会类似fsync一样,同步写文件的元数据和文件的数据,对性能影响非常大。
    O_DSYNC flag, 这个与O_SYNC类似,不过它的语义跟fdatasync类似。
    O_RSYNC flag, 作用于读操作,是与O_SYNCO_DSYNC相结合使用的。具体语义是,如果O_RSYNC|O_DSYNC ,那么在读操作之前,会执行像O_DSYNC一样所有待处理的写操作。
    这个标志的使用场景呢?//TODO

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

    • 缓冲有多处,stdio缓冲,内核缓冲,磁盘高速缓冲
    • 对于stdio缓冲,任意时刻可以调用fflush()刷缓冲; 或者在输出之前,通过调用setbuf(stream,NULL)禁用掉stdio的缓冲,然后直接走系统调用。
    • read,write系统调用,并不是直接进行磁盘IO,而是在读写内核的缓冲区。任意时刻可以调用fsync之类的函数强刷内核缓冲到磁盘。也可以在open的时候设置O_SYNC之类的标志来强刷缓冲。
    • 磁盘的缓冲控制
      禁用:hdparam -W0
      启用://TODO
  6. 裸 I/O: O_DIRECT
    O_DIRECT 需定义_GNU_SOURCE
    裸I/O看起来好麻烦的样子,看裸I/O的语义,就是可以不经过内核缓冲区,直接经过磁盘DMA进行IO,所以速度应该很慢。但是O_DIRECT和 O_SYNC有什么区别呢?//TODO
    参考:http://stackoverflow.com/questions/5055859/how-are-the-o-sync-and-o-direct-flags-in-open2-different-alike
    目前还没验证答案是否正确,大概的意思是说O_DIRECT并没有保证数据刷到磁盘上函数才返回,而O_SYNC 有这个保证(虽然刷到磁盘上还可能有缓冲)。而O_SYNC是会经过内核缓冲区的,O_DIRECT没有经过内存缓冲区,所以O_DIRECT的使用,需要设置缓冲区,并且有各种内存对齐的要求:

    • 用于数据传递的缓冲区,内存边界必须为block大小(不同环境的block大小不一样)的整数倍。
    • 数据传输的起点,必须是block大小的整数倍。
    • 待传递的数据的长度,必须是block大小整数倍。
  7. posix_fadvise() //TODO
    给内核提供建议,优化性能。

Part 3 库函数和系统调用混用
/**
有的函数对于文件传递的是 FILE* 指针,有的是一个int型的文件描述符
#include <stdio.h>
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是带符号的,也就是可以从文件最后往前读。

思考的几个问题

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

  2. 文件空洞//TODO

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

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

  5. 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
    • 两个进程修改状态标志会相互影响吗?参考下面的文件描述符与文件的关系。
  6. 文件描述符与文件的关系
    书中一张神图解决所有问题//TODO

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

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

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

readv    //read vector
writev   //write vector

pread    //position read
pwrite   //position write
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 1 University students can understand innovation through learning from the past. 2. Students can better review by breaking down complex concepts into smaller components and studying the material in an organized way. 3. When learning from the past to understand innovation, it is important to focus on understanding the big picture and to not get bogged down in the details. ### 回答2: 1. 大学生如何理解故而故而是一种学习方法,它要求我们在学习识之前先回顾和巩固已经学过的识。大学生理解故而意味着要在学习识之前,先回顾和复习以前学过的相关识或基础识。通过故,我们能够加深对已有识的理解和记忆,从而更好地理解和掌握识。 2. 学生如何更好地去复习? 学生要更好地复习,可以采取以下策略: 首先,制定一个合理的复习计划,将要复习的内容分配到不同的时间段,确保每个科目都有足够的时间。 其次,采用多种复习方法,如阅读教材、做练习题、参加讨论等,以帮助加深理解和牢固记忆。 另外,与同学或老师一起讨论复习内容,通过讲解和互动来加深理解。 此外,保持良好的学习习惯,比如及时复习、做好笔记等,能够帮助学生更好地掌握和复习识。 3. 故而的过程需要注意什么? 在故而的过程中,需要注意以下几点: 首先,要有针对性,根据自己的学习需求和复习目标,选择性地回顾和复习相关识点。 其次,要有系统性,将复习内容进行分类整理,形成一个清晰的识框架,有助于加深理解和记忆。 另外,要关注重难点,重点复习那些相对较难或容易遗忘的识点,加强对这些内容的学习和理解。 还要有耐心和恒心,故而是一个持续的过程,需要长期坚持和不断巩固。 最后,要善于总结和归纳,通过整理和回顾复习过程中的笔记和练习,提炼出关键概念和思维模式,便于记忆和应用。 ### 回答3: 1. 大学生如何理解故而? 大学生可以理解为通过回顾过去的识和经验,来获取的见解和理解。故是指回顾已经学过的识,了解其中的原理、概念和重要点。而则是指通过对识的学习,扩展和更自己的识体系。故而相辅相成,是一个持续学习和发展的过程。 2. 学生如何更好地去复习? 学生可以通过以下方式更好地进行复习: - 制定合理的复习计划:根据时间安排和课程难度,合理分配复习时间,确保每个学科都有足够的复习时间。 - 多种复习方法结合:采用不同的学习方式,如阅读教材、做练习题、参与讨论、制作思维导图等,帮助巩固记忆和理解识。 - 主动参与课堂:积极参与讨论和提问,与同学和老师交流,加深对识的理解和记忆。 - 不断反思和总结:及时检查自己的复习情况,发现不足和问题,并及时调整学习方法和计划。 3. 故而的过程需要注意什么? 在故而的过程中,学生需要注意以下几点: - 有目的性地故:针对具体的识点或者问题进行回顾,明确自己的学习目标和重点。 - 理解和记忆结合:不仅要理解概念和原理,还要通过多次的复习和记忆,帮助信息在大脑中形成长期记忆。 - 理论联系实际:将学到的识应用到实际情境中,加深对识的理解和记忆。 - 及时巩固复习成果:通过做练习题、整理笔记、与同学讨论等方式,巩固复习的成果,确保识掌握得更牢固。 - 长期持续学习:故而是一个持续的过程,要保持学习的热情和动力,不断更自己的识体系。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值