Linux内核第二节

作者:武西垚

深入理解函数调用堆栈

堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间

堆栈的作用

  • 函数调用框架
  • 传递参数
  • 保存返回地址
  • 提供局部变量空间

堆栈相关的寄存器

  • esp,堆栈指针,指向栈顶
  • ebp,基址指针,指向栈底,在C语言中用作记录当前函数调用基址。

其他关键寄存器

cs(代码段寄存器) : eip:总是指向下一条的指令地址

  • 顺序执行:总是指向地址连续的下一条指令
  • 跳转/分支:执行这样的指令的时候, cs : eip的值会根据程序需要被修改

参数传递与局部变量

  • 建立框架(相当于 call 指令)
  • push %ebp

movl %esp,%ebp

  • 拆除框架(相当于 ret 指令)
  • movl %ebp,%esp

pop  %ebp

函数返回时一定会拆除框架,建立和拆除是一一对应的。

  • 传递参数

在建立子函数的框架之前,局部变量的值保存在调用者堆栈框架中,所以在子函数框架建立之前可以采用变址寻址的方式将变量值入栈。

函数的返回值通过eax寄存器传递

借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断

mykernel实验思想

中断实现了多道程序设计,再各个程序的执行流之间来回切换,CPU将程序的ebp压入栈,并指向一个中断处理程序,从而由CPU和内核代码共同实现了保存现场和恢复现场

C代码中嵌入汇编代码的写法

__asm__(

汇编语句模板:

输入部分:

输出部分:

破坏描述部分:);

实验——mykernel基础上构造一个简单的操作系统内核

实验过程

本次实验是通过分析一个简单的时间片轮转多道程序内核源代码来理解操作系统的工作原理。

首先运行此内核,可以看到提供可一个代码在内核中运行的上下文环境。

然后cd mykernel 找到mymain.c和myinterrupt.c两个源代码,进入https://github.com/mengning/mykernel/blob/master可以找到本次实验需要的几个重要的源代码。然后将上面两个代码修改成网站中找到的代码,除此之外还要加上mypcb.h。

返回之后再次运行,可看到0、1、2、3几个进程相互切换。

源代码分析

mypcb.h

这个代码的目的是定义一个进程控制块(PCB)。

/*

*  linux/mykernel/mypcb.h

*

*  Kernel internal PCB types

*

*  Copyright (C) 2013  Mengning

*

*/

#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;

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);//调用了my_schedule,表示调度器

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];

tPCB * my_current_task = NULL;

volatile int my_need_sched = 0;//定义一个标志,用来判断是否需要调度

void my_process(void);

void __init my_start_kernel(void)

{

int pid = 0;//初始化一个进程0

int i;

/* Initialize process 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;

//定义进程0的入口为my_process

task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];

task[pid].next = &task[pid];

//因为一开始系统里只有进程0,所以这一行代码表示的是pid的next还是指向自己

/*fork more process */

//创建更多其他的进程,在初始化这些进程的时候可以直接拷贝0号进程的代码

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,%%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表示参数thread.ip,%1表示参数thread.sp。

movl %1,%%esp表示把参数thread.sp放到esp中;

接下来push %1,又因为当前栈为空,esp=ebp,所以等价于push ebp;

然后push thread.ip;ret等价于pop thread.ip;最后pop ebp  */

void my_process(void)//定义所有进程的工作,if语句表示循环1000万次才有机会判断是否需要调度。

{

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);

}

}

}

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 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)

/*  用于设置时间片的大小,时间片用完时设置调度标志。

当时钟中断发生1000次,并且my_need_sched!=1时,把my_need_sched赋为1。

当进程发现my_need_sched=1时,就会执行my_schedule。  */

{

#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 //task为空,即发生错误时返回

|| my_current_task->next == NULL)

{

return;

}

printk(KERN_NOTICE ">>>my_schedule<<<\n");

/* schedule */

next = my_current_task->next;//把当前进程的下一个进程赋给next

prev = my_current_task;//当前进程为prev

if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */

{

/* switch to next process */

/*如果下一个进程的状态是正在执行的话,就运用if语句中的代码表示的方法来切换进程*/

asm volatile(

"pushl %%ebp\n\t"       /* save ebp 保存当前进程的ebp*/

"movl %%esp,%0\n\t"     /* save esp 保存当前进程的esp*/

"movl %2,%%esp\n\t"     /* restore  esp 把下一个进程的sp放到esp中*/

"movl $1f,%1\n\t"       /* save eip 保存eip*/

"pushl %3\n\t"

"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

/*  与上一段代码不同的是如果下一个进程为新进程时,就运用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 */

"movl %%esp,%0\n\t"     /* save esp */

"movl %2,%%esp\n\t"     /* restore  esp */

"movl %2,%%ebp\n\t"     /* restore  ebp */

"movl $1f,%1\n\t"       /* save eip */

"pushl %3\n\t"

"ret\n\t"               /* restore  eip */

: "=m" (prev->thread.sp),"=m" (prev->thread.ip)

: "m" (next->thread.sp),"m" (next->thread.ip)

);

}

return;

}

时间: 2024-10-12 12:19:15

Linux内核第二节的相关文章

Linux学习第二节课(2019.9.7)

昨晚课程由于加班没有参加到上课,第二天补听了课程,再来写博客.第二节课,老刘跟我们第一章讲了如何安装虚拟机,安装linux系统,RPM和YUM仓库各种关系.然后老刘给我们讲一个"灰常恐怖的gui故事"来引入第二章的内容,讲述shell的作用,最后讲述了命令的长格式和短格式以及常用命令的ehco(echo 老刘真帅).date.reboot.poweroff.wget命令.目前还是能跟得上老刘的课程,要继续努力,复习预习,争取早日考过rhce. ---------------------

linux内核第二周

chapter 1 知识点梳理 (一)计算机是如何工作的?(总结)——三个法宝 ①存储程序计算机工作模型,计算机系统最最基础性的逻辑结构: ②函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能: 1 2 3 4 5 6 7 enter pushl %ebp movl %esp,%ebp leave movl %ebp,%esp popl %ebp 函数参数传递机制和局部变量存储 ③中断,多道程序操

Linux学习第二节课

七.用户空间与内核空间 1.用户空间不能直接访问硬件,需要通过操作系统来从而实现: 2.用户控件和内核控件为了安全,它们之间是相互隔离的,即使用户空间程序崩溃,内核也不受影响: 3.CPU在用户空间和内核空间来回切换进行工作运算的行为叫作上下文切换,脚本程序可以通过task绑定cpu线程等措施来减少上下文切换的次数实现减少消耗, 从而提升速度: 4.用户空间中如果程序直接调用操作系统会比较繁琐难懂,因此用操作系统或某些软件提供的功能库调用操作系统会更方便.简单. 八.Linux哲学思想 一切都是

深入理解Linux内核 - 第二章 内存寻址 04

问题:段描述符中的字段含义? 段首地址的线性地址/偏移/粒度/特权限制等信息. 1,快速访问段描述符 在加载段选择符时,相应的段描述符被自动由内存装入对应的非编程CPU寄存器.装入后就不再访问主存中的GDT或LDT. 2,段选择符的字段 index:指定了放在GDT或LDT中相应段描述符的入口 TI:0 GDT 1 LDT RPL:特权级,特指装入CS段中当前CPU的特权级. 3,段描述符地址换算 GDT地址:0x00020000 index:2 地址:0x00020000+(2*8))0x00

深入理解Linux内核 - 第二章 内存寻址 03

问题:1,逻辑地址怎么求?2,段描述符的base字段怎么求? 逻辑地址:由16位段选择符和32位偏移量组成,并不是说这个地址就是段选择符偏移32位再和偏移量相加. 段选择符:或叫段标识符,是一个16位长的字段.顾名思义,用于选择段的.为了快速找到段选择符,处理器提供段寄存器. 15~3bit,段索引号,2bit TI位, 1 0 位,特权级别. 由于段选择符仅仅在转换成线性地址中发挥作用,则通常认为32位偏移量就是逻辑地址.也即c语言中用&求得一个变量的地址就是逻辑地址. 段描述符:段选择符中,

深入理解Linux内核 - 第二章 内存寻址 01

1,三个地址 逻辑地址,机器语言指令中用来指定一个操作数或一条指令的地址. 线性地址:32位无符号整数,高达4GB.64位的cpu就是64位的线性地址 物理地址:内存芯片级内存单元寻址.老式x86由32位或36位无符号整数表示. 2,MMU内存控制单元 存在一个分段单元的硬件电路将一个逻辑地址转换为线性地址 存在一个分页单元的硬件电路将线性地址转换成一个物理地址. 问题: 1,为什么物理地址能够用36位的地址来表示? 2,为什么是逻辑地址转换成线性地址,再由线性地址转换为物理地址,这3个地址是什

深入理解Linux内核 第二章 内存寻址

内存地址 当使用80x86微处理器时,必须区分以下三种不同的地址: 1)逻辑地址(logical address),每一个逻辑地址都由一个段(segment)和偏移量(offset或者displacement)组成,偏移量指明了从段开始的地方到实际地址之间的距离. 2)线性地址(linear address),也称虚拟地址(virtual address),是一个32bit无符整数,可以用来表示4G的地址,通常由16制数字表示. 3)物理地址(physical address),用于内存芯片级内

linux入门-第二节:如何关机

如果你要关机,必须要保证当前系统中没有其他用户在线.可以下达 who 这个指令,而如果要看网络的联机状态,可以下达 netstat -a 这个指令,而要看背景执行的程序可以执行 ps -aux 这个指令.使用这些指令可以让你稍微了解主机目前的使用状态!(这些命令在以后的章节中会提及,现在只要了解即可!) 正确的关机流程为:sysnc ? shutdown ? reboot ? halt sync 将数据由内存同步到硬盘中. shutdown 关机指令,你可以man shutdown 来看一下帮助

《Linux内核分析》 第二节 操作系统是如何工作的

黄胤凯   原创作品转载请注明出处   <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 根据实验要求,在实验楼中的运行.(也不知道是我的问题还是实验楼的问题,运行代码时久久出不来结果,所以部分截图出自教学视频中) (时间片轮转,可以看出是my start kernel的代码) (输入相应指令查看mykernel相关的源代码) (mymain.c的代码,其中头文件已忽略,之前都是硬件的载入,从这里开始是操作系统的