计算机可以在各种存储介质(诸如磁盘、磁带和光盘)上存储信息。为了方便使用计算机系统,操作系统提供了信息存储的统一逻辑视图。操作系统对存储设备的物理属性加以抽象,从而定义逻辑存储单位,即文件(file)。文件由操作系统映射到物理设备上。这些存储设备通常是非易失性的,因此在系统重新启动之间内容可以持久。
文件是记录在外存上的相关信息的命名组合。从用户角度来看,文件是逻辑外存的最小分配单元,也就是说,数据只有通过文件才能写到外存。
通常,文件表示程序(源形式和目标形式)和数据。数据文件可以是数字的、字符的、字符数字的或二进制的。文件可以是自由形式的,例如文本文件,或者可以是具有严格格式的。通常,文件为位、字节、行或记录的序列,其含义由文件的创建者和用户定义。因此,文件概念非常通用。
文件信息由创建者定义。文件可存储许多不同类型的信息,如源程序或可执行程序、数字或文本数据、照片、音乐、视频等。文件具有某种定义的结构,这取决于其类型。比如:
文件被命名以方便用户,并且通过名称可以引用。名称通常为字符串,例如 example.c。有的系统区分名称内的大小写字符,而其他系统则不区分。当文件被命名后,它就独立于进程、用户,甚至创建它的系统。
例如,一个用户可能创建文件 example.c,而另一个用户可能通过这个名称来编辑它。文件所有者可能会将文件写入 USB 盘,或作为 email 附件发送,或复制到网络上并且在目标系统上仍可称为 example.c。
文件的属性因操作系统而异,但通常包括:
有些较新的文件系统还支持扩展文件属性,包括文件的字符编码和安全功能,如文件校验和。
所有文件的信息保存在目录结构中,该目录结构也保存在外存上。通常,目录条目由文件的名称及其唯一标识符组成。根据标识符可定位其他文件属性。记录每个文件的这些信息可能超过 1KB 字节。在具有许多文件的系统中,目录本身的大小可能有数兆字节。由于目录(如文件)必须是非易失性的,因此必须存在设备上,并根据需要而被调入内存。
文件为抽象数据类型。为了正确定义文件,需要考虑可以对文件执行的操作。操作系统可以提供系统调用,来创建、写入、读取、重新定位、删除及截断文件。
下面讨论操作系统如何执行这 6 个基本文件操作:
这 6 个基本操作组成了所需文件操作的最小集合。其他常见操作包括:将新信息附加到现有文件的末尾和重命名现有文件。然后,这些基本操作可以组合起来实现其他文件操作。
例如,创建一个文件副本,或复制文件到另一 I/O 设备,如打印机或显示器,可以这样来完成:创建一个新文件,从旧文件读入并写出到新文件。还希望有文件操作,以用于获取和设置文件的各种属性的操作。例如,可能需要操作以允许用户确定文件状态,如文件长度;设置文件属性,如文件所有者等。
以上提及的大多数文件操作涉及搜索目录,以得到命名文件的相关条目。为了避免这种不断的搜索,许多系统要求,在首次使用文件之前进行系统调用 open()。操作系统有一个打开文件表以用于维护所有打开文件的信息。当请求文件操作时,可通过该表的索引指定文件,而不需要搜索。当文件最近不再使用时,进程关闭它,操作系统从打开文件表中删除它的条目。系统调用 create() 和 delete() 是针对关闭文件而不是打开文件而进行操作的。
有的系统在首次引用文件时,会隐式地打开它。当打开文件的作业或程序终止时,将自动关闭它。然而,大多数操作系统要求,程序员在使用文件之前通过系统调用 open() 显式地打开它。操作 open() 根据文件名搜索目录,以将目录条目复制到打开文件表。
调用 open() 也会接受访问模式信息,如创建、只读、读写、只附加等。根据文件权限,检查这种模式。如果允许请求模式,则会为进程打开文件。系统调用 open() 通常返回一个指针,以指向打开文件表的对应条目。这个指针,而不是实际的文件名,会用于所有 I/O 操作,以避免任何进一步搜索,并简化系统调用接口。
对于多个进程可以同时打开文件的环境,操作 open() 和 close() 的实现更加复杂。在多个不同的应用程序同时打开同一个文件的系统中,这可能发生。通常,操作系统采用两级的内部表:每个进程表和整个系统表。每个进程表跟踪它打开的所有文件。该表所存的是进程对文件的使用信息。例如,每个文件的当前文件指针就存在这里,文件访问权限和记账信息也存在这里。
单个进程表的每个条目相应地指向整个系统的打开文件表。系统表包含与进程无关的信息,如文件在磁盘上的位置、访问日期和文件大小。一旦有进程打开了一个文件,系统表就包含该文件的条目。当另一个进程执行调用 open(),只要简单地在其进程打开表中增加一个条目,并指向系统表的相应条目。通常,系统打开文件表为每个文件关联一个打开计数,用于表示多少进程打开了这个文件。每次 close() 递减打开计数;当打开计数为 0 时,表示不再使用该文件,并且可从系统打开文件表中删除这个文件条目。
总而言之,每个打开文件具有如下关联信息:
有的操作系统提供功能,用于锁定打开的文件(或文件的部分)。文件锁允许一个进程锁定文件,以防止其他进程访问它。文件锁对于多个进程共享的文件很有用,例如,系统中的多个进程可以访问和修改的系统日志文件。
文件锁提供类似于读者一写者锁。共享锁类似于读者锁,以便多个进程可以并发获取它。独占锁类似于写者锁,一次只有一个进程可以获取这样的锁。重要的是要注意,并非所有操作系统都提供两种类型的锁,有些系统仅提供独占文件锁。
另外,操作系统可以提供强制或建议文件锁定机制。如果锁是强制性的,则一旦进程获取独占锁,操作系统就阻止任何其他进程访问锁定的文件。
例如,假设有一个进程获取了文件 system.log 的独占锁。如果另一进程(如文本编辑器)尝试打开 system.log,则操作系统将阻止访问,直到独占锁被释放。即使文本编辑器并没明确地获取锁,也会发生这种情况。或者,如果锁是建议性的,则操作系统不会阻止文本编辑器获取对 system.log 的访问。相反,必须编写文本编辑器,以便在访问文件之前手动获取锁。
换句话说,如果锁定方案是强制性的,则操作系统确保锁定完整性。对于建议锁定,软件开发人员应确保适当地获取和释放锁。作为一般规则,Windows 操作系统采用强制锁定,UNIX 系统采用建议锁定。
使用文件锁定,与普通进程同步一样,还是需要谨慎的。例如,程序员在具有强制锁定的系统上开发时,应小心地确保只有在访问文件时才锁定独占文件。否则,他们将阻止其他进程也对文件进行访问。此外,必须采取一些措施,来确保两个或更多进程在尝试获取文件锁时不会卷入死锁。
当设计文件系统(甚至整个操作系统)时,总是需要考虑操作系统是否应该识别和支持文件类型。如果操作系统识别文件的类型,则它就能按合理的方式来操作文件。
例如,一个经常发生的错误就是,用户尝试输出二进制目标形式的一个程序。这种尝试通常会产生垃圾;然而,如果操作系统已得知一个文件是二进制目标程序,则尝试可以成功。
实现文件类型的常见技术是将类型作为文件名的一部分。文件名分为两部分,即名称和扩展,通常由句点分开(表 1)。这样,用户和操作系统仅从文件名就能得知文件的类型。大多数操作系统允许用户将文件名命名为字符序列,后跟一个句点,再以由附加字符组成的 扩展名结束,示例包括 resume.docx、server.c 和 ReaderTliread.cpp。
文件类型 | 常用扩展名 | 功能 |
---|---|---|
可执行文件 | exe, com, binor none | 可运行的机器语言程序 |
目标文件 | obj, o | 已编译的、尚未链接的机器语言 |
源代码文件 | c, cc, java,perl,asm | 各种语言的源代码 |
批处理文件 | bat, sh | 命令解释程序的命令 |
标记文件 | xml, html,tex | 文本数据、文档 |
文字处理文件 | xml, rtf,docx | 各种文字处理程序的格式 |
库文件 | lib, a, so, dll | 为程序员提供的程序库 |
打印或可视文件 | gif, pdf, jpg | 打印或图像格式的 ASCII 或二进制文件 |
档案文件 | rar,zip, tar | 相关文件组成的一个文件,(有时压缩)用于归档或存储 |
多媒体文件 | mpeg, mov, mp3,mp4, avi | 包含音频或 A/V 信息的二进制文件 |
操作系统使用扩展名来指示文件类型和可用于文件的操作类型。例如,只有扩展名为 .com、.exe 或 .sli 的文件才能执行。.com 和 .exe 文件是两种形式的二进制可执行文件,而 .sh 文件是外壳脚本,包含 ASCII 格式的操作系统命令。
应用程序也使用扩展名来表示所感兴趣的文件类型。例如,Java 编译器的源文件具有 .java 扩展名,Microsoft Word 字处理程序的文件以 .doc 或 .docx 扩展名来结束。这些扩展名不总是必需的,因此用户可以不用扩展名(节省打字)来指明文件,应用程序根据给定的名称和预期的扩展名来查找文件。因为这些扩展名没有操作系统的支持,所以它们只能作为应用程序的“提示”。
下面考虑 Mac OS X 操作系统。这个系统的每个文件都有类型,例如 .app (用于应用程序)。每个文件还有一个创建者属性,用来包含创建它的程序名称。这种属性是由操作系统在调用 create() 时设置的,因此其使用由系统强制和支持。
例如,由字处理器创建的文件采用字处理器名称作为创建者。当用户通过在表示文件的图标上双击鼠标以打开文件,就会自动调用字处理器,并加载文件以便编辑。
UNIX 系统采用位于某些文件开始部分的幻数,大致表明文件类型,如可执行程序、shell 脚本、PDF 文件等。不是所有文件都有幻数,因此系统特征不能仅仅基于这种信息。UNIX 也不记录创建程序的名称。UNIX 允许文件名扩展提示,但是操作系统既不强制也不依赖这些扩展名;这些扩展名主要帮助用户确定文件内容的类型。扩展名可以由给定的应用程序采用或忽略,但这是由应用程序的开发者所决定的。
文件类型也可用于指示文件的内部结构。我们直到,源文件和目标文件具有一定结构,以便匹配读取它们的程序的期望。此外,有些文件必须符合操作系统理解的所需结构。
例如,操作系统要求可执行文件具有特定的结构,以便可以确定将文件加载到内存的哪里以及第一条指令的位置是什么。有些操作系统将这种想法扩展到系统支持的一组文件结构,以便采用特殊操作来处理具有这些结构的文件。
让操作系统支持多个文件结构带来一个缺点,操作系统会变得太复杂。如果操作系统定义了5个不同的文件结构,则它需要包含代码,以便支持这些文件结构。此外,可能需要将每个文件定义为操作系统支持的文件类型之一。如果新应用程序需要按操作系统不支持的方式来组织信息,则可能导致严重的问题。
例如,假设有个系统支持两种类型的文件:文本文件(由回车符和换行符分隔的 ASCII 字符组成)和可执行的二进制文件。现在,如果我们(作为用户)想要定义一个加密的文件,以保护内容不被未经授权的人读取,则我们可能会发现两种文件类型都不合适。加密文件不是 ASCII 文本行,而是(看起来)随机位。虽然加密文件看起来是二进制文件,但是它不是可执行的。因此,我们可能要么必须绕过或滥用操作系统文件类型机制,要么放弃加密方案。
有些操作系统强加(并支持)最小数量的文件结构。UNIX、Windows 等都采用这种方案。UNIX 认为每个文件为 8 位字节序列,而操作系统并不对这些位做出解释。这种方案提供了最大的灵活性,但是支持的也很少。每个应用程序必须包含自己的代码,以便按适当结构来解释输入文件。但是,所有操作系统必须支持至少一种结构,即可执行文件的结构,以便系统能够加载和运行程序。
在内部,定位文件的偏移对操作系统来说可能是比较复杂的。磁盘系统通常具有明确定义的块大小,这是由扇区大小决定的。所有磁盘 I/O 按块(物理记录)为单位执行,而所有块的大小相同。物理记录大小不太可能刚好匹配期望的逻辑记录的长度。逻辑记录的长度甚至可能不同。这个问题的常见解决方案是,将多个逻辑记录包装到物理块中。
例如,UNIX 操作系统将所有文件定义为简单的字节流。每个字节可以通过距文件的开始(或结束)的偏移来单独寻址。在这种情况下,逻辑记录大小为 1 字节。根据需要,文件系统通常会自动将字节打包以存入物理磁盘块,或从磁盘块中解包得到字节(每块可为 512 字节)。
逻辑记录大小、物理块大小和打包技术确定了每个物理块可有多少逻辑记录。打包可以通过用户应用程序或操作系统来完成。不管如何,文件都可当作块的序列。所有基本 I/O 功能都以块为单位来进行。从逻辑记录到物理块的转换是个相对简单的软件问题。
由于磁盘空间总是按块为单位来分配的,因此每个文件的最后一块的某些部分通常会被浪费。例如,如果每个块是 512 字节,则 1949 字节的文件将分得 4 个块(2048 字节),最后 99 字节就浪费了。按块(而不是字节)为单位来保持一切而浪费的字节称为内部碎片。所有文件系统都有内部碎片,块大小越大,内部碎片也越大。