博客主要为《Linux内核设计的艺术》(以下简称《设计艺术》)和《Linux内核完全注释》(以下简称《完全注释》),以及非常好的Linux内核视频 - Linux内核精讲内容的搬运和阅读笔记,以及相关博客链接的整理。代码来源于《完全注释》配套代码。
写着玩儿的,如有错误,欢迎指正。
main.c调用了大量初始化函数。
让我们来到一切的起点,来看main()的第一个片段:
// 此时中断仍被禁止着,做完必要的设置后就将其开启。 // 下面这段代码用于保存: // 根设备号 -> 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;// 否则则设置缓冲区末端=1Mb main_memory_start = buffer_memory_end;// 主内存起始位置=缓冲区末端; #ifdef RAMDISK // 如果定义了虚拟盘,则主内存将减少。 main_memory_start += rd_init(main_memory_start, RAMDISK*1024); #endif
根设备和文件系统有关,与硬件相关的我都不做深究,这部分主要就是划分内存区域。
这里还涉及到1M以下的地址为内核地址,管理方式与1M以上的地址是不同的。
// 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,实在看 // 不下去了,就先放一放,看下一个初始化调用-- 这是经验之谈:) mem_init(main_memory_start,memory_end); trap_init(); // 陷阱门(硬件中断向量)初始化。(kernel/traps.c) blk_dev_init(); // 块设备初始化。(kernel/blk_dev/ll_rw_blk.c) chr_dev_init(); // 字符设备初始化。(kernel/chr_dev/tty_io.c)空,为以后扩展做准备。 tty_init(); // tty 初始化。(kernel/chr_dev/tty_io.c) time_init(); // 设置开机启动时间 -> startup_time。 sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c) buffer_init(buffer_memory_end);// 缓冲管理初始化,建内存链表等。(fs/buffer.c) hd_init(); // 硬盘初始化。(kernel/blk_dev/hd.c) floppy_init(); // 软驱初始化。(kernel/blk_dev/floppy.c) sti(); // 所有初始化工作都做完了,开启中断。
这部分是各种初始化配置,这些函数在《设计艺术》中都有较为详细的讲解,主要关注几个小点。
trap_init()第一个是trap_init()(kernel/traps.c)的定义:
// 下面是异常(陷阱)中断程序初始化子程序。设置它们的中断调用门(中断向量)。
// set_trap_gate()与set_system_gate()的主要区别在于前者设置的特权级为0,后者是3。因此
// 断点陷阱中断int3、溢出中断overflow 和边界出错中断bounds 可以由任何程序产生。
// 这两个函数均是嵌入式汇编宏程序(include/asm/system.h,第36 行、39 行)。
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);// 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3);
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 下面将int17-48 的陷阱门先均设置为reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);// 设置协处理器的陷阱门。
outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。
outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯片的IRQ13 中断请求。
set_trap_gate(39,¶llel_interrupt);// 设置并行口的陷阱门。
}
set_trap_gate的定义:
设置中断门函数。 // 参数:n - 中断号;addr - 中断程序偏移地址。 // &idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是14,特权级是0。 #define set_intr_gate(n,addr) _set_gate((unsigned long*)(&(idt[n])),14,0,(unsigned long)addr) 设置陷阱门函数。 // 参数:n - 中断号;addr - 中断程序偏移地址。 // &idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是15,特权级是0。 #define set_trap_gate(n,addr) _set_gate((unsigned long*)(&(idt[n])),15,0,(unsigned long)addr) 设置系统调用门函数。 // 参数:n - 中断号;addr - 中断程序偏移地址。 // &idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是15,特权级是3。 #define set_system_gate(n,addr) _set_gate((unsigned long*)(&(idt[n])),15,3,(unsigned long)addr)
可以看到,上面三个门都应用了同一个设置函数_set_gate:
设置门描述符宏函数。
// 参数:gate_addr -描述符地址;type -描述符中类型域值;dpl -描述符特权层值;addr -偏移地址。
// %0 - (由dpl,type 组合成的类型标志字);%1 - (描述符低4 字节地址);
// %2 - (描述符高4 字节地址);%3 - edx(程序偏移地址addr);%4 - eax(高字中含有段选择符)。
void _inline _set_gate(unsigned long *gate_addr,
unsigned short type,
unsigned short dpl,
unsigned long addr)
{// c语句和汇编语句都可以通过
gate_addr[0] = 0x00080000 + (addr & 0xffff);
gate_addr[1] = 0x8000 + (dpl << 13) + (type << 8) + (addr & 0xffff0000);
}
关于门描述符的讲解参考博客1
当然,参考博客1中的门描述符格式与代码中有些不一致,具体格式参考参考博客2。
关于块设备见参考博客3,当然,本文对硬件驱动并不关心,不过,如果需要做嵌入式系统移植的话,就需要很关心这些硬件初始化函数。
还有门的定义参考博客4。
sched_init()操作系统的初始化以创建0号和1号进程为结束标志,这里需要关注一下0号进程的手动初始化:
// 全局表中第1 个任务状态段(TSS)描述符的选择符索引号。
#define FIRST_TSS_ENTRY 4
// 全局表中第1 个局部描述符表(LDT)描述符的选择符索引号。
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
// 宏定义,计算在全局表中第n 个任务的TSS 描述符的索引号(选择符)。
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// 宏定义,计算在全局表中第n 个任务的LDT 描述符的索引号。
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
// 宏定义,加载第n 个任务的任务寄存器tr。
//#define ltr(n) __asm__( "ltr %%ax":: "a" (_TSS(n)))
_inline void ltr(unsigned long n)
{
n=_TSS(n);
_asm{
ltr word ptr n
}
}
// 宏定义,加载第n 个任务的局部描述符表寄存器ldtr。
//#define lldt(n) __asm__( "lldt %%ax":: "a" (_LDT(n)))
_inline void lldt(unsigned long n)
{
n=_LDT(n);
_asm{
lldt word ptr n
}
}
......
// 调度程序的初始化子程序。
void sched_init (void)
{
int i;
struct desc_struct *p; // 描述符表结构指针。
if (sizeof (struct sigaction) != 16) // sigaction 是存放有关信号状态的结构。
panic ("Struct sigaction MUST be 16 bytes");
// 设置初始任务(任务0)的任务状态段描述符和局部数据表描述符(include/asm/system.h,65)。
set_tss_desc (gdt + FIRST_TSS_ENTRY, &(init_task.task.tss));
set_ldt_desc (gdt + FIRST_LDT_ENTRY, &(init_task.task.ldt));
// 清任务数组和描述符表项(注意i=1 开始,所以初始任务的描述符还在)。
p = gdt + 2 + FIRST_TSS_ENTRY;
for (i = 1; i < NR_TASKS; i++)
{
task[i] = NULL;
p->a = p->b = 0;
p++;
p->a = p->b = 0;
p++;
}
// NT 标志用于控制程序的递归调用(Nested Task)。当NT 置位时,那么当前中断任务执行
// iret 指令时就会引起任务切换。NT 指出TSS 中的back_link 字段是否有效。
// __asm__ ("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位NT 标志。
_asm pushfd; _asm and dword ptr ss:[esp],0xffffbfff; _asm popfd;
ltr (0); // 将任务0 的TSS 加载到任务寄存器tr。
lldt (0); // 将局部描述符表加载到局部描述符表寄存器。
// 注意!!是将GDT 中相应LDT 描述符的选择符加载到ldtr。只明确加载这一次,以后新任务
// LDT 的加载,是CPU 根据TSS 中的LDT 项自动加载。
// 下面代码用于初始化8253 定时器。
outb_p (0x36, 0x43);
outb_p (LATCH & 0xff, 0x40); // 定时值低字节。
outb (LATCH >> 8, 0x40); // 定时值高字节。
// 设置时钟中断处理程序句柄(设置时钟中断门)。
set_intr_gate (0x20, &timer_interrupt);
// 修改中断控制器屏蔽码,允许时钟中断。
outb (inb_p (0x21) & ~0x01, 0x21);
// 设置系统调用中断门。
set_system_gate (0x80, &system_call);
}
TSS用于保存程序运行时一些寄存器的值,没有struct i387_struct i387;的话刚好104个字节(在sched.h的tss_struct里,我亲自数了数,刚好27个long),但我并没有找到i387的作用是什么。TSS段描述符见《设计艺术》P69。
进程描述结构体在sched.h的task_struct里,不过它们的存储位置在操作系统数据段。
关于task_union,它在sched.c中:
union task_union
{ // 定义任务联合(任务结构成员和stack 字符数组程序成员)。
struct task_struct task; // 因为一个任务数据结构与其堆栈放在同一内存页中,所以
char stack[PAGE_SIZE]; // 从堆栈段寄存器ss 可以获得其数据段选择符。
};
我的理解是,分配了一个页的大小,但是只有前956B作为任务结构体的init,剩下的部分作为内核的堆栈,这里堆栈的长度被精心规划,以避免覆盖任务结构。
但是我感觉并没有看明白的一点是,如果task[64]中的每一项都是956B,那么总共岂不是达到近64KB的存储空间,但上图中内核的数据区似乎不大?这个坑以后来填。
最后创建进程:
// 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0。
move_to_user_mode(); // 移到用户模式。(include/asm/system.h)
if (!fork()) {
init();
}
for(;;) pause();
} // end main
这里move_to_user_mode()也值得关注
_asm {
_asm mov eax,esp
_asm push 00000017h
_asm push eax
_asm pushfd
_asm push 0000000fh
_asm push offset l1
_asm iretd
_asm l1: mov eax,17h
_asm mov ds,ax
_asm mov es,ax
_asm mov fs,ax
_asm mov gs,ax
}
由于是0进程(其他进程创建的时候只要fork父进程就行了),所以需要手动创建。
另外,linux是只允许进程在特权级为3的情况下fork新的进程,所以在中断返回时(这是特权级翻转的常用手段,低特权向高特权则需采用系统调用(int 80h)来实现),0号进程的特权级为3。特权级翻转的一些细节见参考博客5。
其余初始化函数在《设计艺术》的P59~79都有简要介绍,在此不作赘述。
init()void init(void)
{
int pid,i;
// 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。
// 该函数是在25 行上的宏定义的,对应函数是sys_setup(),在kernel/blk_drv/hd.c。
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 spacenr",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE); // 打印缓冲区块数和总字节数,每块1024 字节。
printf("Free mem: %d bytesnr",memory_end-main_memory_start);//空闲内存字节数。
// 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值,
// 对于原(父进程)将返回子进程的进程号。所以if (!(pid=fork())) {...} 内是子进程执行的内容。
// 该子进程关闭了句柄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)。
execve("/bin/sh",argv_rc,envp_rc); // 装入/bin/sh 程序并执行。(/lib/execve.c) argv_rc参数 envp_rc 环境变量
_exit(2); // 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。
}
// 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的
// 进程号(pid)。这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的
// 位置。如果wait()返回值不等于子进程号,则继续等待。
if (pid>0)
while (pid != wait(&i))
{ ;}
// --
// 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建
// 一个子进程,如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对
// 于所创建的子进程关闭所有以前还遗留的句柄(stdin, stdout, stderr),新创建一个
// 会话并设置进程组号,然后重新打开/dev/tty0 作为stdin,并复制成stdout 和stderr。
// 再次执行系统解释程序/bin/sh。但这次执行所选用的参数和环境数组另选了一套(见上面)。
// 然后父进程再次运行wait()等待。如果子进程又停止了执行,则在标准输出上显示出错信息
// “子进程pid 停止了运行,返回码是i”,
// 然后继续重试下去…,形成“大”死循环。
while (1) {
if ((pid=fork())<0) {
printf("Fork failed in initrn");
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("nrchild %d died with code %04xnr",pid,i);
sync();
}
_exit(0);
}
这里的逻辑很简单,一直在循环创建1号进程(众所周知,1号进程是0号进程以外的所有进程的父进程)
"/etc/rc"是系统配置文件,挂接文件设置。
execve("/bin/sh",argv_rc,envp_rc);则是执行shell程序。
exit(0)与_exit(0)见参考博客6,虽然我还是没搞明白啥时候该用哪个。
如此一来,linux的启动部分的笔记暂时告一段落,由于本人的水平实在有限,多有谬误和不全之处。另外,光看代码是没用的,自己上手操作和改动才有好的效果,目前能找到的lab只有mit 6.s081,过段时间看完进程和文件管理篇试着做做吧~



