Linux内核设计期中总结
● 知识点
一、计算机是如何工作的
计算机是按照冯·诺依曼存储程序的原理。
在执行程序时须先将要执行的相关程序和数据放入内存储器中,在执行程序时CPU根据当前程序指针寄存器的内容取出指令并执行指令,然后再取出下一条指令并执行,如此循环下去直到程序结束指令时才停止执行。其工作过程就是不断地取指令和执行指令的过程,最后将计算的结果放入指令指定的存储器地址中。
计算机工作过程中所要涉及的计算机硬件部件有内存储器、指令寄存器、指令译码器、计算器、控制器、运算器和输入/输出设备等。
二、操作系统是如何工作的
进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程需要执行的时候操作系统对他分配相应的资源的。这里的资源指的就是计算机的硬件和系统资源。
- CPU只有一个但计算机会有很多进程,所以系统需要轮流给进程分配CPU这个硬件资源,这就是的时间片轮转调度。
操作系统的两把剑是中断上下文和进程上下文的切换。中断是多道程序操作系统的基点,没有中断机制程序只能从头到尾直到运行结束才有可能执行其他进程
进程在进行切换的时候,需要把当前进程的运行状态保存起来,等到下次执行的时候就可以知道是从哪开始执行的。由CPU和内核代码共同实现保护现场和恢复现场
三、构造一个简单的Linux系统的MenuOS
计算机三大法宝
存储程序计算机
函数调用堆栈
中断
操作系统两把宝剑
中断上下文的切换
进程上下文的切换
这一周最重要的是学会用gdb跟踪调试Linux内核的方法
-
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
-S freeze CPU at startup (use ’c’ to start execution)指在CPU初始化之前(刚启动的时候)将其冻结
-s shorthand for -gdb tcp::1234 指在1234这个端口上创建的gdb server,若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
打开另一个shell窗口
(gdb)file linux-3.18.6/vmlinux:在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234:建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel:断点的设置可以在target remote之前,也可以在之后
设置完断点后,输入c命令继续执行,函数会停在断点处。输入list指令可以查看断点处的代码
还要会简单分析start_kernel
init_tast:手工创建的pcb,0号进程即最终的idle进程。当系统没有进程需要执行时就调度到idle进程
trap_init:硬件中断,初始化一些中断向量,系统调用
sched_init:进程调度模块初始化
-
rest_init:创建1号进程
第一个用户态进程:init_process,1号进程,找默认路径下的程序作为1号进程
kthreadd:内核线程,用来管理系统资源
这周看书上第一第二章的内容:有关Linux的简介,链接看这 ↓
http://www.cnblogs.com/javajy/p/5282879.html
四、扒开系统调用的三层皮(上)
一、用户态、内核态和中断
一般现代CPU都有几种不同的指令级别
在高级别执行级别下,代码可以执行特权指令,访问任意的物理地址,称之为内核态
在相应的低指令执行级别下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动,称之为用户态
为什么会有权限级别划分
为了让系统本身更稳定,保证代码不被误写崩溃
CS寄存器的最低两位表明了当前代码的特权级
CPU每条指令的读取都是通过CS:eip这两个寄存器
CS:代码段选择寄存器
eip:偏移量寄存器
在Linux中,地址空间是一个显著的标志,0xc0000000以上的地址空间只能在内核态下访问
地址空间指逻辑地址而不是物理地址
中断处理时从用户态进入内核态的主要方式
系统调用是一种特殊的中断
寄存器上下文——从用户态切换到内核态
-
必须保存用户态寄存器的上下文:要保存哪些?保存到哪里?
中断/int指令会字啊堆栈上保存一些寄存器的值中断发生后的第一件事就是保存现场
二、系统调用概述和系统调用的三层皮
系统调用的概述
系统调用的意义:操作系统为用户态进程与硬件设备进行交互提供了一组接口————系统调用
把用户从底层的硬件编程中解放出来
极大的提高了系统的安全性
使用户程序具有可移植性
操作系统提供的API和系统调用的关系
API只是一个函数调用
系统调用通过软终端向内核法术一个明确的请求
不是每个API都对应一个特定的系统调用
API可能直接通过用户态的服务
一个单独的API可能调用几个系统调用
不同的API可能调用了同一个系统调用
返回值
大部分封装例程返回一个函数,其值的含义依赖于相应的系统调用
-1在多数情况下表示内核不能满足进程的请求
Licb中定义的errno变量包含特定的出错码
第五周学习了书上第五章的内容:系统调用 详细内容看链接 ↓
http://www.cnblogs.com/javajy/p/5299360.html
五、扒开系统调用的三层皮(下)
如何给MenuOS增加命令
步骤
rm menu -rf //强制删除
git clone http://github.com/menging/menu.git // 克隆相关信息到menu
cd menu
make rootfs //自动编译生成根文件系统,自动启动
如何给MenuOS增加命令
更新menu代码到最新版
在main函数中增加MenuConfig
增加对应的函数
make rootfs
使用gdb跟踪系统调用内核函数sys_time
1)qemu -kernel linux-3.18.6/arch/x86/bzImage -initrd rootfs.img -s -S
2)gdb
3)file linux-3.18.6/vmlinux
4)target remote:1234
5)设置断点
6)使用s进行单步的运行
这周看了课本第十八章的内容:调试 详细看链接 ↓
http://www.cnblogs.com/javajy/p/5333890.html
六、进程的描述和进程的创建
分析fork函数对应的内核处理过程sys _clone,理解创建一个新进程如何创建和修改task _struct数据结构;
fork进程的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0) ![进程管理总结](http://www.cnblogs.com/javajy/p/5338786.html)
{
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
fork系统调用在父进程和子进程各返回一次。
子 pid=0
父 pid=子进程的id
新进程是从哪里开始执行的?
ret _ from _ fork
这周看了课本第三章的内容:进程管理 链接 ↓
http://www.cnblogs.com/javajy/p/5338786.html
七、程序和进程
1、可执行文件是怎么得来的
(以C语言代码为例)
编译器预处理(.c)——>编程汇编代码——>(.asm)——>(汇编器)编译成目标代码(.o)——>链接成可执行文件(a.out)
shiyanlou:~/ $ cd Code
shiyanlou:Code/ $ vi hello.c
shiyanlou:Code/ $ gcc -E -o hello.cpp hello.c -m32 //预处理
shiyanlou:Code/ $ vi hello.cpp //替换宏定义
shiyanlou:Code/ $ gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译成汇编代码
shiyanlou:Code/ $ vi hello.s
shiyanlou:Code/ $ gcc -x assembler -c hello.s -o hello.o -m32
shiyanlou:Code/ $ vi hello.o //链接成一个可执行文件
shiyanlou:Code/ $ gcc -o hello hello.o -m32
shiyanlou:Code/ $ vi hello //可执行的二进制文件
shiyanlou:Code/ $ gcc -o hello.static hello.o -m32 -static
shiyanlou:Code/ $ ls -l
可执行文件加载到内存执行
预处理负责把include的文件包含进来以及宏替换等工作
2、目标文件的格式ELF
ABI:应用程序二进制接口
目标文件适应到某一种CPU体系结构
·一个可重定位文件
·一个可执行文件
·一个共享文件
一个ELF头在文件的开始保存了路线图,描述了该文件的组织情况
程序头表:告诉系统如何来创建一个进程的内存映象
section头表:包含了描述文件section的信息
3、静态链接的ELF可执行文件和进程的地址空间
可执行文件加载带内存中开始执行的第一行代码
一般静态链接会将所有的代码都放在一个代码段,动态链接的进程会有多个代码段
可执行程序、共享库和动态链接
1、装载可执行程序之前的工作
-
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
•$ ls -l /usr/bin 列出/usr/bin下的目录信息
•Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
•例如,int main(int argc, char *argv[])
•又如, int main(int argc, char *argv[], char *envp[])
•Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
•int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
•库函数exec*都是execve的封装例程
shell程序——>execve——>sys_execve 然后在初始化新程序堆栈时拷贝进去
函数先调用参数传递,在系统调用参数传递
-
execve系统调用返回到用户态从哪里开始执行
通过修改内核堆栈中的EIP值作为新程序的起点
装载时动链接和运行链接应用举例
动态链接分为可执行程序装载时动态链接和运行时动态链接
-
准备.so文件
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example -
编译成libshlibexample.so文件(生成共享库文件)
1.$ gcc -shared shlibexample.c -o libshlibexample.so -m32
dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
-
编译成libdllibexample.so文件
1.$ gcc -shared dllibexample.c -o libdllibexample.so -m32
-
分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
main.c (1.9 KB) - Main program
编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
可执行程序的装载
2、sys_execve系统调用处理过程
ELF可执行文件被默认映射到0x8048000这个地址
需要动态链接的可执行文件去加载链接器ld
elf_entry:指向可执行文件里规定的头部(main函数对应的位置);如果是动态链接,就是动态链接器的起点(用户态的起点),将cpu控制权交给ld来加载依赖库并完成动态链接
start_thread:把我们返回用户态的位置从0x8048000的下一条变成我们规定的entry的位置
- 对于ELF格式的可执行文件fmt->load_ binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读
3、庄生梦蝶
庄周(调用execve的可执行程序)入睡(调用execve陷入内核)
醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)
4、浅析动态链接的可执行程序的装载
动态链接库的依赖关系会形成一个图
可以关注ELF格式中的.interp和.dynamic
动态链接的过程主要不是内核而是由动态链接器来完成的
这周看了上学期学过了深入理解计算机系统上的第七章:链接
(又一次学这一部分链接 ↓)
http://www.cnblogs.com/javajy/p/4836812.html
八、进程的切换和执行过程概述
1、进程调度与进程调度的时机分析
不同类型的进程有不同的调度需求
-
第一种
I/O-bound
· 频繁的进行I/O
· 通常会花费很多的时间等待I/O操作的完成
CPU-bound
· 计算密集型
· 需要大量的CPU时间进行计算 //导致交互式计算反应迟钝 -
第二种
批处理进程
· 不必与用户交互,通常在后台运行
· 不必很快响应
· 典型的批处理程序:编译程序和科学计算
实时进程
· 有实时需求,不应被低优先级的进程阻塞
· 响应的时间要短、要稳定
· 典型的实时进程:视频音频、机械控制
交互式进程
· 需要经常与用户进行交互,要花很多时间等待用户输入
· 响应时间要快,平均延迟要低于50~150ms
· 典型的交互式程序:shell、文本编辑程序、图形应用
Linux既支持普通的分时进程,也支持实时进程。调度是多种调度策略和调度算法的混合,基于分时和优先级
调度策略:是一组规则,他们决定什么时候以怎样的方式选择一个新进程运行。(内核中的调度算法相关代码使用了类似OOD中的处理模式)
Linux的进程根据优先级排队,优先级是动态的
根据特定的算法计算出进程的优先级,用一个值表示
这个值表示把进程如何适当的分配给CPU
调度程序会根据进程的行为周期性的调整进程的优先级
进程调度的时机
中断处理过程(时钟中断、I/O中断、系统调用和异常)中直接调用schedule(),或者返回用户态时根据need-resched标记调用
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说线程作为一类特殊的进程,只有内核态没有用户态可以主动调度也可以被动调度
-
用户态进程无法实现主动调度,只能被动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进程调度
schedule函数:在运行队列中找到一个函数,把CPU分配给他
2、进程上下文切换相关代码分析
进程的切换:为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程
挂起与中断是保存现场的区别:中断前后是同一个进程上下文,只是由用户态和内核态执行。进程上下文是两个进程。
进程上下文包含进程执行的信息
用户地址空间:程序代码、数据、用户堆栈
控制信息:进程描述符、内核堆栈
硬件上下文
- next = pick_next_task(rq, prev); //进程调度算法都封装这个函数内部
- context_switch(rq, prev, next); //进程上下文切换
- switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
Linux系统的一般执行过程分析
最一般的情况:从正在运行的用户态进程x切换到运行用户态进程y
1、正在运行的用户态进程x
2、发生中断——save cs:eip/esp/eflags(current) to kernel stack,
then load cs:eip(entry of a specific ISP)
and ss esp(point to kernel stack)
3、SAVE_ALL //保存现场
4、中断处理过程中或中断返回前调用了schedule()
其中的switch-to做了关键的进程上下文切换
5、标号1之后开始运行用户态进程y
6、restore-all //恢复现场
7、iret - pop cs:eip/ss:esp/eflags from kernel stack
8、继续运行用户态进程y
几种特殊情况
- 通过中断处理过程中的调度时机,用户态进程和内核线程之间相互切换和内核线程之间相互切换与最一般的状况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换
- 内核线程主动调用schedule,只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略
- 创建子进程的系统调用在子进程中的执行起点及返回用户态
- 加载一个新的可执行程序后返回到用户态的情况
2、内核与舞女
ps:忍不住的想吐槽,我还是个孩子啊!
那么问题来了:如果进程发生切换,代码段堆栈段等是如何进行切换的
内核就是一个出租车!!!哪个进程招手都可以!!!
进程就是出租车的客人!!!
内核是各种中断处理过程和内核线程的集合
操作系统架构和执行过程
Linux操作系统架构概览
回顾
操作系统:基本程序集合
- 内核:进程管理,进程调度,进程间通讯机制,内存管理,中断异常处理
- 其他:函数库,shell程序
- 目的:与硬件交互,管理所以的硬件资源。为用户程序提供一个良好的执行环境
课本第四章:进程调度 链接 ↓
http://www.cnblogs.com/javajy/p/5380783.html
总结
学了半学期的Linux大体上来说是有收获的,学会分析了Linux系统的内核代码以及一些基本的命令操作,明白了进程是如何从产生到结束所经历的一系列的过程。
学了这些最主要的还是要自己经常多动手练习。实验楼提供的环境非常不错,(虽然特别卡)有些实验做一遍可能只是按照步骤来完成得到结果单不知道自己在做什么,但是反复多做几次之后(这就是卡带来的好处,不得不做好多遍)好多步骤也都明白了这是在做什么,明白原理之后自己动手实践起来就不是特别难。比如第八周的视频中就没有给实验步骤,但是根据要求就能想到这跟之前做过的实验都是一样的就可以很容易的做出来。