Linux内核学习总结
Linux内核学习总结
刘浩晨 【 原创作品转载请注明出处 】《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
前言:总结分为三部分,第一部分对课程进行知识点总结,第二部分是学习心得体会,第三部分是附录,对全部作业的博客链接,便于查询与学习。
一、《Linux内核分析》总结
(一)计算机是如何工作的
1.存储程序计算机工作模型
2. X86CPU的寄存器:通用寄存器、段寄存器、标志寄存器等。
3.计算机的汇编指令
(1)movl指令:
- 寄存器寻址,寄存器模式,以%开头的寄存器标示符。不和内存打交道,eax赋值给edx;
- 立即寻址,把立即数直接放在寄存器,立即数是以$开头的数值;
- 直接寻址,直接访问一个指定的内存地址的数据;
- 间接寻址:将寄存器的值作为一个内存地址来访问内存;
- 变址寻址:在间接寻址之时改变寄存器的数值。
注意:AT&T汇编格式与Intel汇编格式略有不同,Linux内核使用的是AT&T汇编格式。
(2)其他指令
堆栈是向下增长的,有一个基址ebp指向堆栈栈底
- pushl 压栈,esp减4,把eax放入esp内存位置
- popl 出栈,从堆栈栈顶取32位放到寄存器eax里面,有两个动作:首先间接寻址,把栈顶数值放到eax里面,再把栈顶加4。
- call 函数调用,把当前的eip压栈,给eip赋新值;
注意:*是指这些指令是伪指令,程序员不能直接修改这些,即eip寄存器不能被直接修改,只能通过特殊指令间接修改。
4.将C代码编译成汇编代码
(1)函数调用堆栈是由逻辑上多个堆栈叠加起来的
(2)函数的返回值默认使用eax寄存器存储返回给上一级函数
(3)使用命令编译成汇编代码:gcc –S –o main.s main.c -m32
(二)操作系统是如何工作的
1. 堆栈——堆栈式C语言程序运行时必须的一个记录调用路径和参数的空间。包括:函数调用框架;传递参数;保存返回地址(如eax);提供局部变量空间
2. 堆栈寄存器:esp 堆栈指针和ebp 基址指针(在C语言中表示当前函数调用基址)
3. 堆栈操作:push栈顶指针减少4个字节(32位)和pop 栈顶指针增加4个字节
4. 参数传递与局部变量
(1)建立框架(相当于 call 指令)
push %ebp
movl %esp,%ebp
(2)拆除框架(相当于 ret 指令)
movl %ebp,%esp
pop %ebp
函数返回时一定会拆除框架,建立和拆除是一一对应的。
(3)传递参数
在建立子函数的框架之前,局部变量的值保存在调用者堆栈框架中,所以在子函数框架建立之前可以采用变址寻址的方式将变量值入栈。
!函数的返回值通过eax寄存器传递
(三)构造一个简单的Linux系统MenuOS
1. 计算机三个法宝:存储程序计算机、函数调用堆栈、中断
2. 操作系统两把宝剑:中断上下文的切换(保存现场和恢复现场)以及进程上下文的切换
3. 总结:rest_init为0号进程,一直存在。0号进程创建了1号进程kernel_init,还创建了其他的服务线程。即道生一(start_kernel....cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)。
Linux在无进程概念的情况下将一直从初始化部分的代码执行到start_kernel,然后再到其最后一个函数调用rest_init。
从rest_init开始,Linux开始产生进程,因为init_task是静态制造出来的,pid=0,它试图将从最早的汇编代码一直到start_kernel的执行都纳入到init_task进程上下文中。在rest_init函数中,内核将通过下面的代码产生第一个真正的进程(pid=1)。然后init_task变为一个idle task,init_idle函数的第一个参数current就是&init_task,在init_idle中将会把init_task加入到cpu的运行队列中,这样当运行队列中没有别的就绪进程时,init_task(也就是idle task)将会被调用,它的核心是一个while(1)循环,在循环中它将会调用schedule函数以便在运行队列中有新进程加入时切换到该新进程上。
(四)扒开系统调用的三层皮
1.用户态和内核态
- 用户态:在相应的低执行状态下,代码的掌控范围受到限制,只能在对应级别允许的范围内活动
- 内核态:在高执行级别下,代码可以执行特权指令,访问任意的物理地址。
Intel x86 CPU有四种不同的执行级别0—3,Linux只是用了期中的0级和3级分别表示内核态和用户态。
2.理解中断处理的完整过程:中断信号(int指令)完成:保存cs:eip的值、当前堆栈段栈顶和当前标志,同时加载了当前中断信号或是系统调用的相关联的中断服务入口到cs:eip里面,把当前对战段和esp也加载到CPU里面。
SAVE ALL完成后若没有发生调度,则接着执行RESTORE_ALL;若发生进程调度,则当前的状态会暂时的保存在系统里面,当下一次发生进程调度切换到当前进程时再接着执行完毕。
3. 系统调用的三个层次
系统调用的三个层次依次是:xyz函数(API)、system_ call(中断向量)和 sys_ xyz(中断服务程序)。
4. 总结:
在Linux系统中是通过激活0x80中断来触发系统调用的,需要调用的系统调用号实现赋值给eax存储器,如果有传入参数可赋值给ebx寄存器,如果多于1个则按顺序赋值给ebx、ecx、edx、esi、edi、ebp,如果超过6个则通过指针变量指向另一片堆栈区,如果无参数传入则赋值为0。
虽然Intel X86 CPU有4种执行级别0~3,但是在Linux系统中仅使用了0和3级,分别表示内核态和用户态。一些涉及底层、硬件、核心的操作必须在内核态下才允许执行,为操作系统程序和驱动程序专享,普通程序仅能执行在用户态下。如果普通程序需要涉及内核态的操作,就需要通过系统调用来实现。这样做的好处是屏蔽平台相关操作降低了软件开发难度,增强了系统安全性,使程序具有更好的移植性(Linux系统及其他Unix系统遵循统一标准,系统调用基本一样)。
(五)进程额管理和进程的创建
操作系统内核三大功能:进程管理(核心)、内存管理和文件系统。
1.Linux通过复制父进程来创建一个新进程,通过调用do_fork来实现。
2.Linux为每个新创建的进程动态地分配一个task_struct结构。
3.为了把内核中的所有进程组织起来,Linux提供了几种组织方式,其中哈希表和双向循环链表方式是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处于同一状态的进程组织起来。
4.fork()函数被调用一次,但返回两次。
(六)可执行程序的装载
1.可执行程序过程:先预处理.cpp,在编译成汇编代码.s到目标代码.o,再链接成可执行文件,加载到内存中执行。
2.可执行文件加载到内存中开始执行的第一行代码,0X8048X00为实际的入口。
3. 动态链接分为可执行程序装载时动态链接和运行时动态链接。
4. do_ execve调用do_ execve_ common,do_ execve_ common主要依靠exec_ binprm,其中重要的函数:search_binary_handler(bprm)。
(七)进程的切换和系统的一般执行过程
1. 进程调度算法——每个进程对CPU、I/O等资源需求不一样。
- 第一种分类:I/O密集型(I/O-bound)和CPU密集型(CPU-bound)
- 第二种分类:批处理进程;实时进程;交互式进程
2. 进程调度(schedule()函数实现)的时机:
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
- 内核线程(只有内核态没有用户态)可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
- 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
注意:用户态进程只能被动调度,内核线程是只有内核态没有用户态的特殊进程。
3. 操作系统(任何计算机系统都包含一个基本的程序集合)有两个目的:
- 与硬件交互,管理所有的硬件资源;
- 为用户程序(应用程序)提供一个良好的执行环境。
4. 本周主要理解Linux中进程调度与进程切换过程。进程调度是按一定的策略动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。而进程切换是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。实质上就是把进程存放在处理器的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。
二、学习感想与体会:
作为一名大三学生,从这学期还没开学开始学习云课堂的《Linux内核分析》,跟着孟老师一步一步了解Linux内核,到现在半个学期已经过去,又对课本《Linux内核设计与实现》的一些章节,搭配着视频进行学习巩固。时间感觉过得很快。但同时也学到了不少知识。
首先,上个学期开始接触Linux,这次又通过这门课的学习,加深了对操作系统理论的理解,知道了Linux系统是如何工作的,如何通过代码阅读、调试去跟踪验证Linux系统的运行机制。其次,Linux作为一个极其成功的操作系统,其内核纷繁复杂、博大精深,我个人学习起来也是相当困难。虽然完成了网课、看了课本,孟老师您也讲得很幽默,但我还是感觉自己刚刚开始学习,而且需要在深入挖掘的东西还有很多很多。
通过半个学期的学习,我认为重要的不是学习到了多少内核代码(其实也很重要);但更重要重要的是学习方法,即从何处着手学习Linux内核,例如:如何调试内核、如何看懂内核中的汇编代码,如何分析系统调用等。这也是我学习之后最大的收获。总之,虽然网课结束了但学习还没有结束,继续加油~
附录——博客作业列表:
第一周学习总结——计算机是如何工作的:http://www.cnblogs.com/lhc-java/p/5217595.html
第二周学习总结——操作系统是如何工作的:http://www.cnblogs.com/lhc-java/p/5246779.html
第三周学习总结——构造一个简单的Linux系统MenuOS:http://www.cnblogs.com/lhc-java/p/5271085.html
第四周学习总结——扒开系统调用的三层皮(上):http://www.cnblogs.com/lhc-java/p/5282826.html
第五周学习总结——扒开系统调用的三层皮(下):http://www.cnblogs.com/lhc-java/p/5326027.html
第六周学习总结——进程额管理和进程的创建:http://www.cnblogs.com/lhc-java/p/5340414.html
第七周学习总结——可执行程序的装载:http://www.cnblogs.com/lhc-java/p/5361824.html
第八周学习总结——进程的切换和系统的一般执行过程:http://www.cnblogs.com/lhc-java/p/5389954.html