文件流媒体实际上是如何工作的?

17
我一直在想,文件流是如何工作的?通过文件流,我指的是在不将整个文件加载到内存中的情况下访问文件的部分内容。
我知道C ++类(i|o)fstream 正是这样做的,但它是如何实现的?是否可能自己实现文件流?
在最低的C/C++ (或者任何支持文件流的语言)级别上,它是如何工作的?C函数 fopen, fclose, freadFILE*指针是否已经处理了流(即不将整个文件加载到内存中)?如果没有,你将如何直接从硬盘读取,并且是否已在C/C++中实现这种功能?
任何关于正确方向的链接、提示、指导都会非常有帮助。我尝试过谷歌搜索,但似乎谷歌并不完全理解我的问题... Ninja-Edit:如果有人了解如何在汇编/机器代码级别上工作以及是否可以自己实现此操作或是否必须依赖于系统调用,那就太棒了 :) 不是答案的要求,但方向链接会很好。

4
如果想要更详细的回答,建议阅读Andrew Tanenbaum的《操作系统设计与实现》。这是一本非常好的书籍,内容丰富且易于理解。 - André Caron
你必须费些周折才能直接从硬盘上读取数据。内核/操作系统缓存文件系统是有原因的,你几乎不应该规避它。 - Fred Nurk
@Andre:非常感谢你提供的链接,我正好在寻找这样的东西! - Xeo
实际上,如果您想了解概念,那么Tanenbaum的《操作系统》是不错的选择,但如果您想了解实现,我强烈建议阅读《深入理解Linux内核》或《Windows核心内幕》。 - malavv
2个回答

29

在最底层(至少对于用户空间代码),您会使用系统调用。在类UNIX平台上,这些包括:

  • open
  • close
  • read
  • write
  • lseek

...等等。这些调用通过传递名为文件描述符的东西来工作。文件描述符只是不透明整数。在操作系统内部,每个进程都有一个文件描述符表,其中包含所有文件描述符和相关信息,例如它是哪个文件,它是什么类型的文件等。

此外,还有类似于UNIX系统调用的Windows API调用:

Windows使用类似于文件描述符的HANDLE来传递参数,但我认为它们比较不灵活。例如,在UNIX上,文件描述符不仅可以表示文件,还可以表示套接字、管道和其他东西。

C标准库函数fopenfclosefreadfwritefseek仅仅是这些系统调用的包装器。

当你打开一个文件时,通常文件内容并没有被读入内存。当你使用freadread时,你告诉操作系统将特定数量的字节读入缓冲区。这个特定数量的字节可以是文件长度,但也可以不是。因此,如果需要,你可以只将文件的一部分读入内存。

对忍者编辑的回答:

你问这在机器代码级别上是如何工作的。我只能解释在Linux和Intel 32位架构上是如何工作的。当你使用系统调用时,一些参数被放入寄存器中。参数放入寄存器后,会触发中断0x80。例如,要从stdin(文件描述符0)读取1千字节到地址0xDEADBEEF,你可能会使用以下汇编代码:

mov eax, 0x03       ; system call number (read = 0x03)
mov ebx, 0          ; file descriptor (stdin = 0)
mov ecx, 0xDEADBEEF ; buffer address
mov edx, 1024       ; number of bytes to read
int 0x80 ; Linux system call interrupt

int 0x80会触发一个软件中断,通常操作系统已在中断向量表或中断描述符表中注册该中断。无论如何,处理器都会跳转到内存中的特定位置。一旦到达那里,通常操作系统将进入内核模式(如果必要),然后对eax进行类似于C语言中的switch的操作。从那里,它将跳转到read的实现中。在read中,它通常会从调用进程的文件描述符表中读取有关描述符的一些元数据。一旦获取了所需的所有数据,它将执行相应的操作,然后返回给用户代码。

假设它正在从磁盘读取,而不是从管道、stdin或其他非物理位置读取。同时,假设它正在从主硬盘上读取。此外,假设操作系统仍然可以访问BIOS中断。

要访问文件,它需要执行一些文件系统操作。例如,遍历目录树以查找实际文件所在的位置。我不打算详细介绍这个过程,因为我相信你能猜到。

有趣的部分是从磁盘读取数据,无论是文件系统元数据、文件内容还是其他内容。首先,你会得到一个逻辑块地址(LBA)。LBA只是磁盘上一块数据的索引。每个块通常为512字节(尽管这个数字可能已经过时了)。如果我们可以访问BIOS并且操作系统使用它,那么它将把LBA转换为CHS表示法。CHS(柱面-磁头-扇区)表示法是另一种引用硬盘部分的方式。它曾经对应物理概念,但现在已经过时,但几乎所有BIOS都支持它。从那里,操作系统将数据塞入寄存器并触发中断0x13,即BIOS的磁盘读取中断。
这就是我能解释的最低层次了。我相信,在我假设操作系统使用BIOS之后的部分已经过时了。不过,在那之前的所有内容仍然是有效的,我相信,如果不是以简化的方式。

看看我的小忍者编辑和我在另一个答案中的评论。 :) - Xeo
Stack Overflow正在合并我的一些编辑,所以编辑注释可能不够准确。 - icktoofay
关于Windows的HANDLE,它与Unix类似。它们可以表示管道和通信端口。然而,奇怪的是它们不能表示套接字。 - MSalters
@MSalters:我知道它们不能表示套接字,但我不知道它们除了文件之外还能表示其他东西。 - icktoofay

2
在POSIX平台上,打开的文件在用户空间中通过“描述符”来表示。文件描述符只是一个整数,在任何给定时间内,它都是开放文件中唯一的。当请求内核执行某个操作时,该描述符用于标识应将该操作应用于哪个打开的文件。因此,read(0,charptr,1024)从与描述符0相关联的打开文件中进行读取(按照惯例,这可能是进程的标准输入)。
就用户空间而言,加载到内存中的文件部分仅为满足像read这样的操作所需的部分。要从文件中间读取字节,还支持另一种操作-“seek”。这告诉内核重新定位特定文件中的偏移量。下一个read(或write)操作将使用来自该新偏移量的字节。因此,lseek(123,100,SEEK_SET)将文件关联到123的偏移量(我刚刚编造的描述符值)重新定位到第100个字节位置。对123的下一次读取将从该位置开始读取,而不是从文件的开头(或先前的偏移量处)开始读取。未读取的任何字节都不需要加载到内存中。
幕后有一些更复杂的内容-磁盘通常无法读取小于“块”的内容,该块通常是4096左右的二次幂;内核可能会进行额外的缓存和一些称为“预读取”的操作。但这些都是优化,基本思想就是我上面描述的。

添加了一个小的忍者编辑,因为我对“幕后”发生的事情非常感兴趣。 - Xeo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接