linux I/O

read

read返回值
  • 调用返回一个等于len的值. 所有len个被读字节存储在buf中. 结果和预期一致.
  • 调用返回了一个大于0但是小于len的值. 读取的字节存入buf中. 这种情况出现在一个信号打断了读取过程, 或在读取中发生了一个错误, 有效字节大于0, 但是比len少时, 或者在读入len个字节前已抵达EOF. 再次进行读取(更新了buf和len的值)将读入剩余字节到buf的剩余空间中.或者支出问题发生的原因.
  • 调用返回0. 这标志着EOF. 没有可以读入的数据.
  • 调用阻塞了, 因为没有可用的用来读取的数据. 这在非阻塞模式下不会发生.
  • 调用返回 -1, 并且errno被设置为EINTR. 这表示在读入字节之前收到了一个信号. 可以重新进行调用.
  • 读取返回-1, 并且errno被设置为EAGAIN. 这表示读取会因没有可用的数据而阻塞, 而度请求应该在之后重开. 这只在非阻塞模式下发生.
  • 调用返回-1, 并且errno被设置不用于EINTR或EAGAIN的值. 这表示某种更严重的错误.

一个len为0的read()调用的结果是立即返回且返回值为0.

####read在非阻塞模式下

如果文件描述符在非阻塞模式下打开(open()中给定O_NONBLOCK)并且没有可读的数据, read()调用会返回-1, 并且设置errno为EAGAIN而不是阻塞掉.
在进行非阻塞I/O时, 必须检查EAGAIN, 否则将可能因导数据缺失而导致严重的错误

write

wirte的行为

也需要考虑 errno值为EINTR和EAGAIN的情况哦. 同read
当fd在非阻塞模式下打开是, 并且发起的写操作会正常阻塞时, write()系统调用返回-1, 并且设置errno值为EAGAIN. 请求应该在稍后重新发起. 通常处理普通文件时不会出现这种情况.

  • 当一个write()调用返回时, 内核已将所提供的缓冲区数据复制到了内核缓冲区中, 但却没有保证数据已写到目的文件. write调用返回对于这样的情况来讲的确太快了.(ps: 因为CPU和disk速度的差异)
    +而且write返回时,可能所在进程已经结束, 这时数据还在缓冲区中. 内核会尝试将延时写的风险最小化. 内核创立了一个缓存最大时效机制, 并且将所有的脏的缓存在他们超过给定时效前写入磁盘. 用户可以通过/proc/sys/vm/dirty_expire_centiseconds来配置这个值. 这个值以10毫秒计(1%秒)
  • 强制文件缓存写会也可以, 甚至可以将所有的写操作同步. 见同步I/O

同步I/O

fsync()和fdatasync()
#include <unistd.h>
int fsync (int fd);
  • 调用fsync()可以保证fd对应文件的脏数据回写到磁盘上. 文件描述符fd必须是以写方式打开的. 在驱动器确认数据已经完成写入之前不会返回.
  • 但是当数据写入硬盘缓存时, fsync()是不可能知道数据是否已经在磁盘上的. 磁盘可能已经报告说数据已经写入, 但数据可能还在磁盘驱动器的缓存上.
#include <unistd.h>
int fdatasync(int fd);
  • 该系统调用完成的事情和fsync()一样, 区别在于它仅写入数据. 改调用不保证元数据同到磁盘上, 故此可能快一些.
  • ps: 这两个调用都不保证任何已经更新的包含该文件的目录项同步到磁盘上. 这意味着如果文件链接刚刚被更改过, 文件数据可能会成功写入磁盘, 但却没有关联到相应的目录项上, 导致文件不可访问. 所以为保证任何对目录项的更新也同步到磁盘上, 必须对目录本身也调用fsync()进行同步.

返回值与错误码
成功时, 两个调用都返回0. 失败时, 都返回-1, 并将errno设置为一下3个值之一.

  • EBADF 给定的文件描述符不是一个可以写入的合法描述符.
  • EINVAL 给定的文件描述符对应的 对象不支持同步.
  • EIO 在同步时发生了一个底层的I/O错误. 这表示一个真正的I/O错误, 此类错误经常在错误发生出被捕获.
    一般来讲, 及时在相应的文件系统上实现了 fdatasync()而未实现fsync(), 在调用fsync()时也会失败. "偏执"的应用可能会在fsync()返回EINVAL时尝试使用fdatasync(). 特殊的文件系统可能只实现了fdatasync(). (可能没有元数据需要同步的文件类型)
sysnc()

sync()系统调用可以用来对磁盘上的所有缓冲区进行同步, 效率不高, 但被广泛使用.

#include <unistd.h>
void sync(void);

该函数没有参数, 也没有返回值. 它总是成功返回. 并确保所有的缓冲区----包括数据和元数据----都能写入磁盘.
标准中并不要求sync()一直等待所有的缓冲区都写到磁盘才返回. 只是调用它来启动将整个缓冲区都写入磁盘的过程即可. 由此, 一般建议同步多次以确保所有的数据都安全的写入磁盘. 然而对于linux来讲. sysnc()一定要等到所有的缓冲区都写入才返回. 因而调用一次sync()就够了. (ps: 磁盘可能会撒谎, 数据可能只是在磁盘的缓存中.)

O_SYNC标志

O_SYNC标志在open()中使用, 使所有在文件上的I/O操作同步. 如果对此有疑问, 查看上面write的写入哦.
O_SYNC标志则强制将write的返回和数据写入磁盘做关联, 从而保证write()调用进行I/O同步
根据写入文件的大小, 可能会使大量的时间小号在进程的I/O等待时间. 此时的O_SYNC会使总耗时增加1到2个数量级别. 是非常可观的. 所以同步I/O一般是在无计可施情况下的最后选择.
一般情况下, 需要确保数据写入磁盘的请用可以使用fsync()或者fadatasync(). 因为需要较少的调用. 相对于O_SYNC来讲, 开销也更少.

O_DSYNChe O_RSYNC

linux上与O_SYNC同义, 他们有相同的行为.
O_DSYNC标志制定在每次写操作后只有普通数据被同步, 团数据则不同步, 看起来就像在每个写请求后隐式调用fdatasync()一样.(只是看起来). 并且因为O_SYNC提供了更高的保证, 在没有明确支持O_DSYNC时并不会导致功能的缺失.只是在O_SYNC有更强要求的情况下有一点性能损失.
O_RSYNC标志要求读请求像写请求那样进行同步. 因此, 该标志只能和O_SYNC或者O_DSYNC一起使用. Linux中通常无法确知O_RSYNC的行为. 对开发者而言最接近的方式是在每个read()调用后调用fdatasync(). 实际上, 极少需要这种操作. (没看懂, 应该和元数据什么的有关. 看起来linux里面没什么鸟用)

直接I/O

就是想约过linux的I/O管理的层次设计. 并进行独立的I/O管理. 一般数据库系统倾向使用自己的缓存, 以尽可能的减少操作系统的影响.

使用O_DIRECT标志会使内核最小化I/O管理的影响. 使用该标志时, I/O操作将忽略页缓存机制, 直接对用户控件缓冲区和设备进行初始化.所有的I/O将是同步的; 操作在完成之前不会返回.

close

close()调用解除了已经打开的文件描述符的关联, 并分离进程和文件关联. 给定的文件描述符不再有效, 内核可以随意将其作为随后的open()或creat()调用的返回值而重新使用.
close在成功时返回0, 错误时返回-1, 并设置errno为相应值,.
需要注意的是: 关闭文件和文件被写入磁盘没什么关系. 如果应用想保证文件在关闭前写到磁盘, 需要使用"同步I/O"中的同步选项.

  • 错误码:
    一个常见的错误是不检查close()的返回值.除了EBADF(给定的文件描述符不合法), 最重要的错误值是EIO, 这个错误值表明一个可能和实际的close操作并不相关的底层I/O错误.
    尽管POSIX允许, 但close()绝对不会返回EINTR. 这样的实现并不明智.
lseek

一般的, 一个文件中的I/O是现行的.lseek()系统调用能够给定文件描述符引用的文件位置设定指定值.

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t pos, int origin);

lseek()的行为依赖初始参数.

  • SEEK_CURT 当前文件位置fd设置为当前值加上pos, pos可以为负值, 0或正值. 一个为0的pos返回当前文件位置值.
  • SEEK_END 当前文件位置fd设置为当前文件长度加上pos, pos可以为负值, 0或正值. 一个为0的pos设置偏移量为文件末尾.
  • SEEK_SET 当前文件位置fd设置为pos. 一个为0的pos设置偏移量为文件起始.

调用在成功时返回新文件位置. 错误时返回-1 并设置适当的errno值.

off_t ret;
ret = lseek(fd, (off_t) xxx, SEEK_SET);
if (res == (off_t) -1)
    /* error*/

由于lseek()返回更新过的文件位置, 可以用SEEK_CUR 和0值来确认文当前位置.

int pos;
pos = lseek(fd, 0, SEEK_CUR);
if (pos == (off_t) -1)
    /* error */
else
    /* xxxxx*/

文件末尾之后进行查找.
lseek()是可以在文件指针超过文件末尾之后进行查找的.

int res;

ret = lseek (fd, (off_t) 1688, SEEK_END);
if (ret == (off_t) -1)
/*error*/

这种用法来说, 查找到文件末尾之后没什么影响, 到最近文件位置的读hi范围EOF. 但是如果在接下来会有写请求, 贼会在新的就长度之间建立新的空间, 并由0填充.
这种0填充方式成为"空洞"(hole). 在Unix风格的文件系统上, 空洞不占用任何物理上的磁盘空间. 这暗示着文件系统上所有的文件大小加起来可以超过磁盘的物理大小. 带空洞的文件叫做"稀疏文件"(sparse file). 稀疏文件可以节省可观的空间并提升效率, 因为操作那些空洞并不引发任何物理I/O.
一个对文件空洞部分的读请求将返回相应数量的二进制0.

错误码
出错时, lseek()返回-1, 并将errno设置为下面4个值之一

  • EBADF 出给的文件描述符没有指向任何打开的文件.
  • EINVAL origin的值不是SEEK_SET, SEEK_CUR or SEEK_END其中之一, 或者最终计算的文件位置为负数. 事实上出现EINVAL的这种错误都是很不幸的. 前者肯定是一个编译时错误, 而后者可能代表一个隐蔽得多的运行时逻辑错误.
    EOVERFLOW 计算后的文件偏移不能被off_t 表示. 这种情况只会发生在32位架构上. 当前.文件位置会被修改.而这个错误只是表示不能返回这个值.
    ESPIPE 给出的文件描述符关联到了一个不能执行查找操作的对象上, 例如管道, FIFO或套接字.

限制
文件位置的上限值被限定为off_t类型大小. 从内部实现来看, 内核将偏移量存储成C的long long类型. 这个处理方式在64位机器上没有问题, 但是在32位机器上作相应查找时可能产生EOVERFLOW错误.

定位读写

linux提供了两种read() 和 wirte()的变体来替代lseek(), 每个调用都以需要读写的文件位置为参数. 完成时, 不修改文件位置.
读形式的调用为pread():

#define _XOPEN_SOURCE 500
#include <unistd.h>
ssize_t pread(int fd, void *buff, size_t count, off_t pos);

这个调用从文件描述符fd的pos文件位置写count个字节到buf中.
除了他们不管当前文件位置, 这些调用的行为和read(), write()几乎没有区别.他们使用的pos提供的值而不是当前位置. 此外, 在调用完成时, 他们不会修改文件位置. 换句话说, 任何混杂的read()和write() 调用可能破坏了定位读写的结果.

两种定位读写调用都只能用于可以进行定位操作的文件描述符. 从语义角度来讲, 相当于在调用read()或write()前使用lseek()进行定位, 但是仍有3点区别:

    1. 这些调用更加简单易用, 尤其是在文件中做反向移动和随机移动这种技巧性很强的操作时更是如此.
    1. 当操作完成时, 不修改文件位置指针.
    1. 最重要的. 避免了任何使用lseek()时可能出现的潜在竞争. 由于线程共享文件描述符, 可能在一个线程调用了lseek()之后, 但尚未进行读写操作前, 另一个线程修改文件位置. 我们可以用过使用pread()和pwrite()来避免这样的竞争.

错误码
成功时, 两个调用返回读或写的字节数. pread()返回0表示EOF; 而对pwrite(), 一个0返回表明调用没有写任何东西.
出错时. 两者均返回-1, 并设置errno为相应值. 而对pread()而言, 任何对read()或lseek()的errno值都是可能出现的.对pwrite()而言, 任何write()或lseek()的errno值也都是可能出现的.

截短文件

linux提供了两个系统调用来截短文件, 二者都在各类的POSIX标准中定义并(不同程度的)实现

#include <unistd.h>
#include <syd/types.h>
int ftruncate(int fd, off_t len);
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t len);

两个系统调用都将文件截短到len制定的长度.ftruncate()系统调用操作一个打开的并且可写的文件描述符fd. truncate()系统调用操作path制定的一个可写的文件. 二者都在成功时返回0, 错误时返回-1, 并设置errno为相应值.这两个系统调用最常见的用途是将文件截短到比源文件长度小一些. 成功返回时, 文件长度编程len. 之前len和旧长度之间的数据将被忽略, 并不可读取.

他们也可以将文件"截短"到比原长度更长, 类似前面"查找到文件末尾之后" 中查找加上写操作的结合. 扩展出来的字节全部填充为0(空洞文件)
两个操作均不修改当前文件位置.

内核内幕

几种关注三个主要的内核子系统: 虚拟机文件系统(VFS), 也缓存, 和页回写.

  • 虚拟文件系统
    对于系统程序员来说, VFS的影响是很重要的. 程序员不需担心文件所在的文件系统或者介质. 通过系统调用-----read(), write(), 以及其他------能够在任意支持的文件系统和介质上操作文件.
页缓存

页缓存是一种在内存中保存最近在磁盘文件系统上访问过的数据的方式. 内核在接下来对相同数据的后续请求可以直接从内存中读取, 尽量避免重复磁盘访问.

  • 页缓存利用了引用局部性(locality of reference) 的一种方法-------时间局部性(temporal locality), 该方法使刚被访问资源很可能会在不久后再次被访问.
    向磁盘交换一块很少使用的数据, 比如从页缓存中清楚掉一条常常使用的且很可能将在下次重读中使用的数据更有意义(交换允许内核在磁盘上存储数据, 得到比机器的RAM更大的内存空间). 交换和缓存间的平衡可以通过/proc/sys/vm/swappiness来调整. 这个文件可以再0到100间取值, 默认为60. 较高的值表示倾向于在内存中保留页缓存, 较低的值表示更倾向于清理页缓存而不是进行交换.
  • 引用局部性(locality of reference)的另一种形式是空间局部性(sequential locality), 是关于数据的连续使用的性质.基于这个原理, 内核实现了页缓存预读技术. 预读是在每次读请求时从磁盘数据中读取鞥多的数据到页缓存中的动作-------都一点点会很有效. 当内核从磁盘读取一块数据时, 也会读取接下来一两块数据. 一次读取较大的连续数据块时磁盘不需要经常寻道, 所以会比较有效.
    和页缓存类似, 内核管理预读也是动态的. 如果它注意到一个进程持续使用预读来的数据, 内核就会增加预读窗口, 因而预读进更多的数据. 预读窗口最小为16K, 最大为128KB. 反之, 如果内核发现预读没有造成任何有用的命中------就是说, 应用在文件中来回查找而不是连续的读-------他可以完全关闭预读.
    页缓存技术的存在对程序员来讲是透明的.
页回写

像先前在"write()的行为"中讨论的那样, 内核使用缓冲区来延时写操作.
最终"脏"缓冲区需要写入磁盘, 将磁盘文件和内存数据同步, 这就是回写. 以下两个条件会触发回写:

  • 当空闲内存小于设定的阈值时, 脏的缓冲区就会回写到磁盘上, 被清理的缓冲区可能会被溢出, 来释放内存空间.
  • 当一个脏的缓冲区寿命超过设定的阈值时, 缓冲区会被回写至磁盘上, 被清理的缓冲区可能会被移除, 来释放内存空间.

回写由一些叫做pdflush的内核线程操作(推测可能是page dirty flush 之意, 谁知道呢). 当以上两种情况之一出现时, pdflush线程被唤醒, 并开始将脏的缓冲区提交到磁盘, 直到没有处罚条件呗满足. 可能同时有多个pdflush线程在回写. 这么做事为了更好的利用并行性而避免阻塞. 如果来自其他块设备有脏缓冲区存在, 其他pdflush线程会充分利用每一块设备. 这里改良了之前内核的溢出不足: 先前的pdfluash线程(bdflush, 一个单一线程) 可能要消耗掉所有的时间来等待一个块设备, 而同时其他块设备处于空闲. 在一台线代机器上, Linux内核现在可以使多个磁盘维持饱和状态.
在早起的linux内核版本中-----2.4之前-----缓冲子系统和页缓存是分离的, 这样就同时有一个页缓存和一个缓冲缓存. 这意味着数据可以同时在缓冲缓存(作为脏的缓冲区)和页缓存(用来缓存数据)之宗存在. 自然的, 同步这两块缓存需要用一些时间. 在2.4linux内核中引用了统一的页缓存, 这是一个不错的改进.
linux系统编程笔记.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值