操作系统思考题

请注意:

操作系统课作业习题的个人解答,仅供参考!
复习考试的话,最好自己整理,才能有最大的收获。仅供参考!!

下面有些题目答案是不对的,希望细心的你能发现!

也可以参考这个大佬的博客:https://blog.csdn.net/qq_27500493/article/details/86560421

操作系统思考题(1)

1,为什么计算机启动最开始的时候执行的是BIOS代码而不是操作系统自身的代码?
计算机被设计为从内存中运行程序,无法直接从软盘或者硬盘中运行。
刚开始加电过程中,内存没有数据,操作系统也存放在硬盘中。计算机也无法将操作系统直接加载到内存中运行。
二是因为在启动操作系统前也需要做一些准备工作,比如对计算机的硬件进行检测,建立中断向量表和中断服务程序。
BIOS程序存放在ROM中,ROM断电后也能保持信息,但一被烧录就不能改变数据,适合存放BIOS这种不需要修改的例行工作。
2,为什么BIOS只加载了一个扇区,后续扇区却是由bootsect代码加载?为什么BIOS没有把所有需要加载的扇区都加载?
这是一种协调机制,计算机可以安装各种各样的系统,不可能让所有系统都与BIOS建立一一对应的协调机制。
因此,现行的方法是定位识别。BIOS只负责将0盘面0磁道1扇区的代码加载进去,后续的部分由操作系统自己来做。
如果BIOS一次性加载,对于不同的操作系统,其代码长度不一样,可能导致操作系统加载不完全。
3,为什么BIOS把bootsect加载到0x07c00,而不是0x00000?加载后又马上挪到0x90000处,是何道理?为什么不一次加载到位?
0x00000放着BIOS建立的中断向量表,这些数据还有用处,所以不能加载到0x00000。
挪到0x90000是对内存的进一步规划,0x07c00只是BIOS初步设置的一个位置,这个位置后续可能会被其他内容覆盖。

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

bootsect->setup:

  jmpi 0,SETUPSEG

bootsect利用int 0x13中断分别加载setup程序和system模块。执行上面语句会跳转到0x090200处,就是setup程序加载的位置。CS:IP指向setup.s的第一条指令的位置,意味着现在由setup程序接着bootsect程序继续执行。此时还是实模式。
setup-> head:

  jmpi 0,8

执行完setup后,内核被移到了0x00000处,CPU变为保护模式,执行jmpi 0,8并加载了中断描述符表和全局描述符表。该指令执行后,跳转到GDT的1项中的0x00000000为基地址,偏移量为0处,即head程序的开始位置,意味着开始执行head程序。
5,setup程序里的cli是为了什么?
关闭中断,以防意外的中断产生,导致当前中断机制还没建立好的系统崩溃。
6,setup程序的最后是jmpi 0,8 为什么这个8不能简单的当作阿拉伯数字8看待?
当成8看待,程序的意思就很难理解,8当成二进制的01000看待。
最后两位表示特权,其中00是内核,11是用户。
倒数第3位如果是0表示GDT,1则表示LDT。
前两位表示这个表的第几项。GDT表的0表示空,1表示内核代码段,2表示内核数据段。LDT表的0表示空,1表示用户代码段,2表示用户数据段。

7,打开A20和打开pe究竟是什么关系,保护模式不就是32位的吗?为什么还要打开A20?有必要吗?
打开A20意味着CPU可以进行32位寻址,最大寻址空间是4GB。
打开PE意味着CPU进入保护模式,此时物理地址和线性地址一一对应。
有必要,打开PE只是说明系统进入保护模式,而打开A20才能32位寻址。
**8,在setup程序里曾经设置过一次gdt,为什么在head程序中将其废弃,又重新设置了一个?为什么折腾两次,而不是一次搞好?**P33
原来GDT所在位置是setup.s中设置的,将来setup.s的位置会被用于缓冲区,所以不重新设置一个,就会被覆盖。
如果先复制GDT的内容,后移动system模块,GDT还是会被system所覆盖
如果先移动system模块,后复制GDT,它又会把head.s的部分内容覆盖。
9,Linux是用C语言写的,为什么没有从main开始,而是先运行3个汇编程序,道理何在?
main 函数运行在 32 位的保护模式下,但系统启动时默认为 16 位的实模式,开机时的 16 位实模式与 main 函数执行需要的 32 位保护模式之间有很大的差距,这个差距需要由 3 个汇编程序来填补。其中 bootsect 负责加载, setup 与 head 则负责获取硬件参数,准备 idt,gdt,开启 A20, PE,PG,废弃旧的 16 位中断响应机制,建立新的 32 为 IDT,设置分页机制等。这些工作做完后,计算机处在32 位的保护模式状态下时,调用main的条件才算准备完毕。

**10,为什么不用call,而是用ret“调用”main函数?画出调用路线图,给出代码证据。**P42 1-6

如果用call调用操作系统的main函数,那ret时就会出现问题,不知道返回给谁。因此操作系统使用ret调用,就不需要再用ret了。通过手工编写代码压栈和跳转,模仿了call的全部动作,实现了调用setup_paging函数。压栈的EIP值不是调用setup_paging函数的下一行指令的地址,而是操作系统的main函数的执行入口地址,这样当setup_paging函数执行到ret时,从栈中将操作系统的main函数的执行入口地址_main自动出栈给EIP,EIP指向main函数的入口地址。实现了用返回指令调用main函数。

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:	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 */

操作系统思考题(2)

1、进程0的task_struct、内核栈、用户栈在哪?证明进程0的用户栈就是未激活进程0时的0特权栈,即user_stack,而进程0的内核栈并不是user_stack,给出代码证据。

进程0的task_struct、内核栈、用户栈都在内核数据段。

进程0的task_struct 的具体内容如下代码所示:

// 进程0的task_struct
#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, \
		{} \
	}, \
}

以下代码说明user_stack在内核数据段。(0x10)(参考杨炯的内核代码完全注释)一个任务的数据结构与其内核态堆栈是放在同一内存页中。所以进程0的内核栈是跟着task struct后面的,所以进程0的内核栈不是user_stack,user_stack是进程0的用户栈。(P91的图)

long user_stack [ PAGE_SIZE>>2 ] ;//定义用户堆栈4K,指针指向最后一项
//该结构用于设置堆栈ss:esp(数据段选择符)
struct {
	long * a;
	short b;
	} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };//内核数据段
//每个进程在内核态运行时都有自己的内核态堆栈,这里定义了内核态堆栈的结构
union task_union {
	struct task_struct task;
	char stack[PAGE_SIZE];
};
static union task_union init_task = {INIT_TASK,};

2、在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,这是为什么?说明理由。

set_trap_gate 和set_intr_gate的dpl是3,set_system_gate的dpl是0。dpl为0表示只能在内核态下允许,dpl为3表示系统调用可以由3特权级调用。

当用户程序产生系统调用软中断后, 系统都通过system_call总入口找到具体的系统调用函数。 set_system_gate设置系统调用,须将 DPL设置为 3,允许在用户特权级(3)的进程调用,否则会引发 General Protection 异常。set_trap_gate 及 set_intr_gate 设置陷阱和中断为内核使用,需禁止用户进程调用,所以 DPL为 0。

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

linux规定除了进程0以外的所有进程,都必须在特权级为3下创建。所以进程0 fork进程1之前,要调用move_to_user_mode将0特权级翻转到3特权级。move_to_user_mode()用的方法是模仿中断硬件压栈,顺序是ss、esp、eflags、cs、eip。然后执行iret,出栈恢复现场,翻转0特权级到3特权级。

CPU响应中断的时候,根据DPL的设置,可以实现指定的特权级之间的翻转。所以模拟中断硬件压栈可以实现特权级的翻转。

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

根据IA-32手册,某些系统指令是禁止应用程序使用的。cli指令与CPL和eflags寄存器中的IOPL有关,只有CPL的权限高于eflags寄存器的IOPL,即数值上:CPL<= IOPL才可以执行,否则会产生一般保护异常。在初始化进程0时,就设置了eflags寄存器的值。目的是通过设置IOPL的值,来控制3特权级的进程代码使用cli。

\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")
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
	"pushl $0x17\n\t" \
	"pushl %%eax\n\t" \
	"pushfl\n\t" \
	"pushl $0x0f\n\t" \
	"pushl $1f\n\t" \
	"iret\n" \
	"1:\tmovl $0x17,%%eax\n\t" \
	"movw %%ax,%%ds\n\t" \
	"movw %%ax,%%es\n\t" \
	"movw %%ax,%%fs\n\t" \
	"movw %%ax,%%gs" \
	:::"ax")

5、用户进程自己设计一套LDT表,并与GDT挂接,是否可行,为什么?

不行。基于硬件和特权级这两个原因。

  • GDT和LDT是CPU硬件认定的,这两个数据结构的首地址挂接在CPU中的GDTR、LDTR两个寄存器上。

  • GDTR、LDTR的指令LGDT、LLDT只能在0特权级下运行。

6、分析初始化IDT、GDT、LDT的代码。

IDT:

  • 初始化各种异常处理服务程序的中断描述符
  • 将IDT的int 0x11~0x2F都初始化,将IDT中对应的指向中断服务程序的指针设置为reserved
  • 设置协处理器的IDT项
  • 允许主8259A中断控制器的IRQ32、IRQ3的中断请求
  • 设置并口(可以接打印机)的IDT项
//init IDT
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);
	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);
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
	set_trap_gate(45,&irq13);
	outb_p(inb_p(0x21)&0xfb,0x21);
	outb(inb_p(0xA1)&0xdf,0xA1);
	set_trap_gate(39,&parallel_interrupt);

在GDT中初始化进程0所占的4、5两项,即初始化TSS0和LDT0。具体来说,就是这两行代码在GDT表中的第5项和第6项的地方挖了两个空间分别放TSS0和LDT0。这步是初始化进程0相关的管理结构的最后一步:将TR寄存器指向TSS0、LDTR寄存器指向LDT0,这样CPU就可以通过这两个寄存器找到TSS0和LDT0,也就能找到一切和进程0相关的管理信息。

set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));

7、在sched_init(void)函数中有这样的代码:

for(i=1;i<NR_TASKS;i++) {
    task[i] = NULL;
    ……

但并未涉及task[0],从后续代码能感觉到已经给了进程0,请给出代码证据。

上面的代码将task[64]除进程0占用的0项外的其余63项清空。

以下代码说明了task[0]已经给了进程0。

struct task_struct * task[NR_TASKS] = {&(init_task.task), };

操作系统思考题(3)

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

linux规定除了进程0以外的所有进程,都必须在特权级为3下创建。所以进程0 fork进程1之前,要调用move_to_user_mode将0特权级翻转到3特权级。move_to_user_mode()用的方法是模仿中断硬件压栈,顺序是ss、esp、eflags、cs、eip。然后执行iret,出栈恢复现场,翻转0特权级到3特权级。

CPU响应中断的时候,根据DPL的设置,可以实现指定的特权级之间的翻转。所以模拟中断硬件压栈可以实现特权级的翻转。

2、为什么static inline _syscall0(type,name)中加上关键字inline?

inline是内联函数的意思,相当于define,直接展开函数执行,而不需要利用栈空间,也不需要保持eip,效率很高。如果不加inline,对于_syscall0(int,fork),需要调用两次,就会导致第一次调用fork结束时,eip出栈,第二次调用返回的eip出栈值将会是一个错误的值。

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

copy_process函数调用了fork函数,产生了中断,中断使得CPU硬件自动将这五个寄存器的值压入栈中,所以找不到这五个参数的压栈代码。又因为函数传递参数需要使用栈,所以作为copy_process的最后五个参数。

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

每个线性地址是32位,MMU按照10-10-12的长度来识别线性地址,并分别将其解析为页目录项号,页表项号,页面内偏移,最终映射到物理地址。MMU解析线性地址时,先找CR3中的页目录表的基址,这样可以找到页目录表。通过解析线性地址值中表示页目录项的10位数据,就可以在页目录表中找到页目录项,页目录项里记录着页表的物理地址,由此找到页表位置。再通过解析线性地址表示页表项的10位数据找到页表项。同样,页表项中记录着表示页面的物理地址值,据此再找到页面的位置,之后再分析页内偏移的12位物理地址值,就找到了物理地址。

总结:10:页目录项 10:页表项 12:页内偏移。 CR3->页目录表->页目录项->页表->页表项->页面->物理地址

5、分析get_free_page()函数的代码,叙述在主内存中获取一个空闲页的技术路线。P89-90(略不太理解)

  1. 将EAX设置为0,EDI设置为指向mem_map的最后一项,std设置扫描从高地址向低地址。从mem_map的最后一项反向扫描,找出引用次数为0的页,如果没有找到则退出,找到则将找到的页设置引用数为1.
  2. ECX左移12位得到页的相对地址,加Low_MEM得到物理地址,将此页最后一个字节的地址赋值给EDI
  3. stosl将EAX的值设置到ES:EDI所指内存,反向清零1024*32bit,清空此页
  4. 返回页的地址

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

先为新的页表申请一个空闲页面,并把进程0中第一个页表里面前160个页表项复制到这个页面中进程0和进程1页表暂时都指向了相同的页面,意味着进程1可以控制进程0的页面,之后对进程1的页目录表进行设置。最后重置CR3,刷新页变换高速缓存。设置完毕。

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

都占用内核的线性地址空间。

通过逆向扫描页表位图,并由第一空页的下标左移 12 位加 LOW_MEM 得到该页的物理地址,位于 16M 内存末端。

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: cld"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res;
}

进程 0 和进程 1 的 LDT 的 LIMIT 属性将进程 0 和进程 1 的地址空间限定0~640KB, 所以进程 0、 进程 1 均无法访问到这两个页面, 故两页面占用内核的线性地址空间。进程0的局部描述符如下:

include/linux/sched.h: INIT_TASK
/* ldt */ {0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \

内核线性地址等于物理地址(0x00000~0xfffff), 挂接操作的代码如下(head.s/setup_paging):

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

8、根据代码详细分析,进程0如何根据调度第一次切换到进程1的。

  1. 进程0 通过fork函数创建进程1,使其处于就绪态
  2. 进程0调用pause函数,pause函数通过int 0x80中断,映射到sys_pause函数,将自身设置为可中断等待状态,调用schedule函数。
  3. schedule函数分析到当前有必要进行进程调度,第一次遍历进程,只要地址指针不为空,就要针对处理。第二次遍历所有进程,比较进程的状态和时间片,找出处在就绪态且时间片最大的进程。此时只有进程0和1,且进程0是可中断等待状态,只有进程1是就绪态,所以切换到进程1去执行。

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

a对应EIP、b对应CS,ljmp通过CPU中的电路进行硬件切换,进程由当前进程切换到进程n。CPU将当前寄存器的值保存到当前进程的TSS中,将进程n的TSS数据和LDT的代码段和数据段描述符恢复给CPU的各个寄存器,实现任务切换。

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

# copy_process

p->pid = last_pid;

…

p->tss.eip = eip;

p->tss.eflags = eflags;

p->tss.eax = 0;

…

p->tss.esp = esp;

…

p->tss.cs = cs & 0xffff;

p->tss.ss = ss & 0xffff;

…

p->state = TASK_RUNNING;

return last_pid;

copy_process中,内核将进程的tss复制得到进程1的tss,并将进程1的tss.eax设置为0,而进程0中的eax为1,,在进程调度时,tss中的值被恢复至相应的寄存器中,包括eip、eax等。所以中断返回后,进程0和进程1均会从int 0x80的下一句开始执行,所以fork执行了两次。

由于eax代表返回值,所以进程0和进程1会得到不同的返回值,fork返回到进程0后,进程0判断返回值为非0,因此执行代码for(;😉 pause();

在sys_pause函数中,内核设置了进程0的状态TASK_INTERRUPTIBLE,并进行进程调度。由于只有进程1处于就绪态,因此,调度执行进程1的指令。由于进程1在tss中设置了eip等寄存器的值,因此,从int 0x80的下一条指令开始执行,且设定返回eax的值作为fork的返回值,因此进程1执行了init函数。

总结:主要是利用两个系统调用sys_fork和sys_pause对进程状态的设置,以及利用了进程调度机制。

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

answer 1:

在task_struct中,从后往前遍历,寻找进程状态为“就绪态”且时间片最大的进程作为下一个要执行的进程。通过调用switch_to函数跳转到指定进程。在此过程中,如果发现存在状态为就绪态的进程,但没有时间片,则从后往前重新分配时间片。然后重新执行上述过程。寻找状态为就绪态,时间片最大的进程作为下一个要执行的进程。如果没有就绪态,就跳转到进程0.

answer 2:

  1. 进程中有就绪进程,且时间片没有用完。

正常情况下,schedule()函数首先扫描任务数组。通过比较每个就绪(TASK_RUNNING)任务的运行时间递减滴答计数counter 的值来确定当前哪个进程运行的时间最少。哪一个的值大,就表示运行时间还不长,于是就选中该进程,最后调用switch_to()执行实际的进程切换操作

  1. 进程中有就绪进程,但所有就绪进程时间片都用完(c=0)

如果此时所有处于TASK_RUNNING 状态进程的时间片都已经用完,系统就会根据每个进程的优先权值priority,对系统中所有进程(包括正在睡眠的进程)重新计算每个任务需要运行的时间片值counter。计算的公式是:

counter = counter + priority/2

然后 schdeule()函数重新扫描任务数组中所有处于TASK_RUNNING 状态,重复上述过程,直到选择出一个进程为止。最后调用switch_to()执行实际的进程切换操作。

  1. 所有进程都不是就绪的c=-1

此时代码中的c=-1,next=0,跳出循环后,执行switch_to(0),切换到进程0执行,因此所有进程都不是就绪的时候进程0执行。


操作系统思考题(四)

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

  • 形成所有块设备数据的统一集散地,操作系统的设计更方便、更灵活
  • 对块设备的文件操作运行效率更高

2、操作系统如何利用buffer_head中的 b_data,b_blocknr,b_dev,b_uptodate,b_dirt,b_count,b_lock,b_wait管理缓冲块的?(第七章)

b_data指向缓冲块,用于找到缓冲块的位置。

b_blocknr b_dev可以唯一确定硬盘块,内核用这两个标志绑定缓冲块和硬盘数据库的关系。用于保证交互数据的正确性。同时控制缓冲块在缓冲块呆的时间。

b_uptodate 标志着缓冲块的数据是基于硬盘数据块的,内核可以放心地支持进程与缓冲块进行数据交互。

b_dirt为1表示该缓冲块数据内容已被写过,最终需要同步到硬盘上。为0则不需要同步。

b_count记录每个缓冲块有多少进程共享。当进程不需要共享缓冲块,内核会解除该进程与缓冲块的关系,将b_count减1。当b_count为0时,可以当作新的缓冲块来使用。

b_lock表示该缓冲块是否加锁,为1表示缓冲块正与硬盘交互,不允许其他进程来干扰数据交互,以免出现差错。为0表示可以操作该缓冲块。

b_wait记录等待缓冲块解锁而挂起的进程,指向等待队列前面进程的task_struct。

3、操作系统如何处理多个进程等待同一个正在与硬盘交互的缓冲块?

对于一个正在与硬盘交互的缓冲块,操作系统将其加锁。进程遇到加锁的缓冲块,需要执行wait_on_buffer。

static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();
	while (bh->b_lock)
		sleep_on(&bh->b_wait);
	sti();
}

在wait_on_buffer中,如果缓冲块加锁,进程需要执行sleep_on函数。

void sleep_on(struct task_struct **p)
{
	struct task_struct *tmp;

	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
	tmp = *p;
	*p = current;
	current->state = TASK_UNINTERRUPTIBLE;
	schedule();
	if (tmp)
		tmp->state=0;
}

在sleep_on函数中,使用内核栈的机制实现了缓冲块等待队列。通过指针操作,在调用调度程序之前,队列头指针指向了当前任务结构,而函数中的临时指针tmp指向了原等待任务。从而通过该临时指针的作用,在几个进程为等待同一资源而多次调用该函数时,程序隐式构筑了一个等待队列。在插入等待队列后,sleep_on函数会调用schedule()函数执行别的进程,当进程被唤醒而重新执行时,就会执行后面的语句,把比它早进入等待队列的一个进程唤醒。

简而言之,就是操作系统用内核栈实现了一个缓冲块进程等待队列,来管理多进程等待一个缓冲块。

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

等待缓冲区解锁这段时间,缓冲块可能会被别的进程占用,因此需要再次判断一下b_count是否为0,如果不为0,则还得继续等。

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

同步前可以被进程读写。缓冲块是否能被进程读写,取决于b_uptodate。只要b_uptodate为1,缓冲块就能被进程读写。读操作不会改变缓冲块中数据的内容,写操作后,改变了缓冲区内容,需要将b_dirt置1。由于之前缓冲块已经和硬盘块更新了,所以后续同步过程中缓冲块没有写入新数据的部分和原来硬盘对应的部分相同,往硬盘上同步时,所有的数据都是进程希望同步到硬盘数据块上的,不会把垃圾数据同步到硬盘上去。所以b_uptodate仍为1。所以,b_dirt为1,进程仍能对缓冲区进行读写。

//fs/blk_dev.c
int block_write(int dev, long * pos, char * buf, int count)
{
	int block = *pos >> BLOCK_SIZE_BITS;
	int offset = *pos & (BLOCK_SIZE-1);
	int chars;
	int written = 0;
	struct buffer_head * bh;
	register char * p;

	while (count>0) {
		chars = BLOCK_SIZE - offset;
		if (chars > count)
			chars=count;
		if (chars == BLOCK_SIZE)
			bh = getblk(dev,block);
		else
			bh = breada(dev,block,block+1,block+2,-1);
		block++;
		if (!bh)
			return written?written:-EIO;
		p = offset + bh->b_data;
		offset = 0;
		*pos += chars;
		written += chars;
		count -= chars;
		while (chars-->0)
			*(p++) = get_fs_byte(buf++);
		bh->b_dirt = 1;
		brelse(bh);
	}
	return written;
}

//fs/file_dev.c
int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	off_t pos;
	int block,c;
	struct buffer_head * bh;
	char * p;
	int i=0;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 */
	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;
		c = pos % BLOCK_SIZE;
		p = c + bh->b_data;
		bh->b_dirt = 1;
		c = BLOCK_SIZE-c;
		if (c > count-i) c = count-i;
		pos += c;
		if (pos > inode->i_size) {
			inode->i_size = pos;
			inode->i_dirt = 1;
		}
		i += c;
		while (c-->0)
			*(p++) = get_fs_byte(buf++);
		brelse(bh);
	}
	inode->i_mtime = CURRENT_TIME;
	if (!(filp->f_flags & O_APPEND)) {
		filp->f_pos = pos;
		inode->i_ctime = CURRENT_TIME;
	}
	return (i?i:-1);
}

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

panic函数是当系统发现无法继续运行下去的故障时调用它,会导致程序终止,由系统显示错误号。如果出现错误的函数不是进程0,则进行数据同步,把缓冲区的数据尽量同步到硬盘上去。

改进:将死循环改成跳到的内核进程(始终运行在0特权级的进程),让内核继续执行。

/*
 * This function is used through-out the kernel (includeinh mm and fs)
 * to indicate a major problem.
 */
#include <linux/kernel.h>
#include <linux/sched.h>

void sys_sync(void);	/* it's really int */

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");
	else
		sys_sync();
	for(;;);
}

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

在task_struct中,从后往前遍历,寻找进程状态为“就绪态”且时间片最大的进程作为下一个要执行的进程。通过调用switch_to函数跳转到指定进程。在此过程中,如果发现存在状态为就绪态的进程,但没有时间片,则从后往前重新分配时间片。然后重新执行上述过程。寻找状态为就绪态,时间片最大的进程作为下一个要执行的进程。如果没有就绪态,就跳转到进程0.

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

有可能很多进程都在等待一个缓冲块。在缓冲块同步完毕后,唤醒等待进程到轮转到某一进程的过程中,很有可能之前等的缓冲块被别的进程占用并加锁。如果使用if,则该进程被唤醒以后回来不会再判断缓冲块是否被占用而直接使用就会导致出错。使用while,就会再判断一下缓冲块是否被占用,确认未被占用后使用,就不会发生之前的错误。

就好像很多人都在等一个厕所,某个人看到人太多了,就跑别的地方去了,然后回来还要看一看厕所有没有人。如果用if就直接走进去和别人一起上厕所了(大雾)

9、add_request()函数中有下列代码
if (!(tmp = dev->current_request)) {
dev->current_request = req;
sti();
(dev->request_fn)();
return;
}
其中的
if (!(tmp = dev->current_request)) {
dev->current_request = req;
是什么意思?

检查指定设备是否正忙,若目前设备没有请求项,则将设备当前请求项指针指向该请求项。

**电梯算法完成后为什么没有执行do_hd_request函数?**P202 赵炯

执行完电梯算法后,将该请求项放入请求项队列后,此时有可能有其他进程对文件操作,不能直接发起do_hd_request请求。

**10、getblk()函数中,两次调用wait_on_buffer()函数,两次的意思一样吗?**P114-115

static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();
	while (bh->b_lock)
		sleep_on(&bh->b_wait);
	sti();
}

两次的意思一样,都是等待缓冲块解锁。

第一次:已经找到一个比较合适的空闲缓冲块,但可能是加锁的,等待该缓冲块解锁

第二次:找到了一个脏的缓冲块,等待该缓冲块将数据同步到硬盘块后。同步过程加锁了,所以也是等待缓冲块解锁。

11、getblk()函数中

   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);

说明什么情况下执行continue、break。

continue:当缓冲块的引用次数不为0,则continue,继续遍历直到找到一个count为0的空闲块。

break: BADNESS为0的意思是这个块既没加锁,也没被写过,这种块是最适合的。因此如果找到了BADNESS为0的块,就决定是这个块了,然后break跳出循环。也只有在系统运行初期才会存在这样的块。

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

12、make_request()函数

 if (req < request) {
     if (rw_ahead) {
       unlock_buffer(bh);
       return;
     }
     sleep_on(&wait_for_request);
goto repeat;

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

当前进程在等空闲的请求项。

代码意思是如果请求项没有空项,则让此次请求睡眠,过会再查看请求项。

13、bread()函数代码中

if (bh->b_uptodate)
   return bh;
ll_rw_block(READ,bh);
wait_on_buffer(bh);
if (bh->b_uptodate)
   return bh;

**为什么要做第二次if (bh->b_uptodate)判断?**P134

第一次从缓冲区取出设备号块号一致的缓冲块,判断缓冲块是否有效,有效则使用;无效则发出读设备数据库请求。

第二次等待指定数据块读入,等缓冲块解锁以后,唤醒进程后,要重新判断缓冲块是否有效,如果缓冲区中数据有效,则返回缓冲区头指针退出,否则释放该缓冲区返回Null。

在等待过程中,数据可能发生了变化,所以要二次判断。


附加:

1、找到uptodate关键词

extern inline void end_request(int uptodate)
{
	DEVICE_OFF(CURRENT->dev);
	if (CURRENT->bh) {
		CURRENT->bh->b_uptodate = uptodate;
		unlock_buffer(CURRENT->bh);
	}
	if (!uptodate) {
		printk(DEVICE_NAME " I/O error\n\r");
		printk("dev %04x, block %d\n\r",CURRENT->dev,
			CURRENT->bh->b_blocknr);
	}
	wake_up(&CURRENT->waiting);
	wake_up(&wait_for_request);
	CURRENT->dev = -1;
	CURRENT = CURRENT->next;
}

2、找出四个队列源码

多个进程等一个缓冲块

static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();
	while (bh->b_lock)
		sleep_on(&bh->b_wait);
	sti();
}
void sleep_on(struct task_struct **p)
{
	struct task_struct *tmp;

	if (!p)
		return;
	if (current == &(init_task.task))
		panic("task[0] trying to sleep");
	tmp = *p;
	*p = current;
	current->state = TASK_UNINTERRUPTIBLE;
	schedule();
	if (tmp)
		tmp->state=0;
}

缓冲区等待队列 buffer_wait (等有缓冲区空位)

struct buffer_head * getblk(int dev,int block)
{
	struct buffer_head * tmp, * bh;

repeat:
	if (bh = get_hash_table(dev,block))
		return bh;
	tmp = free_list;
	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);
	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;
	}
/* NOTE!! While we slept waiting for this block, somebody else might */
/* already have added "this" block to the cache. check it */
	if (find_buffer(dev,block))
		goto repeat;
/* OK, FINALLY we know that this buffer is the only one of it's kind, */
/* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */
	bh->b_count=1;
	bh->b_dirt=0;
	bh->b_uptodate=0;
	remove_from_queues(bh);
	bh->b_dev=dev;
	bh->b_blocknr=block;
	insert_into_queues(bh);
	return bh;
}

请求项等待队列 wait_for_request (等有请求项空位)

static void make_request(int major,int rw, struct buffer_head * bh)
{
	struct request * req;
	int rw_ahead;

/* WRITEA/READA is special case - it is not really needed, so if the */
/* buffer is locked, we just forget about it, else it's a normal read */
	if (rw_ahead = (rw == READA || rw == WRITEA)) {
		if (bh->b_lock)
			return;
		if (rw == READA)
			rw = READ;
		else
			rw = WRITE;
	}
	if (rw!=READ && rw!=WRITE)
		panic("Bad block dev command, must be R/W/RA/WA");
	lock_buffer(bh);
	if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {
		unlock_buffer(bh);
		return;
	}
repeat:
/* we don't allow the write-requests to fill up the queue completely:
 * we want some room for reads: they take precedence. The last third
 * of the requests are only for reads.
 */
	if (rw == READ)
		req = request+NR_REQUEST;
	else
		req = request+((NR_REQUEST*2)/3);
/* find an empty request */
	while (--req >= request)
		if (req->dev<0)
			break;
/* if none found, sleep on new requests: check for rw_ahead */
	if (req < request) {
		if (rw_ahead) {
			unlock_buffer(bh);
			return;
		}
		sleep_on(&wait_for_request);
		goto repeat;
	}
/* fill up the request-info, and add it to the queue */
	req->dev = bh->b_dev;
	req->cmd = rw;
	req->errors=0;
	req->sector = bh->b_blocknr<<1;
	req->nr_sectors = 2;
	req->buffer = bh->b_data;
	req->waiting = NULL;
	req->bh = bh;
	req->next = NULL;
	add_request(major+blk_dev,req);
}

多个进程等一个请求项

struct request {
	int dev;		/* -1 if no request */
	int cmd;		/* READ or WRITE */
	int errors;
	unsigned long sector;
	unsigned long nr_sectors;
	char * buffer;
	struct task_struct * waiting;
	struct buffer_head * bh;
	struct request * next;
};

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

     \linux0.11\kernel\blk_drv\hd.c

     if (--CURRENT->nr_sectors) {

               do_hd = &read_intr;

               return;

     }

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

4、Linux0.11是怎么将根设备从软盘更换为虚拟盘,并加载了根文件系统?
rd_load函数从软盘读取文件系统并将其复制到虚拟盘中并通过设置ROOT_DEV为0x0101将根设备从软盘更换为虚拟盘,然后调用mount_root函数加载跟文件系统,过程如下:初始化file_table和super_block,初始化super_block并读取根i节点,然后统计空闲逻辑块数及空闲i节点数:

(代码路径:kernel/blk_drv/ramdisk.c:rd_load) ROOT_DEV=0x0101;

主设备号是1,代表内存,即将内存虚拟盘设置为根目录。

5、 在虚拟盘被设置为根设备之前,操作系统的根设备是软盘,请说明设置软盘为根设备的技术路线。
答:首先,将软盘的第一个山区设置为可引导扇区:

(代码路径:boot/bootsect.s) boot_flag: .word 0xAA55

在主Makefile文件中设置ROOT_DEV=/dev/hd6。并且在bootsect.s中的508和509处设置ROOT_DEV=0x306;在tools/build中根据Makefile中的ROOT_DEV设置MAJOR_TOOT和MINOR_ROOT,并将其填充在偏移量为508和509处:

(代码路径:Makefile) tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) > Image

随后被移至0x90000+508(即0x901FC)处,最终在main.c中设置为ORIG_ROOT_DEV并将其赋给ROOT_DEV变量:

(代码路径:init/main.c)

62 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)

113 ROOT_DEV = ORIG_ROOT_DEV;
5、copy_mem()和copy_page_tables()在第一次调用时是如何运行的?
答:copy_mem()的第一次调用是进程0创建进程1时,它先提取当前进程(进程0)的代码段、数据段的段限长,并将当前进程(进程0)的段限长赋值给子进程(进程1)的段限长。然后提取当前进程(进程0)的代码段、数据段的段基址,检查当前进程(进程0)的段基址、段限长是否有问题。接着设置子进程(进程1)的LDT段描述符中代码段和数据段的基地址为nr(1)*64MB。最后调用copy_page_table()函数

copy_page_table()的参数是源地址、目的地址和大小,首先检测源地址和目的地址是否都是4MB的整数倍,如不是则报错,不符合分页要求。然后取源地址和目的地址所对应的页目录项地址,检测如目的地址所对应的页目录表项已被使用则报错,其中源地址不一定是连续使用的,所以有不存在的跳过。接着,取源地址的页表地址,并为目的地址申请一个新页作为子进程的页表,且修改为已使用。然后,判断是否源地址为0,即父进程是否为进程0 ,如果是,则复制页表项数为160,否则为1k。最后将源页表项复制给目的页表,其中将目的页表项内的页设为“只读”,源页表项内的页地址超过1M的部分也设为"只读"(由于是第一次调用,所以父进程是0,都在1M内,所以都不设为“只读”),并在mem_map中所对应的项引用计数加1。1M内的内核区不参与用户分页管理。
6、缺页中断是如何产生的,页写保护中断是如何产生的,操作系统是如何处理的?P264,268-270

答:

① 缺页中断产生 P264

每一个页目录项或页表项的最后3位,标志着所管理的页面的属性,分别是U/S,R/W,P.如果和一个页面建立了映射关系,P标志就设置为1,如果没有建立映射关系,则P位为0。进程执行时,线性地址被MMU即系,如果解析出某个表项的P位为0,就说明没有对应页面,此时就会产生缺页中断。操作系统会调用_do_no_page为进程申请空闲页面,将程序加载到新分配的页面中,并建立页目录表-页表-页面的三级映射管理关系。

② 页写保护中断 P268-270

假设两个进程共享一个页面,该页面处于写保护状态即只读,此时若某一进程执行写操作,就会产生“页写保护”异常。操作系统会调用_do_wp_page,采用写时复制的策略,为该进程申请空闲页面,将该进程的页表指向新申请的页面,然后将原页表的数据复制到新页面中,同时将原页面的引用计数减1。该进程得到自己的页面,就可以执行写操作。

7、详细分析多个进程(无父子关系)共享一个可执行程序的完整过程。

答:

假设有三个进程A、B、C,进程A先执行,之后是B最后是C,它们没有父子关系。A进程启动后会调用open函数打开该可执行文件,然后调用sys_read()函数读取文件内容,该函数最终会调用bread函数,该函数会分配缓冲块,进行设备到缓冲块的数据交换,因为此时为设备读入,时间较长,所以会给该缓冲块加锁,调用sleep_on函数,A进程被挂起,调用schedule()函数B进程开始执行。

B进程也首先执行open()函数,虽然A和B打开的是相同的文件,但是彼此操作没有关系,所以B继承需要另外一套文件管理信息,通过open_namei()函数。B进程调用read函数,同样会调用bread(),由于此时内核检测到B进程需要读的数据已经进入缓冲区中,则直接返回,但是由于此时设备读没有完成,缓冲块以备加锁,所以B将因为等待而被系统挂起,之后调用schedule()函数。

C进程开始执行,但是同B一样,被系统挂起,调用schedule()函数,假设此时无其它进程,则系统0进程开始执行。

假设此时读操作完成,外设产生中断,中断服务程序开始工作。它给读取的文件缓冲区解锁并调用wake_up()函数,传递的参数是&bh->b_wait,该函数首先将C唤醒,此后中断服务程序结束,开始进程调度,此时C就绪,C程序开始执行,首先将B进程设为就绪态。C执行结束或者C的时间片削减为0时,切换到B进程执行。进程B也在sleep_on()函数中,调用schedule函数进程进程切换,B最终回到sleep_on函数,进程B开始执行,首先将进程A设为就绪态,同理当B执行完或者时间片削减为0时,切换到A执行,此时A的内核栈中tmp对应NULL,不会再唤醒进程了。
8、详细分析一个进程从创建、加载程序、执行、退出的全过程。P273

  1.  创建进程,调用fork函数。
    

a) 准备阶段,为进程在task[64]找到空闲位置,即find_empty_process();

b) 为进程管理结构找到储存空间:task_struct和内核栈。

c) 父进程为子进程复制task_struct结构

d) 复制新进程的页表并设置其对应的页目录项

e) 分段和分页以及文件继承。

f) 建立新进程与全局描述符表(GDT)的关联

g) 将新进程设为就绪态

  1.  加载进程
    

a) 检查参数和外部环境变量和可执行文件

b) 释放进程的页表

c) 重新设置进程的程序代码段和数据段

d) 调整进程的task_struct

  1.  进程运行
    

a) 产生缺页中断并由操作系统响应

b) 为进程申请一个内存页面

c) 将程序代码加载到新分配的页面中

d) 将物理内存地址与线性地址空间对应起来

e) 不断通过缺页中断加载进程的全部内容

f) 运行时如果进程内存不足继续产生缺页中断,

  1.  进程退出
    

a) 进程先处理退出事务

b) 释放进程所占页面

c) 解除进程与文件有关的内容并给父进程发信号

d) 进程退出后执行进程调度

8、为什么get_free_page()将新分配的页面清0?P265

答:

因为无法预知这页内存的用途,如果用作页表,不清零就有垃圾值,就是隐患。

答2:Linux在回收页面时并没有将页面清0,只是将mem_map中与该页对应的位置0。在使用get_free_page申请页时,也是遍历mem_map寻找对应位为0的页,但是该页可能存在垃圾数据,如果不清0的话,若将该页用做页表,则可能导致错误的映射,引发错误,所以要将新分配的页面清0。
9、进程0创建进程1时调用copy_process函数,在其中直接、间接调用了两次get_free_page函数,在物理内存中获得了两个页,分别用作什么?是怎么设置的?给出代码证据。

答:

第一次调用get_free_page函数申请的空闲页面用于进程1 的task_struct及内核栈。首先将申请到的页面清0,然后复制进程0的task_struct,再针对进程1作个性化设置,其中esp0 的设置,意味着设置该页末尾为进程 1 的堆栈的起始地址。代码见P90 及 P92。

kenel/fork.c:copy_process
p = (struct task_struct *)get_free_page();
*p = *current
p->tss.esp0 = PAGE_SIZE + (long)p;

第二次调用get_free_page函数申请的空闲页面用于进程1的页表。在创建进程1执行copy_process中,执行copy_mem(nr,p)时,内核为进程1拷贝了进程 0的页表(160 项),同时修改了页表项的属性为只读。代码见P98。

mm/memory.c: copy_page_table
if(!(to_page_table = (unsigned long *)get_free_page()))
	return -1;
*to_dir = ((unsigned long)to_page_table) | 7;

10、用图表示下面的几种情况,并从代码中找到证据:
A当进程获得第一个缓冲块的时候,hash表的状态

B经过一段时间的运行。已经有2000多个buffer_head挂到hash_table上时,hash表(包括所有的buffer_head)的整体运行状态。

C经过一段时间的运行,有的缓冲块已经没有进程使用了(空闲),这样的空闲缓冲块是否会从hash_table上脱钩?

D经过一段时间的运行,所有的buffer_head都挂到hash_table上了,这时,又有进程申请空闲缓冲块,将会发生什么?

A

getblk(int dev, int block) à get_hash_table(dev,block) -> find_buffer(dev,block) -> hash(dev, block)

哈希策略为:

          #define _hashfn(dev,block)(((unsigned)(dev block))%NR_HASH)

          #define hash(dev,block) hash_table[_hashfn(dev, block)]

此时,dev为0x300,block为0,NR_HASH为307,哈希结果为154,将此块插入哈希表中次位置后

B

//代码路径 :fs/buffer.c:   static inline void insert_into_queues(struct buffer_head * bh) {

/*put at end of free list */   

bh->b_next_free= free_list;   

bh->b_prev_free= free_list->b_prev_free;   

free_list->b_prev_free->b_next_free= bh;   

free_list->b_prev_free= bh;

/*put the buffer in new hash-queue if it has a device */   

bh->b_prev= NULL;   

bh->b_next= NULL;   

if (!bh->b_dev)        

return;  

bh->b_next= hash(bh->b_dev,bh->b_blocknr);   

hash(bh->b_dev,bh->b_blocknr)= bh;   

bh->b_next->b_prev= bh

}

C

不会脱钩,会调用brelse()函数,其中if(!(buf->b_count–)),计数器减一。没有对该缓冲块执行remove操作。由于硬盘读写开销一般比内存大几个数量级,因此该空闲缓冲块若是能够再次被访问到,对提升性能是有益的。

D

进程顺着freelist找到没被占用的,未被上锁的干净的缓冲块后,将其引用计数置为1,然后从hash队列和空闲块链表中移除该bh,然后根据此新的设备号和块号重新插入空闲表和哈西队列新位置处,最终返回缓冲头指针。

Bh->b_count=1;
Bh->b_dirt=0;
Bh->b_uptodate=0;
Remove_from_queues(bh);
Bh->b_dev=dev;
Bh->b_blocknr=block;
Insert_into_queues(bh);

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

b_uptodate是缓冲块中针对进程方向的标志位,它的作用是告诉内核,缓冲块的数据是否已是数据块中最新的。当b_update置1时,就说明缓冲块中的数据是基于硬盘数据块的,内核可以放心地支持进程与缓冲块进行数据交互;如果b_uptodate为0,就提醒内核缓冲块并没有用绑定的数据块中的数据更新,不支持进程共享该缓冲块。

当为文件创建新数据块,新建一个缓冲块时,b_uptodate被置1,但并不会引起数据混乱。此时,新建的数据块只可能有两个用途,一个是存储文件内容,一个是存储文件的i_zone的间接块管理信息。

如果是存储文件内容,由于新建数据块和新建硬盘数据块,此时都是垃圾数据,都不是硬盘所需要的,无所谓数据是否更新,结果“等效于”更新问题已经解决。

如果是存储文件的间接块管理信息,必须清零,表示没有索引间接数据块,否则垃圾数据会导致索引错误,破坏文件操作的正确性。虽然缓冲块与硬盘数据块的数据不一致,但同样将b_uptodate置1不会有问题。

综合以上考虑,设计者采用的策略是,只要为新建的数据块新申请了缓冲块,不管这个缓冲块将来用作什么,反正进程现在不需要里面的数据,干脆全部清零。这样不管与之绑定的数据块用来存储什么信息,都无所谓,将该缓冲块的b_uptodate字段设置为1,更新问题“等效于”已解决

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

(先画图见P54 图2-9然后解释)以set_trap_gate(0,&divide_error)为例,其中,n是0,gate_addr是&idt[0],也就是idt的第一项中断描述符的地址;type是15,dpl(描述符特权级)是0;addr是中断服务程序divide_error(void)的入口地址。
代码证据:P53

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

如何分页

head.s在setup_paging开始创建分页机制。将页目录表和4个页表放到物理内存的起始位置,从内存起始位置开始的5个页空间内容全部清零(每页4KB),然后设置页目录表的前4项,使之分别指向4个页表。然后开始从高地址向低地址方向填写4个页表,依次指向内存从高地址向低地址方向的各个页面。即将第4个页表的最后一项指向寻址范围的最后一个页面。即从0xFFF000开始的4kb 大小的内存空间。将第4个页表的倒数第二个页表项指向倒数第二个页面,即0xFFF000-0x1000开始的4KB字节的内存空间,依此类推。

挂接关系图

代码证据: P39最下面

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破落之实

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值