第八章 文件系统
第一节 介绍
这一章将要描述Linux内核在文件系统内维护其支持的文件。它描述了虚拟文件内存(VFS),并且解释了内核是如何支持真实文件系统。
其中一个重要的特点就是它支持很多不同的文件系统。这是它变得非常可变,比较容易和其它的操作系统共同生存。Linux支持很多的文件系统;ext, ext2,ext3,xia,minix,umsdos,msdos,vfat,proc,smb,ncpfs,iso9660,sysv,hpfs,affs,etc.还有很多呢。
在Linux和Unix,这些分别的文件系统中有一些可能不能被设备识别器识别访问(比如驱动器号码或者驱动名称)但是它们被组成单一的分层的树结构代表文件系统是一个完整的单一整体。Linux添加每一个新安装的文件系统到单文件系统树。所有的文件系统,不管是哪个种类,都被安装到这个目录, 安装的文件系统也都覆盖了当前的目录以及内容。这个目录就称为安装目录或者安装点。当文件系统写在的时候,安装目录自己的文件就被再一次的显示出来。
当磁盘被初始化的时候(比如使用fdisk)就会有一个分区结构,把物理磁盘分成几个逻辑分区。每一个分区都有一个单文件系统,比如EXT2文件系统。文件系统整理文件连同目录到逻辑单一结构中,软连接等等在物理设备快上的。那些可以包括文件系统的第一个系统IDE磁盘驱动,是一个块设备。Linux文件系统认为这些块设备就是简单的线性的快集合,它们不知道,也不关心物理内存的空间。这就是每个块设备驱动的任务,去映射需求读取特定的设备快到另一个设备;这个硬盘的特定的跟踪,扇区,圆柱块设备。一个文件系统要不论哪一个设备保存它,都要用一个方法查找,感觉并且操作它。还有,使用Linux文件系统,不论这些不同的文件系统在不同的物理媒体,被不同的硬件控制。这些文件系统有可能都不在本地系统,可能是远程安装的一个硬盘。
文件在文件系统中就是一个数据的集合。一个文件系统不仅仅有数据,还包括文件系统的文件,还有文件系统的结构。它有Linux用户和进程的所有信息,软链接目录,文件保护信息等等。而且它还安全的保管这些信息,基本的操作系统完整性就建立在它的文件系统之上。没有人会使用一个经常丢失数据和文件操作系统。
Minux第一个Linux拥有的文件系统,它的表现严谨但是有很多不足。
它的文件名不可以多于14个字符(但是比8.3文件名要好一点)最大量存储文件是64MB。64MB可能一眼看来够了,但是一些比较大的文件,比如数据库文件的保存也非常重要。第一个文件系统就是为Linux设计的,扩展的文件系统,或者EXT,在1992年4月发明,解决了很多问题,但是在表现上还是有很多不足。
所以在1993年,第一版扩展文件系统,EXT2诞生了。
这个文件系统在这个章节末尾有详细的介绍。
一个重要的发展再EXT文件系统添加到Linux的时候诞生。真实文件系统从操作系统和操作服务分开,通过界面分层,也就是虚拟文件系统VFS.
VFS允许Linux支持很多,不同的文件系统,每一个都展示了一个VFS共同的软件界面。所有的Linux文件系统细节都被转换为软件,所以所有的文件系统就可以识别其它的Linux内核和系统运行程序。Linux虚拟文件系统层次也允许你在表面上在同一时间安装很多不同的文件系统。
Linux虚拟文件系统地实现,使访问文件变得快速有效。它必须要确定文件及其数据正确无误的保存。这些需要可能彼此感到奇怪。Linux VFS从每个安装的文件系统安装信息在内存里面。必须注意正确升级文件系统,所以在文件系统目录以内的数据才会安全。如果你可以看到运行的内核中的文件系统的数据结构,你就可以描述被访问的文件和目录。这些安装中最重要的就是缓存安装,完整的一个独立文件系统访问到重要的快设备。因为块被访问,它们在缓存安装并且被保存在不同的基于它们状态的的队列中。缓存安装不是仅仅安装数据缓存,还帮助和块设备驱动器管理同步界面。
第二节 VFS
图8.1显示了Linux内核的虚拟文件系统和真实文件系统之间的关系。虚拟文件系统必须管理所有不同任何时间安装的文件系统。这样来保存虚拟文件系统和真实,安装的文件系统内的数据结构。
这并不复杂,VFS用和EXT2文件系统使用的方法一样描述了系统文件。就像EXT2,VFS在系统内描述了文件和目录;虚拟文件系统的内容和拓扑。从现在开始避免混淆,我要写一些关于VFS和VFS 超级块(superblock)来分辨它和EXT2。
每一个文件系统的初始,它都和VFS注册。这在操作系统初始化自己在系统启动时间。真实的文件系统是建立在内核之内的或者建立成可加载的模块。文件系统模块是在系统需要它的时候可加载的,所以如果VFAT文件系统是一个内核模块,所以它旨在VFAT文件系统安装的时候加载。当一个基于块设备的文件系统安装的时候,这就包括了root文件系统,VFS必须读取超级块。每个文件系统的种类的超级块堵去历程必须要作出文件系统的拓扑,并且映射信息到VFS 超级块数据结构。VFS保持一个安装的文件系统的表格在系统中和它的VFS 超级块。每一个VFS 超级块都包括信息和指针到历程表现特定的功能。所以如果一个超级块代表了一个安装的EXT2文件系统包括一个指针到EXT2特定点读取历程。这个EXT2项所有的文件系统读取历程一样,填满VFS领域。每一个VFS 超级块包括一个指针到VFS点在文件系统。Root文件系统是一个代表为“/”的目录。这个映射信息非常有效地给EXT2文件系统但是少于其它的文件系统。
因为系统进程访问目录和文件,系统里程被调用VFS。
比如,键入ls给一个目录或者cat一个文件, 因为虚拟文件系统查找VFS代表的文件系统。每一个文件和目录在系统之内都代表了VFS,所以一些就不停的被访问。这些点被保存在索引节点(Inode)安装中,使访问它们更加快速。如果一个索引节点不是再索引节点 缓存中,那么文件系统特别历程就一定要在读取合适的索引节点之后才能去调用。这些读索引节点把它放在索引节点 缓存中的动作,更加访问索引节点保存在缓存中。很少使用的VFS就要从缓存中被删除。
所有的Linux文件系统使用一般的缓存缓存来缓存数据缓存,从重要的设备来帮助提高所有文件系统到保存文件系统的物理设备访问速度。
这个缓存独立于其它的文件系统,是一个整体的机械,Linux内核使用分配和读取,写入数据缓存。有特定的优势使Linux文件系统独立于支持它的重要的媒体和设备驱动。所有的快结构的设备和Linux内核和当前的块在一个界面下寄存。即使是相关的复杂块设备如SCSI设备服务于它。真实文件系统从重要的物理硬盘读取数据,这个结果需要块设备驱动器从它们控制的设备读取物理块数据。独立到块设备界面的是缓存。文件系统读取块,保存在全球所有的Linux内核系统文件的共享缓存中。其中的缓存被它们的块号码和设备缓存独立识别器识别。所以如果相同的数据经常需要,就需要从缓存而不是硬盘中读取,就需要更长的时间。一些设备支持读取数据块,以备不时之需。
VFS还保存了缓存目录,因为索引节点经常要读取目录,就可以快速找到。
根据你的经验,使这列出一个你还没有列出的目录。第一次列出的时候,你可能会注意到一个小的展厅,但是第二次列出它的内容的时候,就比较流畅。目录缓存不帮助目录存储索引节点s;这些要在索引节点缓存中保存,目录缓存就是简单的存储完整目录名称和它们的索引节点号码的映射。
1. VFS 超级块
每一个安装的文件系统都代表为VFS 超级块; 在其它的信息当中,VFS 超级块包括:
设备:这是一个设备识别器,给包括文件系统的块设备识别器。比如/dev/hda1,第一个IDE硬盘在这个系统中有设备识别器0x301,
索引节点 pointers: 安装了的索引节点指针指向第一个文件系统的索引节点。Covered 索引节点指针指向索引节点代表目录,安装了的文件系统。Root文件系统的VFS 超级块没有covered 指针。
Blocksize: 文件系统的块大小,比如1024比特。
超级块 操作:一个指针指向一个文件系统的超级块历程。在其它事务中间,这些历程用来VFS去读些索引节点和超级块.
文件系统种类:指针指向安装的文件系统的file_system_type数据结构。
特定的文件系统:指针指向文件系统需要的信息。
2. VFS 索引节点
就像EXT2文件系统一样,每一个文件,目录什么的都在VFS中代表一个唯一的VFS 索引节点.
在每一个VFS 索引节点的信息中,从重要文件系统通过文件系统特别里程来建立VFS信息。VFS 索引节点s只是存在在内核内存当中,并且保存在VFS 索引节点缓存中,只要它们对系统有用。在其它的信息中,VFS 索引节点包括以下领域:
设备:这个是由文件或者VFS 索引节点s代表的任何文件的设备驱动识别器。
索引节点号码:这个是系统中索引节点唯一号码。设备和索引节点 号码的集合,是虚拟文件系统中的唯一号码。
模式:如同EXT2一样,这个领域描述了VFS 索引节点代表了访问权限。
用户身份:用户的身份识别。
频率:创建,修改和写入的次数。
块大小:文件的块大小,比如1024比特。
索引节点操作:一个指针指向历程地址块。这些历程时特别的文件系统而且它们的表现操作给这个索引节点,比如截断代表这个索引节点的文件。
计数:当前使用这个VFS 索引节点的系统原件数量。0的意思就是索引节点空闲。
Lock: 这个领域用来锁住VFS 索引节点, 比如当它被文件系统读取的时候。
Dirty: 表示这个VFS 索引节点被写入,如果是这样,重要文件系统就需要修改。
文件系统特别信息
3. 在虚拟文件系统查找一个文件
在虚拟文件系统中查找一个VFS 索引节点, VFS必须要同时解析和命名一个目录,查找代表每个中级目录的VFS 索引节点的名称。每个目录查找都包括调用文件系统特殊查找某个代表VFS 索引节点中父目录的保存地址。这个工作因为我们总是有VFS 超级块系统。每次真实文件系统查找一个索引节点的时候,都要查找缓存中的目录。如果没有这个目录的缓存项,真实文件系统就从重要文件系统或者索引节点缓存中得到VFS 索引节点。
4. VFS 索引节点缓存
安装的文件系统导航,它们的VFS 索引节点也继续读取,在某些情况下写入。虚拟文件系统维护一个索引节点缓存来加速安装文件系统的访问速度。每一次一个VFS 索引节点从索引节点 缓存读取的时候,系统就保存访问到一个物理设备当中。
VFS 索引节点缓存实现hash表格项是指到VFS 索引节点的指针,得到同样的hash值。这个索引节点从它的索引节点号码和设备识别器的物理设备中得到hash值。不管什么时候虚拟文件系统需要访问到一个索引节点,就从计算hash值然后使用它在索引节点 hash表格中。这就给了指针一个有同样hash值的索引节点s列表。从每一个索引节点读取直到找到一个同样的索引节点数字,同样它寻找的设备识别器。
可以在缓存中找到索引节点,计数的增加就是表示它有另外一个用户,文件系统访问历程。否则一个空闲的VFS 索引节点必须被找到才可以是文件系统从内存当中读取数据。VFS有选择数字关于如何得到一个空闲的索引节点。如果系统分配更多的VFS 索引节点s;它分配内和叶面并且把它们变成新的,空闲的索引节点s并且方它们在索引节点表格当中。所有的系统VFS 索引节点都在一个first_索引节点表格中,就像hash表格一样。如果系统已经有所有系统允许的索引节点s,它就必须找到一个索引节点可以修正。那些有用的计数为零的索引节点是不错的选择;这些是系统非当前使用的。真正重要的VFS 索引节点s,比如文件系统root 索引节点s总是有大于零的计数值,所以从来没有可修正的。当修正被放置好并且清理过后。VFS 索引节点可能就会成为dirty这样就需要从新写入文件系统或者锁在这个状况,系统就要在继续之前等待解锁。候选的VFS 索引节点必须要先清理才可以被修正。
然而找到新的VFS 索引节点的时候,一个文件系统特殊历程就被调用填充从重要真实文件系统读取的信息。在填满之后,新的VFS 索引节点就有了技术的功能并且锁在访问,直到有有效的信息。
得到VFS 索引节点,文件系统就需要访问几个其它的索引节点s.这就是当你读一个目录的时候;只有索引节点到最终的目录,但是索引节点到中级目录的时候就要被读取。就像VFS 索引节点缓存被使用和填充,更少使用索引节点s就会被删除,多余的索引节点s就会被保存在缓存当中。
第三节 EXT2 文件系统
第二版的扩展文件系统作为可扩展的有利文件系统发明。它也是目前在Linux社区中文件系统中最成功的,而且是所有当前Linux供应商的基础。
EXT2文件系统,像其它的文件系统一样,提前建立数据保存在文件数据块中。这些数据块都是同样长度的,尽管它们的长度可以从EXT2文件系统块大小到特殊EXT2文件系统创建时候的大小之间。每个文件的大小是根据独立快的书量决定的。如果一个快的大小是1024比特,那么1025比特的文件就会占用两个1024比特的块。这就意味着你平均浪费了每个文件块的一半空间。一般计算CPU的内存使用和硬盘空间利用。在这个案例中,Linux和大多数操作系统都销售一个相关的步态有效的硬盘作为降低CPU工作强度.不是所有的文件系统块里面都有数据,有一些要使用包括信息描述在文件系统的结构。EXT2决定了文件系统拓扑通过每个文件在系统内和一个索引节点数据结构。一个索引节点描述了那个数据块在文件内包含访问文件的权限,文件的修改次数和文件种类。每个在EXT2文件系统内的文件都用单索引节点描述,每个单独号码识别。索引节点全部被保存在索引节点列表中。EXT2目录是一个简单的特殊文件包括指针到它们的目录项。
图8.2表现了EXT2文件系统的排列,占据了一个块结构设备的的序列。目前来说每个文件系统都被考虑到,块设备是块的序列,可以被读取和写入。一个文件系统不需要考虑自己的物理内存,这个是设备驱动器的工作。不论什么时候一个文件系统需要从一个块设备读取信息或者数据,它都要求设备驱动器的读取支持。EXT2文件系统分出逻辑分区就占据了块组(block group)。
每一个组复制信息到单独的文件系统并且保存真实文件和目录为信息数据块。这个复制应该是对文件系统的恢复非常重要的。分支描述了更多的块组内容的细节。
1. EXT2 索引节点
在EXT2文件系统中,索引节点是一个基础的块;每一个文件目录在文件系统之内都被一个一个索引节点描述。EXT2 索引节点s保存了每个块组和点阵在索引节点表格中,允许系统保存分配和回收的索引节点s.图8.3展现了EXT2 索引节点的格式,在其它的信息中,包括了以下的领域:
Mode: 这包括两个信息块;就是索引节点描述和允许用户使用。对于EXT2, 一个索引节点可以描述一个文件,目录,符号链接,块设备,设备符号或者是FIFO.
用户信息:拥护和此文件或目录的组识别器。这允许文件系统正确的允许访问权利。
空间:文件的大小比特。
Timestamps: 索引节点创建的时间和最后一次修改的时间。
Datablocks: 指针指向块,这个索引节点描述的一些数据。前十二个指针指向物理块包括这个索引节点和最后3个指针包括更多层次的间接数据描述。比如,双间接块指针指向一个指针到数据块。这就是说文件多少都和12个数据块在长度上一样,比大型的文件更快的链接。
你需要注释EXT2 索引节点s可以描述特殊的设备文件。这些不是真实的文件,而是处理可以使用连接设备的程序。所有这些设备文件都在/dev中,允许程序访问Linux设备。比如安装一个程序需要一个设备文件要安装参数。
2. EXT2 超级块
超级块包括对这个文件系统基本空间和形状的描述。其中的信息允许文件系统管理器使用并且维护这个文件系统。一般来说,只有超级块在块组0的时候读取文件安装但是每个块组都包括一个拷贝防止文件系统崩溃。在其它信息中就包括:
Magic Number: 它允许安装软件来检查EXT2文件系统的超级块。当前的版本是EXT2 0xEF53.
Revision Level: 主要的和次要的修订版层次允许安装代码来检测时候这个文件系统支持仅仅在特定版本的文件系统的特点。还有一些兼容性的特点,可以帮助安装代码来检测一些新的特点,可以被安全应用在这个文件系统之上。
Mount Count and Maximum Mount Count: 这些一起来允许系统监测时候文件系统完全检查。安装计数每次增加,文件系统就安装,直到现实最大安装计数的警告信息“达到最大安装数量,要求运行e2fsck”。
块组 Number: 块组数字保存超级块的副本。
Block 空间:文件系统Block的空间用比特计算,比如1024比特。
Block per Group: 组内block的数量。就像block空间一样在文件系统创建的时候是确定的。
Free Blocks: 文件系统内空闲block的数量。
Free 索引节点s: 文件系统内空闲索引节点s的数量。
First 索引节点: 这是文件系统内第一个索引节点的索引节点号码。第一个索引节点在EXT2root文件系统内应该是“/”目录的目录项。
3. EXT2组描述器
每个块组有一个数据结构来描述它。就像超级块,所有的Block组的组描述器豆腐之在每一个块组中,以免文件系统崩溃。
每一个组描述器都包括以下的信息:
Block 点阵: Block组block分配点阵的block号码。这在block分配和回收时使用。
索引节点 点阵: 索引节点分配点阵给Block组的Block号码,用在索引节点分配和回收的时候。
索引节点表格:开始Block组 索引节点 table 的Block号码。每个索引节点代表EXT2 索引节点 以下描述的数据结构。
Free blocks count, Free 索引节点s count, Used directory count: 空闲的块,空闲的索引节点s和使用过的目录的数字。
描述的组在另一个之后,一起制作组描述表格。每一个块组都包括整个组描述。只有第一个副本是被EXT2文件系统使用。其它的副本还在那里,就像超级块的副本,避免主要副本崩溃。
4. EXT2目录
在EXT2文件系统,目录是特殊的文件,用来新建并且在文件系统中保存到文件的访问路径。图8.4展示了内存的目录项
一个目录文件是一个目录项的列表,每一个都包括了以下的信息:
索引节点:
这个节点是给目录项的。这是一个节点数组的索引,在块组的节点表格中。在图8.4中,文件file的目录项有到节点的变量i1,
名长:目录项以比特计算的长度
名:目录项的名称
每个目录的前两项总是”.”和”..”,这个项的意思就是“子目录”和“父目录”
5. 在EXT2文件系统中查找一个文件
Linux文件名和所有的Unix文件名格式相同。就是用斜杠“/”分开的目录名序列,以文件名结束。比如:/home/rusling/.cshrc就是说/home和/rusling是目录名称,.cshrc是文件的名称。和所有其它的Unix系统一样,Linux不在乎文件名的格式;可以是任意的长度,包括任何可以打印的字符。查找索引节点代表这个文件在EXT2文件系统中,系统必须分析文件名的目录,直到我们找到这个文件。第一个节点我们需要文件系统的root节点,我们查找这个数字在文件系统的超级块中。读取EXT2索引节点,我们必须要在节点列表的相应块组中查找。比如,root节点的数字是42,我们就要在块组0种查找第42个节点。Root节点是为EXT2目录的,用另一句话说就是root节点描述它为一个目录,它的数据块包括了EXT2目录项。
Home只是一个很多目录项,这个目录项给我们很多代表/home目录的节点。我们要读取这个目录(通过读取这个节点,然后从描述节点的数据块读取目录项)来查找rusling项,给我们一些代表/home/rusling目录的索引节点。最后我们读到指针指向/home/rusling目录项,查找索引节点数字.cshrc文件并且从这里我们得到了包括这个文件的数据块。
6. 改变EXT2文件系统中目录的大小
文件系统的一个普遍的问题就是它有趋势分裂。保存文件数据的块分散在所有的文件系统中,这就是访问一个文件的数据块变得越来越不方便,越难以访问到数据块。EXT2文件系统是着解决这个问题,通过分配新的块给文件,物理上距离当前的数据块更近,或者至少在同一个当前数据块的块组中。只有当这个办法失败的时候才会分配数据块在另外一个块组。
不论什么时候,一个进程试着写入数据到一个文件,Linux文件系统查找看是否文件最后分配的块中的数据丢失。如果是这样,就必须分配一个新的数据块给这个文件。直到分配完全结束,进程不可以运行;就要等待文件系统分配一个新的数据块,并且在它继续之前写入剩余的数据。第一件事就是EXT2块分配历程,锁住EXT2超级块到文件系统。分配和回收改变超级块中的改变栏位,Linux文件系统不匀许多于一个进程在同时间做这个操作。如果另一个进程需要分配更多的数据块,它要等待一个进程结束之后才可以进行。进程挂起等待超级块,就不能运行,直到当前用户撤回超级块的控制。访问超级块在于第一次,第一个服务基础和一个进程有超级块的控制,一直控制,直到进程结束。锁住超级块,进程要检查是否有足够的块空间在文件系统之内。如果没有足够的块,就分配更多的就会失败,进程也会被这个文件系统的超级块撤销控制。
如果有足够的空闲块空间在文件系统,进程聚会分配一个地方。
如果EXT2文件系统建立在提前分配数据块,那我们就要去用其中一个。提前分配块没有实在的存在,只是在分配了的块点阵中预留的。VFS索引节点代表了我们要分配新数据块的文件,有两个EXT2特殊领地,prealloc_block和prealloc_count,这些是第一个提前分配的数据块的块号码,和总共有多少个。如果没有提前分配了的块或者不能够提前分配块,那么EXT2文件系统就必须分配一个新的块。EXT2文件系统首先查找看是否文件树上有数据块在最后一个数据块之后。逻辑上说,这是最有效的分配,是序列访问更加快捷。如果一个块不是空显得,那么查找就会扩大,就会找数据块在64个理想块之内的块。这个块,虽然不是理想的,但是至少和相同的块组接近,其它的数据块也属于这个文件。
如果还是没有空闲的,进程就会查找所有其它的块组,直到找到一些空闲的块。块分配代码在块组中查找八个空闲数据块的群集。如果不能找到八个,就少一点。如果需要块提前分配,就要升级prealloc_block和prealloc_count.
不论什么时候查找空闲的块,块分配编码省级块组的块点阵,并且分配一个数据环存在缓存器中。数据缓存是一个文件系统支持的设备识别器的特殊识别的设备,还有分配块的块号码。在缓存器中的数据是0,缓存器就显示为“dirty”来显示内容还没有被写入物理硬盘。首先,超级块显示为“dirty”表示更改过,而且已被解锁。如果有任何的进程等待超级块,第一个队列就允许再次运行,并且得到独家控制超级块文件操作的权利。进程的数据写入新的数据块,如果数据块填满,整个进程就反复,另一个数据块也被分配。
第四节 Proc文件系统
/proc文件系统表示Linux虚拟文件系统的力量。它不是真实存在的(另一个Linux的魔术戏法),既不是/proc目录也不是它的支目录,它的文件确实存在。所以你怎么能够像真实文件系统一样cat /proc/devices的/proc文件系统呢,它在虚拟文件系统之内寄存。然而,当VFS作出调用的时候,就需要节点,它的文件和目录打开了,/proc文件系统从内核信息中新建一些文件和目录。比如,内核的/proc/devices文件是从描写它的设备的内核的数据结构中生成的。
/proc文件系统代表了一个用户的可读窗口,到内核的内任务中。几个Linux支系统,比如内核模块在/proc文件系统中新建项。
第五节 练习
Abstract抽象:在存在的EXT2文件系统中,每一个文件系统的目的就是链接9比特访问权限,指定读、写、执行权限给用户,用户在同一个组里面可以是所有者和其它人。然而,这个9比特访问权限并不够生成到达一些复杂的,但是有用的访问权限。比如,一个文件系统的目标是组y00.如果这个组中的用户需要有不同的访问权限,那么9比特访问权限就不能解决这个种类的需求。
Access Control List(ACL) 访问控指表:一个ACL定义为在下列格式中包括元组的列表:
(用户,组,访问权限)
在用户领域识别用户要通过UID,组区域识别组用GID,这个访问识别权限可以用以下的表格:可读或者不可读,可写或者不可写,可执行或者不可执行。有时候我们不想要制定那个用户或者那个特别的组,我们就像设置用户区域或者组区域(X)。我们不管新术语,我们只要四个可能的元组种类。
案例 ACL 元组 优先 注
A (用户,组,访问权限) 1st 可以多于一个元组
B (用户,X,访问权限) 2nd
C (X,组,访问权限) 3rd 可以多于一个元组
D (X,X,访问权限) 4th
如果一个模式或者ACL元组没有允许一个特殊的访问,那个访问就被拒绝,我们新的文件系统相关于传统的模式比特和ACL。换句话说,就是一个访问植被访问控制方法允许的时候接受。比如,文件“thizlinux.ps”只有一下访问模式和三个ACL元组。
-rwxr-xr-x 1 user2 thizgroup 12345 Nov 3 14:51 thizlinux.ps
1st 元组:(用户3,thizgroup,{可读,不可写,可执行})
2nd 元组:(X,X,{不可读,不可写,不可执行})
3rd 元组:(X, thizgroup,{不可读,不可写,可执行})
用户1属于thizgroup组,想要访问这个文件,那么它有什么文件许可呢?两个元组匹配,第二第三。因为第三元组在第二元组之前,就有更高的权限,被使用过。用逻辑的AND在组权限和这些制定到第三元组到ACL,用户1的权限就是可读可写可执行。
启用:任何文件系统的ACL的目标包括存储在数据块中,被节点指向的文件和目录。你可以修改ext2文件系统支持这个用法,但是确定你作出的是适当的改变使系统工作。让你的生活变得更加轻松,最好在ext2floppy盘上面测试你的工作。你要在单独盘块中最大化ACL元组的数字,并且选择一个好的数据结构添加,删除,查找ACL元组。你可以自由的设计你自己的功能协议/原型给升级了的ext2文件系统。请实现一些容易的操作(用户水准程序)来测试你的ACL操作。
系统调用:
//create ACL block
Asmlinkage int sys_create_acl(char * name);
//destroy ACL block
Asmlinkage int sys_sestroy_acl(char * name);
//add tuple to ACL block
Asmlinkage int sys_add_acl(char * name, uid_t uid, gid_t gid,
Mode_t mode);
//remove tuple from ACL block
Asmlinkage int sys_remove_acl(char * name, uid_t uid, gid_t gid);
//show all tuples in ACL block
Asmlinkage int sys_show_acl(char * name);
User programs:
Create_acl <fs object name>
Destroy_acl<fs object name>
Add_acl<fs object name ><user name><group name ><permission>
Del_acl<fs object name><user name><group name>
Show_acl<fs object name>
e.g.
./create_acl /mnt/floppy/file
./destroy_acl /mnt/floppy/file
./add_acl /mnt/floppy/file user1 user1 7
,/remove_acl /mnt/floppy/file user1 9999
./show_acl /mnt/floppy/file
(You should use 9999 as don’t care term)
第九章 设备驱动
操作系统其中一个目的就是掩盖硬件设备的缺点。比如说虚拟文件系统展示一个物理设备的安装文件系统没关系的统一观点。这一章描述了Linux内核如何管理系统物理设备。
CPU不是系统中唯一智能的设备,每一个物理设备都有其自己的硬件控制器。键盘,鼠标和串行端口都被超级IO芯片控制,IDE盘被IDE控制器控制,SCSI盘被SCSI控制器控制等等。每个硬件控制器都有其自己的控制和状态寄存(CSRs),这些不同的设备都有不同的控制器。CSRs为Adaptec2940 SCSI控制器,完全不同于NCR810 SCSI控制器。CSRs用来开始和停止设备,初始化并且诊断任何问题。不是写入编码管理这些系统中每个应用系统的硬件控制器,这些编码在Linux内核之中处理过着管理一个硬件的驱动器就叫做设备驱动器。Linux内核设备驱动是一个共享的库,低阶硬件处理历程。Linux的设备驱动器处理这些管理它们的设备的特点。
一个基本的特点就是处理设备。所有的硬件设备看起来就像是一般的文件;它们可以打开,关闭,读和写,使用相同的,标准的,系统调用来处理文件。每个系统设备代表一个设备特殊文件,比如系统第一个IDE盘就代表为/dev/hda. 块盘和字符设备,这些设备特殊文件用mknod命令生成,它们使用主要和次要的设备成员来描述。网络设备也用设备特殊文件代表,但是它们用Linux创建成查找和初始化网络控制器。次要的成员用来分开不同的设备和它们的设备驱动器,比如每一个主要IDE磁盘的分区有一个特殊的次要设备成员。所以/dev/hda2,主IDE第二个分区有主要的成员3和一个次要的成员2。Linux映射设备特殊文件在系统调度中传输到设备驱动器,主要的设备驱动号码,使用和系统表格号码,比如字符设备驱动,chrdevs.
Linux支持3个种类的硬件设备:字符,块和网络。自付设备可直接免缓存器读写,比如系统序列点/dev/cua0和/dev/cua1.块设备只能读写从块大小的乘数块中,比如512或1024bytes。块设备通过缓存器访问,可以随机访问,就是说任何得快都可以读写,不管是在什么设备上。块设备可以通过它们特殊的设备特殊文件访问,但是大多数情况它们通过文件系统访问。只有块设备可以支持安装文件系统。网络设备通过BSD访问界面和网络支系统。
有许多不同的设备驱动器在内核中(这也是内核的优势),但是所有的都共享相同的分配:
内核代码:设备驱动其实内核的一部分,就像其它内核内的编码一样,如果它们错了就会严重的危害到系统。一个写错了的驱动器可以摧毁整个系统,可能会使文件系统崩溃并丢失所有数据。
内核界面:设备驱动器必须提供一个给内核或者支系统的标准界面。比如,终端驱动提供文件I/O界面给内核河SCSI设备驱动器提供一个SCSI设备界面给SCSI支系统,相反,提供文件I/O和缓存器界面给内核。
内核和服务:设备驱动用标准的内核服务,比如分配内存,中断传输和等待队列操作。
可加载的:大多数的设备驱动器可以加在为内核态,当它们被需要的时候就被加载,不需要就卸载。这使内核在系统资源内非常的有效和可调。
可配置的:Linux设备驱动起了以建立在内核上。当内核编译后设备建立为可配置的。
活性:如同系统启动和每一个设备驱动器被初始化,都要查找控制的硬件驱动器。不论是否设备被一个特别的设备驱动器管理或者不存在。这种情况设备驱动器都简单的仅仅占用系统内存的一小部分而已。
第一节 字符和块设备
1.字符设备
字符设备是Linux设备中最简单的,像文件一样访问,应用程序使用标准系统调用来打开它们,写入它们,关掉它们,好像真的只是文件一样。即使设备是一个连接Linux系统的PPPdaemon使用的调制解调器也一样。像字符设备被初始化它的设备驱动寄存器,和Linux内核,添加一个项到device_struct数据结构chrdevs矢量。设备的主要设备识别器(比如tty设备的4)就用来做这个矢量的索引。主要的设备识别器被修订。
每一个chrdevs矢量的项,device_struct数据结构包含两个元件;一个指向注册设备驱动器的名字的指针,和一个指向文件操作块的指针。这个文件操作块是历程地址在设备字符设备驱动其中,每一个都处理一个特殊的文件操作,比如打开,读写和关闭。/proc/devices的内容在字符设备中,是从chrdevs矢量来的。
当一个字符特殊文件代表一个字符设备打开的时候(比如/dev/cua0)内核就要设置好,所以正确的字符设备驱动器的文件操作历程就会被调用。就像一般文件或者目录,每个设备特殊文件都用VFS节点代表。VFS节点为一个字符特殊文件,当然是所有设备特殊文件,包含主要和次要的设备识别器。这个VFS节点用重要的文件系统生成,比如EXT2,从真实文件系统的信息查找设备特殊文件名。
每一个VFS节点用一系列的文件操作联结,这些不同在于节点代表的文件系统目标。当一个VFS节点代表一个字符特殊文件被创建的时候,它的文件操作就被设置为默认字符设备操作。这只有一个文件操作,打开文件操作。当字符特殊文件被一个应用程序打开的时候,普通的打开文件操作使用设备的主要识别器作为到chrdevs矢量的索引,跟踪这个特殊设备的文件操作块。它还设置文件数据结构描述字符特殊文件,是她的文件操作指针指向那些设备驱动器。这样所有的应用程序文件操作就会被映射到文件操作的字符设备调用。
2.块设备
块设备也支持被访问的文件。这个设备用来提供正确的文件操作地址,给打开得块序列文件,和字符设备基本一样。Linux保持寄存块设备再blkdevs矢量中。就像chrdevs矢量,所引用设备主要设备号码。它的项也在device_struct数据结构中。和字符设备不同,有块设备的分类。SCSI设备有一个分类,IDE设备和另一个。这个分类寄存自己在内核中,并提供文件操作。设备驱动器给一个块设备分类提供分类特别界面。
每一个块设备驱动器都要提供一个缓存器的界面,和一个一般的文件操作界面。每一个块设备驱动都填充了blk_dev_struct数据结构的blk_dev矢量项。这个矢量的索引就是设备的主要号码。Blk_dev_struct数据结构包括了需要历程的地址,和一个指向request数据结构列表的指针,每一个都代表了一个缓存器需要读取的数据块。
缓存器要读取或者写入数据块,或者从寄存器设备中添加一个request数据结构到blk_dev_struct.图9.2表示了每一个需要都有一个指向一个或更多buffer_head数据结构的指针,每一个都要求读取或者写入一个数据块。Buffer_head结构被缓存锁住,有可能有一个进程等待缓存完成在块操作。每一个request结构都从一个数据列表中分配,all_request列表。如果这个需要被添加到一个空的需要列表中,驱动器的需要功能就被调用来开始进程的需求队列。否则驱动器就简单的处理每一个在需求列表中的需求。
当一个设备驱动期结束需求,就要从request结构的buffer_head结构中删除,标注它们到期并且解锁。这个buffer_head的解锁会唤醒任何沉睡等待块操作结束的进程。这个的案例就是一个文件名被解决,EXT2文件系统必须阅读包含下EXT2目录项的数据块,从有文件系统块设备中。进程在buffer_head中睡眠,包括了目录项指导设备驱动被唤醒。request数据结构也被自由标注,所以就可以用在另一个块需求上面。
第二节 轮询和中断
每次一个设备给出一个命令,比如“move the read head to sector 42 of the floppy disk”设备驱动器就有了选择查找到这个命令是如何完成的。设备驱动器就可以使用轮询或者中断。
轮询设备一般意味着读取它的状态寄存器,非常经常,以至于设备的状态改变为完成需求。因为设备驱动其实内核的一部分,那么如果一个驱动器要在内核中轮询,直到运行设备已经完成了需求。不是轮询设备驱动其使用系统计时器,来使内核调度历程在设备驱动器之中。这个计时历程可以查看命令的状态,也就是Linux软盘驱动是如何驱动的。轮询使用计时器是最合适不过的了,但是更加有效的方法就是使用中断。
一个中断驱动的设备驱动器是硬盘驱动器的一部分,不论什么时候需要服务,就会生成硬件中断。比如,以太网络设备驱动器就在接受到以太网络包的时候中断。Linux内核需要能够传输中断,从硬件到准确的设备驱动器。这被设备驱动器完成,寄存它的内核中断和用法。它寄存了中断处理历程的地址,和想要控制的中断号码。你可以在以下的/proc/interrupts看出那个终端被设备驱动其使用,就像有多少种类的中断被使用过:
这个中断资源的需要在驱动器初始化事件结束。一些系统中断被修改,这个IBM单机架构的神话。所以例如,软盘控制器总是使用中断6。其它的中断,比如PCI设备终端就自动地被分配到了启动时间。在这种情况下设备驱动器必须首先发现设备中断号码。因为PCI中断Linux支持的标准PCI BIOS叫回到决定系统设备信息,包括它们的IRQ号码。
那么中断是怎么被传送到CPU的呢,它自己是一个建立在大多数终端上的架构,使用一个特殊的模式传输来停止其它系统的中断。一个设备驱动器可以尽量少的在它的中断处理历程做事,所以内核就可以忽略中断并且返回中断之前的操作。设备驱动器需要做很多工作,这是接受中断可以使用内核底部一般处理器或者任务队列到队列历程,稍候调用。
第三节 DMA
当一些数据很低的时候,就要使用中断设备,驱动器传输数据使一个硬件设备工作良好。比如9600波特调制解调器就可以使当初传输一个字符一微秒(一千分之一秒)。如果中断被推迟,市场就会在硬件设备间提高,中断和设备驱动的中断处理历程被调用很低(两微秒),那么完整数据传输的系统影响就很低。9600波特调制解调器数据传输只使用了0.002%的CPU进程时长。对高速设备来说,硬盘控制或者以太网设备,数据传输比率就要高很多。一个SCSI设备就可以传输40M比特的信息每秒。
直接内存访问DMA,被用来解决这个问题。DMA控制器允许设备传输数据或从系统内存中没有处理器的干涉。PC的ISA DMA控制器有8个DMA频道,7个可以被设备驱动器使用。每个DMA频道都与16比特地址和计数寄存器相关联。初始一个数据传输器,设备驱动器就设置DMA频道地址和计算机存器和数据传输直接一起计算。高速设备在需要的时候要启动DMA。当传输器结束的时候,设备中断计算机。传输器代替CPU让它可以做起它的任务。
设备驱动器要小心使用DMA。首先所有的DMA控制器都不知道虚拟内存,只能访问系统物理内存。所以DMA必须成为一个物理内存的邻居块。这就意味着你不可以DMA直接到进程虚拟地址空间。你可以拦截一个进程的物理页,防止它们在DMA操作的时候换出设备。第二,DMA控制器不能访问整个物理内存。DMA频道地址寄存器代表了前16bits的地址,下8bits为内存。DMA频道就是资源,只有7个,它们不可以在设备驱动间共享。就像中断,有一些驱动器有修改DMA频道。软盘设备,比如,总是用DMA2.有时候DMA设备频道可以用跳线设置;一些以太网设备使用这个技术。更灵活的设备可以通过CSRs,DMA频道使用的,在这种情况,设备驱动就可以简单的选择一个空闲的DMA频道使用。
Linux跟踪DMA频道使用dma_chan数据结构矢量的方法。Dma_chan数据结构包括两个领域,一个指针描述DMA频道,还有一个标注代表DMA频道是否被分配。这个dma_chan数据结构矢量在你cat /proc/dma的时候显示。
第四节 使用字符设备驱动器
设备一般来说分为两个种类:字符设备和块设备。在这一节中,我们就要学习如何应用字符设备驱动。图9.3展示了文件系统的分层,设备驱动和实际硬件。
1.代码排列
我们首先包含了完成排列的简单字符设备驱动,点到点的解释一下一段。设备驱动器的案例,zerodev.c,就是一个零设备,和/dev/zero设备很相似。
2.设备驱动设置
编译这个程序,运行:
Gcc –c –D__KERNEL__-DMODULE zerodev.c
模块zerodev.o应该被创建
加载模块,运行:
Insmod ./zerodev.o
我们要使一个文件系统节点代表这个设备:
Mknod /dev/zerodev c 42 0
Mknod /dev/zerodev1 c 42 1
这就是字符设备节点/dev/zerodev由主要的数字42和次要的数字0。我们也是令一个零设备/dev/zerodev1由主要数字42,但是它的次要数字是1。我们要在下一节展示一个单设备驱动器如何在多设备上运行。
3.内核模块
如果以内核模块来实现设备驱动就更加方便了,当我们能够编译设备驱动没有重编译整个内核图像。实现一个内核模式,我们需要两个功能:
Int init_module(void){
…
}
Int cleanup_module(void){
…
}
这个init_module()功能是一个模拟的到main()功能的C程序。因为模块已经安装,init_module()功能被运行,我们就出示并且安装驱动器在init_module()功能中。
为了真实设备驱动器,init_module应该有责任特使硬件和驱动器寄存器。如果硬件没有被测试到,init_module就应该返回错误(负数)所以模块就不会被安装。在我们的案例中,因为没有驱动器的真实硬件,我们总要返回成功(零)。
Cleanup_module功能在模块被从系统中删除的时候调用。真实设备驱动器,cleanup_module要对放置硬件到停止状态和未寄存驱动器负责任。
4.寄存一个字符设备
在一个设备驱动器功能化之前,我们需要使用一些特定的特征,来驱动硬件。比如,我们的零设备要实现打开,读写,关闭功能,所以用户层应用系统可以和硬件相互配合。所以,设备驱动器要告诉内核驱动器支持哪一个特征,通过填写file_operation结构:
以上的表明了zerodev_fops的变量种类struct file_operations.有很多struct file_operations的领域,但是在我们的结构中,我们只有ioctl,打开,释放,读,写,和llseek初始化功能地址。其它的领域被忽略并设置为0。整个的struct file_operation可以在include/linux/fs.h内核源代码中找到:
如我们所见,结构包含了不同设备操作的功能指针。用来实现所有这些功能并不必需。而是很多设备驱动器一般不实现所有这些,因为有一些功能部和设备操作兼容。
实现打开和释放功能是必需的。否则没有人可以打开我们的设备并使用它。打开和释放,我们的零设备也支持读,写,和ioctl。我们要在其后做出一一解释。
设想我们实现了打开,关闭,阅读,写入,和ioctl功能。我们就要告诉内核我们已经准备好了驱动我们的硬件,通过寄存设备驱动器:
Int init_module(void){
Int r ;
R=register_chrdev(ZERODEV_MAJOR, ZERODEV_NAME,
&zerodev_fops);
If(r==0){
Printk(KERN_INFO”ZERODEV DEVICE REGISTERED!\n”);
}
Else {
Printk(KERN_INFO”unable to register Zerodev device!\n”)
Return r;
}
Return 0
}
我们使用功能register_chrdev寄存我们的字符设备驱动器。驱动器寄存器在init_module中完成,所以每次模块都被加载。驱动器被寄存,并成为内核模块。
第一个register_chrdev参数就是主要的设备号码。我们的设备使用主要号码42,是为了demo目的的。你可以检查主要号码的完整的列表在文件档案/device.txt在内核源码中。
第二个register_chrdev参数就是设备名,最后一个参数就是之前提到的struct file_operation。当寄存成功后,内和上层(如文件系统)使用我们在zerodev_fops中的功能和我们的设备交流,不管什么时候访问主号码42的设备。
当设备不再被使用的时候,我们就可以把设备驱动器解除寄存,使用unregister_chrdev:
Int cleanup_module(void){
Int r;
R=unregister_chrdev(ZERODEV_MAJOR, ZERODEV_NAME);
If(r==0){
Printk(KERN_INFO”Zerodev Device unloaded!\n”);
}
Else{
Printk(KERN_INFO”Zerodev Device unloaded!\n”);
}
Return 0
}
这里,我们解除寄存了驱动器,使用传递主要号码和设备名为参数。解除寄存功能一般防灾cleanup_module中,所以无论何时我们的设备驱动模块被卸载的时候,我们都告诉系统设备驱动器不存在了,就调用unregister_chrdev.
5.实现文件操作
我们要讨论一下如何和进行驱动器寄存解除寄存。现在,我们要学习如何实现这些需要的操作。在这一节中,我们将会讨论一些最常用的字符设备操作。
打开和释放操作
打开和释放是模拟打开关闭一个文件。一个文件必须在我们可以读/写之前被打开。一个Linux设备必须被打开,在它可以被访问之前,像我们所作的文件打开一样。所以打开和释放操作就是设备的打开和关闭。
zerodev打开操作就是:
static int zerodev_open(struct inode *inode,
struct file *file){
Printk(KERN_INFO”Zerodev Open!\n”);
MOD_INC_USE_COUNT ;
Return 0 ;
}
我们的zerodev只是简单的打开一个设备,除了做出MOD_INC_USE_COUNT和现实一个信息。MOD_INC_USE_COUNT还提高内部模块实用计数。所以无论何时一个设备被打开,模块技术都是正数,所以删除我们的模块就会被一个模块正在使用中的错误拒绝。
注释我们打开操作总是成功是非常重要的。一个真实设备驱动器需要局限代码的使用,通过一些并发打开的数量限制。
当进程结束我们的设备访问的时候,进程调用我们的释放操作,通知我们访问结束。释放操作zerodev
释放操作zerodev_close做出了打开操作完全相反的。它减少了内部使用计数并且返回成功。
读写操作
读写操作是用来分别从设备中读写。像之前谈到的,我们的zerodev操作和/dev/zero设备相似。Zerodev读操作很简单。只是简单的返回null字符到读取的调度器。然而,我们用简单的其它特点来表示少数的用法。
我们的设备驱动器可以在同一时间驱动多设备。设备有少数0,读操作返回到null字符。设备有除了0的其它少数号码,打开操作返回字符为“a”.
Zerodev读操作是:
有四个读操作的参数。第一个就是指向文件结构的指针,只项文件结构打开我们的设备。从文件结构,我们就可以访问很多有用的文件系统信息,就像设备节点。我们从文件结构得到设备节点,从节点得到少数设备。通过这个方法,我们就可以通过它们的少数号码分辨不同的设备,驱动多个相似的设备用一个设备驱动器。
第二个读操作的参数就是用户空间缓存,存储在读数据中,当地三个参数分类读取的比特时候。读取,读操作的目标就从设备到用户复制数据。所以我们的设备驱动器就药方数据从硬件到用户空间缓存。在我们的案例中,我们的硬件是简单的0设备,有无限的null字符给用户。所以我们就要方数据到用户缓存。
最后一个参数就是位置。如果设备是原位的。为之久储存在当前的读/写操作位置。如果进程从设备中读取10个比特,设备就要提高0比特并且升级位置变量。在我们的zerodev案例中,我们的设备存储是完全的null字符(或者a)这个位置对我们没有任何意义。所以我们就要忽略这个参数在我们的案例中。
在读操作中,从文件结构中第一个找到的设备节点,查找它的少数号码。然后使用功能put_user来放置数据从我们的设备到用户空间缓存。少数号码0的案例,我们放置null字符。否则,我们放置字符a。在放置字符到用户空间缓存之后,读操作就结束了,功能也恢复到放入的比特数。
注意到我们不可以用memcpy复制数据从内核空间到用户空间是非常重要的。理由就是内核空间和用户空间使用不同的地址段访问自己的变量。所以,用户空间抵制传输在读操作的第二个参数,不能被直接使用,像我们的设备驱动器使用内核段来引述数据,而不是使用用户段。为了拷贝数据从内核空间到用户空间,我们需要使用功能put_user,或者功能copy_to_user.这些功能用在不同的段,拷贝数据。
现在,我们要有了zerodev的写操作:
Return count;
}
写操作的参数和读操作的相似,除了我们要从用户缓存中得到数据而不是把数据防到其中。我们的写操作仅仅读取了用户空间缓存的前10比特,然后从中显示。真是设备驱动器要放这些数据到硬件。
最后,读取的比特数返回,写操作成功。
Ioctl操作
有时候不可能仅仅用读和写操作控制一个设备。比如,一个可移动磁盘(光学磁盘驱动,CD-RW等等),读写操作用来读写磁盘的数据。我们怎能命令一个设备驱动器操作一些特殊的功能比如弹出设备呢?
Ioctl操作就是一个可选的方法来控制设备。Ioctl要求包括两个元件:命令和函数。根据设备驱动起决定ioctl的命令。我们的ioctl在zerodev仅仅显示命令和函数:
我们要通过使用ioctl在块设备段。但是首先让我们看一下案例是如何使用ioctl命令一个设备去做些什么事情的,比如弹出光驱:
#include<linux/module.h>
#if defined(CONFIG_SMP)
#DEFINE __SMP__
#endif
这个简单的案例展示给我们如何使用ioctl弹出Cdrom托盘。我们首先打开设备device/dev/cdrom,然后是用ioctl执行需求。命令CDROMEJECT和函数0最终被传送到设备驱动。设备驱动检查命令0X5309,然后做出回应需求的操作弹出光驱托盘。
在CDROM EJECT的案例中,命令0x5309定义用来弹出光驱。根据设备驱动定义它自己的ioctl命令。约束ioctl需要的函数可以是4比特的函数(IA-32).对于复杂的ioctl操作来说这是很多的函数需求,需要使用引用。
第五节 实现块设备驱动器
当字符驱动器操作比特单元(一般是比特流),块设备就是操作块单元的设备。比如,你的硬盘。每次读取硬盘的时候,数据都不是一个比特一个比特的传输。而是整个数据段的传输,即是用互相指传输一个单比特。也要有其它种类的驱动器给这些块设备在块传输字符中相遇。
我们首先展示完整的样本块设备驱动器列表,并且在以后的段落中解释。样本块设备是一个随机盘,是用内核内存启动一个块设备。
1.设备驱动器安装
编译这个程序,运行:
Gcc –c –D__KERNEL__-DMODULE thizramdisk.c
模块thizramdisk.o被新建。
加载模块,运行:
Insmod ./thizramdisk.o
是文件系统节点代表设备:
Mknod /dev/thizramdisk b 42 0
这将使块设备节点/dev/thizramdisk有主要数字42次要数字0。
这个随即盘没有分区表格支持。我们可以新建一个文件系统在这个随即盘中,就像新建一个文件系统在一个硬盘分区当中一样:
Mkfs.ext2 /dev/thizramdisk
在新建了ext2文件系统之后,我们可以安装存储文件盘。这个文件会在随即盘中知道模块被卸载。
2.寄存一个块设备
我们再一次使用模块实现块设备驱动。我们寄存设备驱动在init_module.寄存块设备,我们用:
R=register_blkdev(THIZ_RAMDISK_MAJOR,
THIZ_RAMDISK_NAME,
&thiz_ramdisk_fops);
这将寄存我们的thiz_ramdisk_fops操作给块设备,由主要数字THIZ_RAMDISK_MAJOR(42).
然后我们给这个设备驱动器命名,最后一个参数是STRUCT BLOCK_DEVICE_OPERATIONS.block_device_operations结构和file_operations结构不同。让我们看看block_device_operations在我们的随机盘中:
在我们的案例中,我们设定了自己的ioctl,打开,释放,检查媒体交换,设备功能。你可能会注意到没有读写操作在我们的block_device_operations当中。读写操作被单功能处理,是一个需要的功能。细节会在以后的章节详细解释。我们定义自己的需求功能thiz_ramdisk_requesk,来处理IO。我们要寄存需要功能,分开使用:
Blk_init_queue(BLK_DEFAULT_QUEUE(THIZ_RAMDISK_MAJOR),
Thiz_ramdisk_request);
这是我们的需要功能处理主要数字42的需求块设备。
我们还需要提供一些基本的设备属性,通过复制给全球数组:
这个就是这些全球变亮的描述:
Hardsect_size[MAJOR]: 这是一个设备段数组空间,用比特为单位。一般来说512。注意HARDSECT_SIZE是一个数组的数组。所以hardsect_size[MAJOR]是一个储存不同次要数字设备的段大小设备。我们的设备有次要数字0,所以我们设置数组为单数
Blksize_size[MAJOR]这是一个块大小的数组,每个设备来使用。这个一般是1024bytes.注意blksize_size是一个数组的数组,blksize_size[MAJOR]十一个不同次要号码设备储存块大小的数组。
Blk_size[MAJOR]这是一个设备的大小用千比特为单位。注意blk_size是一个数组的数组,blk_size[MAJOR]是一个存储不同次要号码设备的数组。
Read_ahead[MAJOR]这是一个读取之前的数字段。读取前是减少分开结果的推迟。注意READ AHEAD值被同一个主要数字的设备共享。
当我们设置好了之后,我们的设备驱动器就要设置功能了。当我们的设备是用随机存储磁盘的时候,我们要首先分配内存存储数据。我们做INIT_MODULE功能,使用vmalloc:
Thiz_ramdisk_data = (char *)vmalloc(
Thiz_ramdisk_devicesize);
磁盘上的数据存储在thiz_ramdisk_data中。读写操作会成为从分配内存复制的。
当我们结束时用随机盘的时候,我们要删除我们的随机盘,用卸载模块。Cleanup_module功能,我们解除寄存块设备驱动,使用:
R = unregister_blkdev(THIZ_RAMDISK_MAJOR,
THIZ_RAMDISK_NAME);
然后我们要解除寄存需求功能,分开:
Blk_cleanup_queue(BLK_DEFAULT_QUEUE(THIZ_RAMDISK_MAJOR));
最后,我们回到分配内存到内核通过VFREE调用:
Vfree(thiz_ramdisk_data);
3.实现块设备操作
打开和释放操作
打开和释放操作是模拟打开和关闭一个文件。一个设备必须在被访问之前打开。打开和释放一个随机盘很简单:
在打开中,我们提高了内部模块使用计数并且返回成功。在关闭的时候,我们降低了模块使用计数,并且用fsync在我们的设备上是所有的缓存回到我们的随机盘。在字符设备中没有这个步骤,但是在块设备中,缓存期是在设备之上。就是说数据在缓存其中可能不能留在设备当中如果数据被修改。Fsync步骤就保证我们的设备留在缓存器。
检查媒体改变和有效操作
这两个功能,检查媒体改变和有效,一般是给支持可移动媒体驱动器的(光驱,软驱)。所以,实现这两个功能不是必须需要的。如果你不管新媒体改变支持,你就不用这两个功能。我们包括这两个功能在这里是要给你看如何改变处理一个移动媒体驱动器。
这个检查媒体改变功能是给内核来认定是否一个媒体改变。
Static int thiz_ramdisk_media_cahnge(kdev_t dev){
Printk(KERN_INFO”Media changing : changed =%d\n”,
Thiz_ramdisk_changed);
Return thiz_ramdisk_changed;
}
在我们的案例中,我们返回变量thiz_ramdisk_changed,初始1当模块备加载的时候。所以,在第一次我们的设备驱动器被问到是否媒体改变的时候,我们总是报告媒体改变。这很重要,注释当我们报告媒体改变了的时候,内核知道相关的缓存器不再有效,内核就显出缓存中的数据,并且重新从改变后的媒体中读出。
我们就有了有效功能:
Static int thiz_ramdisk_revalidate(kdev_t dev){
Printk(KERN_INFO”Media revalidated\n”)
Thiz_ramdisk_changed = 0;
Return 0;
}
无论什么时候媒体改变发生的时候都会调用。我们的设备没有硬件有效功能,所以我们没办法对硬件有效做出任何操作,除了使thiz_ramdisk_changed变量归0,以假装我们的媒体已经有效,没有其它的方法改变。
Ioctl 操作
Ioctl操作是用来控制设备的。有一些基本的ioctl块设备操作:
BLKGETSIZE返回到设备段之前的大小。它的函数是用户空间变量的用户空间地址。
BLKRASET设置读取前的段。函数是读取之前的段号码。
BLKRAGET得到读取前的段数字。函数是用户空间变量的用户空间地址。
BLKSSZGET得到短段,以比特为尺度的大小。函数是用户空间变量的用户空间地址。
BLKFLSBUF冲刷缓冲器到硬盘。函数忽略。
让我们看一下IOCTL功能在我们的随机盘上:
Ioctl简单的复制或者升级了get_user或者put_user.注意抵制函数再ioctl中是所有用户空间地址,我们需要使用get_user或者put_user复制变量,从一个空间到另一个。因为BLKFLSBUF案例,这个数据是从缓存器到设备的。我们这样做,通过设备的fsync_dev,调用invalidate_buffers来告诉内核相关的缓存器已经不需要了。
5. 实现需要功能
不需要读写功能在设备驱动器。除了读写,我们有另外需求的功能。理由就是有需求功能而不是直接实现读写,因为读写操作已经在内核缓存管理其中了。真正的I/O转换使用功能来分开的。
在一个驱动器的立场来看,驱动器要对设备的交流负责。调度有效地I/O并且缓存管理应该不被设备驱动处理,因为这些操作不是特别的设备。所以,已经有读写操作帮我们实现了,我们就和缓存管理器和调度企合作就好了。读写操作用来调度,当它们决定移动数据块从设备到内核缓存器中,它们就要简单的调用一个需求功能去做就可以了。需求功能,处理传输数据到一个真正的设备,用设备驱动器实现。
我们随机盘需要的功能是:
Static void thiz_ramdisk_request( request_queue_t *queue){
Unsigned long req_offset, req_size ;
仔细来看需求功能,就能看出需求功能从来没有返回。它是进程循环。Label thiz_ramdisk_begin_request标注了循环的开始。不管什么时候我们结束这个要求,我们总会挑回这个标签的起点。
第一个重要的注释就是宏INIT_REQUEST,从需要队列中得到需求。当读写操作决定向设备索求,它们简单的放置一个需要在需要队列中,并且让设备驱动器处理这个需要。INIT_REQUEST就得到了队列的需求。在INIT_REQUEST之后,当前的叫做CURRENT1得需要就会被定义。4各领域,驱动器必须需要的就可以完成这个需要:
CURRENT->cmd分类是否需要是一个读或者写的操作。如果是读操作,CURRENT->cmd就是READ。如果是写需求,CURRENT->cmd就是WRITE.
CURRENT->sector这就决定了是否启动一个需要数据的段。当我们读写的时候,我们需要知道从哪里读写。所以我们有了CURRENT->sector分类启动我们要读写的段。
CURRENT->current_nr_sectors决定我们需求数据的大小。当我们读写的时候,需求的大小可能比一个段落要多,用参数来决定段落的数量。
CURRENT->决定内核空间缓存地址。读需要复制数据从一个设备到缓存器,写入需要要复制数据从缓存器到设备。
当我们的设备驱动从当前的需求得到4个领域,我们就可以表现传输数据到/从设备的需求了。需求段被转换成我们的需求,因为我们在内存中储存了数据,但是不是一个世纪的设备。我们就处理数据传输用MEMCPY.如果是读需求,我们复制数据从随机盘到缓存器:
Memcpy(CURRENT->BUFFER,
Thiz_ramdisk_data+req_offset,
Req_size);
我们还有相似的memcpy给写需求,除了复制的引导。
注意两个缓存和我们的设备数据都在内核空间里面,我们要简单的使用memcpy而不是put_user或者copy_to_user从数据传输器。
当我们完成了一个需求,我们要告诉内核使用end_request功能:如果需求成功结束,我们就要调用end_rquest(1),否则我们就要调用end_request(0).在end_request之后,我们回到开始的地方得到另一个需求,使用INIT_REQUEST。一定要注释没有更多的需求,我们的需求功能会被阻拦INIT_REQUEST中,直到一些需求到达。
你可能怀疑为什么INIT_REQUEST或者CURRENT可以处理我们的需求,不用更多的设备支持信息。额外的信息其实在开始的时候就有了:
INIT_REQUEST和CURRENT使这些值定义下来(比如MAJOR_NR)来定义INIT_REQUEST和CURRENT在linux/blk.h中。所以,这些值就要在我们包括linux/blk.h之前定义。
第六节 练习
功能性键盘支持:有一些现代的键盘有附加键,比如睡眠,Email,声音调节等等。这些功能间和一般间的趋势和发送到应用程序的文本不同。这些用来引发一些功能。
现在我们就要支持这种键盘上的功能键。首先,我们要修改键盘设备驱动器,得到特殊键的扫描代码。然后修改扫描代码到键代码表格并且发送相应的键代码到特殊的char设备。然后一个daemon就可以读取这些键代码,再char设备当中,并且做出相应的功能。
比如,如果一个daemon启动了,谁碰到了email键,daemon就应该运行一个命令,比如mozilla –mail或者evolution。命令可以被用户配置文件定义。
你可以设想这个项目只支持你现在使用的键盘。
第十章 内核模块
第一节 介绍
这一章将介绍Linux内核可以自主加载功能,比如,文件系统,只有在需要的时候才会。
Linux是一个完整内核;就是说独一,单一,巨大的程序,所有的内核功能原件可以访问所有的内部数据结构和历程。可选的就是有微内核结构,所有的功能模块被分成独立的单元,又严谨的通信进程,而非浪费时间的进程。比如你想要使用SCSI驱动器给NCR810SCSI你的内核中没有的。你就要配置并且在使用NCR810前建立一个新的内核。有一个选择,Linux允许你自主加载,并且卸载操作系统中需要的原件。Linux模块是代码的集合,可以在系统启动之后的任意点被自主链接到内核。它们也可以在不再需要的时候被从内核断开链接并且删除。大多数的内核是设备驱动器,pseudo-设备驱动器比如网络驱动,或者文件系统。
你可以加载或者卸载Linux内核模块,使用insmod和rmmod命令,或者内和自己要求内核kmod加载卸载自己需要的模块。
自主加载需要的编码非常好,而且可以把内核的容量降到最低,使内核保持灵活。我当前的Intel内核使用了扩展模块,只有406Kbytes.我很少使用VFAT文件系统,所以我建立了我的Linux内核在我mount安装一个VFAT分区的时候自动加载VFAT文件系统。在我卸载VFAT分区的时候,系统监测我不再需要VFAT文件系统模块,它就自动从系统中删除。模块可以非常有用,因为你可以不用重新启动机器来试验新的内核编码。很自由,而且可以简单的实现,内存最终联系到内核的模块。很少编码需要提供特别的数据结构,也仅仅需要多一点内存。也有一些模块的间接的介绍时访问内核资源有一点费力。
当Linux模块被加载之后,它就成为了内核的一个部分,与其它的内核代码没有什么分别。它又和其它内和代码同样的权利和责任;也就是说Linux内核模块可以像其它的内核代码,设备驱动器一样摧毁内核。
所以加载的模块也可以使用内核资源,它们必须首先找到。比如一个模块需要调用kmalloc(),内核内存的历程。在建立的时候,模块不知道kmalloc()在内存的那个部分,所以当模块加载的时候,内核要保持所有的内核资源再内核代码表格中所以它就可以解决这些加载模块的资源的问题。Linux也允许模块堆栈,这就是模块需要另一个模块的帮助。比如VFAT文件系统模块需要FAT文件系统模块的帮助,VFAT文件系统多少是FAT文件系统的扩展。一个模块需要另一个模块的服务或者资源的时候,和它需要从内核当中获得服务或资源差不多。只有在这里需要的服务是在另一个之前加载过的模块。每一个模块加载之后,内核都会修改内核符号表格,添加到资源或者符号表格,被新加载的模块输出。这就意味着,当下一个模块加载的时候,它就有了所有已经加载了的模块的服务。
当要卸载一个模块的时候,内核就需要知道模块不需要使用了,需要用某种方法证明模块要被卸载。这个方法模块就可以在从内核中删除之前释放任何系统资源,比如内核内存或者是中断。当模块写在之后,内核删除任何从这个模块输出到内核的符号。
除了加载有害的模块会摧毁操作系统,还有另外的危险。如果你加载一个以前有的并且正在运行的模块,这就会引起一些问题,比如模块进行内和历程调用,并且提供错误的参数。内核可以选择保护,在加载之前检查版本。
第二节 加载和卸载模块
1. 加载模块
有两种方法可以加载内核模块。第一个方法就是使用insmod命令手动添加到内核。
第二种方法就是更加聪明的方法,就是加载需要的内核;这叫做需要加载。
当内核发现需要某个模块的时候,比如当用户安装了一个不在内核当中文件系统的时候,内核就要需要内核线索(kmod)加载合适的模块。
当内核需要一个模块的时候,kmod就醒来并且execve()s modprobe, 传导需要的名称。Modprobe是一个处理加载内核的高级层面。使用“makefile”depmod生成的文件,自动从目录树中加载相关的模块。
Modprobe会自动加载模块堆栈所需要的所有基本模块,就像文件模块.dep。如果加载这些模块当中的一个失败,整个的当前模块堆栈就会恢复到加载之前的状态,删除之前的失败操作。
Modprobe有两个加载模块的方法。一个方法是加载一个列表中的模块。MOdprobe在加载成功之后停止加载。这就是用自动加载Ethernet驱动器。另一个方法就是加载所有列表中的模块。
调用insmod加载模块到内核。内核模块连接到墓地文件,就像系统其它的程序一样,只是程序是以可重置图表来连接的。这就是,没有联接到运行的特殊地址的图标。它们可以是a.out或者elf各式的目的文件。Insmod有优先权系统调度查找内和输出符号。
这些被一对一对的保存在符号名和值内,比如它的地址。内核的输出符号表格被放在第一个模块的数据结构,在内核维护的模块表格之内,被module_list指针指向。
只有一个特殊的输入符号被添加到这个表格,就使内核编译和链接的时候建立的,不是每个符号在内核内都输出代模块。一个例子符号就是“request_irq”就是内和历程必须被调用,当一个驱动器要控制一个特殊的系统中断。在我当前的内核,有一个特殊的值0x0010cd30。你可以在/proc/ksyms中或者是使用ksyms容易地看到输出的内核符号和它们的值。Ksyms用法可以给你看所有的输出内核符号或者只是那些加载模块输出的符号。Insmod读取模块到虚拟内存并且修没有解决的问题到内核历程并且从内核使用输出符号资源。这些维修使用模块图标修补。Insmod物力上写入符号地址到适当的模块位置。
当insmod修补了模块的地址输出内核符号,它就要求内核给它足够的空间保存新的内核,然后使用优先系统调度。内核分配一个新的模块数据结构还有足够的内核内存来存储新的模块,并且把它放在内核模块列表的最后。新的模块就称为UNINITIALIZED.
图10.1表现了内核在添加两个模块之后的图表,VFAT和VFAT被加载到内核当中。不是在第一个模块的位置,那是PSEUDO-MODULE只是保存内和输出符号表格。你可以使用命令LSMOD列出所有的加载的内核模块和它们的相互关系。LSMOD在/proc/modules中,从内核模块数据结构中建立。内存,内核分配的就映射到insmod进程地址空间,所以就可以访问到它。insmod复制模块到分配的空间,并且重置它,所以它就会从分配的内核地址运行。这个模块不可以在同一个地址内加载两次,就更不用提在不同的Linux系统中加载在同一个地址了。而且,这个重置包括了使用恰当的地址修复模块图标。
新的模块也输出符号到内核和insmod建立一个表格给这些输出的图标。每个内核模块都包括模块初始化和模块清理历程,这些符号可以不被输出但是insmod必须知道它们的地址,所以就可以把它们传输到内核。所有这些,insmod现在都准备好了初始化模块,并且传递模块初始化和清理历程的地址,内核地址作出优先的系统调度。
当一个新的模块添加到内核中,就必须升级内核的符号并且修改模块,以让它们适用于新的模块。模块有其它模块的依赖必须在最后的符号列表中保持一个相关列表,由module数据结构指针指到这个地址。图10.1显示了VFAT文件系统模块依赖在FAT文件系统模块之上。所以FAT模块包括一个到达VFAT模块的地址;在VFAT模块加载的时候地址被添加。内核调用模块初始化历程,如果成功,就执行安装模块。模块清理历程抵制存储在它的模块数据结构中,在模块写在的时候就会被内核调用。最后,模块的状态就设置位RUNNING.
2. 卸载模块
通过RMMOD命令模块可以被删除,但是通过kmod在模块不被使用的时候,需要的加载模块就会被自动地从系统中删除。每次发呆器过期的时候,kmod都会发出系统调用请求所有没有使用过的需要加载的模块从系统中删除。
只要其它的内核以来一个模块的某一个原件,这个模块就不可以被删除。比如,你不可一删除VFAT文件系统模块如果你有安装一个或者以上的VFAT文件系统。如果你看lsmod的输出,你就可以看到每个模块都有一个数字。比如:
Module: #pages: Used by:
Msdos 5 1
Vfat 4 1(autoclean)
Fat 6 [vfat msdos] 2(autoclean)
这个数字就是内核中所有依赖在其之上的模块数量。在以上的例子当中,vfat和msdos模块都是依赖在fat模块上的,所以就被算作是2。Vfat 和msdos模块都有一个依赖模块,就是一个安装的文件系统。如果我加载另外一个VFAT文件系统所以vfat模块的数字就成为了2。模块的数字在第一个图标的longword。
这个领域也被加载有AUTOCLEAN和VISITED标注。两个标注都用来要求加载的模块。这些模块被标注为AUTOCLEAN所以系统就认为它们可以自动卸载。VISITED标注表示模块在被一个或者更多的系统原件使用;在不论什么时候另一个原件使用模块的时候都开始设置。每次kerneld要求系统删除没有使用过的加载模块的时候,它就会看所有这些模块。它看那些有AUTOCLEAN在RUNNING状态中。如果被检查的模块有VISITED标注,那么就会被删除,否则就会清理VISTITED标注然后查找下一个模块。
设想一个模块被卸载了,它的清理历程也被调用,使房内何曾分配给它的资源空间。
模块数据结构标注为DELETED并且不可以被从内核模块表链接。其它依赖在这个模块上的地址列表也被重置,直到没有任何依赖在它之上的模块。所有这个模块需要的内核内存都被释放。
第三节 参数传输
在一般情况下,一个模块用自己的功能,不用输出任何的符号。你会需要输出符号,然而,不论什么时候起它的模块可能从事用它们获益。你可能会需要包括特殊的指令来避免输出没有数据的符号,很多版本的模块仅仅通过默认输出所有的符号。
LINUX内核题注文件提供便捷的方法来管理你的符号,降低namespace污染,和提高适当信息的隐藏。
如果你的模块输出没有符号,就要考虑在你的资源文件中放一条有宏调度线:
EXPORT_NO_SYMBOLS;
宏扩展到汇编器,可以在模块之内的任何位置显示。可移动的代码,然而,要放在模块初始化功能之内(init_module),因为这个宏的版本决定了sysdep.h的更老的内核可以在那里工作。
如果你需要从你的模块中输出一些符号,第一步就是确定处理器宏EXPORT_SYMTAB.这个宏必须在包括module.h前确定。在编译时间确定-D编译器标注在makefile很平常。
如果EXPORT_SYMTAB确定之后,单独的符号就和一些宏输出:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_NOVERS(name);
宏的每个版本都可以给符号有效的输出,第二个版本(EXPORT_SYMBOL_NOVERS)输出富豪就没有版本信息了。
第四节 练习
实现LINUX未删除模块
你有没有这样的经历,一不小心,你删除了一个有用的文件,被删除的文件不能被恢复,所以你就要使这连接RM命令在SHELL中:
Function rm
{
Timestamp=$(date +%s)
Mkdir /tmp/$timestamp
Mv$1 /tmp/$timestamp
}
不是直接的删除文件,这个功能会删除在/tmp目录下的文件。所以你就可以从/tmp目录中重新找回你的文件。
但是你可能在某些情况下使用不了,程序直接使用unlink()系统调用(比如使用文件管理器)。所以我们就要写一个模块来主动修改系统调用。当模块被加载的时候,系统调用unlink()就被修改,比如移动一般的文档到/tmp目录。当模块被加载之后,系统调用就表现得平常。
注:系统调用是一些存储在sys_call_taable[]之内的功能性指针。如果只阵地之改变,那么功能也就改变了。你要修改内核所以sys_call_table[]的内容才可以和模块一起改变。
还有,你要写一个用户程序来重置进程。比如:
%undelete –l 23/07/03
The following files are deleted on 23/07/03
Timestamp File size
1058955673 /home/thiz/abc.txt 234
1058955673 /home/thiz/test.txt 110
1058955673 /home/thiz/test2.txt 1021
1058955900 /public/abc.ps 12345
1058955900 /public/abc.txt 1245
%undelete –t 1058955673
Abc.txt undelete successfully
Test.txt undelete successfully
Test2.txt undelete successfully
%undelete –n /public/abc.txt
Abc.txt undelete successfully
这个程序可以列表并且处置属于当前使用者的文件,或者所有的root文件。
第十一章 对称多进程
尽管处理器处理进程的速度在提高,但是还是应用程序需要比单一一个处理器可以提供的更多的处理器资源。这个问题的解决方法就是在一个多任务系统中使用几个进程,以达到真正的平行进程任务。如同在所有的平行系统内,执行没有通过同时处理几个任务提高速度。二是操作系统要提高责任来分配所有的进程,使一些进程可以不影响其它的进程处理。
第一节 Intel多进程处理器
大多数当前有效的多任务处理器的主板使用Intel处理器。它的处理器已经有一些内部功能支持多进程操作,比如同步安装,处理期间中断处理,和原子操作查找,设置和在主内存交换值。安装同步在特殊的SMP设备中在内核实现。
Intel的多任务处理器MP1.4[SMP]定义了硬件和软件的相互左右,使其SMP发展操作系统,使这些系统可以在新的硬件运行。这个特别点的目标就是生成一个多进程的平台100%能够和PC/AT兼容。
*内存对称 内存是当所有进程共享同一个内存空间并且访问同一个地址的时候对称的。内存对称提供了一个非常重要的特点:给所有的处理器能力来执行但操作系统的副本。任何存在的系统和应用软件要执行同一个,不管系统内处理器的数量。
*I/O对称 I/O对称适当所有处理器共享访问同一个I/O支系统的时候(包括I/O ports和中段控制台)任何处理器可以从任何资源接受中断。一些多处理器系统有对称访问内存就是和I/O中断对称,因为它们用一个处理器中断功能。I/O对称帮助潜在的I/O瓶颈,所以提高了系统的可伸缩性。
第二节 多处理器系统的问题
对于任何内核功能要提供内部的加锁和保护自己的数据,避免两个进程同时进行升级,使用统一部分的内存快。在Unix系统中,有两个方法解决这个问题。传统的Unix系统使用相关的coarse-grained锁;有时候甚至整个内核被锁住,所以只有一个进城个已在内核内执行。有更多优点的系统使用更好的锁,必须是高附加值的并且一般用在多处理器和实时操作系统中的。在这点,好的锁就降低了所必须使用的时间,允许特殊的重要任务延迟的时间。
在Linux内核之内包括了一些保证。没有进程在内核内运行就会先被另一个内核模式进程清空,除非她自己进入睡眠模式。这就保证了内核编码对更加有效的院子对其它进程的尊重并且简化了很多操作。第二点中断有可能清除一个内核运行的进程,但是经常恢复到该进程。一个内核的进程模式可能不能保证中断不在内核任务时候清空。这就是中断运行或者被其它中断清空。
首先,SMP内核选择继续这些基本的保证来使初始化操作和发展变得容易。单锁保持所有的处理器。这个锁需要访问内核空间。任何处理器可能保存并且重新访问内核,为中断和其它服务,直到锁终止。这个锁就确保了内核态进程不会被提前清空,排除中断在内核态表现正确。这就保证了只有处理器处理可以在内核态处理锁,只有内核模式进程可以中止中断,只有有锁的处理器才可以处理一个中断。
这个设计使I/O加强应用程序的能力,因为CPU时长在内核态内成为了瓶颈。更好的锁就是最新的内核的。只有这可以保证更好的平行和最终更好的系统表现。当前的Linux多处理器执行就达到了好的表现给CPU加强进程,大多数时间在用户态,还有进程有更大的I/O数量。
第二节 转换到内核
内核改变成为一般化SMP支持转换和架构特殊改变需要兼容不同的Linux支持的处理器。
1.内核初始化
第一个问题就是多处理器内核开始其它的处理器。Linux/SMP确定单处理器进入一般内核项点start_kernel().其它的处理器可能不开始,或者去访问了其它的地方。第一个处理器从一般的Linux初始化开始,开始设置分页,中断和陷阱处理器。在得到启动CPU的处理其信息之后,架构功能:
Void smp_store_cpu_info(int processor_id)
被调用来存储关于处理器的任何信息到每一个处理器组。
有完整的内核初始化架构特殊功能
Void smp_boot_cpus(void)
被调用,被期待开始其它的处理器并且使其进入start_kernel()还有分页寄存器,和其它的控制信息正确的加载。每个处理器跳过设置,除了调用陷阱和irq初始化功能需要一些处理器来正确的设置每一个CPU。这些功能可能会在当前内核复制它的时候修改信息。
每个附加的CPU调用架构功能:
Void smp_callin(void)
进行任何最终设置然后在启动处理器forks off的时候为其它的处理器spin处理器。这因为调度器假设总有一些进程在运行,所以很重要。生成这些线程派生了init架构特点
Void smp_commence(void)
功能调用。任何最终的设置和显示了系统多处理器模式现在被激活。所有的处理器围绕着smp_callin()功能被释放运行发呆进程,它们将要在没有真实任务进程的时候运行。
2.调度
ThizLinux调度器展示了巨大的改变。O(1)调度提供了很好的SMP可伸缩性,通过使用分开的运行队列,并且锁住每个CPU。所以调度和环境转换完全是在平行状态,没有任何的交互锁。所有的相关调度数据都有最大化的结构伸缩性。
而且调度器还比SMP有更好的兼容性。旧的调度器有一个特殊的缺点就是它会引起任务在CPU if/when当更高优先权的任务来临的时候引起即时反弹。新的调度器解决了这个问题,用在每个CPU基本上,分配时间切片,不用任何全球性的同步或者重新计算。
3.中断处理
中断用I/O-APIC被分配到处理器。在系统开始的时候,然而,所有的中断只是通过BSP转发。每个SMP操作系统必须转换APIC到SMP模式,所以其它的处理器就可以处理中断。LINUX支持这个操作模式,所以终端就可以分配给所有有效的处理器。