Linux内核分析——第二周学习笔记20135308
第二周 操作系统是如何工作的
第一节 函数调用堆栈
存储程序计算机:是所有计算机基础的框架
堆栈:计算机中基础的部分,在计算机只有机器语言、汇编语言时,就有了堆栈。堆栈机制是高级语言可以运行的基础。
计算机“三宝”:存储程序计算机、函数调用堆栈和中断机制。
- 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间
- 函数调用框架(eg:enter、leave )
- 传递参数(32位,通过堆栈来传递参数)
- 保存返回地址(用eax)
- 提供局部变量空间
- 等等
♦ C语言编译器对堆栈的使用有一套的规则,不同的系统中,编译C代码会有不同的汇编代码。
♦ 了解堆栈存在的目的和编译器对堆栈使用的规则是理解操作系统一些关键性代码的基础。
一、堆栈寄存器和堆栈操作
pop:从高地址向低地址
push:从低地址向高地址
二、利用堆栈实现函数的调用和返回
1.其他关键寄存器
- cs : eip:总是指向下一条的指令地址
• 顺序执行:总是指向地址连续的下一条指令 • 跳转/分支:执行这样的指令的时候,cs : eip的值会 根据程序需要被修改 • call:将当前cs : eip的值压入栈顶,cs : eip指向被 调用函数的入口地址 • ret:从栈顶弹出原来保存在这里的cs : eip的值,放 入cs : eip中
中断机制是如何工作的?
call指令: 1)将eip中下一条指令的地址A保存在栈顶 2)设置eip指向被调用程序代码开始处
三、函数堆栈框架的形成
- call xxx
- 执行call之前
- 执行call时,cs : eip原来的值指向call下一条指令,该值被保存到栈顶,然后cs : eip的值指向xxx的入口地址
2. 进入xxx
- 第一条指令: pushl %ebp
- 第二条指令: movl %esp, %ebp
- 函数体中的常规操作,可能会压栈、出栈
3. 退出xxx
- movl %ebp,%esp
- popl %ebp
- ret
第二节 借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
当一个中断信号发生时,CPU把当前的eip,esp,ebp压到内核堆栈中去,并把eip指向中断处理程序的入口。
C代码中嵌入汇编代码
第三节 在mykernel基础上构造一个简单的操作系统内核
执行:
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); } } }
mymain.c是一个死循环,一直不断地打印my_start_kernel here。中间加上if判断只是因为,cpu处理一次循环的速度太快,它只每100000次循环打印一次,以便我们观察。
嵌入式汇编
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; }
myinterrupt.c是一个被时间周期调用的函数,也可以看成是一个死循环的功能,一直打印>>>>>my_timer_handler here <<<<<。但与一般的死循环不同是,一般的死循环只是在做循环,而这个是周期性的调用函数。因此,mymain.c中的my_start_kernel只调用了一次,而myinterrupt.c中的my_timer_handler一直被调用。相当于,进程的初始化与进程的调度。
操作系统的两个法宝:
- 保存现场与恢复现场:中断上下文处理程序
- 进程上下文的切换
总结:操作系统是如何工作的
工作机制:存储程序计算机、函数调用堆栈和中断机制。
存储程序计算机最早是由著名数学家冯·诺伊曼等人在1946年总结并明确提出来的。任何一个程序通常都包括代码段和数据段,程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。堆栈中存放的就是与每个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。引入中断机制使内核可以处理硬件外设I/O。中断来源有I/O请求、时钟以及系统调用。
在单处理系统中,任何时候只有一个进程在运行,它要么处于用户态,要么处于内核态。然而在linux内核是可重入的,这意味着若干个进程可以同时在内核态下执行。用户态切换到内核态有3种方式,系统调用、异常、外围设备的中断。系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的。
进程是动态执行的实体,内核是进程的管理者。进程不但包括程序的指令和数据,而且包括程序计数器和CPU的所有寄存器以及存储临时数据的进程堆栈。所以,正在执行的进程包括处理器当前的一切活动。进程既可以在用户态下运行,也能在内核下运行,只是内核提供了一些用户态没有的核心服务,因此进程在访问这些服务时会产生中断,必须进行用户态与内核态的切换。