打开一个文件实际上是做什么的?
在所有的编程语言(至less我使用)中,您必须先打开一个文件,然后才能读取或写入。
但是这个开放操作实际上是做什么的?
典型function的手册页实际上并没有告诉你“打开一个文件进行读/写”:
http://www.cplusplus.com/reference/cstdio/fopen/
https://docs.python.org/2/library/functions.html#open
很显然,通过使用这个函数,你可以告诉它涉及到创build一些对象来方便访问一个文件。
另一种方法是,如果我要实现一个open
函数,在Linux上需要做什么?
在几乎所有高级语言中,打开文件的函数都是相应的内核系统调用的一个包装。 它也可能做其他花哨的东西,但在当代操作系统中,打开文件必须经过内核。
这就是为什么fopen
库函数的参数或者Python的open
参数与open(2)
系统调用的参数非常相似的原因。
除了打开文件之外,这些函数通常还会设置一个缓冲区,这个缓冲区会被读写操作使用。 这个缓冲区的目的是确保每当你想读取N个字节时,相应的库调用将返回N个字节,而不pipe对底层系统调用的调用是否返回更less。
我实际上并不想执行我自己的function; 只是在了解到底是怎么回事……“如果你喜欢,那就超越语言”。
在类Unix操作系统中,一个成功的调用open
返回一个“文件描述符”,这个文件描述符在用户进程的上下文中只是一个整数。 这个描述符因此被传递给与打开文件交互的任何调用,并且在调用close
之后,描述符变为无效。
重要的是要注意, open
电话就像一个validation点,在这个点上进行各种检查。 如果不是所有条件都满足,则调用失败,返回-1
而不是描述符,错误types在errno
指示。 基本检查是:
- 文件是否存在;
- 调用进程是否有权在指定模式下打开此文件。 这是通过将文件权限,所有者ID和组ID与调用进程的相应ID进行匹配来确定的。
在内核的上下文中,进程的文件描述符和物理打开的文件之间必须有某种映射。 映射到描述符的内部数据结构可能包含处理基于块的设备的另一个缓冲区,或者指向当前读取/写入位置的内部指针。
我build议你通过open()
系统调用的简化版本来看看这个指南 。 它使用下面的代码片段,它代表了当你打开一个文件后在幕后发生的事情。
0 int sys_open(const char *filename, int flags, int mode) { 1 char *tmp = getname(filename); 2 int fd = get_unused_fd(); 3 struct file *f = filp_open(tmp, flags, mode); 4 fd_install(fd, f); 5 putname(tmp); 6 return fd; 7 }
简而言之,下面是代码的function:
- 分配一个由内核控制的内存块,并从用户控制的内存中复制文件名。
- select一个未使用的文件描述符,您可以将其视为一个整数索引到一个可扩展的当前打开的文件列表中。 每个进程都有它自己的这个列表,虽然它是由内核维护的; 你的代码不能直接访问它。 列表中的条目包含底层文件系统将用于从磁盘中提取字节的任何信息,例如inode号,进程权限,打开标志等等。
-
filp_open
函数有实现struct file *filp_open(const char *filename, int flags, int mode) { struct nameidata nd; open_namei(filename, flags, mode, &nd); return dentry_open(nd.dentry, nd.mnt, flags); }
这有两点:
- 使用文件系统查找与传入的文件名或path对应的inode(或更一般地说,文件系统使用的任何内部标识符)。
- 用关于inode的基本信息创build一个
struct file
并返回它。 这个结构成为我之前提到的打开文件列表中的条目。
-
将返回的结构存储(“安装”)到进程的打开文件列表中。
- 释放分配的内核控制的内存块。
- 返回文件描述符,然后将其传递给像
read()
,write()
和close()
这样的文件操作函数。 它们中的每一个都会将控制权移交给内核,内核可以使用文件描述符来查找进程列表中相应的文件指针,并使用该文件指针中的信息来实际执行读取,写入或closures。
如果您觉得雄心勃勃,可以将这个简化的例子与Linux内核中open()
系统调用的实现进行比较,该函数称为do_sys_open()
。 你不应该有任何困难find相似之处。
当然,这只是当你调用open()
时发生的事情的“最上层” – 或者更确切地说,它是在打开文件的过程中调用的最高级别的内核代码片段。 高级编程语言可能会在其上添加额外的图层。 还有很多事情在低层次上进行。 (感谢Ruslan和pjc50的解释。)粗略地说,从上到下:
-
open_namei()
和dentry_open()
调用文件系统代码,它也是内核的一部分,用于访问文件和目录的元数据和内容。 文件系统从磁盘读取原始字节,并将这些字节模式解释为文件和目录树。 - 文件系统使用块设备层 (也是内核的一部分)从驱动器获取这些原始字节。 (有趣的事实:Linux可以使用
/dev/sda
等来访问块设备层的原始数据。) - 块设备层调用存储设备驱动程序(也是内核代码),以从诸如“读取扇区X”的中等级别的指令转换为机器代码中的单独的input/输出指令 。 有几种types的存储设备驱动程序,包括IDE , (S)ATA , SCSI , Firewire等等,对应于驱动器可以使用的不同通信标准。 (请注意,命名是一团糟。)
- I / O指令使用处理器芯片和主板控制器的内置function在发送到物理驱动器的电线上发送和接收电信号。 这是硬件,而不是软件。
- 另一方面,磁盘的固件(embedded式控制代码)解释电子信号以旋转盘片并移动磁头(HDD),或者读取闪存ROM单元(SSD),或访问所需的任何数据该types的存储设备。
由于caching,这也可能有些不正确 。 :-严肃地说,我遗漏了很多细节 – 一个人(不是我)可以写多个书来描述整个过程如何工作。 但是,这应该给你一个想法。
任何你想谈论的文件系统或操作系统都可以。 太好了!
在ZX频谱上,初始化一个LOAD
命令将使系统进入一个紧密的循环,读取audioinput线。
数据开始由恒定的音调表示,之后接着是一个长/短脉冲序列,其中短脉冲是二进制0
,较长脉冲是二进制1
( https://en.wikipedia)。 org / wiki / ZX_Spectrum_software )。 紧密加载循环收集位,直到填充一个字节(8位),将其存储到内存中,增加内存指针,然后循环返回以扫描更多位。
通常情况下,加载程序要读取的第一个内容是一个简短的固定格式头文件 ,至less表示需要的字节数,以及可能的附加信息,例如文件名,文件types和加载地址。 读完这个简短的头文件后,程序可以决定是继续加载大部分的数据,还是退出加载程序并向用户显示适当的消息。
文件结束状态可以通过接收尽可能多的字节来识别(固定的字节数,硬件连接在软件中,或可变数字,如标题中所示)。 如果加载回路没有在预期的频率范围内接收到一段时间的脉冲,则会引发错误。
这个答案的一点背景
所描述的程序从常规audio磁带加载数据 – 因此需要扫描audioinput(它与标准插头连接到磁带录音机)。 LOAD
命令在技术上与open
文件相同 – 但实际上与实际加载文件相关联。 这是因为录音机不受计算机控制,您不能(成功)打开文件但不加载文件。
因为(1)CPU,一个Z80-A(如果是内存服务),真的很慢:3.5 MHz,(2)Spectrum没有内部时钟! 这意味着它必须准确地保持每个T状态 (指令时间)的计数。 单。 指令。 在那个循环内,只是为了保持准确的蜂鸣声时间。
幸运的是,CPU的低速度有一个明显的优势,就是你可以计算一张纸上的周期数,从而计算出他们将要花费的实际时间。
这取决于操作系统在打开文件时究竟发生了什么。 下面我将描述Linux中会发生什么情况,因为它让您知道打开文件时会发生什么情况,如果您对更详细的内容感兴趣,可以查看源代码。 我不覆盖权限,因为它会使这个答案太长。
在Linux中,每个文件都被称为inode的结构识别。 每个结构都有唯一的编号,每个文件只有一个inode编号。 此结构存储文件的元数据,例如文件大小,文件权限,时间戳和指向磁盘块的指针,但不是实际的文件名本身。 每个文件(和目录)都包含文件名称条目和用于查找的inode编号。 当您打开文件时,假设您具有相关权限,则使用与文件名关联的唯一索引节点编号创build文件描述符。 由于许多进程/应用程序可以指向同一个文件,所以inode有一个链接字段,用于维护到文件的链接的总数。 如果一个文件存在于一个目录中,它的链接数是1,如果它有一个硬链接,它的链接数将是2,并且如果一个文件被一个进程打开,则链接数会增加1。
主要是簿记。 这包括各种检查,如“文件是否存在? 和“我有权打开这个文件来写吗?”。
但是这都是内核的东西 – 除非你正在实现自己的玩具操作系统,没有什么可钻研的(如果你是,玩得开心 – 这是一个很好的学习经验)。 当然,你仍然应该学习打开一个文件时可能遇到的所有可能的错误代码,这样你就可以正确地处理它们 – 但这些通常是很好的抽象概念。
代码级别中最重要的部分是它为您提供了打开的文件的句柄 ,您可以使用它来处理您使用文件执行的所有其他操作。 难道你不能使用文件名而不是这个任意的句柄? 那么,当然 – 但使用手柄给你一些优势:
- 系统可以跟踪当前打开的所有文件,并防止它们被删除(例如)。
- 现代操作系统是围绕手柄build立的 – 有很多有用的东西你可以用手柄来做,而且所有不同种类的手柄的performance几乎相同。 例如,在Windows文件句柄上完成asynchronousI / O操作时,句柄会发出信号 – 这允许您在句柄上阻塞信号,或完全asynchronous完成操作。 等待文件句柄与等待线程句柄(例如线程结束时发送信号),进程句柄(再次,当进程结束时发信号通知)或套接字(当某个asynchronous操作完成时)完全相同。 同样重要的是,句柄由它们各自的进程拥有,所以当一个进程意外终止(或者应用程序写得不好)时,操作系统知道它可以释放哪些句柄。
- 大多数操作是位置 – 你从文件的最后位置
read
。 通过使用句柄来识别文件的特定“开放”,可以对同一个文件使用多个并发句柄,每个句柄都从自己的位置读取。 在某种程度上,句柄作为文件的可移动窗口(以及发出asynchronousI / O请求的方式,非常方便)。 - 手柄比文件名小得多。 一个句柄通常是指针的大小,通常是4或8个字节。 另一方面,文件名可以有数百个字节。
- 句柄允许操作系统移动文件,即使应用程序已经打开 – 句柄仍然有效,并且它仍然指向相同的文件,即使文件名已经改变。
还有一些其他的技巧你可以做(例如,在进程之间共享句柄以便不使用物理文件来build立通信通道;在unix系统上,文件也被用于设备和各种其他虚拟通道,所以这不是必须的),但是他们并没有真正与open
操作绑在一起,所以我不打算深入研究。
它的核心,当开放阅读没有什么幻想实际上需要发生。 它所需要做的就是检查文件是否存在,应用程序是否有足够的权限来读取它,并创build一个句柄,在该句柄上可以向文件发出读命令。
在这些命令上,实际的阅读将被分派。
操作系统通常会通过开始读取操作来填充与句柄关联的缓冲区,从而开始读取。 然后,当你真的做了读取,它可以立即返回缓冲区的内容,而不是需要等待磁盘IO。
要打开一个新的文件进行写操作,操作系统将需要在新目录(当前为空)的目录中添加一个条目。 再次创build一个句柄,您可以在其中发出写入命令。
基本上,打开的调用需要find文件,然后logging下需要的内容,以便以后的I / O操作可以再次find它。 这是相当模糊的,但在我立即想到的所有操作系统上都是如此。 具体情况因平台而异。 在这里已经有很多答案讨论现代桌面操作系统。 我在CP / M上做了一些编程,所以我会提供关于它如何在CP / M上工作的知识(MS-DOS可能以相同的方式工作,但出于安全原因,今天通常不这样做)。
在CP / M上,你有一个叫做FCB的东西(就像你提到的C,你可以把它叫做一个结构;它实际上是一个包含不同字段的RAM中的一个35字节的连续区域)。 FCB具有写入文件名的字段和标识磁盘驱动器的(4位)整数。 然后,当你调用内核的打开文件的时候,你把一个指向这个结构的指针放在其中一个CPU的寄存器中。 一段时间后,操作系统返回结构稍有改变。 无论你对这个文件做什么I / O操作,你都会将一个指向这个结构体的指针传递给系统调用。
CP / M对这个FCB有什么作用? 它保留了某些用于自己使用的字段,并使用这些字段来跟踪文件,因此最好不要在程序中触摸它们。 打开文件操作search磁盘开始处的表,以查找与FCB中名称相同的文件(“?”通配符与任何字符匹配)。 如果find文件,它会将一些信息复制到FCB中,包括文件在磁盘上的物理位置,以便随后的I / O调用最终调用可能将这些位置传递给磁盘驱动程序的BIOS。 在这个层面,具体情况各不相同。
简单来说,当你打开一个文件时,你实际上是要求操作系统从辅助存储器加载所需的文件(复制文件的内容)到ram进行处理。 这个(加载文件)的原因是因为你不能直接从硬盘上处理文件,因为它的速度与Ram相比非常慢。
打开命令将生成一个系统调用,该系统调用又将文件的内容从辅助存储器(硬盘)复制到主存储器(Ram)。
我们closures一个文件,因为文件的修改内容必须反映到硬盘中的原始文件。 🙂
希望有所帮助。