朱婷婷 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
初次学习linux内核相关的知识,如果有些写的不对或者理解的不正确希望大牛们指正。
一、实验前温习
计算机有三个法宝:1.存储程序计算机,就是之前有提到的冯诺依曼体系结构2.函数调用堆栈。C语言编译器对堆栈的使用有一套规则,了解堆栈存在的目的以及编译器对于堆栈的使用规则是理解操作系统调用代码的基础。 3.中断机制。 有了中断之后,cpu就可以跑多道程序,由cpu和内核共同实现保存现场和恢复现场。
二、实验内容
完成一个简单的时间片轮转多道程序内核代码,进程的启动和进程的切换机制
/*
* linux/mykernel/mypcb.h
*
* Kernel internal PCB types
*
* Copyright (C) 2013 Mengning
*
*/
#define MAX_TASK_NUM 4 //最大进程数
#define KERNEL_STACK_SIZE 1024*2 # unsigned long //每个进程内核栈的大小
/* CPU-specific state of this task */
struct Thread {
unsigned long ip; //用于保护进程的eip
unsigned long sp; //用于保护进程的esp
};
typedef struct PCB{
int pid; //进程的id号
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
unsigned long 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);
该头文件定义了一个进程控制块,主要是线程结构体和进程控制块的结构体。在PCB结构体定义了进程入口
/*
* 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;
volatile int my_need_sched = 0;
void my_process(void);
void __init my_start_kernel(void)
{
int pid = 0; //初始化当前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; //0号进程的入口地址是my_process()
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; //0号进程的栈顶指针为指向进程内核栈的最后一个元素
task[pid].next = &task[pid]; //才启动的时候系统里面只有自己一个进程,所以next指针指向自己
/*fork more process */
for(i=1;i<MAX_TASK_NUM;i++)
{
memcpy(&task[i],&task[0],sizeof(tPCB)); //复制1,2,3号进程
task[i].pid = i;
//*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-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] */
pid = 0;
my_current_task = &task[pid];
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 */
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
}
int i = 0;
void my_process(void)
{
while(1)
{
i++;
if(i%10000000 == 0)
{
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);
}
}
}
在开始处理0号进程的时候,有一段嵌入式汇编代码,以下来说下此处汇编代码的含义及作用:
movl %1,%%esp\n\t
将0号进程edx寄存器也就是task[pid].thread.sp赋值给esp,
pushl %1\n\t
由于当前是空栈,所以此处相当于将ebp压栈
pushl %0\n\t
将当前进程的eip压栈
ret
也就是pop eip 进入process()
到此为止内核的初始化工作就完成了,并且启动了0号进程,在上面代码中我们把所有进程都用的process的代码,主动执行process,执行一千万次才会调度一次
/*
* 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)
/*当时钟中断发生1000次并且my_need_sched不等于1的时候,把my_need_sched赋为1.当进程发现my_need_sched=1, 就会执行my_schedule*/
{
#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;
}
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" /* 保存当前进程的ebp到自己的栈中。 save ebp */
"movl %%esp,%0\n\t" /* 保存当前进程的esp到自己的栈中。 save esp */
"movl %2,%%esp\n\t" /* 从next->thread.sp中弹出下一个进程的esp。与第二句相对应。 restore esp */
"movl $1f,%1\n\t" /* 将下一个进程的eip设置为1f。$1f就是指标号1:的代码在内存中存储的地址 save eip */
"pushl %3\n\t" /* 将next->thread.ip压入当前进程的栈中。*/
"ret\n\t" /* 从当前进程的栈中弹出刚刚压入的next->thread.ip。完成进程切换。 restore eip */
"1:\t" /* 即$1f指向的位置。next process start here */
"popl %%ebp\n\t" /* 切换到的进程把ebp从栈中弹出至ebp寄存器。与第一句相对应。*/
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
my_current_task = next; //当前进程切换为next
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); //打印切换信息
}
else//如果下一个将要运行的进程还从未运行过。
{
next->state = 0;//将其设置为运行状态。
my_current_task = next;当前进程切换为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" /* 将要被切换出去的进程的ip设置为$1f。这样等一下它被切换回来时(一定是运行状态)肯定会进入if判断分支,可以从if中的标号1处继续执行。 save eip */
"pushl %3\n\t" /* 将next->thread.ip(因为它还没有被运行过,所以next->thread.ip现在仍处于初始状态,即指向my_process(),压入将要被切换出去的进程的堆栈。*/
"ret\n\t" /* 将刚刚压入的next->thread.ip出栈至eip,完成进程切换。 restore eip */
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
return;
}
这段代码是进程切换的关键,个人觉得关键就在于保存ebp(栈底)和esp (栈顶)的信息。 if()里面执行的内容是发生在所有进程不是第一次执行时,因为第一次执行的时候除了0号进程的状态是runnable,其他进程都是第一次执行,因此它们的状态都是unrunable, 走else路径
pushl %%ebp\n\t
这句话保存当前进程的ebp栈底指针的信息
"movl %%esp,%0\n\t"
把当前进程的esp赋给prev->thread.sp保存起来放到内存中,以便以后恢复
movl %2,%%esp\n\t
把下一个的进程的next->thread.sp赋给寄存器esp,此时已经处于下一个进程的堆栈空间了
movl $1f,%1\n\t
将跳转地址赋给当前eip,这样做的目的是等到下次回到该进程的时候可以继续从这里执行
pushl %3\n\t
把下一个进程的eip push到堆栈中
ret\n\t"
然后pop eip ,切换到新的进程
1:\t
回到进程指令1的位置
popl %%ebp\n\t
又切换回来原来的进程了
整个过程的分析如下:
首先内核从__init my_start_kernel()开始启动,创建了0,1,2,3四个进程,一开始只有0号进程处于runnable状态,即task[0].state = 0。 并且0号的函数入口被初始化为my_process(), 然后0号进程不断运行,直到my_timer_handler()被内核调用, 触发了中断后my_need_sched = 1, 然后0号进程在在my_process()函数里走到if(my_need_sched == 1)分支,进入my_schedule(void)开始执行进程调度,当时0号进程的next是1号进程,由于1号进程还没有被调度过,因此next->state == -1,走else分支
完成进程的切换,同样的2,3,4的进程切换流程也是如此。
三、自己的理解
如孟宁老师所说,操作系统的两把剑分别是中断上下文和进程上下文的切换。有了中断后系统可以同时跑多道程序。当一个中断发生后,cpu就会把当前的eip, esp, ebp压到内核栈中,然后把eip指向一个中断处理的程序入口, 然后保护现场。 由CPU 和内核代码共同实现了保存现场和恢复现场。