操作系统中的文件操作

当我们打开电脑,通过资源管理器查看文件时,可以看到各种属性:文件的名字、大小、创建时间等等。这些文件以一种直观的方式呈现在我们眼前,它们存储在外部存储器中,如硬盘或者SSD中。然而,当我们自己写程序操作这些文件时,这种简单的可视化就不够了。那么,如何用代码和这些文件进行交互?如何实现文件的读取、写入,甚至删除呢?这就是操作系统中文件操作的内容,今天我们将带你深入了解这些背后的机制。

文件系统的差异与抽象

首先,文件系统的机制各不相同,但是只要一个能用的文件系统,文件的创建、读取、写入、删除都表现出相似的操作方式。操作系统的一个重要任务就是抹平这些底层文件系统的差异,为程序提供一种统一的接口(API)来操作文件。比如,无论是在Windows的 NTFS 还是Linux的 ext4 上,函数openreadwrite这些操作都差不多。这意味着我们在编写程序时,不需要过多关心底层是哪个文件系统,除非涉及到一些特定的文件系统专属高级功能(例如Linux的特定文件权限或Windows的NTFS数据流等)。

操作系统通过将底层文件系统的实现细节抽象化,使得应用程序可以方便地使用统一的接口进行文件操作,这样一来,我们可以专注于实现应用程序的逻辑,而不必深入了解底层的文件系统的复杂性。举个例子,NTFS和ext4文件系统虽然在实现上有很多不同,但程序员可以通过相同的openreadwrite函数完成同样的文件操作。这种抽象化也使得程序的跨平台能力大大增强,让程序员可以编写一次代码,便可以在不同平台上运行。

而高级语言通常又提供了自己的文件操作库。操作系统抹平了文件系统的差异,而高级语言又抹平了操作系统的差异,使得编程更加简单。

文件操作的基本流程:打开、读写和关闭

在编程中操作文件,通常的流程是:打开文件读写文件关闭文件。当我们打开一个文件时,往往是通过文件的名字(如"myfolder/myfile.txt")来找到它,然后系统会返回一个文件描述符(File Descriptor, FD)(在 Windows 下叫做文件句柄 Handle,后文仍用 FD 来称呼)。这个描述符实际上是一个整数,它唯一地代表了当前程序中的当前文件,之后对文件的所有操作都要通过这个描述符来进行。

文件的读写操作依赖于这个描述符和一个操作缓冲区(Buffer)。当你准备用 buffer 数组装下文件内容,read(fd, buffer, size)函数将从文件中读取数据;如果要将 buffer 数组写入文件,而write(fd, buffer, size)函数则会将缓冲区的数据写入文件。完成操作后,我们需要通过close(fd)来关闭文件,释放系统资源。

这意味着在整个文件操作过程中,打开、读写、关闭是密不可分的步骤。打开文件相当于向操作系统申请访问权限,并获取一个描述符,读写是通过这个描述符来具体实现数据的传递,最后通过关闭文件来释放占用的资源,确保系统的资源得以回收。每个步骤都有其必要性和作用,缺一不可。

文件读写模式和指针

打开文件时,我们可以在系统调用参数里选择不同的模式,常见的模式包括:

  • r表示只读,文件不存在则失败
  • w表示写入,清空原内容,文件不存在则创建
  • a表示追加写入,不清空原内容,且文件指针默认在文件末,文件不存在则创建
  • r+/w+/a+表示读写,写读,追加。可同时读写性质参考r,w 和 a
  • rb/wb/ab表示二进制读/写,在以上模式后加上 b,表明处理的是二进制文件;与之对应的不加 b 的默认模式(即文本读/写),操作系统进行回车处理等操作。读取一个纯文本文件时,可以通过文本模式来简化换行符的处理,而在读取图片、音频等二进制文件时,则需要使用二进制模式以保证数据的原始格式不被破坏。

在一般情况下,在打开文件获得描述符的同时,也获得了一个隐含的文件指针(File Pointer),它用于标记文件中当前读写的位置。例如,r 和 w 的指针默认在文件开头,随着你进行顺序读写自动向后移动。所以如果你需要正常读全文或顺序写,指针自动移动让你感知不到其存在。

指针的移动也可以手动完成,通过lseek(fd, offset, whence)函数,你可以将指针移动到文件的任意位置。这样就可以随机地访问文件中的内容。这种指针的存在和操作使得文件的访问更加灵活,尤其是在处理大文件时。通过随机访问的方式,可以跳过不必要的数据读取,直接访问感兴趣的部分。

缓冲区与文件读写的关系

逻辑上,open将文件信息加载到内存,readwrite会在主存和外存间传送数据。但实际上,为了效率考虑,调用open函数只会加载一些基本信息,并不会立即将文件的内容加载到内存中,实际上文件的读取是在调用read时逐步完成的。

具体来说,而第一次读取文件时,操作系统会将文件中的一部分数据读入缓冲区(通常是一次性读取比请求的数据更多的数据),这样当程序继续需要读取时,可以直接从内存中取出而无需每次都访问磁盘。

类似地,调用write时,数据并不会立即写回磁盘,而是会先存入内存中的缓冲区(Buffer),稍后再统一写回。这种机制提高了文件操作的效率,而在close时,所有缓冲区数据会正确写回。我们也可以通过调用fsync(fd)等系统调用手动控制缓冲区的刷新,以确保数据及时保存。

缓冲区的存在可以显著提升文件的读写效率,尤其是在频繁的小规模读写操作中。不过,缓冲区的使用也带来了风险,比如在程序异常终止时,缓冲区中的数据可能尚未写回磁盘,从而造成数据丢失。因此,程序员需要谨慎处理,确保在合适的时机刷新缓冲区,并最终调用close来让操作系统确认写回数据保证数据的一致性和完整性。即使如此,断电等也可能导致不一致,你可以手动控制缓冲区的写回,在效率和一致性上自行取舍。

系统如何处理文件的打开、读写和关闭

逻辑上,当我们调用open函数时,操作系统会在磁盘上查找文件,并将文件的基本信息(如大小、权限等)加载到内存中。在执行read操作时,操作系统会将文件的内容逐块复制到内存缓冲区中,而执行write操作则是反向地将缓冲区的数据写入到磁盘。在调用close时,操作系统会确保将所有未写入的数据写回磁盘,并释放相关资源。

多个程序可以同时打开同一个文件,但它们各自的描述符和指针是独立的。系统中维护了一个全局打开文件表(Global Open File Table),其中每个文件只会在首次被打开时加载基本信息到全局表中。当多个程序打开同一个文件时,系统会在全局打开文件表中只保留一份条目,并且与所有打开该文件的程序形成一对多的映射。这样做的好处是减少了文件多次打开的开销,避免了重复读取文件信息。只有当所有打开该文件的程序都调用close关闭文件时,该文件的条目才会从全局打开文件表中移除。

这种设计带来了很多优势,特别是在需要频繁访问同一个文件时。全局打开文件表的存在减少了重复的I/O操作,使得程序对文件的访问更加高效。多个程序共享一个全局条目,而操作系统则负责维护每个程序自己的文件描述符和指针,这样既提高了资源利用率,又确保了不同程序之间的操作不会互相干扰。

多程序对文件的并发访问

如前所述,系统中维护了全局的打开文件表,这使得多个程序可以共享同一个文件的基础信息,而各自拥有独立的文件描述符和指针。这样,尽管不同程序可以共享文件,但它们的读写行为互不干扰,从而提高了文件访问的效率。

多个程序可以同时打开并读取同一个文件,但每个程序都有各自独立的文件描述符和指针。在操作系统层面,open/read/write本身没有限制并发的读写,但由于缓冲区的存在,多个程序同时读写同一个文件时,可能会导致数据的不一致。这种情况下需要程序员自己设计机制(如锁)来防止数据冲突或损坏。

在并发写入时,问题会变得更加复杂。操作系统也不会主动防止两个程序同时写入同一个文件,因此可能会产生竞态条件(Race Condition)。这意味着如果多个程序同时对文件进行写操作,数据的正确性将无法保证,可能会出现覆盖、交叉等错误。因此,程序员在并发环境下进行文件写操作时,需自行设计合理机制。

内存映射文件:mmap与mapviewoffile

除了传统的open/read/write方式,还有一种机制是通过内存映射(Memory Mapping)来操作文件。在Linux中可以通过mmap函数,在Windows中可以通过MapViewOfFile函数实现。这种方式将文件的内容直接映射到进程的内存地址空间中,因此程序可以通过直接访问内存的方式来读取或写入文件。这种机制特别适合于对大文件的随机读写操作,因为它避免了多次系统调用的开销,且在多程序之间共享文件时,可以使得对文件的修改立即对所有映射的进程可见。

内存映射文件的优点在于它简化了对文件的读写操作,特别是在多进程间共享文件的场景下,但它的缺点是对内存的消耗较大,并且不适合对小片段数据进行频繁的读写操作。通过内存映射,程序可以将文件的某一部分或全部映射到内存中,这样对文件内容的访问就如同访问普通内存一样简单快速。此外,在多个进程共享同一个文件时,缓存和回写由 OS 控制,内存映射可以确保一个进程的修改对其他进程立即可见,从而实现高效的数据共享。

不过,内存映射也有其局限性。首先,它对系统内存的消耗较大,尤其是在映射大文件时,可能会占用大量的虚拟内存。其次,对于频繁的小规模写操作,内存映射并不是最佳选择,因为它可能会导致内存和磁盘之间的频繁同步,进而降低效率。因此,程序员需要根据具体的应用场景选择合适的文件操作方式,以达到最佳的性能效果。

posted @ 2024-10-22 15:24  Ofnoname  阅读(39)  评论(0编辑  收藏  举报