BNN (enthusiast)
01-11-21 08:40
|
[精华] Open Source Book: Linux Kernel (Chinese Version)
|
| |
Linux 核 心
"First, they ignored us; then they laughed at us; then they fighted against us; then we win." --From 1st Linux Conference
------------------------------------------------------------------------
原著:David A Rusling
编译: Left Blank Intentionally, -BNN
------------------------------------------------------------------------
译者的话:
历尽半年多,《Linux 核心》终于和大家见面了.作为译者,心中非常高兴。 基于Linux 核心2.0.33, 本书介绍了Linux核心是如何工作的。它不是一本关于核心的手册,而是描述了Linux核心中使用的原理,机制和Linux为什么使用这些原理和机制。希望本书能给读者带来些益处。
编译本书的过程中,我们没有局限于原作者的内容,加入了一些译者自己的理解。由于我们的专业和英语水平有限,疏漏之处在所难免。敬请读者谅解并望指出。请给我们发 Email或在我们的BBS上留下您的意见。谢谢。
本书版权属于GPL性质。故读者可以在非赢利的目的下随便拷贝和传播。但请表明出处,以尊重我们一字一字的劳动。
谢谢,
译者全体敬上
12/1/1999
--------------------------------------------------------------------------
--------------------------------
I Love Operating System......:-) Edited by BNN on 11/24/01 11:41 AM.
文章选项:
|
BNN (enthusiast)
01-11-21 08:50
|
|
前言
-----------------------------------------------------------------------
Linux是Internet的产物,从属于一个学生(Linus orvalds)的个人爱好演变成为一 个当今最流行的免费操作系统。对许多人而言,Linux是乎是个迷。 一个免费的东
西怎么会有价值?在一个被一群软件巨头统治的(系统)软件王国里,一个由一些电 脑hackers编写的操作系统如何能够参与竞争?一个由不同的国家许多不同的人编写
的软件如何能够保持其稳定性和高效性?这里的答案是Linux具有非常好的可靠性, 高效性和竞争能力。许多大学和研究机构都在用Linux来作计算。许多人们已在其
PC上安装了Linux。绝大多数公司都或多或少地在使用Linux。Linux被广泛地用来浏 览web站点,文件处理,发送email ,玩计算机游戏。Linux绝不是一个计算机界的 玩具,而是一个由全世界的爱好者开发的非常完善的,专业化的操作系统。
Linux 的源头可以追溯到Unix 家族。1969年,贝尔实验室的研究人员Ken Thompson其 开始在一台空闲的PDP-7机器上实验其多用户,多任务的操作系统(multi-user, multi-tasking operating system) 。不久Dennis Richie和其他两位同事加入了他的行列。他们与 实验室中的其他同事一道开发出了最早期的Unix版本。Richie在早期的项目MULTICS 中发挥了很大的作用。Unix其实是MULTICS的双关语。早期的Unix是用汇编写的。第
3版时采用了C语言。C语言是Richie设计并编写的,以用来作为编写操作系统设计的 语言。用C改写过的Unix使得Unix可以被移植PDP-11/45 和DIGITAL 11/70 计算机上。
Unix移植到DIGITAL 11/70是一个历史性的转折,使得Unix正式从实验室走向大型机 计算环境。很快,绝大多数的计算机制造商都发布了其相应的Unix版本。
Linux诞生的原因极其简单。Linus Torvalds,Linux的作者和主要管理者,当时穷的只能够付的起Minix。Minix是一个非常简单,Unix风格的,被广泛用在教学上的操作系统。Linus对Minix的功能不是很满意(译者注:不知Andrew Tanenbaum看见这句话会有何感想。“后生可畏?”。有兴趣的读者可以访问Andrew的主页.)
决定自己动手编写一个软件。因为在学校里每天用的都是Unix, 所以他选择将Unix作为他的软件的模型.最开始的工作是在一台Intel 386 的PC机上。他的进展很迅速。
Linus对他所做的事情充满了兴趣,并通过刚刚出现的,还局限在学术领域的计算机网络,将已有的代码共享给其他的学生。 其他的人看见了Linus的软件并开始加入开发的行列。 不同的人由于在使用Linux时碰到不同的问题,所以这个软件也就不断地被更新和完善。
不久之后,Linux就成为一个完整的操作系统了。值得注意的是Linux中没有任何Unix代码,而是根据POSIX标准重新编写的Linux中使用了许多在Cambridge, Massachusetts的Free Software Foundation的提供GNU软件。
多数人仅把Linux 当做一个简单的工具来使用。也有许多用户在Linux进行应用程序的开发。只有很少数的人敢于为Linux写设备驱动程序和核心的patches(译者注:这
个词不太好翻译)。 Linus Torvalds接受来自任何人和地方的关于核心的补充和修改。 这似乎有点象无政府主义。当Linus严把质量关并由他自己将新的代码加入核心。当然,在任何时候,从事Linux核心开发的人员毕竟是只有一少部份人。
大多数Linux用户似乎不关心这个操作系统是如何构造和运行的。这不是个明智的决定因为学习Linux是更多理解一个操作系统功能的有利途径。Linux不仅仅是设计的
很好,更重要的是其源代码是公开的。这是因为虽然作者拥有这个软件的版权,但在Free Software Foundation's GNU Public License的基础上,源代码是可以免费
获取的。对刚接触源码的人,起初会觉得迷惑当看见一些叫做kernel,mm,和net的子目录。它们含有些什么?这些代码是如何工作的?为了解决上述问题,我们需要
的是对整个Linux的结构有个总体的了解。这也正是本书的目的:提供一个关于Linux核心如何工作的清楚的画面,从而使得当你在运行一些应用程序时,你可以明白操作系统核心处正在发生着什么(译者注:这一点是为什么要鼓励应用程序开发者了解操作系统的结构。例如,不理解操作系统的调度,很难想象一个应用程序开发者可以
编出一个高效的多线程并发程序)。
(此处译者略去原作者的自我介绍,有兴趣的读者可以访问其site)
这本书讲述的是Alpha AXP-based Linux版本(译者注:这是因为作者在写此书时在Digital公司(后来被Compag吞并;著名的Alta Vista seearch engine出自Digital)任
职。然而95%的Linux核心源代码是与具体的硬件平台无关的。换而言之,本书95%的内容是讲述与硬件无关的Linux核心。
关于读者
本书不要求读者必须具备任何知识和经验的前提。我们假设读者会在需要时补充相关的知识。具备一定的计算机知识和C语言功底将帮助读者更好地理解本书。
本书的组织
本书并不是关于Linux内部的手册。它是关于通常意义上的操作系统,特别是对于Linux的介绍。每一章节按照从”普通到特殊“的规则来布局。首先在具体介绍细节之前,我们给出一个对核心子系统的综述。
关于核心的具体算法被特意地略去。有兴趣的读者可以参阅具体的源代码。本书将重点放在核心数据结构和其之间的关系上。
本书每一章都相对独立,就象每一个Linux子系统完成相对独立的功能一样。当然章节之间也是相关联的。例如,你不能很好地描述一个进程如果不理解虚拟内存是如
何工作的。
硬件基础章节给出一个对当代PC的简要介绍。一个操作系统必须工作在一个硬件基础上。操作系统中的某些功能是与硬件相关的。为了理解Linux操作系统,读者需要
具备一定的底层硬件知识。
软件基础章节介绍了基本的软件知识原理并论及了汇编和C语言。它们都是编写一个操作系统不可缺少的工具。另外本章给出了对操作系统的功能和目标的一个综述。
内存管理章节描述了Linux如何管理系统中的物理和虚拟内存。
进程章节描述了什么是一个进程,Linux核心如何创见,管理和删除系统中的进程。
进程之间,进程与核心之间通过通讯来调整/安排它们的行为。Linux支持很多种进程间通讯--IPC(Inter-Process Communication)机制。信号与管道是其中的两种Linux还支持System V IPC机制。关于IPC机制的内容在IPC章节描述。
PCI标准是一个高效的PC总线。PCI章节讲述了Linux核心如何初始化合使用PCI总线和系统中的PCI 设备。
中断和中断处理章节探讨了Linux核心如何处理中断。尽管操作系统核心里提供一些通用的机制和接口来处理中断,一些关于中断处理的细节是与硬件体系结构有关的。
Linux的优点之一是它支持许多硬件设备。设备驱动程序章节描述了Linux核心如何控制系统中的物理设备。
文件系统章节讲述了Linux核心如何维护其文件系统中的文件,描述了虚拟文件系统(Virtual File System)和如何支持Linux核心中的真正文件系统。
网络与Linux是一对同义词。从某种真实的意义上讲,Linux是Internet或World Wide Web (WWW)的产物。Linux的开发和使用 者通过web来交换信息和代码 。Linux=通常被用来支持机构的网络需求。网络章节描述了Linux如何支持TCP/IP协议族。
核心机制章节探讨了Linux核心需要支持/完成的一些通用任务和机制,以用来使得核心其他部份有效地工作在一起。
模块章节讲述了Linux如何动态地装载功能模块,例如文件系统。
源代码章节描述了对应于特定的核心功能的Linux核心源代码地址。读者可以依据本章的介绍来开始阅读源代码。
本书的约定
serif font 用户必须输入的命令或文本。
Type font 数据结构和数据结构中的域。
本书中所引用的源程序都基于相对路径/usr/src/linux。如foo/bar.c,则其实际的路径是/usr/src/linux/foo/bar.c。
商标
Caldera, OpenLinux and the ``C'' logo are trademarks of Caldera, Inc.
Caldera OpenDOS 1997 Caldera, Inc.
DEC is a trademark of Digital Equipment Corporation.
DIGITAL is a trademark of Digital Equipment Corporation.
Linux is a trademark of Linus Torvalds.
Motif is a trademark of The Open System Foundation, Inc.
MSDOS is a trademark of Microsoft Corporation.
Red Hat, glint and the Red Hat logo are trademarks of Red Hat Software,
Inc.
UNIX is a registered trademark of X/Open.
XFree86 is a trademark of XFree86 Project, Inc.
X Window System is a trademark of the X Consortium and the Massachusetts
Institute of Technology.
感谢
I must thank the many people who have been kind enough to take the time
to e-mail me with comments about this book. I have attempted to incorporated
those comments in each new version that I have produced. Special thanks
must go to John Rigby and Michael Bauer who gave me full, detailed review
notes of the whole book. Not an easy task.
--------------------------------
I Love Operating System......:-) Edited by BNN on 11/21/01 08:52 AM.
文章选项:
|
BNN (enthusiast)
01-11-21 08:53
|
|
第1章 硬件基础
一个操作系统必需紧密地和其支撑--硬件系统结合在一起。操作系统需要一些只能由硬件
提供的一些服务。为了很好的理解Linux操作系统,读者需要明白一些低层的硬件知识。
本章对当代PC系统的一些硬件作一个介绍。
1975年的1月,当“Popular Electronics”杂志在其封面上给出Altair 8080的照片后,一场革
命就开始了。
Altair 8080当时的价格是非397美金。对于今天而言,其Intel 8080的处理器,256字节的内
存,没有屏幕和键盘的配置是微不足道的。它的发明者Ed Roberts将其新的发明称之为
“个人计算机”。
计算机爱好者看见了Altair得潜力并开始为它写软件和在其上为它配置硬件。对这些先驱
者来说,这是一种摆脱呆版的基于批处理的大型机的自由。一些退学的在校生通过这些
个人计算机一夜之间获得了巨大的财富。市场上出现了大量的硬件设备。软件hackers们
非常高兴地为这些新机器编写软件。有趣的是,是IBM公司在1981年制造了当代PC--IBM
PC并在1982年交给用户使用。当时IBM PC的配置是Intel 8088处理器, 64K内存(可扩展到
256K),两个软磁盘和一个80字符,25行的CGA显示适配器。1983年,IBM推出了IBM
PC-XT,含有一个10M的硬盘。不久之后,许多公司,如compaq,推出了IBM PC的兼容机。
PC的体系结构变成了一个工业标准。这个工业标准使得大量的硬件公司在一个基础上进
行竞争。从而使的PC价格变得越来越便宜。早期PC的许多结构特徵被现代PC所继承。例
如,即使先进的Intel Pentium Pro系统在开始时也运行在Intel 8086的地址模式下。当Linus
Torvalds 开始写Linux时,他选择了当时最多的,价格也较合理的Intel 80386 PC。
图1.1 一个典型的PC主板
从外面看一个PC,最明显的部件是一个系统主机,键盘,鼠标器和 一个显示器。大多数系统还
有CD ROM。如果你想要保护数据,还可以有一个磁带驱动器用来做数据备份。这些设备通称为外设。
虽然CPU是系统的主要控制部件,它不是系统中唯一具有智能的。所有的外设控制器,例如,IDE控
制器,都有一定的智能成份。在一个PC的内部(如图1.1所示),读者可以看见一个含有CPU的主板,
内存和一些ISA或PCI外设控制器插槽。有些控制器,如IDE磁盘控制器,有可能被直接作在系统板上。
1.1 CPU
CPU,或微处理器,是任何一个计算机系统的心脏。CPU通过读取并执行内存中的指令来进行计算,执
行逻辑运算和管理数量流。
(以下略去关于CPU的介绍,有兴趣的读者请参考有关书籍)
1.2 存储器
所有的系统都有一个存储器的层次结构。每一层的速度和大小不一样。最快的存储器是缓冲(cache)存
储器,用来暂时存放主存储器的内容。这种存储器非常快但价格很贵,因此大多数处理器在芯片中只含
有较少的cache存储器。更多的则在系统板上。有些处理器在cache中混合存放数据和指令;有些则分开
存放。一个cache为指令;其他一个 cache为数据。Alpha处理器中含有两个内部存储cache;D-cache为
存放数据;I-cache为存放指令。外部的cache(B-cache)混合存放数据。最后,存储体系结构中的是主存
储器。相对于外部cache,主存比较慢。如果与CPU内部的cache比较,主存的速度就象爬一样。
缓冲与主存之间必须保持一致性(coherent)。换句话说,如果主存中的一个字在cache中的一个或多个地
方,系统必须保证cache中的内容与主存中的一致。cache一致性的工作一部份是有硬件完成的,一部份是
有操作系统完成的。这一点对于大多数系统都是一样的。系统中的软硬件必须互相合作完成功能。
1.3 总线
系统板上的部件通过总线相连。系统总线份外三个逻辑功能部份:地址总线,数据总线和控制总线。地址
总线指定数据传送的地址。数据总线负责运送要传送的数据. 数据总线是双向的。允许数据读进CPU和从
CPU写出。控制总线包含一些信号线用来控制时序和系统中的其他控制信号。
1.4 控制器和外设
外设是真实的设备,例如显示卡或磁盘。外设都由系统主板上的控制器所控制。IDE磁盘由IDE控制器
芯片控制;SCSI磁盘被SCSI磁盘控制器所控制。控制器之间,控制器与CPU通过总线相连。大多数系
统通过PCI和ISA总线将系统的部件相连。控制器与CPU一样是一种处理器,可以看作是CPU的智能助手。
CPU是系统的控制中心。
所有的控制器都不相同。但通常它们都有一些用来控制作用的寄存器。在CPU之上运行的软件必须能
够读和写这些控制寄存器。一个寄存器可能包含一个用来描述错误的状态;另一个寄存器可能被用来
作为控制,例如改变控制器的模式。总线上的每个控制器都可以分别地被CPU所访问。从而设备驱动
程序软件可以写上述寄存器以控制这些控制器。
1.5 地址空间
系统总线将CPU与主存相连,这与将CPU与系统硬件外设相连的总线是分开的。总的来说,硬件外设
所占据的存储空间叫做I/O空间。CPU可以既可以存取系统存储空间,也可以存取I/O空间。然而
控制器只能在CPU的帮助下间接地访问系统主存。从设备的观点看,例如软盘控制器,它只能看见
其控制寄存器所在的ISA空间的地址。一般而言,CPU使用不同的指令系统来存取系统存储器和
I/O空间。例如,可能存在一个指令“从I/O地址0x3f0读一个字节到寄存器x”。CPU就是这样通过
读和写来控制系统的硬件外设的。在I/O空间中,一些常用的硬件外设(IDE控制器,串行口,
软盘驱动器等)的寄存器地址已经被固定下来。例如0x3f0正好是串口一(com1)的一个控制寄存器。
有时候,控制器需要在系统主存之间传送大量的数据。例如将数据写入磁盘。这种情况下,直接
储存存取(DMA)控制器被用来使得硬件外设直接存取系统内存。但是这个过程是在CPU的严格控制和
监督之下。
1.6 定时器
所有的操作系统都需要知道时间。所以PC中含有一个特殊的设备叫做实时时钟(Real Time Clock)。
RTC提供两种功能:每天的时间和准确的时间脉冲。RTC有其自己的电平。因此即使PC没开电源,
RTC也在运行。这就是为什么你的PC的时间一直在更新的原因。RTC提供的时间脉冲使得操作系统可以
准确的调度必须的工作。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:55
|
|
第 2 章 软件基础
程序就是一组执行特定任务的计算机指令。程序既可以用非常低级的计算机语言--汇编语
言,也可以用高级的、独立于机器的语言如C来编写。操作系统是一种特殊的程序,它允
许用户运行各种应用程序如制表程序和字处理程序。本章介绍基本的程序设计原理,并对
操作系统的目标和功能做一综述。
2.1 计算机语言
2.1.1 汇编语言
CPU从内存中取出并运行的指令对人来说根本无法理解。它们是精确指示机器如何操作的
机器代码。例如,十六进制数0x89E5是Intel80486的一条指令,把ESP寄存器的内容拷
到EBP寄存器中。汇编器是最早发明的软件工具之一,它输入人类可以理解的源代码,
汇编为机器代码。汇编语言显式地处理寄存器和数据操作,与特定的微处理器相关(应为
与特定的处理器相关--译者注)。IntelX86微处理器的汇编语言就与Alpha AXP微处理器
的汇编语言大相径庭。以下Alpha AXP汇编代码表示了程序可以进行的一种操作:
ldr r16, (r15) ; Line 1
ldr r17, 4(r15) ; Line 2
beq r16,r17,100 ; Line 3
str r17, (r15) ; Line 4
100: ; Line 5
第一条指令(见第一行)把寄存器15存放的地址中的内容装入寄存器16。下一条指令把
内存中下一个位置的内容装入寄存器17。第三行把寄存器16和寄存器17的内容比较,如
果相等,分支转向标号100。如果两个寄存器包含数值不等,程序继续运行第四行,把寄
存器17的内容存到内存。如果两个寄存器包含数值相等,那么没有数据需要保存。编写
汇编语言程序枯燥乏味、技巧性强而且易于出错。Linux核心只有很少的一点用汇编语言
编写,目的是为了效率,这些部份是与特定机器相关的。
2.1.2 C语言和编译器
用汇编语言编写大型程序十分困难而且消耗大量时间。这样做易于出错,得到的程序也无
法移植,限制在特定的处理器族上。用独立于机器的语言如C,会好得多。C允许你用逻
辑算法和其操作的数据结构来描述程序。叫作编译器的特定程序读入C程序,并把它翻译
成汇编语言,生成相应的机器代码。好的编译器所产生的汇编指令的效率接近于好的汇编
语言程序员编写的汇编语言程序。大部份Linux核心是用C语言编写的。以下的C片段:
if (x != y)
x = y ;
与前一个例子中汇编代码的操作完全相同。如果变量x和y的内容并不完全相同,就把y
的内容拷给x。C代码组织为例程,每一个例程执行一个任务。例程可以返回C支持的任
何数值或者数据类型。象Linux核心这样的大型程序包含很多独立的C源模块,每个模块
都有自己的例程和数据结构。这些C源代码模块把象文件系统处理这样的逻辑功能组合在
一起。
C支持很多类型的变量。所谓变量,就是内存中的一个位置,可以用符号名字来引用。在
以上C片段中,x和y指引了内存中的位置。程序员不关心变量究竟存放在内存中的何
处,这是连接器(见下面所述)的任务。一些变量含有不同类型的数据、整数和浮点数,
另一些则是指针。
指针就是包含地址--其它数据在内存中的位置,的变量。考虑叫做x的变量,它可能处于
内存地址0x80010000。你可以有一个指针,叫做px,指向x。px可能处于地址
0x80010030,而px的值是0x80010000,变量x的地址。
C允许你把相关的变量绑在一起,形成数据结构。例如,
struct {
int i ;
char b ;
} my_struct ;
是一个叫做my_struct的数据结构,它包含两个元素:叫做i的整数(32位数据)和叫做b
的字符(8位数据)。
2.1.3 连接器
连接器是一种程序,它可以把几个目标模块和库连接在一起,产生一个独立的、自洽的程
序。目标模块是汇编器或编译器生成的机器代码输出,含有可执行的机器代码和数据,以
及允许连接器把模块连接起来的信息。例如一个模块可能含有程序中所有的数据库函数,
而另外一个则含有命令行参数处理函数。连接器负责解决目标模块之间的引用,例如一个
模块中引用的例程或数据结构事实上在另外一个模块之中。Linux核心就是一个与很多成
员目标模块连接在一起的独立大程序。
2.2 什么是操作系统?
没有软件的计算机就是一堆发热的电子器件。如果说硬件是计算机的核心,那么软件就是
计算机的灵魂。所谓操作系统,就是允许用户在其上运行应用软件的一组系统程序。操作
系统对系统的真正硬件进行抽象,向系统的用户和应用程序给出一个虚拟机。在很现实的
意义上说,软件提供了系统的特点。绝大部份PC能运行一个或多个操作系统,每一个擦
系统都有一个完全不同的外观和风格。Linux是由一批功能上分离的部件组成,其中明显
的一个是核心。但是即使是核心,如果脱离库和外壳程序也是没有用的。
为了开始理解什么是操作系统,请考虑当你敲入以下的简单命令时会发生的情况:
$ ls
Mail c images perl
docs tcl
$
这里$是由登录外壳程序(在此例为bash)给出的提示符。这意味着它在等待你--用户,
敲入命令。敲入ls后,键盘驱动程序识别出已经有字符输入。键盘驱动程序把这些字符传
给外壳程序,外壳程序则通过寻找可执行程序的映象来处理这个命令。它在/bin/ls发现了
映象,于是调用核心服务来把ls可执行程序的映象拖入虚拟内存,开始执行。ls的映象调
用核心的文件子系统,以找出有哪些文件可以获得。文件系统有可能要使用放在cache中
的文件系统的信息或者用磁盘驱动程序来从磁盘读出这些信息,甚至可能用网络驱动程序
与远程机器交换信息,以找出本系统能够存取的远程文件的细节(文件系统可以通过"网
络文件系统"NFS来远程mount)。无论是用哪种方式定位信息,ls都会把信息写出来,由
视频驱动程序把它显示在屏幕上。
以上看起来很复杂,但是说明了一个道理:即使是最简单的命令,也需要相当的处理,操
作系统事实上是一组互相合作的函数,它们在整体上给用户以一个系统的完整印象。(以
上一句是根据译者理解翻译的,未必忠实于原文--译者注)
2.2.1 内存管理
如果有无限的资源,例如内存,很多操作系统需要做的事情都是冗余的。操作系统的一个
基本技巧是使一小块内存看起来象很多内存。这种表面上的大内存称为虚拟内存。其思想
是使系统中运行的软件以为它在很多内存上运行。系统把内存分成很多容易控制的页面,
把一些页面交换到硬盘上。由于另外的一个技巧--多道处理,软件注意不到这一点。
2.2.2 进程
进程可以想象为一个活动中的程序。每一个进程是一个独立的实体,在运行一个特定程
序。如果你看看你的Linux系统中的进程,你就会发现一大堆。例如,在我的系统中敲入
ps可以显示如下进程:
$ ps
PID TTY STAT TIME COMMAND
158 pRe 1 0:00 -bash
174 pRe 1 0:00 sh /usr/X11R6/bin/startx
175 pRe 1 0:00 xinit /usr/X11R6/lib/X11/xinit/xinitrc --
178 pRe 1 N 0:00 bowman
182 pRe 1 N 0:01 rxvt -geometry 120x35 -fg white -bg black
184 pRe 1 < 0:00 xclock -bg grey -geometry -1500-1500 -padding 0
185 pRe 1 < 0:00 xload -bg grey -geometry -0-0 -label xload
187 pp6 1 9:26 /bin/bash
202 pRe 1 N 0:00 rxvt -geometry 120x35 -fg white -bg black
203 ppc 2 0:00 /bin/bash
1796 pRe 1 N 0:00 rxvt -geometry 120x35 -fg white -bg black
1797 v06 1 0:00 /bin/bash
3056 pp6 3 < 0:02 emacs intro/introduction.tex
3270 pp6 3 0:00 ps
$
如果我的系统有很多CPU,每个进程(至少在理论上)可以运行在一个不同的CPU上。
不幸的是,我只有一个CPU,所以系统只能求助于让每个进程轮流运行一小段时间的办
法。这一小段时间称为时间片。这种技巧称为多道处理或者调度,它使得每个进程以为自
己是唯一的进程。在进程之间进行保护,以便当一个进程崩溃或者错误时,不会影响其它
进程。操作系统给每个进程一个单独的、只有它自己能存取的地址空间,以达到这样的目
的。
2.2.3 设备驱动程序
设备驱动程序构成了Linux核心的主要部份。就象操作系统的其它部份一样,设备驱动程
序在高优先级的环境下运行,一旦发生错误就可能造成危险。设备驱动程序控制操作系统
和其控制的硬件设备之间的互相作用。例如,在把块写到IDE硬盘时,文件系统使用一个
通用块设备接口。驱动程序负责细节,控制与设备相关的事情。设备驱动程序是针对其控
制的特定芯片的,所以如果你有一个NCR810 SCSI控制器,那么你就需要一个NCR810
SCSI驱动程序。
2.2.4 文件系统
在Linux中,就象在Unix中一样,系统可以使用的不同的文件系统并非通过设备标识来
存取(例如驱动器号或驱动器名),而是被组织在一个单一的分层树结构里,每个文件系
统用一个实体来表示。文件系统通过把新的文件系统mount在某个目录下--例如
/mnt/cdrom,从而把新的文件系统加入树中。Linux的最重要特点之一是支持多个不同的文
件系统,这使得其伸缩性好,易于与其它操作系统共存。最广为人知的Linux文件系统是
EXT2,受到所发行的绝大部份的Linux的支持。
文件系统屏蔽了下层物理设备或文件系统类型的细节,给予用户察看硬盘上文件和目录的
一个清楚视角。Linux透明地支持多种不同文件系统(例如MS-DOS和EXT2),把所有
mount上的文件和文件系统都组织在一个虚拟文件系统之中。所以,一般而言,用户和进
程并不需要知道哪个文件在哪个文件系统之中,只需要直接去用它就可以了。
块设备驱动程序隐藏了物理块设备类型之间的差别(例如IDE和SCSI的差别),从文件
系统的角度来看,物理设备只是数据块的线形聚集。不同设备的块大小不同,例如软盘通
常是512字节,而IDE设备通常是1024字节,但是系统的用户是看不到这一点的。无论
放在什么设备上,EX2文件系统看起来都一样。
2.3 核心数据结构
操作系统必须保存关于系统当前状态的很多信息。在系统中发生了事情之后,这些数据结
构必须修改,以反映当前的真实状况。例如,在一个用户登录到系统之后,可能会创建一
个新的进程。核心必须创建表示新进程的数据结构,并把它和表示系统中所有其它进程的
数据结构连接在一起。
通常这些数据结构存在于物理内存之中,只能由核心及其子系统存取。数据结构包括数据
和指针--其它数据结构的地址或例程的地址。总而言之,Linux使用的数据结构看起来很
复杂难懂。虽然其中某些可能为几个核心子系统所使用,但是每个数据结构都有其目的,
所以这些数据结构事实上比刚看起来要简单。(以上一句是根据译者的理解翻译,未必正
确和符合原文--译者注)
理解Linux核心依赖于理解其数据结构以及核心中各种函数对数据结构的使用。本书对
Linux核心的描述就是基于其数据结构的。本书讨论了每个核心子系统的算法、完成工作
的方法和对核心数据结构的使用。
2.3.1 链表
Linux使用一系列软件工程技术来把数据结构连接起来。在很多情况下要使用链表。如果
每个数据结构描述某事物的一个实例或一次发生,例如一个进程或一个网络设备,那么核
心就必须能够找到所有的实例。在链表中,根指针含有表中第一个数据结构,或者称为
“元素”,的地址,而每个数据结构都含有一个指向表中下一个元素的next指针。最后一
个元素的next指针为0或NULL,以示它已经是表尾。在双链表中,每个元素含有指向表
中下一个元素的next指针和指向表中前一个元素的previous指针。使用双链表方便了增加
或删除表中间的元素,当然,内存的存取也增加了。这是一个典型的操作系统中的折中:
消耗内存存取与消耗CPU周期的折中。
2.3.2 Hash表
链表可以方便地把数据结构绑在一起,但是浏览链表效率很低。如果你想查找特定的一个
元素,很可能回不得不看完全表才找到需要的那个。Linux使用Hash技术来避开这个限
制。所谓Hash表,是一个指针的数组或者向量。这里说的数组或者向量,是指内存中的
一组顺序存放的东西。因此,书架可以说成是书的数组。数组使用索引进行存取,而索引
就是数组中位置的偏移量。把书架的比喻继续下去,你可以通过在书架上的位置来描述每
本书。例如,你可以要求拿第5本书。
所谓Hash表,是指向数据结构的指针数组,采用数据结构中的信息作为Hash表的索引。
如果你有描述村庄人口的数据结构,那么你可以采用人的年龄作为索引。为了寻找某个特
定人的数据,你可以采用年龄作为人口Hash表的索引,然后按照指针找到含有此人细节
的数据结构。(这里的指针相当于普通教科书上所说的Hash函数:Hash(ad)=*ad,--译者
注)不幸的是,很可能村中的许多人年龄相同,所以Hash表的指针成了指向一个数据结
构链的指针,每个数据结构描述相同年龄的一个人。然而,搜索这些短链还是比搜索全部
数据结构要快。
由于Hash表加快了普通数据的存取,Linux经常使用Hash表来实现cache。cache通常是
总体信息的一部份,被抽出来需要加速存取。操作系统常用的数据结构要放在cache中保
存。cache的不利之处在于使用和维护都比简单链表或Hash表更加复杂。假如能在cache
中找到数据结构(称为“命中”),那么好得很。假如找不到,那么所有相关数据结构都
要被搜索,如果最终能找到,那么该数据结构将被加入cache中。把新的数据结构加入
cache 可能会需要挤出一个旧的cache项入口。Linux必须决定到底挤出哪一个才好,以尽
可能避免恰恰挤出下面要用的数据结构。
2.3.3 抽象接口
Linux核心经常对接口进行抽象。所谓接口,是例程和数据结构的集合,它通过某种特定
方式进行操作。例如所有的网络设备驱动程序必须提供操作某些特定数据结构的某些特定
例程。这样,就能有使用低层特殊代码的通用代码层。例如网络层是通用的,受到遵循标
准接口的与设备相关的代码的支持。
通常这些低层在启动时注册到高层。注册时一般要把一个数据结构加到一个链表中。例如
核心中内置的每个文件系统在启动时或者(如果使用模块的话)首次使用时注册到核心。
通过查看文件/proc/filesystems能够看到哪些文件系统已经注册。注册数据结构通常包含函
数指针。这些都是进行特定任务的软件函数的地址。再次用文件系统注册作为一个例子,
每个文件系统在注册时传给Linux核心的的数据结构包含与文件系统相关的函数地址,这
些函数在该文件系统mount时必须调用。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:55
|
|
第 3 章 内存管理
内存管理子系统是操作系统中最重要的组成部份之一。从早期计算机开始,系统的
实际内存总是不能满足需求,为解决这一矛盾,人们想了许多办法,其中虚存是最
成功的一个。虚存让各进程共享系统内存空间, 这样系统就似乎有了更多的内存。
虚存不仅使计算机的内存看起来更多,内存管理子系统还提供以下功能:
扩大地址空间
操作系统扩大了系统的内存空间。虚存能比系统的实际内存大许多倍。
内存保护
系统中每个进程都有它自己的虚拟地址空间。这些虚拟地址空间之间彼此分开,以
保证应用程序运行时互不影响。另外,虚存机制可以对内存部份区域提供写保护,以
防止代码和数据被其它恶意的应用程序所篡改。
内存映射
内存映射被用于将映像和数据文件映射到一个进程的虚拟地址空间中, 也就是将
文件内容连接到虚地址中。
公平分配内存
内存管理子系统公平地分配内存给正在运行的各进程。
虚存共享
尽管虚存允许各进程有各自的 ( 虚拟 ) 地址空间,但有时进程间需要共享内存。
例如,若干进程同时运行 Bash 命令。并非在每个进程的虚地址空间中,都有一个
Bash的拷贝。在内存中仅有一个运行的Bash拷贝供各进程共享。又如,若干进程可
以共享动态函数库。
共享内存也能作为一种进程间的通信机制(IPC)。两个或两个以上进程可以通过共享
内存来交换数据。Linux 支持 Unix 系统 V 的共享内存 IPC标准。
3.1 一个抽象的虚存模型
图 3.1 :虚地址与物理地址之间的映射
在分析 Linux 实现虚存的方法前,让我们先来看一个没有过多细节的抽象模型。
当处理器执行一段程序时,它先从内存中读出一条指令并对它进行解码。解码时可
能需要在内存中的某一地址存取数据。然后处理器执行这条指令并移向下一条。可
见处理器总是不断地在内存中存取数据或指令。
在虚存系统中,所有地址都是虚地址而非物理地址。处理器根据操作系统中的一组
表格而把这些虚地址翻译成相应的物理地址。
为使这翻译的过程更容易,虚存和物理内存被划分成许多适当大小的块,叫做“页”
(page)。为便于系统管理,这些页都是一样大小的。在 Alpha AXP 上的 Linux 系
统中,每页有 8 Kbyte,但在 Intel x86 系统中,每页有 4 Kbyte。每一页又被分
配了一个各不相同的数字,叫页号 ( PFN ) 。
在本模型中,一个虚地址由两部份组成;偏移量和虚页号。如果页的大小是 4 Kbytes,
那么虚地址的0至11位是偏移量,第12位以上是虚页号。每次处理器遇到虚地址时,
它先取出偏移量和虚页号。然后,处理器把虚页号翻译成物理页号,再由偏移量得
到正确的物理地址,最后存取数据。处理器需要使用页表来完成这整个过程。.
图 3.1 显示了两个进程的虚存地址空间。进程 X 和进程 Y 分别有各自的页表。页
表记录各进程虚页和物理页之间的映射。如图:X 虚存的第0页对应物理地址的第4页。
理论上,页表中每条记录包含以下信息:
-有效性标志。 用以标识页表记录有效与否。
-页号。用以记录对应的物理内存页号。
- 存取控制信息。描述这页该怎样被使用。可否可写?可否包含可执行代码?
页表中使用虚页号作为偏移量。虚页 5 将是表中的第 6 条记录( 0 是第一条记录
)。
把一个虚地址翻译成物理地址时, 处理器必须先得出虚页号和偏移量。页的大小总
是 2 的幂,这便于进行mask和移位操作。图 3.1中, 假定页的大小是 0x2000 字
节 ( 它是十进制的 8192 ),在进程 Y的地址空间中有一虚地址 0x2194。那么处理
器将把这个地址翻译成偏移量为 0x194,虚页号为1。
处理器使用虚页号作为检索进程页表记录的索引。如果对应那偏移量的页表记录是
有效的,处理器就从中拿出物理页号。如果记录是无效的,表明进程想存取一个不
在物理内存中的地址。在这种情况下,处理器不能翻译这虚地址,必须把控制权传
给操作系统,让它处理。
当进程试图存取一个无法翻译的虚地址时,处理器将通知操作系统, 这被称为一个
页错。各种处理器处理页错的方法是不同的,但都会通知操作系统产生页错的虚地
址和原因。
假设找到的是一有效的页表记录,处理器就取出物理页号并且乘以页的大小,得到
内存中页的基地址。最后,处理器加上偏移量得到它需要数据的地址。
例如, 进程 Y 的虚存第1页被映射到内存第 4页,它从 0x8000(4 x 0x2000 )开始。
加上偏移量 0x194 字节就得到最后的物理地址是 0x8194。
由虚地址映射到物理地址时, 虚存各页映射到系统内存中的顺序是任意的。例如,
在图 3.1 中, 进程 X 的虚存第 0 页被映射到内存第1页,而虚存第 7页被映射到
内存第 0页。这说明了虚存的一个有趣现象,虚存各页在物理内存中不必有任何顺
序。
3.1.1 按需装载页(Demanding Paging)
虚存比实际内存大很多,所以操作系统一定要小心有效地使用内存。节省内存的一
个方法是只装载被当前执行程序使用的虚页。例如,有一个用来查询数据库的程序。
此时,并非所有数据库中的数据都需被装载进内存,只需要那些正在被访问的数据。
如果正运行一条数据库搜索命令,那么就不必载入添加新记录的代码。当代码或数
据被访问时才装载进内存,这叫作按需装载页(demand paging)。
当进程试图存取一个不在内存中的虚地址时,处理器不可能在页表中找到这一虚页
的记录。例如,在图 3.1中, 进程 X 的虚存第 2页没有对应的页表记录,如果尝
试对这页进行读操作,那么处理器不能把虚地址翻译成物理地址。处理器就会通知
操作系统页错发生了。
如果页错(faulting) 对应的虚地址是无效的,这意味着进程试图存取它不应该访问
的虚地址。这也许是因为应用程序出了某些错误, 例如试图在内存中任意进行写操
作。在这种情况下,操作系统将终止这个错误进程,以保护其它进程。
如果页错(faulting) 对应的虚地址是有效的,只是它所在页目前不在内存中,操作
系统必须将对应的页从磁盘载入内存。相对来说,磁盘存取会花很多时间,所以进
程必须等待相当一会儿直到页被读入。这时候,如果有其它进程能运行,操作系统
将选择其中之一。被取的页将被读入内存一空页中,并在进程页表中加入一条记录。
然后,进程从产生页错的机器指令重新启动。这次处理器能将虚地址翻译成物理地
址了,因此进程能继续运行下去。
Linux 使用按需装载页来读入可执行进程的映像。一个命令被执行时,包含它的文
件被打开,它的内容被印射入进程的虚存。这操作需修改描述这进程内存映像的数
据结构 (memory mapping)。然而,只有映像的第一部份被实际载入物理内存,余下
部份被留在磁盘上。当映像执行时,它将不断产生页错, Linux 使用进程的内存映
像表来决定哪块映像该被载入内存。
3.1.2 页交换 (Swapping)
当进程要装载一虚页进物理内存时,如果得不到空页, 操作系统必须从内存中丢弃
别的页,为这页提供空间。
如果从内存中被丢弃的那页是从映像或数据文件中来的,并且映像和数据文件没被
修改过,那这页不需再被保存,可以直接丢掉。如果进程再需要那页,它可以重新
被从映像或数据文件中读入内存。
但如果该页已被修改了,操作系统必须保存这页的内容以便它以后能再被访问。这
类页叫作脏 (dirty) 页,当它们被从内存中移出时,它们被作为特殊的交换文件
(swap file) 保存。相对于处理器和内存的速度,交换文件的存取时间是很长的,
所以操作系统必须权衡是否需要把页写到磁盘上,还是保留在内存中以备后用。
如果交换算法的效率不高,那么thrashing现象就会发生。在这种情况下,页常常一
会儿被写到磁盘上,一会儿又被读回来,操作系统忙于文件存取而不能执行真正的
工作。例如,图3.1 中,如果内存第 1页不断被访问,那它就不应该被交换到硬盘
上。进程当前正在使用的页的集合被叫作工作集 (working set)。有效的交换算法
将保证所有进程的工作集都在内存中。
Linux 使用最近最少使用算法(Least Recently Used) 来公平选择从内存中被丢弃
的页。这个算法中,当页被存取时,它的年龄 (aging) 就变化了。页越多被存取,
便越年轻;越少被存取就越旧。旧页通常是被丢弃的好候选。
3.1.3 共享虚存
虚存使得若干进程更容易共享内存。进程所有的内存访问都要通过页表,并且各进
程有各自独立的页表。当多进程共享内存中一页时,物理页号就会同时出现在每个
进程的页表中。
图3.1 中显示两进程共享物理第4页。对进程 X 而言,那是虚存的第 4页,对进程
Y而言,
那是虚存第 6页。这说明一个有趣的现象:被共享的物理页对应的虚存页号可以各
不相
同。
3.1.4 物理和虚拟地址模式
把操作系统运行在虚存中是不明智之举,如果操作系统还要为自己保存页表,那将
是一场恶梦。因此,很多种处理器同时支持虚拟地址模式和物理地址模式。物理地
址模式不需要页表,处理器不必做任何地址翻译。 Linux 内核被直接连在物理地址
空间中运行。
Alpha AXP 处理器没有物理地址模式。相反, 它把内存划分成若干区域并且指定其
中两
块为物理地址区。这段核地址空间叫作KSEG,包括所有0xfffffc0000000000以上的
地址。
在 KSEG执行的 (按定义,核代码 ) 或在那里存取数据的代码肯定是在核模式下执
行。在 Alpha 上的 Linux 核被连接从0xfffffc0000310000开始执行。
3.1.5 存取控制
页表记录中也包含了存取控制信息。处理器使用页表记录来把虚地址翻译成物理地
址的同时,它也很容易地使用其中的存取控制信息来检查进程是否在正确地访问内
存。
在很多种情况下,你想要为内存的一段区域设置存取限制。一段内存, 例如包含可
执行的代码, 应为只读内存;操作系统应该不允许进程在它的可执行的代码上写数
据。相反的,包含数据的页能被写,但是当指令试图执行那段内存时,应该失败。
大多数处理器的执行代码有两种模式:核 态和用户态。你将不想由一个用户执行核
代码,或者让核数据结构被不是核态执行的代码所访问。
图 3.2 : Alpha AXP 的页表记录 (Page Table Entry)
存取控制信息被保存在 PTE中,并且不同的处理器,PTE的格式是不同的;图3.2 显
示的是 Alpha AXP 的PTE。各位包含以下信息:
V 有效位。如果设置,表示这 PTE 是有效的。
FOE (Fault on Execute) 无论何时试图在这页执行指令时,处理器将报告页错,
并且把控制权传给操作系统。
FOW (Fault on Write) 当在这页上进行写操作时报页错。
FOR (Fault on Read) 当在这页上进行读操作时报页错。
ASM(Address Space Match) 地址空间匹配。当操作系统仅仅希望清除翻译缓冲区中
若干记录时,这一位被使用。
KRE 在核模式下运行的代码能读这页。
URE 在用户模式下运行的代码能读这页。
GH 粒度性,指在映射一整块虚存时,是用一个翻译缓冲记录还是多个。
KWE 在核模式下运行的代码能写这页。
UWE 在用户模式下运行的代码能写这页。
页号 在有效的PFE中, 这域包含对应的物理页号 (page frame number )。对无效
的PTEs ,如果这域不是零,它包含了页在交换文件中的信息。
以下两位是 Linux 定义并使用的:
_PAGE_DIRTY 如果设置,页需要被写到交换文件中。
_PAGE_ACCESSED 由 Linux 标记这页是否曾被访问。
3.2 缓存
如果你按照上面理论模型,可以实现一个工作的系统,但不会特别高效。操作系统
和处理器的设计者都在努力提高系统性能。除提高处理器和内存的速度外,最好的
途径是把有用的信息和数据保存在缓存中。 Linux 就使用了很多与内存管理有关的
缓存:
缓冲区
缓冲区包含块设备驱动程序 (block device driver) 使用的数据缓冲区。
这些缓冲区有固定的大小 ( 例如 512 个字节 ) ,记录从一台块设备读或写的信息。
一台块设备只能存取整块数据。所有的硬盘都是块设备。
缓冲区通过设备标识符和需要的块号的索引来迅速发现所需数据。块设备只能通过
缓冲区进行存取操作。如果数据在缓冲区中,那么它就不需要再从块设备中被读(例
如硬盘),这样存取得更快。
页缓存
它被用来加快磁盘上映像和数据的存取。
它被用来一次缓存文件的一页,存取操作通过文件名和偏移量来实现。当页从磁盘
被读进内存时,他们被缓存在页缓存中。
交换缓存
只有修改了的页,即脏(dirty ) 页,被保存在交换文件中。
只要一页在被写进交换文件以后,没有再被修改,下次这页被换出内存时,可以直
接被扔掉。对一个进行许多页面交换的系统,这将节省许多不必要的并且昂贵的磁
盘操作。
硬件缓存
处理器中有一经常用到的硬件缓存:页表记录的缓存。通常情况下,处理器并不总
是直接读页表,而是用页表缓存保留用到的记录。这些被叫做 Translation Look-aside
Buffer,保存了系统中多个进程页表的拷贝。
当翻译地址时,处理器先试图找到一匹配的TLB 记录。如果它发现了一个,它能直
接把虚地址翻译成物理地址,并且对数据进行存取操作。如果处理器不能发现一匹
配的 TLB 记录,那就必须借助操作系统。它发信号给操作系统,报告有一个 TLB
疏漏。特定的机制将把异常信号送给操作系统的代码。操作系统为印射的地址产生
一个新的 TLB 记录。当异常被解决后,处理器将尝试再翻译那个虚地址。因为现在
那个地址在 TLB 中有一个有效的记录,这次的地址翻译一定成功。
使用缓冲区,硬件缓存等的缺点是Linux 必须花费更多的时间和空间来维护这些缓
存, 如果缓存发生错误,系统将崩溃。
3.3 Linux 页表
图 3.3 : 3级页表
Linux页表有3层。每一层负责保存下一层页表所在的页号。图3.3 显示一个虚地址
被分成了很多域;每个域记录在某一层页表中的偏移量。把一个虚地址翻译成物理
地址时,处理器拿出每个域的内容把它变成页表中的偏移量,进而读出下层页表的
所在页号。这样重复 3 次直到找到包含虚地址的物理页号。虚地址的最后一个域,
叫做字节偏移量, 被用来在物理页内找到所需数据。
每个运行 Linux 的平台必须提供翻译宏(Translation macros) 以便内核可以检索
页表,完成某种操作。这样,内核不需要知道各平台上页表记录的具体格式和它们
是怎么被安排的。
这就是为什么 Linux 的 Alpha 处理器和Intel x 86 处理器使用一样的页表操作代
码, 而Alpha有3层页表,Intel x86处理器只有2层页表。
3.4 页的分配和回收
在系统中,对页有许多操作。例如, 当一段映像被装载进内存时,操作系统需要分
配页。当映像执行完成并且被卸掉时,这些页将被释放。页的另外的用途是保存内
核特定的数据结构,例如页表。页的分配和回收机制是维持虚存分系统效率的关键。
系统中所有物理内存页由 mem_map 数据结构描述,men_map由一列 mem_map_t 组成。
在初始化时,每个 mem_map_t 描述系统中的一页。它重要的域如下(有关内存管理
) :
计数器 描述使用这页的用户数。如果计数器比一大,则这页被多进程共享。
年龄 描述页的年龄,被用来决定页是否是被丢弃或交换的好候选。
map_nr 描述这个 mem_map_t 对应的页的物理页号。
页分配代码使用矢量 free_area 来寻找并释放页。这机制支持整个缓冲区管理,对
于代码来说,页的大小和处理器对页的操作机制是与其无关的。
free_area 每个单元都包含一种页块的信息。在数组的第一单元描述单个的页, 下
一单元描述 2 页块,再下一单元描述 4 页的块,并以2的幂上升。表中每个单元作
为一个队头,有指针指向mem_map 数组中的页。空的页块在这里排队。map是指向bitmap的
一个指针,bitmap 记录了这种大小页块的分配情况。位图中,如果第 n 块页是空
的,那么位 N 被置。
图 free_area_figure 显示的是 free_area 的结构,第0单元记录有一个空页,从
第0页开始。第2单元记录有两个4页的空块,第一块从第4页开始,第二块从第56页
开始。
3.4.1 页的分配
Linux 使用伙伴(Buddy) 算法来有效地分配和回收页块。页分配代码被用来分配一
页或多页的块。页的大小总是 2 的幂,即能分配1页, 2 页, 4 页等等。只要系统
中有足够满足请求的空页 ( nr_free_pages >min_free_pages ),分配代码就能在
free_area 里找到所需大小的页块。free_area每个单元有一张分配图 (bitmap)。
例如, 数组的单元 2 有描述长度为4的页块的分配图。
算法寻找所需大小的页块时,它先搜索 free_area 数据结构中那种页块的队列。如
果所需大小的页块没有空,就在下一对列中寻找(页块的大小是所需的两倍)。继续
这一过程直到free_area 中所有单元都被找过了或发现了一空页块。如果找到的空
页块比所需的大,它必须被分割成正确的大小。
图 3.4 : free_area 数据结构
例如, 在图3.4 中,如果需要一 2 页块,那么第一个空的 4 页块 (从第4页起 )
将被分成两半。从第4页开始的 2 页块被返回给请求者;从第6页开始的 2 页块被
排在free_area的空的两页块的队中。
3.4.2 页的回收
页分配时容易将大块连续的内存分成很多小块。页的回收代码须尽可能将小块的空
内存重新组合成大块的。事实上,页块的大小对内存的重新组合很重要。
当一页块被释放时,系统会检查它旁边的和一样的大小的页块,看它们是否是空的。
如果是,它们将被拼成一个大的整块。每次当两块内存被拼成了更大的空块时,页
回收代码尝试将它们与其它空块继续组合,以得到更大的空间。这样得到的空页块
可以满足任何对内存的需求。
例如,在图 3.1中,如果第 1 页被释放,那它将与第0页结合,并被放到 free_area
的两页空块的队中。
3.5 内存印射
当一映像被执行时,它的内容必须被读入进程的虚存。它调用的库函数也必须被读
入虚存。这个可执行文件并非被实际读入内存, 相反它只是被连接入进程的虚存。
然后,当程序的一部份被应用程序调用时,系统才将这部份映像读入内存。将映像
连接到进程的虚地址空间叫做内存印射(memory mapping)。
图 3.5 :虚存
每个进程的虚存空间由一个 mm_struct 数据结构表示。这包含当前正在执行的映像
的信息 (例如 Bash ),还有很多指向 vm_area_struct 的指针。每个 vm_area_struct
数据结构
描述一段虚存区域的开始和结束,及进程对那段虚存的存取权限和允许的操作。这
些操作是Linux 对这段虚存必须使用的一套例程。例如, 当进程试图存取虚存中某
页,但发现这页并不在内存中时,应执行的正确操作是 nopage 操作(通过页错)。
Linux使用nopage 操作可以按需将一页可执行映像载入内存。
当一段可执行映像被印射入进程的虚存时,会产生一组 vm_area_struct 数据结构。
每个 vm_area_struct 数据结构代表可执行映像的一部份;可执行代码, 初始化数据
(变量),未初始化数据等等。 Linux 支持很多标准的虚存操作,当 vm_area_struct
数据结构产生时,系统会把正确的虚存操作集与他们相联。
3.6 按需换页 (Demanding Paging)
当一部份可执行映像被印射入进程虚存后,它就可以开始执行了。可是这时只有映
像的开始部份被实际读入内存,它将不断访问不在内存中的部份。当进程存取一个
没有有效页表记录的虚地址时,那处理器将报页错给 Linux 系统。
页错描述页错发生的虚地址和引起的存取操作。
Linux 必须先找到代表页错发生区域的 vm_area_struct。由于搜索 vm_area_struct
数据结构对高效处理页错非常关键,所以所有 vm_area_struct 被连接成AVL树结构
(Adelson-Velskii and Landis)。 如果没有 vm_area_struct 代表这页错发生的
虚地址, 表示这进程企图访问一个非法的虚地址。Linux 将发送 SIGSEGV 信号给
进程,如果进程没有对应这个信号的处理程序,它将被终止。
Linux 再检查存取操作是否是被允许的。如果进程在用一个非法的方法存取内存,
例如,写一个只读区域,它也将引起一个内存错误信号。
如果 Linux 确定页错是合法的, 它就会处理它。
Linux 必须首先区别映像是在交换文件中还是在磁盘上。它是通过页表记录来区别
的。
如果那页的页表记录是无效的,但非空,说明产生页错的那页当前在交换文件中。
例如, Alpha AXP 页表记录中,这样的记录有效位未置,但是PFN 域不为零。在这
种情况下, PFN 域容纳的信息表示这页被保持在哪个交换文件中的哪里。本章后半
部将讲述怎样处理在交换文件中的页。
并非所有的 vm_area_struct 数据结构都有一组虚存操作,即使有,也不一定有nopage
操作。缺损情况下,Linux 将分配一页新内存,并为这页增加一项页表记录。但如
果这段虚存有 nopage 操作,Linux 将使用它。
通常 Linux 的 nopage 操作被用于把可执行映像通过页缓存读入内存。
当页被读入内存后,进程的页表将被更新。特别是如果处理器使用TLA 缓冲区的话,
它可能需要通过硬件操作来完成更新。页错被处理后,进程在产生页错的指令处重
新开始执行。
3.7 Linux 页缓存
图 3.6 : Linux 页缓存
Linux 页缓存的作用是加快从磁盘上存取文件的速度。每次系统读取文件的一页并
将它放在页缓存中。图 3.6 显示页缓存包括 page_hash_table,它是一组指向 mem_map_t
的指针。
Linux 的每个文件由一 VFS inode 数据结构表示 (请参看文件系统章 ),并且每个
VFS inode 是唯一的并且描述一个且仅一个文件。页表中的索引包括了文件的 VFS号
及其在文件中的偏移量。
当从印像文件中读一页时,例如,按需装载一页回内存时,读操作将通过页缓存。
如果页在缓存中,一个指向它的 mem_map_t 指针将被返回给处理页错的代码。否则,
这页必须被从文件系统中读入内存。 Linux 需分配一页内存并从磁盘上读文件。
如果可能, Linux 将开始读文件的下一页。向前多读一页意味着如果进程是连续地
访问文件,那么下一页将等在内存中。
页缓存中的内容将随着文件的存取而越来越多。当他们不再被任何进程使用时,这
些页将被从缓存中移出。当 Linux 的空闲内存变得很少时,Linux 将减少页缓存的
大小。
3.8 页的交换和释放
当空内存变得很少时, Linux 内存管理系统必须释放一些页。这任务由内核交换程
序来完成( kswapd )。
内核交换程序是一种特殊的进程,是一个核线程。核线程是没有虚存的进程,他们
在物理地址空间以核模式运行。内核交换程序不仅把页交换到系统的交换文件中,
它的角色是保证系统有足够的内存而使内存管理系统可以高效工作。
内核交换程序被内核 init 进程在初始时启动,并等待内核交换定时器周期性地到
期时开始运行。
每次定时器到期,内核交换程序检查系统中的空页数是否变得太低。交换程序使用
两个变量,free_pages_high 和 free_pages_low 来决定是否它应该释放一些页。
只要系统的空页数大于 free_pages_high, 内核交换程序不做任何事情;它继续休
息直到定时器再次到期。在做这项检查时,交换程序计算了正在往交换文件中写的
页数。每次有一页等待写入交换文件时,计数器加1,当操作结束后,计数器减1。
free_pages_low 和free_pages_high 在系统开始时被设置,并且与系统内存的页数
有关。如果系统的空页数小于 free_pages_high 或甚至小于 free_pages_low , 核
交换驻留程序将尝试 3 种方法以减少系统使用的页数:
减少缓冲区和页缓存的大小
换出系统 V 的共享页
换出并释放一些页
如果系统的空页数小于 free_pages_low , 核交换程序在它下次运行以前,将尝试
释放 6 页,否则它将尝试释放 3 页。上面的方法将依次被使用直到有足够的页被
释放。核交换程序将记住上一次它是用什么方法释放内存的,下一次将首先使用这
个成功的方法。
在系统有足够的空页后,交换程序将休息直到它的定时器到期。如果上次空页数小
于free_pages_low, 它只休息一半时间。直到空页数多于 free_pages_low,核交
换程序才恢复休息的时间。
3.8.1 减少页缓存和缓冲区的大小
在页缓存和缓冲区中保存的页是被释放的最佳候选。页缓存保存着内存映像文件,
很可能包括了许多没用的页。同样,缓冲区中,它保存读写物理设备的数据, 也很
可能包含许多不需要的数据。当系统的内存页快用完时,从这些缓存丢弃页是相对
容易的 (不同于从内存交换页),因为它们不需要写物理设备。丢弃这些页除了使访
问设备和内存的速度减慢一些以外,没有其它的副作用。并且如果对各进程公平对
待的话,对各进程的影响是相同的。
每次内核交换程序尝试缩小这些缓存时,它先检查在 mem_map 中的页块,看是否有
页可以被从内存中释放。如果内核交换程序经常作交换操作,也就是系统空页数已
经非常少了,它会先检查大一些的块。页块会被轮流检查;每次减少缓存时检查一
组不同的页块。这被称作时钟算法,像钟的分针一样轮流检查 mem_map 中的页。
检查一页是看它是否在页缓存或缓冲区中。应该注意共享页在这时候不能被释放,
并且一页不能同时在两个缓存中。如果页不在任何一个缓存中,那么就检查 mem_map
中的下一页。
页被缓存在缓冲区中 ( 或页内的缓冲区被缓存 )是为更有效地分配和回收缓存。缩
小内存代码将尝试释放被检查页中的缓冲区。
如果所有的缓冲区都被释放了,那么对应它们的内存也就被释放了。如果被检查的
页在 Linux 页缓存中,它将被从页缓存中移出并释放。
当足够的页被释放后,内核交换程序将等到下一个周期再运行。因为释放的页都是
进程的虚存部份 ( 他们是被缓存的页 ), 所以没有页表记录需要更新。如果没有释
放足够的页,那么交换程序将试着释放一些共享页。
3.8.2 交换出系统 V 的 共享页
系统 V 共享内存提供了进程间的通信机制。进程间如何共享内存,请参看IPC章。
系统 V 的共享区域被描述成一个 shmid_ds 数据结构。这包含一根指向一组 vm_area_struct
数据结构的指针,每个 vm_area_struct 对应共享这区域的一个进程。vm_area_struct
数据结构描述了每个进程在各自虚存的哪里共享系统 V 的这个区域。每个 vm_area_struct
由 vm_next_shared 和vm_prev_shared 指针相连。每个 shmid_ds 数据结构还包括
一组页表记录,描述这些共享页是对应内存中的哪些页。
内核交换程序也使用时钟算法来换出系统 V 的共享页。每次它运行时,它记得上次
换出的是哪个共享页。它将其记录在两个索引中,第一个是 shmid_ds 数据结构的
索引, 第二个是系统的这段共享内存的页表记录的索引。这保证它公平地对待系统
V 的所有共享页。
由于共享页的物理页号在每一个共享进程中都有记录,内核交换程序必须修改这些
页表,显示页已不在内存中了,而被保存在交换文件中。对于每个换出的共享页,
内核交换程序是顺着 vm_area_struct 的指针找到这共享页在各个进程中的页表记
录。如果这共享的系统 V 的页对应的页表记录是有效的,交换程序将把它改成无效,
标为在交换文件中,再将对应这页的计数器减1。被换出的系统 V 的共享页仍包括
两个索引,第一个是 shmid_ds 数据结构的索引, 第二个是系统中共享这段内存的
进程的页表记录的索引。
如果各进程的页表修改过后,页的计数器变成0,那么这页就可以被写入交换文件了,
shmid_ds 中各页表记录的值将变为交换文件中的地址,在交换文件中的页的记录
包括其对应交换文件的索引和偏移量。当这页要被重新读回内存时,这些信息将被
使用。
3.8.3 换出及释放(进程的)页
交换程序检查系统中每一个进程,看它们是不是好的候选。好的候选是那些能被换
出的进程或那些能从内存中换出并释放若干页的进程。只有当这些页不能从其它地
方得到时,它们才会被写进交换文件。
许多映像的内容是可以从映像文件中读出的。例如, 一段映像的可执行指令决不会
被修改,所以不用被写进交换文件。这些页能被直接释放;当他们再被进程调用时,
他们将被从可执行映像中重新读入内存。
一旦确定了换出的进程,交换程序将检查它所有的页表记录,找出不是共享或被锁
的区域。
Linux 并不换出它所选择进程的所有可交换页;相反它仅移出其中的一小部份。
如果页在内存中被锁住了,它们就不能被换出或释放。
Linux 交换算法使用页的年龄 (aging)。每页有一个记数器 (保持在 mem_map_t 数
据结构中),告诉交换程序是否应将它移出。当它们闲置时,页会变老;当被访问时,
页变年轻。交换程序仅仅移出旧页。缺省状态下,当一页被分配时,起始年龄是3,
每次它被访问,它的年龄从 3 增加直到最大值 20。每次内核交换程序运行时,它
把所有页的年龄数减1。这些缺省操作都能被改变,它们被存储在 swap_control 数
据结构中。
如果页是旧的 ( 年龄 = 0 ),交换程序就进一步处理它(将它移出内存)。脏页也可
以被移出。Linux 用PTE中的特定位来标示 (见 3.2图)。然而, 并非所有的脏页必
须被写进交换文件。进程的每个虚存区域都可以有它们自己的交换操作 (由 vm_area_struct
中的 vm_ops 指出),这个特定的操作将被调用。否则,交换程序将分配一页交换文
件,并将那页写到磁盘上。
页对应的页表记录将被改为无效,但包含了它在交换文件中的信息,它将指出是哪
个交换文件,并且偏移量是多少。无论采取什么交换方法,原来的物理页将被放回
free_area。.乾净的 (not dirty) 页可以直接被释放并放回 free_area以备后用。
如果有足够的页被换出或释放, 交换程序就又开始休息。下一次它运行时,它将检
查系统中的下一个进程。这样,交换程序对每个进程都移出几页,直到系统内存恢
复正常,这比移出一整个进程来的公平。
3.9 交换缓存
当将页移入交换文件中时,并非所有情况,Linux 都需进行写操作。有时一页既在
交换文件中,又在内存中。这种情况是由于这页本来被移到了交换文件中,后又因
为被调用,重又被读入内存。只要在内存中的页没被修改过, 在交换文件中的拷贝
仍然是有效。
Linux 使用交换缓存来记录这些页。交换缓存是一张页表记录的表,每条记录对应
一页。每条页表记录描述被换出的页在哪个交换文件中及其在文件中的位置。如果
一交换缓存记录非零,表示在交换文件中的那页没被修改过,如果页被修改了(被写
),它的记录将被从交换缓存中移出。
当 Linux 需要移出一页内存到交换文件中时,它先查询交换缓存, 如果这页有一个
有效的记录,它就不需要把页写到交换文件中了。因为自从它上次被从交换文件中
读出后,在内存中没被修改过。
交换缓存中的记录描述已被移到交换文件中的页。它们被标为无效,但是告许Linux
页在哪个交换文件以及在交换文件的哪一页。
3.10 移入页
保存在交换文件中的脏页可能会被再次调用。例如,一个应用程序要将某些内容写到一
已移出的页中。当这页被换到交换文件中时,描述这页的页表记录已被标记为“无效”。
这样,存取不在内存中的虚地址将引起页错。页错是由处理器发信号给操作系统,告诉
操作系统它不能把某个虚地址翻译成物理地址,并告之引起页错的虚地址及原因 (不同的
处理器是用不同的格式传递这些信息的),同时,处理器把控制权交给操作系统。
操作系统用特定(与处理器有关)的代码来找到引起页错的虚地址对应的 vm_area_struct 数
据结构。在这个过程中,系统检索进程所有的 vm_area_struct。这段代码对时间的要求很
高,所以vm_area_struct 应被合理组织起来,以缩短所需的时间。
系统执行了以上操作,证实了引起页错的虚地址是有效的后,处理页错的其它代码是与
处理器无关的。
下一步,(系统)处理代码寻找虚页对应的页表记录。如果它发清b页表记录指示这页在交
换文件中, Linux 就把这页读回内存。页表记录的格式因处理器的不同而各不相同,但“有
效位”都应该是无效,并都保存着有关这页在交换文件中的信息。Linux 需要利用这些信
息来把页重新载入内存。
此时,Linux 知道了引起页错的虚地址及其对应的页表记录,记录中保存着有关交换文件
的信息。而将页从交换文件中读回内存的函数通常由vm_area_struct 中的指针指向。这种
函数叫移入(swapin) 函数。如果能从vm_area_struct 中找到这函数,Linux 就会调用它。例
如,因为系统 V 的页的格式与一般的页不同,所以系统 V 中移出的页需要特殊处理,这
时就需要调用它们的移入函数。然而,某页可能没有对应的移入函数,在这种情况下,
Linux 将认为它是一普通的页,而不需要做任何特别处理。
系统将分配内存中的一空页并从交换文件中把这页读回来,交换文件中的地址信息是从
无效的页表记录中取回的。
如果引起页错的不是写操作,那么这页将被留在交换缓存中,它的页表记录不会被标为
“可写”。如果后来这页被写了,那么会产生另一个页错,这时,页被标成“dirty”,并
被从交换缓冲中删去。如果这页没被修改过,而它又需要被换出,Linux将不会再把这页
写到交换文件中,因为它已经在那儿了。
如果引起页错的是写操作,页将被从交换缓存中删除,它的页表记录将被标成“dirty”和“可写( writable)”。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:56
|
|
第 4 章 进程
本章讲述什么是进程, 以及 Linux 核心是如何创建, 管理和清除系统中的进程的.
在操作系统中, 进程是任务的执行者。 程序只是存贮在盘上的可执行映像里面的机器
指令和数据的集合, 因此是被动的实体。 进程可以被看作正在运行的计算机程序。
进程是一个动态实体, 随着处理器执行着机器指令而不断变化。 除了程序中的指令和
数据之外, 进程中还包括了程序计数器, CPU 的所有寄存器, 堆栈(包含着象过程参数,
返回地址,保存的变量等临时数据)。 当前正在执行的程序, 也就是进程, 含有微处理
器当前的所有活动。 Linux 是一个多重处理型的操作系统(multiprocessing, 或叫做多道)。
进程各司其职, 如果某个进程崩溃, 不会导致系统中别的进程崩溃。 每个进程在独立
的虚拟地址空间中运行, 除非通过核心提供的安全的机制之外, 不能和别的进程相互作
用。
进程在其生命周期内要使用许多系统资源, 它要用 CPU 运行指令, 用物理内存存贮
指令和数据; 它会打开并使用文件系统中的文件, 直接或间接使用物理设备。 Linux 必
须了解进程使用资源的情况以便合理地管理系统中的所有进程。 假如让某个进程独占大
部份系统物理内存或者 CPU, 对别的进程就不公平。
系统中最重要的资源是 CPU, 通常只有一个。 作为一个多重处理操作系统, Linux 的
目标是让系统中的每个 CPU 上面始终有一个进程在执行, 以充份利用 CPU。 如果进程
数多于 CPU 数(通常总是这样), 多余的进程必须等待有 CPU 空闲下来才能运行。
多重处理的想法很简单: 让进程一直执行直到它必须等待, 通常是等待使用一些系统
资源; 当它可以使用这个资源时, 可以再让它运行。 在一个单一处理的操作系统
(uniprocessing, 或叫做单道)中, 例如 DOS, CPU 在进然b等待资源的时候将无所事事,
白白浪费时间。 在一个多重处理操作系统中, 内存中同时存在许多进程。 每当一个进
程必须等待, 操作系统就把 CPU 分配给别的需要运行的进程。 系统中专门有一个调度器
(scheduler)负责选出下一个要运行的进程。 Linux 使用很多调度策略来保证调度的公平。
Linux 支持很多不同的可执行的文件格式, 比如 ELF , 还有 Java。 这些格式必须被透明
地管理。
4.1 Linux 进程
Linux 系统为了管理进程,用 task_struct 数据结构表示每个进程 (任务和进程 (task and
process) 在 Linux 中是可以互换使用的术语)。 任务向量(task vector)是一个指针数组, 里
面的指针指向系统中的每个 task_struct 数据结构。
这样就意味着系统中的最大进程数受到任务向量的大小的限制; 缺省它有 512 个入口。
当创建新进程时, 新的 task_struct 从系统存储器中被分配出来并被加入任务向量。 为了
便于查找, 一个 current 指针指向当前的进程。
除了普通进程, Linux 还支持实时进程。 所谓实时是指这些进程必须能够快速响应外
部的事件。 调度器会区别对待实时进程和普通进程。 尽管 task_struct 数据结构相当大,
而且很复杂, 但是其中能够划分出很多功能区域:
State (状态)
进程执行时会根据不同的情形改变状态。 Linux 进程有下列状态:
1。Running 运行态
进程或者正在运行(它是系统的当前进程), 或者是准备运行的(它正在等待被分到系
统的 CPU 之一) 。
2。Waiting 等待态
进程正在等一个事件或一个资源。 Linux 中的等待态有性质不同的两种类型:
interruptible (可中断的)和 uninterruptible (不可中断的)。 可中断的等待进程能被信号打断而
不可中断的等待进程直接等待某种硬件条件, 在任何情形下都不能被中断。
3。Stoped 停止态
进程被停止了, 通常是通过接受一个信号的方法。 被调试的进程能处于一个停止态。
4。Zombie 僵死态
某个已经终止的进程, 由于一些原因, 仍然在任务向量中占有一个 task_struct 数据
结构, 就处于僵死态。
Scheduling Information (调度信息)
调度程序需要这个信息以便相当决定系统中哪个进程最需要运行。
Identifiers (标识符)
每个进程有一个进程标识符。 进程标识符不是任务向量的一个索引, 它就是一个数字
而已。 每个进程也有用户和组标识符, 它们是用来控制这个进程对系统中的文件和设备
的访问的。
Inter-Process Communication (IPC, 进程间通讯)
Linux 支持 Unix 中经典的 IPC 机制, 如: signal (信号), pipe (管道) 和 semaphore (信
号灯),并且支持System V (Unix的一种较流行的标准版本)中的 share memory (共享存储器
), semaphore (信号灯) 和 message queue (消息队列)。 Linux 所支持的 IPC 机制在第 IPC
章中有详细讲述。
Links (连接)
Linux 系统没有进程与别的进程完全无关。 除了初始化进程(init process)之外, 每个进
程都有一个父进程(parent process)。 新进程不是被凭空创造出来的, 它们是从已有的进
程拷贝得来, 或者是克隆得来的。 代表进程的每个 task_struct 中都有指针指向它的父进
程, 兄弟进程(同一个父进程产生的进程之间是兄弟关系), 以及自己的子进程。 你能使
用 pstree 命令看到正在运行的进程的家庭关系, 下面是某次运行 pstree 命令得到的结
果:
init(1)-+-crond(98)
|-emacs(387)
|-gpm(146)
|-inetd(110)
|-kerneld(18)
|-kflushd(2)
|-klogd(87)
|-kswapd(3)
|-login(160)---bash(192)---emacs(225)
|-lpd(121)
|-mingetty(161)
|-mingetty(162)
|-mingetty(163)
|-mingetty(164)
|-login(403)---bash(404)---pstree(594)
|-sendmail(134)
|-syslogd(78)
`-update(166)
另外, 系统中有一个以初始化进程的 task_struct 数据结构为根的双向链表, 把所有进
程都链接在里面。 有了这样的表, Linux 核心就可以方便地查看系统中的每个进程。 这
是为了支持 ps 和 kill 这样的命令(分别是列出系统中的进程的命令和向进程发送信号的命
令(通常用于终止进程)).
Times and Timers (时钟和定时器)
在进程的生命周期内, 核心记录进程的创建时间并随时记录进程消耗的 CPU 时间。
每过一次时钟滴答(tick)的时间, 核心就更新当前进程在系统态和用户态所花的 CPU 时间
(以 jiffy 为单位)。 Linux 也支持进程特定的间隔定时器, 进程可以使用系统调用设置定时
器, 当定时器所设置的时间间隔已到, 核心就会给进程发送一个信号。 这些定时器可以
是一次性的或周期性地触发。
File system (文件系统)
进程可以打开和关闭文件。 进程的 task_struct 中包含了指向打开的文件的描述符
(descriptor)的指针, 还有两个指向 VFS i节点(inode)的指针。 VFS i节点能够唯一描述文件
系统中的一个文件或目录, 它也是文件系统所提供的统一的访问文件的接口。 关于
Linux 系统中怎样支持文件系统, 请参看第章文件系统。 第一个指针指向进程的根目录
(进程的可执行映像文件所在的目录), 第二个指向进程的当前目录或者叫 pwd 目录(得名
于 Unix 中的 pwd 命令, 是 print working directory 之意。)。 VFS i节点中有一个域用来记
录有多少个进程指向它们。 现在你明白为什么当一个进程的 pwd 目录是你想删除的目录
或者是这个目录的一个子目录的时候, 你就不能删除它的原因了吧?
Virtual memory (虚存)
大多数进程有一些虚存(核心线程和精灵(daemon)除外), Linux 核心必须追踪虚存到系
统物理内存上的映射关系。
Processor Specific Context (处理器特定的上下文)
进程可以被看作是系统的当前的各种状态的集合。 进程运行时要使用处理器的寄存器,
堆栈等等。 这就是所谓的进程上下文。 当进程被挂起时(暂时不再运行), 这个进程的
CPU 特定的上下文必须被保存到这个进程的 task_struct 中。 当进程被调度器重新启动时,
它就从这里恢复它的上下文。
4.2 Identifiers 标识符
Linux 象所有的 Unix 一样, 使用用户(user)和组(group)标识符在来检查进程对系统中文
件或者映像的访问权限。 Linux 系统中的文件都有所有权和许可权, 这些许可权描述了系
统中的用户对那个文件有什么访问权限。 基本的许可权有读(read), 写(write)和执行
(execute), 它们被分派到3类用户: 文件的主人(owner), 属于某个特定组的所有进程,
还有系统中的所有进程。 每一类用户可以有不同的许可权, 例如: 一个文件可以允许它
的主人读写, 允许文件所在的组读并且不允许系统中的其它进程访问。
Linux 系统中, 使用组就能够把文件的权限分配到一组用户而不是简单地到一个用户或
到所有的进程。 例如, 你可以为一个软件项目中的所有用户创建一个组, 并且只允许这
个组中的用户能够读写该项目的源程序。 进程能属于若干组(缺省最多能够属于32个组)。
每个进程的 task_struct 中有一个组向量(group_vector)来记录这些组。 只要进程所属的组
中有一个具有访问权限, 这个进程就有权访问那个文件。
每个进程的 task_struct 中有4对用户和组的标识符:
1。 uid, gid
进程所代表的用户(也就是启动这个进程的用户)的用户标识符和组标识符。
2。 effective uid and gid (有效的 uid 和 gid)
有一些程序在执行的时候会把 uid 和 gid 改变为它们的自己的特定的某个 uid 和 gid
(这些程序的可执行映像文件的 VFS i节点中有一个属性规定了这样的行为)。 这些程序被
称为 "setuid" 程序。 它是限制系统服务(service)的权限一个方法, 尤其在实现为别的用户
服务的网络精灵程序等类似的服务时很有用。 有效的 uid 和 gid 来自程序的映像文件本身,
和启动它的用户无关。 核心在检查权限的时候会使用有效的 uid 和 gid.
3。 file system uid and gid (文件系统 uid 和 gid)
这两个标识符通常与有效的 uid 和 gid 一样, 当检查文件系统存取权限时会用上。
这两个标识符是为了建立 NFS(Network File System, 网络文件系统)而使用的, 因为用户
模式的 NFS 服务器需要像一个特别的进答7b一样来访问文件。 在这种情况下, 只有文件
系统 uid 和 gid 被改变(有效的 uid 和 gid 不变)。 这样可以防止恶意的用户向 NFS 服务器
发送 kill 信号。 Kill 信号会被以一个特别的有效 uid 和 gid 发送到进程。
4。 saved uid and gid (节省的 uid 和 gid)
这是 POSIX 标准中要求的两个标识符。 当然b序通过系统调用来改变 uid 和 gid 的时
候必须要用它们来保存真实的 uid 和 gid 。
4.3 Scheduling 调度
进程执行时总是一会儿在用户态下, 一会儿在系统态下。 不同的硬件如何实现对这两
种模式的支持不一定相同, 但是都有一种安全机制保证从用户态进入系统态然后再回到
用户态。 用户态时进答7b的权限比较系统态要小。 每当进程进行系统调用的时候就会从
用户态切换到系统态, 然后继续运行。 进入系统态之后, 核心代码开始执行, 为这个
进程服务。 在 Linux 系统中, 进程不能从当前正在运行的进程那里强占执行的权利。 当
执行的进程需要等待某个系统事件的时候, 它就让出 CPU 。 例如, 进答7b可能等待从一
个文件中读出一个字符。 这个等待在系统调用内部, 处于系统态; 这时, 等待事件的
的进然b将被核心暂停, 其它更着急的进程会被选中来运行。
进程总是要经常做系统调用所以就经常会这样等待。 尽管如此, 如果进程愿意, 它还
是可以长时间地不做系统调用从而不合理地占用 CPU 的处理时间。 因此, Linux 系统要
使用抢先式的调度。 在这种情况下, 每个进程被允许运行一小段时间, 比如 200ms,
如果时间到了, 核心就会暂停当前的进程, (不管它是不是愿意), 选择别的进程来运行。
这一小段时间就是所谓的 time slice (时间片)。
负责在系统中所有可以运行的进程中选择最该运行的进程的核心部份是调度器。可以
运行的进程(runnable process)是指这个进程就在等待 CPU 来执行。
Linux 使用基于优先级的相当简单的调度算法在系统在当前的进程之间选择。 当选择了
新进程来运行时, 它保存当前的进程的状态, 特定的处理器寄存器以及其它的上下文,
到这个进程的 task_struct 数据结构中。
然后它恢复新进程的状态(这仍就是处理器相关的), 把系统的控制交给这个进程, 开始
运行它。 调度器为了能够公平地分配 CPU 时间, 它在每个进程的 task_struct 中保存了下
列信息:
policy (策略)
在这个进程上使用的调度策略。 Linux 进程有两种类型, 普通和实时。 实时进程比其
它所有的进程的优先级都要高。 如果有实时进程可以运行, 它将总是首先运行。 实时进
程有2种调度策略, Round Robin (轮转式)和 First in First out (先入先出式)。在轮转式调度
下, 每个 runnable 的实时进程轮流运行; 在先入先出式调度下, 每个 runnable 的进程依
次运行, 次序就是它们进入运行队列式的顺序, 而且不会变化。
priority (优先级)
进程的优先级。它也是这个进程被允许运行的时间的总量(以 jiffy 为单位)。 通过系统调
用和 nice 命令能够改变进程的优先级。
rt_priority (实时优先级)
实时进程的优先级高于其他类型的进程。 这个域允许调度器给每个实时进程以相对的
优先级。 实时的进程的优先级可以通过系统调用来改变。
counter (计数器)
这是该进程被允许运行的时间的总量(以 jiffy 为单位)。 进程第一次贻d始运行时, 这个
值就被设定为优先级的大小, 每次时钟中断一次, 这个数值就被减小。
核心内若干地方会运行调度器。 把当前的进程放入等待队列后会运行调度器; 在系统
调用结束, 即将返回到用户态的时候, 也可能会运行。 如果系统定时器把当前的进程的
counter 减小到了零, 它也需要运行。 调度器运行时, 需要做的事情是:
kernel work (核心工作)
调度器运行 bottom half handler (一种推迟处理任务的机制)并处理调度器的任务队列。
关于 bottom half handler 以及这些轻量的核心线程在 第11章 核心机制 中有详细讲解。
process current process (处理当前进程)
在选择其它进程运行之前,必须处理当前进程。 如果当前进程的调度策略是 Round
Robin (轮转式), 它就被放到运行队列的末尾,
如果任务是可以被中断的(INTERRUPTIBLE), 并且自从最后一次调度它之后, 它收到
了一个信号, 那么就设置它的状态为 RUNNING(运行)。
如果当前的进程执行超时了,那么它的状态变为 RUNING。
如果当前的进程就是 RUNNING,它将保持这个状态。
不处于 RUNNING 态并且也不是 INTERRUPTIBLE 的进程就被移出运行队列。 这意味着
当调度器要寻找最需要运行的进程时, 不再会考虑它们。
Process selection (进程选择)
调度器查找整个运行队列来选择最需要运行的进程。 如果有实时进程(调度策略是实时
的那些), 它们就会获得比普通进程更高的权重量。 正常的进程的权重是它的 counter (或
者优先级), 而实时进程是 counter 加 1000。 这说明如果系统中有处于 runnable 状态的实
时进程, 它们就会比普通的 runnable 的进程先执行。 当前的进程, 因为已经执行了一段
时间, 经过了若干时间片, 它的 counter 就被减去了一些, 所以如果有同样优先级的进
答7b的话, 它就要让位了。 这正是需要的。 如果若干进程有同样的优先级, 在队列前
面的被先选中。 当前进程会被放到队列的最后。 在有许多优先级相同的进程的平衡的系
统中, 它们会被轮流运行。 这就是称为 Round Robin 的调度方案。然而, 进程会等待资
源, 它们的运行顺序就会发生变化。
Swap processes (交换进程)
如果最需要运行的进程不是当前进程, 当前进程就必须被暂停, 新的进程将取而代之。
进程在运行时, 它在使用 CPU 的寄存器和系统的物理内存。 调用过程时, 它用寄存器
传递参数, 并且可能需要把返回地址放在堆栈中。 因此, 当调度器运行时它是在当前进
程的上下文(Context)中。 这时, CPU 处于特权态下, 也即核心态, 但是正在运行的仍
然是当前进程。 如果要暂停它, 就必须把它的上下文保存进它的 task_struct 数据结构中。
然后, 新进程的机器状态的必须被装载。 这是和具体的系统相关的操作, 各种 CPU 的
做法很不相同, 但是通常有一些硬件辅助来做这件事。
进程上下文的切换在调度器运行结束时进行。 所切换的上下文是与被调度进程有关的
硬件环境在此时的一个快照。
如果刚才的进程或新的当前进程使用虚存, 系统的页表的项目可以需要更新。 同样地,
这是和特定的机器体系结构相关的。 象 Alpha AXP 这样的处理器, 使用 Look-aside
Tables (转换对照表)或者 cached Page Table Entries (缓冲页表项), 必须刷新那些属于先
前进程的表项。
4.3.1 多处理机系统中的调度
多个 CPU 的系统在 Linux 世界中是相当稀罕的, 但是 Linux 系统中已经做了很多工作
使其成为一个 SMP(Symetric Multi Processing 对称多处理) 操作系统。 那就是说, 有能力
在系统的 CPU 之间平衡工作。 在调度器中做这种工作是最合适的。
多处理器系统中, 理想的情况是所有处理器均忙于运行进程。 每当一个 CPU 的当前
的进程用尽它的时间片或必须等一个系统资源, 就会单独运行调度程序。 关于一个
SMP 统要注意的第一事情是系统中不止存在一个空闲的进程。 在单处理器系统中空闲的
进程是在任务向量的第一任务, 在一个 SMP 系统中每个 CPU 都有一空闲的进程, 并且
你可能有不止一个空闲的 CPU 。另外每个 CPU 有一个当前进程, 因此 SMP 系统必须追
踪每个处理器上的当前进程和空闲进程。
SMP 系统中每个进程的 task_struct 包含它当前正运行在上面的处理器的数字
(processor)以及上次运行在上面的处理器的数字(last_processor)。 虽然进程可以每次在不
同的 CPU 上面运行, Linux 可以使用 processor_mask 来限制进程可以使用的 CPU 。 如
果 processor_mask 的第 N 位被设置, 这个进程就能在处理器 N 上运行。 当调度器选择
新进程, 它不会选择 processor_mask 中和当前的处理器对应的位被清除的进程。 调度器
会略微照顾上次在这个处理器上面运行的进程, 因为把进程在不同的处理器之间移动通
常会带来一定的性能损失。
4.4 Files (文件)
图 4.1 :进程的文件
图 4.1 表明系统中的每个进程有2个数据结构描述文件系统相关的信息。
第一, fs_struct, 包含指针指向进程的 VFS i节点 和它的 umask 。 umask 是创建新文件
时使用的缺省模式, 可以用系统调用改变。
第二, files_struct, 包含进程当前正在使用的所有文件的信息。 然b序从 standard input
(标准输入) 读并且写到 standard output (标准输出)。 任何错误消息应该输出到 standard
error (标准错误)。 这些可以是文件, 终端输入/输出或一台真实的设备, 但是程序都把它
们当作文件。 每个文件有它的自己的 descriptor (描述符), files_struct 中包含可以指向
256 个文件数据结构的指针, 每个可以描述进程打开的一个文件。f_mode 描述文件是以
什么模式被创建的:只读, 读写 或者 只写。 f_pos 记录下一个读或写操作的位置。
f_inode 指向描述该文件的 VFS i节点, 而 f_ops 是一个指向例程地址的向量的指针, 每
一个例程实现你希望在文件上做的一个操作, 例如, 一个写数据的例程。 这种对界面的
抽象非常有用, 允许 Linux 系统支持各种各样的文件类型。 我们以后就会看到, Linux 中
的 pipe (管道) 就是用这个机制实现的。
每打开一个文件, 在 files_struct 的一个空闲的文件指针被用来指向新文件结构。 Linux
进程启动的时候, 会有 3 个文件描述符已经打开, 它们是标准输入, 标准输出和标准错
误, 通常都是从父进程中继承来的。 所有的文件访问都要使用系统调用, 它们使用或者
返回 file descriptor (文件描述符)。 文件描述符是到进程的 fd 向量的索引, 所以标准输入,
标准输出和标准错误的文件描述符是 0 ,1 和 2 。 文件的每次访问都要使用文件数据结
构的文件操作例程和 VFS i节点。
4.5 虚存
进程的虚存包含从许多来源来的可执行的代码和数据。
首先, 程序映像被装载。 例如 象 ls 一样的命令。 这个命令, 象所有的可执行的映像一
样, 都由可执行代码和数据组成。 映像文件包含装载可执行的代码以及有关的程序数据
到进程的虚存所需的全部信息。
第二, 进程运行时能分配(虚拟)存储器, 比如说保留它正在读的文件的内容。 这最新
分配的, 虚拟的存储器要被连接进进程的已有的虚存才能使用。
第三, Linux 进程通常使用的公用代码库, 例如处理文件的例程。 每个进程有库的自己
的拷贝, 这很不明智。 Linux 使用能同时被若干运行的进答7b使用的共享库。 共享库的
代码和数据必须被连接到共享这个库的多个进程的虚拟地址空间。
在任何给定的时间段内, 进程不会使用在它的虚存中包含的所有代码和全部数据。 它
可以包含仅仅在某些状况下被使用的代码, 例如在初始化期间或一个特别的事件发生时。
它可能仅仅使用了从共享库连接的一些例答7b。 装载这些无用的东西进物理存储器, 实
在是一种浪费。 考虑到系统中同时存在多个进程, 这将使系统很低效地运行。 为此,
Linux 使用 demand paging (请求换页) 技术, 仅仅当进程试图访问某页时, 才把它装入物
理内存。 因此, Linux 核心只要改变进程的页表, 把虚拟的空间标明为存在但是不在内
存中就行了, 而不需要直接装载代码和数据进物理存储器。 当进程尝试访问这里的代码
或数据时, 系统硬件将产生 page fault (页错) 并且把控制传递给 Linux 核心来处理。 因此,
Linux 核心需要知道进程的虚拟地址空间的各个区域是从何处来的以及如何把它装入内存,
这样才能处理 page fault。
图 4.2 :进程的虚存
Linux 核心需要管理虚存的所有这些区域。 进程的虚存的内容在 mm_struct 数据结构中
描述, 进程的 task_struct 有指针指向这个结构。 进程的 mm_struct 数据结构也包含已装
载的可执行的映像的信息, 还有到进程的页表的指针。 进程的页表包含一些指针, 指到
vm_area_struct 数据结构的一个表, 每个表示进程虚存的一个区域。
这张链接的表是按虚存地址升序链接的, 图 4.2 显示了一个简单进程的虚存的布局以
及管理它的核心数据结构。 因为虚存中的那些区域从若干来源, Linux 让 vm_area_struct
指向一套处理虚存的抽象接口的例程(经由vm_ops)。 这样不管管理那存储器的内在的服
务怎么不同, 进程的所有虚存都能用一致的方法处理。 例如有一个例程在进程试图存取
存储器并且它不存在时, 将被调用, 这就是用来处理 page fault 的。
进程的 vm_area_struct 数据结构的会被 Linux 核心很频繁地调用。 这就使得寻找到
vm_area_struct 结构的时间对系统性能影响很大。为了加快存取, Linux 另外把
vm_area_struct 数据结构排列成一个 AVL (Adelson-Velskii 和 Landis)树 (也称平衡树)。 这
棵树上, 每个 vm_area_struct (或节点) 有一左一右两个指针指到它的邻近的
vm_area_struct 结构。 左指针指向的节点虚拟地址小于右指针指向的节点。寻找正确的节
点时, Linux 从树根开始, 根据每个节点的左右指针指向的地址的大小关系决定向何处去
找, 直到找到为止。 当然, 没有免费的午餐, 把一个新的
vm_area_struct 插入到这棵树要花一些额外的处理时间。
当进程分配虚存时, Linux 实际上不为进程保留物理存储器。 相反, 它创建新的
vm_area_struct 数据结构描述虚存, 再连接进进程的虚存的表。 当进程试图在那个新虚
存区域以内写时,系统将发生 page fault (页错)。 处理器将试图进行虚拟地冶d译码, 但
是因为这块存储器的没有页表入口, 它将失败并引发 page fault 异常, 让 Linux 核心来处
理。 Linux 检查引用的虚拟的地址是否在当前的进程的虚拟的地址空间。 如果是, Linux
创造适当的 PTEs 并且为这个进程的分配物理存储器的一页。 代码或数据可能需要从文
件系统或从交换磁盘拷贝到那个物理页。 进程然后在引起了 page fault 的指令处被重启并
且, 这次因为存储器物理上存在, 它可以继续运行。
4.6 创建进程
当系统启动时, 它在核心态运行并且有,从某种意义上说, 仅仅一个进程, initial
process (初始进程)。 象所有的进程一样, 初始进程的机器状态由堆栈, 寄存器等等表示。
当系统的另外的进程被创建并运行时, 这些将在初始进程的 task_struct 数据结构被保存。
系统初始化结束时, 初始进程启动一个核心线程(叫 init) 然后进入一个无事可做的空闲循
环。 当没有别的事情做时, 调度器将运行这个空闲进程。 空闲进程的 task_struct 是唯一
一个不被动态地被分配的, 当构造核心的时候, 它就静态地在核心里面定义并且被叫做
init_task, 相当含糊。
Init 核心线程或进程的进程标识符为1, 是系统的第一个真正的进程。 它做一些系统初
始化设置工作(例如打开系统控制台, 安装根文件系统)然后运行系统初始化程序。 这个
程序是 /etc/init, /bin/init 或者 /sbin/init, 与你的系统有关。 init 程序使用 /etc/inittab 作为脚本
文件来创建系统中的新进程。 这些新进□
'7b可能还要再创建新进程。 例如, 当用户试图登录时, getty 进程可能会创建 login 进程。
所有这些进程都是 init 核心线程的后代。
新进程通过克隆旧进程,或克隆当前进程来创建。 一个新任务通过系统调用(fork 或
clone)来创建。 克隆在核心态由核心来完成。 在系统调用结束时如果调度器选择了新进
程, 新进程就可以运行了。 新的 task_struct 数据结构在系统物理内存中分配, 而且有一
页或多页物理内存页被用来作为克隆进程的堆栈(用户堆栈和核心堆栈)。 新的进程标识符
被创建, 它在系统内唯一。 但是有理由让克隆出来的进程记住它的父进程。 新的
task_struct 被加入 task vector (任务向量), 老进程的 task_struct 的内容被复制到克隆的进
程的 task_struct.
当克隆进程时, Linux 允许两个进程共享资源而不是各自复制一份。 这包括进程的文
件, 信号处理程序, 以及虚存。 当资源被共享时, 各自的计数域将被增加, 这样当两
个进程全部释放资源的时候 Linux 才会回收它。
克隆进程的虚存比较困难。 新的 vm_area_struct 数据结构集合要被创建, 还有它们所
拥有的 mm_struct 数据结构, 以及被克隆的进程的页表。 这时还没有进程的虚存的内容
被复制。 这可能是个很困难的工作因为有的虚存在物理内存, 有的在可执行映像里, 有
的在交换文件里。 为此, Linux Linux 使用称为 "copy on write" (写时复制) 的技术, 具体
做法是当其中一个进程试图写共享虚存时才进行复制。 实现的方法是把可写的内存区域
在页表中标为 "read only" (只读), 在 vm_area_struct 数据结构中标为 "copy on write "。 当
某个进程试图写时, 就会发生 page fault, 此时 Linux 就进行内存的复制, 并修改页表和
虚存的数据结构。
4.7 时间和定时器
在进程的生命周期内, 核心记录进□
'7b的创建时间并随时记录进程消耗的 CPU 时间。 每过一次时钟滴答(tick)的时间, 核心
就更新当前进程在系统态和用户态所花的 CPU 时间(以 jiffy 为单位)。 除了这些用于记账
的定时器之外, Linux 也支持进然b特定的间隔定时器, 当定时器所设置的时间间隔一到,
核心就会给进程发送信号。 有3种间隔定时器:
Real (实时)
定时器实时地走动。 当定时器到时, 进程会收到一个 SIGALRM 信号。
Virtual (虚拟)
当进程正在运行时定时器才走。 如果到时, 这个定时器会发送一个 SIGVTALRM 信号
给进然b。
Profile (活动总计)
当进程正在运行时或者当系统代表进程在执行时, 这个定时器就走动。 它会发送
SIGPROF 信号。
Linux 系统把间隔定时器的信息存放在进程的 task_struct 数据结构中。通过系统调用能
够添加定时器, 启动, 停止以及读取定时器的当前的时间。
每当系统的时钟的一次滴答到来, 当前进程的所有间隔定时器的计数值就被减少, 如
果时间间隔已到, 就会发送相应的信号给进程。
实时间隔定时器有点特别。 Linux 在核心中使用了定时器机制来处理它。 每个进程有
自己的 timer_list 数据结构, 当实时间隔定时器运行时, 系统的 timer list (定时器列表)中把
它排入了队列。 当定时器的时间间隔一到, 负责处理定时器事件的 bottom half handler 会
把它从队列中删除, 然后调用调用间隔定时器的处理器(并不是 CPU, 而是一段代码)。 这
个处理器这就产生了 SIGALRM 信号并且重启间隔定时器, 又把它加入系统定时器队列。
请参看 第11章 核心机制 中的具体讲解。
4.8 Executing programs 执行程序
象 Unix 系统一样, Linux 系统中的程序和命令通常是由一个命令解释器来执行的。 一
个命令解释器是一个用户进程, 一般被称为 shell , 因为它就象是系统的外壳, 被用户
直接感受到。
Linux 系统中有许多命令解释器, 最流行的一些是 sh, bash 和 tcsh 。 除了一些内部命
令之外, 例如 cd 和 pwd , 一个命令就是一个可执行的二进制的文件。 对每个输入的命
令, 命令解释器在进程的搜索路径中指定的目录中查找能够匹配的可执行的映像文件。
搜索路径由 PATH 环境变量定义。 如果找到了匹配的文件, 它就被装载执行。
命令解释器使用上面说的 fork 机制克隆自己。 新的子进程用所找到的可执行的二进制
映像文件的内容替换自己原先的内容, 也就是命令解释器自身。 通常命令解释器等待命
令完成, 也就是等待子进程退出。 你能让命令处理器不要等待, 只要把子进程放到后台
运行就可以做到。 使用 control-Z 组合键, 它会导致一个 SIGSTOP 信号被送给子进程,
让它暂停。 然后你可以用 shell 命令 bg 把它放到后台。 命令解释器向它发送一个
SIGCONT 信号让它恢复运行, 它将一直在哪儿, 直到运行结束或者它需要做终端输入或
输出。
一个可执行的文件能有许多格式或甚至是一个脚本文件。 脚本文件必须被识别出来并
且用适当的解释器来处理。 例如 /bin/sh 解释 shell 脚本。 可执行的目标文件中包含可执
行的代码和数据, 以及足够的信息以便操作系统能够装载并运行。 Linux 系统中使用的最
多的目标文件格式是 ELF (参见下面的小节)。 但是理论上, Linux 灵活到几乎能处理任何
格式的目标文件。
图 4.3 :注册的二进制格式
就象文件系统, 格式由 Linux 支持了的二进制代码是在核被造了进核的任何一个造时
间或可得到作为模块被装载。核坚持支持二进制的
格式的一张表 ( 参见图 4.3 ) 并且当被尝试执行一个文件时,每二进制的格式接着被试用
直到一个人工作。
通常被支持了的 Linux 二进制代码格式是 a.out 和ELF 。可执行的文件不必须完全被读进
存储器, 作为装载的需求被知道的技术被使用。
当可执行的图象的每部份被进程使用,它被使存储器。图象的闲置的部份可以从存储器
被丢弃。
4.8.1 ELF
ELF (Executable and Linkable Format) 目标文件格式, 由 Unix 系统实验室所设计,是
Linux 系统中最常用的格式。 虽然同其它的目标文件格式, 例如 ECOFF 和 a.out, 比较,
ELF 在性能上略有损失, 但 ELF 更灵活。 ELF 可执行文件中包含可执行的代码(有时称
为正文(text)), 还有数据。 除此之外, 还有表说明程序应该怎样被放进进程的虚存。 静
态连接的映像可以用连接器(ld)构造, 或用连接编辑器, 结果成为一个包含运行时所需的
全部代码和数据的单个的映像。 映像中还说明了映像在内存中的布局, 以及第一条指令
在映像中的地址。
图 4.4 :ELF 可执行文件文件格式
图 4.4 显示了一个静态连接的 ELF 可执行的映像的内部布局。
这是一个简单的 C 程序, 打印 "Hello, world!" 然后结束。 文件头说明它是一个 ELF 映
像, 在文件头起始的 52 个字节是 2 个物理的头。 第一个物理头中指示在映像中的可执
行的代码。 代码起始于虚地址 0x8048000 , 有 65532 个字节。 因为它是为 printf 包含图
书馆代码的所有的一幅静态地被连接了的图象,这是 () 输出"你好世界"的呼叫。 映像的
入口点, 也就是程序的第一条指令, 不在映像的开始, 而是在虚地址 0x8048090 (
e_entry ) 处。 代码紧跟在第二物理的头之后。 这个物理头说明程序的数据, 要在装在虚
地址 0x8059BB8 处。 数据是可读可写的。 你会注意到在文件中的数据块的大小是 2200
个字节( p_filesz ), 而在内存中所占的大小是 4248 个字节。 这是因为第一个 2200 个字节
包含预初始化的数据而随后的 2048 个字节包含将由执行的代码来初始化的数据。
当 Linux 装载 ELF 可执行文件映像到进程的虚拟地址空间时, 它实际上没有真的装载
映像。
它设置虚存数据结构, 进程的 vm_area_struct 树和它的页表。 当程序执行时, 页差错
(page fault)将导致程序的代码和数据被装进物理内存。 程序中没用到的部份的部份决不会
被装载进存储器。 当 ELF 二进制格式装载器检验认为这个映像确实是一个 ELF 可执行映
像后, 它就从进程的虚存中刷新当前的可执行映像。 因为这个进程是一个克隆的映像(所
有的进程都这样) 这旧映像就是父进程正在执行的程序。 刷新导致旧的虚存数据结构被废
弃, 进程的页表被重新设置。 它也清除所有的信号 handler, 关闭已经打开的文件。 刷
新过后, 进程就可以用新的可执行映像了。 不管可执行的映像是什么格式的, 进程的
mm_struct 中需要设置同样的信息。 有指向映像的代码和数据的开始和结束的指针。 这
些值在读入 ELF 可执行映像的物理头时被得到, 它们所说明的程序段被映射到进程的虚
拟地址空间。 此时, vm_area_struct 数据结构被设置, 进程的页表也被修改。
mm_struct 数据结构中还包含指针指向传递给程序的参数以及进程的环境变量。
ELF 共享库
反之, 一个动态连接的映像, 并没有包含运行所必需的全部代码和数据。 部份代码和
数据在共享库里, 当映像执行的时候会被连接进来。 这时, ELF 共享库的表也被连接进
了映像。 Linux 使用若干动态的连接器, ld.so.1 , libc.so.1 和 ld-linux.so.1 , 都存放在
/lib序目录下。库中包含公用的代码, 比如语言的子程序。 如果没有动态连接, 所有的程序需
要把库中的这些代码各自复制一份, 这样会需要多得多的磁盘空间和虚拟内存。 有了动
态连接, 每个被引用到的子程序都在 ELF 映像的表中保存了信息, 动态连接器根据这个
信息知道怎样找到库中的代码并把它连接到程序的内存空间。
4.8.2 脚本文件
脚本文件是需要一个解释器来运行的可执行文件。 有各式各样的解释器可以在Linux中
使用, 例如 wish, perl 和命令处理程序比如 tcsh 。 Linux 使用标准的 Unix 习惯, 就是在
脚本文件的第一行中包含解释器的名字。 因此, 一个典型的脚本文件将这样开头:
#! /usr/bin/wish
为了找到脚本指定的解释器, 脚本二进制代码装载器试图打开在脚本文件的第一行中
指名的可执行的文件。 如果能打开它, 就让这个文件, 也就是一个解释器, 来执行这
个脚本。 脚本文件的名字成为参数零(第一参数)并且所有其它的参数向后移动一个位置
(原来第一参数成为新的第二参数, 依此类推)。 装入解释器的方法和 Linux 中装入一个
可执行文件的方法是一样的。 Linux 试用每一种二进制格式直到某个格式能够成功为止。
这样, 从理论上, 你能够安排若干个解释器以及二进制格式, 使 Linux 的二进制格式处
理器变得非常灵活。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:57
|
|
第 5 章 进程间通信的机制
进程之间、进程与核心之间互相通信,以协调它们的活动。Linux支持一系列进程间通信
机制,信号和管道是其中的两种,此外还有SVR的进程间通信机制。
5.1 信号
信号是unix系统最早使用的进程间通信方法之一。它们用来对一个或多个进程发送异步事
件。信号可以由键盘中断产生,也可以由进程试图读取虚拟存储器中不存在的位置而引
发。另外,信号也可以用于外壳程序向它们的子进程发送作业控制命令。
有一组预先定义的信号,核心可以产生,具有相应优先权的进程也可以产生。使用kill -l
命令可以列出系统的信号集合。例如,Intel平台上列出:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGIOT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR
对于Alpha平台而言,数字又有所不同。进程可以选择忽略产生的大部份信号,除了两个
特殊的之外:使进程停止运行的SIGSTOP和使进程退出的SIGKILL。对此外的信号,进
程可以任意选择处理的方法。进程可以阻塞信号,或者由自己的代码处理信号,或者交由
核心来处理信号。
如果是核心处理信号,它将进行这个信号要求的缺省处理。例如,进程收到SIGFPE(浮
点溢出)信号时,缺省处理是core dump并退出。信号之间没有天然的相对优先关系。如
果两个信号同时为同一个进程产生,它们可以以任意顺序交给进程处理。进程也没有任何
方法区别自己收到的是一个还是四十二个SIGCONT信号。Linux用存储在进程的
task_struct中的信息实现信号。所支持的信号数受到字长的限制,32位处理器可有32个信
号,64位处理器就可有64位信号。当前未处理的信号保存在signal域中,blocked中为阻
塞信号的掩码。除了SIGSTOP和SIGKILL之外,一切信号都能够被阻塞。如果一个被阻
塞的信号产生,除非解除其阻塞,否则它不会被处理。Linux持有每一个进程如何处理每
一个可能的信号的信息(放在每个进程的task_struct指向的一组sigaction数据结构中)。
在sigaction之中,要么含有信号处理例程的地址,要么放置标志告诉系统:进程希望忽略
这个信号,或者进程希望核心处理这个信号。进程通过系统调用来修改信号处理方法,这
些系统调用把相应信号的sigaction或者blocked进行修改。
并不是任何一个进程都能够发信号给所有进程,只有核心和超级用户进程才有此权力。普
通进程只能发送信号给具有相同uid和gid的进程,或者同一个进程组中的进程。要产生
一个信号,只要把task_struct中signal域的相应比特设置一下。如果进程没有阻塞信号,
并且处于可中断的等待状态,那么它会被唤醒,转变为运行态,并确认它在运行队列中。
这样,调度程序在下一次调度时将把它作为运行的候选进程之一。如果缺省处理是需要
的,Linux能够优化信号的处理。例如,如果产生了信号SIGWINCH(X-Window改变焦
点),而缺省处理程序正在使用,那么什么也不会做。
信号并非在产生后立刻送给进程,而是要等到进程重新被运行。每当一个进程从系统调用
中退出,它的signal和blocked域都被检查,如果发现未被阻塞的信号,则将送给进程。
这看起来似乎不太可靠,但是事实上每个进程总是在不断进行系统调用,例如把字符写到
终端。如果愿意,进程可以选择等待信号,这是它处于可被信号的来临中断的挂起状态。
Linux的信号处理代码通过查看sigaction结构以便决定处理方法。
如果信号的处理被设置为缺省,那么核心将负责处理它。SIGSTOP信号的缺省处理停止当
前进程的运行并且使用调度程序选择下一个运行的进程。SIGFPE信号的缺省处理使进程
core dump并且让它退出。进程也可以选择指定自己的处理代码。该代码为一个每当信号
产生时可调用的例程,sigaction结构保存有这个例程的地址。核心必须调用进程的信号处
理例程,如何实现这一点与具体的处理器相关,但是无论如何CPU必须注意到当前进程
正处于核心态运行,并且即将返回用户态。通过对栈和寄存器的操作能解决这个问题。进
程的程序计数器被设置到信号处理例程的地址,调用参数被通过调用帧或是寄存器传递。
当进程得以继续时,看来似乎信号处理例程是被正常调用的。
Linux是POSIX兼容的,所以进程能够在信号处理例程调用时指定哪些信号被阻塞。这就
意味着在信号处理例程中改变blocked掩码。例程结束时,blocked掩码必须被恢复原有
值。所以Linux增加了一个清理进程,该进程负责把原始blocked掩码恢复到接收信号进
程的调用栈的顶端。某些情况下,几个信号处理例程需要被用堆栈方式调用,以便保证每
个例程退出时,立刻调用下一个例程,直至清理例程被调用。对此,Linux需要进行优
化。
5.2 管道
普通的Linux外壳都允许重定向。例如
$ ls | pr | lpr
把ls命令的输出文件名通过管道作为pr命令的标准输入,后者对之进行分页
($$paginate?$$),最后pr的标准输出又通过管道送入lpr的标准输入,lpr把结果打在
缺省打印机上。所以,管道就是连接一个进程的标准输出到另外一个进程的标准输入的单
向字节流。进程无法知道这个重定向,仍然正常工作。负责在进程之间建立临时管道的是
外壳。
在Linux中,管道是通过两个指向同一个虚拟文件系统i节点的file结构来实现的,i节点
本身则指向内存中的一个物理页。图5.1(略)显示,每个file数据结构含有指向不同的文
件操作例程向量的指针,一个用于写管道,另一个用于读管道。
这就隐藏了与读写普通文件的一般的系统调用之间的区别。当写进程在写管道时,字节被
拷到共享数据页上,而当读进程在当读管道时,字节被从共享数据页上拷出来。Linux 必
须对共享数据页的存取进行同步,它使用锁、等待队列和信号来保证读写进程之间的轮
流。
当写进程想写管道时,它使用标准写库函数。这些库函数都传递文件描述符,而文件描述
符是进程的file数据结构集合的索引,每一个代表一个打开的文件或者一个打开的管道。
Linux系统调用使用描述这个管道的file数据结构指向的例程。那个写例程使用表示管道的i
节点中保存的信息来管理写要求。
如果有足够的空间供所有的字节写入管道,只要管道没有为读进程锁住,Linux将会为写
进程锁住管道,并且把所有的待写字节从进程的地址空间拷到共享数据页中。如果管道为
读进程锁住,或者没有足够的数据空间,那么将使当前进程睡眠在管道i节点的等待队列,
调用调度程序运行另外一个进程。进程的状态是可中断的,所以它能收到信号,能在写数
据空间变得足够或是管道被解锁之后被写进程唤醒。写完数据之后,管道的i节点被解锁,
睡眠在i节点等待队列的读进程将被唤醒。
从管道读数据与写数据非常类似。
允许进程做非阻塞读(依赖于打开文件或管道的模式),在此情况下,如果没有数据可读
或者管道被锁住,将返回一个错误。这意味着进程可以继续运行。另一种方法是等在管道i
节点的等待队列里直到写进程完成工作(即阻塞读--
译者注)。当两个进程都完成了管道上的工作,管道i节点将被与共享数据页一起丢弃。
Linux也支持“有名”管道,也被称为FIFO,因为管道的工作方式是先入先出的。最早写入
这种管道的数据也最早被读出。与一般管道不同的是,FIFO不是临时对象,而是文件系统
中的实体,可以用mkfifo命令创建出来。只要进程有足够的存取权限,就能自由地使用FIF
O。打开FIFO的方式和打开管道的方式也稍有不同。一个管道(包括它的两个file数据结构
,它的虚拟文件系统i节点和共享数据页)是一次性产生的,而FIFO是已经存在的,由用
户负责它的打开和关闭。如果在写进程打开FIFO之前,读进程先打开了它,或者读进程去
读一个没有被写入数据的管道,Linux必须加以处理。除此之外,FIFO与管道完全相同,
因为它们采用的数据结构和操作是一致的。
5.3 套接字
注:在网络章完成后增加。
5.3.1 SVR的进程间通信机制
Linux支持三类SVR首创的进程间通信机制:消息队列、信号灯和共享内存。这些SVR进程
间通信机制都共用相同的认证方法。进程只能通过系统调用向核心传送一个唯一的引用标
识,才能存取这些资源。这些SVR进程间通信对象的存取通过存取权限来控制,与文件存
取的权限控制非常类似。由对象的创造者通过系统调用来设置对象的存取权限。在每一种
机制之中,对象的引用标识被用作资源表的索引。当然,索引本身并不简单,还需要一些
操作来产生。
所有表示SVR进程间通信对象的Linux数据结构都包含一个ipc_perm结构,该结构包含了所
有者和创造者进程的用户和组标识。对象的存取模式(所有者、组和其它)以及对象的ke
y(这句似乎少了些什么,但是原文如此--
译者注)。这个key只是用于定位对象的引用标识的一种方法。支持两类key:公开key和秘
密key。如果key是公开的,那么系统中的任何进程,只要有足够的存取权限,都能找到对
象的引用标识。SVR进程间通信对象绝对不能用key来引用,而只能用引用标识来引用。
5.3.2 消息队列
消息队列允许一个或者多个进程读/写消息。Linux维护一个消息队列表--
msgque向量,其中每一个元素指向一个msqid_ds数据结构,该数据结构将完整地描述消息
队列。当创建一个消息队列时,从系统内存中分配出一个新的msqid_ds数据结构,插入向
量之中。
每一个msqid_ds数据结构包含了一个ipc_perm数据结构和指向队列中消息的一批指针。
另外,Linux保存有队列修改时间,例如最后一次写队列的时间等等。msqid_ds也包含两
个等待队列,一个用于队列的写者进程,另一个用于队列的读者进程。
每当进程试图写消息到写队列中时,它的有效用户标识和组标识将与队列的ipc_perm数据
结构中的存取模式进行比较。如果进程能够写队列,那么消息将被从进程的地址空间中拷
到一个msg数据结构中,并且把该数据结构放到消息队列的尾部。根据应用进程之间的约
定,每个消息被用一个类型标记出来,这里的类型划分是与应用有关的。然而,由于
Linux限制了能向队列中写的消息的长度和数量,队列中剩余的空间可能不足容纳这次要
写的消息。这是,进程将被加入消息队列的写等待队列之中,调用调度程序来选择一个新
的进程运行。当有消息从队列中读出后,写等待队列中的进程将被唤醒。
读消息队列也类似。同样,需要检查进程对于写队列的存取权限。读进程可以选择读取队
列中的第一个消息,而不计其类型,或者选择只读特定类型的消息。如果没有消息满足读
进程的标准,那么它将被加入消息队列的读等待队列,然后运行调度程序。当一个新消息
写入队列时,进程将被唤醒,重新运行。
5.3.3 信号灯
最简单类型的信号灯是内存中一个可以被一个或者多个进程测试并设置的位置。就进程而
言,测试并设置操作是不可中断的,或者说是原子的。测试并设置操作的结果是信号灯当
前值加上了所设置的数值,这个数值可以随便是正的或负的。根据测试并设置操作的结
果,进程可能会被迫睡眠,直到另一个进程改变信号灯的值为止。信号灯可以用于实现关
键区--一次只能有一个进程进入运行的关键代码区域。
例如,假设你有很多进程在同时读写一个数据文件的记录,你想对文件的存取进行严格的
协调。你可以用一个初始值是1的信号灯,在文件操作代码的前后,放上两个信号灯操
作。第一个信号灯操作是测试并且减少信号灯的值,第二个信号灯操作是测试并且增加信
号灯的值。实际运行时,存取文件的第一个进程将试图减少信号灯的值,它当然会成功,
这时信号灯的值变成了0。于是进程能够继续下去,使用数据文件。这时,如果另外一个
进程也想使用文件,当它试图减少信号灯的值的时候,它会失败,返回结果-1。该进程将
会被挂起,直到第一个进程完成该数据文件的操作。第一个进程完成数据文件操作时,它
增加信号灯的值,使之回到1。这时等待进程可以被唤醒,它增加信号灯数值的尝试将会
成功。
每一个SVR信号灯对象描述一个信号灯序列,Linux使用semid_ds数据结构来代表之。系
统中所有的semid_ds数据结构都被semary向量中的一组指针所指引。每一个信号灯序列
中含有sem_nsems个信号灯,每一个信号灯用sem_base指向的一个sem 数据结构描述。
所有有权操作信号灯序列的进程可以通过系统调用来对它们进行操作。系统调用可以指定
很多操作,每个操作用三个输入来描述:信号灯索引,操作值和一组标志。信号灯索引是
信号灯序列中的索引,操作值是将被加到信号灯当前值上的数值。首先Linux测试是否所
有的操作都能成功。操作能够成功当且仅当操作值加到当前值上之后结果大于0,或者操
作值与当前值都是0。如果其中的任何信号灯操作失败,Linux将挂起进程,除非操作标
志要求系统调用是非阻塞的。如果需要挂起进程,Linux将保存信号灯操作的状态,并把
当前进程送入等待队列。实现的方法是创立并填写一个sem_queue数据结构,放在信号灯
对象的等待队列之中(使用sem_pending和sem_pending_last指针),并调用调度程序运
行另外一个进程。(这句话是译者根据自己理解翻译的,未必确切--译者注)
如果所有的信号灯操作都成功并且当前进程不需要被挂起,那么Linux继续下去,对信号
灯序列中适当的成员进行操作。现在Linux必须检查所有的等待、悬挂的进程能否进行它
们的操作了。它依次看每一个信号灯操作等待队列sem_pending,测试这些操作这一次是
否会成功。如果能成功,则从队列中删除sem_queue数据结构,进行信号灯操作,并唤醒
睡眠进程,使它在调度程序下一次运行时具备候选资格。Linux从头检查等待队列,直到
发现无法再进行任何信号灯操作,也不可能有更多进程被唤醒。
信号灯还有一个死锁的问题。当一个进程进入关键区,改变信号灯的数值之后,由于瘫痪
或者被杀而无法离开关键区,就会发生死锁。Linux防止死锁的方法是维护信号灯序列的
矫正表。这里的思想是用这些矫正值把信号灯恢复到操作之前的原有状态。矫正值放在
sem_undo数据结构里,同时在信号灯序列的semid_ds数据结构和进程的task_struct数据
结构之中排队。
每一个信号灯操作都要求有一个矫正值。Linux为每个进程对每个信号灯序列的操作最多
保存一个sem_undo数据结构。如果需要的进程没有此数据结构,则在需要时创建一个。
新的sem_undo数据结构同时排在进程的task_struct数据结构和信号灯序列的semid_ds数
据结构之中。当对信号灯序列进行操作时,操作值的相反数会被加在进程的sem_undo数
据结构的矫正值序列中相应于这个信号灯的那个。所以,如果操作值是2,矫正值加上的
就是-2。进程被删除时,Linux处理它们的sem_undo数据结构,对信号灯进行矫正。如果
删除一组信号灯,那么sem_undo数据结构仍然排在进程的task_struct之中,但是信号灯
序列的标识被置为无效。遇到这种情况,信号灯清除代码就只要丢弃sem_undo数据结构
即可。
5.3.4 共享内存
共享内存允许一个或者多个进程通过同时出现在它们的虚地址空间内的内存进行通信。虚
存的页面由各个进程的页表的入口指引,并不需要共享内存在每一个进程的虚拟内存的地
址都相同。与所有的SVR进程间通信对象一样,共享内存的存取由key和存取权限检查
来控制。一旦内存被共享,无法对进程如何使用它进行检查。必须依赖其它机制,如信号
灯,来对内存的存取进行同步。
每一个新创建的共享内存区域由一个shmid_ds数据结构代表。这些数据接被保存在
shm_segs向量之中。shmid_ds数据结构描述了共享内存区域的大小,使用共享内存区域的
进程的数量,和关于共享内存如何映射到进程的地址空间的信息。正是共享内存的创建者
控制着其存取权限和其key是否公开。如果创建者具有足够的存取权限,它可以把共享内
存锁定在物理内存之中。
每一个希望共享内存的进程必须通过系统调用与虚拟内存相联,从而产生一个
vm_area_struct数据结构,为此进程描述该共享内存。进程可以选择把共享内存放在它的
虚拟地址空间的何处,也可以任由Linux选择一块足够大的空间。新的vm_area_struct结
构放在shmid_ds所指的vm_area_struct结构表之中。vm_next_shared和vm_prev_shared指
针把这些结构串联起来。虚拟内存在相联时并没有真正创建出来,而是在第一次有进程试
图存取时创建出来。
当第一次有进程存取共享虚拟内存的一个页面时,发生一个缺页错误。在Linux处理缺页
错误时,它会找到描述该虚拟内存的vm_area_struct数据结构,其中包含了指向处理该类
共享虚拟内存的例程的指针。共享内存缺页错误处理代码查看该shmid_ds列出的页面对应
的页表入口的表,确定是否有此页存在。如果不存在,就分配一个物理页面,在页表中为
它创建一个入口。该入口不仅放入当前进程的页表之中,也放入该shmid_ds之中。这意味
着当下一个试图存取该内存的进程得到一个缺页错误时(这里似应说明为同一虚存页面--
译者注),共享内存错误处理代码将使用这个新创建的物理页面。所以,第一个存取共享
内存某页使得它被创建,此后其它进程的存取使它被加入相应进程的虚拟地址空间。
当进程不再想使用虚拟内存时,就与它断联。只要还有其它进程还在使用该内存,断联就
只会影响当前进程。它的vm_area_struct被从shmid_ds数据结构中删除、去配,并修改当
前进程的页表,使原来使用的共享内存区域无效。当最后一个共享该内存的进程与它断
联,当前在物理内存中的共享内存页面被释放,共享内存的shmid_ds数据结构也释放。
如果共享虚拟内存没有锁定在物理内存,就更复杂一些。这时,共享内存的页面在内存使
用频繁时可以被换出到系统的交换磁盘上。共享内存如何换入和换出虚拟内存,见“内存
管理”一章。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:57
|
|
第 6 章 PCI
Peripheral Component Interconnect(PCI),外设部件连接,是一个标准,描述的是如何将一个系统中
的外设以一种结构化的,可控制化的方式连接在一起。PCI标准刻划了系统外设部件连接的电气方案,
和在该标准下外设部件的行为归约。本章探讨Linux核心如何初始化系统的PCI总线和PCI设备。
图6.1 基于PCI总线的系统
图6.1是一个基于PCI总线系统的例子的逻辑框图。PCI总线和PCI桥负责将系统中的部件连接在一起。
CPU连在PCI总线0;在主PCI总线(PCI 0)上挂着视频设备。PCI-PCI桥,一个特殊的PCI设备将主PCI
总线于次PCI总线(PCI 1)相连。用PCI归约的术语来讲,PCI总线1被称作PCI-PCI桥的downstream,
PCI总线0叫做桥的upstream。在第二个PCI总线上,是系统的SCSI和Ethernet设备。这个PCI-PCI桥,
第二个PCI总线和其上的两块设备在物理上都可以在一个PCI卡上。系统中的PCI-ISA桥支持ISA设备。
上图所示ISA总线下挂着一个多功能I/O控制器,控制系统的键盘,鼠标器和软驱。
6.1 PCI地址空间
CPU与PCI设备需要存取在它们之间共享的内存。设备驱动程序使用这片内存来控制PCI设备并用来传
送信息。一般而言,这些共享内存中包含设备的控制和状态寄存器。这些寄存器用来控制设备和读
取设备的状态。例如,PCI SCSI设备驱动程序读取SCSI设备的状态寄存器以探测该设备是否已就绪
可以将一个数据块写入SCSI磁盘。又例如,设备驱动程序可以在控制寄存器中写入控制数据从而使
设备开始运转在设备电源被开启之后。
上述的共享内存可以是CPU的系统内存。但如果这样的话,每次PCI设备存取内存时,CPU将被阻塞,
等待PCI设备存取的结束。一般而言,在某个特定时刻,只能一个系统部件存取一个特定的内存。
所以,上述方法会使系统性能降低。另外,允许系统的外设不在一个良好的控制下存取主内存不
是一个好的方法。这将会是非常危险的事情。一个“淘气”的设备可以使得系统非常不稳定。
因此,外设一般拥有它们自己的内存空间。CPU可以存取这个空间。但是反之外设存取系统内存必须
在DMA(Direct Memory Access)的控制之下。ISA设备可以存取两种地址空间,ISA I/O(Input/Output
)和ISA memory。PCI可以有三种:PCI I/O,PCI memory和PCI Configuration空间。所有的这些地址
空间都可以被CPU所存取。其中设备驱动程序要使用PCI I/O和PCI memory空间。Linux核心中的PCI初
始化代码要用到PCI Configuration空间。
6.2 PCI 配置头(Configuration Header)
图6.2 PCI Configuration Header (配置头)
系统中的每个PCI设备,包括PCI-PCI桥,都有一个配置数据结构在PCI Configuration地址空间中。
这个PCI Configuraton Header(配置头)被系统用来定位和控制一个设备。至于这个数据结构具体
位于PCI Configuration空间的何处,依赖于设备位于PCI拓朴结构中的位置。例如,一个PCI视频
(Video)卡插在PCI 母板(motherboard)上的一个PCI槽中,它的数据配置头结构将会在一个地方;
如果被插在另一个地方,其数据配置头将会位于PCI Configuration空间中的另外一个地方。当然,
这没有关系。不论PCI设备和PCI桥在哪里,系统都将检测到,并使用它们的配置头中的状态和配置
寄存器来对它们进行配置。
一般来讲,每个PCI插槽的PCI配置头都位于在PCI Configuration空间的一个偏移量(Offset)处,
这个偏移量是与每个PCI插槽的位置顺序相关的。例如,PCI板上的第一个PCI插槽的PCI配置在偏
移量0;第二个PCI插槽的配置在偏移256处(所有的配置头的大小是256个字节)。系统中提供一
与硬件有关的机制,使得PCI配置代码可以试图检测在一个给定的PCI总线上所有可能的PCI配置
头,从而知道哪个PCI插槽上目前有设备,哪个插槽上暂无设备。这是通过读取配置头上的某个
域而完成的(一般是“Vendor Identification" 域)。如果一个插槽上为空,上述操作会返回一些
错误返回值,如0xFFFFFFFF。
图6.2所示的是一个完整的256字节的PCI配置头数据结构。它包含下列数据域:
Vendor Identification(厂商标识)
一个唯一的数字标识描述一个PCI设备的出处。Digital的PCI厂商标识是
0x1011; Intel的是0x8086。
Device Identification(设备标识)
一个唯一的数字标识用来描述一个设备。例如Digital的21141快速Ethernet
网卡有一个设备标识0x0009。
Status(状态)
这个域给出一个设备的状态。这个域的每个位的含义是由一个标准来定义的。
Command(命令)
系统通过在这个域中写入数据来控制设备。例如,让设备存取PCI I/O空间。
Class Code(设备类代码)
这个域用来标定一个设备的类型。每一种设备都对应一个标准的类。
如video(视频),SCSI等等。SCSI设备的类码是0x0100。
Base Address Registers(基地址寄存器)
这些寄存器用来决定和分配一个设备可用的存储空间类型
(如,PCI I/O和PCI memory)大小和位置。
Interrupt Pin(中断管脚)
在每个PCI卡上,有4个物理管脚可以传送中断信号到PCI总线上。其标准的
标号是A,B,C和D。这个”Interrupt Pin"域描述的是这个PCI设备正在用
那个物理中断管脚。通常来讲,对一个特定的设备,其使用的中断管脚是
被固定好的。也就是说,每次系统重新启动时,该设备使用同样的中断管
脚。这个信息使得中断处理子系统知道如何管理这个设备的中断。
(译者注:请参阅中断处理章节。注意PCI设备于ISA设备在中断方面的区别)
Interrupt Line(中断线)
设备的PCI配置头数据结构的"Interrupt Line"域用来在PCI初始化代码,
设备驱动程序和Linux中断处理子系统之间传递一个中断处理。这个域中的
值对于设备驱动程序而言是无意义的,但其可以使得中断例程正确地将一个
中断从一个PCI设备传递(route)到Linux中相应的设备驱动程序的中断处理
代码中。请参阅第七章关于Linux如何处理中断。
6.3 PCI I/O 和PCI Memory 地址
设备使用这两种地址空间来与其在Linux核心中运行的设备驱动程序进行通信。例如,
DECchip 21141 快速Ethernet设备映射其内部寄存器到PCI I/O地址空间中。从而其设备驱动程
序可以读和写这些寄存器来控制该设备。Video驱动器经常使用大量的PCImemory空间来存放视
频信息。
在PCI系统初始化完成,使用上述的”command“命令允许设备存取地址空间之前,系统不能存取
这块地址。值得注意的是只有PCI配置代码才读和写PCI Configuration地址。Linux设备驱动程
序只能读和写PCI I/O和PCI memory地址空间。
6.4 PCI-ISA桥
PCI-ISA桥通过将对PCI I/O和PCI memory地址空间的存取转换到对ISA I/O和ISA memory地址空间
的存取来支持对ISA设备的支持。许多系统中含有一些ISA总线槽和几个PCI总线槽。随着时间的推
移,这种为了向后兼容的机器配置将会消失。系统将会只支持PCI系统。在ISA地址空间中(ISA
I/O和ISA memory),系统中的ISA设备的寄存器地址被固定在某个地方(自从早期的Intel 8080 PC开
始)。例如,一个$5000的基于Alpha AXP的计算机的软盘控制器与早期的IBM PC的在ISA地址空间中
占据的是同一片地址。PCI归约(Specification)通过在PCI I/O 和PCI memory地址空间的低端为
ISA外设的使用保留一片区域并通过PCI-ISA桥来将对这片保留的PCI地址空间的存取映射到对系统中
ISA地址的存取上。(译者注:细心的读者不难发现,这种通过加”Layer",或“映射”的方法在计算
机软硬件系统中几乎无处不见。系统就是这样一层一层的抽象出更高的概念提供给更高的层使用,
直到用户层,从而使得一切细节的复杂性变的越来越透明。)
6.5 PCI-PCI桥
PCI-PCI桥是一种特殊的PCI设备。它将系统中的PCI总线粘合作一起。简单的系统只有通常一个PCI总
线。一个PCI总线上可支持的PCI设备的数目是有限的。使用PCI-PCI桥可以解决上述问题,允许将更
多的PCI总线加入系统从而支持更多的PCI设备。这对于高性能的服务器来说是非常重要的。Linux全
面地支持PCI-PCI桥机制。
6.5.1 PCI-PCI桥:PCI I/O 和PCI memory 窗口
PCI-PCI桥只负责传递一部份对PCI I/O和PCI memory读和写的请求到downstream(请参见第一节)。例
如,图6.1中,仅当读和写请求中的PCI I/O或PCI memory地址属于PCI-PCI桥SCSI或Ethernet设备时
PCI-PCI桥才将这些总线上的请求从PCI总线0传递到PCI总线1。其他的将被忽略。这种过滤机制可以
避免地址在系统中没必要的繁衍。为了做到这点,每个PCI-PCI桥必须正确地被设置好它所负责的PCI
I/O和PCI memory地址的起始和大小。当一个读或写请求落在其负责的范围之内,这个请求将被映射到
次一级的PCI总线上。系统中的PCI-PCI桥一旦设置完毕,如果Linux中的设备驱动程序存取的PCI I/O
和PCI memory地址只在这些窗口之内,这些PCI-PCI桥是不可见的(译者注:窗口在这里的含义是指每
个PCI-PCI桥都仅负责一定范围的空间映射;上述原文是:"Once the PCI-PCIBridges in a system
have been configured then so long as the Linux device drivers only access PCI I/O and PCI
Memeory space via these windows, the PCI-PCI Bridges are invisible.")。这是个很重要的特性
使得Linux PCI设备驱动程序开发者的工作容易些。然而,这也使得Linux配置PCI-PCI桥变的有点迷惑。
我们将在下面的章节中看到这一点。
6.5.2 PCI-PCI桥:PCI配置周期(Configuration Cycles)和PCI总线计数方法(Bus Numbering)
图6.3 PCI配置周期类型0
图6.4 PCI配置周期类型1
既然CPU的PCI初始化代码可以存取那些不在主PCI总线上的设备,那么必须存在一个机制使得这些桥能
够判断是否将一个PCI配置周期从它们的(译者住:它们指的是PCI-PCI桥设备)主接口传递它们的次接口。
所谓一个“周期”指的是一个出现在PCI总线上的一个地址。PCI归约(Specification)定义了两种PCI配
置寻址格式;类型0和类型1。图6.3和图6.4所示分别是这两种寻址类型。PCI配置周期类型0中不含有总
线号码,被所有的设备当作针对当前这个PCI总线上的PCI配置周期。类型0地址中的31-11位被用来作为
设备选择域。一种设计方案是每一位对应一个设备。在这种情况下,位11表示插槽0上的PCI设备。位12
表示插槽2上的PCI设备并以此类推。另一种方法是直接地将设备的插槽号码写入位31-11中。具体采用
哪种机制依赖于系统的PCI存储控制器(memory controller)。
类型1的PCI配置周期地址中含有一个PCI总线号码。当这种类型的配置周期(或命令)出现在一个PCI总线
上时,除了PCI-PCI桥之外,所有其他的PCI设备会将其忽略。所有”看见“配置命令类型1的PCI-PCI桥
可能选择将类型1的配置周期传递到其downstream PCI总线上或忽略之。选择的决定依赖于PCI-PCI桥的
配置。没一个PCI-PCI桥有一个主PCI总线接口号和一个第二PCI总线接口号。主总线接口是那个离CPU更
近的;第二总线接口是那个离CPU较远的。每个PCI-PCI桥还拥有一个次级总线号码。这个数目代表着在
这个PCI-PCI桥第二总线接口之下的所有PCI总线中的最大数。换种方式讲,这个次级总线数目是这个
PCI-PCI桥的PCI总线downstream的最大数目。当一个PCI-PCI桥看见一个类型1 PCI配置周期时,桥的行
为如下:
* 忽略这个命令,如果指定的总线号码不在第二和次级总线号码之间。(包含边界值)
* 将其转换成类型0配置命令,如果指定的总线号码是桥的第二级总线接口。
* 将其(原封不动地)传递到第二级总线接口,如果指定的总线号码大于该桥的第二总线号
但小于或等于次级总线号。
因此,如果我们想访问在图6.9中总线3上的设备1,我们必须从CPU产生一个类型1的PCI配置命令。桥1
将原封不动地将此命令传递到总线1;桥2将忽略此命令,但桥3将接受这个命令并将其转换成一个类型0
的配置命令,然后发送到设备1所在的PCI总线3上。
关于在PCI配置期间如何分配总线号码依赖于具体的操作系统。然而不论什么样的分配方案,对系统中所
有的PCI-PCI桥来说,必须满足下列要求:
”在PCI-PCI桥后面的所有PCI总线的编号必须在该桥的第二总线接口号码和次级总线号码之间“
如果违反这个规则,PCI-PCI桥将不会正确地传递和翻译类型1的PCI配置命令。系统将不能正确地发现和
初始化系统中的PCI设备。为了完成这个赋值方案,Linux依照一个特殊的顺序来配置这些PCI设备。
从图6.6开始我们描述了Linux PCI桥和总线的号码赋值方案。并举出了一个例子。
6.6 Linux PCI初始化
Linux PCI 初始化代码逻辑上分为三个部份:
PCI设备驱动程序
这个伪设备驱动程序从总线0开始查询PCI系统并且定位系统中所有的PCI设备和桥。它建立一个
可以用来描述这个PCI系统拓朴层次的数据结构链表。并且对所有的发现的桥编码。
PCI BIOS
这个软件层提供在bib-pci-bios归约中描述的服务。虽然Alpha AXP不提供BIOS服务,在其Linux
版本中包含了相应的功能。
PCI Fixup
与特定系统相关的PCI初始化修补代码
6.6.1 Linux核心的PCI数据结构
图6.5 Linux 核心PCI数据结构
当Linux核心初始化PCI系统时,它建立一些可以描述系统PCI拓朴的数据结构。图6.5所示是反映图6.1系统
的数据结构之间的关系。
每一个PCI设备(包括PCI-PCI桥)用一个pci_dev数据结构来描述。每个PCI总线用一个pci_bus结构来描述。
这样的结果是产生了一个PCI总线树状关系结构。每个PCI总线结构pci_bus下挂着在该总线上的PCI设备。
因为除了主PCI总线,总线0,PCI总线只能通过PCI-PCI桥来存取,每个pci_bus中含有一个指向其上的PCI
设备(PCI桥)的指针(这些设备pci_bus结构用链表连在一起,如图6.5)。一个PCI设备是其”父“PCI总线的
”孩子“。(请注意图6.5中指针children)
图6.5中没有显示出来的一个指针是pci_devices。它用来指向系统中所有的PCI设备。系统中所有的PCI设
备将其pci_dev数据结构加入到这个队列中。这个队列被Linux核心用来快速查找系统中的PCI设备。
6.6.2 PCI设备驱动程序
PCI设备驱动程序并不是真正的,严格意义上的驱动程序。它是在系统初始化时被调用的一个操作系统函数。
PCI初始化代码必须扫描系统中所有的PCI总线,寻找系统中所有的PCI设备(包括PCI-PCI桥设备)。
它使用PCI BIOS代码来发现它正在扫描的PCI总线上的每个插槽上是否已有设备安装。如果在一个插槽上发现
了一个设备,一个用来描述该设备的pci_dev数据结构将被创建并且加入到被pci_devices所指向的队列中。
PCI初始化代码从PCI总线0开始扫描。它通过读取”Vendor Identification"和"Device Identification"
来试图发现每一个插槽上的设备(请参阅6.2)。
如果发现了一个PCI-PCI桥,则创建一个pci_bus数据结构并且连入到由pci_root指向的pci_bus和pci_dev
数据结构组成的树中。PCI初始化代码通过设备类代码0x060400来判断一个PCI设备是否是PCI-PCI桥。然后,
Linux核心开始构造这个桥设备另一端的PCI总线和其上的设备。如果还发现了桥设备,就以同样的步骤来
进行构建。这个处理过程称之为深度优先算法。系统的PCI拓朴在广度查询之前,先进行深度优先查找。
请参阅图6.1,由上述算法可知,在构造PCI总线0上的Video设备之前,Linux将首先构造PCI总线1和其上的
Ethernet和SCSI设备(译者注:这里的前提是:图6.1中PCI-PCI桥所在的插槽号码小于video卡所在的PCI插
槽的号码)
当Linux查询downstream PCI总线时,它必须构造PCI-PCI桥的第二级和次级总线编号的号码。下面我们将对
此进行相信描述。
构造PCI-PCI桥---对PCI总线号码进行赋值
图6.6构造一个PCI系统:第一步
PCI-PCI桥要想正确传递对PCI I/O,PCI Memory或PCI Configuration地址空间的读和写请求,必须知道下
列信息:
Primary Bus Number(主总线号)
该PCI-PCI桥的紧接的upstream总线的编号。
Secondary Bus Number(第二级总线号)
该PCI-PCI桥的紧接的downstream总线的编号。
Subordinate Bus Number(次级总线号)
该桥的downstream总线中最大的总线编号。
PCI I/O 和 PCI Memory 窗口
对于该桥的所有downstream地址中的PCI I/O和PCI Memory地址空间的窗口的基址和大小。
存在的问题是当你想要配置一个PCI-PCI桥的时候,你不知道这个桥的次级总线接口号码。你不知道该桥下
是否还有其他的PCI-PCI桥。即使你知道,也不清楚如何对它们进行赋值。解决方案是利用上述讲过的深度
递归算法来扫描每个总线。每当发现PCI-PCI桥就对它们进行赋值。当发现一个PCI-PCI桥时,它的第二级
PCI总线接口号可以被确定。然后我们暂时先将其次级总线接口号赋值为0xFF。紧接着,开始扫描该
PCI-PCI桥的downstream桥。这个过程看起来有点复杂。但下面的例子将给出清晰的解释。
PCI-PCI桥的赋值--第一步
以图6.6的拓扑结构为例,扫描时首先发现的桥是Bridge1(桥1)。桥1的downstream PCI总线号码被赋值1。
自然该桥的第二级总线号码也是1。其次级总线号码被暂时赋值为0xFF。上述赋值的含义是所有类型1的
含有PCI总线1或更高(<255)的号码的PCI配置地址将被桥1传递到PCI总线1上。如果PCI总线号是1,桥1
还负责将配置地址的类型转换成类型0。否则,就不做转换。上述动作就是开始扫描总线1时Linux初始化
代码所完成的对总线0的配置工作。
图6.7 构造一个PCI系统:第二步
PCI-PCI桥的赋值--第二步
Linux使用深度优先算法进行扫描。所以初始化代码开始扫描总线1。从而PCI-PCI桥2被发现。因为在桥2
下面不再发现有PCI-PCI桥,所以桥2的次级总线号是2,等于它的第二总线接口号。图6.7显示了在这个时刻
总线和PCI-PCI桥的赋值情况。
图6.8 构造一个PCI系统:第三步
PCI-PCI桥的赋值--第三步
PCI初始化代码从总线2的扫描中回来接着进行扫描总线1。这时,另外一个PCI-PCI桥,桥3,被发现。它的
主总线号被赋值为1;第二级总线号为3。因为总线3上还发现了桥,所以桥3的次级总线号被暂时赋值0xFF。
图6.8显示了这个时刻系统配置的状态。到目前为止,含有总线号1,2和3的类型1PCI配置周期都可以被正确
地传送到相应的总线上。
图6.9 构造一个PCI系统:第四步
PCI-PCI桥的赋值--第四步
现在Linux开始扫描PCI总线3,桥3的downstream.PCI总线3上有另外一个PCI-PCI桥,桥4。因此桥4的主总线
号的值为3。第二总线号为4。由于桥4下面没有别的桥设备,所以桥4的次级总线号为4。然后初始化代码回到
PCI-PCI桥3。这时就将桥3的次级总线号从0xFF改为4,表示总线4是从桥3往下走的最远的PCI-PCI桥。最后,
PCI初始化代码将4以同样的道理赋值给桥1的次级总线号。图6.9反映了系统最后的状态。
6.6.3 PCI BIOS函数
PCI BIOS函数是一些在所有平台上都通用的一些标准例程。例如,对于Intel和Alpha AXP系统,它们都一样。
BIOS函数的存在使得CPU可以存取所有的PCI地址空间。
只有Linux核心代码和设备驱动程序可以使用这些函数。
6.6.4 PCI Fixup(补充或修补)
相对于Intel系统,Alpha AXP系统的PCI fixup代码要作更多的事情。对于Intel系统,基本上PCI fixup
什么也不做。
对于Intel系统,系统的BIOS在启动时,已经基本上将PCI系统构造好了。这使得Linux只需将配置映射过来
就好了。对于非Intel系统,Linux还需做如下构建:
*为每个PCI设备分配PCI I/O和PCI Memory空间。
*为系统中的每个PCI-PCI桥,配置相应的PCI I/O和PCI Memory地址窗口。
*为每个设备的配置头产生“Interrupt Line"值;这些值控制设备的中断处理。
下面我们讲述上述行为的实现。
查询设备所需的PCI I/O和PCI Memory空间大小
系统对每个找到的PCI设备查询设备所需的PCI I/O和PCI Memory空间大小。为了做到这一点,每个基址
寄存器先全写入1然后再读。设备将在没有用的位上返回0值。从而我们可以得知地址空间的大小。
图6.10 PCI配置头:基地址寄存器
基地址寄存器分两种类型,以表示一个寄存器是位于PCI I/O空间或PCI Memory空间。这是通过寄存器的位
0来设置的。图6.10所示是对应于PCI I/O和PCI Memory的两种形式的基址寄存器。
为了探测一个给定的基地址寄存器要申请的地址空间的大小,可以通过上述先向寄存器写入全1然后再读取
的方法。返回值即是该基地址寄存器所申请的空间大小。这种设计还保证了所有的地址空间都是2的幂数从
而且是自然对齐的。
例如当初始化DECChip 21142 PCI快速Ethernet设备时,我们会知道它需要0x100字节的PCI I/O或PCI Memory
空间。Linux PCI初始化代码将负责分配这片内存。然后,21142的控制和状态寄存器就可以在这些地址上被
访问。
为PCI-PCI桥和PCI设备分配PCI I/O和PCI Memory空间
象所有的存储空间一样,PCI I/O和PCI Memory空间也是非常有限的,或稀少的。PCI Fixup代码必须非常
有效地为每个设备分配其申请的空间。PCI I/O和PCI Memory必须以自然对齐的方式来被分配。例如,如果一
个设备申请0xB0字节的PCI I/O空间,它必须对齐在一个是0xB0倍数的地址上。另外,对任何一个桥,其所需
要的PCI I/O和PCI Memory必须分别对齐4K和1M的边界。由于一个桥的所有的downstream设备的地址空间都必
须位于PCI-PCI桥的地址空间内,所以有必要提供一个有效的算法来进行控制。
Linux使用的算法依赖于由PCI设备驱动程序建立的总线/设备树状数据结构中的每个设备分配的空间。空间
是朝上增长的。系统使用一个递归算法来扫描pci_bus和pci_dev数据结构。扫描从PCI总线的根开始
(其指针是pci_root)。具体的行为如下:
*分别依照4K和1M字节的边界,调整当前的PCI I/O和PCI Memory的基址。
*对当前总线上的每个设备:
。分配PCI I/O和Memory空间
。相应调整全局的PCI I/O和PCI Memory基址
。使能(enable)设备使用被分配的空间
*递归地对该总线下面的所有总线进行空间分配。注意这会改变PCI I/O和PCI Memory基址。
*分别依照4K和1M字节的边界,调整当前的PCI I/O和PCI Memory的基址。并且计算出当前
PCI-PCI桥的PCI I/O和PCI Memory空间窗口的基址和大小。
*将上一步骤计算出来的值对当前的PCI桥进行赋值。
*打开桥的对PCI I/O和PCI Memory地址过滤功能。这意味着如果一个在桥的主总线上的对
PCI I/O和PCI Memory 地址的寻址落在这个桥的PCI I/O和PCI Memory窗口内,该寻址指令
将被传递到桥的第二级总线上。
以图6.1 PCI系统为例,我们给出PCI Fixup代码的工作如下:
校准PCI基址
PCI I/O在0x4000; PCI Memory在0x100000。这使得PCI-ISA桥将接受所有低于这些值的寻址,作为ISA寻
址周期。
Video 设备
这个设备需要0x200000字节的PCI Memory。因为为了和要求的空间大小对齐,我们从0x20000地址开始
分配0x200000空间。PCI Memory的基址移到0x400000。PCI I/O的基址还是0x4000。
PCI-PCI桥
现在碰到了PCI-PCI桥并对其分配PCI内存。 注意在这里我们不需要调整基地址。
Ethernet 设备
该设备为其PCI I/O和PCI Memory空间各要求0xB0字节。在PCI Memory的基址0x400000和PCI I/O的基址
0x4000的基础上进行分配。结果是PCI I/O基址的值为0x40B0。PCI Memory基址为0x4000B0。
SCSI 设备
该设备要求0x1000 PCI Memory空间。系统依照对齐的要求,在0x401000的基础上开始分配。从而PCI
Memory的基址被调整至0x402000。PCI I/O的基址不变。
PCI-PCI桥的PCI I/O和PCI Memory 窗口
现在来设置桥的PCI I/O和PCI Memory的窗口大小。PCI I/O的窗口在0x4000与0x40B0之间。
PCI Memory的窗口在0x400000与0x402000之间。这将使得桥总线上忽略对Video的寻址,而将传递对
SCSI和Ethernet的寻址。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:58
|
|
第 七 章 中 断 与 中 断 处 理
第七章 中断与中断处理
本章讲述Linux内核如何处理中断。
虽然通常操作系统都提供一些通用的机制和接口来处理中断,大多数中
断处理的细节是与具体的设备体系结构有关的。
图7.1
Linux支持许多不同的硬件。视频设备驱动显示器,IDE设备驱动磁盘等
等。你可以同步地驱动这些设备:发出一个操作请求然后等待操作的完
成(比如将一块内存的内容写进磁盘)。这种方法虽然在逻辑和实践上
都行的通,但是效率很差。在等待你请求的操作返回的时候,操作系统
什么也不能作(busydoingnothing),浪费了许多CPU时间(译者:外设的速
度比CPU通常慢很多)。一个更好的,更有效的方法是:操作系统发出
外设操作请求,然后去做别的事情。当外设请求完成并返回时,中断操
作系统。通过这种方案,系统里可以同时支持许多设备请求,而不是
严格的同步。
不管系统采用什么CPU,我们必须用一些硬件来提支持设备中断CPU。
大多数处理器(比如AlphaAXP)采用类似的方法:CPU管葵7d(Pin)中的一些
上的电压的变化(例如从+5伏到-5伏)可以导致CPU停止它正在处理的
事情,而转到一段特殊的代码过程上去处理中断。在那些CPU管脚中,
有一个管脚连接在一个内部定时器上,从而可以每秒接收1000次的中断
。其他的负责中断的管脚会连在相应的其他设备上,比如SCSI驱动器。
在传递中断信号到一个CPU中断管脚前,系统常采用一个中断控制器并
用它将众多的设备中断组合起来。这样就节省了CPU的中断管脚并且给
系统设计带来了灵活性。中断控制器常用其的Mask寄存器和状态寄存器
来控制来自设备的中断信号。通过设置Mask寄存器的位操作可以允许
或屏蔽一些外设的中断。状态寄存器可以用来查询当前系统中已激活的
待处理的中断。
系统中有一些中断管脚是固定连接的,比如,实时时钟的定时器可能被
永久地连在中断控制器的管脚3上。当然,这些管脚具体连接的是什么
设备取决于插在ISA或PCI插槽上的是设备控制器。比如,中断控制器
的管脚4可能对应于PCI插槽0,而在此插槽上有可能今天是一块
Ethernet网卡,明天是一个SCSI控制器。对于这样每一种设备都提供不
同的,为特定设备而写的中断处理过程,操作系统必须提供足够的灵活
性来处理。
大多数通用微处理器采用同样的方式处理中断:当一个硬件中断发生时
,CPU停止它正在处理的指令,跳转到内存中的一个地址。在那个地址
处,含有中断处理过程或一条可以指向中断处理过程的指令。这段代
码一般运行在CPU的一种特殊模式---中断模式。一般而言,在这种模式
下,其他的中断不会被接受。当然也存在例外的情况。一些CPU将中断
按优先级划分,从而在处理低优先级的中断时,更高级的中断可以被
处理。换句话说,最低级的那断中断处理过程必须非常细心的编写。例
如,需要一个自己的栈空间以用来保存CPU的执行状态(所有的CPU寄
存器和上下文)在CPU被剥夺并处理更高一级的中断之前。(译者注:1
。上下文:context.这里的上下文讲的是用户进程切换到核心态时的上
下文。2。中断处理是一个过程,通常依附在一个进程的上下文中。3
。这里讲的栈通常是指一个进程在核心态下的核心栈。所有核心态下的
“过程”调用包括中断处理过程都在这个栈上处理。)。有些CPU提供
一套特殊的寄存器集。这套寄存器集只存在于中断模式下。从而中断过
程处理代码可以利用它们来保存大多数的,需要保存的上下文。(译者
注:现代操作系统调度中,一个很大的代价发生在进程上下文切换中。
感兴趣的读者可以访问UCBerkeley的NOW项目中的co-schedule部份。提
供一套寄存器有利于性能优化。)
当中断处理完毕后,CPU的状态被恢复;CPU将继续从断点处执行(译
者住:这个断点有可能是会到用户态,也有可能仍然在核心态,比如,
继续完成系统调用或处理低一级的中断请求。)。所以,中断处理程序
要尽可能的高效以防止堵塞其他的中断。
7.1可编程中断控制器
系统设计师可以随意选择他们希望的中断控制器硬件。IBM-PC系列用的
是INTEL的82C59A-2CMOS可编程中断控制器系列。这种控制器自从有了
PC就存在了,提供了一套可编程的,在ISA地址空间里,地址是周知
(Well-Known)的寄存器。任何一个现代的逻辑chipsets为这些寄存器保留着
同样的ISA内存地址。非INTEL处理器系统,例如基于AlphaAXP的PC就
不受上述限制。它们采用不同的中断控制器。
图7.1所示是两个8位的控制器连在一起,PIC1和PIC2。每一个控制器
有一个Mask寄存器和一个中断状态寄存器,Mask与中断状态寄存器的
地址分别在ox21,oxA1和ox20,oxA0。对一个Mask的某一位置1将允许一个
中断;置0将屏蔽一个中断。例如,位3置1将使能(Enable)中断3。反之
将屏蔽中断3。遗憾的是,中断Mask寄存器是只能写,不可读,你不能
读回刚刚写进去的位值。这意味著Linux必须在核心中保存一份当前Mask
寄存器的备份。每次核心先改写这个“最近的”备份然后一次性的刷
新Mask寄存器。
当一个中断到来时,中断处理过程读图中的两个中断状态寄存器(ISR)。
系统把在0x20的ISR当做这个16位中断状态寄存器的低8位,在地址0xA0
上的ISR为高8位。所以如果在oxA0上的ISR的第一位被置一的话,系统
认为来了一个中断9。PIC1的第2位被用来连接PIC2。所以任何PIC2的中
断都会导致PIC1的第2位被置1。
7.2中断处理数据结构的初始化
核心的中断处理数据结构的设置由设备驱动程序(DeviceDriver)来负责,因
为是它们需要控制中断。设备驱动程序利用Linux核心中的一些服务例
程(译者注:核心中一些预先编好的功能函数。)来请求,使能或屏蔽一
个中断。
这些各自不相同的设备驱动程序通过调用上述例程来等级它们的中断处
理过程地址。
对于PC体系结构,一些中断的中断号是固定的,约定好的。初始化时
,驱动器只要申请这个中断就可以。比如软盘驱动器将固定使用IRQ6。
有时候设备驱动程序不知道设备将使用那个中断。对于PCI设备驱动程
序而言,这不是个问题因为设备占用的中断号可以被知道。但对于ISA
设备驱动程序就不是那么容易知道。Linux通过允许设备驱动程序探测中
断号来解决上述问题。
首先,这个设备驱动程序通过一些操作使得这个设备发出中断。然后使
能所有的,系统中还没有分配出去的中断号。这意味著这个设备发出
的中断通过中断控制器会被系统接收。然后Linux读取ISR的内容并将当
前值传递给上述的设备驱动程序。一个非0值将意味著一个或多个中断
已经发生。这时,设备驱动程序重新屏蔽所有未分配的中断口。
ISA设备驱动程序在知道它的设备占用的中断号后,就可以象正常一样
去注册它的中断处理过程了。
基于PCI的系统比起基于ISA的系统有更多的灵活性。ISA设备一般通过
设置硬件板上的跳线(Jumpers)来设置中断。跳线设置后,在系统初始化
后,核心程序中这个中断号是已经固定的分配给这个设备了(译者注:
如果没有中断冲突的化)。然而,PCI设备的中断是在系统启动时,通过
PCIBIOS或PCI子系统在初始化时来分配的。每一个PCI设备卡有四个中
断管脚,A,B,C和D。通常设备缺省使用管脚A。每一个PCI插槽的A,B,C和
D中断管脚都被引向中断控制器。所以PCI插槽4的管脚A可能映射在中
断控制器的管脚6上,管脚B可能映射在中断控制器的管脚7上。
PCI中断的如何映射跟不同的系统有关。任何一个系统都要提供一些代
码用来解释PCI中断映射拓朴。基于INTEL的PC通过BIOS代码。对于没有
BIOS的系统(基于AlphaAXP的系统),Linux核心将会负责处理上述任务。
上述PCI设置代码将每块PCI设备相对应的IRQ号写入一个PCI配置头
(ConfigurationHeader)数据结构。IRQ号的获得是通过PCI中断映射拓朴,
PCI插槽和哪一个PCI中断管脚正被使用而推导出来的。对每块PCI设备
,它用的IRQ号将被固定下来并写入其相应的PCI配置头数据结构的值域
中--"interruptline"。当这个设备运行时,它读取这个信息然后向Linux核心
要求占有这个中断的处理权。
在一个系统中,有可能同时存在许多PCI中断源。例如当PCI桥的情况下
。所以就有可能中断源的数目超过系统提供的可编程中断控制器的管脚
数目。这种情况下,PCI设备之间可能要共用一些中断口,中断控制器
一个管脚将接收来自多个PCI设备的中断。Linux允许第一个申请占有一
个特定中断口的中断源愿不愿意将这个中断口被其他设备共享。共享的
中断口信息都存放在一些叫做irqaction的数据结构中。irqaction结构的地址
可在一个向量irq_action中找到。当一个共享的中断发生时,Linux将调用
挂在这个中断上的,所有的设备的,中断处理程序。因此任何一个可以
支持共享中断的设备驱动程序(所有的PCI驱动程序)都必须能够支持其
中断处理过程被调用虽然在那个时刻这个设备没有中断发生。
7.3中断处理
图7.2
Linux中断处理子系统的一个首要任务是当处理中断时,将指令控制指向
正确的中断处理代码过程。完成上述任务的代码必须了解系统的中断分
部情况。例如,如果软盘驱动控制器用的中断口是中断控制器的管脚6
,那么当接收到一个中断信号6时,系统必须将CPU执行地址转到软盘
设备驱动程序代码处。Linux使用一系列指针指向含有中断处理例程的数
据结构。这些例程分别属于系统中不同的设备驱动程序。每一个设备驱
动程序在初始化时负责申请它所需要的中断号。如图7.2所示,irq_action
是一个指针向量指向irqaction数据结构。每一个irqaction数据结构含有为这
个中断口(译者注:irq_action向量的下标加1)服务的处理程序的信息(包含
中断处理程序的入口地址)。至于系统支持的中断数目和中断如何被处
理,对于不同的(硬件)体系结构和操作系统,方法不一样。Linux的中断
处理代码是与体系结构有关的。irq_action向量的大小依赖于系统中中断
源的数目。
当一个中断发生时,Linux首先必须通过读取当前的ISR(中断状态寄存器
)来决定中断的来源。然后核心把这个中断源映射到irq_action向量一个偏
移量上。例如,一个来自软驱的中断6将被映射到向量的第7个入口。
如果对于一个发生了的中断没有一个中断处理句柄相对应,Linux核心将
记载一个错误。否则,核心将通过查询所有的”挂“这个中断口上
irqaction结构并调用相应的中断处理例程。(译者注:如果是线性的链表
查询的话,可以在这里做一些算法上的优化。如将最近常发生中断的那
个中断源的数据结构移到链表的前面。很多现成的算法可以用上来。)
当一个设备驱动程序的中断处理例程被Linux核心调用以后,它必须迅速
地解决为什么来了中断并做出反应。为了找出中断的原因,设备驱动程
序会读取这个中断设备的状态寄存器。这个设备有可能正在汇报一个
错误或一个请求的操作完成。比如,一个软盘控制器汇报对一个指定的
磁盘扇区的磁头定位已经结束。一旦中断的原因被查明,设备驱动程
序有可能需要采取更多的工作去响应这个中断。如果是这样,Linux核心
提供机制允许设备驱动程序推延其操作。从而可以避免CPU花费太多的
时间在中断模式下。有关这方面的细节请参阅设备驱动程序章节。
-------------------------------- I Love Operating System......:-)
文章选项:
|
BNN (enthusiast)
01-11-21 08:59
|
|
第八章 设备驱动程序
--------------------------------------------------------------------------
操作系统的目的之一就是掩盖掉各种硬件的特殊性。使得系统中的硬件设备对于用
户而言是透明的,例如,不管底层是什么样的物理设备,虚拟文件系统提供一个一致的,
安装好的文件系统。本章将描述Linux核心如何管理系统中的物理设备。
系统中CPU不是唯一的智能设备,每一个物理外设都有其设备控制器。键盘,鼠标和
串行接口由多功能卡(SuperIO)控制,IDE磁盘由IDE控制器掌握,SCSI磁盘有SCSI控制器
控制。每一个于硬件控制器都有其自己的控制和状态寄存器(CSR)。这些CSR在不同的设备
中是不一样的。一个Adaptec 2940 SCSI控制器的CSR与NCR810 SCSI控制器差别很大。CSR用
来启动和停止一个设备,用来初始化一个设备和检测故障。用来管理系统中硬件控
制器的代码位于Linux核心中,而不是在每个应用程序中。用来管理硬件控制器的软件通
常叫做设备驱动程序。Linux核心的设备驱动程序基本上是一些共享库(Shared Library),在
库中含有一些特权的,常住内存的,一些用来处理底层硬件的例程。Linux的设备驱动程
序用来处理各种硬件的多样性。
操作系统的基本功能之一是对设备处理的抽象化。所有的物理设备被当做正规的文
件来处理,可以被“打开”,“关闭”,“读”和”写“,就像我们用系统调用处理文
件一样。(译者注:“文件”是一个逻辑上的概念;设备是一个实体。这里谈的是把设备
抽象在/dev文件系统下。)系统中每一个设备都对应一个设备特殊文件(device special
file),例如,系统中的第一个IDE磁盘的设备文件名是/dev/hda。对于块设备(如,磁盘)
和字符设备,它们的的设备特殊文件通常是通过mknod命令用主设备号和次设备号来描述和
创建。(译者注:主设备号和次设备号用来定位系统中两个表。一个主设备对应一个设备驱
动程序。次设备的含义是系统中可以存在多个设备属于同一类,比如多个IDE磁盘。 但它们
只需要一个同样的设备驱动程序来管理。)网络设备也同样是一个设备特殊文件,但它是由
Linux核心来创建当系统发现并初始化网络控制器的时候。被同样一个设备驱动程序所管理的
所有设备拥有一个同样的主设备号。次设备号用来区分不同的设备和设备控制器。例如,每个
IDE磁盘主设备的每个分区都有个不同的次设备号。所以,/dev/hda2,这个第2个分区的主设备
号是3,次设备号是2。Linux将系统调用中(比如将一个文件系统安装在一个块设备上)传递过来
的设备特殊文件名映射到相应的设备驱动程序(根据其相应的主设备名)和许多系统表中,如字
符设备表,chrdevs。
Linux支持三种硬件设备类型:字符,块和网络设备。字符设备的读写不需要缓冲,
例如系统的串行接口/dev/cua0和/dev/cua1。块设备的读和写只能以块的单位来进行,块的大
小一般是512字节或1024字节。块设备的读写是通过缓冲Cache并且可以被随机存取。
随机存取意味著你可以定位块设备的任一个块并进行读取;块设备的存取可以通过
其设备特殊文件,但更通常的是通过文件系统。只有块设备支持文件系统的安装(Mount)
。网络设备的存取是通过BSD的Socket接口和网络子系统(请参阅网络章节)。
Linux支持许多不同的设备驱动程序(Linux的优点之一)。它们都具备一些共同的属
性:
核心态:
设备驱动程序是核心的一部份,就象核心中其他代码一样,如果不正确运行,会严
重地毁坏系统。一个写的不好的驱动程序 可能使系统崩溃,并可能将文件系统打乱
丢失数据. (译者注:作者在这提“核心态”的目的是指核心态下运行的代码可以几
乎完全控制一个系统。)
核心接口:
设备驱动程序必须提供一个标准的接口给Linux核心或相应的子系统。例如,终端
驱动程序提供一个文件I/O接口给Linux核心;SCSI设备驱动程序提供一个SCSI设备
接口给SCSI子系统。SCSI设备接口提供文件I/O,SCSI子系统提供缓冲机制。
核心机制和服务:
设备驱动程序利用标准的核心服务,如内存分配,中断传送,等待队列来运行。
可装卸的:
大多数的Linux设备驱动程序可以在需要时被载进系统作为核心的一个模块;可以
被卸下当不再被使用。这使的核心的自适应性非常好,系统的资源可以有效地被利
用。(译者注:读者可以联想一下Windows 操作系统中的DLL(Dynamic Link Library)的
概念)
可重构的:
Linux设备驱动程序可以被构造进核心。当核心重新编译时,那些设备就是可重构
的。
动态的:
当系统启动时,每一个设备驱动程序进行初始化,寻找其控制的设备。如果核心中
一个设备驱动程序所对应的控制设备不存在(译者注:例如没有安装SCSI磁盘虽然系
统有SCSI驱动程序),也没有关系。这种情况下,系统中只不过是多了一个“多余的”
驱动程序,占用了一些系统内存而已。对系统本身无碍。
8.1 检测与中断
每次设备接受一个命令,例如,“移动读磁头到软盘的第42扇区”,为了知道这个
命令是否完成,设备驱动程序有两种选择:(不断地)检测这个设备或使用中断。(译
者注:“不断地”可以理解为:“while(!(read_device_status_register()));”
)
检测一个设备意味著频繁地读(设备的)状态寄存器直到状态寄存器值的变化显示该
设备已经完成请求。如果一个设备驱动程序是核心的一部份,上述行为将是一种灾
难性的因为核心什么其他的也不能作直到设备完成服务请求(译者注:这种方法极大
地牺牲了系统的并发性。例如,其他进程全部被阻塞因为在核心态时,进程是不可
被抢先的(或被剥夺的。)。一个替代的方法是使用一个系统定时器,设备驱动程序
每隔一定时间调用设备驱动程序中的一个例程去检测服务命令是否完成。Linux的软
盘驱动程序就是这样工作的(译者注:不知道这种方法的优点何在?)。一种更有效
的方法是使用中断。
中断驱动的设备驱动程序意味著:任何时候,它所管理的设备需要被处理时,该设
备会发出一个中断。例如,每当一个Ethernet网卡控制器从网络上接收一个Ethernet数
据包时,系统将会接收到一个中断。Linux核心需要能够传送这个来自设备的中断到相应的设
备驱动程序。这是通过该设备驱动程序(在初始化时)登记它所管理的中断号来达到
的(译者注:请参阅中断处理章节)。它并且登记对应该中断的中断处理程序的地址。
读者可以通过/proc/interrupts来查阅哪一个中断别哪一个设备驱动程序所使用和
其中断的类型。
0: 727432 timer
1: 20534 keyboard
2: 0 cascade
3: 79691 + serial
4: 28258 + serial
5: 1 sound blaster
11: 20868 + aic7xxx
13: 1 math error
14: 247 + ide0
15: 170 + ide1
这个申请中断资源的过程发生在驱动程序初始化的时候。系统中有一些中断号的使
用是固定的,这是由于IBM PC体系结构的习惯遗留(Legacy)而来。例如,软盘控制
器将一直使用中断6。其他中断,如PCI设备的中断是在系统启动时动态分配的
(译者注:请注意ISA设备与PCI设备在中断号占用方面的区别)。这种情况下,设备
驱动程序在登记/申请系统中一个中断号之前,将首先探测它所管理的设备所将占用
的IRQ。对PCI中断,Linux支持标准的PCI BIOS回调函数,以用来决定系统中设备的
信息,包括其中断号。
一个中断如何被传递到CPU中,不同的硬件体系结构有不同的方法。但大多数系统中,
中断的传递是通过一种特殊的模式,在这种模式下,系统其他的中断不会发生(译者
注:这与处理中断时,屏蔽掉同等级的中断不是一回事,这里讲的是“传递”中断
)。一个设备驱动程序的中断处理例程要尽可能地简单快速,从而Linux核心可以能
够很快地撤销(Dismiss)这个中断并回到被中断之前的现场(译者注:系统被中断时,
有可能一个进程正在用户态下运行)。需要为接收/处理中断作很多工作的设备驱动
程序可以使用核心的bottom half handlers 或任务队列。该任务队列存放着那些将
被待后调用的函数例程。
8.2 直接内存存取-DMA(Direct Memeory Access)
当数据量很小的情况下,使用中断驱动的设备驱动程序来从/向硬件设备传递数据是
合理的,可以工作的很好。例如,一个9600波特率的Modem的传输速率近似于没毫秒
(millisecond)一个字符。如果中断的延迟,硬件设备发出中断和设备驱动程序处理
该中断的时间非常小(比如2毫秒),那么数据传输的总体系统影响也非常小。这个9600波
特率的Modem数据传输只要占用0.002%的CPU处理时间。但是对于高速设备,比如硬
盘控制器或Ethernet设备,它们的的传输速率要高很多。 一个SCSI设备能达到40M字
节每秒。
直接内存存取,或DMA,被提出用来解决传输上述大批量数据的问题。一个DMA控制
器允许设备与内存之间发送或接收数据,但不影响处理器CPU。PC的ISA DMA控制器
有8个DMA通道。第7个通道被用来为设备驱动程序服务。每一个DMA通道与一个16位
的地址寄存器器和一个16位的计数寄存器相关联。当想要发起一次数据交换时,设
备驱动程序设置相应DMA通道的地址,计数寄存器的大小,这次数据传输的方向(读
或写)。然后通知设备可以启动DMA操作。当DMA结束时,设备才中断系统。因此,在
数据传输的过程中,CPU可以作其他的事情。
在使用DMA时,设备驱动程序必须额外小心。首先,对于DMA控制器而言,没有虚拟
内存的概念,它所面对的,存取的是系统中的物理内存。因此被DMA的内存必须是一
块连续的物理内存块。这意味著你不能通过DMA去“直接”存取进程的虚拟空间地址。
当然一个方法是在DMA期间,可以锁住一个进程的一些物理页面,防止操作系统将其
对换到swap空间上,从而保证DMA正确地完成。
DMA通道是“短缺”资源,只有7个通道。而且通道不能被设备驱动程序间共享。就
象中断一样,一个设备驱动程序必须能够知道哪一个DMA通道它要使用。有些设备使
用固定的中断号,就象有些设备使用固定的中断号一样。例如,软驱设备使用的DMA通
道一直是通道2。有时一个设备的DMA通道可以由跳线来设置。许多以太(Ethernet)设
备使用这种技术。一些更灵活的设备可以通过其CSR得知当前系统中哪些DMA通道是
空着的。从而设备驱动程序可以随便挑选一个DMA通道使用。
Linux通过一个向量数据结构dma_chan(每一个DMA通道对应一个这样的数据结构)来
掌握DMA通道的使用情况。dma_chan结构中只包含两个域:一个指向一个字符串的指
针,这个指针描述了这个DMA通道拥有者。另外一个域是一个标志,用来显示当前的
DMA通道是空着的还是已被占据。当你使用命令"cat /proc/dma"时,其实是核心中
的向量dma_chan被打印出来了。
8.3 存储器
在使用内存时,设备驱动程序要小心,因为它们是核心的一部份,故不能使用虚拟
内存(译者注:作者在上一节和这里反复强调“虚拟内存”是因为运行在不同“虚拟
内存”空间中的用户态进程之间不会发生冲突。操作系统的内存管理机制将负责。
)。每一次设备驱动程序因为来了中断,或者bottom half或任务队列中的句柄被调
度到而运行,当前的进程有可能被剥夺。所以设备驱动程序不能依赖于一个特殊的
运行的进程,虽然设备驱动程序运行在一个进程的上下文上。象核心中的其他部份
一样,设备驱动程序使用数据结构来管理跟踪它所控制的设备。这些数据结构可以
静态地分配,作为设备驱动程序代码的(数据的)一部份,但这样会使得核心变的太
大,造成资源的浪费。大多数设备驱动程序采用从核心中动态分配非页面的内存用
来存储数据。
Linux提供核心内存分配和释放的例程以供设备驱动程序使用。核心内存的分配是以
2的幂次方为单位的。例如,128字节或512字节即使设备驱动程序需要的内存量少于
这些值。设备驱动程序申请的(被分配的)字节数被“凑”到下一个块的边界处。这
种方法使得内存的释放回收更容易因为系统可以将这些小的空闲块合并成更大的内
存块。(译者注:以2的幂次方为单位进行内存分配可以减少系统中内存被弄的零碎。
)
当核心内存被申请时,Linux有可能要作许多额外的工作。如果剩余的内存太下的话,
一些物理页面需要被丢弃或写进对换磁盘空间。通常地,Linux将这个处理挂起并放
到一个等待队列中直到系统中有足够的物理内存。当然不是所有的设备驱动程序(至
少Linux核心代码)都希望这样被处理。所以当不能立刻分配内存时,核心内存分配
例程可以直接返回一个“失败”。如果设备驱动程序希望用DMA与被分配的内存来交
换数据,它可以指定这片内存是DMA'able的。这种情况下Linux核心需要了解系统中
什么地方构成了DMA'able的内存。
8.4 设备驱动程序与核心的接口
Linux核心必须能够通过一些标准的方法来和设备驱动程序接口。每一类设备驱动程
序,(字符,块和网络)都提供一个一致的,共同的接口给核心以用来核心向它们申
请服务。这些共同接口(common interfaces)意味著核心可以将这些不同的设备和其
驱动程序一样来对待。例如,SCSI和IDE磁盘的行为是不同的。但Linux核心对它们
使用一个同样的接口进行操作。
Linux是非常动态的,可重构的。每次一个Linux核心启动时,可能遇到不同的物理
设备,因此需要不同的相应的设备驱动程序。在核心重新构建(Build)的时候,Linux允
许通过配置文件将设备驱动程序带进核心。当这些驱动程序在机器启动时初始化的
时候,有可能系统中并没不存在相应的物理设备。有些驱动程序可以在需要时被装
载进入核心。为了处理设备驱动程序的这种动态特性,系统要求设备驱动程序在初
始化时向系统进行登记。Linux核心负责维护一些含有登记了的设备驱动程序的表。
这些表中包含了一些例程(rountines)的指针和其他一些信息以用来支持核心与那些
设备的接口。
8.4.1 字符设备
图8.1 字符设备
字符设备,Linux中最简单的设备,是通过”文件“的形式被存取。应用程序使用标
准的系统调用“打开”,“读”,“写”,和“关闭”字符设备就像它是一个文件
一样,即使这个设备是一个被PPP监控程序(Daemon)用来将Linux系统连接上网的Modem。
当一个字符设备初始化时,它的设备驱动程序在Linux核心中登记,通过添加一个入
口项(Entry)在含有device_struct数据结构的chrdevs向量中。这个设备的主设备号
(例如,4对于tty设备)被用来作为其在这个向量的索引。一个设备的主索引号是固
定的。
chrdevs向量的每一个入口项是一个device_struct数据结构,含有两个元素。一个
指向那个登记”在这个入口处“的设备驱动程序名字的指针;一个指向一系列文件
操作函数地址的指针。这些文件操作函数位于这个字符设备的驱动程序里并负责处
理相应的具体的文件操作如:打开,读,写和关闭。文件/proc/devices中对于字符
设备的内容是从chrdevs向量获取的。
当一个代表一个字符设备的字符特殊文件被打开时(例如/dev/cua0),系统必须正确
地工作保证相应的字符设备驱动程序的文件操作例程被调用。就象一个普通文件或
目录一样,每一个设备特殊文件对应一个VFS inode。这个VFS inode(数据结构)中
含有这个设备的主和次设备号。VFS inode是当一个特殊设备文件名被查询时,由文
件系统所创见。
每一个VFS inode与一套文件操作相联系。每当一个代表字符特殊文件的VFS inode被
创建时,对应这个 VFS inode的文件操作被设置成缺省的字符设备操作。
当一个字符特殊文件被一个应用程序打开时,这个“open"操作将使用这个设备的主
设备号作为chrdevs向量的索引来查找对应这个设备的文件操作集的(例程的)地址。
并且还要设置一个描述这个字符特殊文件的数据结构--file,使得file结构中关于
文件操作的指针指向设备驱动程序中相应的部份。经过这些之后,所有用户层的文
件操作将被映射到对于这个字符设备的设备驱动程序提供的文件操作。
8.4.2 块设备
块设备同样支持以文件的形式被存取。当遇到打开块设备操作时,用来提供一套对
应的文件操作的机制与字符设备基本上是一样的。Linux在blkdevs向量中维护登记
了的块设备。与chrdevs向量一样,blkdevs向量使用设备的主设备号作为其索引。
向量的每一个入口仍是一个device_struct数据结构。与字符设备不同的是,这些数
据结构是属于块设备的。SCSI设备和IDE设备是其中两个例子。这些设备数据结构在
核心中登记并为核心提供对应于其设备的文件操作。对应于某类设备的设备驱动程
序提供实现这些接口的细节。例如,一个SCSI设备驱动程序必须为SCSI子系统提供
接口。SCSI子系统利用这些接口,提供给核心一个一致的文件接口。
除了文件操作接口,每个块设备还必须提供缓冲区接口。每一个块设备驱动程序在
一个blk_dev向量中添加其入口。blk_dev向量的每个元素是一个blk_dev_struct数
据结构。向量的索引仍然是设备的主设备号。blk_dev_struct数据结构中含有一个
请求例程的地址和一个指向“request"数据结构的指针。每一个“request"数据结
构代表了一个从缓冲区到驱动程序的读或写数据块的请求。
图8.2 块设备的缓冲
每次一个缓冲区想要读或写一块数据从/到一个登记了的设备,它将插入一个"request"数
据结构在blk_dev_struct中。由图8.2所示,每一个申请含
有一个指向一个或多个”buffer_head"的数据结构。每一个buffer_head是读或写一个块数
据的请求(译者注:请参阅Linux数据结构章节)。Buffer_head结构是被缓冲区锁住的。因此
有可能存在一个进程正在等待对这个缓冲区操作的完成。每一个“request"数据结构是从一
个静态的链表中(all_requests)分配而来。如果一个请求(request)被加在一个空的请求队列
上,设备驱动程序的请求函数(译者注:blk_dev_struc结构中函数
指针所指向的函数)将被立即调用来处理这个请求队列。否则,驱动程序将顺序地处
理请求队列中的所有请求。
一旦设备驱动程序完成一个请求,它必须从这个请求中移去每一个buffer_head结构,
将它们标志成为更新并释放对其的锁。对一个buffer_head锁的释放将唤醒所有在睡
眠中等待这个块操作完成的进程。一个例子是:当要解释一个文件名时,EXT2文件
系统必须从块设备中读取下一个EXT2目录项。这个进程将睡眠在那个含有目录项的
buffer_head上直到被设备驱动程序唤醒。这个request数据结构将被回收从而可以
被其他的块请求使用。
8.5 硬盘
磁盘将数据保存在磁盘片上,提供一种持久的存储方式。为了写数据,一个很小的
磁头在磁盘片的表面上磁化小微粒(minute particles)。数据也通过磁头来读写。
磁头能检测一个小微粒是否被磁化。
一个磁盘有一个或多个磁盘片(platters)组成。每个磁盘片的表面分成一些小的同
心圆---磁道(track)。磁道0是最外层的磁道,最大编号的磁道最靠近圆心。一个柱
面(cylinder)是指一个有同样编号的磁道集合。因此所有磁片上的所有磁道5构成了
柱面5。因为柱面的数目等于磁道德数目,我们经常看见人们使用柱面来描述磁盘。
每一个磁道分为一些扇区(sectors)。一个扇区是一个硬盘读或写的最小单位。一个
扇区的大小就是一个块的大小(译者注:换句话说,磁盘的读写是以块为单位的)。
通常一个扇区的大小是512字节。一个扇区的大小通常是在磁盘格式化的时候就被确
定了。
一个磁盘通常用其几何参数来描述,柱面的数目,磁头的数目和扇区的数目。例如,
在启动时,Linux描述一个IDE磁盘:
hdb: Conner Peripherals 540MB - CFS540A, 516MB w/64kB Cache, CHS=1050/16/63
上述意味著这个磁盘含有1050个柱面,16个磁头(8个磁片)和63个扇区/每个磁道。
如果每个扇区(或每个块)大小是512字节,这个磁盘的大小是529200字节。这个大小
与系统声称的516M大小不一致。这是因为磁盘的一些扇区已被用来存放磁盘分区信
息。例外,一些磁盘可以自动地发现坏扇区并重心索引磁盘以绕过这些坏扇区。
硬盘可以更深一步地分为一些分区(partitions)。一个分区是一个用来作某个特殊
用途的扇区的集合。将一个磁盘分区允许这个磁盘被几个操作系统使用,或允许用
来作不同的用途。许多Linux系统只有一个磁盘,但分为3个分区。一个含有DOS文件
系统;一个含有EXT2文件系统;第3个是对换分区(译者注:用于虚拟内存管理系统
)。一个硬盘的分区由一个分区表来描述。分区表中的每一个条目(entry)通过磁头,
扇区和柱面,描述了这个分区的起始和结束地址。fdisk支持3类分区类型。主分区,
扩展分区和逻辑分区。扩展分区不是一个真正的分区,可以含有任意数目的逻辑分
区。扩展和逻辑分区的发明是用来绕过系统中只允许4个主分区的限制。下面是用fdisk对
一个含有2个主分区的磁盘分区的信息:
Disk /dev/sda: 64 heads, 32 sectors, 510 cylinders
Units = cylinders of 2048 * 512 bytes
Device Boot Begin Start End Blocks Id System
/dev/sda1 1 1 478 489456 83 Linux native
/dev/sda2 479 479 510 32768 82 Linux swap
Expert command (m for help): p
Disk /dev/sda: 64 heads, 32 sectors, 510 cylinders
Nr AF Hd Sec Cyl Hd Sec Cyl Start Size ID
1 00 1 1 0 63 32 477 32 978912 83
2 00 0 1 478 63 32 509 978944 65536 82
3 00 0 0 0 0 0 0 0 0 00
4 00 0 0 0 0 0 0 0 0 00
上面所示,第一个分区起始于柱面或磁道0,磁头1和扇区1,共延伸到477柱面,扇
区32和磁头63。因为一个磁道有32个扇区,整个磁盘有64个读/写磁头,所以这个分
区占据的柱面是完整的。确省地,fdisk自动将分区依照柱面的边界对齐。它起始于
最外层的柱面(0)并向里面延伸478个柱面。第二个分区是对换分区,起始于下一个
柱面(478)并一直延伸到最里面的那个柱面。
图8.3 磁盘的链接表
在系统初始化期间,Linux将映射系统中所有硬盘的拓朴。它发现有多少硬盘和其类
型。另外,Linux发现这些磁盘是如何分区的。这一切将体现在由指针gendisk_head
指向的,一个元素为gendisk数据结构的链表中。当每个磁盘子系统初始化时,例如
IDE,它负责产生代表它所发现的磁盘的gendisk数据结构。这个行为发生在与登记
它的文件操作接口,插入一个入口项在blk_dev数据结构中的同一时间。每一个gendisk数
据结构有一个唯一的主设备号。这个主设备号与该块设备的主设备号一致。例如,
SCSI磁盘子系统产生一个单一的gendisk记录”sd“。记录中,其主设备号是8。系
统中所有的SCSI磁盘设备都拥有这个同样的主设备号。图8.3所示是两个gendisk记录,
第一个是为SCSI磁盘子系统,第二个是为IDE磁盘控制器,ide0, 主磁盘控制器。
虽然磁盘子系统在初始化时创建gendisk记录,这些记录只在Linux进行分区检测时
使用。然而,每个磁盘子系统维护一个自己的数据结构,以用来映射设备的主设备
号和次设备号到物理磁盘的分区中。任何时刻当一个块设备被读或写,不管是来自
缓冲区还是文件操作,核心将使用在块设备特殊文件中发现的主设备号(例如,/dev/sda2)
,引导读或写操作指向正确的设备。值得注意的是各个设备驱动程序负责映射次设
备号到具体的物理设备中。
8.5.1 IDE 磁盘
在Linux系统中,最常见的是IDE(Integrated Disk Electronic)磁盘。IDE是一个磁
盘接口,而不是一个I/O总线,例如象SCSI。每个IDE控制器可以支持多达2个磁盘。
一个主(master)磁盘;一个副(slave)磁盘。主和副磁盘是通过设置磁盘的跳线来完
成的。系统中的第一个IDE控制器叫做主IDE控制器。依此类推,下一个叫做第二IDE控
制器。IDE接口可以达到3.3M的传输速率。IDE磁盘容量最大是538M字节。扩展IDE(EIDE)
可以达到8.6G字节和16.6M字节的传输速率每秒。IDE和EIDE磁盘比SCSI磁盘要便宜。
大多数PC机都有一个或多个IDE控制器。
Linux依据发现IDE控制器的顺序对其上的IDE磁盘命名。在主控制器上的主磁盘是/dev/hda,
副磁盘是/dev/hdb。/dev/hdc是第二个IDE控制器 上的主磁盘。IDE子系统在核心中
登记IDE主控制器,而不是磁盘。主控制器的主设备号是3;副IDE控制器
的主设备号是22。这意味著如果系统有两个IDE控制器,在向量blk_dev和blkdevs中
将插入两个IDE子系统记录在向量索引3和22的地方。从设备特殊文件名中可以体现
这点。在主IDE控制器上的磁盘/dev/hda 和/dev/hdb的主设备号是3。任何对这两个
设备特殊文件的操作都会被核心根据被访问的主设备号传到其对应的IDE子系统中。
IDE子系统将负责是哪一个IDE磁盘被申请,通过设备特殊文件的次设备号。次设备
号里含有信息关于哪一个分区和哪一个磁盘。/dev/hdb的设备标识是(3,64)。该磁
盘上的第一个分区(/dev/hdb1)的设备标识是(3,65)。
8.5.2 IDE子系统的初始化
IDE磁盘一直贯穿在IBM PC机的历史。在这个期间,IDE接口发生了许多变化。这使
得IDE子系统的初始化变得越来越复杂。
Linux最多可以支持4个IDE控制器。每个控制器将体现在向量ide_hwifs的ide_hwif_t数
据结构中。每个ide_hwif_t中含有两个ide_drive_t数据结构,对应于可能的主和副
IDE驱动器。IDE子系统初始化期间,Linux首先通过系统的CMOS中信息查看是否有磁
盘。CMOS的位置由系统的BIOS设定并可以告诉Linux什么IDE控制器和磁盘驱动器在
系统中。Linux从BIOS中得到磁盘的几何描述信息并为这些驱动器设立ide_hwif_t
数据结构。目前越来越多的PC机使用包含了PCI EIDE控制器的PCI芯片集(chipsets)(例
如,Intel's 82430 VX )。IDE子系统使用PCI BIOS的回调(callback)来定位系统中
的PCI E(IDE)控制器,然后调用PCI专门的为这些控制器准备的例程(来初始化核心
的向量结构)。
一旦每个IDE接口和控制器被发现,相应的ide_hwif_t数据结构将被建立以反映这些
控制器和其上的磁盘。在运行期间,IDE驱动程序向I/O空间的IDE命令寄存器写入命
令。主IDE控制器的控制和状态寄存器的缺省I/O地址在0x1F0 - 0x1F7。IDE驱动程
序在Linux的块缓冲数据结构中登记每个控制器,在blk_dev和blkdevs分别加入记录。
IDE驱动器还将申请占有某个中断。对于主IDE控制器,约定的中断号是14;第二个
IDE控制器是15。然而,上述都可以别核心的命令选项所覆盖。IDE驱动
程序还要为每个IDE控制器在gendisk链中加入一个gendisk记录。这个链表用来查找
所有在启动时发现的磁盘的分区表信息。
8.5.3 SCSI磁盘
SCSI(Small Computer System Interface)总线是一个快速的端到端的数据总线。每
个SCSI总线上支持8个设备,其中包含一或多个hosts。每个SCSI设备必须有一个唯
一的标识名(一般通过磁盘的跳线来设置)。总线上的两个设备可以同步地或异步地
32位地交换数据。速率可达40M字节。SCSI总线可在设备之间数据和状态信息,并且
在发起者(initiator)和目标(target )之间,一个单一的事务(transaction)可以包
含多达8个不同状态的信息。我们可以从来自总线上的5个信号来辨别SCSI总线上当
前的状态(phase )。
BUS FREE (总线空)
当前没有设备正占据总线。没有活跃的事务处理。
ARBITRATION (仲裁)
一个SCSI设备试图占据总线。它通过将其SCSI标识数据“插入”地址线。(如有竞
争,)SCSI标识最大的获得总线。
SELECTION (选择)
当一个设备通过仲裁,成功地获得总线后,它必须向目标(Target)发出信号表示它
想要对目标发出命令。这是通过将目标的SCSI标识放入地址线而达到的。
RESELECTION
SCSI设备有可能断开连接在一个请求处理过程中。目标有可能等会儿重新选择发起
者(initiator)。不是所有的SCSI设备都支持这个状态。
COMMAND
在此状态下,可以从发起者传到目标端传送6,10 或 12 字节的命令。
DATA IN, DATA OUT
在这些状态下,数据在发起者和目标端之间传送。
STATUS
当所有命令都完成时,可以进入这个状态。允许目标发出一个状态字节以向发起者
表明成功或失败。
MESSAGE IN, MESSAGE OUT
附加的信息将在发起端和目标端传送。
Linux的SCSI子系统有两个基本元素组成。每一个都通过一些数据结构在核心中得到
体现。
host
一个SCSI host是一个物理硬件,即一个SCSI控制器。NCR810 PCI SCSI控制器就是
一个SCSI host的例子。如果一个Linux系统存在多于一个的,同样类型的SCSI控制
器,每一个将对应于不同的SCSI host。这意味著一个SCSI设备驱动程序可能控制多
于一个的SCSI控制器。SCSI host一般都是SCSI命令的发起者。
device(设备)
最常见的SCSI设备就是SCSI磁盘。但SCSI标准还支持其他的几类设备,如磁带,CD-ROM和
通用(generic)SCSI设备。SCSI设备一般都是SCSI命令的目标。这些设备必须不同的
对待。例如,可移动的介质CD-ROMs或磁带,Linux需要检测介质是否被移动了。不同
的设备类型有不同的主设备号,Linux可以依此引导不同的对块设备的请求到其相应
的SCSI设备类型上。
SCSI子系统的初始化
由于SCSI总线和设备的动态特性,SCSI子系统的初始化比较复杂。Linux在系统启动
时初始化SCSI子系统。它首先发现系统中的SCSI控制器(即SCSI hosts)然后探测在
所有SCSI总线上的所有的设备。然后初始化这些设备。通过向核心提供一套规范的
文件操作和缓冲区操作例程集,使得对于Linux核心系统来说“可见”。这个初始化
的过程分为4个阶段:
首先,Linux检测在核心构建时已加入核心的那些SCSI hosts或控制器上是否有设备
需要控制。上面每个SCSI hosts在builtin_scsi_hosts 向量中有一个入口记录相对
应。每个记录是一个Scsi_Host_Template 数据结构。Scsi_Host_Template 中含有
一些函数指针。这些函数用来执行SCSI hosts的一些特定功能,如检测什么设备正
挂在SCSI hosts上。这些例程SCSI子系统所调用,属于这种hosts设备类型的设备驱
动程序的一部份。每个在其上存在SCSI设备的SCSI host将Scsi_Host_Template 数
据结构加入一个Scsi_Host结构到一个scsi_hostslist列表 链中。例如,如果一个
系统有两个NCR810 PCI SCSI 控制器,系统数据结构中将有两个Scsi_Host记录在
scsi_hostslist列表链中。每一个Scsi_Host指向代表起设备驱动程序的
Scsi_Host_Template。
图8.4 SCSI 数据结构
到现在,系统中所有的SCSI host都已备发现,SCSI子系统必须知道在每个host上是
些什么SCSI设备。SCSI设备是按照0-7来标号的。每个SCSI设备的标号在其所安装的
host上是唯一的。SCSI标号通常是由设备上的跳线来设置的。SCSI初始化代码通过
发出TEST_UNIT_READY命令来探测一个SCSI总线上的SCSI设备。当一个设备存在并回
答时,它的SCSI标识信息(包括厂商,设备型号和版本号)被读取,通过一个ENQUIRY
命令。SCSI命令含在一个Scsi_Cmnd数据结构中。这些Scsi_Cmnd数据被传递到属于
这个SCSI host的设备驱动程序的相关函数中。这些函数的指针在先前已被登记在
Scsi_Host_Template结构中。每个已发现的SCSI设备将对应一个Scsi_Device数据结构.
每个 Scsi_Device数据结构指向其host的数据结构Scsi_Host。所有的Scsi_Device结构
链在一个叫做scsi_devices的链表上(译者注:Scsi_Device:SCSI设备;Scsi_Host:
SCSI host或控制器;Scsi_Host_Template:SCSI host的设备驱动程序入口)。图8.4所
示是上述数据结构的关系。
SCSI设备有四种:磁盘,磁带,CD和generic。每一种都分别在核心中以不同的主块
设备号进行登记。当然,只有在发现系统中存在相关的SCSI设备时才会进行登记。
每个SCSI类型,例如SCSI磁盘,维护一套其自己的表数据结构。这些表用来将来自
核心的块操作请求映射到相应的设备驱动程序上或相应的SCSI host上。每一个SCSI类
型在核心中对应于一个Scsi_Device_Template 数据结构。这个结构中含有这种设备
的信息和各种针对这种设备的例程地址。SCSI子系统使用这些模板来调用对应于每
种SCSI设备的SCSI类型函数。换句话说,如果SCSI子系统想要”attach“(添加)一
个SCSI磁盘,它将调用SCSI磁盘类型的”attach“函数。Scsi_Device_Template 数
据结构全部挂在scsi_devicelist链表上。(译者注:请注意 Scsi_Device_Template与
Scsi_Host_Template 的关系和区别)
SCSI子系统初始化的最后一步是对应于每一个登记了的Scsi_Device_Template调用”
完成“(finish)函数。对应于SCSI磁盘类型,这意味著旋转机器上所有的SCSI磁盘
然后读取它们的几何参数。(根据获得的几何参数),核心填写为每个SCSI磁盘填写
gendisk数据结构在gendisk链中。
传送块设备请求
一旦Linux 完成SCSI子系统的初始化,SCSI设备就可以被使用了。每个存在相应设
备的设备类型在核心进行了登记,从而Linux可以正确地将块设备请求定位/传送到
正确的设备上。这些请求可以是来自blk_dev 的缓冲区操作或来自blkdevs的文件操
作。举一个例子,如果有一个SCSI磁盘含有一个或两个EXT2文件系统分区,当其中
一个EXT2文件系统已被安装(mounted),核心的缓冲区申请如何被定位到正确的磁盘
上?
每个读或写一块SCSI磁盘数据的请求都导致在blk_dev向量中的current_request链
表中加入一个新的request 结构。如果这个申请队列正在被处理,缓冲区不需要做
其他的事情。否则,必须提醒SCSI磁盘子系统去处理request 队列。系统中每个SCSI磁
盘对应于一个Scsi_Disk 数据结构在rscsi_disks 向量中。rscsi_disks的索引使用
了部份SCSI磁盘分区的次设备号信息。例如,/dev/sdb1的主设备号是8,次设备号
是17,其在rscsi_disks中的索引号是1。 每个Scsi_Disk结构中含有一个指针指向
代表这个设备的Scsi_Device 结构。然后通过Scsi_Device 指向对应的Scsi_Host结
构(译者注:对应于这个SCSI磁盘所属的SCSI磁盘控制器)。 从缓冲区来的request结
构被转换成描述SCSI命令的Scsi_Cmd 结构并将其放入这个对应的Scsi_Host的队列
中。当一旦要求的块数据被读或写完成之后,SCSI设备驱动程序将会处理这些Scsi_Cmd
结构。
8.6 网络设备
从Linux的网络子系统的角度而言,一个网络设备是用来发送和接收数据的一个”实
体“或一个”东西“,比如一个ethernet网卡。每个网络设备在核心中对应于一个
device 数据结构。核心启动时,网络设备驱动程序初始化并登记其控制的设备。这
个device 结构中包含了关于这个设备的信息和一些函数的地址。这些函数被用来对
各种高层的网络协议提供底层支撑。它们大多数是关于在物理网络设备上传输数据。
网络设备使用标准的网络机制将接收到的数据向高层网络协议传送。所有传送的和
接收的数据报(packets)都对应于sk_buff数据结构。sk_buff是非常灵活的数据结构,
允许网络协议头(network protocol headers)很轻松地被加入和移去。网络协议层
如何使用网络设备,如何使用sk_buff来回传递数据,请参阅第10章网络。本章关于
网络方面的重点是网络设备数据结构和网络设备如何被检错与初始化。
device 数据结构含有网络设备的如下信息:
Name
与用mknod 命令来创建设备特殊文件的块和字符设备不同的是,网络设备特殊文件
是当系统网络设备被发现并初始化时出现的。它们的名字是标准的。每一个名字代表
了它是哪一种网络设备类型。属于同一类型的设备的名字从数字0开始往上走。因此
ethernet设备名是/dev/eth0,/dev/eth1,/dev/eth2等等 。下面是一些通用的网络
设备名:
/dev/ethN Ethernet设备
/dev/slN SLIP设备
/dev/pppN PPP设备
/dev/lo Loopback设备
Bus Information
这个信息被设备驱动程序用来控制设备。irq 数据是这个设备使用的中断。
base address 是设备的控制和状态寄存器在I/O空间的地址。DMA channel 是这个网
络设备用的DMA通道。所有的上述信息在设备初始化时被设置。
Interface Flags
用来描述网络设备的特性和能力:
IFF_UP (网络)接口在运行,
IFF_BORADCAST device中的广播地址是有效的,
IFF_DEBUG 设备的调试功能已被打开,
IFF_LOOPBACK 当前设备是一个loopback设备,
IFF_POINTTOPOINT 这是个点到点的连接(SLP和PPP),
IFF_NOTRAILERS 没有网络跟踪(No network trailers),
IFF_RUNNING 分配的资源,
IFF_NOARY 不支持ARP协议,
IFF_PROMISC 设备处在混杂接收模式,将接收任何网上数据包,
IFF_ALLMULTI 接收所有的IP多点广播(multicast)数据帧,
IFF_MULTICAST 能够接收IP多点广播(multicast)数据帧。
Protocol Information
通过这些信息,设备描述自己将如何被网络协议层所使用。
mtu 该设备能传输的最大报文大小(不包括所需要的报文头)。这个最大值
协议层被用来,例如IP,选择适当的发送报文的大小。
Family 显示该设备可以支持的协议族。所有Linux网络设备支持的协议族是
AF_INET, Internet 地址族。
Type 这个硬件接口类型描述该网络设备正与什么介质相连。在Linux网络
设备中,可以支持很多种不同的设备,包括Ethernet, X.25, Token Ring,
Slip, PPP 和 Apple Localtalk。
Address
device数据结构含有许多与该设备相关的地址,例如:IP地址。
Packet Queue
sk_buff 报文的队列。等待在这个网络设备上传输。
Support Functions
每个设备提供一套标准的例程作为该设备连接层接口一部份。
从而协议层可以进行调用。这些例程包括:设置和帧传输例程;
添加标准报文帧头和收集统计信息的例程。这些统计信息可以通过
ifconfig命令来查看。
8.6.1 网络设备的初始化
与其他Linux设备驱动程序一样,网络设备驱动程序也可以被预先构造在核心中。
每一个潜在的网络设备都对应于一个device数据结构。这些结构组成一个由dev_base
指向的链表。如果网络层需要网络设备完成一个特定的任务,它调用 一个地址已在
device结构中的网络设备服务例程。在最开始,device结构中只含有初始化(initialization)
和探测(probe)函数的地址。
网络设备驱动程序需要解决两个问题。首先,不是所有的已构建在核心中的网络设
备驱动程序都有相应的外设存在。第二,不管什么样的设备驱动程序,系统中的ethernet设
备名始终是/dev/eth0, /dev/eth1等等。网络设备不存在的问题很容易解决。因为
当为每个网络设备初始化的例程被调用时,其函数返回值将显示物理设备是否存在。
如果不存在,这个驱动程序对应的device数据结构将从被dev_base指向的链表中被
移去。如果驱动程序确实发现一个设备,device结构的剩余部份将被填充。这些包
括设备信息和设备驱动程序中的支撑函数的地址。
第二个问题是关于如何动态地将标准的/dev/ethN设备特殊文件名赋值给系统中的ethernet
设备。 在device链中,有8个标准记录。从eth0, eth1 到 eth7。它们的初始化都是一
样的:依次检测是否每个ethernet设备驱动程序有相应的物理设备存在直到发现一个。
当找到一个ethernet设备时,驱动程序填充其当前占据的ethN device数据结构。与
此同时,网络设备驱动程序初始化刚刚找到的物理设备,找出该物理设备想要占据
的IRQ号,DMA通道等等。一个驱动程序有可能检测到几个它所控制的网络设备 ,在
这种情况下,驱动程序将占据几个/dev/ethN device 数据结构。当所有的8个标准
的/dev/ethN都被分配光之后,系统将不检测其他的ethernet 设备。
-------------------------------- I Love Operating System......:-)
文章选项:
| |