http://www.ibm.com/developerworks/cn/linux/l-anaconda/index.html
引言
Linux 的安装过程可以分为两个阶段,第一个阶段就是加载内核,创建供后续安装过程使用的系统环境,第二阶段就是加载系统安装程序,执行具体的安装过程。对于第一 阶段,有不少资料作了比较详细的介绍,而对于第二阶段,也就是具体的安装过程,却鲜有资料介绍,本文作者结合自己在实践中的心得体会,针对 redhat9.0的安装程序anaconda作了粗略的分析。
为了抓住主线,文中所列代码 片断基本都经过格式调整,并裁减掉了不影响理解的细节,读者可参照源代码。另外,anaconda是用python语言写的,所以读者最好具备一些 python语言的基础和面向对象的基本知识,如果有过图形界面程序的设计的经验,则无论是对图形安装模式还是字符安装模式安装过程(本文只讨论图形安装 模式)的理解都会有很大帮助。
1 概述
系统启动,加载启动映像,在内存中建立了linux系统环境后,解析安装程序映像文件,将安装程序载入内存,执行主执行程序anaconda,该程序是具体安装程序的一个口,负责启动具体的安转过程,完成linux系统的安装。
我 们首先看一下安装映像文件。进入redhat9.0第一张系统盘的redhat/base目录,其中stage2.img就是当安装介质为CD-ROM时 的安装映像文件。我们采用如下命令解析该映像文件(注意:命令中的挂载点/mnt/stage2要根据读者的实际情况修改):
mount -o loop /mnt/cdrom/Redhat/base/stage2.img /mnt/stage2
解 析后,我们可以看到,安装程序的主执行体anaconda在/mnt/stag2/usr/bin目录下,其它安装脚本模块均在 /mnt/stage2/usr/lib/anaconda目录下,今后的行文中,除非是anaconda主执行体,其余均指 /mnt/stage2/usr/lib/anaconda目录下的模块,并将该目录简称为anaconda主目录。
我们先看一下anaconda主目录的结构:
- installclasses 子目录中的各个模块定义了在安装过程中用户可选择的安装类型。redhat9.0下包含了四个文件workstation.py,server.py, custom.py和personal_desktop.py。其中,workstation.py描述了工作站安装类型,server.py描述了服务 器安装类型,custom.py描述了用户自定义安装类型,personal_desktop.py描述了个人桌面安装类型。每个安装类型描述文件根据相 应安装类型的特点,分别对安装步骤、分区策略以及安装包的取舍给出了不同的方案。
- Iw子目录下包含所有安装图形界面类所在的模块,每个图形界面对应一个类,负责相应安装步骤图形界面的具体外观显示及与用户的交互,(可能)调用anaconda主目录下的相关安装行为模块完成具体的安装操作。
- textw子目录和iw子目录含义是一致的,只是包含的是字符安装模式的前端字符用户界面类所在的模块,每个字符用户界面对应一个类,负责与用户的交互,字符界面的采用了python的snack库。
- 如果说用户界面类是处理安装程序外观的话,则anaconda主目录下的各python模块则执行每个安装界面背后具体的安装行为,包括那些无用户界面安装步骤的安装操作。
Python 的许多内置模块在目录/mnt/stage2/usr/lib/pythonXX下,其中XX指版本号。另外可参考redhat的anaconda源码 包,所有的相关工具都在源码包里,读者在理解了安装过程的原理后,可修改源码,重新编译,然后搭建测试环境进行测试。
2 总体分析
图形安装界面采用python + Gtk开发,各个模块(类)之间的逻辑关系可用下图表示:
图1 图形安装模式下模块(类)逻辑关系图
安装程序逻辑上可划分为三个层次,我们先看最主要的一层:调度中心。可以说,Dispatcher类和InstallControlWindow类控制了整个安装程序的运转,理解这两个核心类的功能及其与用户界面类和安装行为类的逻辑关系,就基本上理解了整个安装程序。
Dispatcher 类在anaconda主目录下的dispatch.py模块中,负责整个安装流程的控制,在安装过程中,某些安装步骤有的需要前置安装操作,有的又需要后 置安装操作,而某些安装操作则是没有用户界面的,我们统称这些安装操作为无用户界面的安装操作,那么,这些没有用户界面的安装操作由谁来调度运行呢?答案 就是Dispatcher。
InstallControlWindow 类在 anaconda 主目录下的 gui.py 模块中,控制安装过程中前端图形界面的显示,总体调度各个安装图形界面类,InstallControlWindow 建立图形界面的主窗体,每个具体的图形安装界面可视为其子窗体。InstallControlWindow 调用 Dispatcher 控制整个安装流程。
安装过程中,每个具体的图形安装界面均对应一个具体的类,由其对应的具体的类完成具体的安装任务,我们将这些图形界面类归为前端显示层,位于iw目录下的模块中。
anaconda将某些用户界面类中的具体安装操作分离出来,放到了另外一些模块中,这些模块放在了anaconda的主目录下。由Dispatcher直接调用的没有用户界面的安装步骤对应的函数也放在了anaconda的主目录下,我们将这些模块归为安装行为层。
图中虚线表达的含义是"可能调用"。如在安装过程中,负责前端显示的package_gui.py模块可能要调用负责安装行为的packages.py模块或其它负责安装行为模块中的类或函数。
2.1 主要控制类分析
2.1.1 Dispatcher
该 类在anaconda主目录下的dispatch.py模块中。无论是图形模式安装还是字符模式安装,都由该类来控制安装流程,所以,分析该类时,我们将 图形用户界面和字符用户界面统称为用户界面,涉及到字符安装模式的叙述,读者暂时可略过,读到字符安装模式分析时,再回头来看。dispatch.py模 块中有一个序列(sequence)数据结构:installSteps。installSteps中记录了有序排列的整个安装过程中所有可能的安装步 骤,在生成具体的Dispatcher实例时,会根据安装类型制定对此进行相应裁减。
installSteps中的条目(item)有如下两种格式,第一种格式:( name, tuple)
这种格式表示有用户界面的安装步骤。其中,name代表安装步骤的名称,tuple(元组,python的一种内置数据类型)存放创建相应安装步骤的用户界面的参数。
第二种格式:( name, Function, tuple)
这种格式表示没有用户界面的安装步骤,其中,name代表安装步骤的名称,Function指安装操作的具体执行函数,tuple存放的是传递给Function的参数。该安装步骤直接由Dispatcher调度进行。
下面我们结合具体的程序具体地解释一下上面的叙述,加深我们对安装流程的理解。
首先看下面从序列installSteps中截取的一个程序片断:
installSteps = [
……
("partitionmethod", ("id.partitions", "id.instClass")),
("partitionobjinit", partitionObjectsInitialize, ("id.diskset","id.partitions","dir", "intf")),
("partitionmethodsetup", partitionMethodSetup, ("id.partitions","dispatch")),
("autopartition", ("id.diskset", "id.partitions", "intf", "dispatch")),
("autopartitionexecute", doAutoPartition, ("dir", "id.diskset","id.partitions", "intf",
"id.instClass", "dispatch")),
("fdisk", ("id.diskset", "id.partitions", "intf")),
……
]
|
我们假设当前安装步骤为 partitionmethod,根据其条目的数据格式可以看出该安装步骤具有用户界面,根据安装步骤名称,如果熟悉linux安装过程,读者应该可以猜 出这个安装步骤是分区方式选择,对应该安装步骤的用户界面的类是PartitionMethodWindow,该类属于前端显示层,在iw目录下 partitionmethod_gui.py模块中。我们看一下该类的部分代码:
class PartitionMethodWindow(InstallWindow):
def getNext(self):
if self.useAuto.get_active():
self.partitions.useAutopartitioning = 1
else:
self.partitions.useAutopartitioning = 0
……
|
我们可以想象当用户在用户界面上点击"下 一步"按钮时getNext函数被调用,分析该函数,我们可以看出,当用户选择自动分区后,该函数设置 partitions.useAutopartitioning = 1,如果选择手动分区,则设置partitions.useAutopartitioning = 0。
本步骤完 成,安装过程进行下一步,Dispatcher首先在installSteps中取下一个条目"partitionobjinit",该条目中含有 function,说明此安装步骤没有用户界面,Dispatcher直接调用函数(Function) partitionObjectsInitialize,从字面上我们就可以看出,该函数是对分区操作进行相关初始化方面的操作,执行完该安装步骤后, Dispatcher继续调用下一个安装操作"partitionmethodsetup",该安装步骤也没有用户界面,Dispatcher直接调用函 数partitionMethodSetup,函数partitionMethodSetup和partitionObjectsInitialize都 属于安装行为层,在anaconda主目录下的partitioning.py模块中。我们看一下函数partitionMethodSetup的部分代 码:
def partitionMethodSetup(partitions, dispatch):
dispatch.skipStep("autopartition", skip = not partitions.useAutopartitioning)
dispatch.skipStep("autopartitionexecute", skip = not partitions.useAutopartitioning)
……
|
我们看出,如果用户选择手动分区,即 partitions.useAutopartitioning = 0,则not partitions.useAutopartitioning) = 1,即skip = 1,那么安装流程控制实例Dispatcher就会略过(skipsteip)安装步骤"autopartition"和 "autopartitionexecute"继续下一个安装步骤,下一个安装步骤有用户界面,对于图形安装模式,Dispatcher将控制权交给 InstallControlWindow,对于字符安装模式,Dispatcher将控制权交给InstallInterface。
我们再分析另一个从installSteps中截取的片断:
installSteps = [
……
("bootloaderadvanced", ("dispatch", "id.bootloader", "id.fsset", "id.diskset")),
("networkdevicecheck", networkDeviceCheck, ("id.network", "dispatch")),
("network", ("id.network", "dir", "intf")),
……
]
|
我们假设当前安装步骤为 "bootloaderadvanced",其后的另一个具有用户界面的安装步骤为网络配置"network",而进行网络配置的前置条件就是安装的主机 要具有网络设备,因此,系统安装程序在"network"之前插入了步骤"networkdevicecheck"。假设安装过程从当前安装步骤 "bootloaderadvanced"进行到下一个安装步骤"networkdevicecheck",该安装步骤没有用户界面, Dispatcher直接调用函数networkDeviceCheck,我们看一下该函数的代码(network.py):
def networkDeviceCheck(network, dispatch):
devs = network.available()
if not devs:
dispatch.skipStep("network")
|
可以看到,该函数首先检测是否存在网络设备,如果没有网络设备,Dispatcher则忽略安装步骤"network"。这里,可以说函数networkDeviceCheck是有用户界面的安装步骤"network"的前置操作。
通过上面的分析,我想我们不难理解为什么Dispatcher被称为"主要安装流程控制类"了。其它的安装步骤控制基本相同,我们不一一分析了。
下面我们看一下Dispatcher的主要接口。
gotoNext & gotoPrev:这两个接口分别从当前安装步骤前进(后退)到下一个(上一个)具有用户界面的安装步骤,在图形界面安装模式下,由 installcontrolwindow调用,在字符模式下,由InstallInterface调用。这两个函数只是简单的设置安装方向,然后调用 movestep函数,其核心操作是movestep。我们来重点分析movestep函数:
def moveStep(self):
……
if self.step == None:
self.step = self.firstStep
else:
self.step = self.step + self.dir
while ((self.step >= self.firstStep and self.step < len(installSteps))
and (self.skipSteps.has_key(installSteps[self.step][0])
or (type(installSteps[self.step][1]) == FunctionType))):
info = installSteps[self.step]
if ((type(info[1]) == FunctionType) and (not self.skipSteps.has_key(info[0]))):
(func, args) = info[1:]
rc = apply(func, self.bindArgs(args))
if rc == DISPATCH_BACK:
self.dir = -1
elif rc == DISPATCH_FORWARD:
self.dir = 1
self.step = self.step + self.dir
if self.step == len(installSteps):
return None
|
我 们重点看一下程序while循环体,首先看一下循环条件:当下一个安装步骤是合法的,即在第一个安装步骤和最后一个安装步骤之间,并且(and)该步骤被 裁减了或者该步骤是一个无用户界面的安装步骤,即installSteps的条目的第二个元素是一个function,则进入循环体。进入循环后, Dispatcher直接调用该函数执行安装操作,其中bindArgs是Dispatcher类的一个函数,负责参数解析,这里的apply是 python的的一个内置方法,用来执行函数,apply接口的第一个参数是要运行的函数名称,第二个参数是传给该函数的参数。如果下一个安装步骤依然无 用户界面,则继续循环,直到下一个没有被裁减的具有用户界面的安装步骤,对于图形安装模式,Dispatcher将控制权交给 InstallControlWindow,对于字符安装模式,Dispatcher将控制权交给InstallInterface。如果安装过程完成则 退出循环。
currentStep:Dispatcher类的另一个主要接口,取得当前的安装步骤及其相关信息返回 给调用者。在图形安装模式下,该函数主要由InstallControlWindow调度图形用户界面类时调用,在字符模式下,主要由 InstallInterface调度字符用户界面时调用,这两个类通过该接口取得当前安装步骤的用户界面对应的类及创建该用户界面类的实例所需的信息。
另 外,Dispatcher类的主要接口还有skipStep(self, stepToSkip, skip = 1, permanent = 0)是裁减安装步骤函数。setStepList(self, *steps)是安装步骤设置函数,主要由安装类型实例调用,每个安装类型会根据自身的特点设置安装步骤。这些接口的实现逻辑都比较简单,这里不一一给出 分析了。
至此,Dispatcher的主体基本分析完了,看完下面InstallControlWindow以及后面字符安装模式的InstallInterface类的分析,我们对Dispatcher将会有更好的理解。
3 InstallControlWindow
该类在anaconda主目录下的gui.py模块中。首先我们先看一下启动图形安装界面的入口函数run:
def run (self, runres, configFileData):
self.configFileData = configFileData
self.setup_window(runres)
gtk.main()
|
该函数调用了setup_window接口,该接口调用gtk"绘制"图形安装界面的主窗体,然hou控制权交给了gtk。
nextClicked & prevClicked:这两个接口分别执行从当前图形安装界面向前(向后)到下一个图形安装界面的操作,我们可以想象安装过程中当用户点击"下一步"或 "上一步"按钮时,这两个函数被调用。这两个函数首先调用主流程控制Dispatcher实例向前(向后)前进到下一个图形安装界面,然后调用 setScreen函数,从函数名称的字面上看,setScreen是设置图形界面的,那么接下来我们看一看该函数的具体功能,在分析其之前,我们要首先 看一下gui.py模块中的一个字典数据结构:stepToClass。该字典中记录了安装过程中所有的具有图形用户界面的安装步骤。
stepToClass = {
"language" : ("language_gui", "LanguageWindow"),
"keyboard" : ("keyboard_gui", "KeyboardWindow"),
……
}
|
每一个条目从左到右依次是安装步骤名称、 图形界面对应的类所在的模块,图形界面类的名称。如language为安装步骤名称,language_gui为该步骤对应的图形界面类所在的模块 language_gui.py,LanguageWindow为图形界面对应的类名。理解了该数据结构后,我们开始分析setScreen函数:
def setScreen (self):
(step, args) = self.dispatch.currentStep()
if not stepToClass[step]:
if self.dir == 1:
return self.nextClicked()
else:
return self.prevClicked()
(file, className) = stepToClass[step]
newScreenClass = None
s = "from %s import %s; newScreenClass = %s" % (file, className, className)
while 1:
exec s
break
self.destroyCurrentWindow()
self.currentWindow = newScreenClass(ics)
new_screen = apply(self.currentWindow.getScreen, args)
self.installFrame.add(new_screen)
self.installFrame.show_all()
……
|
前面的nextClicked和 prevClicked函数已经通过Dispatcher将要进行的安装步骤标记为当前安装步骤,所以该函数首先通过Dispatcher的 currentStep从Dispatcher的数据结构installSteps中取得当前安装步骤名称及相关信息,接下来,做了一下判断,如果 Dispatcher的当前安装步骤不在字典stepToClass中,则忽略该步骤,调用nextClicked或prevClicked继续下一个图 形界面安装步骤,直到下一个步骤在字典stepToClass中。验证通过后,从字典stepToClass中取得当前图形安装界面对应的类及该类所在模 块,然后导入该模块并创建图形安装界面的实例,销毁前一个图形安装界面,并将新创建的图形界面实例置为当前安装界面,调用图形安装界面实例的 getScreen函数生成该安装步骤的图形用户界面,然后显示。
至此,InstallControlWindow的主要逻辑已经分析完了,接下来涉及每个具体安装界面及其安装操作读者可以到iw目录下逐个深入分析。
4 主执行程序anaconda分析
前 面我们比较详细的分析了图形模式和字符模式的安装过程,对于图形安装过程,图形环境运行是建立在X Server基础上的,因此在进行图形安装时,必须运行X。因此,简单的说,对于图形模式,anaconda的主执行体就作了两件事:1、运行X服务器 (如果X没有运行)。2、启动图形模式安装过程。而对于字符模式,anaconda的主执行体就作了一件事,启动字符模式安装过程。
4.1 启动X server
如 果是图形模式安装,则anaconda首先要启动X server。anaconda首先检测图形正常显示所需的硬件资源。Python提供了许多内置模块实现相关硬件的操作。这些模块在目录 pythonXX/rhpl下(XX表示版本号,在笔者的机器上路径是/mnt/stage2/usr/lib/python2.2):
Videocard.py、 Monitor.py、Mouse.py、keyboard.py分别是显示卡、监视器、鼠标和键盘的相关操作和配置数据。xserver.py - 图形模式下X server启动初始化方面的工作,包括相关硬件的探测和X server的启动,xserver为videcard、monitor和mouse的探测提供了统一个接口,实际调用的是相应硬件模块的接口。
下边的程序片段探测显示卡、监视器和鼠标、键盘,并且利用xhwstate记录该配置状态,供后续安装过程使用。
import rhpl.xserver as xserver
(videohw, monitorhw, mousehw) = xserver.probeHW(skipDDCProbe=skipddcprobe,
skipMouseProbe = skipmouseprobe)
kbd = keyboard.Keyboard()
if keymap:
kbd.set(keymap)
xcfg = xhwstate.XF86HardwareState(defcard=videohw, defmon=monitorhw)
|
硬件检测完成后,启动X server的条件已就绪,如果X server没有启动,并且安装模式为图形模式,则启动X server:
xsetup_failed = xserver.startXServer(videohw, monitorhw, mousehw, kbd,
runres,
xStartedCB=doStartupX11Actions,
xQuitCB=doShutdownX11Actions,
logfile=xlogfile)
|
该调用除了传递相关硬件的配置信息外,还传递了两个函数。
doStartupX11Actions函数在X server启动时调用,该函数主要是启动窗口管理器,设置相关资源。
doShutdownX11Actions函数在X server退出时调用,关闭窗口管理器。
4.2 启动安装过程
我 们前面分析安装过程原理时指出:安装的流程由Dispatcher控制,对于图形模式,图形模式的前端显示及与用户的交互由 InstallControlWindow调度,而字符模式的前端显示层由InstallInterface调度。因此,启动安装过程,实际就是创建主要 控制类的实例,调用实例的接口,启动安装过程,然后再由这几个主要的控制类的实例创建具体安装界面,创建安装行为类的实例,调用具体的函数完成具体的安装 过程。
- 创建实例
1. 创建dispatch实例
dispatch = dispatch.Dispatcher(intf, id, methodobj, rootPath)
2. 创建InstallInterface实例
对 于字符模式,前端字符用户界面由InstallInterface调度。而对于图形模式,我们在前面分析时,没有提到InstallInterface 类,其实在图形模式下,也有这么一个同名类,在模块gui.py中,但在图形安装模式下,这个类的作用很小,除了封装了创建常用对话框(包括 messageWindow,progressWindow,exceptionWindow等)的接口外,一个主要的作用就是为创建 installcontrolwindow实例提供了一个接口,为什么不在anaconda主执行程序中直接创建 installcontrolwindow实例呢?个人认为可能是为了保证代码的一致性。
if (display_mode == 'g'):
from gui import InstallInterface
if (display_mode == 't'):
from text import InstallInterface
if display_mode == "t":
intf = InstallInterface ()
else:
intf = InstallInterface ()
|
3. 创建installcontrolwindow实例
installcontrolwindow实例在gui.py模块中的InstallInterface类的接口run中创建,该接口创建了InstallControlWindow实例后运行InstallControlWindow实例的run接口启动图形安装界面:
def run(self, id, dispatch, configFileData):
……
self.icw = InstallControlWindow (self, self.dispatch, lang)
self.icw.run (self.runres, configFileData)
|
-
启动安装过程
安 装控制类的实例创建完成后,调用其相应接口启动安装过程。对于字符安装模式,则直接调用InstallInterface实例的run接口。而对于图形安 装模式,则是由InstallInterface实例的run接口间接的调用installcontrolwindow实例的run接口。
intf.run(id, dispatch, configFileData)
|
另外,在启动安装过程前,anacoanda的主执行程序作了大量的繁琐的准备工作,主要包括引用模块路径设置、参数解析、内存检测、安装类型设置、创建安装介质实例等等。
anaconda安装程序较大,限于篇幅,文中分析不可能面面俱到,读者可以在理解程序运行的主要逻辑的基础上,参照原代码进一步分析。限于作者水平,分析中难免存在偏颇,恳请读者多多指正,欢迎来信讨论。