linux内核分析课程笔记(二)
运行一个精简的操作系统内核
存储程序计算机是几乎所有计算机的基础逻辑框架。
堆栈是计算机中非常基础的东西,在最早计算机没有高级语言时,在高级语言出现之前,我们没有函数的概念。但高级语言出现后有了函数调用后,堆栈就显得非常重要了。
堆栈
堆栈式C语言运行时必须记录的一个记录调用路径和参数的空间:
- 函数调用框架
- 传递参数
- 保存返回地址
- 提供局部变量空间
32位x86是使用堆栈传递参数,64位的稍有不同。
C语言编译器对堆栈的使用有一套的规则,不同的指令序列也可能实现相同的功能,所以在Linux上反汇编一个C程序,和在windows上反汇编得到的AT&T汇编一般有差异。
寄存器
堆栈相关寄存器:
- esp,堆栈指针(stack pointer)
- ebp,基址指针(base pointer)
其他关键寄存器:
- cs:eip : 总是指向下一条的指令地址(注意这里下一条指令的地址不一定地址连续,要看控制流而定!)
- 顺序执行:指向地址连续的下一条指令
- 跳转/分支,cs:eip 根据程序需要被修改
- call: 将cs:eip 压入栈顶,cs:eip 指向被调用函数的入口地址
- ret: 将栈顶弹出原来保存在这里的cs:eip 的值,放入cs:eip中。
pushl %ebp
movl %esp,%ebp
do sth.(Assembly)
首先使用 gcc -g
可以得到test.c的可执行文件test,然后使用objdump -S
可以获得 test的反汇编文件。
有压栈必有出栈,有生必有死。
#include <stdio.h>
int main() {
/* val1+val2=val3 */
unsigned int val1 = 1;
unsigned int val2 = 2;
unsigned int val3 = 0; printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
asm volatile(
"movl $0,%%eax\n\t" /* clear %eax to 0*/
"addl %1,%%eax\n\t" /* %eax += val1 */
"addl %2,%%eax\n\t" /* %eax += val2 */
"movl %%eax,%0\n\t" /* val2 = %eax*/
: "=m" (val3) /* output =m mean only write output memory variable*/
: "c" (val1),"d" (val2) /* input c or d mean %ecx/%edx*/
);
printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
return 0;
}
Asm内联汇编的一个重要的技巧在于在后面涉及到
:
有关的,按照顺序从前到后可以使用编号替换变量。编号的顺序按照:
后出现的前后关系而定,从0开始编号。比如在这个例子里,%0
即是变量val3
,%1
即是变量val1
,%2
即变量val2
。
因为%n
代表的是变量,所以我们在表示寄存器时需要转义,即使用双百分号%%eax
其实等价于%eax
,第一个百分号表示转义。
利用mykernel模拟多进程切换
早期的计算机执行完一个程序之后,执行另外一个程序。
由CPU和内核代码共同实现了保存现场和恢复现场。
疑问:
在内联汇编
asm volatile(
"movl %1,%%esp\n\t"
"pushl %1\n\t"
"pushl %0\n\t"
"ret\n\t"
"popl %%ebp\n\t"
:
:"c"(task[pid].thread.ip),"d"(task[pid]).thread.sp)
);
疑问:movl %1,%%esp 第一句已经将栈顶设置为 task[pid].thread.sp了。
但是为什么后面要push %1\n\t 在 popl %%ebp呢?
上面这段代码与下面这段代码难道是不等价的吗?
movl %1,%%esp\n\t
pushl %0\n\t
ret\n\t
movl %%esp,%%ebp\n\t
自问自答:实际上是不一样的,因为这段汇编到ret的时候,会跳走执行task[pid].thread.ip地方的汇编,不是一段直接顺序执行到底的汇编代码!
而实际上为了保持对称性,所以在一开始的时候将整个系统的ebp压入栈中,不过事实上如果内核运行正常,是不会执行到最后的Popl的,因为在调度中应当是个死循环,while(1),保持一直有进程在占用运行,否则整个系统就"停下来了"。
实验截图
内核的重新make编译生成第一步:
内核编译生成后,第二步:
最终跑起来内核调度程序时的现象:
进程切换分析
首先我们来分析主程序代码:
mykernel/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;
/*指向当前PCB块的指针*/
volatile int my_need_sched = 0;
/*是否要调度的标志,当 my_need_sched 为1时表明要被调度*/
void my_process(void);
void __init my_start_kernel(void)
{
int pid = 0;
int i;
/* 初始化0号进程 */
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
/* 初始化进程的代码段入口,并且注意要设置进程上下文中的ip(ip = %eip?) */
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
/* 这里使用了数组来模拟一个进程的栈,实际上 &task[pid].stack[KERNEL_STACK_SIZE-1] = &*(add of task[pid].stack + KERNEL_STACK_SIZE - 1),
最终得到的是 stack 所模拟的栈顶的位置,这个栈是从数组标号高的地方向低的地方增长。 */
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++)
{
/* 将task[0]的有关tPCB的所有信息拷贝到 task[i]处,进程的初始化信息大部分是一致的,注意tPCB 包含了 Thread*/
memcpy(&task[i],&task[0],sizeof(tPCB));
task[i].pid = i;
/* 目前只有0号进程应该被调度执行,其他暂时还不行 */
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] */
/* 首先启动0号进程开始运行 */
pid = 0;
my_current_task = &task[pid];
asm volatile(
/* 根据进程控制块中的sp值,设置当前task的栈顶指针 */
"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*/
);
}
void my_process(void)
{
int i = 0;
while(1)
{
i++;
/* 之前我们设置了0号进程的task_entry(代码段入口)是 my_process,所以当0号进程启动并开始执行代码段时,my_process开始执行。
if(i%10000000 == 0)
{
/* 一个进程每循环1000万次后即打印一条信息 */
printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
/* 如果 my_need_sched 为1时,则启动调度器去调度其他进程 */
if(my_need_sched == 1)
{
my_need_sched = 0;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}
有一个有意思的地方在于这段代码使用简单的两行实现了一个循环链表的结构。在初始化0号进程之后,我们使用了task[pid].next = &task[pid];
,此时当前PCB的next指向自身,形成一个循环的结构。之后在向循环链表中插入新的元素时使用了task[i].next = task[i-1].next; task[i-1].next = &task[i];
,非常精妙。
在 my_process 里是一个不断运行的死循环,那么我们什么时候才会使 my_need_sched 为1呢?答案在 my_timer_handle 函数中,当时钟中断产生后,会默认调用该函数:
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;
}
我们可以看到,当 time_count 为 1000的倍数 且 my_need_sched 仍为0 时,我们设置了 my_need_sched 为1,从而"阻塞"当前进程,使得它主动放弃处理器,执行my_schedule
函数调度其他进程。
void my_schedule(void)
{
tPCB * next;
tPCB * prev;
if(my_current_task == NULL
|| my_current_task->next == NULL)
{
return;
}
printk(KERN_NOTICE ">>>my_schedule<<<\n");
/* schedule */
next = my_current_task->next;
prev = my_current_task;
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
{
/* switch to next process */
/* 这里是进程切换中最重要的一步,进程上下文的切换 */
asm volatile(
/* 一个是保存ebp 栈顶,一个是更新 thread中的栈顶指针 */
"pushl %%ebp\n\t" /* save ebp */
"movl %%esp,%0\n\t" /* save esp */
/* %esp = next-> thread.sp,切换栈顶指针*/
"movl %2,%%esp\n\t" /* restore esp */
/* 这句的语法很独特,按照意思来理解是 将 prev->thread.ip 设置为 label(标签)1的位置的地址*/
"movl $1f,%1\n\t" /* save eip */
/* pushl 和 ret 使得切换到另一个进程本身的代码段开始运行 */
"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
{
next->state = 0;
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
/* switch to new process */
/* 要选择切换到的是一个新的进程,是第一次切换到该进程运行,但注意,这里可以发现没有label 1,但是依旧使用了 movl $1f,%1\n\t */
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;
}
最困惑的地方:
1、为什么在进程上下文切换的时候,使用了这样的语句搭配label 1: 设置下个进程该开始的地方呢?进程代码段之间是独立的,为什么要强行设置另一个进程的ip呢?
"movl $1f,%1\n\t" /* save eip */
2、这里的pushl %ebp和 popl %ebp 本身是否是有意义的?
总结
第一节中我们了解到冯诺依曼体系结构实际上是存储计算机模型,即CPU运行已经装在内存中的预定代码段以达到预期的效果。
在这节中我们了解到这种特性,其中一点映证就在于在操作系统中是不断有进程在运行的,不会出现某个时刻系统正常但没有任何一个进程在运行的情况。进程的上下文也可以看作是进程的环境,每个进程在不同的时刻环境会不同,所以为了保证进程之间不会互相影响,实现进程之间的互相隔离(也叫透明性),所以在进程切换时也要进行进程上下文的切换。
在本次实验中的精简内核第一个运行的进程是0号进程,但本次实验中的0号进程好像没有什么特殊之处,与其余进程执行的是相同的代码~
linux内核分析课程笔记(二)的更多相关文章
- Linux内核分析课程笔记(一)
linux内核分析课程笔记(一) 冯诺依曼体系结构 冯诺依曼体系结构实际上就是存储程序计算机. 从两个层面来讲: 从硬件的角度来看,冯诺依曼体系结构逻辑上可以抽象成CPU和内存,通过总线相连.CPU上 ...
- Linux内核分析课程总结
Linux内核分析课程总结 By 20135203齐岳 知识梳理 (思维导图地址http://mindmap.4ye.me/mkxM0cFh/1) 从start _ kernel构造一个新的Linux ...
- Linux内核分析课程期中总结
Linux内核分析课程期中总结 姓名:王朝宪 学号:20135114 注: 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com ...
- linux内核分析 课程总结
Linux内核分析 链接汇总 Linux内核分析第一周学习总结--计算机是如何工作的 Linux内核分析第二周学习总结--操作系统是如何工作的 Linux内核分析第三周学习总结--构造一个简单的Lin ...
- 【课程总结】Linux内核分析课程总结
程涵 原创博客 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 每周实验报告: 反汇编一个简单的C程序 ...
- Linux内核分析 读书笔记 (第一章、第二章)
第一章 Linux内核简介 1.1 Unix的历史 Unix很简洁,仅仅提供几百个系统调用并且有一个非常明确的设计目的. 在Unix中,所有东西都被当做文件,这种抽象使对数据和对设备的操作是通过一套相 ...
- 20135239 益西拉姆 linux内核分析 读书笔记之第四章
chapter 4 进程调度 4.1 多任务 多任务操作系统就是能同时并发的交互执行多个进程的操作系统. 多任务系统可以划分为两类: - 非抢占式多任务: - 进程会一直执行直到自己主动停止运行(这一 ...
- Linux内核分析 读书笔记 (第五章)
第五章 系统调用 5.1 与内核通信 1.调用在用户空间进程和硬件设备之间添加了一个中间层.该层主要作用有三个: 为用户空间提供了硬件的抽象接口. 系统调用保证了系统的稳定和安全. 实现多任务和虚拟内 ...
- Linux内核分析 读书笔记 (第四章)
第四章 进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间.进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统.只有通过调度程序的合理调度,系统资源才能最大限 ...
随机推荐
- nodejs+express+jade安装备忘
安装步骤 1.安装nodejs,比如安装在E:\nodejs. 确保有两个环境变量 用户环境变量:C:\Users\Administrator\AppData\Roaming\npm 系统环境变量:e ...
- GEMR: Get the parent window for view
Window window = Utilities.GetVisualParent<Window>(this); if (window != null) { window.DialogRe ...
- Windows 10开启默认网络驱动器访问
在Windows 10的系统策略中,驱动器盘符的网络访问是默认关闭的,用管理员权限打开注册表,找到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Curre ...
- 高大上技术之sql解析
Question: 为何sql解析和高大上有关系?Answer:因为数据库永远都是系统的核心,CRUD如此深入码农的内心...如果能把CRUD改造成高大上技术,如此不是造福嘛... CRUD就是Cre ...
- Linux 的启动流程(转)
原文链接:http://blog.jobbole.com/46078/ 半年前,我写了<计算机是如何启动的?>,探讨BIOS和主引导记录的作用. 那篇文章不涉及操作系统,只与主板的板载程序 ...
- GoldenGate 配置extract,replicat进程自启动
在GoldenGate中主进程是manager进程,使用start mgr启动.可以在mgr进程中添加一些参数用来在启动mgr进程的同时启动extract和replicat进程 GGSCI (gg01 ...
- ATA/SATA/SCSI/SAS/FC总线简介
ATA/SATA/SCSI/SAS/FC 都是应用于存储领域的总线,在当今的存储系统中,普遍应用的硬盘接口主要有 SATA . SCSI . SAS 和FC , ATA 比较古老,在一些老的低端存储系 ...
- 8 个最优秀的 Android Studio 插件
Android Studio是目前Google官方设计的用于原生Android应用程序开发的IDE.基于JetBrains的IntelliJ IDEA,这是Google I/O 2013第一个宣布的作 ...
- XSHELL配色方案及导入配色方案的方法
[ubuntu] text(bold)=ffffff magenta(bold)=ad7fa8 text=ffffff white(bold)=eeeeec green=4e9a06 red(bold ...
- HTTP协议学习
面试过程中又一个常见的问题,http协议,因为做服务器开发如果用http协议的话,现在各种开源软件都封装好了,python中只需要简单的继承定义好的类,重写get或者post等方法,几行代码就可以搭建 ...