本文纯属学习笔记,为个人理解,内容正确性不能保证。访问请移步至(David’s Wikipedia) https://www.qingdujun.com/ ,这里有能“击穿”平行宇宙的乱序并行字节流…


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.11\include\linux\sched.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.11\kernel\sched.c
    struct task_struct *task[NR_TASKS] = { &(init_task.task), };
    

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

  • 答:图参考P39。
    注意,页目录表需指向全部页表;页表需要指向全部页;页目录表、页表本身也是页。

    \linux0.11\boot\head.s
    setup_paging:movl $1024*5,%ecx      /* 5 pages - pg_dir+4 page tables */xorl %eax,%eaxxorl %edi,%edi           /* pg_dir is at 0x000 */cld;rep;stoslmovl $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,%edimovl $0xfff007,%eax     /*  16Mb - 4096 + 7 (r/w user,p) */std
    1:  stosl           /* fill pages backwards - more efficient :-) */subl $0x1000,%eaxjge 1bxorl %eax,%eax        /* pg_dir is at 0x0000 */movl %eax,%cr3     /* cr3 - page directory start */movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0        /* set paging (PG) bit */ret            /* this also flushes prefetch-queue */
    

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.11\boot\head.s
    after_page_tables:pushl $0      # These are the parameters to main :-)pushl $0pushl $0pushl $L6     # return address for main, if it decides to.pushl $_mainjmp setup_paging
    L6:jmp L6           # main should never return here, but# just in case, we know what happens.
    

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

  • 答:对中断描述符表的初始化,就是将中断、异常处理的服务程序与IDT进行挂接,逐步重建中断服务体系。 set_trap_gate(0,&divide_error); //除零错误

    \linux0.11\include\asm\system.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,%%ax\n\t" \"movw %0,%%dx\n\t" \"movl %%eax,%1\n\t" \"movl %%edx,%2" \: \: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \"o" (*((char *) (gate_addr))), \"o" (*(4+(char *) (gate_addr))), \"d" ((char *) (addr)),"a" (0x00080000))
    

    可以看出,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.11\include\linux\sched.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.11\include\asm\system.h
    #define move_to_user_mode() \
    __asm__ ("movl %%esp,%%eax\n\t" \"pushl $0x17\n\t" \"pushl %%eax\n\t" \"pushfl\n\t" \   //eflags进栈"pushl $0x0f\n\t" \"pushl $1f\n\t" \"iret\n" \//..:::"ax")
    

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

    \linux0.11\kernel\fork.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;
    }
    

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

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

15.在system.h里

\linux0.11\include\asm\system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \"movw %0,%%dx\n\t" \"movl %%eax,%1\n\t" \"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)

这里中断门、陷阱门、系统调用都是通过_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.11\mm\memory.c
    unsigned long get_free_page(void)
    {register unsigned long __res asm("ax");__asm__("std ; repne ; scasb\n\t""jne 1f\n\t""movb $1,1(%%edi)\n\t""sall $12,%%ecx\n\t""addl %2,%%ecx\n\t""movl %%ecx,%%edx\n\t""movl $1024,%%ecx\n\t""leal 4092(%%edx),%%edi\n\t""rep ; stosl\n\t""movl %%edx,%%eax\n""1:":"=a" (__res):"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),"D" (mem_map+PAGING_PAGES-1):"di","cx","dx");
    return __res;
    }
    

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.11\mm\memory.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;if ((from&0x3fffff) || (to&0x3fffff))panic("copy_page_tables called with wrong alignment");from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */to_dir = (unsigned long *) ((to>>20) & 0xffc);size = ((unsigned) (size+0x3fffff)) >> 22;for( ; size-->0 ; from_dir++,to_dir++) {if (1 & *to_dir)panic("copy_page_tables: already exist");if (!(1 & *from_dir))continue;from_page_table = (unsigned long *) (0xfffff000 & *from_dir);if (!(to_page_table = (unsigned long *) get_free_page()))return -1;   /* Out of memory, see freeing */*to_dir = ((unsigned long) to_page_table) | 7;nr = (from==0)?0xA0:1024;for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {this_page = *from_page_table;if (!(1 & this_page))continue;this_page &= ~2;*to_page_table = this_page;if (this_page > LOW_MEM) {*from_page_table = this_page;this_page -= LOW_MEM;this_page >>= 12;mem_map[this_page]++;}}}invalidate();return 0;
    }
    

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

  • 答:这两个页占用的是内核的线性地址空间。

    \linux0.11\boot\head.s
    setup_paging://…movl $pg3+4092,%edimovl $0xfff007,%eax     /*  16Mb - 4096 + 7 (r/w user,p) */std
    1:  stosl           /* fill pages backwards - more efficient :-) */subl $0x1000,%eax//…
    

    内核的线性地址空间为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.11\include\linux\sched.h

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \"je 1f\n\t" \"movw %%dx,%1\n\t" \"xchgl %%ecx,_current\n\t" \"ljmp %0\n\t" \"cmpl %%ecx,_last_task_used_math\n\t" \"jne 1f\n\t" \"clts\n" \"1:" \::"m" (*&__tmp.a),"m" (*&__tmp.b), \"d" (_TSS(n)),"c" ((long) task[n])); \
}

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

  • 答:ljmp %0\n\t通过任务门机制并未实际使用任务门,将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.11\kernel\fork.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;
    }
    

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

    \linux0.11\kernel\system_call.s
    _system_call://…call _sys_call_table(,%eax,4)pushl %eax
    

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

    \linux0.11\init\main.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; \
    }
    

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

    \linux0.11\kernel\sched.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();
    }
    

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

    \linux0.11\kernel\sched.c
    int sys_pause(void)
    {current->state = TASK_INTERRUPTIBLE;schedule();return 0;
    }
    \linux0.11\kernel\sched.c
    void schedule(void)
    {//…switch_to(next);
    }
    

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

    \linux0.11\fs\buffer.c
    #define switch_to(n) {\//…"ljmp %0\n\t" \//…
    }
    

    执行switch_to()函数中,当程序执行到“ljmp %0\n\t”这行时,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.11\fs\buffer.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;
    }
    

    字段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.11\fs\file_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;elsepos = 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;} //…
    }
    

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

    \linux0.11\fs\buffer.c
    struct buffer_head * bread(int dev,int block)
    {struct buffer_head * bh;if (!(bh=getblk(dev,block)))panic("bread: getblk returned NULL\n");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.11\fs\buffer.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;//…
    }
    

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

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

  • 答:该函数用来显示内核中出现的重大错误信息,并运行文件系统同步函数,然后进入死循环——死机。如果当前进程是任务0的话,还说明时交换任务出错,并且还没有运行系统同步函数。关键字volatile用于告诉gcc该函数不会返回,死机。

    \linux0.11\kernel\panic.c
    volatile void panic(const char * s)
    {printk("Kernel panic: %s\n\r",s);if (current == task[0])printk("In swapper task - not syncing\n\r");elsesys_sync();for(;;);
    }
    

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

    volatile void panic(const char * s)
    {printk("Kernel panic: %s\n\r",s);if (current == task[0])printk("In swapper task - not syncing\n\r");elsesys_sync();for(;;);
    }
    

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

  • 答:schedule()函数的主要过程为,首先依据task[64]这个结构,第一次遍历所有进程,只要地址指针不为空,就要针对它的signal、alarm分析,这里先不考虑。第二次遍历所有进程,比较进程的状态和时间片,找出处在就绪态且counter最大的进程。

    \linux0.11\kernel\sched.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);
    }
    

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

    \linux0.11\fs\buffer.c
    #define switch_to(n) {\//…"ljmp %0\n\t" \//…
    }
    

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

  • 答:以下为wait_on_buffer函数源代码。

    \linux0.11\fs\buffer.c
    static inline void wait_on_buffer(struct buffer_head * bh)
    {cli();while (bh->b_lock)sleep_on(&bh->b_wait);sti();
    }
    

    从上述源码中可知,一旦缓冲块被加锁,当前请求进程必被挂起在该缓冲块等待队列中,直到在某一时间被重新唤醒。这时候,缓冲块肯定已经被解锁了,但是可能被队列中其他进程又把该缓冲块给占用了。这时候使用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.11\kernel\blk_drv\ll_rw_blk.cif (!(tmp = dev->current_request)) {dev->current_request = req;sti();(dev->request_fn)();return;}

其中的

 if (!(tmp = dev->current_request)) {dev->current_request = req;}

是什么意思?(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.11\kernel\blk_drv\hd.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;//…
    }
    

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

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

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

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

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

  • 答:不一样。第一处“wait_on_buffer(bh);”是已经找到一个比较合适的空闲缓冲块了,于是先等待缓冲块解锁。第二处“wait_on_buffer(bh);”是如果该缓冲区已被修改,则将数据写盘,并再次等待缓冲块解锁。

    \linux0.11\fs\buffer.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;}//…
    }
    

37、getblk()函数中

 \linux0.11\fs\buffer.cdo {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);

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

  • 答:getblk()函数主要是获取高速缓冲中的指定缓冲块。下面的宏用于判断缓冲块的修改标志,并定义修改标志的权重比锁定标志大。

    #define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)
    

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

38、make_request()函数

 \linux0.11\kernel\blk_drv\ll_rw_blk.cif (req < request) {if (rw_ahead) {unlock_buffer(bh);return;}sleep_on(&wait_for_request);goto repeat;}

其中的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.11\boot\setup.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.11\init\main.c
    void main(void)     /* This really IS void, no error here. */
    {           /* The startup routine assumes (well, ...) this *///…hd_init();floppy_init();sti();//…
    }
    

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.11\kernel\fork.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)
    {//…
    }
    

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

    \linux0.11\init\main.c
    static inline _syscall0(int,fork)
    \linux0.11\include\unistd.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; \
    }
    

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

    \linux0.11\kernel\system_call.s
    _system_call:cmpl $nr_system_calls-1,%eaxja bad_sys_callpush %dspush %espush %fspushl %edxpushl %ecx        # push %ebx,%ecx,%edx as parameterspushl %ebx       # to the system callmovl $0x10,%edx     # set up ds,es to kernel spacemov %dx,%dsmov %dx,%esmovl $0x17,%edx     # fs points to local data spacemov %dx,%fscall _sys_call_table(,%eax,4)pushl %eaxmovl _current,%eaxcmpl $0,state(%eax)      # statejne reschedulecmpl $0,counter(%eax)      # counterje reschedule
    

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

    \kernel\system_call.s
    _sys_fork:call _find_empty_processtestl %eax,%eaxjs 1fpush %gspushl %esipushl %edipushl %ebppushl %eaxcall _copy_processaddl $20,%esp
    1:  ret
    \linux0.11\kernel\fork.c
    int find_empty_process(void)
    {//…for(i=1 ; i<NR_TASKS ; i++)if (!task[i])return i;return -EAGAIN;
    }
    

    第四步:进程号及在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.11\kernel\fork.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;}//…
    }
    

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

    \linux0.11\kernel\fork.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;
    }
    

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

    \linux0.11\mm\memory.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 *///…
    }
    

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

  • 答:只要调用get_free_page()函数,就要吧内存清零,因为无法预知这页内存的用途。如果是用作页表,不清零就有垃圾值,就是隐患。

    \linux0.11\mm\memory.c
    unsigned long get_free_page(void)
    {//…"leal 4092(%%edx),%%edi\n\t""rep ; stosl\n\t"//…
    }
    

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.11\mm\page.s
    _page_fault://…testl $1,%eaxjne 1fcall _do_no_pagejmp 2f
    1:  call _do_wp_page
    

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

    \linux0.11\mm\memory.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();
    }
    

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

    int copy_page_tables(unsigned long from,unsigned long to,long size)
    {//..this_page &= ~2;*to_page_table = this_page;//…
    }
    

    页写保护中断服务程序是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);
    }
    

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

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

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

  • 答:通过bootsect、setup、head三个汇编操作(参见问题4),在建立保护模式的同时,亦将根设备号存储于0x901fc处。

    \linux0.11\init\main.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;//…
    }
    

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

  • 答:没有。在向虚拟盘中copy任何数据之前,通过rd_init()对其进行初始化,它只是一块“白盘”,尚未经过类似格式化处理,还不能当做一个块设备使用。 rd_load()函数,用软盘上的256以后扇区中的信息格式化虚拟盘,使之成为一个块设备,并设置为根设备。为加载根文件系统做好准备。

    \linux0.11\kernel\blk_drv\ramdisk.c
    long rd_init(long mem_start, int length)
    {//…cp = rd_start;for (i=0; i < length; i++)*cp++ = '\0';return(length);
    }
    \linux0.11\kernel\blk_drv\ramdisk.c
    void rd_load(void)
    {//…cp = rd_start;while (nblocks) {if (nblocks > 2) bh = breada(ROOT_DEV, block, block+1, block+2, -1);elsebh = bread(ROOT_DEV, block);if (!bh) {printk("I/O error on block %d, aborting load\n", block);return;}(void) memcpy(cp, bh->b_data, BLOCK_SIZE);brelse(bh);printk("\010\010\010\010\010%4dk",i);cp += BLOCK_SIZE;block++;nblocks--;i++;}printk("\010\010\010\010\010done \n");ROOT_DEV=0x0101;
    }
    

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

  • 答:首先,通过rd_load()函数从软盘读取文件系统并将其复制到虚拟盘中(格式化虚拟盘),最后设置ROOT_DEV=0x0101实现将根设备从软盘更换为虚拟盘。

    \linux0.11\kernel\blk_drv\ramdisk.c
    void rd_load(void)
    {//…ROOT_DEV=0x0101;
    }
    

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

    \linux0.11\kernel\blk_drv\hd.c
    int sys_setup(void * BIOS)
    {//…rd_load();mount_root();//…
    }
    

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

    \linux0.11\fs\super.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.11\fs\super.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++;elsebreak;for (i=0 ; i < s->s_zmap_blocks ; i++)if (s->s_zmap[i]=bread(dev,block))block++;elsebreak;//…
    }
    

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

@qingdujun
2018-1-14 于 北京 怀柔

漫谈 | 从52个思考题来看《Linux内核设计的艺术》相关推荐

  1. 跟我一起玩《linux内核设计的艺术》第1章(四)——from setup.s to head.s,这回一定让main滚出来!(已解封)

    看到书上1.3的大标题,以为马上就要见着main了,其实啊,还早着呢,光看setup.s和head.s的代码量就知道,跟bootsect.s没有可比性,真多--这确实需要包括我在内的大家多一些耐心,相 ...

  2. Linux内核设计的艺术

    Linux内核设计的艺术这本书是我认为对Linux内核描述非常优秀的书籍.书籍中描述了内核启动的流程,内核运行的机理,内存管理,进程管理等等. #书籍目录 第1章 从开机加电到执行 main函数之前的 ...

  3. Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理

    为什么80%的码农都做不了架构师?>>>    Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理 本书使用高分辨率精心绘制的300多张图片,帮助大家理解操作系统特有 ...

  4. 【Linux 内核设计的艺术】从开机加电到执行 main 函数之前的过程

    本笔记依据 <Linux 内核设计的艺术> 新设计团队 著 机械工业出版社 以及本人平时的笔记积累而书写,Linux0.11. 文章目录 1.1 启动 BIOS,准备实模式下的中断向量表和 ...

  5. 图解linux内核设计艺术,Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理...

    Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理 作者:新设计团队 出版日期:2011年05月 文件大小:29.01M 支持设备: ¥6.00在线试读 适用客户端: 言商书局 iPa ...

  6. linux的内核设计,Linux内核设计的艺术 清晰完整版PDF+配套源码

    <Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理>内容提要: 关于Linux内核的书已经不计其数,但这本书却是独树一帜的,它的内容代表着Linux内核研究成果的世界顶尖 ...

  7. 跟我一起玩《linux内核设计的艺术》第1章(二)——linux内存加载和16位实模式使命的终结

    经过折腾了整整一篇的BIOS,总算可以跑linux 0.11内核源码了.第一个执行的是bootsect.s,如果现在你还没有下载linux 0.11的源码就赶紧去下载.如果自己懒得找,可以参见我下一篇 ...

  8. 《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——2.15 本章小结...

    2.15 本章小结 本章开始执行以main()函数为代表的用C语言编写的操作系统内核代码,内容涉及硬件初始化.为内核及进程的正确运行所做的初始化.激活进程0. 硬件初始化又可以分为两类:一类是与主机有 ...

  9. 读《Linux内核设计与实现》我想到了这些书

          从题目中可以看到,这篇文章是以我读<Linux内核设计与实现>而想到的其他我读过的书,所以,这篇文章的主要支撑点是<Linux内核>.       开始读这本书已经 ...

最新文章

  1. AIX下RAC搭建 Oracle10G(一)检測系统环境
  2. Python之pandas:pandas中数据处理常用函数(与空值相关/去重和替代)简介、具体案例、使用方法之详细攻略
  3. win10下linux子系统6,Windows10下安装Linux子系统2020年7月最新版
  4. 初识Docker-什么是docker
  5. 用户密码登录改造实现
  6. Java数据结构Map List Set及Queue相关的类图
  7. PYTHON语言之常用内置函数
  8. 微信小程序手机号快速填写及会员卡开卡组件开放
  9. 如何制作高效率的数据可视化大屏
  10. linux音频设备节点,Linux音频驱动之三:PCM设备的创建
  11. 7.JavaScript变量
  12. 非常强大的时间日期插件 --- JeDate.js
  13. php如何解决高并发问题
  14. 基于WEB的PDF打印——浏览器中静默打印PDF文件
  15. 特征值和特征值的几何意义
  16. CE扫雷20211031
  17. [爬虫笔记01] Ajax爬取今日头条文章
  18. 这个儿童节,我们和小时候有什么区别?
  19. UPlayer播放器问题相关
  20. linux系统ubuntu简介

热门文章

  1. Linux实验精华总结
  2. 用float/double定义可以输入整数
  3. 6.3.1 延迟操作接口
  4. 网页打开慢,响应时间慢,如何定位这个问题?
  5. ArcGIS基础知识之shape文件的构成
  6. XP系统谷歌浏览器一直提示“adobe flash player 因过期而遭阻止”解决方法
  7. slack 开源系统_Slack团队聊天的5种开源替代品
  8. java 分贝_Android实时获取音量(单位:分贝)
  9. [leetcode]322. 零钱兑换(Coin Change )C++代码实现
  10. 955 不加班的公司名单:955.WLB