UNIX 环境编程 之 文件系统

本文详细介绍了UNIX环境中文件系统的各个关键概念,包括文件描述符、无缓存I/O、标准I/O、文件系统的工作原理,以及进程与文件系统的交互。文件描述符用于标识进程中的文件,无缓存I/O涉及数据如何通过内核缓存到磁盘,而标准I/O带有缓冲区。文件系统通过虚拟文件系统(VFS)为不同底层文件系统提供统一视图。在进程交互中,文件描述符表、文件表和索引节点表起着核心作用。同一文件在不同进程中可能有不同的当前位移量,使用O_APPEND标志可以避免写操作覆盖问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 文件描述符

文字描述符是一个小的非负整数,内核用以标识一个特定进程正在存访的文件。当内核打开一个现存文件或创建一个新文件时,它就返回一个文件描述符。当读、写文件时,就可使用它。

每当运行一个新程序时,所有的 shell 都为其打开三个文件描述符:标准输入、标
准输出以及标准出错。

  • 文件描述符0与进程的标准输入相结合 STDIN_FILENO
  • 文件描述符1与标准输出相结合 STDOUT_FILENO
  • 文件描述符2与标准出错输出相结合 STDERR_FILENO

二 不带缓存的I/O

不带缓存的IO : open , read , write ,lseek 。posix标准,在用户空间没有缓冲,在内核空间还是进行了缓存的。
数据过程 数据-----内核缓存区----磁盘
假设内核缓存区长度为100字节,你调用ssize_t write (int fd,const void * buf,size_t count);写操作时,设每次写入count=10字节,那么你要调用10次这个函数才能把这个缓存区写满,没写满时数据还是在内核缓冲区中,并没有写入到磁盘中,内核缓存区满了之后或者执行了fsync(强制写入硬盘)之后,才进行实际的IO操作,把数据写入磁盘上。

虽然write 系统调用位于C标准库I/O缓冲区的底层,被称为Unbuffered I/O函数,但在write 的底层也可以分配一个内核I/O缓冲区,所以write 也不一定是直接写到文件的,也 可能写到内核I/O缓冲区中,可以使用fsync函数同步至磁盘文件,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别 的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的, 而c标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的.

fsync:是把内核缓冲刷到磁盘上。
fflush:是把C库中的缓冲调用write函数写到磁盘[其实是写到内核的缓冲区]。

三 标准IO (带缓存的I/O)

带缓存区(标准IO):fopen fwrite fget 等,是c标准库中定义的。
数据-----流缓存区-----内核缓存区----磁盘。
假设流缓存区长度为50字节,内核缓存区100字节,我们用标准c库函数fwrite()将数据写入到这个流缓存中,每次写10字节,需要写5次流缓存区满后调用write()(或调用fflush()),将数据写到内核缓存区,直到内核缓存区满了之后或者执行了fsync(强制写入硬盘)之后,才进行实际的IO操作,把数据写入磁盘上。标准IO操作fwrite()最后还是调用无缓存IO操作write.

以fputc 为例,用户程序调用fputc 通常只是写到I/O缓 冲区中,这样fputc 函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲 区中的数据传给内核,内核最终把数据写回磁盘或设备。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时, 不同类型的缓冲区具有不同特性。

  • 全缓冲: 如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。
  • 行缓冲: 如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。
  • 无缓冲: 用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

四 文件系统

一个操作系统可以支持多种底层不同的文件系统(比如NTFS, FAT, ext3, ext4),为了给内核和用户进程提供统一的文件系统视图,Linux在用户进程和底层文件系统之间加入了一个抽象层,即虚拟文件系统(Virtual File System, VFS),进程所有的文件操作都通过VFS,由VFS来适配各种底层不同的文件系统,完成实际的文件操作。如下图所示
在这里插入图片描述
而每一个物理设备上可以包含一个或者多个文件系统,而上图三个分区其实就有三个文件系统,一个文件系统就是一个逻辑设备,每个逻辑设备(或文件系统)由一个逻辑设备号进行标识。
磁盘 分区 文件系统
图 4-1 注意点:

  • 引导块:在文件系统的开头,典型地一般为一个扇区。如果操作系统装在该文件系统中,则该块包含了引导或初始操作系统的引导代码。如果没该文件系统没安装操作系统,则这个块的内容可能是空的。
  • 超级块:文件系统中第一个块被称为超级块。这个块存放文件系统本身的结构信息。比如,超级块记录了每个区域的大小,超级块也存放未被使用的磁盘块的信息。
  • i-节点表:存储文件的属性,比如大小以及所有者之类的。每个i-节点结构的大小一致,而且访问的方式就和数组类似,比如要访问i-节点号为9的i-节点结构,就对应i-节点表中的第10个位置。
  • 数据区:文件的内容就保存在这个区域,磁盘上的所有块的大小都是相同的,如果该文件的内容存储下来不止需要一个块,那就使用多个块进行存储。如果文件很大的话,就需要很多个磁盘块才能存储下来。

在这里插入图片描述
图4-2 注意点:

  • i节点是固定长度记录项,包含着文件信息。在图中有两个目录项指向同一个i节点,每个i节点中都有一个连接计数,只有当节点的连接计数减少为0,才可删除该文件(即释放该文件占用的数据块)
  • 目录项中的 i节点编号数指向的是同一文件系统中的 i节点,所以不能使一个目录项指向另一个文件系统的 i节点。

创建一个文件的过程:
当who > test.txt 创建一个文件时,这整个文件存储过程又是怎么样的呢?
i节点表存放test.txt文件的属性,而test.txt 文件的内容放到数据块中,目录项记录好文件名和该文件名对应的i结点编号。
详细过程:

  • 存储属性 也就是文件属性的存储,内核先找到一块空的i-节点。例如:内核找到i-节点号 50。内核把文件的信息记录其中。如文件的大小、文件所有者、和创建时间等
  • 存储数据 即文件内容的存储, 假设test.txt文件内容需要三块数据块存储,核会从自由块的列表中找到3个自由块,例如 433 288 734 三块自由块,然后会将数据依次存放进 433 288 734 。
  • 记录分配情况 数据存放到433 288 734 数据块中,下次查找时怎么知道数据放入了这三块数据块呢? 因此需要将分配的情况记录到i 结点中的磁盘序列号列表中。(补充,i结点的大小是固定的,导致i 结点中的磁盘序列号列表只能记录大概13个分配项,当数据超过13块时怎么办呢?Linux用到一个间接块来解决此问题.比如我们要记录14个块的编号,可以把前面10个记录在i-节点的磁盘序号列表里。另外4个编号放在一个数据块中。在i-节点的第11项里记录存放编号的数据块的指针,通过这个指针就能找到余下的4个数据块的编号,这个用来存放编号的数据就叫间接块。)
  • 添加文件名到目录, 最后内核将文件名和该文件名对应的i结点编号(50,test.txt)添加到目录文件中。文件名和i-节点号之间的对应关系将文件名和文件和文件的内容属性连接起来,找到文件名就找到文件的i-节点号,通过i-节点号就能找到文件的属性和内容。至此整个文件系统就关联起来了

五 进程和文件系统的交互

在不同进程间共享打开文件,需先了解三种数据结构,分别是用户文件描述符表(user file descriptor table)、文件表(file table)和索引节点表(inode table)。
在这里插入图片描述

  • 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    (a) 文件描述符标志。
    (b) 指向一个文件表项的指针
  • 内核为所有打开文件维持一张文件表。每个文件表项包含:
    ( a) 文件状态标志(读、写、同步、非阻塞等)。
    ( b) 当前文件位移量
    ( c) 指向该文件v节点表项的指针。
  • 每个打开文件(或设备)都有一个 v节点结构。 v节点包含了文件类型和对此文件进行各种操作的函数的指针信息。对于大多数文件, v节点还包含了该文件的 i节点(索引节点)。这些信息是在打开文件时从盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如, i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件在盘上所使用的实际数据块的指针等等。

因此进程想要获取文件系统中某个文件时,会通过打开的文件描述符表,找到文件表项,而文件表项记录着该文件所在的目录项,从而可以从目录项找到i结点,而i结点通过i结点记录的磁盘序列号找到存放文件数据块。

六 同一文件不同表项结构

1.两个独立进程各自打开了同一文件,下图可以发现虽然进程共用相同的i结点,但是确实不同的文件项,因此每个进程都有它自己的对该文件的当前位移量。
不同进程打开的文件
这中间其实会有个问题,当两个进程同时操作一个文件时,就会导致其中一个进程写的数据会覆盖另一个进程写的数据。
假定进程A调用了lseek,它将对于进程A的该文件的当前位移量设置为1500字节(当前文件尾端处)。
然后内核切换进程使进程B运行。进程B执行lseek,也将其对该文件的当前位移量设置为1500字节(当前文件尾端处)。
然后B调用write,它将B的该文件的当前文件位移量增至1600。因为该文件的长度已经增加了,所以内核对v节点中的当前文件长度更新为1600。
然后,内核又进行进程切换使进程 A恢复运行。当A调用write时,就从其当前文件位移量(1500)处将数据写到文件中去。这样也就代换了进程 B刚写到该文件中的数据。

而目前UNIX 提供一种方法,其方法就是在打开文件时设置O_APPEND标志。这就使内核每次对这种文件进行写之前,都将进程的当前位移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。

不过当你需要写的位置不是文件尾端的时候,不能通过标志位设置,只能通过lseek设置 unix 就没提供原子性操作了。

2.dup 或者 一个打开的描述符,后调用fork,情况如下图所示 新文件描述符与参数 filedes共享同一个文件表项

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值