【UNIX环境高级编程】—《文件IO》读书笔记

【UNIX环境高级编程】——《文件IO》读书笔记

UNIX环境下的文件共享

文件描述符用来表征一个文件,但是为什么操作系统要用这么一个整数来表征一个文件呢?这就操作系统底层实现有莫大的关系。
  在进程PCB中有着这么一个部分,IO状态信息,说的再具体点,在PCB中存在着一张表,我们可以叫它文件描述符表也可以叫做打开文件描述符表,这张表每个进程都会有且为进程独有,所以它是进程级的。这张表上的每一个表项都有两个部分组成,文件描述符标志以及一个文件指针。其中文件描述符标志也就是我们所使用的文件描述符fd,当然我们也可以将其看做是这张表的下标。这张表长这样。
在这里插入图片描述

这张表中每一项都有一个文件指针,那么这个指针又指向哪里呢?这就要提到另一张表打开文件表,注意这张表由操作系统管理,且系统中只有唯一一张这样的表,因此这张表是系统级的。这张表中的每一项都存储着一个进程与这个文件相关的一些信息,其中主要分为三个部分:文件状态标志,文件当前偏移量,v-node结点指针

文件状态标志就是文件在打开时的状态标志,例如可读,可写,可读写,阻塞等都会记录在其中,这些状态标志也可以使用fcntl函数修改。
  文件当前偏移量就是文件指针当前在文件中指向的位置,我们可以用lseek函数修改。

这张表属于系统级的,系统中任何进程打开任何文件都会在其中添加一个记录项,按照一般情况下来说两个不同的进程打开相同的文件也会在表中创建两个不同的表项,因此两个进程对同一个文件可以有不同的状态标志以及文件当前偏移量,一个进程中不同的文件描述符所代表的文件描述符表项中的文件指针也该指向不同的打开文件表项,但是在某些情况下文件描述符表中不同表项的指针却又有可能指向系统级打开文件表中的同一个表项。例如我们在fork子进程时,子进程复制父进程PCB中的大部分信息包括IO状态信息时会复制文件描述符表,因此两个不同的进程此时就会打开同一个文件,并且文件指针的指向也不会改变会指向相同的打开文件表表项;在使用dup函数重定向时一个进程中不同文件描述符表项中的文件指针也会指向同一个打开文件表中的表项。
  这张表中的每个表项长这样。
在这里插入图片描述

最后还剩一个问题,这个v-node结点指针干嘛用的?v-node节点指针当然指向v-node节点的啊。那么什么是v-node节点?说到v-node就不得不提起i-node节点,在UNIX操作系统中操作系统管理文件的方式是通过使用v-nodei-node节点的方式进行管理的,每个文件都会有这样的节点用于保存相关的文件信息,例如v-node节点上保存了文件类型,对这个文件进行操作的函数指针以及对应的i-node节点的指针;而i-node节点上保存了文件长度,文件数据存储在磁盘的位置,文件所属者等。这些文件信息平时存储在磁盘上,当一个文件倍打开时系统会将这些信息读入内存,并且相同的文件的i-nodev-node节点在内存中只会存在一份。这两个节点长这样。

在这里插入图片描述

那么为什么要用两个节点保存这些信息呢?这是为了在一个操作系统上对多文件系统进行支持。把与文件系统无关的文件信息存储在v-node节点上,其余信息存在i-node上,分开存储,这样的系统也叫做虚拟文件系统。而Linux比较特殊,他其中没有v-node节点而是用了两个不同的i-node节点,但是结果而言大同小异。
  综上所述,把以上集中数据结构连接起来就构成了一个进程对文件进行控制的完整脉络,进程也就得到了和文件控制有关的所有信息,可见并不是所有文件信息都保存在PCB中的。

在这里插入图片描述

对于两个不同的进程打开同一个文件,他们的文件指针可能指向不同的打开文件表项,但是最终都会指向同一个v-nodei-node节点,正如之前所说,相同文件的有关信息在内存中只会存在一份。如下图。

在这里插入图片描述

原子性操作

考虑这么一种场景,两个不同的进程同时打开了一个文件,要对文件进行追加写,但是问题来了,两个进程这里都使用了lseek的方式将当前文件偏移量置为文件末尾处再写,这样的操作并不是一个原子性操作,很有可能导致两个进程同时先将偏移量移到末尾,然后一个写文件结束,另一个再继续在之前的偏移量接着写,这时的偏移量并不在文章末尾,会导致将第一个进程写的数据覆盖。举个例子,假设一个文件目前长度为1500,进程都将偏移量置为了1500,然后第一个线程先写400的数据,之后第二个进程接着准备写400数据,但是第二个进程的偏移量还在1500处,并没有更新为1900,此时再写入数据就会把之前进程写入的数据覆盖。
  以上的问题想要解决也很容易,使用O_APPEND选项,在打开文件加入这个选项后,每次写入数据都会自动将偏移量置为文件末尾处再写,不用lseek保证了原子性.

IO效率问题

首先看以下一段读取文件常用的代码:

#define BUFSIZE 4096
bool Test2()
{
  int n = 0;
  char buf[BUFSIZE];
  while((n = read(STDIN_FILENO, buf, 4096)) > 0)
  {
    if(write(STDOUT_FILENO, buf, n) != n)
    {
      perror("write error:");
      return false;
    }
  }
  if(n < 0)
  {
    perror("read error:");
    return false;
  }
  return true;
}

int main()
{
  if(Test2() == false)
  {
    std::cerr << "copy error" << std::endl;
    return -1;
  }                                        
}

这段代码是很普通的一段从标准输入读取数据写入标准输出的文件读取写入代码,但是其中有一个重要的问题,我们在用文件读取的时候往往需要在程序内开辟一块缓冲区用作数据暂存,问题来了,这块buffer开多大呢?
  这里跟文件系统相关了,我们都知道数据在磁盘上是按照扇区读取的,但是操作系统读取磁盘数据的最小单位是磁盘块,也就是说我们每次读取数据最小都要读取一个磁盘块大小的数据,如果读取数据长度小于磁盘块操作系统也要把整个磁盘块数据先读进来然后再拿其中一部分剩下的丢掉,这样就导致一个问题如果我们读取的数据小于一个磁盘块就会导致效率低下,造成性能浪费,而在Linux操作系统上一个磁盘块大小为4K,所以我们一次读取数据大于等于4K并且为4K整数倍的话效率是最高的。不过现在的操作系统为了提高效率使用了预读技术,这使得不带缓冲的文件IO在使用较小缓冲区读取大的连续存储的文件时也能有较高地效率,我们可以从下图看出:

在这里插入图片描述

下一章节记录IO相关函数。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值