2018-2019-1 20189215《Linux内核原理与分析》第三周作业

《庖丁解牛》第二章书本知识总结


  • 函数调用框架
    call指令有两个作用:
    (1) 将CS:EIP中下一条指令的地址A保存在栈顶;
    (2)设置CS:EIP指向被调用程序的第一行。
    ret指令在被调用函数完成之后,将地址A恢复到CS:EIP中。

  • 传递参数
    因为存储参数使用堆栈,根据后入先出的规则,传递参数的方法是从右到左依次压栈。

  • 计算机的3大法宝:存储程序计算机、函数调用堆栈机制、中断。

  • 内嵌汇编语法规则

asm volatile (
汇编语句:
输出部分: //需要在行首加上冒号:
输入部分: //需要在行首加上冒号:
破环描述部分
)

(1)volatile是告诉编译器不要优化代码
(2)汇编代码嵌入时和直接方式有些许不同,体现在%转义符号。寄存器前面会有两个%,而%加数字则表示在输出部分、输入部分、破环描述部分的编号,从零开始,最高到总个数-1。
(3)一些符号

"c"、"d"等符号分别表示寄存器ecxedx
"r"表示将输入变量放到通用寄存器,也就是eaxebxecxedxesiedi中的一个。
"="表示操作数在指令中是只写的,输出操作数。
"+"表示输入输出操作数是读写类型的。
"m"表示内存变量。

实验:mykernel时间片轮转多道程序内核


  • 进入实验楼实验,在终端中分别输入以下命令
cd LinuxKernel/linux-3.9.4
rm -rf mykernel
patch -p1 < ../mykernel_for_linux3.9.4sc.patch //打补丁
make allnoconfig
make //编译内核时间较长
qemu -kernel arch/x86/boot/bzImage

make过程如下图:

make成功后mykernel运行:

  • 在mykernel的基础上添加mypcb.h,修改mymain.cmyinterrupt.c文件,实现一个简单的操作系统内核,设置为时钟中断发生100次时,将需要调度的flagmy_need_sched设置为1。运行结果如下:

mykernel时间片轮转代码分析


实验中,基于mykernel,添加了mypcb.h,修改mymain.cmyinterrupt.c文件,下面对这三个文件的代码进行分析。

  • 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;                 //定义进程id
    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;

void my_schedule(void); //声明调度函数

这段代码主要定义了进程控制块PCB,包括:
pid:进程id
state:进程状态,初始化值就是-1,如果被调度运行起来,其值就会变成0 ,被终端后,其值>0
stack:本进程使用的堆栈
thread:当前正在执行的线程信息
task_entry:进程入口函数
next:指向下一个PCB,实验环境中所有的PCB是以链表的形式组织起来的。

  • 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];  //PCB的数组task
tPCB * my_current_task = NULL; //当前task指针
volatile int my_need_sched = 0; //是否需要调度

void my_process(void);  //my_process函数声明

void __init my_start_kernel(void) //mykernel内核代码的入口
{
    int pid = 0;
    int i;
    /* 初始化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;
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    
    /*fork其他进程 */
    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];
    }
    
    /* 用task[0]开始0号进程 */
    pid = 0;
    my_current_task = &task[pid];
	asm volatile(
    	"movl %1,%%esp
	" 	/* set task[pid].thread.sp to esp */
    	"pushl %1
	" 	        /* 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*/
	);
}   

void my_process(void)
{
    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);
        }     
    }
}

本实验环境中,mykernel是内核代码的入口,负责初始化内核的各个组成部分,在实际的Linux内核中,入口为init/main.c中的start_kernel(void)函数。
在本实验环境中,每个进程的函数都是 my_process函数,进程在运行中打印当前进程号,并通过my_need_sched变量判断是否需要调度,这是在myinterrupt.c文件中进行赋值的一个变量。
下面对0号进程的启动进行分析:
1"movl %1,%%esp " %1指第2个输入输出"d" (task[pid].thread.sp),将进程的栈顶位置存入ESP寄存器
2"pushl %1 " 将task[0].thread.sp压栈,即保存当前EBP值
3"pushl %0 " 将task[0].thread.ip压栈,当前进程的EIP值入栈
4"ret " 将栈顶值取出到EIP寄存器,即刚刚入栈的0号进程ip值
5 "popl %%ebp " 在执行完其他进程之后,回到0号进程,释放栈空间
6 : 没有输出
7 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) 输入,将0号进程的ip、sp值分别存入ecx、edx寄存器中。

  • 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引用全局变量
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%100 == 0 && my_need_sched != 1) //当时钟中断发生100次,并且my_need_sched不为1时,赋值为1
    {
        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 
        || my_current_task->next == NULL)
    {
    	return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<
");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ //下一个进程可运行,执行进程切换
    {
    	my_current_task = next; 
    	printk(KERN_NOTICE ">>>switch %d to %d<<<
",prev->pid,next->pid);  
    	/* 切换进程 */
    	asm volatile(	
        	"pushl %%ebp
	" 	    /* save ebp */
        	"movl %%esp,%0
	" 	/* save esp */
        	"movl %2,%%esp
	"     /* restore  esp */
        	"movl $1f,%1
	"       /* save eip */	
        	"pushl %3
	" 
        	"ret
	" 	            /* restore  eip */
        	"1:	"                  /* next process start here */
        	"popl %%ebp
	"
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	); 
 	
    }
    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
	" 	    /* 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;	
}

myinterrupt.c包含my_timer_handler和my_schedule两个函数。 my_timer_handler每隔100次将my_need_sched赋值为1,等待mymain.c中my_process函数的主动调度。my_schedule保存恢复进程上下文。
切换进程的分析:
01 "pushl %%ebp 保存当前ebp到堆栈中
02 "movl %%esp,%0 " 保存当前ESP到当前进程sp中
03 "movl %2,%%esp " esp指向下一个进程
04 "movl $1f,%1 " 将1f存储到thread.ip中,$1f是标号“1: ”处,再次调度到该进程时就会从1:开始执行
05 "pushl %3 " 将下一个进程的ip入栈
06 "ret " eip指向下一个进程的起始地址,也做了一次出栈操作
07 "1: " 标号1:,即next进程开始执行的位置
08 "popl %%ebp " 待下一个进程执行完后释放栈空间,恢复现场
09 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) %0、%1分别对应,m代表内存变量
10 : "m" (next->thread.sp),"m" (next->thread.ip) %2、%3分别对应

遇到的问题


  1. 实验中修改代码后,make找不到文件。
    解决:修改代码是在/mykernel目录下进行修改的,make编译内核需要在LinuxKernel/linux-3.9.4目录下进行,修改完之后要cd ..
  2. 不懂myinterrupt.c文件中my_timer_handler函数在哪调用。
    解决:仔细看了书上的35页,在实验环境中已经配置好了一个时钟中断,在时钟中断发生的时候就会调用一次该函数。
  3. 不懂$1f的含义。
    解决:查阅资料得知,"movl $1f,%1 "是将进程原来的ip替换为$1f,是at&t的一种语法,f代表向后跳转(b表示向前,f表示向后),1f指的就是下一条指令,即当前进程的eip。
原文地址:https://www.cnblogs.com/jsjliyang/p/9854589.html