Linux操作系统内核的进程调度简析
学习的过程其实就是不断的模仿,重复老师演示的内容,不断地练习,直到成为自己所能独立表述的知识。自己实在很笨了,作业勉强完成,好在也算努力,花时间多些,毕竟是自己的辛苦学习的过程体现。所以摆出来给方家一笑,好歹也是自己学习的收获。
一、 实验用的是实验楼环境,虚拟机环境如下: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号进程,开始运行,自己建立自己所需要的环境。其次,操作系统毕竟是为实际的程序服务的,接下来就要负责创建其他进程执行环境、资源分配,采用链表机制切换到新进程,并且执行。最后,内核要负责管理进程的状态,利用中断机制实现进程切换,控制程序的执行。总之,操作系统所作的就是中断上下文的处理和进程切换上下文的处理。