Linux内核阅读1·启动与初始化(2)

博客主要为《Linux内核设计的艺术》(以下简称《设计艺术》)和《Linux内核完全注释》(以下简称《完全注释》),以及非常好的Linux内核视频 - Linux内核精讲内容的搬运和阅读笔记,以及相关博客链接的整理。代码来源于《完全注释》配套代码。
写着玩儿的,如有错误,欢迎指正。

main.c

main.c调用了大量初始化函数。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

main()

让我们来到一切的起点,来看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,&divide_error);// 设置除操作出错的中断向量值。以下雷同。
	set_trap_gate(1,&debug);
	set_trap_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);  //第3特权级
	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,&parallel_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);
/*	unsigned short tmp = 0x8000 + (dpl << 13) + (type << 8);
	_asm mov eax,00080000h ;
	_asm mov edx,addr ;
	_asm mov ax,dx ;// 将偏移地址低字与段选择符组合成描述符低4 字节(eax)。
	_asm mov dx,tmp ;// 将类型标志字与偏移高字组合成描述符高4 字节(edx)。
	_asm mov ebx,gate_addr
	_asm mov [ebx],eax ;// 分别设置门描述符的低4 字节和高4 字节。
	_asm mov [ebx+4],edx ;*/
}

关于门描述符的讲解参考博客1
当然,参考博客1中的门描述符格式与代码中有些不一致,具体格式参考参考博客2
一些地方将系统调用与陷阱混为一谈,感觉在这里是不合理的。另外在_set_gate中我没有看到对IF位的设置,不知道应该在哪个位置?

关于块设备见参考博客3,当然,本文对硬件驱动并不关心,不过,如果需要做嵌入式系统移植的话,就需要很关心这些硬件初始化函数。

还有门的定义参考博客4

sched_init()

操作系统的初始化以创建0号和1号进程为结束标志,这里需要关注一下0号进程的手动初始化:

/*
* 寻找第1 个TSS 在全局表中的入口。0-没有用nul,1-代码段cs,2-数据段ds,3-系统段syscall
* 4-任务状态段TSS0,5-局部表LTD0,6-任务状态段TSS1,等。
*/
// 全局表中第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,这样以后就不会有麻烦 */
// 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);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p (LATCH & 0xff, 0x40);	/* LSB */// 定时值低字节。
	outb (LATCH >> 8, 0x40);	/* MSB */// 定时值高字节。
  // 设置时钟中断处理程序句柄(设置时钟中断门)。
	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里,不过它们的存储位置在操作系统数据段。

set_tss_desc和set_ldt_desc是在设置gdt的项,指向了任务结构体里tss和ldt的位置,但是我没有弄明白的是,这里的&(init_task.task.tss)所指的是虚拟地址还是物理地址?目前所知的是,在C语言中,&a往往是指虚拟地址,但并没有找到在内核中的指向。

关于task_union,它在kernel\sched.c中:

union task_union
{				// 定义任务联合(任务结构成员和stack 字符数组程序成员)。
	struct task_struct task;	// 因为一个任务数据结构与其堆栈放在同一内存页中,所以
	char stack[PAGE_SIZE];	// 从堆栈段寄存器ss 可以获得其数据段选择符。
};

我的理解是,分配了一个页的大小,但是只有前956B作为任务结构体的init,剩下的部分作为内核的堆栈,这里堆栈的长度被精心规划,以避免覆盖任务结构。
在这里插入图片描述

但是我感觉并没有看明白的一点是,如果task[64]中的每一项都是956B,那么总共岂不是达到近64KB的存储空间,但上图中内核的数据区似乎不大?另外task_union被分配的地址是谁来控制的呢?这些坑以后来填。

最后创建进程:

// 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0。
	move_to_user_mode();	// 移到用户模式。(include/asm/system.h)
	if (!fork()) {		/* we count on this going ok */
		init();
	}
	/*
 * 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
 * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任
 * 务0 在任何空闲时间里都会被激活(当没有其它任务在运行时),
 * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运行,如果没
 * 有的话我们就回到这里,一直循环执行'pause()'。
 */
	for(;;) pause();
} // end main

这里move_to_user_mode()也值得关注

_asm { \
	_asm mov eax,esp /* 保存堆栈指针esp 到eax 寄存器中。*/\
	_asm push 00000017h /* 首先将堆栈段选择符(SS)入栈。*/\
	_asm push eax /* 然后将保存的堆栈指针值(esp)入栈。*/\
	_asm pushfd /* 将标志寄存器(eflags)内容入栈。*/\
	_asm push 0000000fh /* 将内核代码段选择符(cs)入栈。*/\
	_asm push offset l1 /* 将下面标号l1 的偏移地址(eip)入栈。*/\
	_asm iretd /* 执行中断返回指令,则会跳转到下面标号1 处。*/\
_asm l1: mov eax,17h /* 此时开始执行任务0,*/\
	_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 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 值,
// 对于原(父进程)将返回子进程的进程号。所以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))
		{	/* nothing */;}

// --
// 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建
// 一个子进程,如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对
// 于所创建的子进程关闭所有以前还遗留的句柄(stdin, stdout, stderr),新创建一个
// 会话并设置进程组号,然后重新打开/dev/tty0 作为stdin,并复制成stdout 和stderr。
// 再次执行系统解释程序/bin/sh。但这次执行所选用的参数和环境数组另选了一套(见上面)。
// 然后父进程再次运行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() */
}

这里的逻辑很简单,1号进程一直在循环创建2号进程(众所周知,1号进程是0号进程以外的所有进程的父进程)

while (pid != wait(&i))应该是在处理孤儿进程。

"/etc/rc"是系统配置文件,挂接文件设置。

execve("/bin/sh",argv_rc,envp_rc);则是执行shell程序,这部分参考《完全注释》P247。

exit(0)与_exit(0)见参考博客6,虽然我还是没搞明白啥时候该用哪个。

如此一来,linux的启动部分的笔记暂时告一段落,由于本人的水平实在有限,多有谬误和不全之处。另外,光看代码是没用的,自己上手操作和改动才有好的效果,目前能找到的lab只有mit 6.s081,过段时间看完进程和文件管理篇试着做做吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值