陈铁 + 原创作品转载请注明出处 + 《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
原文:http://swordautumn.blog.51cto.com/1485402/1619999