一.函数调用堆栈
存储程序、函数调用堆栈(高级语言起点)和中断机制是计算机工作的三大法宝。其中函数调用堆栈是本次学习的重点。先介绍一些基本的知识点:
1.ebp 在C语言中用作记录当前函数调用的基址;
2.call 将当前cs:eip的值压入栈顶,cs:eip指向被调用函数的入口地址。
pushl %ebp
movl %esp,%ebp
//被调用者函数体
//do sth
movl %ebp,%esp
popl %ebp
ret
call指令将eip中下一条指令的地址保存在栈顶。然后设置eip指向被调用程序代码开始处。
3.ret 从栈顶弹出原来保存在栈中的cs:eip的值并放进cs:eip中。
4.我们可以把函数调用堆栈分成以下三个部分:
call某个函数之前:
先把cs:eip中下一条指令压栈,目的是为了等函数调用完再返回到这个地址,保证程序的正确执行;然后cs:eip指向函数的入口地址。
进入该函数后:
//创建该函数的堆栈
pushl %ebp
movl %esp,%ebp
退出该函数调用后:
//消除该函数之前的栈空间
movl %ebp,%esp
popl %ebp
栈空间恢复如初(这里的初指调用该函数之前的状态)。
总结:
参数传递:用变址寻址,变量的偏移+(%ebp),然后将其压栈。
局部变量:系统会在该函数的堆栈空间预留一部分空间。
声明一个变量:用变址寻址,例 movl $0x61,0xffffffff3(%ebp)
将0x61放进该变量地址。
内核代码分析
mypcb.h
#define MAX_TASK_NUM 10
#define KERNEL_STACK_SIZE 1024*8
#define PRIORITY_MAX 30
struct Thread {
unsigned long ip;
unsigned long sp;
struct task_struct *task;
struct exec_domain *exec_domain;
unsigned long flags;
unsigned long status;
mm_segment_t add_limit;
unsigned long previous_esp;
};
typedef struct PCB{
int pid; // pcb id
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
char stack[KERNEL_STACK_SIZE];
struct Thread thread;
unsigned long task_entry;
struct PCB *next;
unsigned long priority;
}tPCB;
void my_schedule(void);
分析:
在mypcb.h中定义了两个结构体:线程thread和PCB(进程控制盒)。thread主要的属性有:cpu执行指令的地址、线程栈顶地址,另外我补充了课本上的一些属性。
PCB结构体的属性有进程id、进程状态、进程申请栈的大小、进程调用的线程、进程的入口地址、所指向的下一个进程、优先级等。最后声明进程调度函数。
mymain.c
tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;
void my_process(void);
unsigned long get_rand(int );
void sand_priority(void)
{
int i;
for(i=0;i<MAX_TASK_NUM;i++)
task[i].priority=get_rand(PRIORITY_MAX);
}
void __init my_start_kernel(void)
{
int pid = 0;
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];
for(pid=1;pid<MAX_TASK_NUM;pid++)
{
memcpy(&task[pid],&task[0],sizeof(tPCB));
task[pid].pid = pid;
task[pid].state = -1;
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].priority=get_rand(PRIORITY_MAX);
}
task[MAX_TASK_NUM-1].next=&task[0];
printk(KERN_NOTICE "
system begin :>>>process 0 running!!!<<<
");
pid = 0;
my_current_task = &task[pid];
asm volatile(
"movl %1,%%esp
"
"pushl %1
"
"pushl %0
"
"ret
"
"popl %%ebp
"
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)
);
}
void my_process(void)
{
int i = 0;
while(1)
{
i++;
if(i%10000000 == 0)
{
if(my_need_sched == 1)
{
my_need_sched = 0;
sand_priority();
my_schedule();
}
}
}
}
unsigned long get_rand(max)
{
unsigned long a;
unsigned long umax;
umax=(unsigned long)max;
get_random_bytes(&a, sizeof(unsigned long ));
a=(a+umax)%umax;
return a;
}
分析:
顺序分析这段程序可知:它最先定义了MAX_TASK_NUM个任务(进程)查看头文件可知MAX_TASK_NUM=10。然后开始执行my_process();我们先不看这行代码。接下来初始化进程列表并启动0号进程。在PCB进程中有一条注释/* -1 unrunnable, 0 runnable, >0 stopped */
所以void __init my_start_kernel(void)
函数中task[pid].state = 0;
目的就是启动0号进程,当然启动一个进程光把state属性设置为0还不行,紧接着把0号进程的入口地址给了cpu,让cpu下一条指令指向0号地址,相当于给eip赋值。以此类推,将0号进程的PCB属性全部赋值。值得一提的是这里的next属性指向1号进程,这样做的目的是形成进程链表。其中调用者为被调用者的父进程。函数unsigned long get_rand(max)
的目的是给定义的所有进程随机赋一个优先级。最后我们再返回来看my_process()函数,根据其定义我们可以得出这是一个死循环,用来模拟正在运行的cpu,每加一千万个数(相当于cpu时钟运转10000000次)就调用一次之前所定义的进程链表。至此,mymain.c分析完毕。
遇到的问题:
C代码中嵌入的汇编代码的目的,或者说优点是什么?它的执行顺序是符合C语言的执行顺序还是在编译预处理阶段就处理所嵌入的汇编代码。
myinterrupt.c
#include "mypcb.h"
#define CREATE_TRACE_POINTS
#include <trace/events/timer.h>
extern tPCB task[MAX_TASK_NUM]; //声明一些全局变量和函数
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;
void my_timer_handler(void) //时间计数函数
{
#if 1
if(time_count%2000 == 0 && my_need_sched != 1) //时钟中断2000次并且未调度任何进程
{
my_need_sched = 1; //把my_need_sched赋1,执行一次my_schedule()。
//time_count=0;
}
time_count ++ ;
#endif
return;
}
时钟控制函数主要的目的是模拟cpu运行,然后在固定时间调度一次进程调度函数。
tPCB * get_next(void)
{
int pid,i;
tPCB * point=NULL;
tPCB * hig_pri=NULL;
all_task_print();
hig_pri=my_current_task;
for(i=0;i<MAX_TASK_NUM;i++) //遍历所有进程,并把hig_pri指向优先级最高的进程。
if(task[i].priority<hig_pri->priority)
hig_pri=&task[i];
printk(" higst process is:%d priority is:%d
",hig_pri->pid,hig_pri->priority);
return hig_pri;
}
优先级调度函数的目的是遍历当前进程链表中的所有进程,把优先级最高的进程打印出来(相当于调用一次)。
void my_schedule(void)
{
tPCB * next;
tPCB * prev;
if(my_current_task == NULL //如果当前进程不存在,并且它没有子进程,则报错。
|| my_current_task->next == NULL)
{
printk(KERN_NOTICE " time out!!!,but no more than 2 task,need not schedule
");
return;
}
/* schedule */
next = get_next(); //当前进程的子进程赋给next;
prev = my_current_task; //prev指向当前进程;
printk(KERN_NOTICE " the next task is %d priority is %u
",next->pid,next->priority);
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ //如果下一个进程的状态为0,即正在运行,则调用嵌入式汇编代码切换进程
{
asm volatile(
"pushl %%ebp
" /* save ebp */
"movl %%esp,%0
" /* save esp */ //保存的ebp和esp都是当前进程的!
"movl %2,%%esp
" //把下个线程的esp赋值给当前esp寄存器
"movl $1f,%1
" /* save eip ,指向标号为1:的地方,也就是下三行*/
"pushl %3
"
"ret
" /* restore 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 from %d process to %d process
>>>process %d running!!!<<<
",prev->pid,next->pid,next->pid);
}
else //else与上面if中的内容相呼应,就是如果当前进程的下一个进程不处于运行状态应该执行的操作
{
next->state = 0; //把下一个进程的运行状态赋为0
my_current_task = next;
printk(KERN_NOTICE " switch from %d process to %d process
>>>process %d running!!!<<<
",prev->pid,next->pid,next->pid);
/* switch to new process */
asm volatile(
"pushl %%ebp
" /* save ebp */
"movl %%esp,%0
" /* save esp */
"movl %2,%%esp
" /* restore esp */
"movl %2,%%ebp
" /* restore ebp */
"movl $1f,%1
" /* save eip */
"pushl %3
"
"ret
" /* restore eip */
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
return;
}
my_schedule函数是进程调度的关键函数。它通过当前进程所指向的下一个进程的不同状态分为两类:下一个进程正处在运行状态(即if中的代码),这时候会执行汇编代码,因为它最起码执行过,可能之前只是被优先级更高的进程打断,所以在栈空间中一定保存有属于它栈空间,只要把它恢复出来即可;else中是处理下一个进程未执行过的情况,这时候会新建下一个进程的栈空间,然后再调用下个进程。这是两段代码最大的区别。
总结
本周主要通过分析两个简单的类时间片轮转代码学习了有关进程管理和系统调用的内容。本科操作系统就接触过这部分内容,当时学的时候只是概念堆叠,并没有像现在一样边分析代码,边理解进程的调度,再一次学到这些内容,让我对进程调度有了新的认识,尤其是进程上下文切换时对于没有运行完的进程的压栈保存,以前就是对这块内容留有困惑,这样分析下来,豁然开朗了许多。