13.5 main.c中的初始化

 head.s在最后部分调用main.c中的start_kernel()函数,从而把控制权交给了它。所以启动程序从start_kernel()函数继续执行。这个函数是main.c乃至整个操作系统初始化的最重要的函数,一旦它执行完了,整个操作系统的初始化也就完成了。

如前所述,计算机在执行start_kernel()前处已经进入了386的保护模式,设立了中断向量表并部分初始化了其中的几项,建立了段和页机制,设立了 九个段,把线性空间中用于存放系统数据和代码的地址映射到了物理空间的头4MB,可以说我们已经使386处理器完全进入了全面执行操作系统代码的状态。但直到目前为止,我们所做的一切可以说都是针对386处理器所做的工作,也就是说几乎所有的多任务操作系统只要使用386处理器,都需要作这一切。而一旦start_kernel()开始执行,Linux内核的真实面目就步步的展现在你的眼前了。start_kernel()执行后,你就可以以一个用户的身份登录和使用Linux了。

让我们来看看start_kernel到底做了些什么,这里,通过介绍start_kernel()所调用的函数,我们来讨论start_kernel()的流程和功能。

我们仿照C语言函数的形式来进行这种描述,不过请注意,真正的start_kernel()函数调用子函数并不象我们在下面所写的这样简单,毕竟这本书的目的是帮助你深入分析Linux。我们只能给你提供从哪儿入手和该怎么看的建议,真正深入分析Linux,还需要你自己来研究代码。start_kernel()这个函数是在/init/main.c中,这里也只是将main.c中较为重要的函数列举出来。

 

start_kernel()          /*定义于init/main.c */

{

……

setup_arch(); 

}

 它主要用于对处理器、内存等最基本的硬件相关部分的初始化,如初始化处理器的类型(是在386486,还是586的状态下工作,这是有必要的,比如说,Pentium芯片支持4MB大小的页,而386就不支持),初始化RAM盘所占用的空间(如果你安装了RAM盘的话)等。其中,setup_arch()给系统分配了intel系列芯片统一使用的几个I/O端口的口地址。

      

paging_init();    /*该函数定义于arch/i386/mm/init.c */

 

 它的具体作用是把线性地址中尚未映射到物理地址上的部分通过页机制进行映射。这一部分在本书第六章有详细的描述,在这里需要特别强调的是,当paging_init()函数调用完后,页的初始化就整个完成了。

 

  trap_init(); /*该函数在arch/i386/kernel/traps.c中定义*/

  

这个初始化程序是对中断向量表进行初始化,详见第四章。它通过调用set_trap_gate(或set_system_gate等)宏对中断向量表的各个表项填写相应的中断响应程序的偏移地址。

事实上,Linux操作系统仅仅在运行trap_init()函数前使用BIOS的中断响应程序(我们这里先不考虑V86模式)。一旦真正进入了Linux操作系统,BIOS的中断向量将不再使用。对于软中断,Linux提供一套调用十分方便的中断响应程序,对于硬件设备,Linux要求设备驱动程序提供完善的中断响应程序,而调用使用多个参数的BIOS中断就被这些中断响应程序完全代替了。

另外,在trap_init()函数里,还要初始化第一个任务的LdtTSS,把它们填入Gdt相应的表项中。第一个任务就是init_task这个进程,填写完后,还要把init_taskTSSLDT描述符分别读入系统的TSSLDT寄存器。

 

init_IRQ()   /* arch/i386/kernel/irq.c中定义*/

 

 这个函数也是与中断有关的初始化函数。不过这个函数与硬件设备的中断关系更密切一些。

我们知道intel80386系列采用两片8259作为它的中断控制器。这两片级连的芯片一共可以提供16个引脚,其中15个与外部设备相连,一个用于级连。可是,从操作系统的角度来看,怎么知道这些引脚是否已经使用;如果一个引脚已被使用,Linux操作系统又怎么知道这个引脚上连的是什么设备呢?在内核中,同样是一个数组(静态链表)来纪录这些信息的。这个数组的结构在irq.h中定义:

   struct irqaction {

        void *handler)(int void * struct pt_regs *;

         unsigned long flags;

        unsigned long mask;   

        const char *name; 

        void *dev_id;

   struct irqaction *next;}

 

具体内容请参见第四章。我们来看一个例子:

 

static void math_error_irqint cpl void *dev_id struct pt_regs *regs

   {

       outb00xF0;

       if ignore_irq13 || !hard_math 

           return;

       math_error();

   }

static struct  irqaction  irq13 = { math_error_irq 0 0 "math error" NULL NULL };

 

该例子就是这个数组结构的一个应用,这个中断是用于协处理器的。在init_irq()这个函数中,除了协处理器所占用的引脚,只初始化另外一个引脚,即用于级连的2引脚。不过,这个函数并不仅仅做这些,它还为两片8259分配了I/O地址,对应于连接在管脚上的硬中断,它初始化了从0x20开始的中断向量表的15个表项(386中断门),不过,这时的中断响应程序由于中断控制器的引脚还未被占用,自然是空程序了。当我们确切地知道了一个引脚到底连接了什么设备,并知道了该设备的驱动程序后,使用setup_x86_irq这个函数填写该引脚对应的386的中断门时,中断响应程序的偏移地址才被填写进中断向量表。

 

sched_init() /*/kernel/sched.c中定义*/

 看到这个函数的名字可能令你精神一振,终于到了进程调度部分了,但在这里,你非但看不到进程调度程序的影子,甚至连进程都看不到一个,这个程序是名副其实的初始化程序:仅仅为进程调度程序的执行做准备。它所做的具体工作是调用init_bh函数(在kernel/softirq.c中)把timertqueueimmediate三个任务队列加入下半部分的数组。

 

time_init()/*arch/i386/kernel/time.c中定义*/

时间在操作系统中是个非常重要的概念。特别是在LinuxUnix这种多任务的操作系统中它更是作为主线索贯穿始终,之所以这样说,是因为无论进程调度(特别是时间片轮转算法)还是各种守护进程(也可以称为系统线程,如页表刷新的守护进程)都是根据时间运作的。可以说,时间是他们运行的基准。那么,在进程和线程没有真正启动之前,设定系统的时间就是一件理所当然的事情了。

我们知道计算机中使用的时间一般情况下是与现实世界的时间一致的。当然,为了避开CIH,把时间跳过每月26号也是种明智的选择。不过如果你在银行或证交所工作,你恐怕就一定要让你计算机上的时钟与挂在墙上的钟表分秒不差了。还记得CMOS吗?计算机的时间标准也是存在那里面的。所以,我们首先通过get_cmos_time()函数设定Linux的时间,不幸的是,CMOS提供的时间的最小单位是秒,这完全不能满足需要,否则CPU的频率1赫兹就够了。Linux要求系统中的时间精确到纳秒级,所以,我们把当前时间的纳秒设置为0

完成了当前时间的基准的设置,还要完成对8259的一号引脚上的8253(计时器)的中断响应程序的设置,即把它的偏移地址注册到中断向量表中去。

 

parse_options()   /*main.c中定义*/

这个函数把启动时得到的参数如debuginit等等从命令行的字符串中分离出来,并把这些参数赋给相应的变量。这其实是一个简单的词法分析程序。

 

console_init() /*linux/drivers/char/tty_io.c中定义*/

这个函数用于对终端的初始化。在这里定义的终端并不是一个完整意义上的TTY设备,它只是一个用于打印各种系统信息和有可能发生的错误的出错信息的终端。真正的TTY设备以后还会进一步定义。

 

kmalloc_init() /*linux/mm/kmalloc.c中定义*/

kmalloc代表的是kernel_malloc的意思,它是用于内核的内存分配函数。而这个针对kmalloc初始化函数用来对内存中可用内存的大小进行检查,以确定kmalloc所能分配的内存的大小。所以,这种检查只是检测当前在系统段内可分配的内存块的大小,具体内容参见第六章内存分配与回收一节。

 

下面的几个函数是用来对Linux的文件系统进行初始化的,为了便于理解,这里需要把Linux的文件系统的机制稍做介绍。不过,这里是很笼统的描述,目的只在于使我们对初始化的解释工作能进行下去,详细内容参见第八章的虚拟文件系统。

虚拟文件系统是一个用于消灭不同种类的实际文件系统间(相对于VFS而言,如ext2fat等实际文件系统存在于某个磁盘设备上)差别的接口层。在这里,您不妨把它理解为一个存放在内存中的文件系统。它具体的作用非常明显:Linux对文件系统的所有操作都是靠VFS实现的。它把系统支持的各种以不同形式存放于磁盘上或内存中(如proc文件系统)的数据以统一的形式调入内存,从而完成对其的读写操作。(Linux可以同时支持许多不同的实际文件系统,就是说,你可以让你的一个磁盘分区使用windowsFAT文件系统,一个分区使用UnixSYS5文件系统,然后可以在这两个分区间拷贝文件)。为了完成以及加速这些操作,VFS采用了块缓存,目录缓存(name_cach,索引节点(inode缓存等各种机制,以下的这些函数,就是对这些机制的初始化。

 

inode_init() /*Linux/fs/inode.c中定义*/

这个函数是对VFS的索引节点管理机制进行初始化。这个函数非常简单:把用于索引节点查找的哈希表置入内存,再把指向第一个索引节点的全局变量置为空。

 

name_cache_init() /*linux/fs/dcache.c中定义*/

这个函数用来对VFS的目录缓存机制进行初始化。先初始化LRU1链表,再初始化LRU2链表。

 

Buffer_init()/*linux/fs/buffer.c中定义*/

 

这个函数用来对用于指示块缓存的buffer free list初始化。

 

mem_init()  /* arch/i386/mm/init.c中定义*/

 

启动到了目前这种状态,只剩下运行/etc下的启动配置文件。这些文件一旦运行,启动的全过程就结束了,系统也终将进入我们所期待的用户态。现在,让我们回顾一下,到目前为止,我们到底做了哪些工作。

其实,启动的每一个过程都有相应的程序在屏幕上打印与这些过程相应的信息。我们回顾一下这些信息,整个启动的过程就一目了然了。

当然,你的计算机也许速度很快,你甚至来不及看清这些信息,系统就已经就绪,“Login:”就已经出现了,不要紧,登录以后,你只要打一条dmesg | more命令,所有这些信息就会再现在屏幕上。

 

Loading ……】出自bootsect.S ,表明内核正被读入。

uncompress ……】很多情况下,内核是以压缩过的形式存放在磁盘上的,这里是解压缩的过程 。

 

 下面这部分信息是在main.cstart_kernel函数被调用时显示的。

Linux version 2.2.6 root@lance) (gcc version 2.7.2.3)】Linux的版本信息和编译该内核时所用的gcc的版本。

Detected 199908264 Hz processor】调用init_time()时打出的信息。

Console:colour VGA+ 80x251 virtaul consolemax 63)】调用 console_init()打出的信息 。初始化的终端屏幕使用彩色VGA模式,最大可以支持63个终端。

Memory: 63396k/65536k available 848k kernel code 408k reserved 856k data)】调用 init_mem()时打印的信息。内存共计65536K,其中空闲内存为63396K,已经使用的内存中,有848K用于存放内核代码,404K保留,856K用于内核数据。

VFS:Diskquotas version dquot_6.4.0 initialized】调用dquote_init()打出的信息 。quota是用来分配用户磁盘定额的程序。关于这个程序请参看第八章。

 

  以下是对设备的初始化 :

PCI: PCI BIOS revision 2.10 entry at 0xfd8d1         |

 PCI: Using configuration type 1                      |

 PCI: Probing PCI hardware 调用pci_init()函数时显示的信息。

Linux NET4.0 for Linux 2.2                                  

 Based upon Swansea University Computer Society NET3.039      

 NET4: Unix domain sockets 1.0 for Linux NET4.0.            

 NET4: Linux TCP/IP 1.0 for NET4.0                            

 IP Protocols: ICMP UDP TCP调用socket_init()函数时打印的信息。使用Linux4.0版本的网络包,采用sockets 1.0 1.0版本的TCP/IP协议,TCP/IP协议中包含有ICMPUDPTCP三组协议。

 

Detected PS/2 Mouse Port        

 Sound initialization started             

 Sound initialization complete             

 Floppy drives: fd0 is 1.44M                                           

 Floppy drives: fd0 is 1.44M           

 FDC 0 is a National Semiconductor PC87306  调用device_setup()函数时打印的信息。包括对ps/2型鼠标,声卡和软驱的初始化。

 

看完上面这一部分代码和与之相应的信息,你应该发现,这些初始化程序并没有完成操作系统的各个部分的初始化,比如说,文件系统的初始化只是初始化了几个内存中的数据结构,而更关键的文件系统的安装还没有涉及,其实,这是在init进程建立后完成的。下面,就是start_kernel()的最后一部分内容。