《Linux内核分析》之操作系统是如何工作的 实验总结

前言

实验阶段,由于学校网速等条件限制,未能在真机上搭建出实验环境。在实验楼中,将代码粘贴进去出现严重的缩进错位,最终未能完成编译新的。本文以分析关键代码为主。

环境搭建简易过程

此处为环境搭建的简易过程,详细的可以参考孟宁老师的github:mykernel,这里不再进行赘述。

环境搭建简易过程

1、创建(mkdir)工作区SG13225146

2、将linux-3.9.4文件夹剪切到刚创建工作区SG13225146

3、将mykernel_for_linux3.9.4sc.patch复制到工作区SG13225146

4、查看工作区内容

5、patch -p1 < ../mykernel_for_linux3.9.4sc.patch

6、make allnoconfig 复位

7、make 编译

8、安装qemu

9、使用qemu查看内核

10、结合网上所查资料,在mykernel文件夹中主要写入mypcb.h、mymain.c、myinterupt.c三个文件。之后再在linux-3.9.4文件夹中make 编译一下。

11、使用qemu再次查看内核,正常情况下应该可以看到更改后的。

小总结:1-7步是编译linux内核过程,8-9为查看内核信息的过程,10-11为编写自己的简易内核过程。

相关图片

mymain.c部分截图

代码粘进去严重错位了= =

linux原内核工作状态

实验及总结

 主要代码及分析

各文档所包含的头文件不在列出

mypcb.h

这个头文件主要定义了进程控制结构PCB

mypcb.h

#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8
/*
*Thread用于存储eip和esp
*/
struct Thread{
        unsigned long ip;
        unsigned long sp;
};
typedef struct PCB{
        int pid;/*进程id*/
        volatile long state;/*进程状态 -1 unrunable,0 runable*/
        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);/*调度器*/

mymain.c 

这个文件主要是定义了启动N个进程的过程

mymain.c

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(初始化0号进程的数据结构)*/
        task[pid].pid = pid;
        task[pid].state = 0;/* -1 unrunable,0 runable,>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 more process */
        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(
                "movl %1,%%espnt" /*set task[pid].thread.sp to esp */
                "pushl %1nt"      /*push ebp*/
                "pushl %0nt"      /*push task[pid].thread.ip*/
                "retnt"           /*pop task[pid].thread.ip to eip*/
                "popl %%ebpnt"
                :
                :"c"(task[pid].thread.ip),"d"(task[pid].thread.sp) /*input %ecx/%edx*/
        );

}
void my_process(void)
{
        int i = 0;
        while(1)
        {
                i++;
                if(i%10000000 ==0)
                {
                        printk(KERN_NOTICE "This is process %d -n",my_current_task->pid);
			if(my_need_sched == 1)
                        {
                                my_need_sched = 0;
                                my_schedule();
                         }
                        printk(KERN_NOTICE "This is process %d + n",my_current_task->pid);
                }
        }
}
代码解析

my_start_kernel可以看做操作系统的入口

my_start_kernel中主要过程

1、初始化循环体,初始一个单PCB循环链表

2、扩充循环链表,使用memcpy将task[0]初始状态复制到task[i]。该代码中到结束时形成一个有4个PCB的循环链表。

3、利用一段汇编代码,初始化堆栈esp、ebp、eip

汇编代码部分解析
        /*start process 0 by task[0]*/
        pid = 0;
        my_current_task = &task[pid];
        asm volatile(
                "movl %1,%%espnt" /*set task[pid].thread.sp to esp */
                "pushl %1nt"      /*push ebp*/
                "pushl %0nt"      /*push task[pid].thread.ip*/
                "retnt"           /*pop task[pid].thread.ip to eip*/
                "popl %%ebpnt"
                :
                :"c"(task[pid].thread.ip),"d"(task[pid].thread.sp) /*input %ecx/%edx*/
        );

1、将task[0]的栈顶(即task[pid].thread.sp)赋值给esp。

2、将task[pid].thread.sp(即栈顶)压栈,由于当前栈为空栈,故当前ebp压栈同时esp的值被修改,为以后的%ebp的复位使用。

3、将task[0].thread.ip(即程序入口my_process)压栈。

4、执行ret等价于pop task[pid].thread.ip to eip,即出栈,将 task[0].thread.ip(即程序入口my_process)赋给eip。

5、再次出栈,之前保存的sp赋给ebp寄存器。

这些步骤完成之后0号进程正式启动。

进程的运行
{
        int i = 0;
        while(1)
        {
                i++;
                if(i%10000000 ==0)
                {
                        // 该进程停止运行
                        printk(KERN_NOTICE "This is process %d -n",my_current_task->pid);
			if(my_need_sched == 1)
                        {
                                my_need_sched = 0;
                                my_schedule();//进行调度
                         }
                        // 该进程继续运行
                        printk(KERN_NOTICE "This is process %d + n",my_current_task->pid);
                }
        }
}

此过程为从i=0开始,每运行一千万次,程序自己检测是否需要进行调度(是否需要调度由时钟中断函数决定),如果是,就执行调度函数,切换到下一个进程。

myinterupt.c

这个文件主要是时钟中断函数和进程调度函数的具体实现,通过该文件中的函数完成最终的进程调度。

myinterupt.c

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 interrupy
* 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)
        {
                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 here <<<n");
        /*schedule*/
        next = my_current_task->next;
        prev = my_current_task;

        if(next->state == 0)/* -1 unrunable,0 runable,>0 stopped*/
        {
                /*switch to next process */
                asm volatile(
                        "pushl %%ebpnt"      /*save ebp*/
                        "movl %%esp,%0nt"   /*save esp*/
                        "movl %2,%%espnt"   /*restore esp*/
                        "movl $1f,%1nt"     /*save eip*/  /*$1f是指接下来的标号1:的位置*/
                        "pushl %3nt"
                        "retnt"           /*restore eip*/
                        "1:t"             /*next process start here*/
                        "popl %%ebpnt"
                        : "=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 %%ebpnt"        /*save ebp*/
                        "movl %%esp,%0nt"     /*save esp*/
                        "movl %2,%%espnt"    /*restore esp*/
                        "movl %2,%%ebpnt"   /*restore ebp*/
                        "movl $1f,%1nt"    /*save eip*/
                        "pushl %3nt"
                        "retnt"           /*restore eip*/
                        : "=m"(prev->thread.sp),"=m"(prev->thread.ip)
                        : "m"(next->thread.sp),"m"(next->thread.ip)
                );
        }
        return;
}
代码分析

函数void my_timer_handler(void)作为时钟中断函数主要功能为判断是否需要进行进程调度。通过设置时间片的大小,时间片用完时设置一下调度标志。

又注释中提到”该函数运行在当前进程的地址空间内,所以它使用当前进程的内核栈空间“。故每个进程中均有一个自己的time_count用来计算时间片。又此函数中time_count达到1000的倍数时my_need_sched才改变一次,故可知每个进程运行的时间是1000个CPU时钟。

当my_need_sched = 1时执行void my_schedule(void)函数,此时下一个进程状态一般分为正在执行和尚未执行。

下一个进程正在执行时

两个正在运行的进程进行上下文切换,此时执行if中的代码。

 if(next->state == 0)/* -1 unrunable,0 runable,>0 stopped*/
        {
                /*switch to next process */
                asm volatile(
                        "pushl %%ebpnt"      /*save ebp*/
                        "movl %%esp,%0nt"   /*save esp*/
                        "movl %2,%%espnt"   /*restore esp*/
                        "movl $1f,%1nt"     /*save eip*/  /*$1f是指接下来的标号1:的位置*/
                        "pushl %3nt"
                        "retnt"           /*restore eip*/
                        "1:t"             /*next process start here*/
                        "popl %%ebpnt"
                        : "=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);
        }

1、保存当前进程的ebp

2、将当前进程的esp赋到当前进程的sp,即保存当前的esp

3、将新进程的sp放到esp中

4、保存eip,即将eip保存到prev的ip

5、将新进程的eip压栈

6、ret 出栈,将next的ip赋给eip

7、此时新进程开始运行

8、恢复ebp (注意这里已经切换了进程)

小结:

1.保存prev进程的ebp、esp和eip

2.恢复next进程的esp、ebp和eip

下一个进程未执行过时

切换到一个新进程时,此时执行else中的代码。

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 %%ebpnt"        /*save ebp*/
                        "movl %%esp,%0nt"     /*save esp*/
                        "movl %2,%%espnt"    /*restore esp*/
                        "movl %2,%%ebpnt"   /*restore ebp*/
                        "movl $1f,%1nt"    /*save eip*/
                        "pushl %3nt"
                        "retnt"           /*restore eip*/
                        : "=m"(prev->thread.sp),"=m"(prev->thread.ip)
                        : "m"(next->thread.sp),"m"(next->thread.ip)
                );
        }

1、保存当前进程的ebp

2、将当前进程的esp赋到当前进程的sp,即保存当前的esp

3、将新进程的sp放到esp中

4、将新进程的sp放到ebp中

5、保存eip,即将eip保存到当前的ip

6、将当前进程的ip压栈(即将当前程序的入口保存)

7、ret 出栈,将prev进程的ip赋给eip

小结:

1.保存prev进程的ebp、esp和eip

2.设置新进程的eip、ebp和esp。

因为是新进程,所以ebp和esp相同,都是从存储的sp那里取值。

两种进程切换的不同之处

当切换到一个新进程时,新进程的ebp不再是从栈顶恢复,而是设置一个新的值。

总结

初始化好的CPU从my_start_kernel开始执行,时钟中断机制周期性性执行my_time_handler中断处理程序,执行完后中断返回总是可以回到my_start_kernel中断的位置继续执行。

即操作系统通过CUP执行进程的同时判断分配到的时间片是否用完,当用完时保存当前中断现场的相关信息并进行进程调度,开始另一个进程,当另一个进程的时间片用完时,再回到之前中断的地方恢复并继续执行后面的内容,如此循环的方法进行工作。

附录

C语言中嵌入汇编语言的格式:

1、基本格式

__asm__(

汇编语句模板:

输出部分:

输入部分:

破坏描述部分);

__asm__可写为asm

2、%1等相当于函数中的参数

3、/*$1f是指接下来的标号1:的位置*/

windCoder原创作品转载请注明出处

参考资料

Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

【转】Go语言Http Server源码阅读

目录(?)[-] 前言 几个重要概念 具体分析 几个接口 Handler ResponseWriter Flusher Hijacker response H...

30540
来自专栏python3

bs4爬虫实战二:获取双色球中奖信息

访问双色球网站:http://www.zhcw.com/ssq/kaijiangshuju/index.shtml?type=0

13920
来自专栏静默虚空的博客

JAVA 设计模式 命令模式

用途 命令模式 (Command) 将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化; 对请求排队或请求日志,以及支持可撤销的操作。 命令模...

22060
来自专栏lgp20151222

Git 的 .gitignore 配置

.gitignore 配置文件用于配置不需要加入版本管理的文件,配置好该文件可以为我们的版本管理带来很大的便利,以下是个人对于配置 .gitignore 的一些...

11830
来自专栏hotqin888的专栏

HydroCMS完成Ip地址段的权限设计

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

11820
来自专栏Golang语言社区

package http

要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:

24840
来自专栏向前进

vue-cli脚手架npm相关文件解读(4)utils.js

系列文章传送门: 1、build/webpack.base.conf.js 2、build/webpack.prod.conf.js 3、build/webp...

33560
来自专栏nummy

Grunt快速入门

Grunt是基于JavaScript的命令行构建工具,它可以帮助开发者们自动化重复性的工作。你可以把它看成是JavaScript下的Make或者Ant。它可以完...

9820
来自专栏大内老A

ASP.NET MVC集成EntLib实现“自动化”异常处理[实例篇]

个人觉得异常处理对于程序员来说是最为熟悉的同时也是最难掌握的。说它熟悉,因为仅仅就是try/catch/finally而已。说它难以掌握,则是因为很多开发人员却...

228100
来自专栏Janti

基础巩固——长连接 、短连接、心跳机制与断线重连

本文将从长连接和短连接的概念切入,再到长连接与短连接的区别,以及应用场景,引出心跳机制和断线重连,给出代码实现。

58910

扫码关注云+社区

领取腾讯云代金券