且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Linux0.11内核剖析--初始化程序(init)

更新时间:2021-12-02 22:58:31

1、概述

在内核源代码的 init/目录中只有一个 main.c 文件。 系统在执行完 boot/目录中的 head.s 程序后就会将执行权交给 main.c。该程序虽然不长,但却包括了内核初始化的所有工作。因此在阅读该程序的代码时需要参照很多其它程序中的初始化部分。如果能完全理解这里调用的所有程序,那么看完这章内容后你应该对Linux 内核有了大致的了解。
从本文开始,我们将接触大量的 C 程序代码,因此读者***具有一定的 C 语言知识。***的一本参考书还是 Brian W. Kernighan 和 Dennis M. Ritchie 编著的《C 程序设计语言》。在注释 C 语言程序时,为了与程序中原有的注释相区别,我们使用'//'作为注释语句的开始。对于程序中包含的头文件( *.h),仅作概要含义的解释。
本文地址:http://www.cnblogs.com/archimedes/p/linux011-init.html,转载请注明源地址。

2、main.c 程序

1、功能描述
main.c 程序首先利用 setup.s 程序取得的系统参数设置系统的根文件设备号以及一些内存全局变量。这些内存变量指明了主内存的开始地址、系统所拥有的内存容量和作为高速缓冲区内存的末端地址。如果还定义了虚拟盘( RAMDISK),则主内存将适当减少。整个内存的映像示意图如图所示:
Linux0.11内核剖析--初始化程序(init)

(系统中内存功能划分示意图)
图中,高速缓冲部分还要扣除被显存和 ROM BIOS 占用的部分。

高速缓冲区是用于磁盘等块设备临时存放数据的地方,以 1K( 1024)字节为一个数据块单位。

主内存区域的内存是由内存管理模块 mm 通过分页机制进行管理分配,以 4K 字节为一个内存页单位。

内核程序可以***访问高速缓冲中的数据,但需要通过 mm 才能使用分配到的内存页面。然后,内核进行所有方面的硬件初始化工作,包括陷阱门、块设备、字符设备和 tty,包括人工创建第一个任务( task 0)。待所有初始化工作完成就设置中断允许标志,开启中断。在阅读这些初始化子程序时,***是跟着被调用的程序深入进去看,如果实在看不下去了,就暂时先放一放,继续看下一个初始化调用。在有些理解之后再继续研究没有看完的地方。在整个内核完成初始化后,内核将执行权切换到了用户模式, 也即 CPU 从 0 特权级切换到了第 3 特权级。然后系统第一次调用创建进程函数 fork(),创建出一个用于运行 init()的子进程。在该进程(任务)中系统将运行控制台程序。如果控制台环境建立成功,则再生成一个子进程,用于运行 shell 程序/bin/sh。若该子进程退出,父进程返回,则父进程进入一个死循环内,继续生成子进程,并在此子进程中再次执行 shell 程序/bin/sh,而父进程则继续等待。
代码注释:

/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include <unistd.h>
#include <time.h>

/*
 * 我们需要下面这些内嵌语句 - 从内核空间创建进程(forking)将导致没有写时复制(COPY ON WRITE) !!!
 * 直到一个执行 execve 调用。这对堆栈可能带来问题。处理的方法是在 fork()调用之后不让 main()使用
 * 任何堆栈。因此就不能有函数调用 - 这意味着 fork 也要使用内嵌的代码,否则我们在从 fork()退出
 * 时就要使用堆栈了。
 * 实际上只有 pause 和 fork 需要使用内嵌方式,以保证从 main() 中不会弄乱堆栈,但是我们同时还
 * 定义了其它一些函数。
 */
static inline _syscall0(int,fork)  // 是 unistd.h 中的内嵌宏代码。以嵌入汇编的形式调用
  // Linux 的系统调用中断 0x80。该中断是所有系统调用的
  // 入口。该条语句实际上是 int fork()创建进程系统调用
  // syscall0 名称中最后的 0 表示无参数,1 表示 1 个参数
static inline _syscall0(int,pause)  // int pause()系统调用:暂停进程的执行,直到收到一个信号
static inline _syscall1(int,setup,void *,BIOS)  // int setup(void * BIOS)系统调用,仅用于linux 初始化(仅在这个程序中被调用)
static inline _syscall0(int,sync)  // int sync() 系统调用:更新文件系统。

#include <linux/tty.h>  // tty 头文件,定义了有关 tty_io,串行通信方面的参数、常数。
#include <linux/sched.h>// 调度程序头文件,定义了任务结构 task_struct、第 1 个初始任务
  // 的数据。还有一些以宏的形式定义的有关描述符参数设置和获取的
  // 嵌入式汇编函数程序。

#include <linux/head.h>  // head 头文件,定义了段描述符的简单结构,和几个选择符常量。
#include <asm/system.h>  // 系统头文件。以宏的形式定义了许多有关设置或修改描述符/中断门等的嵌入式汇编子程序。
#include <asm/io.h>  // io 头文件。以宏的嵌入汇编程序形式定义对 io 端口操作的函数。
 
#include <stddef.h>  // 标准定义头文件。定义了 NULL, offsetof(TYPE, MEMBER)。
#include <stdarg.h>  // 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-个
   // 类型(va_list)和三个宏(va_start, va_arg 和 va_end),vsprintf、vprintf、vfprintf。
#include <unistd.h>
#include <fcntl.h>  // 文件控制头文件。用于文件及其描述符的操作控制常数符号的定义。
#include <sys/types.h>  // 类型头文件。定义了基本的系统数据类型。

#include <linux/fs.h>  // 文件系统头文件。定义文件表结构( file,buffer_head, m_inode 等)。

static char printbuf[1024];  // 静态字符串数组。

extern int vsprintf();  // 送格式化输出到一字符串中(在 kernel/vsprintf.c,92 行)。
extern void init(void);   // 函数原形,初始化(在 168 行)。
extern void blk_dev_init(void);  // 块设备初始化子程序( kernel/blk_drv/ll_rw_blk.c,157 行)
extern void chr_dev_init(void);  // 字符设备初始化( kernel/chr_drv/tty_io.c, 347 行)
extern void hd_init(void);   // 硬盘初始化程序( kernel/blk_drv/hd.c, 343 行)
extern void floppy_init(void);   // 软驱初始化程序( kernel/blk_drv/floppy.c, 457 行)
extern void mem_init(long start, long end);  // 内存管理初始化( mm/memory.c, 399 行)
extern long rd_init(long mem_start, int length);  //虚拟盘初始化(kernel/blk_drv/ramdisk.c,52)
extern long kernel_mktime(struct tm * tm);  // 建立内核时间(秒)。
extern long startup_time;  // 内核启动时间(开机时间)(秒)。

/*
 *  以下这些数据是由 setup.s 程序在引导时间设置的。
 */
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)

/*
 * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象它还能运行。如果有
 * 关于实时时钟更多的资料,那我很感兴趣。这些都是试探出来的,以及看了一些 bios 程序,呵!
 */

#define CMOS_READ(addr) ({ \     // 这段宏读取 CMOS 实时时钟信息。
outb_p(0x80|addr,0x70); \      // 0x70 是写端口号, 0x80|addr 是要读取的 CMOS 内存地址。
inb_p(0x71); \          // 0x71 是读端口号。
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)  // 将 BCD 码转换成数字。

static void time_init(void)  // 该子程序取 CMOS 时钟,并设置开机时间Æstartup_time(秒)。
{
    struct tm time;

    do {
        time.tm_sec = CMOS_READ(0);  // 参见后面 CMOS 内存列表。
        time.tm_min = CMOS_READ(2);
        time.tm_hour = CMOS_READ(4);
        time.tm_mday = CMOS_READ(7);
        time.tm_mon = CMOS_READ(8);
        time.tm_year = CMOS_READ(9);
    } while (time.tm_sec != CMOS_READ(0));
    BCD_TO_BIN(time.tm_sec);
    BCD_TO_BIN(time.tm_min);
    BCD_TO_BIN(time.tm_hour);
    BCD_TO_BIN(time.tm_mday);
    BCD_TO_BIN(time.tm_mon);
    BCD_TO_BIN(time.tm_year);
    time.tm_mon--;
    startup_time = kernel_mktime(&time);
}

static long memory_end = 0;  // 机器具有的内存(字节数)。
static long buffer_memory_end = 0;  // 高速缓冲区末端地址。
static long main_memory_start = 0;  // 主内存(将用于分页)开始的位置。

struct drive_info { char dummy[32]; } drive_info;  // 用于存放硬盘参数表信息。

void main(void)        /* 这里确实是 void,并没错。*/
{            /* 在 startup 程序(head.s) 中就是这样假设的。*/
/*
 * 此时中断仍被禁止着,做完必要的设置后就将其开启。
 */
 // 下面这段代码用于保存:
 // 根设备号 -> ROOT_DEV; 高速缓存末端地址 -> buffer_memory_end;
 // 机器内存数 -> memory_end;主内存开始地址 -> main_memory_start;
     ROOT_DEV = ORIG_ROOT_DEV;
     drive_info = DRIVE_INFO;
    memory_end = (1<<20) + (EXT_MEM_K<<10);  // 内存大小=1Mb 字节+扩展内存(k)*1024 字节。
    memory_end &= 0xfffff000;        // 忽略不到 4Kb(1 页)的内存数。
    if (memory_end > 16*1024*1024)        // 如果内存超过 16Mb,则按 16Mb 计。
        memory_end = 16*1024*1024;
    if (memory_end > 12*1024*1024)         // 如果内存>12Mb,则设置缓冲区末端=4Mb
        buffer_memory_end = 4*1024*1024;
    else if (memory_end > 6*1024*1024)    // 否则如果内存>6Mb,则设置缓冲区末端=2Mb
        buffer_memory_end = 2*1024*1024;
    else
        buffer_memory_end = 1*1024*1024;
    main_memory_start = buffer_memory_end;
#ifdef RAMDISK
    main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
    mem_init(main_memory_start,memory_end);
    trap_init();    // 陷阱门(硬件中断向量)初始化。(kernel/traps.c,181 行)
    blk_dev_init();    // 块设备初始化。  (kernel/blk_dev/ll_rw_blk.c,157 行)
    chr_dev_init();  // 字符设备初始化。  (kernel/chr_dev/tty_io.c,347 行)
    tty_init();    // tty 初始化。 (kernel/chr_dev/tty_io.c,105 行)
    time_init();  // 设置开机启动时间->startup_time(见 76 行)。
    sched_init();    // 调度程序初始化(加载了任务 0 的 tr, ldtr) (kernel/sched.c,385)
    buffer_init(buffer_memory_end);        // 缓冲管理初始化,建内 存链表等。(fs/buffer.c,348)
    hd_init();    // 硬盘初始化。 ( kernel/blk_dev/hd.c,343 行)
    floppy_init();    // 软驱初始化。 ( kernel/blk_dev/floppy.c,457 行)
    sti();        // 所有初始化工作都做完了,开启中断。
             // 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务 0。
    move_to_user_mode();  // 移到用户模式。 ( include/asm/system.h,第 1 行)
    if (!fork()) {        /* we count on this going ok */
        init();
    }
/* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
 * 回就绪运行态,但任务 0(task0)是唯一的意外情况(参见'schedule()'),因为任务 0 在
 * 任何空闲时间里都会被激活(当没有其它任务在运行时),因此对于任务 0'pause()'仅意味着
 * 我们返回来查看是否有其它任务可以运行,如果没有的话我们就回到这里,一直循环执行'pause()'。
 */
    for(;;) pause();
}

static int printf(const char *fmt, ...)
{
 // 产生格式化信息并输出到标准输出 设备 stdout(1) ,这里是指屏幕上显示。参数'*fmt'指定输出将
 // 采用的格式,参见各种标准 C 语言书籍。该子程序正好是 vsprintf 如何使用的一个例子。
 // 该程序使用 vsprintf() 将格式化的字符串放入 printbuf 缓冲区,然后用 write()将缓冲区的内容
 // 输出到标准设备(1--stdout)。
    va_list args;
    int i;

    va_start(args, fmt);
    write(1,printbuf,i=vsprintf(printbuf, fmt, args));
    va_end(args);
    return i;
}
  
static char * argv_rc[] = { "/bin/sh", NULL };  // 调用执行程序时参数的字符串数组。
static char * envp_rc[] = { "HOME=/", NULL };   // 调用执行程序时的环境字符串数组。

static char * argv[] = { "-/bin/sh",NULL };    // 同上。
static char * envp[] = { "HOME=/usr/root", NULL };

void init(void)
{
    int pid,i;
    
// 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。
// 该函数是在 25 行上的宏定义的,对应函数是 sys_setup(),在 kernel/blk_drv/hd.c,71 行。
    setup((void *) &drive_info);
    (void) open("/dev/tty0",O_RDWR,0);   // 用读写访问方式打开设备“ /dev/tty0”,这里对应终端控制台。
                           // 返回的句柄号 0 -- stdin 标准输入设备。
    (void) dup(0);  // 复制句柄,产生句柄 1 号 -- stdout 标准输出设备。
    (void) dup(0);  // 复制句柄,产生句柄 2 号 -- stderr 标准出错输出设备。
    printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
        NR_BUFFERS*BLOCK_SIZE);        // 打印缓冲区块数和总字节数,每块 1024 字节。
    printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);    //空闲内存字节数。
    
 // 下面 fork() 用于创建一个子进程(子任务)。对于被创建的子进程,fork() 将返回 0 值,
 // 对于原(父进程)将返回子进程的进程号。所以 180-184 句是子进程执行的内容。该子进程
 // 关闭了句柄 0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和
 // 环境变量分别由 argv_rc 和 envp_rc 数组给出。参见后面的描述。
    if (!(pid=fork())) {
        close(0);
        if (open("/etc/rc",O_RDONLY,0))
            _exit(1);    // 如果打开文件失败, 则退出(/lib/_exit.c,10)。
        execve("/bin/sh",argv_rc,envp_rc);    // 装入/bin/sh 程序并执行。
        _exit(2);    // 若 execve()执行失败则退出(出错码 2,“文件或目录不存在”)。
    }

 // 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid)。
 // 这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的位置。如果 wait()返回值不
 // 等于子进程号,则继续等待。
    if (pid>0)
        while (pid != wait(&i))
            /* nothing */;

 // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程,
 // 如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对于所创建的子进程关闭所有
 // 以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,然后重新打开
 // /dev/tty0 作为 stdin,并复制成 stdout 和 stderr。再次执行系统解释程序/bin/sh。但这次执行所
 // 选用的参数和环境数组另选了一套(见上面 165-167 行)。然后父进程再次运行 wait()等待。如果
 // 子进程又停止了执行,则在标准输出上显示出错信息“子进程 pid 停止了运行,返回码是 i”,然后
 // 继续重试下去…,形成“大”死循环。
    while (1) {
        if ((pid=fork())<0) {
            printf("Fork failed in init\r\n");
            continue;
        }
        if (!pid) {
            close(0);close(1);close(2);
            setsid();
            (void) open("/dev/tty0",O_RDWR,0);
            (void) dup(0);
            (void) dup(0);
            _exit(execve("/bin/sh",argv,envp));
        }
        while (1)
            if (pid == wait(&i))
                break;
        printf("\n\rchild %d died with code %04x\n\r",pid,i);
        sync();
    }
    _exit(0);    /* NOTE! _exit, not exit() */
}

3、其它信息

1、CMOS 信息
PC 机的 CMOS(complementary metal oxide semiconductor 互补金属氧化物半导体)内存实 际上是由电池供电的 64 或 128 字节 RAM 内存块,是系统时钟芯片的一部分。有些机器还有更大的内存容量。该 64 字节的 CMOS 首先在 IBM PC-XT 机器上用于保存时钟和日期信息。由于这些信息仅用去 14 字节,剩余的字节就用来存放一些系统配置数据了。 CMOS 的地址空间是在基本地址空间之外的。因此其中不包括可执行的代码。它需要使用在端口70h,71h 使用 IN 和 OUT 指令来访问。为了读取指定偏移位置的字节,首先需要使用 OUT 向端口 70h 发送指定字节的偏移值,然后使用 IN 指令从 71h 端口读取指定的字节信息。这段程序中(行 70)将欲读取的字节地址或上了一个 80h 值是没有必要的。 因为那时的 CMOS 内存容量还没有超过 128 字节,因此或上 80h 的操作是没有任何作用的。之所以会有这样的操作是因为当时 Linus手头缺乏有关 CMOS 方面的资料, CMOS 中时钟和日期的偏移地址都是他逐步实验出来的,也许在他实验中将偏移地址或上 80h(并且还修改了其它地方)后正好取得了所有正确的结果,因此他的代码中也就
有了这步不必要的操作。不过从 1.0 版本之后,该操作就被去除了 (可参见 1.0 版内核程序 drivers/block/hd.c第 42 行起的代码)。下面是 CMOS 内存信息的一张简表。
地址偏移值             内容说明
0x00            当前秒值 (实时钟)
0x01            报警秒值
0x02            当前分钟 (实时钟)
0x03            报警分钟值
0x04            当前小时值 (实时钟)
0x05            报警小时值
0x06           一周中的当前天 (实时钟)
0x07           一月中的当日日期 (实时钟)
0x08           当前月份 (实时钟)
0x09           当前年份 (实时钟)
0x0a RTC          状态寄存器 A
0x0b RTC          状态寄存器 B
0x0c RTC          状态寄存器 C
0x0d RTC          状态寄存器 D
0x0e POST          诊断状态字节
0x0f            停机状态字节
0x10           磁盘驱动器类型
0x11           保留
0x12           硬盘驱动器类型
0x13           保留
0x14           设备字节
0x15           基本内存 (低字节)
0x16           基本内存 (高字节)
0x17           扩展内存 (低字节)
0x18           扩展内存 (高字节)
0x19-0x2d        保留
0x2e           校验和 (低字节)
0x2f            校验和 (高字节)
0x30           1Mb 以上的扩展内存 (低字节)
0x31           1Mb 以上的扩展内存 (高字节)
0x32           当前所处世纪值
0x33           信息标志
0x34-0x3f         保留

2、调用 fork()创建新进程
fork 是一个系统调用函数。 该系统调用复制当前进程, 并在进程表中创建一个与原进程(被称为父进程)几乎完全一样的新表项,并执行同样的代码,但该新进程(这里被称为子进程)拥有自己的数据空间和环境参数。 在父进程中,调用 fork()返回的是子进程的进程标识号 PID,而在子进程中 fork()返回的将是 0 值,这样,虽然此时还是在同样一程序中执行,但已开始叉开,各自执行自己的那段代码。如果 fork()调用失败,则会返回小于 0 的值。如图所示:
Linux0.11内核剖析--初始化程序(init)
init 程序即是用 fork()调用的返回值来区分和执行不同的代码段的。 上面代码中第 179 和 194 行是子进程的判断并开始子进程代码块的执行(利用 execve()系统调用执行其它程序,这里执行的是 sh),第 186和 202 行是父进程执行的代码块。