第一章 介绍
第一节 总体架构
当UNIX系统问世的时候,操作系统的内部结构就发生了巨大的变化。在那个时候,以高级编程语言C来编写内核程序是非常革命性的。现在的趋势是向微内核架构发展,就像Windows NT内核一样。现在的实际内核仅仅提供必须的基本功能(进程间通信和内存管理)可以以此实现成微小而且紧凑的形式。在此微内核基础之上,操作系统的其余功能就重新建于主程序中,与微内核通过一个设计良好的界面。这个建构的主要优点(除了外观好看)就是系统结构比较易于维护。独立的原件负责各自的工作,不会彼此影响,并且比较容易相互代替。新原件的发展已经被简化。
这本身成为了这种架构的缺点。微内核架构迫使界面的维护在独立原件之间,防止复杂的最优化架构。就是说,如今的硬件架构,进程间通信需要微内核,比简单的调用功能更加广泛。这个小的速度劣势很快被大家接受了,因为当前的UNIX硬件速度已经够快,因为较为简单的系统维护减少了发展的开支。
微型内核构架无疑代表了未来操作系统的发展。LINUX在另一个层面,在“慢”386架构这种UNIX系统运行最低极限的这种速度比较慢的处理器中出现了。它在探索所有优化的可能,最主要的考虑就是给程序提供良好的运行时间。这就是为什么LINUX在传统的独立内核架构中应用。另一个理由无疑就是微型内核构架基于精良系统设计这个事实。当LINUX借着革新成长起来的时候,从发展一个系统的方面来讲,LINUX显然不是一个娱乐性的系统。
尽管它是基于独立内核的,LINUX也并不是一个混乱的程序代码的集合。
很多内核的原件仅仅可以通过精确界面而进入。一个很好的例子就是虚拟文件系统-Virtual File System--(VFS),它代表了一个可到达所有源文件的抽象的界面的操作。但是这些混乱是在细节当中体现的。在临界时间点,程序的段落经常以“优化手指”的C语言编写,让它们跟踪起来比较复杂。幸运的是,这些程序段落非常少见,对一个标准来说,解释的非常好。
进程和任务 在linux底下运行的进程来说,内核就是服务的提供者。每个进程都是独立存在的,不可能和别的进程一同存在,所以不可能直接影响到彼此的运行。每一个进程自己的内存区域都被保护,免得其它进程对其进行修改。
运行Linux系统的内部观点则是一个不同的问题。只有一个程序-操作系统-在计算机上运行,可以访问到所有的资源。多种的任务以共同路径执行-就是说,每一个任务都由自己决定是否、何时需要把控制权限传递给另一个任务。一个结果就是内核程序中的一个错误可以停止整个的操作系统。任何一个任务都可以从一个任务连接所有资源并且编辑它们。
一些任务的某部分在进程中以较低的用户权限的模式运行。这些任务部分从外部显示(有些人认为该从内核内部看)为进程。在这些任务的处理来看,这才是真正的多任务处理模式。
在下几页中,然而,我们不会精确区别任务在系统中的运行模式的定义,这可能要用很久。我们将在第五章中对此作出详细的讨论。
第二节 源码架构
在源码树的最高层/usr/src/linux你就会看到很多目录:
Arch分档文件:Arch这个分支目录包括所有内核建构代码。每一个被支持的架构都有一个子目录,比如说i386和Alpha.
include载入:include这个支目录包括很多需要建立在内核代码上的载入文件。它也有许多的分支目录,每个支持的架构都有一个include支目录。include/asm目录是个链接到架构需要的include目录的软链接,比如include/asm-i386.改变建构你需要编辑内核makefile,并且重新启动Linux内核配置程序。
init启动:这个目录包括了内核启动代码,是一个很好的起点,看清楚内核是如何工作的。
mm内存管理:这个目录包括了所有的内存管理代码。架构特殊内存管理代码放在arch/*/mm目录中,比如arch/i386/mm/fault.c.
drivers驱动:所有的系统设备驱动在这个目录中。它们分配在不同的设备驱动目录中,比如说block.
ipc进程间通信:这个目录包括内核的进程间通信代码。
modules模块:这是一个简单的包括建立好的模块的目录。
fs文件系统:所有的文件系统代码。还有分支结构,每一个被支持的文件系统都有一个支目录,比如vfat和ext2.
Kernel内核:主要的内核代码。再说一次,内核的构架代码在arch/*/kernel目录下。
net网络:内核的网络代码。
lib库:这个目录包括了内核的库的代码。库的构架代码在arch/*/lib/中。
scripts脚本:这个目录包括了脚本(比如说awk和tk脚本)在配置内核的时候会用到。
第三节 配置和编译
细节步骤:注:在'bash[num ]'中表示了bash提示符,你要在'bash[num ]'提示符后面敲入命令。以下的例子是在ThizLinux Kernel 2.4.20-3上面测试的,但是如果是在其它版本上应用,可以做微小的更改。还可以在更原始的内核版本2.2,2.0和1.3上使用。还可以在未来或者更新一些的内核版本上面使用。
注:你可以在系统上有很多内核图像。跟随以下的步骤你就会保存你内核的安全,不至于写的太多或者伤害到你已有的内核。这些步骤是安全的,你目前的内核就会保证原封不动。
1. 源码解包:以root身份登录。mount ThizLinux cdrom并且安装linux kernel source rpm
bash$ su – root
bash$ mount /mnt/cdrom
bash$ cd /mnt/cdrom/ThizLinux/RPMS
bash$ rpm -i kernel-headers*.rpm
bash$ rpm -i kernel-source*.rpm
bash$ cd /usr/src
bash$ ls -l
# You should see that /usr/src/linux is soft link pointing to source total 8
lrwxrwxrwx 1 root root 18 Jul 2 20:29 linux -> linux-2.4.20-3Thiz
lrwxrwxrwx 1 root root 18 Jul 2 20:31 linux-2.4 -> linux-2.4.20-3Thiz
drwxr-xr-x 16 root root 4096 Jul 2 20:30 linux-2.4.20-3Thiz
drwxr-xr-x 7 root root 4096 Jul 2 20:38 thiz
2. 可选项 - Copy config file: 如果你要挽救一些原始的设置的话,这个步骤可以帮你省时间。不论何时你安装内核,你都把配置文件放在/boot中。所以,你可以用存在的配置文件版本。
bash# mv /usr/src/linux/ .config /usr/src/linux/ .config.save
bash# cp /boot/config-2.4.18-19.8.0 /usr/src/linux/ .config
另一个方法就是——你可以从你旧版的Linux 内核源码树上复制.config文件到新的内核树上。
bash# ls -l /usr/src/lin*
# You can see that /usr/src/linux is a soft link
bash# cd /usr/src/linux
bash# cp ../linux-old-tree/.config .
# Example cp ../linux-2.4.19/ .config
或者另一个方法——你可以用“make oldconfig”来初始化你在./.config文档中所有的内容。
3. 清洁: 在作以下的mrproper之前,你最好先备份.config文件。
bash# cd /usr/src/linux
bash# cp .config. config.save
bash# make clean
bash# make mrproper
# Must do this if want to start clean slate or
# if you face lot of problems
4. 配置
如果你用的是ThizLinux X-window模式
bash# cd /usr/src/linux
bash@ make xconfig
如果你不是在X-window环境下就用
bash# cd /usr/src/linux
bash# make menuconfig
如果menuconfig不成功就试试
bash# cd /usr/src/linux
bash# make config
Make xconfig或者make menuconfig就可以给你一个用户友好的图形化界面。Make config给你命令行界面。你可以从/usr/src/linux/ .config加载配置文档(在Config这个词前有一个点要注意)。点击“Load Configuration from File”.
在“make xconfig”中你要做以下操作以避免产生问题
*非常重要!!!:选择适当的CPU种类-奔3,AMD K6, Cyrix, 奔4,Intel386, DEC Alpha, PowerPC否则内核编译就会失败,即使编译成功也不能够启动。
*选择SMP支持-不管是单个CPU或是多CPU.
*文档系统-选择Windows95 Vfat, MSDOS, NTFS为内核的一部分并不像模块一样容易加载。
*激活可以加载的内核模块支持。用这个操作你可以加载/卸载系统设备驱动程序。
保存和退出。所有你选择的选项已经在配置文档中储存为/usr/src/linux/ .config了。
5. 从属:
bash# make dep
源码的从属被重新算过。它使用了GNU C编译程序的能力创建了文卷编译的从属文卷。这些从属文卷被放在一个叫做.depend的独立文档目录里面,随后再植入文卷编写中。
6. 执行:
bash# make bzImage
这是一个长过程的工作,要用10-20分钟来完成。它要把内核放在/usr/src/linux/arch/i386/boot/bzImage这个目录下。
7. 可加载模块
有很多设备的驱动和文件系统都没有链接入内核才可以创建成为模块的。可以这样做
bash# cd /usr/src/linux
bash# make modules
#Do this, only after the above make command is successful
bash# make modules_install
这个操作可以把模块都拷贝到/lib/modules目录下。
8. bzImage:
在bzImage成功以后,复制内核图像到/boot目录。你一定要复制新的内核图像到/boot目录,否则新的内核就不会被启动。你还要把配置文档拷贝到/boot区域显示新的内核图像,为了文档的需求。
bash# cd /usr/src/linux
bash# cp arch/i386/boot/bzImage /boot/bzImage.07jul2003
#You should copy the config file to reflect the
#corresponding kernel image for documentation purpose
bash# cp .config /boot/config-<your_kernelversion_date>
# Example: cp .config /boot/config-2.4.20-3_07jul2003
9. initrd:
在编译了内核图像和模块以后,现在就是用Initrd文件的时候了。initrd的意思就是Initial RAM disk. 当在root权限中安装非双扩展名文件时需要使用initrd, 或者必需的设备驱动链接root文件系统设备被编译成内核模块(比如SCSI硬盘的 scsi卡驱动)
bash# mkinitrd /boot/initrd-2.4.20-3_07jul2003 2.4.20-3_07jul2003
以免你没有改变文卷编写的版本号码,以上的命令可能就不适用,你就要用这条命令
bash# mkinitrd /boot/initrd-2.4.20-3_07jul2003 2.4.20-3Thiz
10. 配置GRUB
ThizLinux使用GRUB作为默认的启动加载器。你要把它加在新的内核中。
bash# grubby –add-kernel /boot/bzImage.07jul2003 \
--title=ThizLinux kernel 07jul2003 \
--initrd=/boot/initrd-2.4.20-3_07jul2003
然后你就可以重启计算机。一个新的选项会在启动菜单中出现,你要从新的内核启动。
11. Clean:
可选的-清理(如果你要更多的磁盘空间)。
第四节 主要算法
1. 信号
信号是UNIX系统中最早的进程间通信的一种方法。它们在异步程序的一个或者更多的步骤间发送信号。信号可以是由键盘指令发送的,或者是当程序导入到虚拟内存中一个不存在的地址而产生的错误状态。信号也是Shell用来用命令控制的子程序的工作的。
有很多信号是内核可以带动的,或者系统中其它程序可以带动的有一定定义的信号,它们在系统中有一定的优势。你可以列出一个kill命令的信号表格,在我的Intel ThizLinux中是以下这些:
1)SIGHUP 2)SIGINT 3)SIGQUIT 4)SIGILL
5)SIGTRAP 6)SIGABRT 7)SIGBUS 8)SIGFPE
9)SIGKILL 10)SIGUSR1 11)SIGSEGV 12)SIGUSR2
13)SIGPIPE 14)SIGALRM 15)SIGTERM 16)SIGCHLD
17)SIGCONT 18)SIGSTOP 19)SIGTSTP 20)SIGTTIN
21)SIGTTOU 22)SIGURG 23)SIGXCPU 24)SIGXFSZ
25)SIGVTALRM 26)SIGPROF 27)SIGWINCH 28)SIGIO
29)SIGPWR 30)SIGSYS
这些数字和Alpha AXP Linux box中不一样。进程已经可以选择忽略生成的大量信号,有两个明显的特例:不是登录信号 SIGSTOP信号可以阻止一个执行进程,也不是SIGKILL信号可以忽略正在退出的进程。否则,一个程序可以选择它如何处理不同的信号。进程可以拦截信号,如果它们不拦截信号,它们就只能选择自己搞定或者让内核来处理它们了。如果是内核处理它们,就要作出信号要求的默认操作。比如说,当一个进程收到 SIGFPE(浮点除外)这个信号的时候,默认的操作就是核心转储并且退出。信号没有固有的相关优势。如果两个信号同时因为一个进程而产生,它们就要被进程或者其它的某个命令来处理。并没有处理同类多信号的机械手段。一个进程也不能够搞清楚它接受的1个或者42个SIGCONT信号。
Linux用储存在给进程的task_struct中的信息执行信号。支持信号的数量局限于处理器中字的大小。操作一个32字节大小的进程要用32个信号,像Alpha AXP中那种64个字节的信号就要用有64字节的处理器处理。目前待用的信号放在信号文件夹中,被拦截的信号放在一个叫被拦截的文件夹中。除了那种SIGSTOP和SIGKILL的信号,其它所有信号都可以被拦截,如果生成了一个被拦截了的信号,它就一直要保持待用状态直到接触拦截。Linux也有一些关于每个进程如何处理每个可能的信号,这些都放在sigaction数据结构阵列中,被每一个进程的task_struct指向。在别的当中,它包括了或是处理信号的例程地址,又或是一个告诉Linux应该让内核或者进程来处理这个信号的一个标注。进程编辑默认的信号,用作处理系统调用,这些调用改变sigaction使信号像被拦截的屏蔽。
不是系统中的每一个进程都可以给每一个其它进程发送信号,只有内核或者超级用户可以。一般的进程仅仅可以发送信号给相同uid和gid或者在相同的进程组中操作。信号因为得到适合的task_struct's 而产生。如果进程没有阻拦信号,仅仅是等待但是可以随时中断,那么它就要运行并且确定它确实在运行队列中。这样调度程序就可以考虑在系统下一次调度时运行它。如果需要默认的处理,Linux就可以最优化信号的处理。比如说信号 SIGWINCH(X Window改变了目标)就使用默认处理,那么也就是什么都没有完成。
信号不是在生成进程时马上就出现了的,它们要等到进程再次运行才能生成。每一次一个进程在系统调用信号并且拦截文档结束的时候,如果有被拦截的信号,它们就在这个时候传输。这个方法也许非常不可靠,但是每个进程在系统中都在执行系统调度,比如任何时间在终端中书写一个字。进程可以等待信号来临,它们一直都在等待信号的来临,从不偷懒。Linux信号进程代码看着sigaction结构,看看随时有没有没有被拦截的信号出现。
如果一个信号处理器在默认操作模式,内核就会处理信号。SIGSTOP信号的默认处理器将改变当前的处理器,停止并且运行调度程序选择一个新的进程来运行。SIGFPE默认的操作信号会让核心的进程退出处理器。最终,进程就会有它自己的信号处理器。这个例程中,会生成信号,SIGACTION结构也会有这个例程的地址。内核必须调度处理器的信号处理例程,处理器CPU一定要和当前在内核中运行的进程匹配,要回到叫做在用户态的内核或者系统例程的处理器中。这个问题的解决是靠操作堆栈并且登录进程。处理器程序计算器已经被设置好了地址,如果信号处理器和例程参数加入调度模式或者在登录的过程中通过。当进程在继续执行操作的时候,显示的是信号的例程是否处理正常。
Linux是支持POSIX-可移植性操作系统接口-的,进程可以在一个特殊信号处理例程被调度的时候分出哪个信号是被阻拦的。这就意味着在进程信号的处理过程中改变阻拦的屏蔽。在信号处理例程结束的时候,被阻拦了的屏蔽必须返回原始的值。这样Linux在一个储存在原始被阻拦屏蔽中整齐的例程中加了一个呼叫在有信号的进程。Linux还最优化一些信号处理的例程的堆栈,所以每次一个例程开始的时候,下一个也在一个例程被整理好的时候被呼叫。
2. 中断
中断是承认硬件和操作系统通信的动作。在这里我们要讨论执行中断的管理原则。相关的编码在arch/i386/kernel/irq.c和include/asm/irq.h.目录中。
Linux当中有两种类型的中断方式:快速的和慢速的。我们也可以说有三种,第三种就是代表系统呼叫的中断,就是通过中断这种模式切断。然而,这一章节只会讲述关于硬件的中断。
慢速中断:慢速中断是一种普通的模式。其它的中断是在处理过程中合法的。在慢速中断执行之后,其它附加的行为就会被系统调出。一个比较典型的慢速中断就是计时器的中断。中断的处理包括以下行为。
PSEUDO_CODE IRQ(intr_num, intr_controller, intr_mask)
{
首先,所有的登录都保存在SAVE_ALL,中断的记录也在ACK的确认下完成。同时,更多的中断确认就被阻拦了。
SAVE_ALL; /* macro in include/asm/irq.h */
ENTER_KERNEL /*macro in include/asm/irq.h SMP lock */
ACK(intr_controller, intr_mask);
在多进程系统中,对ENTER_KERNEL的系统调用例程就用在同步进程和内核链接中使用。
中断嵌套的深度在intr_count中体现,在更多的中断被启用之后,中断的例程就被调用。提供了一系列的中断进程登录的复件。一些中断的处理器会应用这些登录的记录(比如,计时器的中断)来确定是否中断这个动作中断了用户进程或者内核。
++intr_count;
sti();
do_IRQ(intr_num, Register)
当成功执行中断例程的时候,中断控制台就会发出中断被接收的信号。就是说,中断计数器已被降低。
cli();
UNBLK(intr_controller, intr_mask)
--intr_count;
这样就进入了汇编程序ret_from_sys_call()的例程。这个在慢速中断或者系统呼叫之后提供了更多的一般性管理附加。这个是不可逆转的功能。它重置了以SAV_ALL保存的登录,并且执行以iret结尾的中断例程。
ret_from_sys_call();
} /*PSEUDO_CODE IRQ */
快速中断:快速中断用于简短、相对简单的任务。当执行它们的时候,所有其它的中断被拦截,除非处理例程包括明确的激活它们。一个典型的例子就是键盘中断(driver/char/keyboard.c).
PSEUDO_CODE fast_IRQ(intr_num, intr_controller, intr_mask)
{
首先,向之前一样,保存登录-但是仅保存那些以一般C功能修改的登录。这就是说,如果汇编代码用作处理例程,剩余的登录就必须在之前保存并在之后重置。
SAVE_MOST; /*macro in include/asm/irq.h*/
中断控制台还通知intr_count的增量像慢速中断一样。这个时候,在中断控制器被呼叫之前不会有其它的中断命令被接收。
ENTER_KERNEL; /*macro in include/asm/irq.h*/
ACK(intr_controller, intr_mask);
++intr_count;
do_fast_IRQ(intr_num)
UNBLK(intr_controller, intr_mask)
--intr_count;
LEAVE_KERNEL
这个动作完成了中断的处理。RESTORE_MOST返回了以保存的登录,返回到它们以前的值并被称作iret继续中断进程。
RESTORE_MOST; /* macro in include/asm/irq.h */
} /* PSEUDO_CODE fast_IRQ */
3.计时器中断
一个操作系统有时候需要为未来的任务安排程序表。因此,就需要在一些相对精确的时间表内安排要处理的任务。任何一个微型处理器都希望支持一个可编程的间隔器来随时中断处理器的进程。这个阶段性的中断就是大家知道的像一个走动的系统时钟一样,和系统任务一唱一合。
Linux有一个非常简单的时间观点,当系统启动的时候,它就开始对时间了。所有系统时间都基于这个标准之上,就是我们全世界人所在说的Jiffies。
Linux有两种系统时钟,它们都在一个系统时间内,但是在实现上有一点不同。图1.1中你可以看到两种时钟的机械模式。
首先,旧的计时器机械,有一个32点的数据阵列到timer_struct数据结构,有激活的计算器的掩码,timer_active.
当计时器达到了计时器的数据表清晰的地点。登录条目就像系统时钟一样加入了这个表格。然后,更新的机械用一个计时器数据结构链接表保留升序的时钟顺序。
两种使用Jiffies计时器登出时钟的方法,5秒钟转换5秒的Jiffies并且把它们加入当前的系统时钟中,所以当过期的时候能够使用系统时钟。每个系统时钟在下一个系统日程运行的时候都会激活系统处理器的底端,计时器阵列就这样被处理。计时器底部处理两种系统计时器的进程。老的系统计时器要在设置的时候检查timer_active。
如果一个计时器到期了(到期时间比Jiffies系统时间要少),计时器的例程被呼叫,当前的点位被清除。新的系统计时器,可以在timer_list数据结构表中找到链接。
每一个过期的计时器都会被从列表中删除,例程也被呼叫。新的计时器的机械有把参数传输到计时器例程的优点。
第五节 练习
Linux安装和内核重新编译
按照安装手册说明把thizLinux7安装到计算机硬盘。使用内核2.4.20重新编译自带的内核源码包。实验架构内核不同的几种配置,试着架构一个迷你内核,把你在系统使用中不需要的选项删除掉。
第二章 计算机架构和操作系统
操作系统是用户和计算机之间沟通的界面。它管理硬件资源,比如CPU, 内存,硬盘等。大多出情况下,操作系统需要直接操作硬件为服务于应用程序。比如,如果一个程序需要打开一个已经储存在硬盘中的文件,这个程序并不是直接去打开这个文件,而是让操作系统去打开它并且帮助它阅读。因而,操作系统的设计和应用大多是基于计算机的架构之上的。因为操作系统需要控制并且和硬件沟通。不可能理解操作系统的细节而不知道计算机的构造。所以这一章节,我们要着重在必须了解的Linux内核,以及计算机构造、编程语言的基本背景知识上。
第一节 计算机架构
一台计算机包括了几个重要的原件:CPU,内存,输入/输出设备。CPU是用来执行程序并在内存中储存。输入/输出设备一般是输入数据和从程序中输出数据的。图2.1介绍了计算机的图。
1. CPU被认为是计算机的核心部分,它的主要作用就是执行程序。有很多不同种类CPU的架构。这里,我们仅简单的讨论一下ia32CPU,所以我们就能了解内核是如何和CPU合作来完成程序的运行的。
CPU包括一些寄存器,还有一个执行单元。在这里,我们会省略所有的硬件细节(就是说用逻辑栏操作加法器)从一个程序员的观点来看CPU,CPU的工作就是从内存中读取指令并一个一个的执行。首先读取第一个指令,解码然后执行,然后读取另一个指令,再解码执行,下一个,下一个这样继续下去。
由于硬件的设计,对于指令的读取也有一定的限制。比如很多的指令就需要一个寄存器对象。
在CPU中,我们有一些快速存储器为程序储存数据。这些存储器叫做通用寄存器。大多数的CPU操作都需要寄存器。寄存器是一个非常珍贵的存储器。比如Intel IA32 CPU, 它的通用寄存器是EAX, EBX, ECX, EDX.有一些寄存器如ESI和EDI是做地址索引的。一些寄存器是单单为某项任务工作的,比如EBP, ESP是做程序堆栈。支持因特尔的内存分段使用CS,DS,SS代码分段,数据分段和堆栈分段有自己各自的寄存器。
CPU结构中主要有两个部分:运算符和运算对象。当运算对象是数据,值或者操作的寄存器的时候,运算符主要分配操作(比如加法,减法等)。比如,如果我们想要把一个存在内存里面的文件复制到寄存器中,我们就需要使用Mov命令:
mov <memory address of variable>, register
在这种情况下,mov就是运算符,内存地址和寄存器机是这个指令的运算对象。对于因特尔IA32CPU来说,不同的运算对象需要使用不同的指令。
2.内存
内存是储存程序的数据和指令的仓库。在执行一个程序之前,这个程序必需要先存储到内存中。CPU之后再从内存中把结构一个一个调出来执行。有一些是快速寄存器,用来存储马上就要执行的指令,但是它们不能够存储太多的结果,所以程序就要把结果从寄存器放到内存中。
我们还是从一个程序员的角度来看内存,忽略硬件的细节。然而,我们必须知道我们在实用的内存叫做RAM(Random Access Memory)随机存取存储器,它的内容是比较容易丢失的。当系统断电的时候,RAM中的数据就会丢失。
从一个程序员的观点来看,内存就是一个储存数据和指令的地方。内存和CPU是相互连接的。要读写内存中的数据,我们就要知道地址。每一个内存的位置都有个对应的地址,每个比特的数据都有自己独特的地址。
3. 输入/输出设备
输入/输出设备主要就是输入和输出的工作。比如,你的键盘就是个输入设备,你的显示器就是一个输出设备。对于CPU来说有几种联系这些硬件的方法:
地址:硬件读取或者写入它在内存中数据的地址。比如说这个地址0xb000:0000代表了视频文字显示的开始。我们就可以在这个地址写入数据来显示到显示屏上面输出。
输入/输出港(I/O port):硬件可以从I/O port中读写数据。I/O port数字就是通过总线到达硬件的,如果两地的数字吻合,硬件就要对其作出读写的操作。
第二节 真实模式和保护模式
在IA-32的结构中,CPU有两种模式。当系统启动的时候,CPU从真实模式中执行指令。真实模式因为兼容性而存在。在真实模式中,地址的表示是用16比特的分段和16比特的偏移量来表示的,没有对内存的保护,所以一个程序可以占据整个的内存空间。可以写入覆盖另一个程序或者甚至写入另一个操作系统。真实模式的存在就是为了与旧版CPU的背景兼容性问题。
Linux系统把CPU在启动的时候设置为保护模式。保护模式现对于真实模式有一些优点:
保护模式启动32比特的内存地址,上至4GB的内存可以被地址化。
保护模式有保护性。一个任务不能看到内存中另一个任务的地址空间。
保护模式提供了硬件多任务的支持。
保护模式提供4个特权等级给程序。特权等级0承认程序执行所有的CPU指令,并且经常被操作系统使用。Linux内核也使用特权等级0在系统模式中(系统模式也称作内核态)。程序只被承认呼叫调用比特权等级低的,这就防止了系统程序的内在危险代码。
Linux内核用两个系统特权等级。特权等级0是给系统模式(内核态)特权等级3给用户态。所有除了内核以外的进程都在用户态中运行。
第三节 中断和异常
中断和异常是硬件和程序出现不正常事件时候的重要处理操作。不论什么时候发生中断或者异常,当前执行的程序都会被中断,CPU就会执行一个特别的路径处理中断或者异常。
中断和异常是很相似的,异常是由CPU控制单元发出的指令,中断是硬件系统发出来的。它们两个都会对当前执行的程序进行检查。我们不会分清它们两个的不同,“中断”这个词就代表了硬件的异常。
一个重要的中断就是计时器的中断,就是硬件定期切断。中断是内核定期执行特殊的例程。其中一个例程就是检查程序是否占用了太多CPU的频率,就可以强迫它就会让CPU首先执行别的程序,就像图2.2中一样。这就是如何让CPU在多任务环境中强行执行另一个程序。没有这种定期的中断,当前的程序就会消耗所有的CPU频率,其它的程序就不可能被执行了。
异常也扮演了非常重要的角色,执行系统调用和实现调度。
第四节 数据结构
我们在这里展示几种内核中常用的数据结构。
1. 链接表
第一个数据结构就是链接表。链接表是在图2.3中描述的图表。用C语言编写:
typedef struct item {
int data ;
struct item *next ;
} Item ;
以上的表示了链接表中一个单独的项目,我们可以通过如下的搜索链接到在链接表中保存的数据:
Item *p ;
for( p=list_head ; p ; p=p->next ){
if (p->data= 123 ) {
printf(found 123\n”);
}
}
2. 双链接表
双链接表和链接表非常相似,除了它有指针指向下一个和上一个项目。双链接表在图2.4中。双链接表用C语言编写,可以这样表示:
typedef struct item {
int data ;
struct item *prev ;
struct item *next ;
} Item ;
双链接表相对于单链接表的优势就是它可以更快速的引用上一个项目。对于单链接表来说,查找上一个项目就需要搜索整个的表单。
3. 循环链接表
循环链接表和双链接表很相似,除了第一个项目的前一个指针指向最后一个项目,最后一个项目的下一个指针指向第一个项目。循环双链接表就在图2.4中。我们必须要了解的就是不论我们向前或者向后查找,我们最后都会在开始查找的地方结束。所以它叫做循环链接表。
双链接表和循环双链接表在内核数据结构当中使用非常频繁。它们在调度程序中,在内存管理编码,网络操作,缓冲管理等等。理解这些数据结构非常重要。
4.功能性指针
我们必须了解C语言编写的功能性指针才可以了解内核源码。功能性指针在文件系统编码、设备驱动编码中广泛使用。
功能性指针就是在一个地质中存储的指针。唯一不同的是它的地址是功能的位置。我们可以使用一个指针来谈谈不同的功能。
让我们试着跟随以下步骤使用功能性指针:
#include <stdio.h>
int multiply( int a, int b ){
return (a*b)
}
int add( int a, int b){
return (a+b) ;
}
int main(){
int (*f)( int, int );
f=maltiply ;
printf( "f(3,2)=%d\n",(*f)(3,2));
f=add ;
printf( "f(3,2)=%d\n",(*f)(3,2));
return 0 ;
首先,表明一个功能性指针的句法和标明一个普通指针的句法不同。表明功能性指针F,我们要这样写:
int (*f)( int, int );
表明功能性指针f以及它的功能,这种功能要用两个整数参数并返回一个整数。在宣布这个指针之后,我们让F指向任何一个对应种类的功能。可以选择给这个参数变量的名称。只有参数的种类是必须的。
我们可以把功能性指针f指向 multiply函数:
f = multiply ;
请注意,在这里我们只引用了multiply的地址。所以,在 multiply之后没有括号 ()。在这个赋值操作的过程中并调用任何函数,只是multiply的地址被复制了。
当我们给f赋值一个函数,我们就可以复引用f:
(*f)(3,2);
这个和以下的相等:
multiply (3,2);
如果我们重新给f赋值另一个函数,调用复引用f到另一个函数。我们就可以给f增加一个以上的赋值。
如果你这样运行,你就可以得到以下的结果:
f(3,2)=6
f(3,2)=5
你要确定你完全理解了以上的运算。
第五节 机械编码和集合
让我们想一想一个简单的C程式语句和附加的3个变量:
r=x+y+z;
这个看起来像是一个简单的语句。但是,即使是这个语句,CPU也不能在一个指令之内完成。CPU实际上要给出很多的指令来完成以上这个简单的运算。在你运行这个语句之前,你必须先要进行编译,然后改变成CPU可以读懂的机械编码。这个语句就简单的被编译为:
movl x, %eax
movl y, %ebx
addl %ebx, %eax
movl z, %ecx
addl %ecx, %eax
movl %ecx, %eax
movl %eax, r
如果你不懂以上那些语句也不用担心。这些编码叫做“集成”编码,或者助记符。这些语句只是机械语言的有意义的代表,并且可以一个一个的翻译成为机械语言。这些编码是CPU的特殊编码,也就是说这些编码适用于某种特别的CPU。在以上的例子当中,汇编语言是为IA-32CPU(x86)编写的。
“movl”是一个移动数据或者寄存的指令。我们可以看出,很多种x先要移调到EAX。下一个移动的指令复制y到EBX中寄存。然后addl指令把EAX当中的内容添加到EBX当中。在第一个addl指令完成之后,EAX要寄存x和y的总和。下一个movl and addl指令把z添加到总和当中。在这些之后,我们就得到了x,y,z的总和最后把运算结果复制到r当中。
我们可以看出单一C语句在CPU的指令下被分割成很多的机械语句。这些机械语句就是你的指令可以直接被计算机进行编译的语言编码。
在GNU C编译器中,这个汇编语言扩展比较容易。让我们想一想这个简单的例子在我们的C程式中编写汇编语言。
#include<stdio.h>
#include<stdlib.h>
int main(){
int x, y, z, r ;
x=1 ;
y=5 ;
z=7 ;
/*r=x+y+z ;*/
__asm__volatile (
“movl %1, %%eax \n”
“movl %2, %%ebx \n”
“addl %%ebx, %%eax \n”
“movl %3, %%ecx \n”
“addl %%ecx, %%eax \n”
“movl %%eax, %0 \n”
:”=g”(r)
:”g”(x), “g”(y), “g”(z)
);
printf( “r=%d\n”, r);
return 0 ;
}
这个集成码是为了给r=x+y+z负值。密码__asm__告诉gcc编译器我们直接把继承编码包括在我们的C程序当中。在实际应用中,在C程序中使用汇编语言是会被禁止的,因为它使这个程序不可以在其它的架构上面应用。然而为了实现内核,速度就成为了主要的考虑对象,而且有一些架构目录的汇编语言和内核的源码的架构目录。而且在某些情况下需要特殊的指令(比如说从真实模式到保护模式)这些特殊的指令不能用C语言表示。所以,有一部分的内核语言使用汇编语言编写的。
__asm__包括了3个必需的领域,这些领域用冒号(:)分开。第一个是汇编语言的字符串。因为它有一个以上的指令,所以在指令之间要用反斜线(\)或者分号(;)分隔开。暂存运算元,我们要用“%%”字首(比如存储EAX就是%%eax)。使用C语言中的变量,我们要把变量分成两类:输入变量和输出变量。输入变量的内容是读取数值,输出变量是为了写入数值。
输出变量就在ASM的第二个领域中,并且用逗号(,)分开。每一个输出变量都有一个句法“=x(变量)”x是一个约束(在以后的段落中我们会做出更详尽的解释)。Var就是变量的名称。在我们的案例中,我们用R代表输出变量,所以我们有“=g”(r)在第二个领域中。
第三个领域就是输入变量,这些变量使用逗号(,)分隔,每一个输入变量都有一个句法“x”(变量)。X是一个约束var就是它的名称。我们有x,y,z做我们的输入变量,所以第三个领域就是”g”(x),”g”(y),”g”(z).
为了使用这些变量,我们要在汇编语言中用%数字(例:%0)。%数字的匹配和变量都是很简单的,数字就是变量在输入输出领域当中的顺序号码,第一个就是%0,所以我们的%0就是r, 然后%1是x,%2是y,最终%3是z.所以下一个就是把这些变量复制存储到EAX当中:
“movl %1, %%eax \n”
我们使用了“g”约束了所有的输入输出变量,这个约束就是字符间的连接,描述了一些承认的运算元。这里我们包括了一个GCC手册一般约束的列表:
Whitespace:空白字符可以在任何一个地方插入,除了首字符的位置。这就在它们有变量的约束序列和修正号的时候每一个都是运算元可选的,并且排列成更适合机械阅读的。
“m”承认内存运算元有计算机支持的任何种类的地址。
“o”承认内存的预算元,但仅限可偏移的地址。它的意思就是加入一个小的整数(就是说计算机机械模式决定的运算元的字符长度)可能会被加到地址中,结果也是有效的内存地址。
比如,地址就是可以偏移的恒量;所以地址就是衡量的存储(只要比较大的恒量也在计算机支持的偏移地址的范围之内);但是自动增值或者自动贬值地址是不可偏移的。很多其它的复杂地址也有可能是不可偏移的,要看计算机是否支持这个地址模式。
“v”内存不可偏移运算元。就是很和一个可以适用于“m”但不适用于“o”的。
“<”内存运算元,自动递减模态地址(或者是提前递减模态或者是滞后递减模态)。“>”内存运算元自动递增模态地址(提前递增模态或者滞后递增模态)。
“r”在总寄存器中承认存储运算元
“i”承认立即整数运算元(有常数值)。包括象征性的常数的旨在汇编的时候会被告知。
“n”立即整数运算元有已知数值。很多系统不支持短于一个字长的汇编常数。约束这些运算元要用“n”,而不是“i”.
“I”,”J””K”,…“P” 其它的字母,I到P以特殊领域的整数值承认即使整数运算元。例:68000,“I”可以代表1-8。这个区域就是移动指令的移动计数承认范围。
“E”承认漂浮的即时运算元,(代表编码双常数)只要目标漂浮点格式和主机(编译器的运行地址)格式一样就可以。
“F”即时漂浮运算元(代表编码和双常数)。
“G”G和H是独立机械承认特殊值域的即时漂流运算符。
“s”即时整数运算元,它的值不是外在的整数。
这个可能显示起来比较奇怪;如果一个insn承认在编译的时候一个常数运算元有一个未知的值,所以它必须承认任何已知的值。所以为什么要用“S”而不是“i”?有时候它会更好的生成一些编码。
比如,在完全的指令68000中,使用即时运算元是有可能的;但是如果即时值在-128到127之间,加载编码结果到寄存器会好一点和使用寄存器都会好一点。这是因为加载到寄存器要使用“moveq”指令。我们安排这个是为了在-128-127的整数,然后在运算元约束中列入“Ks”.
“g”承认任何存储,内存或者即使整数运算元,除了那些非一般的寄存器。
“X”承认任何运算元。
“0”,“1”,“2”,…“9” 承认一个适配于列表中其它运算元数字的运算元。如果一个编码与其它的字符在同一个选项之中,编码要在后排列。
这个码应该不只是一个数字。如果很多数字连续不断的相遇,它们就以十位数整数来表示。很少有机会的机会含糊表示,因为适配于1或者0之间的数字很难出错。那我们还应该用别的多数字模式吗?
这就被称作是适配约束,就是汇编其只有一个单一的运算元有两个asm识别的任务。比如说,添加指令使用两个输入运算元和一个输出运算元,但是大多数CISC机械适配于一个添加指令只有两个运算元,其中一个就是输入-输出运算元。
Addl \#35, r12
适配于约束在以下环境当中适用。详细一点就是两个运算元的匹配必须包括一个仅仅输入的运算元。还有,数码必须比运算元在约束中的数字要小。
“P”一个运算元是一个被承认的有效的内存地址。这个是为“加载地址”“推传递地址”指令的。
“p”在约束当中必须和地址运算元一起,就像在匹配运算元中的指定一样。这个制定揭示了模块分类在匹配运算元中,内存模式中代表的地质师有效的。
Other-letters: 其它字符可以定义为机械依赖形式,代表一些特定的寄存器类别或者其它的任意运算元种类。“d””a”和“f”被定义为68000/68020是数据,地址和移动点寄存器。
第六节 模块设计
虽然Linux内核是一个整体的内核,它还是有一定的模块设计,减少内核的空间并且提高适用能力。
在内核内建立每一个设备驱动使内核的空间被没有必要的占用,因为大多数的驱动器可能不会被用到。Linux以处理内核模块解决了这个问题,所以一部分的内核可以依据你的需要进行加载。只有必需的模块才会被加载进计算机。那些没有被加载入的内核模块也不会占用内存空间。
模块被分别储存在root文件系统/lib/modules/目录下。要安装一个模块运行内核,我们就要用指令insmod.比如,如果我们要加载一个Ethernet eepro100的驱动器,我们就可以使用这个命令。
Insmod eepro100
这个insmod的用法在/lib/modules目录中查找eepro100这个模块,然后加载到运行中的内核里。请注意这个操作需要超级用户权限。
模块可以彼此使用,这就是它们之间产生了依赖性。insmod并不会解决这个依赖性,而是加载单一的模块进入内核。更加方便的方法就是modprobe,它使这充满模块依赖,加载其它所需的模块。
Modprobe eepro100
从内核中卸载一个模块,我们要使用rmmod命令:
Rmmod eepro100
在一个模块运行的时候卸载这个模块有可能失败。所以在卸载一个模块之前我们要确认相应的设备不再使用中。
我们要在设备驱动着一张里面使用这些命令。
第七节 练习
实现一个Helloworld内核模块。在这个练习中,学员会学习到:
●如何编译内核模块
●printk的用法
●用dmesg察看内核的用法
●了解Linux内核的模块设计
写一个内核模块可以非常简单:
#include<linux/module.h>
#if defined(CONFIG_SMP)
#define __SMP__
#endif
#include<linux/kernel.h>
Int init_module(void) {
/*called when loaded(e.g.insmod, modprobe)*/
}
Int cleanup_module(void){
/*called when unloaded(e.g.rmmod)*/
编译内核模块,我们要传递额外的gcc参数:
Gcc –c –D__KERNEL__-DMODULE helloworldmodule.c
输出模块是helloworldmodule.o
在内核中打印东西,我们要使用printk命令。例如,
Printk( KERN_INFO”abc %d\n”,1)
察看printk的结果,我们要使用dmesg命令:
Dmesg
现在,写你自己的内核模块。在加载的时候你的模块要打印一个“Hello World”的字在上边,并在卸载的时候打印“Goodbye World”。
第三章 系统启动
当我们给电脑冷启动之后,我们就会看到系统启动。我们可以看到电脑自我进行测试,告诉我们硬件工作正常,然后启动操作系统。操作系统开始加载,最后给你一个用户友好界面,所以你就可以和你的计算机进行交流,告诉它尼的计算机你的指令,让它执行,比如让它播放你喜欢的MP3文件,或者看你喜欢的DVD电影。
在这个章节里,我们要看看在系统启动的时候到底发生了什么。图3.1显示了Linux系统启动的过程。细节在这个途中会被分成段落解释。
第一节 BIOS
在你电脑之内的中央处理器在你启动计算机之后就会工作。然而RAM没有实际的作用,因为RAM非常得不稳定。在RAM中的数据,如果你切断电源就会丢失。所以第一个执行的指令是什么呢?
第一个执行的指令地址在你的ROM中。ROM是一个比较稳定的存储器,其中的内容不需要电源就可以保存。当启动的时候,所有CPU的寄存器就返回了默认状态。编码段(Code Segment)和指令指针(Instruction Pointer)寄存器就分别返回到0xf000和0xfff0。所以CPU就要从0xfffffff0开始提取执行。你的BIOS程序也就开始了执行。
当BIOS运行的时候,如下:
1. 执行自我测试power on self test(POST)
2. 初始化硬件
3. 从floppy,硬盘,CD-ROM查找启动设备
4. 加载第一个启动设备,并执行
POST是为了检测并且识别硬件设备。很多时候你可以看到BIOS显示了BIOS映像和版本,并且用几秒钟检查内存,然后探测安装硬盘和CD-ROM在你的计算机上,当自我检测完成的时候,BIOS就开始了硬件初始化。
硬件初始化就是要初始化硬件并且启动中断编译表格,比如分配IRQ给显卡,硬盘,控制面板等。一般来说,你可以看到表格当中有所有的PCI设备有分配IRQ,ports表格。
BIOS开始查找启动设备,依据启动顺序的描写。比如,它要查找到一个可以启动的floppy,然后找到一个可以启动的硬盘,然后是可以启动的CD-ROM。那么BIOS怎么知道它是否是可以启动的呢?启动段是盘上的第一个部分。它有启动加载器,可以加载整个的操作系统。BIOS以坐落在第一个部分的二进制的两个比特来决定第一个部分是否是可启动的。确定来说,启动部分由OxAA55在0x1FE,就意味着这个部分后两个比特分别是0x55和0xAA.
启动部分在0x7c00这个位置加载到内存当中,并开始执行指令0x7c00。如你再图上看到的一样,程序的编码放在启动最开始的部分。所以启动程序编码就会第一个被加载执行,启动加载器运行的时候。BIOS就结束了它的启动工作,并把控制任务交给启动加载器完成。
第二节 启动加载器
启动加载器的目的很简单:加载操作系统,内核并且执行。不同的操作系统使用不同的启动加载器,Thiz Linux使用Grub当作默认的启动加载器。
启动部分的容量很小,所以不可能占据整个的启动加载器的512比特。所以GRUB包括了几个部分,第一个部分在启动部分之内。如你所见,第一部分是512比特。截断1还要做的就是加载阶段2或者阶段1.5。因为512比特的约束,第一阶段加载第二阶段的GRUB。这就是你所能看到的一切。它给你一个菜单,所以你就能选择启动内核或者操作系统。
第三节 Linux 内核图像
在Linux加载器找到内核并且加载入内存之后,就是执行内核的时候了。然而,内核实在是太大了,所以一般我们用一个内核的压缩图像。
启动过程的细节是分类的架构,我们可以讨论一下启动过程IA32架构。内核启动过程可以分列为几个阶段:
1. 内核在保护模式中解压
2. asm编码执行低级初始化
3. 高级C初始化
启动加载器加载了压缩的内核图像到内存中,在加载器找到内核并且加载到内存之后,就开始了进入指针:它在arch/i386/boot/setup目录中。如名字一样,这个汇编编码是为了初始化硬件。当重要的硬件部分设置好了之后,进程就要转移到保护模式,用lmsw指令执行保护模式运行。
汇编器指令
Jmp0x1000, KERNEL_CS
然后初始化jump1到开始抵制的32比特编码的操作系统内核并且继续startup_32;在arch/i386/kernel/head.S.目录中这里很多的硬件部分都被初始化,环境也也适用于执行内核C功能的初始化。压缩内核图像解压。你可以看到“解压Linux”的信息在控制台显示。内核图像就被解压到0x100000.在解压之后,你举可以看到“Ok, booting the kernel”的信息显示。当初始化完成的时候,第一个C功能start_kernel()就被从init/main.c当中调用
这首先保存了汇编器中所有的数据,所有硬件的数据也都在其中。内核之中的所有区域也都被初始化了。现在运行的进程就是进程0。生成一个内核线程执行init()功能。
最后,0进程的问题就在于未使用的CPU频率。Init()功能负责剩下的初始化。它从bdflushi和kswap守护程序开始,它们负责缓存器的内容和文件系统交换的同步。
之后,系统调用安装来初始化文件系统,mount root文件系统。控制台和文件联结,0,1,2被建立。然后执行程序/etc/init, /bin/init或者sbin/init.从现在开始,控制会转到第一个用户进程,init进程使用以下的编码(init/main.c):
If (execute_command)
Execve(execute_command, argv_init, envp_init);
Execve(“/sbin/init’,argv_init, envp_init);
Execve(“/etc/init”,argv_init, envp_init);
Execve(“/bin/init”,argv_init, envp_init);
Panic(“No init found. Try passing init= option to kernel.”)
以上的编码段显示了内核是如何用execve调用来控制第一用户进程的:
Execute_command很多不同的execute_command是一个在路径用户定义init进程中的链接。不同的execute_command应该是NULL除非已有内核参数init。所以一般if-情况应该是错误的。
Argv_init这个是初始的init进程自变量。一般来说,只有一个变量,这个自变量就是init字符串。
Envp_init这是init进程不同的初始环境。一般来说,只有两个环境设置:
HOME=/
TERM=Linux
Execve这个功能是为了加载和执行一个程序。你也许感到有些零乱,为什么有那么多的execve功能。尽管有许多得execve功能,但是只有一个Init功能会作为开始。是因为execve功能不会成功地返回。所以说,如果内核找出/sbin/init并且执行它,那么execve就不会回到之前的execve功能,因为其它的程序就不可以被运行了。这样,只有一个init会被执行。
Panic如果所有的execve调用都失败了,panic功能就会被系统调用。Panic功能在任何时候内核遇到不可返回的错误的时候都会被调用。它就会打印一份错误信息出来给系统。
Init进程一般都从在Linux下运行背景进程开始,确定getty程序运行连接到了终端,所以用户就可以登陆到系统。如果没有一个以上提到的程序存在的话,那么就要运行/etc/rc进程,然后通过shell超级用户就可以修复系统了。
第四节 系统V Init
我们看到了内核是如何启动第一用户进程的。在这一章节中,我们要看第一用户进程、init进程是如何完成启动程序的。系统V Init Process(/sbin/init)根据运行阶层生成所有其它的系统启动进程。第一个任务就是从储存在/etc/inittab脚本文件中创建进程。一下显示了一部分的/etc/inittab内容:
#The default runlevel.
Id:5:initdefault:
#System initialization.
Si::sysinit:/etc/rc.d/rc.sysinit
…
在Thiz Linux中,文件有一个执行项/etc/rc.d/rc.sysinit去作出启动时的初始化。这就让init在每一行用户可以登陆的地方用gettys。
为了在启动时展示,从第五节层启动,系统V Init 进程首先运行/etc/rc.d/rc.sysinit,就是初始化一些基本的系统环境,比如PATH,HOSTNAME,然后再进行重要的系统初始化。例:安装Proc文件系统并加载需要的内核模块。然后执行所有的在/etc/rc.d/rc5.d/中的S*脚本,所以用户就可以通过虚拟的终端环境从任何一个登陆点登陆。最后X登陆经理就开始了用户从X服务器通过图形界面登陆。用户可以通过用户名和密码登陆一个系统。
第五节 内核参数
在内核中,有很多的可配置的参数,我们调用这些内核参数。有些时候,我们可能需要根据内核改变参数,调整任务或者是为了解决硬件不匹配的问题。比如说,我们可能需要为ide cdrom打开ide-scsi仿真器,我们要告诉内核做什么。内核有能力在启动的命令行中接受这些信息,这些和你要给它的程序的参数列表很相像。总体来说,这就提供给了内核硬件参数信息,因为这些信息使内核自己无法确定的,或者因为某些硬件问题或者特征使内核无法获取这些信息。
所以内核参数到底是什么?我们可以看一下/boot/grub/grub.conf文档:
Title Thiz Linux(2.4.18-12Thiz)
Root(hd0,1)
Kernel /boot/vmlinuz-2.4.18-12Thiz ro root=/dev/hda2 pci=bios noapic
Hdc=ide-scsi initrd /boot/initrd-2.4.18-12Thiz.img
我们可以看出ro root=/dev/hda2 pci=bios noapic hdc=ide=scsi是我们在启动的时候给内核的参数。这是启动加载器的任务(Thiz Linux中的GRUB)放参数到内存当中,所以内核就可以在启动的时候找到它。在内核启动的时候,这些参数就被保存到了不同的saved__command__line中,并且解析这些参数,内核启动通过这些。我们要按照启动把以下的参数分为两个种类,以便理解:
1. 内核处理的参数
2. 设备驱动处理的参数
让我们看看一些举例。还记得在sec:sysvinit中,Init进程内核的execve编码吗?有一条信息是“Noinit found. Try passing init= option to kernel.”告诉我们我们可以把这个参数交给内核处理,所以init=参数就会覆盖默认的init进程。这种参数使用内核处理的。
设备驱动处理的参数是完全为了驱动器设计的。比如,aha154x=参数告诉aha154x scsi设备驱动器硬件资源(IRQ,PORTS等等)基于设备驱动器解释参数的意思。注意设备驱动器启动参数仅仅匹配硬件驱动器,直接编译进去内核。它们没有影响驱动器加载的模块。把参数传递给内核模块,你需要使用模块参数。
注意参数不会用到的空间是非常重要的,为了分开不同的参数。数值在列表中用逗号分开彼此,其中没有任何空间。例如:
Ro root=/dev/hda2 lp=none,parport0 correct
Ro root = /dev/hda2 lp = none, parport0 Incorrect
1. 常用的参数
在这里,我们包括了一些内核中常用的参数。完成的文档在内核中可以找到源码文档文件。
/usr/src/linux/Documentation/kernel-parameters.txt:
Acpi 使用acpi=off关闭ACPI。一些驱动不能很好的支持ACPI,你也有可能需要关闭ACPI配置器。
Cachesize告诉内核CPU level2 cache size in kB.在硬件端口使CPU报告存储容量错误的时候重写。
Console控制台告诉内核在不同的控制台上打印内核信息。控制台是第一个虚拟终端,启动信息在你的VGA屏幕上。因为如果计算机没有VGA输出(比如服务器),我们就能映射控制台信息从另一个设备,比如序列port或者打印机输出。比如,第二个序列9600波特控制台,我们就传输“console=ttyS1,9600到内核。
Init告诉内核使用不同的Init程序作系统启动。比如,使用init=/bin/sh就会启动一个shell作为init进程。
Mem告诉内核安装内存。如果你有多于64MB的内存,但是你的BIOS报告你的系统只有安装了64MB,你就可以用mem=128.
Panic告诉内核自动充气内核的Panic.内核Panic 会在重大错误发生的时候决定重新自我启动。默认的行动是打印一个Panic信息,并且什么都不做。当系统必须重启的时候,也要用户手动重新启动。这个行为有时候并不需要,当即起自己运行的时候。有的特点就会自动让计算机内核Panic重新启动。用panic=30告诉内核在受到Panic信息30秒钟之后重启。Panic=0就是说取消系统自动重启的功能,永远在等待状态。
Quiet告诉内核隐藏所有内核信息。只是提示必须的重要信息。
Ro 告诉内核安装root文件系统为只读模式。这在root安装了文件系统后不可以用fsck正常的查看文档.注意虽然root文件系统安装为只读模式,也可以在以后系统启动的时候改成读写模式。
Root告诉内核root文档系统的设备。比如告诉内核使用3分区硬盘为根文件系统,你可以使用root=dev/hdc3告诉内核。
Rootfstype告诉内核root文件系统的种类。比如,rootfstype=ext2就会强迫内核安装root文件系统为ext2文件系统
Vga这个是一个启动加载器参数,不是一个内核参数。它生成一个特殊的Video模式,而不是默认的80X25的文本模式。你可以使用vga=ask让菜单给你一个列表选择你所需要的Video模式。你可以写vga=791给1024X768给VESA控制台。
第六节 初始化RAM
初始化RAM(initrd)在需要加载一些必须的安装Root文件系统内核模块时使用。比如,如果文件系统在SCSI硬盘上面,就必须在链接之前加载SCSI卡驱动。如果SCSI卡驱动被编译成了内核模块,那么内核就不能链接SCSI硬盘,会在启动的时候出现失败。
最经常使用的解决方法就是使用初始化RAM(initrd).Initrd文件一般包括了最小化的模块,有最基本的文件系统运行的安装模块。在启动的时候安装/dev/initrd。内核就使用数据块设备/dev/initrd的内容给两个阶段的系统启动。
在第一个启动阶段,内核从/dev/initrd开始安装初始化的Root 文件系统.在第二个阶段,附加的设备或者其它的模块也从初始Root设备加载到目录中。在加载附加的模块之后,一个新的Root文件系统(一个一般的root文件系统)就安装好了。
当用initrd启动的时候,系统启动如下:
1. 启动加载器(Thiz Linux中的GRUB)加载内核程序和/dev/initrd的内容进入内存。
2. 内核启动,内核届亚/dev/initrd中的内容复制到/dev/ram0然后再内存中用/dev/initrd释放
3. 内核读写设备从/dev/ram0安装成为初始化root文件系统。
4. 如果一般的root文件系统指出是初始的root文件系统,(/dev/ram0),那么内核就跳过最后的步骤进入一般启动顺序。
5. 如果可以执行的文件/linuxrc在初始的root文件系统当中,/linuxrc就是以执行的uid0.
6. 如果/linuxrc没有被执行,那么当/linuxrc终止的时候,一般的root文件系统就会被安装。
7. 如果一般root文件系统有/initrd目录,设备/dev/ram0从/to/initrd移动过来。否则目录/initrd就不存在在,没有安装在设备/dev/ram0当中。
8. 一般的启动顺序(/sbin/init)在一般的root文件系统下运行。