[关闭]
@windmelon 2019-03-11T02:13:30.000000Z 字数 7583 阅读 1893

Linux系统分析实验(一):时间片轮转多道程序内核

Linux系统分析实验

原创作品转载请注明出处https://github.com/mengning/linuxkernel/
sa18225465


实验环境

ubuntu 18.04 虚拟机
VMware workstation 14 Player

实验目的

实验步骤

下载并编译内核

下载linux3.9.4内核源码

wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz

下载mykernel补丁

wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch

image.png-12.8kB

解压

tar -xvf linux-3.9.4.tar

image.png-14.4kB

打上补丁

cd linux-3.9.4
patch -p1 < ../mykernel_for_linux3.9.4sc.patch

image.png-92kB

编译

make allnoconfig
make

报错,缺少一个头文件
image.png-150kB

进入该目录发现确实没有compiler-gcc5.h
image.png-43.7kB

查阅资料,应该是所用的linux版本太高,可以通过把compiler-gcc4.h重命名为compiler-gcc5.h解决

再次编译,成功

make

image.png-147.7kB

安装qemu

sudo apt-get install qemu # install QEMU
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu

使用qemu启动内核

qemu -kernel arch/x86/boot/bzImage

image.png-22.3kB

修改mykernel源码,实现时间片轮转多道程序内核

放入mykernel的源码,重新编译

make allnoconfig
make

image.png-22.1kB

可以看到此时内核已经在模拟进程运行和切换

代码分析

mypcb.h

  1. #define MAX_TASK_NUM 4
  2. #define KERNEL_STACK_SIZE 1024*2
  3. /* CPU-specific state of this task */
  4. struct Thread {
  5. unsigned long ip;
  6. unsigned long sp;
  7. };
  8. typedef struct PCB{
  9. int pid;
  10. volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
  11. unsigned long stack[KERNEL_STACK_SIZE];
  12. /* CPU-specific state of this task */
  13. struct Thread thread;
  14. unsigned long task_entry;
  15. struct PCB *next;
  16. }tPCB;
  17. void my_schedule(void);

这个头文件里定义了一些宏和进程的PCB

其中有两个数据结构和一个函数声明

  1. struct Thread {
  2. unsigned long ip;
  3. unsigned long sp;
  4. };

这个结构体用来存储进程上下文,其中ip用来保存当前指令执行位置,sp用来保存栈顶位置

  1. typedef struct PCB{
  2. int pid;
  3. volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
  4. unsigned long stack[KERNEL_STACK_SIZE];
  5. /* CPU-specific state of this task */
  6. struct Thread thread;
  7. unsigned long task_entry;
  8. struct PCB *next;
  9. }tPCB;

这个结构体作为进程控制块,存储了进程id,进程状态,并且有next指针,可以形成进程链表

mymain.c

  1. /*
  2. * linux/mykernel/mymain.c
  3. *
  4. * Kernel internal my_start_kernel
  5. *
  6. * Copyright (C) 2013 Mengning
  7. *
  8. */
  9. #include <linux/types.h>
  10. #include <linux/string.h>
  11. #include <linux/ctype.h>
  12. #include <linux/tty.h>
  13. #include <linux/vmalloc.h>
  14. #include "mypcb.h"
  15. tPCB task[MAX_TASK_NUM];
  16. tPCB * my_current_task = NULL;
  17. volatile int my_need_sched = 0;
  18. void my_process(void);
  19. void __init my_start_kernel(void)
  20. {
  21. int pid = 0;
  22. int i;
  23. /* Initialize process 0*/
  24. task[pid].pid = pid;
  25. task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
  26. task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
  27. task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
  28. task[pid].next = &task[pid];
  29. /*fork more process */
  30. for(i=1;i<MAX_TASK_NUM;i++)
  31. {
  32. memcpy(&task[i],&task[0],sizeof(tPCB));
  33. task[i].pid = i;
  34. //*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
  35. task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]);
  36. task[i].next = task[i-1].next;
  37. task[i-1].next = &task[i];
  38. }
  39. /* start process 0 by task[0] */
  40. pid = 0;
  41. my_current_task = &task[pid];
  42. asm volatile(
  43. "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
  44. "pushl %1\n\t" /* push ebp */
  45. "pushl %0\n\t" /* push task[pid].thread.ip */
  46. "ret\n\t" /* pop task[pid].thread.ip to eip */
  47. :
  48. : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
  49. );
  50. }
  51. int i = 0;
  52. void my_process(void)
  53. {
  54. while(1)
  55. {
  56. i++;
  57. if(i%10000000 == 0)
  58. {
  59. printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
  60. if(my_need_sched == 1)
  61. {
  62. my_need_sched = 0;
  63. my_schedule();
  64. }
  65. printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
  66. }
  67. }
  68. }

在程序开头声明了进程数组、指向当前进程的指针,和指示当前进程是否需要被调度的变量

  1. tPCB task[MAX_TASK_NUM];
  2. tPCB * my_current_task = NULL;
  3. volatile int my_need_sched = 0;

此外,还有两个函数,一个是内核被加载时进行初始化的函数,另一个为运行进程的函数

  1. void __init my_start_kernel(void)
  2. {
  3. int pid = 0;
  4. int i;
  5. /* Initialize process 0*/
  6. task[pid].pid = pid;
  7. task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
  8. task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
  9. task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
  10. task[pid].next = &task[pid];
  11. /*fork more process */
  12. for(i=1;i<MAX_TASK_NUM;i++)
  13. {
  14. memcpy(&task[i],&task[0],sizeof(tPCB));
  15. task[i].pid = i;
  16. //*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
  17. task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]);
  18. task[i].next = task[i-1].next;
  19. task[i-1].next = &task[i];
  20. }
  21. /* start process 0 by task[0] */
  22. pid = 0;
  23. my_current_task = &task[pid];
  24. asm volatile(
  25. "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
  26. "pushl %1\n\t" /* push ebp */
  27. "pushl %0\n\t" /* push task[pid].thread.ip */
  28. "ret\n\t" /* pop task[pid].thread.ip to eip */
  29. :
  30. : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
  31. );
  32. }

一步步分析 my_start_kernel()干了些什么

首先初始化了一个pid为0的进程,作为内核中的第一个进程,该进程状态为0,即runnable,然后task_entry指向my_process,即指向my_process()函数的地址,然后thread.sp指向stack[]的最尾地址,最后将next指向自己,因为此时系统中只有自己一个进程

接下来由for循环创建三个进程,并将这总共四个进程使用循环链表的结构连接在一起

最后一段汇编代码作用是启动0号进程

第一步:将task[pid].thread.sp中的值即栈尾地址赋给esp

image.png-31.8kB

第二步:将task[pid].thread.sp压栈

image.png-33.8kB

第三步:将task[pid].thread.ip压栈

image.png-33.9kB

第四步:将task[pid].thread.ip即myprocess函数地址出栈并赋给eip

image.png-30.5kB

接下来程序便会执行myprocess()函数,开始循环0号进程

  1. void my_process(void)
  2. {
  3. while(1)
  4. {
  5. i++;
  6. if(i%10000000 == 0)
  7. {
  8. printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
  9. if(my_need_sched == 1)
  10. {
  11. my_need_sched = 0;
  12. my_schedule();
  13. }
  14. printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
  15. }
  16. }
  17. }

myinterrupt.c

  1. /*
  2. * linux/mykernel/myinterrupt.c
  3. *
  4. * Kernel internal my_timer_handler
  5. *
  6. * Copyright (C) 2013 Mengning
  7. *
  8. */
  9. #include <linux/types.h>
  10. #include <linux/string.h>
  11. #include <linux/ctype.h>
  12. #include <linux/tty.h>
  13. #include <linux/vmalloc.h>
  14. #include "mypcb.h"
  15. extern tPCB task[MAX_TASK_NUM];
  16. extern tPCB * my_current_task;
  17. extern volatile int my_need_sched;
  18. volatile int time_count = 0;
  19. /*
  20. * Called by timer interrupt.
  21. * it runs in the name of current running process,
  22. * so it use kernel stack of current running process
  23. */
  24. void my_timer_handler(void)
  25. {
  26. #if 1
  27. if(time_count%1000 == 0 && my_need_sched != 1)
  28. {
  29. printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
  30. my_need_sched = 1;
  31. }
  32. time_count ++ ;
  33. #endif
  34. return;
  35. }
  36. void my_schedule(void)
  37. {
  38. tPCB * next;
  39. tPCB * prev;
  40. if(my_current_task == NULL
  41. || my_current_task->next == NULL)
  42. {
  43. return;
  44. }
  45. printk(KERN_NOTICE ">>>my_schedule<<<\n");
  46. /* schedule */
  47. next = my_current_task->next;
  48. prev = my_current_task;
  49. if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
  50. {
  51. my_current_task = next;
  52. printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
  53. /* switch to next process */
  54. asm volatile(
  55. "pushl %%ebp\n\t" /* save ebp */
  56. "movl %%esp,%0\n\t" /* save esp */
  57. "movl %2,%%esp\n\t" /* restore esp */
  58. "movl $1f,%1\n\t" /* save eip */
  59. "pushl %3\n\t"
  60. "ret\n\t" /* restore eip */
  61. "1:\t" /* next process start here */
  62. "popl %%ebp\n\t"
  63. : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
  64. : "m" (next->thread.sp),"m" (next->thread.ip)
  65. );
  66. }
  67. return;
  68. }

myinterrupt.c文件中有两个函数,一个用来响应时钟中断,一个用来处理进程切换

  1. void my_timer_handler(void)
  2. {
  3. #if 1
  4. if(time_count%1000 == 0 && my_need_sched != 1)
  5. {
  6. printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
  7. my_need_sched = 1;
  8. }
  9. time_count ++ ;
  10. #endif
  11. return;
  12. }

当系统发生时钟中断时,该函数被调用,并设置变量my_need_sched = 1,导致在mymain.c中的myprocess()函数调用my_schedule()函数

  1. void my_schedule(void)
  2. {
  3. tPCB * next;
  4. tPCB * prev;
  5. if(my_current_task == NULL
  6. || my_current_task->next == NULL)
  7. {
  8. return;
  9. }
  10. printk(KERN_NOTICE ">>>my_schedule<<<\n");
  11. /* schedule */
  12. next = my_current_task->next;
  13. prev = my_current_task;
  14. if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
  15. {
  16. my_current_task = next;
  17. printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
  18. /* switch to next process */
  19. asm volatile(
  20. "pushl %%ebp\n\t" /* save ebp */
  21. "movl %%esp,%0\n\t" /* save esp */
  22. "movl %2,%%esp\n\t" /* restore esp */
  23. "movl $1f,%1\n\t" /* save eip */
  24. "pushl %3\n\t"
  25. "ret\n\t" /* restore eip */
  26. "1:\t" /* next process start here */
  27. "popl %%ebp\n\t"
  28. : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
  29. : "m" (next->thread.sp),"m" (next->thread.ip)
  30. );
  31. }
  32. return;
  33. }

该函数主要完成进程切换的过程,prevnext分别存储当前进程和下一个进程的PCB

切换过程由汇编代码完成

第一步:将ebp压栈

image.png-29.2kB

第二步:将当前esp的值赋给prev->thread.sp

image.png-32.6kB

第三步:将esp指向next->thread.sp即下个进程栈尾

image.png-35.8kB

第四步:将prev->thread.ip的值赋为1f

image.png-35.5kB

第五步:将next->thread.ip压栈,此时如果是第一次执行该进程,会指向myprocess()函数而不是1f

image.png-38.2kB

第六步:next->thread.ip出栈并赋给eip

image.png-35.8kB

第七步:如果是第一次执行该进程,则执行myprocess()函数,如果不是第一次执行该进程,则执行

  1. "1:\t" /* next process start here */
  2. "popl %%ebp\n\t"

进行复位

这样一来,就可以完成进程的切换并且能够保证进程的上下文的正确性

实验总结

实验需要用到基本X86汇编知识和对计算机体系结构的基本了解。通过实现这个简单的时间片轮转多道程序内核,能够加深对计算机操作系统工作原理的了解

操作系统在初始化时只有一个0号进程,之后的所有进程都由该进程fork而来,而进程的切换由时钟中断完成。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注