LINUX内核分析第二周学习总结——操作系统是如何工作的
标签(空格分隔): 20135321余佳源
余佳源(原创作品转载请注明出处)
《Linux内核分析》MOOC课程
http://mooc.study.163.com/course/USTC-1000029000
一、计算机是如何工作的(总结)——三个法宝
-
存储程序计算机工作模型,计算机系统最最基础性的逻辑结构。
-
中断机制,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
-
函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;函数参数传递机制和局部变量存储。
·enter
·pushl %ebp
·movl %esp,%ebp
>
·leave
·movl %ebp,%esp
·popl %ebp
二、函数调用堆栈
1、堆栈
堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间,即CPU内已经集成好了很多功能。堆栈含以下元素:
- 函数调用框架
- 传递参数
- 保存返回地址
- 提供局部变量空间
- C语言编译器对堆栈的使用有一套的规则
- 了解堆栈存在的目的和编译器对堆栈使用的规则是理解操作系统一些关键性代码的基础
2、堆栈相关的寄存器及操作
esp,堆栈指针,指向栈顶
ebp,基址指针,指向栈底,在C语言中用作记录当前函数调用基址。
push,栈顶地址减少4个字节(32位),从低地址向高地址
pop,栈顶地址增加4个字节,从高地址向低地址
·ebp在C语言中用做记录当前函数调用基址
3、其他关键寄存器
cs:eip:指向地址连续的下一条指令
· 顺序执行:总是指向地址连续的下一条指令
· 跳转/分支:执行这样的指令的时候,cs : eip的值会根据程序需要被修改
· call:将当前cs : eip的值压入栈顶,cs : eip指向被调用函数的入口地址
· ret:从栈顶弹出原来保存在这里的cs : eip的值,放入cs : eip中
· 发生中断时……
4、参数传递与框架
- 建立框架(相当于执行call function)
push %ebp
movl %esp,%ebp
cs:eip原来的值指向call下一条指令,该值被保存到栈顶
cs:eip的值指向function的入口地址
- 进入function
pushl %ebp //意为保存调用者的栈帧地址
movl %esp, %ebp //初始化function的栈帧地址
- 拆除框架(相当于退出function)
movl %ebp,%esp
pop %ebp
ret
- 传递参数
在建立子函数的框架之前,局部变量的值保存在调用者堆栈框架中,所以在子函数框架建立之前可以采用变址寻址的方式将变量值入栈。
函数的返回值通过eax寄存器传递
5、中断机制是如何工作的?
6、参数传递
例:
#include <stdio.h>
void p1(char c)
{
printf("%c",c);
}
int p2(int x,int y)//重点关注这里的局部变量
{
return x+y;
}
int main(void)
{
char c ='a';
int x,y;
x =1;
y =2;
p1(c);
z = p2(x,y);//重点关注这里的参数传递过程
printf("%d = %d+%d",z,x,y);
}
其反汇编代码如下:
1.红色圈出的部分是变址寻址,目的是将x+y的值赋给eax;
2.是main函数中z = p2(x,y);一句的汇编代码。可以看到的是,汇编代码中用变址寻址把y的值和x的值存放到堆栈中,然后进行局部变量调用。
三、借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
当一个中断信号发生时,CPU把当前的eip,esp,ebp压到内核堆栈中去,并把eip指向中断处理程序的入口。
C代码中嵌入汇编代码
____asm____(
汇编语句模板:
输入部分:
输出部分:
破坏描述部分:);
四、mykernel实验——在mykernel基础上构造一个简单的操作系统内核
实验截图:
-
使用实验楼的虚拟机启动mykernel
cd LinuxKernel/linux-3.9.4 qemu -kernel arch/x86/boot/bzImage
-
在QEMU窗口,不停的输出字符串:
由图可知:每执行my_ start_ kernel函数一次或两次,my_ time_ hander函数就执行一次。
- mymain.c——系统中唯一的进程
代码完成的工作是每循环10000次,打印一句话。在mymain.c的my_start_kernel函数中有一个循环,不停的输出
my_start_kernel here
- myinterrupt.c——时间中断处理程序
每执行一次,都会执行一次时钟中断,每次时钟中断都调用printk并输出。
程序分析
mypcb.h
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8
/* CPU-specific state of this task */
struct Thread {
unsigned long ip;//保存eip
unsigned long sp;//保存esp
};
typedef struct PCB{//用于表示一个进程,定义了进程管理相关的数据结构
int pid;
volatile long state; /* 定义进程的状态:-1 不可运行, 0 可运行, >0 停止 */
char stack[KERNEL_STACK_SIZE];
//内核堆栈
struct Thread thread;
unsigned long task_entry; //指定进程入口
struct PCB *next;//进程链表
}tPCB;
void my_schedule(void);//调用了my_schedule,表示调度器
mymain.c
#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(初始化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];
task[pid].next = &task[pid];
//因为一开始系统里只有进程0,所以这一行代码表示的是pid的next还是指向自己
/*fork more process */
//创建更多其他的进程,在初始化这些进程的时候可以直接拷贝0号进程的代码
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].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(//%0表示参数thread.ip,%1表示参数thread.sp。
"movl %1,%%esp
" /* set task[pid].thread.sp to esp 把参数thread.sp放到esp中*/
"pushl %1
" /* push ebp 由于当前栈是空的,esp与ebp指向相同,所以等价于push ebp*/
"pushl %0
" /* push task[pid].thread.ip */
"ret
" /* pop task[pid].thread.ip to eip */
"popl %%ebp
"
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)
/* input c or d mean %ecx/%edx*/
);
}
/* %0表示参数thread.ip,%1表示参数thread.sp。
movl %1,%%esp表示把参数thread.sp放到esp中;
接下来push %1,又因为当前栈为空,esp=ebp,所以等价于push ebp;
然后push thread.ip;ret等价于pop thread.ip;最后pop ebp */
void my_process(void)//定义所有进程的工作,if语句表示循环1000万次才有机会判断是否需要调度。
{
int i = 0;
while(1)
{
i++;
if(i%10000000 == 0)
{
printk(KERN_NOTICE "this is process %d -
",my_current_task->pid);
if(my_need_sched == 1)
{
my_need_sched = 0;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +
",my_current_task->pid);
}
}
}
myinterrupt.c
#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
if(time_count%1000 == 0 && my_need_sched != 1) //设置时间片的大小,时间片用完时设置一下调度标志。当时钟中断发生1000次,并且my_need_sched!=1时,把my_need_sched赋为1。当进程发现my_need_sched=1时,就会执行my_schedule。
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<
");
my_need_sched = 1;
}
time_count ++ ;
#endif
return;
}
void my_schedule(void)
{
tPCB * next; //下一个进程
tPCB * prev; //当前进程
if(my_current_task == NULL //task为空,即发生错误时返回
|| my_current_task->next == NULL)
{
return;
}
printk(KERN_NOTICE ">>>my_schedule<<<
");
/* schedule */
next = my_current_task->next; //将当前进程的下一个进程赋给next
prev = my_current_task;//当前进程为prev
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
{
//在两个正在执行的进程之间做上下文切换
asm volatile(
"pushl %%ebp
" /* 保存当前进程的ebp */
"movl %%esp,%0
" /* 保存当前进程的esp */
"movl %2,%%esp
" /* 重新记录要跳转进程的esp,将下一进程中的sp放入esp中 */
"movl $1f,%1
" /* $1f指标号1:的代码在内存中存储的地址,即保存当前的eip */
"pushl %3
" //将下一进程的eip压入栈,%3为 next->thread.ip
"ret
" /* 记录要跳转进程的eip */
"1: " /* 下一个进程开始执行 */
"popl %%ebp
"
: "=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<<<
",prev->pid,next->pid);
}
else /* 与上一段代码不同的是如果下一个进程为新进程时,就运用else中的这一段代码。首先将这个进程置为运行时状态,将这个进程作为当前正在执行的进程。 */
{
next->state = 0;
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<
",prev->pid,next->pid);
/* switch to new process */
asm volatile(
"pushl %%ebp
" /* 保存当前进程的ebp */
"movl %%esp,%0
" /* 保存当前进程的esp */
"movl %2,%%esp
" /* 重新记录要跳转进程的esp */
"movl %2,%%ebp
" /* 重新记录要跳转进程的ebp */
"movl $1f,%1
" /* 保存当前eip */
"pushl %3
"
"ret
" /* 重新记录要跳转进程的eip */
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
return;
}
运行这个精简的内核
可见操作系统内核程序在虚拟机中启动时的结果为:0、1、2、3号进程循环切换执行。
五、小结
操作系统的“两把利剑”:中断上下文、进程上下文的切换
操作系统的核心功能就是:进程调度和中断机制,通过与硬件的配合实现多任务处理,再加上上层应用软件的支持,最终变成可以使用户可以很容易操作的计算机系统。
操作系统是管理计算机系统的全部硬件资源包括软件资源及数据资源;控制程序运行;改善人机界面;为其它应用软件提供支持等,使计算机系统所有资源最大限度地发挥作用,为用户提供方便有效的服务界面。
Linux是一个多进程的操作系统,所以,其他的进程必须等到正在运行的进程空闲CPU后才能运行。当正在运行的进程等待其他的系统资源时,Linux内核将取得CPU的控制权,并将CPU分配给其他正在等待的进程,这就是进程切换。内核中的调度算法决定将CPU分配给哪一个进程。
进程是动态执行的实体,内核是进程的管理者。进程不但包括程序的指令和数据,而且包括程序计数器和CPU的所有寄存器以及存储临时数据的进程堆栈。所以,正在执行的进程包括处理器当前的一切活动。进程既可以在用户态下运行,也能在内核下运行,只是内核提供了一些用户态没有的核心服务,因此进程在访问这些服务时会产生中断,必须进行用户态与内核态的切换。