操作系统的进程调度简析
陈铁 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”。特别说明,所有代码出自孟宁老师的mykernel,也许出于练习的目的有所修改,也可忽略。
学习的过程其实就是不断的模仿,重复老师演示的内容,不断地练习,直到成为自己所能独立表述的知识。自己实在很笨了,作业勉强完成,好在也算努力,花时间多些,毕竟是自己的辛苦学习的过程体现。所以摆出来给方家一笑,好歹也是自己学习的收获。
一、 实验用的是实验楼环境,虚拟机环境如下:Linux d0c756f6c18a 3.13.0-30-generic #55-Ubuntu SMP Fri Jul 4 21:40:53 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux。实验开始使用简单代码,可以看见中断调度演示。
cd LinuxKernel/linux-3.9.4 qemu -kernel arch/x86/boot/bzImage |
二、将老师的代码mypch.b,mymain.c,myinterrupt.c复制到mykernel目录中。回到kernel目录下:
make all qemu -kernel arch/x86/boot/bzImage |
就可以看到进程调度的过程在虚拟机中体现出来。以下截图:
三、下面来分析一下代码的执行过程,描述一下现代操作系统的工作机制。
1.在linux核心中为了实现高效执行,大量使用了内联汇编,所以在此先介绍一下内联汇编的相关知识。(1)虽然现代编译器优化代码,但仍比不过手写的汇编代码;(2)有些平台相关的指令必须手写,在C语言中没有等价的语法,例如x86是端口I/O。
gcc
提供了一种扩展语法可以在C代码中使用内联汇编。最简单的格式是__asm__("assembly code");
,例如__asm__("nop");
就只是执行一条空指令。执行多条汇编指令,则应该用\n\t
将各条指令分隔开。
内联汇编要和C的变量建立关联,使用完整的内联汇编格式:
__asm__(assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* optional */ );
这种格式由四部分组成,第一部分是汇编指令,第二部分和第三部分是约束条件,第二部分指示汇编指令的运算结果
要输出到哪些C操作数中,C操作数应该是左值表达式,第三部分指示汇编指令需要从哪些C操作数获得输入,第四部分是在汇编指令中被修改过的寄存器列表,指示编译器哪些寄存器的值在执行这条__asm__
语句时会改变。后三个部分都是可选的,如果有就填写,没有就空着只写个:
号。
2.mypcb.h代码如下:
/* * linux/mykernel/mypcb.h * * Kernel internal PCB types * * Copyright (C) 2013 Mengning * */ #define MAX_TASK_NUM 4 //定义系统执行的最大进程数。 #define KERNEL_STACK_SIZE 1024*8 //内核堆栈大小 /* CPU-specific state of this task */ struct Thread { //定义结构体Thread unsigned long ip; //存储指令指针和堆栈指针 unsigned long sp; }; typedef struct PCB{ //结构体类型进程控制块PCB int pid; //进程id volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ //进程状态 char stack[KERNEL_STACK_SIZE]; //进程堆栈 /* CPU-specific state of this task */ struct Thread thread; unsigned long task_entry; //入口 struct PCB *next; //形成链表,下一个进程 }tPCB; void my_schedule(void); //调度函数
3.以下mymain.c主程序代码
/* * linux/mykernel/mymain.c * * Kernel internal my_start_kernel * * Copyright (C) 2013 Mengning * */ #include <linux/types.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/tty.h> #include <linux/vmalloc.h> #include "mypcb.h" tPCB task[MAX_TASK_NUM]; //定义进程数组 tPCB * my_current_task = NULL; //当前进程指针,从0号进程开始 volatile int my_need_sched = 0; //0号进程不需要调度 void my_process(void); void __init my_start_kernel(void) //内核创建进程,从0号进程开始初始化 { int pid = 0; int i; /* Initialize process 0*/ 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; //堆栈指向定义的内核Stack 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<MAX_TASK_NUM;i++) //通过fork函数启动更多的进程,本例0,1,2,3 { //我们是简单演示,此处直接复制0号进程的状况作为新的进程 memcpy(&task[i],&task[0],sizeof(tPCB)); task[i].pid = i; task[i].state = -1; task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; task[i].next = task[i-1].next; //进程之间形成链表 task[i-1].next = &task[i]; } /* start process 0 by task[0] */ //启动0号进程 pid = 0; my_current_task = &task[pid]; /* 内联汇编,%0,%1代表输入输出部分的变量"c"代表ECX,"d"代表EDX,"=m"表示内存 %%reg表示寄存器。\n\t表示结束。 以下汇编代码不难理解,就是为了效率。构建起CPU的运行环境,启动了0号进程。 */ asm volatile( "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ "pushl %1\n\t" /* push ebp */ "pushl %0\n\t" /* push task[pid].thread.ip */ "ret\n\t" /* pop task[pid].thread.ip to eip */ "popl %%ebp\n\t" : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ ); } /*以下是我们的简单进程所执行的代码,用来让人类知道CPU执行了哪个进程。实际上很多操作系统进程 只是在后台执行,并不需要进行人机交互,但我们不要忽略了它们。 */ void my_process(void) { int i = 0; while(1) { i++; if(i%10000000 == 0) //循环一千万次,输出一次进程id,主动调度,避免消息机制。 { printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); if(my_need_sched == 1) { my_need_sched = 0; my_schedule(); } printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); } } }
4.以下是myinterrupt.c的代码及简单说明:
/* * linux/mykernel/myinterrupt.c * * Kernel internal my_timer_handler * * Copyright (C) 2013 Mengning * */ #include <linux/types.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/tty.h> #include <linux/vmalloc.h> #include "mypcb.h" extern tPCB task[MAX_TASK_NUM]; extern tPCB * my_current_task; extern volatile int my_need_sched; volatile int time_count = 0; //时间计数已实现主动执行,我们的简单代码不接受输入 /* * Called by timer interrupt. * it runs in the name of current running process, * so it use kernel stack of current running process */ void my_timer_handler(void) { #if 1 //计数1000次并且没有切换进程就输出一行提醒 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; } void my_schedule(void) { tPCB * next; tPCB * prev; if(my_current_task == NULL || my_current_task->next == NULL) { return; //出错处理 } printk(KERN_NOTICE ">>>my_schedule<<<\n"); /* schedule */ next = my_current_task->next; prev = my_current_task; if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ { /* 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 */ "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) ); my_current_task = next; printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); } 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 %2,%%ebp\n\t" /* restore ebp */ "movl $1f,%1\n\t" /* save eip */ "pushl %3\n\t" "ret\n\t" /* restore eip */ : "=m" (prev->thread.sp),"=m" (prev->thread.ip) : "m" (next->thread.sp),"m" (next->thread.ip) ); } return; }
四、实验总结,老师简化的代码还不难理解,但要自己编写还没有这个本事,所以直接抄下来自己理解一下,执行的过程没有出现报错。虽然是简化代码,但对于理解操作系统的工作机制还是很有帮助的。首先是内核的自举,毕竟所有的程序都不过是内存中的代码,内核不过是认为指定了特权,0号进程,开始运行,自己建立自己所需要的环境。其次,操作系统毕竟是为实际的程序服务的,接下来就要负责创建其他进程执行环境、资源分配,采用链表机制切换到新进程,并且执行。最后,内核要负责管理进程的状态,利用中断机制实现进程切换,控制程序的执行。总之,操作系统所作的就是中断上下文的处理和进程切换上下文的处理。
本文出自 “StudyPark” 博客,请务必保留此出处http://swordautumn.blog.51cto.com/1485402/1619999
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。