简单分析操作系统的中断机制与进程上下文切换

#####################################
作者
:张卓
原创作品转载请注明出处:《Linux操作系统分析》MOOC课程 http://www.xuetangx.com/courses/course-v1:ustcX+USTC001+_/about
#####################################

1. 前言
上一篇我们详细地分析一段x86汇编程序的执行过程,知道了计算机是如何工作的,总结来说就是三个关键点:存储程序计算机、函数调用堆栈、和中断机制。
现在我们要简单分析一下操作系统是如何工作的,这里我将重点分析中断机制和进程上下文切换。
2. C代码中嵌入汇编代码
在分析操作系统中断机制和进程上下文之前,我们先了解一下C内嵌汇编,因为在操作系统进行进程上下文切换的时候,就是用内嵌汇编代码实现的。了解它,有助于我们深入理解代码执行流程。
2.1 内嵌汇编语法
__asm__(
    汇编语句模块:
    输出部分:
    输入部分:
    破坏描述部分);
    即格式为asm("statements": output_regs: input_regs: clobbered_regs)
同时 “asm” 也可以由 “__asm__” 来代替,“asm” 是 “__asm__” 的别名。在 “asm” 后面有时也会加上 “__volatile__” 表示编译器不要优化代码,后面的指令保留原样,“volatile” 是它的别名,在这里值得注意的是无论 “__asm__” 还是 “__volatile__” 中的每个下划线都不是一个单独的下划线,而是两个短的下划线拼成的。在后面括号里面的便是汇编指令。
2.2 C中内嵌汇编代码示例

#include
int main()
{
	/* val1+val2=val3 */
	unsigned int val1 = 1;
	unsigned int val2 = 2;
	unsigned int val3 = 0;
	printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
	asm volatile(
	"movl $0, %%eax\n\t"  /* clear %eax to 0 */
	"addl %1,%%eax\n\t"   /* %eax += val1 */
	"addl %2 %%eax\n\t"   /* %eax += val2 */
	"movl %%eax,%0\n\t"   /* val2 =%eax*/
	:"=m"(val3)           /* output=m mean only write output memory variable*/
	:"c"(val1),"d"(val2)  /* input c or d mean %ecx/%edx */
	);
	printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
	return 0;
}
解释:
1). 内嵌汇编指令中寄存器前面有两个%,是因为%在C语言中是特殊字符,需要转义才能输出%;C语言规定要输出%,需要两个%连用。
2). 内嵌汇编指令中%0,%1,%2...是代表输出输入部所用寄存器的编号。
3). 输出输入部,变量前面引号中的内容为限定符。
3. 中断机制
3.1 由CPU和内核代码共同实现了保存现场和恢复现场

当中断发生时,CPU将当前的eip,ebp,esp等压人内核堆栈空间,再将eip指向中断处理程序入口,执行中断服务程序。执行完毕后,恢复现场,继续刚才的程序。
3.2 借助实验楼环境模拟计算机工作模型及时钟中断
我们进入 实验楼环境,使用实验楼的虚拟机打开shell
1). cd LinuxKernel/linux-3.9.4
2). qemu -kernel arch/x86/boot/bzImage
然后cd mykernel 您可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c
简单分析操作系统的中断机制与进程上下文切换_第1张图片
执行后,可以看到屏幕上交替出现my_timer_handler和my_start_kernel。
下面来看看mymain.c的代码:
...
void __init my_start_kernel(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000 == 0)
            printk(KERN_NOTICE "my_start_kernel here  %d \n",i);
            
    }
}
...
这段代码就是操作系统的入口,系统从这里开始启动;只是在这个地方被我们重写。
myinterrupts.c:
...
void my_timer_handler(void)
{
	printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}
...
上面的一段就是发生时钟中断时,执行的代码。在这里我们对它执行的过程有一个初步的了解即可,在后面我们会详细分析中断发生时,操作系统做了那些工作。
4. 进程上下文切换
我们将在上面一个mykernel的实验环境下,模拟构建一个简单操作系统内核,完成一个简单的时间片轮转多道程序实验,通过这个实验来了解进程上下文是如何切换的。
同样我们进入 实验楼环境,执行下面的命令即可看到一个模拟内核的执行:
cd LinuxKernel/linux-3.9.4
rm -rf mykernel
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
make allnoconfig
make #编译内核请耐心等待
qemu -kernel arch/x86/boot/bzImage
简单分析操作系统的中断机制与进程上下文切换_第2张图片
源代码在 mykernel中,现在我们查看并分析代码:
在mypcb.h 文件中,定义进程管理相关的数据结构
mypcb.h
...
/* CPU specific state of this task */
struct Thread{
	unsigned long ip;
	unsigned long sp;
};
typedef struct PCB{
	int pid;
	volatile long state;
	char stack[KERNEL_STACK_SIZE];
	/* CPU-specific state of this task */
	struct Thread thread;
	unsigned long task_entry;
	struct PCB *next;
}tPCB;
...
在mymain.c文件中,内核初始化和0号进程启动
...
tPCB task[MAX_TASK_NUM];
tPCB *my_current_task = NULL;
volatile int my_need_sched = 0;
...
/* 初始化0号进程的数据结构 */
void __init my_start_kernel(void)
{
	int pid = 0;
	int i;
	
	task[pid].pid = pid;
	task[pid].state = 0; /* -1:unrunnable, 0: runnable, >0; stopped */
	task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; /* my_process函数的地址赋值给进程的入口地址 */
	task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
	task[pid].next = &task[pid];
	/* fork more process */
	for(i=1; i
0号进程启动后,执行my_process,在里面执行1千万次后,主动检查my_need_sched的值,看是否需要调到其他进程。my_need_sched是由时间片控制的。在myinterrrupt.c中,设置时间片的大小,时间片用完时设置一下调度标志:
void my_timer_handler(void) 
{ 
#if 1 
	if(time_count%1000 == 0 && my_need_sched != 1) 
	{ 
	printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); 
	my_need_sched = 1; 
	}  
	time_count ++ ;   
#endif 
return;  	 
} 
当需要进程调度时,也是在my_process中调用my_schedule:
/* 两个正在运行的进程之间做进程上下文切换 */
...
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 
{         
	my_current_task = next;  
	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);   
	/* switch to next process */ 
	asm volatile(	 
	   	"pushl %%ebp\n\t" 	    /* save ebp */ 
	   	"movl %%esp,%0\n\t" 	/* save esp */ 
	   	"movl %2,%%esp\n\t"     /* restore  esp */ 
	   	"movl $1f,%1\n\t"       /* save eip */	 /*$1f是指接下来的标号1:的位置*/
	   	"pushl %3\n\t"  
	   	"ret\n\t" 	            /* restore  eip */ 
	   	"1:\t"                  /* next process start here */ 
	   	"popl %%ebp\n\t" 
	   	: "=m" (prev->thread.sp),"=m" (prev->thread.ip) 
	   	: "m" (next->thread.sp),"m" (next->thread.ip) 
	);  
}
else    /* 切换到一个新进程的方法 */
{    
	next->state = 0;    
	my_current_task = next;    
	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);    
	/* switch to new process */    
	asm volatile(	    
	    	"pushl %%ebp\n\t" 	    /* save ebp */    
	    	"movl %%esp,%0\n\t" 	/* save esp */    
	    	"movl %2,%%esp\n\t"     /* restore  esp */    
	    	"movl $1f,%1\n\t"       /* save eip */	    
	    	"pushl %3\n\t"     
	    	"ret\n\t" 	            /* restore  eip */    
	    	"movl %2,%%ebp\n\t"     /* restore  ebp */    
	    	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)    
	    	: "m" (next->thread.sp),"m" (next->thread.ip)    
		);              
}     
...
可以看出,切换进程的汇编程序和函数调用堆栈执行相似,其实原来也是相通的;但是切换进程确实要复杂得多。
5.总结
操作系统通过断上下文和进程上下文的切换完成多任务并行执行,各个任务的调度是通过时间片控制的。

你可能感兴趣的:(简单分析操作系统的中断机制与进程上下文切换)