LINUX内核分析第一周学习总结——操作系统是如何工作的
黄韧(原创作品转载请注明出处)
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
【知识点总结】
(一)计算机是如何工作的?(总结)——三个法宝
- 存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
- 函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;函数参数传递机制和局部变量存储
- 中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
(二)函数调用堆栈
堆栈
1.堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间。
2.堆栈存在的目的:函数调用框架;传递参数;保存返回地址;提供局部变量空间;等等。
3.C语言编译器对堆栈的使用有一套的规则。
4.了解堆栈存在的目的和编译器对堆栈使用的规则是理解操 作系统一些关键性代码的基础。
堆栈寄存器和堆栈操作
1.堆栈相关的寄存器:esp,堆栈指针(stack pointer):ebp,基址指针(base pointer)
2.堆栈操作:push 栈顶地址减少4个字节(32位) pop 栈顶地址增加4个字节
3.ebp在C语言中用作记录当前函数调用基址
其他关键寄存器
cs : eip:总是指向下一条的指令地址
• 顺序执行:总是指向地址连续的下一条指令
• 跳转/分支:执行这样的指令的时候,cs : eip的值会 根据程序需要被修改
• call:将当前cs : eip的值压入栈顶,cs : eip指向被 调用函数的入口地址
• ret:从栈顶弹出原来保存在这里的cs : eip的值,放 入cs : eip中
• 发生中断时
(三)借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
当一个中断信号发生时,CPU把当前的eip,esp,ebp压到内核堆栈中去,并把eip指向中断处理程序的入口。
(四)在mykernel基础上构造一个简单的操作系统内核
【实验指导】
mykernel实验指导
虚拟机打开shell
- cd LinuxKernel/linux-3.9.4
- qemu -kernel arch/x86/boot/bzImage
然后cd mykernel ,可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c
mymain.c
myinterrupt.c
完成一个简单的时间片轮转多道程序内核代码
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 //定义进程控制块,实际内存叫TASK_STRUCT /* CPU-specific state of this task */ struct Thread { //用来存储eip,esp unsigned long ip; //保存eip unsigned long sp; //保存esp }; 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; //定义程序入口,一般是main函数,这里指定了 struct PCB *next; //进程用链表链起来 }tPCB; void my_schedule(void); //调度器
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]; //声明一个TASK数组 tPCB * my_current_task = NULL; //声明当前TASK的指针 volatile int my_need_sched = 0; //是否需要调度的标志 void my_process(void); void __init my_start_kernel(void) { int pid = 0; int i; /* Initialize process 0*/ task[pid].pid = pid; //初始化当前的0号进程 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ //状态:正在运行 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; //入口实际上是my_process task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; //栈顶为stack task[pid].next = &task[pid]; //指向自己本身 /*fork more process */ //初始化更多的进程 for(i=1;i<MAX_TASK_NUM;i++) { memcpy(&task[i],&task[0],sizeof(tPCB)); task[i].pid = i; //复制0号进程的状态 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]; //新fork的进程放在进程列表的尾部 } /* start process 0 by task[0] */ pid = 0; //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 */ "popl %%ebp\n\t" : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ ); } //内核初始化完成,启动了0号进程 void my_process(void) { int i = 0; while(1) { i++; if(i%10000000 == 0) //执行1000万次 { printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); //输出,主动调度 if(my_need_sched == 1) //执行1000万次调度1次 { my_need_sched = 0; my_schedule(); } printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); } } }
每循环十万次打印一个
嵌入式汇编
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一些全局的东西 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 if(time_count%1000 == 0 && my_need_sched != 1) //时间中断一千次,并且my_need_sched不等于1时,把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 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 */ //保存当前进程的ebp "movl %%esp,%0\n\t" /* save esp */ //当前进程的esp赋到0,即thresd.sp "movl %2,%%esp\n\t" /* restore esp */ //把下一个进程的sp放入esp中 "movl $1f,%1\n\t" /* save eip */ //保存eip "pushl %3\n\t" //把下一个进程的eip push到栈里 "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 */ //保存当前ebp "movl %%esp,%0\n\t" /* save esp */ //保存当前esp "movl %2,%%esp\n\t" /* restore esp */ //重新记录要跳转进程的esp,%2为next->thread.sp "movl %2,%%ebp\n\t" /* restore ebp */ //重新记录要跳转进程的ebp,%2为next->thread.sp "movl $1f,%1\n\t" /* save eip */ //保存当前eip,%1为next->thread.ip,%1f指标号:1的代码在内存中存储的地址 "pushl %3\n\t" "ret\n\t" /* restore eip */ //重新记录要跳转进程的eip,%3为next->thread.ip : "=m" (prev->thread.sp),"=m" (prev->thread.ip) : "m" (next->thread.sp),"m" (next->thread.ip) ); } return; }
总结:
实验中,在上述函数的作用下,成功地实现了时间片轮转的中断处理内核的功能。通过分析源码,我们了解到时间片轮转算法的具体方法,即每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。这是一种最古老,最简单,最公平且使用最广的算法。
进程是动态执行的实体,内核是进程的管理者。进程不但包括程序的指令和数据,而且包括程序计数器和CPU的所有寄存器以及存储临时数据的进程堆栈。所以,正在执行的进程包括处理器当前的一切活动。进程既可以在用户态下运行,也能在内核下运行,只是内核提供了一些用户态没有的核心服务,因此进程在访问这些服务时会产生中断,必须进行用户态与内核态的切换。