一个简单的时间片轮转多道程序内核代码
Casualet + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
本文将基于linux 内核3.9.4 来分析一个简单的时间片轮转的多道程序的代码, 并总结其中的原理.
首先,我们要搭建一个实验环境,方便我们来运行代码.本文采用的主机环境是ubuntu14.04.1, 搭建环境的方法可以参考 https://github.com/mengning/mykernel . 总体上分为以下几个步骤:
1.sudo apt-get install qemu
该软件模拟了x86 cpu 的运行,可以用它来运行内核代码
2.sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
安装完上面的软件以后, 我们可以通过qemu-system-i386 来使用. 但是这个名字太长, 所以我们建立一个名字比较短的链接.这样就可以通过qemu来调用.
3.wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz
获得linux内核代码
4.wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch
获得内核的补丁,应该是对内核进行了修改, 是的我们可以使用这个环境.
5. xz -d linux-3.9.4.tar.xz
tar -xvf linux-3.9.4.tar
cd linux-3.9.4
对压缩包进行解压, 然后进入解压目录.
6.patch -p1 < ../mykernel_for_linux3.9.4sc.patch
使用下载的补丁修改内核文件.
7.make allnoconfig
make
这一步完成后, 经过一段时间的编译, 就可以获得如下图所示的结果,注意倒数第四行的BUILD arch/x86/boot/bzImage, 这个是我们下面要使用的文件.
我们发现, 当前目录下多了一个mykernel 文件夹, 里面有我们后面需要修改的代码mymain.c 以及myinterrupt.c. 我们先按现有的运行命令:
qemu -kernel arch/x86/boot/bzImage
可以发现, myinterrupt.c 的代码开始执行,效果如下:
这样,我们有了一个基础的运行环境, 以后只要修改mymain.c 以及myinterrupt.c 然后使用上面的方法, 重新编译,就可以看到自己的程序的运行效果. 接下来, 我们在这个的基础上, 对
mykernel中的代码进行修改, 重新编译, 运行, 并解释过程中的原理.
---------------------------------------分割线------------------------------------------------------------------
首先, 进程是运行的一段代码, 那么是什么代码呢? 我们在这里指定一个自己写的c函数, 作为所有要调度的进程的代码. 这个函数是my_process, 他的作用是, 打印出当前进程的id, 然后在适当的时候调用schedule函数, 进行调度.
为了管理这么多的进程, 我们需要进程控制块. 进程控制块是一个结构体, 用来管理进程. 那么这个结构体包含什么内容呢? 首先, 为了标识不同的进程, 我们需要有进程id, 这个id可以用一个简单的int变量来表示. 进程有自己的代码, 必须要有一直指针指向代码的位置. 进程还得有自己的状态信息. 这样, 我们可以写一个进程空置块的结构体描述:
struct Thread {
unsigned long ip;
unsigned long sp;
};
typedef struct PCB{
int pid;
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;
然后, 我们会初始化进程控制块数组信息, 并且启动进程0, 先给出带有注释的代码.
/*
* linux/mykernel/mymain.c
*
* Kernel internal my_start_kernel
*
* Copyright (C) 2013 Mengning
*
*/
#include <linux/types.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/stackprotector.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/initrd.h>
#include <linux/bootmem.h>
#include <linux/acpi.h>
#include <linux/tty.h>
#include <linux/percpu.h>
#include <linux/kmod.h>
#include <linux/vmalloc.h>
#include <linux/kernel_stat.h>
#include <linux/start_kernel.h>
#include <linux/security.h>
#include <linux/smp.h>
#include <linux/profile.h>
#include <linux/rcupdate.h>
#include <linux/moduleparam.h>
#include <linux/kallsyms.h>
#include <linux/writeback.h>
#include <linux/cpu.h>
#include <linux/cpuset.h>
#include <linux/cgroup.h>
#include <linux/efi.h>
#include <linux/tick.h>
#include <linux/interrupt.h>
#include <linux/taskstats_kern.h>
#include <linux/delayacct.h>
#include <linux/unistd.h>
#include <linux/rmap.h>
#include <linux/mempolicy.h>
#include <linux/key.h>
#include <linux/buffer_head.h>
#include <linux/page_cgroup.h>
#include <linux/debug_locks.h>
#include <linux/debugobjects.h>
#include <linux/lockdep.h>
#include <linux/kmemleak.h>
#include <linux/pid_namespace.h>
#include <linux/device.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/signal.h>
#include <linux/idr.h>
#include <linux/kgdb.h>
#include <linux/ftrace.h>
#include <linux/async.h>
#include <linux/kmemcheck.h>
#include <linux/sfi.h>
#include <linux/shmem_fs.h>
#include <linux/slab.h>
#include <linux/perf_event.h>
#include <linux/file.h>
#include <linux/ptrace.h>
#include <linux/blkdev.h>
#include <linux/elevator.h>
#include <asm/io.h>
#include <asm/bugs.h>
#include <asm/setup.h>
#include <asm/sections.h>
#include <asm/cacheflush.h>
#ifdef CONFIG_X86_LOCAL_APIC
#include <asm/smp.h>
#endif
#include "mypcb.h"
tPCB task[MAX_TASK_NUM];//所有参加调度的进程的控制信息数组
tPCB * my_current_task = NULL;//指向当前进程,初始是0号
volatile int my_need_sched = 0;//一个共享的变量
void my_process(void);
#define MAIN_FREQUENCE 100000000
void __init my_start_kernel(void)
{
int pid = 0;
int i;
task[pid].pid = pid;
task[pid].state = 0;
task[pid].task_entry = task[pid].thread.ip =(unsigned long)my_process;
task[pid].thread.sp = (unsigned long) &task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].next = &task[pid];
/*以上代码实现了第0个进程控制块信息的初始化工作, 设置了pid=0;
* state=0;然后设置了函数my_process的位置作为ip的位置,并且
* 设置了sp执行自己申请的stack空间的最后一个字节的地址.
*next指针指向了自己, 这个是暂时的,后面会做修改.
*/
for(i=1;i<MAX_TASK_NUM;i++){
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-1].next = &task[i];
}
//task[MAX_TASK_NUM -1].next = &task[0];
/*
* 这里使用了循环, 初始化了整个进程控制的数组. 基本的想法是,
* 使用memcpy()函数, 复制task[0]的内容, 然后根据各个task的实际
* 情况,进行定制. 定制的内容有:
* pid, state,sp. 并且利用next指针使得整个的控制信息形成了一个
* 链式的结构.
*/
pid = 0;
my_current_task = &task[pid];
asm volatile(
"movl %1,%%esp\n\t"
"pushl %1\n\t"
"pushl %0\n\t"
"ret\n\t"
"popl %%ebp\n\t"
:
:"c"(task[pid].thread.ip),"d"(task[pid].thread.sp)
);
/*
* 上面使用了嵌入式汇编, 启动了进程0的代码,开始执行进程0的my_process
* 代码. 其基本过程是:
* 首先,初始化的时候, sp指向了task空间的最后一位,所以,要开始
* 进入task[0]的环境,需要使得esp寄存器只想正确的位置. 在这个时候,
* 通过movl是的栈的最后一个字节地址赋给esp, 以后每次压栈,esp都会减小,
* 这样进程0就可以用这个栈空间了.
* 然后由于这个时候,初始状态,esp=ebp,所以我们通过pushl等效达到了
* 保存ebp值的目的. 后面的push 和 ret 是一起用的, 先push ip的值,
* 然后ret是的ip的值放到eip寄存器中. 由于ip中包含了my_process函数的地址
* 所以,这里可以实现跳转到my_process函数进行执行的效果.
*
*/
}
void my_process(void){
int i = 0;
while(1){
i++;
if(i%MAIN_FREQUENCE == 0){
int j = 0;
for(;j<3;j++){
my_delay(1000);
printk(KERN_NOTICE "this is process %d time=%d-\n",my_current_task->pid,j);
}
if(my_need_sched ==1){
my_need_sched = 0;
printk("start schedule in main pid= %d",my_current_task->pid);
my_schedule();
}
printk(KERN_NOTICE "\nPID=%d, after",my_current_task->pid);
}
}
}
/*对这个函数进行少量的修改, 并且从第一次的进程0开始讲起.
*进程0通过上面的机制跳进了这个函数开始执行, 这是一个无限循环.每次
if(i%**)判断成功, 就会循环打印30次的信息, 然后判断my_need_shed是不是
=1.如果=1就会调用schedule函数, 跳到其他进程. 其他进程由于PCB的信息中ip
也是指向这个函数, 所以也会执行这段代码, 但是执行的状态是不一样的.
*
*/
void my_delay(int n){
int i,j;
volatile int temp;
for(i=0;i<n;i++)
for(j=0;j<300000;j++)
temp = j;
}
接下来就是中断,始终中断设置了一个变量的值, 是的调度能够进行, 现给出代码以及注释.
/*
* linux/mykernel/myinterrupt.c
*
* Kernel internal my_timer_handler
*
* Copyright (C) 2013 Mengning
*
*/
#include <linux/kernel_stat.h>
#include <linux/export.h>
#include <linux/interrupt.h>
#include <linux/percpu.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/swap.h>
#include <linux/pid_namespace.h>
#include <linux/notifier.h>
#include <linux/thread_info.h>
#include <linux/time.h>
#include <linux/jiffies.h>
#include <linux/posix-timers.h>
#include <linux/cpu.h>
#include <linux/syscalls.h>
#include <linux/delay.h>
#include <linux/tick.h>
#include <linux/kallsyms.h>
#include <linux/irq_work.h>
#include <linux/sched.h>
#include <linux/sched/sysctl.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <asm/div64.h>
#include <asm/timex.h>
#include <asm/io.h>
#define CREATE_TRACE_POINTS
#include <trace/events/timer.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 = 1;
#define INTERRUPT_FREQUENCE 1000
/*
* Called by timer interrupt.
*/
void my_timer_handler(void){
if(time_count%INTERRUPT_FREQUENCE==0 && my_need_sched !=1){
printk(KERN_NOTICE "\n>>>>>>>time_count = %d,let me chage my_nee_sched to 1!\n\n",time_count++);
my_need_sched = 1;
time_count = 0;
}
time_count++;
}
/*这里的my_time_handler函数, 每次时钟中断产生的时候, 就会被调用.
*调用的时候, time_count会字增, 如果达到了INTERRUPT_FREQUENCE且
当前的my_need_sched==0, 则把my_need_sched=1, 从而正在运行的
my_process函数可以检测到这个变化, 调用my_schedule函数.
*/
void my_schedule(void){
tPCB * next;
tPCB * prev;
if(my_current_task == NULL || my_current_task->next == NULL){
printk(KERN_NOTICE "ERROR STATE");
return;
}
next = my_current_task->next;
prev = my_current_task;
if(next->state == 0){
printk(KERN_NOTICE "not first switch from prev=%d to next=%d",prev->pid, next->pid);
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;
}else{
next->state = 0;
my_current_task = next;
printk(KERN_NOTICE "FIRST START switch from prev=%d to next=%d",prev->pid, next->pid);
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)
);
}
printk(KERN_NOTICE,"\nfinish my schedule prev=%d, next=%d\n",prev->pid,next->pid);
return;
}
总结:
这个程序展示了如何写一个时间片轮转的调度的程序. 首先,我们有一个基础, 就是有一个入口函数void __init my_start_kernel(void), 我们在这里初始化了进程控制块信息, 并且通过push 和ret 跳转到了进程0的执行代码.
然后在进程0中, 有一个判断:
if(my_need_sched ==1){
my_need_sched = 0;
printk("start schedule in main pid= %d",my_current_task->pid);
my_schedule();
}
这个变量的值可以通过时钟中断处理函数来完成设置, 这样, 每个进程运行一段时间, 就检查一次变量, 如果符合条件, 就调用schedule函数进行进程切换. 所谓的进程切换, 就是从一个进程的eip切换到另外一个进程的eip, 同时要保持一些必要的信息, 如esp,ebp等, 这些信息都是保持在进程对应的结构体中的, 这些结构体是一个链表的结构, 有一个current 指针只想当前的pcb, 要进行切换, 只要通过next指针跳到下一个就可以了.
最后附上运行的截图: