实验4:基于内核栈切换的进程切换


线程部分的课程已经完成,花了几天时间把实验也做了一下。实验基本上是按照实验指导书做的,实验过程总体还算顺利。本次实验的需要用到的一些知识:中断时压栈的过程;函数调用时压栈的过程;LDT、GTD、TSS的工作机制、宏函数。

1 线程

1.1 用户级线程

两个执行序列共用一个栈切换执行会导致了执行过程的错乱。多个执行序列之间应该怎样切换执行才会不紊乱呢?课程由此引出用户级线程的切换。两个线程应该使用两个栈,线程在切换执行的同时将栈也一同切换,这样两个线程切换执行才不会紊乱。同理n个线程也是如此。用户级线程有什么缺陷呢?一个缺陷就是内核感知不到用户级线程的存在,只是将它当作一个进程来看,因此内核并没有因为多个用户级线程,而给这个“进程”分配跟多资源。

1.2 核心级线程

核心级线程不再停留于用户态来切换,而是进入内核,让内核来切换线程。相比于用户级线程,两个核心级线程需要使用两套栈来切换(将一个用户栈和一个内核栈关联起来组成一套栈)。从课件中截取了一套栈的样子:
图1.1 一套栈的样子

图1.1 一套栈的样子

图1.1的右边部分就是由一个用户栈和一个核心栈关联形成的一套栈。仔细分析图1.1中的箭头标识,就会明白一个用户栈和一个内核栈是如何关联起来组成一套栈的。

核心级线程切换的大致过程: 线程a调用系统调用,执行 int 0x80 时,硬件会将线程a用户态的ss : sp压入内核栈中,同时将eflags,cs : ip压入内核栈中。这些压栈是由于系统产生了中断 - int 0x80,硬件自动完成了执行压栈过程。可能会有一点点疑问:硬件是怎么知道线程a的内核栈的位置的呢?提示一下 :回想一下TR寄存器的作用和TSS中的内容。在线程a进入内核后,若需要切换到线程b去执行,则内核首先会将线程a的TCB切换到线程b的TCB,然后将切换到线程内核b的内核栈,利用线程b的内核栈中存放的线程b的用户态信息(如线程b的用户态的cs:ip等)弹栈到线程b的用户态去执行。

1.3 一个实际的schedule 函数

schedule函数会选出下一个需要执行的进程,然后调用 switch_to 跳转到下一个进程去执行。下面是对schedule函数的主要内容的注释,看完注释应该就能大致理解调度的过程了:

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */
......
/* 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)//任务处于就绪态且任务counter值(进程已经运行的时间,
			                                                     //越大说明运行的时间越短)最大的就是下一个要执行的进程
				c = (*p)->counter, next = i;
		}
		if (c) break;     //c>0,即找到了下一个需要执行的任务,则退出循环去执行下一个进程。
		                  //若c==-1,即队列中全部是空任务,则退出循环去执行任务0(因为此时next==0) 
						  //若C==0, 说明所有就绪态的任务的时间片都用光了,那么接着执行下面语句给所有任务更新时间片
		//只有当c==0时才执行下面循环
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)    //对所有非空任务更新时间片,注意此时就绪态任务的时间片是等于0的( (*p)->counter == 0),
			           //而阻塞态任务的时间片是大于0的( (*p)->counter > 0 )
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;  //就绪态任务的时间片更新为(*p)->priority,而阻塞态任务的时间片更新为大
						                 //于(*p)->priority,因此阻塞态任务在下一轮切换中优先级会更高
						                 //(*p)->counter >> 1还有一个用意:保证时间片收敛,不会过大。
	}
	switch_to(next);   //切换到下一个进程执行
}

2 实验内容

线程提高了系统的灵活性,本次实验也主要是在探究线程实现原理,但本次实验并不是要实现一个线程,而是通过实现一个基于内核栈切换的进程来探究线程的实现原理。本次实验内容如下:

(1) 编写汇编程序 switch_to:
(2) 完成主体框架;
(3) 在主体框架下依次完成 PCB 切换、内核栈切换、LDT 切换等;
(4) 修改 fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。
(5) 修改 PCB,即 task_struct 结构,增加相应的内容域,同时处理由于修改了 task_struct 所造成的影响。
(6) 用修改后的 Linux 0.11 仍然可以启动、可以正常使用。
(选做)分析实验 3 的日志体会修改前后系统运行的差别。

具体实验内容及实验提示请参考实验指导书:操作系统原理与实践

3 实验过程

本章的3.1、3.2、3.3节主要内容为修改内核代码的过程,是按照实验指导书的提示顺序来修改的,在看这3节的时候最好配合实验指导书一起看。3.4节整理了进程切换过程。实验过程才是最艰辛的 o(╥﹏╥)o

3.1 schedule 与 switch_to

为什么从这里开始修改呢?因为在schedule函数找到下一个要执行的进程后,便会调用 switch_to 让CPU切换到下一个进程去执行,而我们要修改的正是这个切换的过程。我们需要利用内核栈来切换进程,因此就不能再使用TSS来切换了,但LDT还是需要的因为进程间还是需要地址分离。schedule 函数修改如下:

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;
	/*修改1、新增变量,指向下一个进程的PCB */
	struct task_struct *pnext = &(init_task.task);  
/* check alarm, wake up any interruptible tasks that have got a signal */
......
/* 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)
			/*修改2 pnext 指向下一个进程的 PCB*/
				c = (*p)->counter, next = i, pnext = (*p);
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	/*修改3 修改switch_to*/
	switch_to(pnext, _LDT(next));    
}

为了不报错,需要在sched.c文件中添加相关声明:

extern long switch_to(struct task_struct *p, unsigned long address);

3.2 重写 switch_to

原来的 switch_to 是利用 ’ ljmp *%0 ’ 来切换进程的,根据内嵌汇编的知识,结合switch_to代码分析,可以知道 %0 中存放了目标进程的 TSS 描述符,这些内容可以去参考实验指导书和《Linux内核完全注释》。因为我们要基于内核栈切换,所以肯定是要修改 switch_to 的,并且需要将原来的 switch_to 注释掉。switch_to的主要功能就是切换内核栈,需要进行4步,在程序注释中已经用数字序号标出。switch_to程序写在system_call.s文件中,switch_to的完整程序如下:

#...
#修改1 重新 switch_to 函数
.align 2
switch_to:
	pushl %ebp
	movl %esp,%ebp       # c调用汇编程序,先将ebp压栈,然后更新epb(栈帧结构)
	pushl %ecx
	pushl %ebx
	pushl %eax           # 将 ecx ebx eax 压栈,后面会pop回来
	movl 8(%ebp),%ebx    # pnext = 8 + %ebp
	cmpl %ebx,current    # 检查有没有发生切换
	je 1f                # 若没有发生切换则返回
	# 1、切换pcb
	movl %ebx, %eax
	xchgl %eax, current

	# 2、TSS中的内核栈指针的重写
	# tss是一个新定义的用于存放当前进程的tss的全局变量,原switch_to函数中是利用"ljmp %0"
	# 来修改TR,从而切换tss的。下面3句程序只修改了当前tss的esp0,却没切换TR和tss,
	# 这意味着所有进程将共用这个 tss ,TR也不会再更新,始终指向这个tss。这样做
	# 是因为调用int中断时(如 int 0x80),CPU会利用TR找寻找当前进程的内核栈,
	# 从而将用户栈的ss:sp压入内核栈中。那么问题来了,TR是什么时候指向tss的呢?
	# 找一下ltr指令和任务状态段的初始化过程,可以先猜一下。
	movl tss,%ecx
	addl $4096,%ebx
	movl %ebx,ESP0(%ecx)

	# 3、切换内核栈
	movl %esp, KERNEL_STACK(%eax)   # 保存当前的esp到pcb中
	# 再取一下 ebx,因为前面修改过 ebx 的值
	movl 8(%ebp), %ebx
	movl KERNEL_STACK(%ebx), %esp   # 取出下一个进程的esp 覆盖掉当前的esp

	# 4、切换LDT
	movl 12(%ebp), %ecx
	lldt %cx
	
	movl $0x17,%ecx
	mov %cx,%fs
	# 和后面的 clts 配合来处理协处理器,由于和主题关系不大,此处不做论述
	cmpl %eax,last_task_used_math
	jne 1f
	clts

1:	popl %eax
	popl %ebx
	popl %ecx
	popl %ebp
ret  # 这个ret很关键,在schedule()调用switch_to()时会将返回地址压栈,ret会
     # 将返回地址弹出。如果发生了进程切换,则此时栈指针(esp)已经指向了下
     # 一个进程的内核栈,ret会将下一个进程的返回地址弹出,从而跳转到下一个进程的代码去执行。

其中在第3步(切换内核栈)时,由于现在的 Linux 0.11 的 PCB 定义中没有保存内核栈指针这个域(kernelstack),所以需要加上,这个在实验指导书中有详细的介绍。
在 task_struct (sched.h)添加 “内核栈” 字段:

struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	/*修改1 添加内核栈字段*/
	long kernelstack;
	long signal;
	struct sigaction sigaction[32];
	long blocked;	/* bitmap of masked signals */
	....
}

PCB定义在 include/linux/sched.h 中,对PCB的修改如下:

/*
 *  INIT_TASK is used to set up the first task table, touch at
 * your own risk!. Base=0, limit=0x9ffff (=640kB)
 */
/*由于 Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),
其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址。可能不太好理解,但是要注意
栈是向下生长的,因此(long)&init_task 就是一页内存的起始地址(最低地址),
PAGE_SIZE+(long)&init_task 就是一页内存的结束地址(最高地址)*/
#define INIT_TASK \
/* state etc */	{ 0,15,15, \
/*修改1 内核栈 */	PAGE_SIZE+(long)&init_task, \
/* 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, \
		{} \
	}, \
}

extern struct task_struct *task[NR_TASKS];
extern struct task_struct *last_task_used_math;
extern struct task_struct *current;
/*修改2 自己定义一个全局tss,所有进程共用*/
extern struct tss_struct *tss;   

tss定义在sched.c文件中:

/*修改1 自己定义一个全局tss,所有进程共用*/
struct tss_struct *tss = &(init_task.task.tss);   

此外 system_call.s 文件中还要修改一些硬编码,并添加相关的全局声明:

state	= 0		# these are offsets into the task-struct.(这里的偏移量是按字节来算的)
counter	= 4
priority = 8
#修该1 修改KERNEL_STACK、signal、sigaction、blocked、ESP0 硬编码
KERNEL_STACK    = 12    # 把内核栈指针插到这里,后面的偏移量都要跟着改、
signal	= 16
sigaction = 20		# MUST be 16 (=len of sigaction),sigaction是一个含32个结构体元素的数组
blocked = (37*16)   # 这里不太明白为什么是(37*16),本来我写的是(20 + 32 * 16)
ESP0 		= 4     # tss中esp0(内核栈的栈顶)的偏移量(按字节算)
# offsets within sigaction
sa_handler = 0
sa_mask = 4
sa_flags = 8
sa_restorer = 12

nr_system_calls = 72

/*
 * Ok, I get parallel printer interrupts while using the floppy for some
 * strange reason. Urgel. Now I just ignore them.
 */
.globl system_call,sys_fork,timer_interrupt,sys_execve,switch_to,first_return_from_kernel
.globl hd_interrupt,floppy_interrupt,parallel_interrupt
.globl device_not_available, coprocessor_error
#修改2 将switch_to, first_return_from_kernel做全局声明
.globl switch_to, first_return_from_kernel

3.3 修改fork

修改fork主要是为了做出子进程的的内核栈:

//......
extern void write_verify(unsigned long address);
extern void first_return_from_kernel(void);     //在fork.c文件中声明first_return_from_kernel函数
long last_pid=0;
//......
/*
 *  Ok, this is the main fork-routine. It copies the system process
 * information (task[nr]) and sets up the necessary registers. It
 * also copies the data segment in it's entirety.
 */
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)
{
	struct task_struct *p;
	/*修改1 新增变量 krnstack */
	long *krnstack;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;

	/* 修改2 在调用int 0x80 的时候,int 指令已经将父进程用户态的ss,esp,eflags,cs,ip
       压入父进程的内核栈中,这里我们主要是做一下子进程的内核栈,让子进程和父进
       程的内核栈指向一个用户栈。*/
	krnstack = (long *)(PAGE_SIZE + (long)p);
	*(--krnstack) = ss & 0xffff;
	*(--krnstack) = esp;
	*(--krnstack) = eflags;
	*(--krnstack) = cs & 0xffff;
	*(--krnstack) = eip; 
	*(--krnstack) = ds & 0xffff;
	*(--krnstack) = es & 0xffff;
	*(--krnstack) = fs & 0xffff;
	*(--krnstack) = gs & 0xffff;
	*(--krnstack) = esi;
	*(--krnstack) = edi;
	*(--krnstack) = edx;
	/* 接下来的这段压栈有点费解,其实主要是为了让自己写的那个switch_to函数能够正
       确的返回,仔细回想一下调用fork()后整个的压栈过程就会明白了。至于那个压栈:
       *(--krnstack) = 0; 从弹栈的过程来看其实应该放入eax的,这是fork()后子进程的返回值 0 */
	*(--krnstack) = (long)first_return_from_kernel;
	*(--krnstack) = ebp;
	*(--krnstack) = ecx;
	*(--krnstack) = ebx;
	/* 这个0最有意思 */
	*(--krnstack) = 0;
	p->kernelstack = krnstack;

	p->pid = last_pid;/* last_pid是最新进程号,也就是子进程的pid */
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;/* 这里的eip是在调用 int 0x80 压入的eip,也就是说
                        子进程在下次(或者说第一次被调度的时候)被调度执行的的时候,是从 int 0x80 
                        后面一句指令开始执行的,而不是从copy_process()开始执行*/
	p->tss.eflags = eflags;
	p->tss.eax = 0;/* 子进程fork()完后返回0的原因所在*/
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;/* return会让返回值(last_pid)保存在eax中。这里是父进程在fork()完后要返回的子进程的pid。
                       那么子进程fork()完后要返回的0在哪里返回的呢?在 _syscall0(int,fork) 函数的那个return返回。*/
}

最后是 first_return_from_kernel 函数,该函数定义在 system_call.s 文件中:

# 修改1 开始的时候不太明白为什么要加这段函数,用switch_to在ret完后不应该是返回schedule()吗?
# 不应该是像我写的那个关于switch_to最后那句ret的注释一样去返回schedule()吗?但仔细一想,
# 在原程序中switch_to()通过那句"ljmp %0"直接切换的tss(或者是TR),然后直接跳到了下一个进程
# 去执行,并没有直接写出关于栈处理相关的指令
.align 2
first_return_from_kernel:
	popl %edx
	popl %edi
	popl %esi
	pop %gs
	pop %fs
	pop %es
	pop %ds
	iret

3.4 基于内核栈切换的进程切换过程

按照上面步骤修改完Linux0.11内核后,就可以编译、运行了:
图3.0 修改后的运行结果

图3.0 修改后的运行结果

虽然内核是重新跑起来了,不过由于修改过程中方向逐渐迷失,最后修改完了头也晕了,不知道自己做了什么,最后只好又重新整理一下基于内核栈切换的进程切换的过程。阅读修改完成后的内核代码,重新整理一下进程切换的过程。

3.4.1 父进程创建子进程时,父进程的压栈过程

在父进程创建子进程时,父进程会调用fork(),fork()是一个系统调用 ‘static inline _syscall0(int,fork)’。 因此父进程的压栈过程从fork函数中的那句 ‘int 0x80’ 开始看:
图3.1 父进程创建子进程时,父进程的压栈过程

图3.1 父进程创建子进程时,父进程的压栈过程

压栈的过程是地址逐渐减小的过程,因此 SS : ESP 到 addl$20,%esp 地址是在逐渐减小。copy_process 有一堆形参,这是汇编调用C(在linux内核完全注释中有介绍)的过程 ,因此上面压入栈中的内容都作为了 copy_process 的形参。

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)
		{......}

3.4.2 父进程创建子进程后,子进程内核栈的样子

子进程的内核栈是在 copy_process 函数中做出来的,并且子进程的内核栈应该是父进程内核栈的拷贝。copy_process 将子进程的内核栈做成下面这个样子:

图3.2 子进程内核栈的样子

图3.2 子进程内核栈的样子

假设父进程返回的过程中没有发生进程调度,即没有跳转到 reschedule函数:

system_call:
......
	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
ret_from_sys_call:
......

那么父进程在调用完 copy_process 函数后就会返回用户态了,出栈的顺序和入栈顺序(图3.1)恰好相反。有一点要注意一下,父进程返回过程中是不会调用 first_return_from_kernel: 的,而子进程返回却会调用, 这是为什么呢?

还有子进程的内核栈的样子好像和内核栈有一点点不一样:基本的内容一样,顺序有点不一样。接着往下分析,看看子进程被调用后,出栈的情况。

3.4.3 子进程被调度

假设 进程n 在执行的过程中发生了系统时钟中断,并且需要进行调度,恰好被调度到下一个要执行的进程就是上面创建好的子进程。这一部分从 system_call.s文件中的 timer_interrupt: 开始。首先是进程n进入系统时钟中断后内核栈的样子(样子有点粗糙,许多细节没考虑,但大致是这样的):
图3.3 进程n内核栈的样子

图3.3 进程n进入系统时钟中断后内核栈的样子

进程n执行 switch_to 后会将PCB,内核栈切换到上面做好的子进程那里去,然后开始了子进程返回用户态的过程,这里需要对照着图3.2 一起看:

.align 2
switch_to:
......
	jne 1f
	clts

1:	popl %eax       # 首先是 eax,ebx,ecx,ebp出栈
	popl %ebx
	popl %ecx
	popl %ebp
ret                 # ret会将返回地址出栈,然后到返回地址去执行。根据子进程的压栈顺序,这里的返回地址就是 first_return_from_kernel

程序跳转到 first_return_from_kernel:

.align 2
first_return_from_kernel:
	popl %edx      # 将edx,edi,esi,gs,fs,es,ds出栈
	popl %edi
	popl %esi
	pop %gs
	pop %fs
	pop %es
	pop %ds
	iret           # iret会将ss, esp, eflags, cs, eip 出栈

以下是关于iret指令的介绍,这段介绍摘取自:关于IRET指令

(1)当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。
(2)当使用IRET指令返回到一个不同的保护级别时,IRET不仅会从堆栈弹出以上内容,还会弹出堆栈段选择子及堆栈指针分别到SS与SP寄存器。

在执行完 iret 后 子进程就回到了用户态。子进程下次进入内核态时,内核栈的样子可能会和 图3.3 (进程n内核栈的样子 )一样,而不会再是图3.2 (子进程内核栈的样子)的样子,因此下次子进程在从内核态返回用户态时就不会在执行 first_return_from_kernel 了,可能这就是 first_return_from_kernel 取名的由来吧。

4 参考

[1] 哈工大操作系统实验四——基于内核栈切换的进程切换(极其详细)
[2] 关于IRET指令

日拱一卒无有尽,功不唐娟终入海。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
译者序 前言 第1章 系统概貌 1.1 历史 1.2 系统结构 1.3 用户看法 1.3.1 文件系统 1.3.2 处理环境 1.3.3 构件原语 1.4 操作系统服务 1.5 关于硬件的假设 1.5.1 中断与例外 1.5.2 处理机执行级 1.5.3 存储管理 1.6 本章小结 第2章 内核导言 2.1 UNIX操作系统的体系结构 2.2 系统概念介绍 2.2.1 文件子系统概貌 2.2.2 进程 2.3 内核数据结构 2.4 系统管理 2.5 本章小结 2.6 习题 第3章 数据缓冲区高速缓冲 3.1 缓冲头部 3.2 缓冲池的结构 3.3 缓冲区的检索 3.4 读磁盘块与写磁盘块 3.5 高速缓冲的优点与缺点 3.6 本章小结 3.7 习题 第4章 文件的内部表示 4.1 索引节点 4.1.1 定义 4.1.2 对索引节点的存取 4.1.3 释放索引节点 4.2 正规文件的结构 4.3 目录 4.4 路径名到索引节点的转换 4.5 超级块 4.6 为新文件分配索引节点 4.7 磁盘块的分配 4.8 其他文件类型 4.9 本章小结 4.10 习题 第5章 文件系统的系统调用 5.1 系统调用Open 5.2 系统调用read 5.3 系统调用write 5.4 文件和记录的上锁 5.5 文件的输入/输出位置的调整lseek 5.6 系统调用close 5.7 文件的建立 5.8 特殊文件的建立 5.9 改变目录及根 5.10 改变所有者及许可权方式 5.11 系统调用stat和fstat 5.12 管道 5.12.1 系统调用pipe 5.12.2 有名管道的打开 5.12.3 管道的读和写 5.12.4 管道的关闭 5.12.5 例 5.13 系统调用dup 5.14 文件系统的安装和拆卸 5.14.1 在文件路径名中跨越安装点 5.14.2 文件系统的拆卸 5.15 系统调用link 5.16 系统调用unlink 5.16.1 文件系统的一致性 5.16.2 竟争条件 5.17 文件系统的抽象 5.18 文件系统维护 5.19 本章小结 5.20 习题 第6章 进程结构 6.1 进程的状态和状态的转换 6.2 系统存储方案 6.2.1 区 6.2.2 页和页表 6.2.3 内核的安排 6.2.4 u区 6.3 进程的上下文 6.4 进程上下文的保存 6.4.1 中断和例外 6.4.2 系统调用的接口 6.4.3 上下文切换 6.4.4 为废弃返回(abortive return)而保存上下文 6.4.5 在系统和用户地址空间之间拷贝数据 6.5 进程地址空间的管理 6.5.1 区的上锁和解锁 6.5.2 区的分配 6.5.3 区附接到进程 6.5.4 区大小的改变 6.5.5 区的装入 6.5.6 区的释放 6.5.7 区与进程的断接 6.5.8 区的复制 6.6 睡眠 6.6.1 睡眠事件及其地址 6.6.2 算法sleep和wakeup 6.7 本章小结 6.8 习题 第7章 进程控制 7.1 进程的创建 7.2 软中断信号 7.2.1 软中断信号的处理 7.2.2 进程组 7.2.3 从进程发送软中断信号 7.3 进程的终止 7.4 等待进程的终止 7.5 对其他程序的引用 7.6 进程的用户标识号 7.7 改变进程的大小 7.8 Shell程序 7.9 系统自举和进程init 7.10 本章小结 7.11 习题 第8章 进程调度和时间 8.1 进程调度 8.1.1 算法 8.1.2 调度参数 8.1.3 进程调度的例子 8.1.4 进程优先权的控制 8.1.5 公平共享调度 8.1.6 实时处理 8.2 有关时间的系统调用 8.3 时钟 8.3.1 重新启动时钟 8.3.2 系统的内部定时 8.3.3 直方图分析 8.3.4 记帐和统计 8.3.5 计时 8.4 本章小结 8.5 习题 第9章 存储管理策略 9.1 对换 9.1.1 对换空间的分配 9.1.2 进程的换出 9.1.3 进程的换入 9.2 请求调页 9.2.1 请求调页的数据结构 9.2.2 偷页进程 9.2.3 页面错 9.2.4 在简单硬件支持下的请求调页系统 9.3 对换和请求调页的混合系统 9.4 本章小结 9.5 习题 第10章 输入输出子系统 10.1 驱动程序接口 10.1.1 系统配置 10.1.2 系统调用与驱动程序接口 10.1.3 中断处理程序 10.2 磁盘驱动程序 10.3 终端驱动程序 10.3.1 字符表Clist 10.3.2 标准方式下的终端驱动程序 10.3.3 原始方式下的终端驱动程序 10.3.4 终端探询 10.3.5 建立控制终端 10.3.6 间接终端驱动程序 10.3.7 注册到系统 10.4 流 10.4.1 流的详细的示例 10.4.2 对流的分析 10.5 本章小结 10.6 习题 第11章 进程间通信 11.1 进程跟踪 11.2 系统V IPC 11.2.1 消息 11.2.2 共享存储区 11.2.3 信号量 11.2.4 总的评价 11.3 网络通信 11.4 套接字 11.5 本章小结 11.6 习题 第12章 多处理机系统 12.1 多处理机系统的问题 12.2 主从处理机解决方法 12.3 信号量解决方法 12.3.1 信号量定义 12.3.2 信号量实现 12.3.3 几个算法 12.4 Tunis系统 12.5 性能局限性 12.6 习题 第13章 分布式UNIX系统 13.1 卫星处理机系统 13.2 纽卡斯尔连接 13.3 透明型分布式文件系统 13.4 无存根进程的透明分布式模型 13.5 本章小结 13.6 习题 附录A 系统调用 参考文献 索引

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值