区块链技术博客
www.b2bchain.cn

国科大操作系统思考题答案总结

这篇文章主要介绍了国科大操作系统思考题答案总结,通过具体代码讲解7730并且分析了国科大操作系统思考题答案总结的详细步骤与相关技巧,需要的朋友可以参考下

本文实例讲述了国科大操作系统思考题答案总结。分享给大家供大家参考文章查询地址https://www.b2bchain.cn/?p=7730。具体如下:


1.为什么开始启动计算机的时候,执行的是BIOS代码而不是操作系统自身的代码?(P1)

  • 答:加电的一瞬间,计算机内存中,准确的说是RAM中,中空空如也,什么程序也没有。软盘里虽然有操作系统程序,但CPU的逻辑电路被设计为只能运行内存中的程序,没有能力直接从软盘运行操作系统。这就需要硬件主动加载0xffff0处的BIOS程序,由BIOS准备好中断向量表、中断服务程序,接着通过中断“int 0x19”将引导程序bootsect加载至内存,以及后续的一系列操作,最终操作系统自身代码才能位于内存中,被CPU执行。

2.为什么BIOS只加载了一个扇区,后续扇区却是由bootsect代码加载?为什么BIOS没有直接把所有需要加载的扇区都加载?(P6)

  • 答:BIOS和操作系统通常由不同的专业团队开发的,为了能协调工作,对BIOS而言,“约定”接到启动操作系统命令,“定位识别”只从启动扇区把代码加载至0x7c00(BOOTSEG)位置,至于该扇区内容是什么,一概不管。BIOS程序是固化在主板ROM中,ROM内容一般无法改变,为保证其正确性以及生产、维护成本,BIOS只做必要工作。

3.为什么BIOS把bootsect加载到0x07c00,而不是0x00000?加载后又马上挪到0x90000处,是何道理?为什么不一次加载到位?(p5-17)

  • 答:其一,0x07c00是历史约定。其二,0x000000为BIOS中断向量表位置,而后续一段时间内用的都还是BIOS中断,所以不能将其覆盖。其三,挪到0x90000处是操作系统内存规划行为,主要为了避免在内核system占据0x000000处时可能将0x07c00(bootsect)覆盖,造成在main中设置根设备时取不到正确数据。

4.bootsect、setup、head程序之间是怎么衔接的?给出代码证据。(P15,P26)

  • 答:A)bootsect跳转至setup程序:jmpi 0, SETUPSEG;
    解释:通过BIOS的“int 0x13”中断,找到bootsect自身的中断服务程序,将setup加载至SETUPSEG(0x90200)处。同样手法,将system加载至SYSSEG(0x10000)处。bootsect程序任务都已经完成。然后,通过“jmpi 0, SETUPSEG”跳转至setup程序的加载位置,此时CS:IP指向setup程序的第一条指令。
    B)setup跳转至head程序:jmpi 0, 8
    解释:setup通过BIOS提供的中断服务程序提取了系统数据,存储在原来的bootsect位置只保留最后2字节未被覆盖(0x901fc,根设备号)。接着,将IF至0,完成关中断操作。然后,将system移动到0x00000位置,此时head已经占据了0x00000处,同时BIOS中断向量表彻底被覆盖。为此,setup开始为保护模式做准备,设置GDT、IDT并用CPU中专用寄存器IDTR、GDTR看住。接着,打开A20,也就是32位寻址模式,再对可编程中断控制器8259A进行重新编程,并置PE位为1,即设定处理器工作方式为保护模式,以后根据GDT决定执行哪里的程序。最后,通过“jmpi 0,8”跳转到head。“0”表示段内偏移,“8(1000)”是保护模式下的段选择符,最后两位“00”表示内核态,第二位“0”表示GDT,第一位“1”表示GDT表中GDT[1]项(内核代码段),从该项中得知段基址为0x00000000。结合上述偏移0,可知最终跳转至0x0000000处,执行head程序。

5.setup程序的最后是jmpi 0,8 ,为什么这个8不能简单的当作阿拉伯数字8看待,究竟有什么内涵?(P25)

  • 答:此时,工作在32位保护模式下,“0”表示段内偏移,“8(1000)”是段选择符,需要当二进制来看。最后两位“00”表示内核态,如为“11”则表示用户态;第二位“0”表示GDT,如为“1”则表示LDT;最前面的“1”表示GDT [1]。最后从响应的位置(如,GDT[1])获取段基址、限长等内容。

6.保护模式在“保护”什么?它的“保护”体现在哪里?特权级的目的和意义是什么?分页有“保护”作用吗?(P436-P439、P443)

  • 答:其一,保护操作系统不受恶意侵害。其二,主要体现在利用保护和分页、特权级、中断等技术依托CPU提供的硬件机制,对进程调度、内存管理、文件系统等方面进行保护。其三,为了更好的管理资源并保护系统不受侵害,操作系统利用先机,以时间换取特权,先霸占所有特权;依托CPU提供的保护模式,着眼于“段”,在所有的段选择符最后两位标示特权级,禁止用户执行那些至关重要的指令。其四,对于分页来说,用户进程只能使用逻辑地址,而逻辑地址要经过内核转化为线性地址,实现了用户进程不可能访问内核地址,也不能进程间相互访问,起到保护作用。

7.在setup程序里曾经设置过gdt,为什么在head程序中将其废弃,又重新设置了一个?为什么设置两次,而不是一次搞好?(P33)

  • 答:原来GDT所在的位置是设计代码时在setup.s里面设置的数据,将来这个setup模块所在的内存位置会在设计缓冲区时被覆盖。如果不改变位置,将来GDT的内容肯定会被缓冲区覆盖掉,从而影响系统的运行。这样一来,将来整个内存空间中唯一安全的地方就是现在head.s所在的位置了。那么有没有可能在执行setup程序时直接把GDT的内容复制到head.s所在的位置呢?肯定不能。如果先复制GDT内容,后移动system模块,它就会被后者覆盖;如果先移动system模块,后复制GDT内容,它又会把head.s对应的程序覆盖,而这时head.s还没有执行。所以,无论如何,都要重新建立GDT。

8.进程0的task_struct在哪?具体内容是什么?(P70)

  • 答:内核数据段。具体内容包括状态、信号、pid、alarm、ldt、tss等管理该进程所需的数据。
    linux0.11includelinuxsched.h #define INIT_TASK  /* state etc */	{ 0,15,15,  /* signals */	0,{{},},0,  /* ec,brk... */	0,0,0,0,0,0,  /* pid etc.. */	0,-1,0,0,0,  /* uid etc */	0,0,0,0,0,0,  /* alarm */	0,0,0,0,0,0,  /* math */	0,  /* fs info */	-1,0022,NULL,NULL,NULL,0,  /* filp */	{NULL,},  	{  		{0,0},  /* ldt */	{0x9f,0xc0fa00},  		{0x9f,0xc0f200},  	},  /*tss*/	{0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir, 	 0,0,0,0,0,0,0,0,  	 0,0,0x17,0x17,0x17,0x17,0x17,0x17,  	 _LDT(0),0x80000000,  		{}  	},  } linux0.11kernelsched.c struct task_struct *task[NR_TASKS] = { &(init_task.task), }; 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

9.内核的线性地址空间是如何分页的?画出从0x000000开始的7个页(包括页目录表、页表所在页)的挂接关系图,就是页目录表的前四个页目录项、第一个个页表的前7个页表项指向什么位置?给出代码证据。(P39)

  • 答:图参考P39。
    注意,页目录表需指向全部页表;页表需要指向全部页;页目录表、页表本身也是页。
    linux0.11boothead.s setup_paging: 	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */ 	xorl %eax,%eax 	xorl %edi,%edi			/* pg_dir is at 0x000 */ 	cld;rep;stosl 	movl $pg0+7,_pg_dir		/* set present bit/user r/w */ 	movl $pg1+7,_pg_dir+4		/*  --------- " " --------- */ 	movl $pg2+7,_pg_dir+8		/*  --------- " " --------- */ 	movl $pg3+7,_pg_dir+12		/*  --------- " " --------- */ 	movl $pg3+4092,%edi 	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */ 	std 1:	stosl			/* fill pages backwards - more efficient :-) */ 	subl $0x1000,%eax 	jge 1b 	xorl %eax,%eax		/* pg_dir is at 0x0000 */ 	movl %eax,%cr3		/* cr3 - page directory start */ 	movl %cr0,%eax 	orl $0x80000000,%eax 	movl %eax,%cr0		/* set paging (PG) bit */ 	ret			/* this also flushes prefetch-queue */	 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

10.在head程序执行结束的时候,在idt的前面有184个字节的head程序的剩余代码,剩余了什么?为什么要剩余?(P31、P36、P40)

  • 答:剩余内容0x054b8~0x05400处,包含了after_page_tables、ignore_int中断服务程序和setup_paging设置分页的代码。after_page_tables中压入了一些参数,为内核进入main函数跳转做准备,为了谨慎起见,设计者在栈中压入了L6,以使得系统可能出错时,返回L6处执行;ignore_int是IDT的默认初始化值,既可以防止无意中覆盖代码或数据而引起的逻辑混乱,也可以对开发过程中的误操作给出及时的提示;setup_paging在分页完成前不能被覆盖。

11.为什么不用call,而是用ret“调用”main函数?画出调用路线图,给出代码证据。(P42)

  • 答:参考课本P42页。
    linux0.11boothead.s after_page_tables: 	pushl $0		# These are the parameters to main :-) 	pushl $0 	pushl $0 	pushl $L6		# return address for main, if it decides to. 	pushl $_main 	jmp setup_paging L6: 	jmp L6			# main should never return here, but 				# just in case, we know what happens. 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

12.用文字和图说明中断描述符表是如何初始化的,可以举例说明(比如:set_trap_gate(0,&divide_error)),并给出代码证据。(P52、P55)

  • 答:对中断描述符表的初始化,就是将中断、异常处理的服务程序与IDT进行挂接,逐步重建中断服务体系。 set_trap_gate(0,&divide_error); //除零错误
    linux0.11includeasmsystem.h #define set_trap_gate(n,addr)  	_set_gate(&idt[n],15,0,addr) #define _set_gate(gate_addr,type,dpl,addr)  __asm__ ("movw %%dx,%%axnt"  	"movw %0,%%dxnt"  	"movl %%eax,%1nt"  	"movl %%edx,%2"  	:  	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))),  	"o" (*((char *) (gate_addr))),  	"o" (*(4+(char *) (gate_addr))),  	"d" ((char *) (addr)),"a" (0x00080000)) 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    可以看出,n是0;gate_addr是&idt[0],也就是IDT的第一项中断描述符的地址;type是15;dpl(描述符特权级)是0;addr是中断服务程序divide_error(void)的入口地址。

13.在IA-32中,有大约20多个指令是只能在0特权级下使用,其他的指令,比如cli,并没有这个约定。奇怪的是,在Linux0.11中,3特权级的进程代码并不能使用cli指令,这是为什么?请解释并给出代码证据。 (P68、P79、P92)

  • 答:根据Intel Manual,cli和sti指令与CPL和EFLAGS[IOPL]有关。cli,如果CPL的权限高于等于EFLAGS中的IOPL的权限,即数值上CPL<=IOPL,则IF位清除为0,否则它不受影响。如果CPL大于当前程序或过程的IOPL,则产生保护模式异常。由于在内核中IOPL的值初始为0,且未经改变。INIT_TASK的TSS中设置了EFLAGS值,进程0又在move_to_user_mode中,继承了内核的EFLAGS。
    linux0.11includelinuxsched.h #define INIT_TASK  //.. /*tss*/	{0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir, 	 0,0,0,0,0,0,0,0,      //eflags的值,决定了cli这类指令只能在0特权级使用 	 0,0,0x17,0x17,0x17,0x17,0x17,0x17,  	 _LDT(0),0x80000000,  		{}  	},  } linux0.11includeasmsystem.h #define move_to_user_mode()  __asm__ ("movl %%esp,%%eaxnt"  	"pushl $0x17nt"  	"pushl %%eaxnt"  	"pushflnt"    //eflags进栈 	"pushl $0x0fnt"  	"pushl $1fnt"  	"iretn"  	//.. 	:::"ax") 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    而进程1在copy_process中TSS里,设置了EFLAGS的IOPL位为0。总之,通过设置IOPL,可以限制3特权级的进程代码使用cli。

    linux0.11kernelfork.c int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 		long ebx,long ecx,long edx, 		long fs,long es,long ds, 		long eip,long cs,long eflags,long esp,long ss) { 	//… 	p->tss.eip = eip; 	p->tss.eflags = eflags; 	p->tss.eax = 0; 	//… 	return last_pid; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

14.进程0的task_struct在哪?具体内容是什么?给出代码证据。

  • 答:同第8题。(题目重复)

15.在system.h里

linux0.11includeasmsystem.h #define _set_gate(gate_addr,type,dpl,addr)  __asm__ ("movw %%dx,%%axnt"  	"movw %0,%%dxnt"  	"movl %%eax,%1nt"  	"movl %%edx,%2"  	:  	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))),  	"o" (*((char *) (gate_addr))),  	"o" (*(4+(char *) (gate_addr))),  	"d" ((char *) (addr)),"a" (0x00080000)) 

#define set_intr_gate(n,addr)
_set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr)
_set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr)
_set_gate(&idt[n],15,3,addr)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这里中断门、陷阱门、系统调用都是通过_set_gate设置的,用的是同一个嵌入汇编代码,比较明显的差别是dpl一个是3,另外两个是0,这是为什么?说明理由。(P55)

  • 答:set_trap_gate的dpl是0,而set_system_gate的dpl是3。dpl为0的意思是只能由内核处理,dpl为3的意思是系统调用可以由3特权级(也就是用户特权级)调用。

16.进程0 fork进程1之前,为什么先调用move_to_user_mode()?用的是什么方法?解释其中的道理。(P78)

  • 答:Linux规定,除了进程0外,所有进程都要由一个已有的进程在3特权级下创建,进程0此时处于0特权级。按照规定,在创建进程1之前要将进程0转变为3特权级。方法是调用move_to_user_mode()函数,模仿中断返回动作,实现进程0的特权级从0变为3。

17.在Linux操作系统中大量使用了中断、异常类的处理,究竟有什么好处?(P56)

  • 答:在未引入中断、异常处理类处理理念之前,CPU每隔一段时间就要对所有硬件进行轮询,以检测它的工作是否完成,如果没有完成就继续轮询,这样消耗了CPU处理用户程序的时间,降低了系统的综合效率。可见,CPU以“主动轮询”的方式来处理信号是非常不划算的。以“被动响应”模式代替“主动轮询”模式来处理主机与外设的I/O问题,是计算机历史上的一大进步。

18.copy_process函数的参数最后五项是:long eip,long cs,long eflags,long esp,long ss。查看栈结构确实有这五个参数,奇怪的是其他参数的压栈代码都能找得到,确找不到这五个参数的压栈代码,反汇编代码中也查不到,请解释原因。(P84)

  • 答:copy_process执行是因为进程调用了fork函数创建进程,会执行“int 0x80”产生一个软中断,中断使CPU硬件自动将ss,esp,eflags,cs,eip这几个寄存器的值按顺序压入进程0内核栈,又因为函数传递参数是使用栈的,所以刚好可以作为copy_process的最后五个参数。

19.分析get_free_page()函数的代码,叙述在主内存中获取一个空闲页的技术路线。(P89)

  • 答:遍历mem_map[],找到内存中(从高地址开始)第一个空闲(字节为0)页面,将其置为1。ecx左移12位加LOW_MEM得到该页的物理地址,并将页面清零。最后返回空闲页面物理内存的起始地址。
    linux0.11mmmemory.c unsigned long get_free_page(void) { register unsigned long __res asm("ax"); 

asm(“std ; repne ; scasbnt”
“jne 1fnt”
“movb $1,1(%%edi)nt”
“sall $12,%%ecxnt”
“addl %2,%%ecxnt”
“movl %%ecx,%%edxnt”
“movl $1024,%%ecxnt”
“leal 4092(%%edx),%%edint”
“rep ; stoslnt”
“movl %%edx,%%eaxn”
“1:”
:“=a” (__res)
:“0” (0),“i” (LOW_MEM),“c” (PAGING_PAGES),
“D” (mem_map+PAGING_PAGES1)
:“di”,“cx”,“dx”);
return __res;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

20.分析copy_page_tables()函数的代码,叙述父进程如何为子进程复制页表。(P97)

  • 答:进入copy_page_tables函数后,先为新的页表申请一个空闲页面,并把进程0中第一个页表里的前160个页表项复制到这个页面中(1个页表项控制一个页面4KB内存空间,160个页表项可以控制640KB内存空间)。进程0和进程1的页表暂时度指向了相同的页面,意味着进程1也可以操作进程0的页面。之后对进程1的页目录表进行设置。最后,用重置CR3的方法刷新页面变换高速缓存。进程1的页表和页目录表设置完毕。进程1此时是一个空架子,还没有对应的程序,它的页表又是从进程0的页表复制过来的,它们管理的页面完全一致,也就是它暂时和进程0共享一套页面管理结构。
    linux0.11mmmemory.c int copy_page_tables(unsigned long from,unsigned long to,long size) { 	unsigned long * from_page_table; 	unsigned long * to_page_table; 	unsigned long this_page; 	unsigned long * from_dir, * to_dir; 	unsigned long nr; 
    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>from<span class="token operator">&amp;</span><span class="token number">0x3fffff</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">(</span>to<span class="token operator">&amp;</span><span class="token number">0x3fffff</span><span class="token punctuation">)</span><span class="token punctuation">)</span> 	<span class="token function">panic</span><span class="token punctuation">(</span><span class="token string">"copy_page_tables called with wrong alignment"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> from_dir <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">unsigned</span> <span class="token keyword">long</span> <span class="token operator">*</span><span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>from<span class="token operator">&gt;&gt;</span><span class="token number">20</span><span class="token punctuation">)</span> <span class="token operator">&amp;</span> <span class="token number">0xffc</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">/* _pg_dir = 0 */</span> to_dir <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">unsigned</span> <span class="token keyword">long</span> <span class="token operator">*</span><span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>to<span class="token operator">&gt;&gt;</span><span class="token number">20</span><span class="token punctuation">)</span> <span class="token operator">&amp;</span> <span class="token number">0xffc</span><span class="token punctuation">)</span><span class="token punctuation">;</span> size <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token keyword">unsigned</span><span class="token punctuation">)</span> <span class="token punctuation">(</span>size<span class="token operator">+</span><span class="token number">0x3fffff</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">&gt;&gt;</span> <span class="token number">22</span><span class="token punctuation">;</span> <span class="token keyword">for</span><span class="token punctuation">(</span> <span class="token punctuation">;</span> size<span class="token operator">--</span><span class="token operator">&gt;</span><span class="token number">0</span> <span class="token punctuation">;</span> from_dir<span class="token operator">++</span><span class="token punctuation">,</span>to_dir<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 	<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">&amp;</span> <span class="token operator">*</span>to_dir<span class="token punctuation">)</span> 		<span class="token function">panic</span><span class="token punctuation">(</span><span class="token string">"copy_page_tables: already exist"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 	<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">&amp;</span> <span class="token operator">*</span>from_dir<span class="token punctuation">)</span><span class="token punctuation">)</span> 		<span class="token keyword">continue</span><span class="token punctuation">;</span> 	from_page_table <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">unsigned</span> <span class="token keyword">long</span> <span class="token operator">*</span><span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token number">0xfffff000</span> <span class="token operator">&amp;</span> <span class="token operator">*</span>from_dir<span class="token punctuation">)</span><span class="token punctuation">;</span> 	<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token punctuation">(</span>to_page_table <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">unsigned</span> <span class="token keyword">long</span> <span class="token operator">*</span><span class="token punctuation">)</span> <span class="token function">get_free_page</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> 		<span class="token keyword">return</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>	<span class="token comment">/* Out of memory, see freeing */</span> 	<span class="token operator">*</span>to_dir <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token keyword">unsigned</span> <span class="token keyword">long</span><span class="token punctuation">)</span> to_page_table<span class="token punctuation">)</span> <span class="token operator">|</span> <span class="token number">7</span><span class="token punctuation">;</span> 	nr <span class="token operator">=</span> <span class="token punctuation">(</span>from<span class="token operator">==</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token operator">?</span><span class="token number">0xA0</span><span class="token punctuation">:</span><span class="token number">1024</span><span class="token punctuation">;</span> 	<span class="token keyword">for</span> <span class="token punctuation">(</span> <span class="token punctuation">;</span> nr<span class="token operator">--</span> <span class="token operator">&gt;</span> <span class="token number">0</span> <span class="token punctuation">;</span> from_page_table<span class="token operator">++</span><span class="token punctuation">,</span>to_page_table<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 		this_page <span class="token operator">=</span> <span class="token operator">*</span>from_page_table<span class="token punctuation">;</span> 		<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">&amp;</span> this_page<span class="token punctuation">)</span><span class="token punctuation">)</span> 			<span class="token keyword">continue</span><span class="token punctuation">;</span> 		this_page <span class="token operator">&amp;</span><span class="token operator">=</span> <span class="token operator">~</span><span class="token number">2</span><span class="token punctuation">;</span> 		<span class="token operator">*</span>to_page_table <span class="token operator">=</span> this_page<span class="token punctuation">;</span> 		<span class="token keyword">if</span> <span class="token punctuation">(</span>this_page <span class="token operator">&gt;</span> LOW_MEM<span class="token punctuation">)</span> <span class="token punctuation">{</span> 			<span class="token operator">*</span>from_page_table <span class="token operator">=</span> this_page<span class="token punctuation">;</span> 			this_page <span class="token operator">-</span><span class="token operator">=</span> LOW_MEM<span class="token punctuation">;</span> 			this_page <span class="token operator">&gt;&gt;=</span> <span class="token number">12</span><span class="token punctuation">;</span> 			mem_map<span class="token punctuation">[</span>this_page<span class="token punctuation">]</span><span class="token operator">++</span><span class="token punctuation">;</span> 		<span class="token punctuation">}</span> 	<span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token function">invalidate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span> 

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

21.进程0创建进程1时,为进程1建立了task_struct及内核栈,第一个页表,分别位于物理内存16MB顶端倒数第一页、第二页。请问,这两个页究竟占用的是谁的线性地址空间,内核、进程0、进程1、还是没有占用任何线性地址空间?说明理由(可以图示)并给出代码证据。(P39、P68、P97)

  • 答:这两个页占用的是内核的线性地址空间。
    linux0.11boothead.s setup_paging: 	//… 	movl $pg3+4092,%edi 	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */ 	std 1:	stosl			/* fill pages backwards - more efficient :-) */ 	subl $0x1000,%eax 	//… 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    内核的线性地址空间为0x00000~0xfffff(16M),且线性地址与物理地址一一对应。为进程1分配的这两个页,在16MB的顶端倒数第一页、第二页,因此占用内核线性地址空间。进程0的线性地址空间是内存的前640KB,因为进程0的LDT中的limit属性限制了进程0能够访问的地址空间。进程1拷贝了进程0的页表(前160项),而这160个页表项即为内核第一页表的前160项,指向的是物理内存前640KB,因此无法访问到16MB的顶端倒数的两个页面。

22.假设:经过一段时间的运行,操作系统中已经有5个进程在运行,且内核分别为进程4、进程5分别创建了第一个页表,这两个页表在谁的线性地址空间?用图表示这两个页表在线性地址空间和物理地址空间的映射关系。(P266、P270)

  • 答:这两个页面均占用内核的线性地址空间。既然是内核线性地址空间,则与物理地址空间为一一对应关系。根据每个进程占用16个页目录表项,则进程4占用从第65~81项的页目录表项。同理,进程5占用第81~96项的页目录表项。由于目前只分配了一个页面(用做进程的第一个页表),则分别只需要使用第一个页目录表项即可。

23.linux0.11includelinuxsched.h

#define switch_to(n) { struct {long a,b;} __tmp;  __asm__("cmpl %%ecx,_currentnt"  	"je 1fnt"  	"movw %%dx,%1nt"  	"xchgl %%ecx,_currentnt"  	"ljmp %0nt"  	"cmpl %%ecx,_last_task_used_mathnt"  	"jne 1fnt"  	"cltsn"  	"1:"  	::"m" (*&__tmp.a),"m" (*&__tmp.b),  	"d" (_TSS(n)),"c" ((long) task[n]));  } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

代码中的”ljmp %0nt” 很奇怪,按理说jmp指令跳转到得位置应该是一条指令的地址,可是这行代码却跳到了”m” (*&__tmp.a),这明明是一个数据的地址,更奇怪的,这行代码竟然能正确执行。请论述其中的道理。(P107)

  • 答:ljmp %0nt通过任务门机制并未实际使用任务门,将CPU的各个寄存器值保存在进程0的TSS中,将进程1的TSS数据以LDT的代码段、数据段描述符数据恢复给CPU的各个寄存器,实现从0特权级的内核代码切换到3特权级的进程1代码执行。其中tss.eip也自然恢复给了CPU,此时EIP指向的就是fork中的if(__res >= 0)语句。

24.进程0开始创建进程1,调用fork(),跟踪代码时我们发现,fork代码执行了两次,第一次,执行fork代码后,跳过init()直接执行了for(;; ) pause(),第二次执行fork代码后,执行了init()。奇怪的是,我们在代码中并没有看到向转向fork的goto语句,也没有看到循环语句,是什么原因导致fork反复执行?请说明理由(可以图示),并给出代码证据。(P81)

  • 答:以下为相应源码证据。
    linux0.11kernelfork.c int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 		long ebx,long ecx,long edx, 		long fs,long es,long ds, 		long eip,long cs,long eflags,long esp,long ss) { 	//… 	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */ 	//… 	p->start_time = jiffies; 	p->tss.back_link = 0; 	p->tss.esp0 = PAGE_SIZE + (long) p; 	p->tss.ss0 = 0x10; 	p->tss.eip = eip; 	p->tss.eflags = eflags; 	p->tss.eax = 0; 	//… 	return last_pid; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    首先在copy_process()函数中,设置TSS“p->tss.eip = eip;”指向的是if (__res >= 0); 而“p->tss.eax = 0;”决定main()中if (!fork())后面的分支走向。

    linux0.11kernelsystem_call.s _system_call: 	//… 	call _sys_call_table(,%eax,4) 	pushl %eax 
    • 1
    • 2
    • 3
    • 4
    • 5

    接着,copy_process()函数返回后,通过“pushl %eax”将函数返回值,也就是进程1的进程号压栈。

    linux0.11initmain.c #define _syscall0(type,name)  type name(void)  {  long __res;  __asm__ volatile ("int $0x80"  	: "=a" (__res)  	: "0" (__NR_##name));  if (__res >= 0)  	return (type) __res;  errno = -__res;  return -1;  } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    “: “=a” (__res) ”将eax的值赋值给__res,所以“if (__res >= 0) ”实际上是看此时的eax时多少,由上可知,eax=1。

    linux0.11kernelsched.c void main(void)		/* This really IS void, no error here. */ {			/* The startup routine assumes (well, ...) this */ 	//… 	move_to_user_mode(); 	if (!fork()) {		/* we count on this going ok */ 		init(); 	} 	for(;;) pause(); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    回到if (!fork())处执行,!1为“假”,不会执行init(),直接执行“for(;; ) pause();”。

    linux0.11kernelsched.c int sys_pause(void) { 	current->state = TASK_INTERRUPTIBLE; 	schedule(); 	return 0; } linux0.11kernelsched.c void schedule(void) { 	//… 	switch_to(next); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    由pause()函数进入“schedule();”开始调度,然后通过“switch_to(next);”准备切换进程。

    linux0.11fsbuffer.c #define switch_to(n) { 	//… 	"ljmp %0nt"  	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    执行switch_to()函数中,当程序执行到“ljmp %0nt”这行时,ljmp通过CPU任务门机制自动将进程1的TSS值恢复给CPU,自然也将其中的tss.eip恢复给CPU,这时EIP指向fork的if(__res >= 0)这行。而此时的__res值就是进程1中TSS的eax的值,这个值在前面被写死为0,即“p->tss.eax = 0;”所以执行到“return (type)__res;”这行时,返回值为0。返回后,执行到if(!fork())这一行,!0为“真”,调用init()函数!

25、打开保护模式、分页后,线性地址到物理地址是如何转换的?(P260)

  • 答:打开保护模式、分页后,线性地址需要通过MMU进行解析,以页目录表、页表、页面三级映射模式映射到物理地址。具体转换过程是这样的:“每个线性地址值是32位,MMU按照10-10-12的长度来识别地址值,分别解析为页目录项号、页表项号、页面内偏移。CR3中存放着页目录表的基址,通过CR3找到页目录表,再找到页目录项,进而找到对应页表,寻取页表项,然后找到页面物理地址,最后加上12位页内偏移形成的地址,才为最终物理地址”。

26、getblk函数中,申请空闲缓冲块的标准就是b_count为0,而申请到之后,为什么在wait_on_buffer(bh)后又执行if(bh->b_count)来判断b_count是否为0?(P336、P349)

  • 答:以下为getblk()函数源码。
    linux0.11fsbuffer.c #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock) struct buffer_head * getblk(int dev,int block) { repeat: 	if (bh = get_hash_table(dev,block)) 		return bh; 	//… 	wait_on_buffer(bh); 	if (bh->b_count) 		goto repeat; 	//… 	return bh; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    字段b_count,用来标记“每个缓冲块有多少个进程在共享”。只有当b_count=0时,该缓冲块才能被再次分配。举个可能引发异常例子,每个缓冲块有一个进程等待队列,假设此时B、C两进程在队列中,当该缓冲块被解锁时,进程C被唤醒(它开始使用缓冲区之前需先唤醒进程B,使进程B从挂起进入就绪状态),将缓冲区加锁,一段时间后,进程C又被挂起,但此时缓冲区进程C任在使用。这时候,进程B被调度,“if (bh->b_count)”该缓冲区任是加锁状态,进程B重新选择缓冲区…如果,不执行该判断将造成进程B操作一个被加锁的缓冲区,引发异常。

27、b_dirt已经被置为1的缓冲块,同步前能够被进程继续读、写?给出代码证据。(P331)

  • 答:同步前能够被进程继续读、写,但不能挪为它用(即关联其它物理块)。b_dirt是针对硬盘方向的,进程与缓冲块方向由b_uptodate标识。只要缓冲块的b_dirt字段被设置为1,就是告诉内核,这个缓冲块中的内容已经被进程的方向数据改写了,最终需要同步到硬盘上。反之,如果为0,就不需要同步。
    linux0.11fsfile_dev.c int file_write(struct m_inode * inode, struct file * filp, char * buf, int count) { 	//… 	if (filp->f_flags & O_APPEND) 		pos = inode->i_size; 	else 		pos = filp->f_pos; 	while (i<count) { 		if (!(block = create_block(inode,pos/BLOCK_SIZE))) 			break; 		if (!(bh=bread(inode->i_dev,block))) 			break; 	//… } int file_read(struct m_inode * inode, struct file * filp, char * buf, int count) { 	//… 	if ((left=count)<=0) 		return 0; 	while (left) { 		if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) { 			if (!(bh=bread(inode->i_dev,nr))) 				break; 		}  	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    可见,读写文件均与b_dirt无关。

    linux0.11fsbuffer.c struct buffer_head * bread(int dev,int block) { 	struct buffer_head * bh; 	if (!(bh=getblk(dev,block))) 		panic("bread: getblk returned NULLn"); 	if (bh->b_uptodate) 		return bh; 	ll_rw_block(READ,bh); 	wait_on_buffer(bh); 	if (bh->b_uptodate) 		return bh; 	brelse(bh); 	return NULL; } linux0.11fsbuffer.c #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock) struct buffer_head * getblk(int dev,int block) { 	struct buffer_head * tmp, * bh; 

repeat:
if (bh = get_hash_table(dev,block))
return bh;
//…
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

在获取缓冲块时,亦与b_dirt无任何关系。

28、分析panic函数的源代码,根据你学过的操作系统知识,完整、准确的判断panic函数所起的作用。假如操作系统设计为支持内核进程(始终运行在0特权级的进程),你将如何改进panic函数?(该题有待讨论)

  • 答:该函数用来显示内核中出现的重大错误信息,并运行文件系统同步函数,然后进入死循环——死机。如果当前进程是任务0的话,还说明时交换任务出错,并且还没有运行系统同步函数。关键字volatile用于告诉gcc该函数不会返回,死机。
    linux0.11kernelpanic.c volatile void panic(const char * s) { 	printk("Kernel panic: %snr",s); 	if (current == task[0]) 		printk("In swapper task - not syncingnr"); 	else 		sys_sync(); 	for(;;); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    以上为panic函数源代码。如果设计为支持内核进程的话,可能牵涉到sti/cli相关内容。

    volatile void panic(const char * s) { 	printk("Kernel panic: %snr",s); 	if (current == task[0]) 		printk("In swapper task - not syncingnr"); 	else 		sys_sync(); 	for(;;); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

29、详细分析进程调度的全过程。考虑所有可能(signal、alarm除外)(P105)

  • 答:schedule()函数的主要过程为,首先依据task[64]这个结构,第一次遍历所有进程,只要地址指针不为空,就要针对它的signal、alarm分析,这里先不考虑。第二次遍历所有进程,比较进程的状态和时间片,找出处在就绪态且counter最大的进程。
    linux0.11kernelsched.c void schedule(void) { 	int i,next,c; 	struct task_struct ** p; /* check alarm, wake up any interruptible tasks that have got a signal */ 	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 		if (*p) { 			if ((*p)->alarm && (*p)->alarm < jiffies) { 					(*p)->signal |= (1<<(SIGALRM-1)); 					(*p)->alarm = 0; 				} 			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && 			(*p)->state==TASK_INTERRUPTIBLE) 				(*p)->state=TASK_RUNNING; 		} /* this is the scheduler proper: */ 	while (1) { 		c = -1; 		next = 0; 		i = NR_TASKS; 		p = &task[NR_TASKS]; 		while (--i) { 			if (!*--p) 				continue; 			if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 				c = (*p)->counter, next = i; 		} 		if (c) break; 		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 			if (*p) 				(*p)->counter = ((*p)->counter >> 1) + 						(*p)->priority; 	} 	switch_to(next); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    执行switch_to()函数中,ljmp %0nt通过任务门机制并未实际使用任务门,将CPU的各个寄存器值保存在进程0的TSS中,将进程1的TSS数据以LDT的代码段、数据段描述符数据恢复给CPU的各个寄存器,实现从0特权级的内核代码切换到3特权级的进程1代码执行。其中tss.eip也自然恢复给了CPU,此时EIP指向的就是fork中的if(__res >= 0)语句。

    linux0.11fsbuffer.c #define switch_to(n) { 	//… 	"ljmp %0nt"  	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

30、wait_on_buffer函数中为什么不用if()而是用while()?(P302)

  • 答:以下为wait_on_buffer函数源代码。
    linux0.11fsbuffer.c static inline void wait_on_buffer(struct buffer_head * bh) { 	cli(); 	while (bh->b_lock) 		sleep_on(&bh->b_wait); 	sti(); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    从上述源码中可知,一旦缓冲块被加锁,当前请求进程必被挂起在该缓冲块等待队列中,直到在某一时间被重新唤醒。这时候,缓冲块肯定已经被解锁了,但是可能被队列中其他进程又把该缓冲块给占用了。这时候使用while则可以再次判断该缓冲块是否被加锁,如果是,则继续被挂起,循环往复。

31、操作系统如何利用b_uptodate保证缓冲块数据的正确性?new_block (int dev)函数新申请一个缓冲块后,并没有读盘,b_uptodate却被置1,是否会引起数据混乱?详细分析理由。(P328)

  • 答:只要缓冲块的b_uptodate字段被设置为1,缓冲块的数据已经是数据块最新的,就可以放心的支持进程共享缓冲块的数据。反之,如果b_uptodate为0,就提醒内核缓冲块并没有用绑定的数据块中的数据更新,不支持进程共享该缓冲块。值得注意的是b_uptodate被设置为1,是告诉内核,缓冲块中的数据已经用数据块中的数据更新过了,但并不等于两者的数据就完全一致。如题中的,申请一个缓冲块后,并没有读盘,b_uptodate却被置1,这并不会引起数据混乱。这时因为只要为新建的数据块新申请了缓冲块,不管这个缓冲块将来用做什么,反正进程现在不需要里面的数据,干脆全部清零。这样不管与之绑定的数据块用来存储什么信息,都无所谓,将该缓冲块的b_uptodate置为1,更新问题“等效于”以解决。

32、add_request()函数中有下列代码

	linux0.11kernelblk_drvll_rw_blk.c 	if (!(tmp = dev->current_request)) { 		dev->current_request = req; 		sti(); 		(dev->request_fn)(); 		return; 	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

其中的

	if (!(tmp = dev->current_request)) { 		dev->current_request = req; 	} 
  • 1
  • 2
  • 3

是什么意思?(P322)

  • 答:查看指定设备是否有当前请求项,即查看设备是否忙。如果指定设备dev当前请求项(dev->current_request ==NULL) 为空,则表示目前设备没有请求项,本次是第1个请求项,也是唯一的一个。因此可将块设备当前请求指针直接指向该请求项,并立即执行相应设备的请求函数。

33、do_hd_request()函数中dev的含义始终一样吗?(P318)

  • 答:do_hd_request()函数主要用于处理当前硬盘请求项。但其中的dev含义并不一致。“dev = MINOR(CURRENT->dev);”表示取设备号中的子设备号。“dev /= 5;”此时,dev代表硬盘号(硬盘0还是硬盘1)。
    linux0.11kernelblk_drvhd.c void do_hd_request(void) { 	int i,r; 	unsigned int block,dev; 	//… 	dev = MINOR(CURRENT->dev); 	block = CURRENT->sector; 	if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) { 		end_request(0); 		goto repeat; 	} 	block += hd[dev].start_sect; 	dev /= 5; 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

34、read_intr()函数中,下列代码是什么意思?为什么这样做?(P323)

	linux0.11kernelblk_drvhd.c 	if (--CURRENT->nr_sectors) { 		do_hd = &read_intr; 		return; 	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 答:当读取扇区操作成功后,“—CURRENT->nr_sectors”将递减请求项所需读取的扇区数值。若递减后不等于0,表示本项请求还有数据没读完,于是再次置中断调用C函数指针“do_hd = &read_intr;”并直接返回,等待硬盘在读出另1扇区数据后发出中断并再次调用本函数。

35、bread()函数代码中为什么要做第二次if (bh->b_uptodate)判断?(P342)

	linux0.11fsbuffer.c 	if (bh->b_uptodate) 		return bh; 	ll_rw_block(READ,bh); 	wait_on_buffer(bh); 	if (bh->b_uptodate) 		return bh; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 答:bread()函数主要是从块设备上读取数据。调用底层ll_rw_block()函数,产生读设备请求。然后等待指定数据块读入,并等待缓冲块解锁。在睡眠醒来之后,如果缓冲块已更新“if (bh->b_uptodate)”,则返回缓冲块指针。否则,表明读设备操作失败,于是释放该缓冲块返回NULL。

36、getblk()函数中,两次调用wait_on_buffer()函数,两次的意思一样吗?(P372)

  • 答:不一样。第一处“wait_on_buffer(bh);”是已经找到一个比较合适的空闲缓冲块了,于是先等待缓冲块解锁。第二处“wait_on_buffer(bh);”是如果该缓冲区已被修改,则将数据写盘,并再次等待缓冲块解锁。
    linux0.11fsbuffer.c #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock) struct buffer_head * getblk(int dev,int block) { 	//… 	if (!bh) { 		sleep_on(&buffer_wait); 		goto repeat; 	} 	wait_on_buffer(bh);//第一处 	if (bh->b_count) 		goto repeat; 	while (bh->b_dirt) { 		sync_dev(bh->b_dev);//第二处 		wait_on_buffer(bh); 		if (bh->b_count) 			goto repeat; 	} 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

37、getblk()函数中

	linux0.11fsbuffer.c 	do { 		if (tmp->b_count) 			continue; 		if (!bh || BADNESS(tmp)<BADNESS(bh)) { 			bh = tmp; 			if (!BADNESS(tmp)) 				break; 		} /* and repeat until we find something good */ 	} while ((tmp = tmp->b_next_free) != free_list); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

说明什么情况下执行continue、break?(P372)

  • 答:getblk()函数主要是获取高速缓冲中的指定缓冲块。下面的宏用于判断缓冲块的修改标志,并定义修改标志的权重比锁定标志大。
    #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock) 
    • 1

    tmp指向的是空闲链表的第一个空闲缓冲块头“tmp = free_list;”。如果该缓冲块正在被使用,引用计数“tmp->b_count”不等于0,则继续扫描下一项,也就是执行continue。接下来,如果缓冲头指针bh为空,或者tmp所指的缓冲头标志(修改、锁定)权重小于bh头标志的权重,则让bh指向tmp缓冲块头。如果该tmp缓冲块头表明缓冲块既没有修改也没有锁定标志位,则说明已为指定设备上的块取得对应的高速缓冲块,则退出循环,亦即执行break。

38、make_request()函数

	linux0.11kernelblk_drvll_rw_blk.c 	if (req < request) { 		if (rw_ahead) { 			unlock_buffer(bh); 			return; 		} 		sleep_on(&wait_for_request); 		goto repeat;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

其中的sleep_on(&wait_for_request)是谁在等?等什么?(P348)

  • 答:make_request()函数主要功能为创建请求项并插入请求队列。根据具体读写操作,如果request[32]中没有一项是空闲的,则查看此次请求是不是提前读写,如果是则立即放弃此次请求操作。否则让本次请求先睡眠“sleep_on(&wait_for_request);”以等待request请求队列腾出空闲项,一段时间后再次搜索请求队列。

39、setup程序里的cli是为什么?(P18、P78)

  • 答:cli为关中断指令。从实模式到保护模式的转变,亦是废除旧中断机制建立新中断机制的过程。接下来无论是否发生中断,系统都不再对此进行响应,否则将面临旧中断已废除、新中断未建立的尴尬局面。直到新中断服务程序与IDT全部挂接完毕,中断机制才会通过sti重新打开。
    linux0.11bootsetup.s is_disk1: ! now we want to move to protected mode ... 	cli			! no interrupts allowed ! ! first we move the system to it's rightful place linux0.11initmain.c void main(void)		/* This really IS void, no error here. */ {			/* The startup routine assumes (well, ...) this */ 	//… 	hd_init(); 	floppy_init(); 	sti(); 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

40、打开A20和打开PE究竟是什么关系?保护模式不就是32位的吗?为什么还要打开A20?有必要吗?(P21、P24)

  • 答:打开A20意味着CPU可以进行32位寻址,最大寻址空间为4GB。CR0寄存器第0位叫做PE(Protected Mode Enable,保护模式使能),将其置1,即设定处理器工作方式为保护模式。这样就有A20是真正干活者,PE只是个开关。打开A20使系统实现真正32位寻址,再将PE位置1,标识系统处于保护模式下,而非实模式。

41、Linux是用C语言写的,为什么没有从main函数开始?而是先运行了3个汇编程序?道理何在?(P43)

  • 答:Linux是一个32位的实时多任务的现代操作系统,开机时的16位实模式与main函数所需要的32位保护模式有巨大的差异。这就需要通过head.s中的三个汇编程序,完成打开A20,打开PE、PG,废弃旧的16位中断响应机制,建立新的32位IDT,继而进入main函数开始执行。

42、为什么static inline _syscall0(type,name)中需要加上关键字inline?(P81)

  • 答:因为普通函数调用,实现需要将eip入栈,返回时将eip出栈。inline内联函数,编译时直接将代码嵌入、就地展开,不需要普通函数调用的call/ret等指令,效率高。若不加上inline,对于某些函数(如fork,函数需调用两次),第一次fork结束时eip出栈,第二次调用返回时eip出栈值将是一个错误值。

43、根据代码详细说明copy_process函数的参数是如何形成的?(P83-88)

  • 答:以下为源码中,copy_process函数的所有参数。
    linux0.11kernelfork.c int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 		long ebx,long ecx,long edx, 		long fs,long es,long ds, 		long eip,long cs,long eflags,long esp,long ss) {//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第一步:“int $0x80”导致CPU硬件自动将ss、esp、eflags、cs、eip压栈。

    linux0.11initmain.c static inline _syscall0(int,fork) linux0.11includeunistd.h #define _syscall0(type,name)  type name(void)  {  long __res;  __asm__ volatile ("int $0x80"  	: "=a" (__res)  	: "0" (__NR_##name));  if (__res >= 0)  	return (type) __res;  errno = -__res;  return -1;  } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    第二步:以下粗体部分的6个push操作,将ds、es、fs、edx、ecx、ebx压栈。

    linux0.11kernelsystem_call.s _system_call: 	cmpl $nr_system_calls-1,%eax 	ja bad_sys_call 	push %ds 	push %es 	push %fs 	pushl %edx 	pushl %ecx		# push %ebx,%ecx,%edx as parameters 	pushl %ebx		# to the system call 	movl $0x10,%edx		# set up ds,es to kernel space 	mov %dx,%ds 	mov %dx,%es 	movl $0x17,%edx		# fs points to local data space 	mov %dx,%fs 	call _sys_call_table(,%eax,4) 	pushl %eax 	movl _current,%eax 	cmpl $0,state(%eax)		# state 	jne reschedule 	cmpl $0,counter(%eax)		# counter 	je reschedule 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    第三步:上述源码中的“call _sys_call_table(,%eax,4)”指令本身也会压栈保护现场,就形成了none参数。

    kernelsystem_call.s _sys_fork: 	call _find_empty_process 	testl %eax,%eax 	js 1f 	push %gs 	pushl %esi 	pushl %edi 	pushl %ebp 	pushl %eax 	call _copy_process 	addl $20,%esp 1:	ret linux0.11kernelfork.c int find_empty_process(void) { 	//… 	for(i=1 ; i<NR_TASKS ; i++) 		if (!task[i]) 			return i; 	return -EAGAIN; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    第四步:进程号及在task[64]中的位置确定后,会将此时的5个寄存器值继续gs、esi、edi、ebp、eax压栈。注意,最后一个压栈的eax就是find_empty_process函数返回的任务号,也就是nr。

44、进程0创建进程1时,调用copy_process函数,在其中直接、间接调用了两次get_free_page函数,在物理内存中获得了两个页,分别用作什么?是怎么设置的?给出代码证据。(P89、P98)

  • 答:第一处:位于copy_process中,属于直接调用。主要用于给新任务数据结构分配内存,并通过“task[nr] = p;”把当前进程任务结构内容(task_struct)复制到刚申请到的内存页面p开始处。
    linux0.11kernelfork.c int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 		long ebx,long ecx,long edx, 		long fs,long es,long ds, 		long eip,long cs,long eflags,long esp,long ss) { 	//… 	p = (struct task_struct *) get_free_page(); 	if (!p) 		return -EAGAIN; 	task[nr] = p; 	//… 	if (copy_mem(nr,p)) { 		task[nr] = NULL; 		free_page((long) p); 		return -EAGAIN; 	} 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    第二处:属于间接调用,主要路径为copy_process、copy_mem、copy_page_tables。其中,copy_mem函数为新任务在线性地址空间中设置代码段和数据段基址、限长,并复制页表。

    linux0.11kernelfork.c int copy_mem(int nr,struct task_struct * p) { 	//… 	if (copy_page_tables(old_data_base,new_data_base,data_limit)) { 		free_page_tables(new_data_base,data_limit); 		return -ENOMEM; 	} 	return 0; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    函数copy_page_tables主要用于复制页目录表项和页表项。为了保存页目录表项对应的页表,需要通过“get_free_page()”申请1页空闲内存页。注意只是申请新的页面来存放页表,原父进程物理内存将被共享,直到有一个进程执行写操作时,内核才会为新进程分配新的内存页(写时复制机制)。

    linux0.11mmmemory.c int copy_page_tables(unsigned long from,unsigned long to,long size) { 	//… 	from_page_table = (unsigned long *) (0xfffff000 & *from_dir); 	if (!(to_page_table = (unsigned long *) get_free_page())) 		return -1;	/* Out of memory, see freeing */ 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

45、为什么get_free_page()将新分配页面清零?(P265)

  • 答:只要调用get_free_page()函数,就要吧内存清零,因为无法预知这页内存的用途。如果是用作页表,不清零就有垃圾值,就是隐患。
    linux0.11mmmemory.c unsigned long get_free_page(void) { 	//… 	"leal 4092(%%edx),%%edint" 	"rep ; stoslnt" 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

46、用户自己设计一套LDT表,并与GDT挂接,是否可行?(P259)

  • 答:不行。GDT、LDT是CPU硬件认定的,这两个数据结构的首地址必须挂接在CPU中的GDTR、LDTR两个寄存器上,且设置GDTR、LDTR的指令LGDT、LLDT只能在0特权级下运行。

47、内核和普通用户进程并不在一个线性地址空间内,为什么仍然能够访问普通用户进程的页面?(P272)

  • 答:内核是不能跨越线性地址空间直接访问用户进程页面的。但页面操作最终是由内核来完成的,也就是说内核执行时可以对所有页面内容进行改动。这就“等价于”内核可以操作所有进程所在的页面。

48、缺页中断是如何产生的?页写保护中断是如何产生的?操作系统是如何处理的?(P289、P304)

  • 答:MMU解析线性地址时,一旦发现对应的页目录项P位为0时,立即产生缺页中断。page_fault服务程序将处理该中断,并最终在_page_fault中通过“call _do_no_page”调用缺页处理程序。
    linux0.11mmpage.s _page_fault: 	//… 	testl $1,%eax 	jne 1f 	call _do_no_page 	jmp 2f 1:	call _do_wp_page 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    do_no_page函数,首先检查线性地址合法性。接着,查看是否可能与某个现有的进程共享页面。如果都不可能,则通过bread_page从硬盘加载页面。最后,通过put_page将物理内存地址映射到进程的线性空间。

    linux0.11mmmemory.c void do_no_page(unsigned long error_code,unsigned long address) { 	//… 	address &= 0xfffff000; 	tmp = address - current->start_code; 	if (!current->executable || tmp >= current->end_data) { 		get_empty_page(address); 		return; 	} 	if (share_page(tmp)) 		return; 	if (!(page = get_free_page())) 		oom(); 	//… 	bread_page(page,current->executable->i_dev,nr); 	//… 	if (put_page(page,address)) 		return; 	free_page(page); 	oom(); } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    如果一个页面被两个以上的进程共享时,该页面将被设置为“只读”。此时,如果对该页面执行一个写操作,就会触发“页写保护”中断。

    int copy_page_tables(unsigned long from,unsigned long to,long size) { 	//.. 	this_page &= ~2; 	*to_page_table = this_page; 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    页写保护中断服务程序是un_wp_page,程序申请了一个新页面并设置为“可读可写”,将原页面内容全部拷贝过来,然后将原页面引用计数减1。最后,将执行写操作的进程中相应的页表项指向新页面,在此基础上继续操作。

    void un_wp_page(unsigned long * table_entry) { 	//… 	if (!(new_page=get_free_page())) 		oom(); 	if (old_page >= LOW_MEM) 		mem_map[MAP_NR(old_page)]--; 	*table_entry = new_page | 7; 	invalidate(); 	copy_page(old_page,new_page); }	 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

49、为什么要设计缓冲区,有什么好处?(P310)

  • 答:缓冲区不是必须的,设计缓冲区是为了使操作系统更好的运行。主要好处有两点:其一,形成所有块设备数据的统一集散地,操作系统设计更方便、更灵活;其二,对块设备的文件操作效率更高。

50、在虚拟盘被设置为根设备之前,操作系统的根设备是软盘,请说明设置软盘为根设备的技术路线。(P46)

  • 答:通过bootsect、setup、head三个汇编操作(参见问题4),在建立保护模式的同时,亦将根设备号存储于0x901fc处。
    linux0.11initmain.c #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) void main(void)		/* This really IS void, no error here. */ {			/* The startup routine assumes (well, ...) this */  	ROOT_DEV = ORIG_ROOT_DEV;  	drive_info = DRIVE_INFO; 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

51、rd_load()执行完之后,虚拟盘已经成为可用的块设备,并成为根设备。在向虚拟盘中copy任何数据之前,虚拟盘中是否有引导块、超级块、i节点位图、逻辑块位图、i节点、逻辑块?(P135)

  • 答:没有。在向虚拟盘中copy任何数据之前,通过rd_init()对其进行初始化,它只是一块“白盘”,尚未经过类似格式化处理,还不能当做一个块设备使用。 rd_load()函数,用软盘上的256以后扇区中的信息格式化虚拟盘,使之成为一个块设备,并设置为根设备。为加载根文件系统做好准备。
    linux0.11kernelblk_drvramdisk.c long rd_init(long mem_start, int length) { 	//… 	cp = rd_start; 	for (i=0; i < length; i++) 		*cp++ = ''; 	return(length); } linux0.11kernelblk_drvramdisk.c void rd_load(void) { 	//… 	cp = rd_start; 	while (nblocks) { 		if (nblocks > 2)  			bh = breada(ROOT_DEV, block, block+1, block+2, -1); 		else 			bh = bread(ROOT_DEV, block); 		if (!bh) { 			printk("I/O error on block %d, aborting loadn",  				block); 			return; 		} 		(void) memcpy(cp, bh->b_data, BLOCK_SIZE); 		brelse(bh); 		printk("1010101010%4dk",i); 		cp += BLOCK_SIZE; 		block++; 		nblocks--; 		i++; 	} 	printk("1010101010done n"); 	ROOT_DEV=0x0101; } 

52、Linux是怎么将根设备从软盘更换为虚拟盘,并加载了根文件系统?(P46、P135)

  • 答:首先,通过rd_load()函数从软盘读取文件系统并将其复制到虚拟盘中(格式化虚拟盘),最后设置ROOT_DEV=0x0101实现将根设备从软盘更换为虚拟盘。
    linux0.11kernelblk_drvramdisk.c void rd_load(void) { 	//… 	ROOT_DEV=0x0101; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    虚拟盘格式化完成、设置为根设备之后,开始通过mount_root()加载根文件系统。

    linux0.11kernelblk_drvhd.c int sys_setup(void * BIOS) { 	//… 	rd_load(); 	mount_root(); 	//… } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    加载根文件系统有三个主要步骤:其一,复制根设备的超级块到super_block[8]中,将根设备中的根i节点挂在super_block[8]中对应的根设备的超级块上;其二,将驻留缓冲区中16个缓冲块的根设备逻辑块位图、i节点位图分别挂接在super_block[8]中根设备超级块的s_zmap[8]、s_imap[8]上;其三,将当前进程的pwd、root 指针指向根设备的i节点。

    linux0.11fssuper.c void mount_root(void) { 	//… 	if (!(p=read_super(ROOT_DEV))) 		panic("Unable to mount root"); 	if (!(mi=iget(ROOT_DEV,ROOT_INO))) 		panic("Unable to read root i-node"); 	//… 	current->pwd = mi; 	current->root = mi; 	//… } linux0.11fssuper.c static struct super_block * read_super(int dev) { 	//… 	if (s = get_super(dev)) 		return s; 	//… 	if (!(bh = bread(dev,1))) { 		s->s_dev=0; 		free_super(s); 		return NULL; 	} 	*((struct d_super_block *) s) = 		*((struct d_super_block *) bh->b_data); 	//… 	for (i=0 ; i < s->s_imap_blocks ; i++) 		if (s->s_imap[i]=bread(dev,block)) 			block++; 		else 			break; 	for (i=0 ; i < s->s_zmap_blocks ; i++) 		if (s->s_zmap[i]=bread(dev,block)) 			block++; 		else 			break; 	//… } 

References:
[1] 新设计团队. Linux内核设计的艺术[M]. 北京:机械工业出版社, 2014.
[2] 赵炯. Linux内核完全剖析[M]. 北京:机械工业出版社, 2008.

@qingdujun
2018-1-14 于 北京 怀柔
本文地址https://www.b2bchain.cn/?p=7730

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 国科大操作系统思考题答案总结
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们