本文为读书摘要(个人认为重要的知识点)
Linux把设备当作一种特殊文件整合到文件系统中。毎个I/O设备都被分配了一条路径,通常在/dev目录下。例如:一个磁盘的路径可能是 “dev/hdl” ,ー个打印机的路径可能是 “dev/lp”,网络的路径可能是 “dev/net”
特殊文件(设备)分为两类,块特殊文件和字符特殊文件。
ー个块特殊文件由一组具有编号的块组成。 块特殊文件的主要特性是:每ー个块都能够被独立地寻址和访问。也就是说,ー个程序能够打开ー个块特殊文件,并且不用读第。块到第123块就能够读第124块。磁盘就是块特殊文件的典型应用。
字符特殊文件通常用于表示输入和输出字符流的设备。 键盘、打印机、网络、鼠标、绘图机以及大部分接受用户数据或向用户输出数据的设备都使用字符特殊文件来表示。访问ー个鼠标的第124块是不可能的(甚至是无意义的)。
每个特殊文件都和一个处理其对应设备的设备驱动相关联。每个驱动程序都通过一个主设备号来标识。
网络的 I/O 则被封装为 套接字,也是通过文件句柄访问
在Linux中I/O是通过ー系列的设备驱动来实现的,每个设备类型对应ー个设备驱动。设备驱动的功能是对系统的其他部分隔离硬件的特质。通过在驱动程序和操作系统其他部分之间提供ー层标准的接口, 使得大部分I/O系统可以被划归到内核的机器无关部分。
当用户访问ー个特殊文件时,由文件系统提供此特殊文件的主设备号和次设备号,并判断它是ー个块特殊文件还是ー个字符特殊文件。
图10-21展示了一部分可以跟不同的字符设备关联的操作。每一行指向ー个单独的 I/O 设备(即ー个单独的驱动程序)。列表示所有的字符驱动程序必须支持的功能。除此之外,还有几个其他的功能。当一个操作要在ー个字符特殊文件上执行时,系统通过检索字符设备的散列表来选择合适的数据结构,然后调用相应的功能来执行此操作。因此,每个文件操作都包含指向相应驱动程序的一个函数指针。
I/O系统被划分为两大部分:处理块特殊文件的部分和处理字符特殊文件的部分。
Linux系统在磁盘驱动程序和文件系统之间设置了一个高速缓存(cache)(减少传输次数)
除了正常的磁盘文件,还有其他的块特殊文件,也被称为原始块文件(raw block file)。这些文件允许程序通过绝对块号来访问磁盘,而不考虑文件系统。它们通常被用于**分页和系统维护**。 swap分区
可加载模块(动态添加驱动模块)是在系统运行时可以加载到内核的代码块。大部分情况下,这些模块是字符或者块设备驱动,但是它们也可以是完整的文件系统.网络协议、性能监控工具或者其他想要添加的模块。
块设备文件是块设备的物理寻址空间;普通文件(内存中)是块设备的虚拟寻址空间。普通文件比块设备文件多一层文件系统的地址转换机构。
Linux的设计非常有意思,因为它忠实地秉承了 “小的就是美好的” (Small is Beautiful)的设计原则。虽然只是使用了最简的机制和少量的系统调用,但是Linux却提供了强大而优美的文件系统。
支持的文件类型
普通的文件
字符特殊文件
建模串行I/O设备,比如键盘和打印机。如果打开并从/dec/tty中读取内容,等于从键盘读取内容,而如果打开并向 /dev/lp 中写内容,等于向打印机输出内容。
块特殊文件
通常有类似于/dev/hdl的文件名,它用来直接向硬盘分区中读取和写入内容,而不需要考虑文件系统。ー个偏移为 k 字节的read操作,将会从相应分区开始的第 k 个字节开始读取,而完全忽略i节点和文件的结构。原始块设备常被ー些建立(如mkfs)或修补(如fsck)文件系统的程序用来进行分页和交换。
当一台机器上安装了多个磁盘的时候,就产生了如何处理它们的问题。
为了使应用程序能够与在本地或远程设备上的不同文件系统进行交互,Linux采用了一个和其他UNIX系统相同的方法:虚拟文件系统。 VFS定义了一个基本的文件系统抽象以及这些抽象上允许的操作集合。调用上节中提到的系统调用访问VFS的数据结构,确定要访问的文件所属的文件系统,然后通过存储在VFS数据结构中的函数指针调用该文件系统的相应操作。
图10∙30总结了VFS支持的四个主要的文件系统结构。
其中,超级块包含了文件系统布局的重要信息,破坏了超级块将会导致文件系统不可读。
每个i节点表示某个确切的文件(inode)。 值得注意的是,在Linux中目录和设备也被当作文件,所以它们也有自己对应的i节点。超级块和i节点都有相应的结构,由文件系统所在的物理磁盘维护。
为了便于目录操作及路径(比如 /usr/ast/bin)的遍历,VFS支持dentry数据结构,它表示一个目录项。 这个数据结构由文件系统在运行过程中创建。目录项被缓存在dentry-cache中,比如,dentry_cache会包含/, /usr, /usr/ast的目录项。 如果多个进程通过同一个硬连接(即相同路径)访问同一个文件,它们的文件对象都会指向这个cache中的同一个目录项。
file数据结构是ー个打开文件在内存中的表示,并且在调用open系统调用时被创建。 它支持read、 write, sendfile、lock等上一节中提到的系统调用。
struct file结构体定义在include/Linux/fs.h中定义。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。如下所示:
fd只是一个小整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。
ext2的磁盘分区包含了一个如图10-31所示的文件系统。块0不被Linux使用,而通常用来存放启动计算机的代码。在块0后面,磁盘分区被划分为若干个块组,划分时不考虑磁盘的物理结构。每个块组的结构如下:
第一个块是超级块,它包含了该文件系统的信息,包括i节点的个数、磁盘块数以及空闲块链表的起始位置(通常有几百个项)。
下ー个是组描述符,存放了位图(bitmap)的位置、空闲块数、组中的i节点数,以及组中目录数等信息,这个信息很重要,因为ext2试图把目录均匀地分散存储到磁盘上。两个位图分别记录空闲块和空闲i节点。
图10-32中的每个目录项由四个固定长度的域和一个可变长度的域组成。第一个域是i节点号,文件 colossal 的i节点号是19,文件voluminous的i节点号是42,目录bigdir的i节点号是88。接下来是rec_len域, 标明该目录项的大小(以字节为单位),可能包括名字后面的ー些填充。
在图10-32b中,我们看到的是文件voluminous的目录项被移除后同一个目录的内容。这是通过增加 colossal 的域的长度,将voluminous以前所在的域变为第一个目录项的填充。当然,这个填充可以用来作为后续的目录项。
**i节点(inode)**被存放在i节点表中,其中i节点表是ー个内核数据结构,用于保存所有当前打开的文件和目录的i节点。i节点表项的格式至少要包含stat系统调用返回的所有域,以保证stat正常运行(见图10-28)。图10-33中列出了i节点结构中由Linux文件系统层支持的ー些域。
考虑ー个shell脚本s,它由顺序执行的两个命令pl和p2组成。
s > x
如果该shell脚本在命令行下被调用,我们预期p1将它的输出写到x中,然后p2也将输出写到ス中,并且从pl结束的地方开始。
当shell生成pl时,ズ初始是空的,从而pl从文件位置。开始写入。然而,当pl结束时就必须通过某种机制使得p2看到的初始文件位置不是〇 (如果将文件位置存放在文件描述符表中,p2将看到0),而是p1结束时的位置。
实现这ー点的方法如图10-34所示。实现的技巧是在文件描述符表和i节点表之间引入ー个新的表, 叫作打开文件描述表,并将文件读写位置(以及读/写位)放到里面。 在这个图中,父进程是shell而子进程首先是p1然后是p2。当shell生成pl时,pl的用户结构(包括文件描述符表)是shell的用户结构的ー个副本,因此两者都指向相同的打开文件描述表的表项。当p1结束时,shell的文件描述符仍然指向包含p1的文件位置的打开文件描述。当shell生成p2时,新的子进程自动继承文件读写位置,甚至p2和shell都不需要知道文件读写位置到底是在哪里。
然而,当不相关的进程打开该文件时,它将得到自己的打开文件描述表项,以及自己的文件读写位置,而这正是我们所需要的。因此,打开文件描述表的重点是允许父进程和子进程共享一个文件读写位置,而给不相关的进程提供各自私有的值。
为了增强文件系统的健壮性,Linux依靠日志文件系统。ext3是ー个日志文件系统,它在ext2文件系统之上做了改进。ext4是ext3的改进,也是ー个日志文件系统,但不同于ext3,它改变了ext3所采用的块寻址方案,从而同时支持更大的文件和更大的整体文件系统。
日志是一个以环形缓冲器形式组织的文件。日志可以存储在主文件系统所在的设备上也可以存储在其他设备上。由于日志操作本身不被日志记录,这些操作并不是被日志所在的ext4文件系统处理的(不被自己处理),而是使用ー个独立的日志块设备(Journaling Block Device, JBD)来执行日志的读/写操作。
JBD支持三个主要数据结构:日志记录、原子操作处理和事务。ー个日志记录描述一个低级文件系统操作,该操作通常导致块内变化。鉴于系统调用(如write)包含多个地方的改动——i节点、现有的文件块、新的文件块、空闲块列表等,所以将相关的日志记录按照原子操作分成组。Ext3将系统调用过程的起始和结束通知JBD,这样JBD能够保证ー个原子操作中的所有日志记录或者都被应用,或者没有一个被
应用。(原子性)
另ー个Linux文件系统是/proc (process)文件系统。
其基本概念是为系统中的每个进程在/proc中创建一个目录。目录的名字是进程PID的十制数值。例如,/proc/619是与PID为619
的进程相对应的目录。在该目录下是进程信息的文件,如进程的命令行、环境变量和信号掩码等。
许多Linux扩展与/proc中其他的文件和目录相关。它们包含各种各样的关于CPU、磁盘分区、设备、 中断向量、内核计数器、文件系统、已加载模块等信息。
由于汇编语言特定于每台机器,将 UNIX 移植到新机器需要用新机器的汇编语言重写整个代码。 另一方面,一旦 UNIX 用 C 语言编写,只有一小部分操作系统(例如 I/O 设备的设备驱动程序)需要重写。
系统调用接口与操作系统内核紧密耦合。 标准化系统调用接口会带来严格的限制(不太灵活)
这允许 Linux 使用 gcc 编译器的特殊功能(如语言扩展),范围从提供快捷方式和简化到为编译器提供优化提示。 主要缺点是其他流行的、功能丰富的 C 编译器(如 LLVM)不能用于编译 Linux。 如果在未来某个时间 LLVM 或其他编译器在各方面都比 gcc 更好,Linux 将无法使用它。 这可能会成为一个严重的问题。
它们是分开的,因此可以重定向标准输出而不影响标准错误。 在管道中,标准输出可能会转到另一个进程,但标准错误仍会写入终端。
负值允许进程优先于所有正常进程。用户不能被信任拥有这样的权力。 它仅适用于超级用户,然后仅在紧急情况下使用。
text 说 nice 值在 -20 到 +19 的范围内,所以默认的静态优先级必须是 120,确实如此。 通过良好并选择积极的良好值,可以请求将进程置于较低优先级。
通常,守护程序在后台运行,执行诸如打印和发送电子邮件之类的操作。 由于人们通常不会坐在椅子边上等待他们完成,因此他们的优先级较低,会占用交互式进程不需要的多余 CPU 时间。
PID 必须是唯一的。 计数器迟早会回绕并回到 0。然后它将向上移动到例如 15。如果恰好发生进程 15 是几个月前启动的,但仍在运行,则 15 不能分配给新的 过程。 因此,在使用计数器选择了建议的 PID 后,必须搜索进程表以查看 PID 是否已在使用中。
当进程退出时,父进程将被赋予其子进程的退出状态。 需要 PID 能够识别父进程,以便可以将退出状态转移到正确的进程。
在这种情况下,一个页面现在可以由所有三个进程共享。 通常,写时复制机制可能会导致多个进程共享一个页面。 为了处理这种情况,操作系统必须为每个共享页面保留一个引用计数。 如果 p1 在以三种方式共享的页面上写入,它会获得一个私有副本,而其他两个进程继续共享它们的副本。
共享文件的进程,包括当前文件指针位置,可以只共享一个打开文件描述符,而无需更新彼此的私有文件描述符表中的任何内容。同时,另一个进程可以通过单独的打开文件描述符访问同一个文件,获得不同的文件指针,并按照自己的意愿在文件中移动。
代码段不能改变,所以它永远不必被分页。如果需要它的框架,它们可以被丢弃。始终可以从文件系统中检索页面。数据段不能被分页到可执行文件,因为它很可能在被引入后已经改变。分页会破坏可执行文件。堆栈段甚至不存在于可执行文件中。
两个进程可以同时将同一个文件映射到它们的地址空间。这为他们提供了一种共享物理内存的方法。共享内存的一半可以用作从 A 到 B 的缓冲区,一半用作从 B 到 A 的缓冲区。为了进行通信,一个进程将一条消息写入它的共享内存部分,然后向另一个进程发出一个信号,表明有一条消息在等待它。回复可以使用另一个缓冲区。
如果两个块不是朋友,这是可能的。 考虑图 10-17(e) 的情况。 有两个新的请求,每个请求八页。 此时,底部的 32 页内存由四个不同的用户拥有,每个用户有 8 页。 现在用户 1 和 2 发布他们的页面,但用户 0 和 3 持有他们的页面。 这会产生使用八页、空闲八页、空闲八页和使用八页的情况。 我们有两个大小相等的相邻块不能合并,因为它们不是伙伴。
分页到分区允许使用原始设备,而无需使用文件系统数据结构的开销。 要访问块 n,操作系统只需将其添加到分区的起始块即可计算其磁盘位置。 没有必要遍历所有原本需要的间接块。