Minix3的启动牵扯到几次控制权转移,它们发生在mpx386.s中的汇编语言例程和start.c及main.c中的C语言例程之间。
汇编代码需要做许多工作,包括建立一个 栈帧 以便为C编译器编译的代码提供适当的环境,复制处理器所使用的表格来定义存储器段,建立各种CPU寄存器,等等。这些工作结束后,初始化过程通过调用 cstart(start.c) 继续进行。
cstart() 调用另一个例程来初始化 全局描述符表(GDT) ,这是Intel 32位处理器 实现保护模式的内核数据结构; 以及 中断描述符表(LDT) ,它用来 为每种可能的中断类型选择执行代码。 在从 cstart() 返回之后,lgdt 和 lidt 指令通过向其对应的寻址寄存器中装入相应的值来将这些表格激活。
cstart() 做的第一件事就是调用prot_init() 来建立CPU的 保护机制和中断表。 然后 将引导参数复制到内存的内核部分。 并使用get_value() 扫描这些参数,通过查找参数名来返回相应的字符串值。它还确定显示器的型号、处理器类型、总线类型、是否是16位模式、处理器操作模式( 实模式还是保护模式 )等。所有的信息都保存在适当的全局变量中,以便于内核中的所有部分在需要时能够随时访问到它们。
下面代码中用到的一些typedef(类型定义)。
include/minix/type.h
/* 类型定义 */
typedef unsigned int vir_clicks; /* 虚拟地址 / 以clicks为单位 */
typedef unsigned long phys_bytes; /* 物理地址 / 以bytes为单位 */
typedef unsigned int phys_clicks; /* 物理地址 / 以clicks为单位 */
#if (_MINIX_CHIP == _CHIP_INTEL)
typedef unsigned int vir_bytes; /* 虚拟地址和字节长度 */
#endif
#if (_MINIX_CHIP == _CHIP_M68000)
typedef unsigned long vir_bytes;
#endif
#if (_MINIX_CHIP == _CHIP_SPARC)
typedef unsigned long vir_bytes;
#endif
vir2phys() 是一个宏,用于将内核中的虚拟地址转换为物理地址,它与 umap_local(proc_ptr, D, vir, sizeof(*vir)) 的作用是相同的,但成本更低。
#define vir2phys(vir) (kinfo.data_base + (vir_bytes) (vir))
seg2phys() 函数返回段的基地址。
kernel/start.c
/*
* 这个文件中包含了Intel处理器体系下的minix的启动代码。它和mpx386.s一起为main()
* 建立一个良好的环境。
* 对于16位系统,这段代码运行在实模式(real mode)下,并且必须切换到保护模式下对
* 于286机器。
* 对于32位系统,它已经运行在了保护模式(protected mode)下,但选择符仍然由禁用
* 中断的BIOS提供,因此需要重新加载描述符并创建中断描述符。
*/
FORWARD _PROTOTYPE(char *get_value, (_CONST char *params, _CONST char *key));
/*
* 在调用main()之前执行进行初始化。
* 大多数设置是在minix加载程序传递的环境字符串的帮助下确定的。
*
* cs, ds 内核代码段和数据段
* mds 监控数据段
* parmoff, parmsize 启动参数的偏移量和长度
*/
PUBLIC void cstart(U16_t cs, U16_t ds, U16_t mds,
U16_t parmoff, U16_t parmsize)
{
char params[128 * sizeof(char *)]; /* 启动时的监控参数 */
register char *value; /* key=value键值对中的value */
extern int etext, end;
/* 首先确定是否是保护模式;386机器或者更高的版本意味着保护模式。
* 这个工作必须首先完成,因为这是seg2phys()需要的。
* 对于286机器,我们还不能决定保护模式,这是下面要做的。
*/
#if _WORD_SIZE != 2 /* 286机器 */
machine.protected = 1;
#endif
/* 记录内核和监控器的位置 */
kinfo.code_base = seg2phys(cs);
kinfo.code_size = (phys_bytes)&etext; /* 代码段的大小 */
kinfo.data_base = seg2phys(ds);
kinfo.data_size = (phys_bytes)&end; /* 数据段的大小 */
/* 初始化保护模式的描述符,建立CPU的保护机制和中断表 */
prot_init();
/* 将启动参数拷贝到本地缓冲区(内存的内核部分) */
kinfo.params_base = seg2phys(mds) + parmoff;
kinfo.params_size = MIN(parmsize, sizeof(params) - 2);
phys_copy(kinfo.params_base, vir2phys(params), kinfo.params_size);
/* 记录用户空间服务器的杂项信息 */
kinfo.nr_procs = NR_PROCS;
kinfo.nr_tasks = NR_TASKS;
strncpy(kinfo.release, OS_RELEASE, sizeof(kinfo.release));
kinfo.release[sizeof(kinfo.release) - 1] = '\0';
strncpy(kinfo.version, OS_VERSION, sizeof(kinfo.version));
kinfo.version[sizeof(kinfo.version) - 1] = '\0';
kinfo.proc_addr = (vir_bytes)proc;
kinfo.kmem_base = vir2phys(0);
kinfo.kmem_size = (phys_bytes)&end;
/* 确定那些老的机器是否处于保护模式 */
machine.processor = atoi(get_value(params, "processor"));
#if _WORD_SIZE == 2
machine.protected = machine.processor >= 286;
#endif
if (!machine.protected)
mon_return = 0;
/* XT,AT or MCA总线 */
value = get_value(params, "bus");
if (value == NIL_PTR || strcmp(value, "at") == 0)
machine.pc_at = TRUE; /* PC-AT兼容的硬件 */
else if (strcmp(value, "mca") == 0)
machine.pc_at = machine.ps_mca = TRUE;
/* VDU类型 */
value = get_value(params, "video"); /* EGA or VGA视频单元 */
if (strcmp(value, "ega") == 0)
machine.vdu_ega = TRUE;
if (strcmp(value, "vga") == 0)
machine.vdu_vga = machine.vdu_ega = TRUE;
/* 返回到汇编代码,如果是286机器,则切换到保护模式,
* 重新加载选择符并调用main()。
*/
}
/*
* 获取环境值-getenv()函数的内核版本,以避免设置通常的环境数组
*
* params 启动时的监控参数
* name 带查找的值
*/
PRIVATE char *get_value(_CONST char *params, _CONST char *name)
{
register _CONST char *namep;
register char *envp;
for (envp = (char *)params; *envp != 0; ) {
for (namep = name; *namep != '\0' && *namep == *envp; namep++, envp++)
;
if (*namep == '\0' && *envp == '=')
return (envp + 1);
while (*envp++ != 0)
;
}
return (NIL_PTR);
}
这里需要说明一下上述代码中出现的几个宏。
#define PRIVATE static
#define FORWARD static /* 一些编译器要求它是静态的,用在有些函数的声明中 */
#define PUBLIC /* 定义为空串 */
这几个宏只是为了代码的可读性。
include/ansi.h
#ifdef _ANSI /* ANSI C编译器 */
#define _PROTOTYPE(function, params) function params
#define _ARGS(params) params
#define _VOIDSTAR void *
#define _VOID void
#define _CONST const
#define _VOLATILE volatile
#define _SIZET size_t
#else /* K&R C编译器 */
#define _PROTOTYPE(function, params) function()
#define _ARGS(params) ()
#define _VOIDSTAR void *
#define _VOID void
#define _CONST
#define _VOLATILE
#define _SIZET int
#endif /* _ANSI */
_PROTOTYPE 宏贯穿整个系统,所有的函数声明都以这种方式给出。它的出现是为了兼容ANSI C编译器和老式的K&R C编译器。其他的一些类型宏以后也都会遇见的。
之后由 main() 完成初始化,然后开始系统的正常运行。main() 代码的主要部分用来建立 进程表和特权表。这样当调度到第一批任务和进程时,它们的内存镜像和寄存器及特权信息能够被正确地设置。
对于进程表和特权表而言,在它们的一个域中放入一个特定值可以将其标记为未被使用。但对于每一个表项,无论是否被使用,都需要使用一个索引号来初始化。
下面我们来看一下a.out.h中的strcut exec结构。该结构定义了可执行文件的格式。
struct exec { /* a.out header */
unsigned char a_magic[2]; /* 魔数 */
unsigned char a_flags; /* 标志位 */
unsigned char a_cpu; /* cpu id */
unsigned char a_hdrlen; /* header的长度 */
unsigned char a_unused; /* 留作将来使用 */
unsigned short a_version; /* 版本标记(目前还未使用) */
long a_text; /* 文本段的大小 */
long a_data; /* 数据段的大小 */
long a_bss; /* bss段的大小 */
long a_entry; /* 入口点 */
long a_total; /* 分配的总内存 */
long a_syms; /* 符号表的大小 */
long a_trsize; /* 文本段重定位的大小 */
long a_drsize; /* 数据段重定位的大小 */
long a_tbase; /* 文本段重定位的基地址 */
long a_dbase; /* 数据段重定位的基地址 */
};
变量hdrindex总是被赋为0。内核中的进程全部被编译进了内核文件,而有关栈请求的信息则都在镜像表中。由于任务被编译进了内核并且可以调用内核中的任意代码和访问内核中任意位置的数据,所以一个独立任务的大小是没有什么意义的。
内核和每一个任务都读取aout数组中的同一元素,而一个任务的表征其大小的域则按内核大小进行赋值。任务从镜像表中获取它们的栈信息,在编译table.c时进行初始化。
处理完所有的内核进程后,该循环每执行一次,hdrindex就加1,所以所有的用户空间进程都可以从它们的头文件中得到正确的数据。
进程表中有一个入口不需要(也不能够)被调度。进程HARDWARE(任务)只是为了记账操作的目的,它记录中断服务所用的时间。所有其他进程通过lock_enqueue被放置到适当的队列中。函数lock_enqueue在修改队列之前关闭中断,修改完成后再打开中断。
对进程表初始化的最后一步是调用alloc_segments。这是一个与机器相关的过程,它将各进程使用的内存段的位置、大小及运行特权级设置到适当的域中。对一种内存分配方法相异的处理器类型,必须重写alloc_segments。
一旦所有任务、服务器和init初始化完成,系统基本上就可以运行了。变量bill_ptr标明对哪个进程进行CPU使用的计费,它需要一个初值,此时,IDLE进程是一个合适的选择。这时内核已经可以正常工作了。
虽然并非系统所有的其他部分都已能够正常运行,但它们都已能作为独立的进程运行,并被标记为就绪和等待执行。当它们运行时会初始化自身。
内核然后调用announce来声明它已就绪,之后调用restart。main函数的任务到此为止,它的工作只是完成初始化。最后对restart的调用将启动第一个任务,控制权从此不再返回到main.
/*
* 这个文件包含了Minix的主程序和关机部分的代码。
* main()初始化系统并通过设置进程表、中断向量以及调度每个要运行的任务来初始化
* 自身,从而开始系统的运行;shutdown()关闭系统。
*/
FORWARD _PROTOTYPE( void announce, (void));
FORWARD _PROTOTYPE( void shutdown, (timer_t *tp));
/*
* Minix的主程序,与其他程序的主程序不同的是,Minix的主程序
* 只完成系统的初始化工作,在初始化完成之后,控制权将从此
* 不会再返回到main()
*/
PUBLIC int main(void)
{
struct boot_image *ip; /* 引导映像指针 */
register struct proc *rp; /* 进程指针 */
register struct priv *sp; /* 特权结构指针 */
register int i, s;
phys_clicks text_base;
vir_clicks text_clicks, data_clicks;
reg_t ktsb; /* 内核任务栈基地址 */
struct exec e_hdr;
int hdrindex;
/* 初始化中断控制器 */
intr_init(1);
/* intr_init()之所以放在这里,是因为此前必须知道机器类型(因为intr_init调用
* 完全依赖于硬件)。
* 参数(1)指示intr_init是在为Minix3进行初始化,Minix终止并将控制权返回给
* 引导监控程序时可以通过使用参数(0)再次初始化硬件,使其回到原始状态。
* intr_init保证在完成初始化之前,任何中断都不会生效。
*/
/* 初始化pproc_addr数组,这个数组用于加快进程表的访问 */
for (rp = BEG_PROC_ADDR, i = -NR_TASKS; rp < END_PROC_ADDR; ++rp, ++i) {
rp->p_rts_flags = SLOT_FREE; /* 标志为空闲 */
rp->p_nr = i; /* proc number from ptr */
(pproc_addr + NR_TASKS)[i] = rp; /* proc ptr from number */
}
/* 清除特权表和ppriv_addr数组 */
for (sp = BEG_PRIV_ADDR, i = 0; sp < END_PRIV_ADDR; ++sp, ++i) {
sp->s_proc_nr = NONE; /* 初始化为NONE */
sp->s_id = i; /* priv结构索引 */
ppriv_addr[i] = sp; /* priv ptr from number */
}
/* 为任务和服务器设置进程表条目。内核任务的堆栈初始化为数据空间中的数组。
* 监视器已将服务器堆栈添加到数据段,因此堆栈指针被设置为指向数据段的末尾。
* 8086上的所有进程都处于低内存中。而在386上,只有内核处于低内存中,其余内存
* 都加载到了扩展内存中。
*/
/* 任务栈 */
ktsb = (reg_t)t_stack;
/* 使用运行引导映像所需的必要信息来初始化进程
* 所有这些进程在启动时必须存在,而且在正常运行过程中不应该中止。
*/
for (i = 0; i < NR_BOOT_PROCS; ++i) {
ip = &image[i]; /* 指向镜像表项 */
rp = proc_addr(ip->proc_nr); /* 获得进程指针 */
rp->p_max_priority = ip->priority; /* 最大调度优先级 */
rp->p_priority = ip->priority; /* 当前优先级 */
rp->p_quantum_size = ip->quantum; /* 时钟滴答数 */
rp->p_ticks_left = ip->quantum; /* current credit */
strncpy(rp->p_name, ip->proc_name, P_NAME_LEN); /* 设置进程名 */
(void)get_priv(rp, (ip->flags & SYS_PROC)); /* 分配结构 */
priv(rp)->s_flags = ip->flags; /* 进程标志位 */
priv(rp)->s_trap_mask = ip->trap_mask; /* 允许陷阱 */
priv(rp)->s_call_mask = ip->call_mask; /* 内核调用掩码 */
priv(rp)->s_ipc_to.chunk[0] = ip->ipc_to; /* 限制目标 */
/* 检测是否是内核中的进程 */
if (iskerneln(proc_nr(rp))) { /* 是内核部分 */
if (ip->stksize > 0) { /* 内核栈大小为0? */
rp->p_priv->s_stack_guard = (reg_t *)ktsb;
*rp->p_priv->s_stack_guard = STACK_GUARD;
}
ktsb += ip->stksize; /* 指向栈的高端(栈是向下增长的) */
rp->p_reg.sp = ktsb; /* 该任务的初始化栈指针 */
text_base = kinfo.code_base >> CLICK_SHIFT;
hdrindex = 0; /* 都使用第一个a.out header */
} else
hdrindex = 1 + i - NR_TASKS; /* 服务器,驱动程序,init进程 */
/* 引导加载程序在绝对地址'aout'处创建了一个a.out标头数组。
* 并且将一个元素赋给e_hdr。
*/
phys_copy(aout + hdrindex * A_MINHDR, vir2phys(&e_hdr),
(phys_bytes) A_MINHDR);
/* 将地址转换为clicks,并构建进程内存映射 */
text_base = e_hdr.a_syms >> CLICK_SHIFT;
text_clicks = (e_hdr.a_text + CLICK_SIZE-1) >> CLICK_SHIFT;
if (!(e_hdr.a_flags & A_SEP))
text_clicks = 0; /* common I&D */
data_clicks = (e_hdr.a_total + CLICK_SIZE-1) >> CLICK_SHIFT;
rp->p_memmap[T].mem_phys = text_base;
rp->p_memmap[T].mem_len = text_clicks;
rp->p_memmap[D].mem_phys = text_base + text_clicks;
rp->p_memmap[D].mem_len = data_clicks;
rp->p_memmap[S].mem_phys = text_base + text_clicks + data_clicks;
rp->p_memmap[S].mem_vir = data_clicks; /* empty - stack is in data */
/* 设置初始寄存器值。任务的处理器状态字与其他进程的处理器状态字不同,
* 因为任务通常拥有较高的优先级,可以访问I/O端口;而对于权限较低的进程,
* 这是不被允许的。
*/
rp->p_reg.pc = (reg_t) ip->initial_pc;
rp->p_reg.psw = (iskernelp(rp)) ? INIT_TASK_PSW : INIT_PSW;
/* 初始化服务器栈指针。记下一个字给crtso.s中的argc。 */
if (isusern(proc_nr(rp))) { /* 用户空间进程? */
rp->p_reg.sp = (rp->p_memmap[S].mem_vir +
rp->p_memmap[S].mem_len) << CLICK_SHIFT;
rp->p_reg.sp -= sizeof(reg_t);
}
/* 普通进程的启动设置 */
if (rp->p_nr != HARDWARE) {
rp->p_rts_flags = 0; /* 如果没有标志位,则可以运行 */
lock_enqueue(rp); /* 添加到调度队列 */
} else { /* 内核进程 */
rp->p_rts_flags = NO_MAP; /* 禁止运行 */
}
/* 必须在保护模式下分配代码段和数据段 */
alloc_segments(rp);
/* 这是一个与机器相关的过程,它将各进程使用的内存段的位置、大小
* 及运行特权级设置到适当的域中。对一种内存分配方法相异的处理器类型,
* 必须重写alloc_segments。
*/
}
shutdown_started = 0;
/* Minix现已准备就绪,所有启动映像进程都在就绪队列中。
* 现在返回到汇编代码中以开始运行当前进程。
*/
bill_ptr = proc_addr(IDLE); /* 它必须指向某个地方,此时IDLE进程是一个合适的选择 */
announce(); /* 打印Minix启动banner */
restart(); /* 启动第一个任务,控制权将从此不再返回到main() */
}
/* 展示Minix启动banner */
PRIVATE void announce(void)
{
kprintf("MINIX %s.%s."
"Copyright 2006, Vrije Universiteit, Amsterdam, The Netherlands\n",
OS_RELEASE, OS_VERSION);
/* 实模式还是16/32位保护模式? */
kprintf("Executing in %s mode.\n\n",
machine.protected ? "32-bit protected" : "real");
}
/* 准备关闭Minix */
PUBLIC void prepare_shutdown(int how)
{
static timer_t shutdown_timer;
register struct proc *rp;
message m;
/* 在panics上显示调试转储,确保tty任务仍然存在并且可以处理它们,这是通过非阻塞send完成的。
* 在调试转储完成后,我们依赖tty调用sys_abort()。
*/
if (how == RBT_PANIC) {
m.m_type = PANIC_DUMPS;
if (nb_send(tty_PROC_NR,&m)==OK) /* 如果tty没有准备好,请不要阻止 */
return; /* 等待来自tty的sys_abort() */
}
/* 向仍处于活动状态的所有系统进程发送信号,通知它们Minix内核正在关闭。
* 应由用户空间服务器完成正确的关闭处理。在系统发生混乱时,此机制可用作备份,
* 因此系统进程仍可运行其关闭代码,例如,同步fs或让tty切换到第一个控制台。
*/
kprintf("Sending SIGKSTOP to system processes ...\n");
for (rp = BEG_PROC_ADDR; rp < END_PROC_ADDR; rp++) {
if (!isemptyp(rp) && (priv(rp)->s_flags & SYS_PROC) && !iskernelp(rp))
send_sig(proc_nr(rp), SIGKSTOP);
}
/* 我们正在关机,诊断现在可能会有不同的表现 */
shutdown_started = 1;
/* 通知即将关闭的系统进程,并允许它们通过设置一个shutdown()中的watchog
* 计时器来安排自己。计时器参数传递关闭状态。
*/
kprintf("MINIX will now be shut down ...\n");
tmr_arg(&shutdown_timer)->ta_int = how;
/* 1s后继续,让进程有机会安排关机工作 */
set_timer(&shutdown_timer, get_uptime() + HZ, shutdown);
}
/*
* 从prepare_shutdown()或stop_sequence()调用此函数来关闭Minix。
* 关闭参数(如何关闭):
* RBT_HALT(返回监视器),
* RBT_MONITOR(执行给定代码),
* RBT_RESET(硬复位)
*/
PRIVATE void shutdown(timer_t *tp)
{
int how = tmr_arg(tp)->ta_int;
u16_t magic;
/* 现在屏蔽所有中断,包括时钟中断;并停止时钟 */
outb(INT_CTLMASK, ~0);
clock_stop();
if (mon_return && how != RBT_RESET) {
/* 将中断控制器重新初始化位bios的默认值 */
intr_init(0);
outb(INT_CTLMASK, 0);
outb(INT2_CTLMASK, 0);
/* 返回到启动监视器,如果还没有完成,请设置程序 */
if (how != RBT_MONITOR)
phys_copy(vir2phys(""), kinfo.params_base, 1);
level0(monitor);
}
/* 通过跳转到复位地址(实模式)或强制关闭处理器(保护模式)来重置系统。
* 首先通过设置软复位标志来停止BIOS内存测试。
*/
magic = STOP_MEM_CHECK;
phys_copy(vir2phys(&magic), SOFT_RESET_FLAG_ADDR, SOFT_RESET_FLAG_SIZE);
level0(reset);
}