一、《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系统的基础是内核、C库、工具集和系统的基本工具。
1.操作系统:整个系统中负责完成最基本功能和系统管理的部分。
2.内核(操作系统的内在核心,一般处于系统态):
由响应中断的中断服务程序;管理多个进程,分享处理器时间调度程序;管理进程地址空间的内存管理程序;网络、进程间通信等系统服务程序组成。
3.内核空间:系统态和被保护起来的内存空间
4.系统调用:应用程序与内核通信。应用程序通过系统调用界面陷入内核是应用程序完成工作的基本行为方式。
6.Unix内核通常需要硬件系统提供页机制(MMU)以管理内存,这样可以加强对内存空间的保护,并可以保证每个进程都运行于不同的虚地址空间上。
7.单内核与微内核比较:
(1)单内核——以单个静态二进制文件形式存放于磁盘中,所有内核服务在一个大内核地址空间上运行。
特点:内核可以直接调用函数,简单并性能高。但一个功能的崩溃会导致整个内核无法使用。
(2)微内核——内核按功能被划分成各个独立的过程。每个过程叫做一个服务器。所有服务器独立并运行在自己的地址空间上。
特点:通过消息传递处理为内核通信,采用进程间通信(IPC)机制。安全。一个服务器失效不会影响其他服务器。内核各个服务之间的调用涉及进程间的通信,比较复杂且效率低。
8.Linux内核总结:
为单内核,但具备微内核的一些特征:模块化设计、抢占式内核、支持内核线程、动态装载内核模块。同时避微内核设计上的性能缺陷:让所有事情运行在内核态,直接调用函数,无需消息传递。支持动态加载内核模块;支持对称多处理(SMP);内核可以抢占(preemptive),允许内核运行的任务有优先执行的能力;不区分线程和进程;提供具有设备类的面向对象的设备模型、热插拔事件,以及用户空间的设备文件系统(sysfs)。
(二) 第二章 从内核出发
1.使用Git(管理内核源码的分布式控制系统)获取最新提交到Linus版本树的一个副本:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
2.安装内核源代码
两种形式:GNU zip(gzip)(运行:$ tar xvzf linux-x.y.z.tar.gz)和bzip2(运行:$ tar
xvjf linux-x.y.z.tar.bz2)。
3. 配置内核(关于make与config)
配置编译过程:
• make config:遍历所有配置项,并让用户选择
• make deconfig:基于默认的配置
• make oldconfig:先将/boot目录下的配置文件写进.config文件中,采用的是注释的形式写进新增加的功能。
• zcat /proc/config.gz > .config:配置选项CONFIG_IKCONFIG_PROC把完整的压缩过的内核配置文件存放在/
proc/config.gz中,再次编译时可以方便地克隆当前的配置。
• make:默认的Makefile自动化编译。
4.减少编译的垃圾信息;衍生多个编译作业
5.安装新内核——把所有已编译的模块安装到正确的主目录/lib/modules下:% make modules_install。编译时在内核代码树的根目录下创建一个System.map文件(符号对照表),用来将内核符号与它们的起始地址对应起来。
6.同步和并发
• Linux是抢占多任务操作系统
• 内核支持对称多处理器系统(SMP)
• 中断是异步到来的
• Linux内核可以抢占
常用的解决竞争的办法是自旋锁和信号量
(三) 第三章——进程管理
1.进程就是处于执行期的程序(目标码存放在某种存储介质上),不仅局限于一段可执行程序代码,还包含其他资源,如打开的文件、挂起的信号、内核内部数据等。提供两种虚拟机制:虚拟处理器和虚拟内存。
2.线程(执行线程)是在进程中活动的对象,拥有独立的程序计数器、进程栈和一组进程寄存器。注意:内核调度的是线程而不是进程!
3.系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程为父进程,新产生的进程称为子进程。fork()系统调用从内和返回两次:一次回到父进程,一次回到新产生的子进程。
4. 共有五种进程状态:
- 运行TASK_RUNNING——进程是可执行的;或正在执行或等待执行。这是进程在用户空间中执行的唯一可能的状态。
- 可中断TASK_INTERRUPTIBLE——进程正在睡眠或阻塞,等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。出于此状态的进程也会因为接受到信号而提前被唤醒并随时准备投入运行。
- 不可中断TASK_UNINTERRUPTIBLE——通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。出于此状态的任务对信号不作响应。
- __TASK_TRACED——被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
- 停止__TASK_STOPPED——进程停止执行:进程没有投入或不能运行。在接受到SIGSTOP/SIGTSTP/SIGTTOU等信号或在调试期间接受到任何信号时进入此状态。
5.进程创建:写时拷贝——fork()——vfork()
6.进程终结:删除进程描述符——孤儿进程造成的进退维谷
(四)第四章——进程调度
1.多任务系统分为两类:非抢占式多任务和抢占式
2.策略:调度策略通常在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)。
3. 进程优先级
在某些系统中,优先级高的进程使用的时间片也比较长。调度程序总是选择时间片未用尽而且优先级最高的进程运行。用户和系统都可以通过设置进程的优先级来影响系统的调度。
Linux采用了两种不同的优先级范围——nice值和实时优先级值。
(1) nice值,范围是-20到19,数值越大优先级越低,默认值为0。Linux中,nice值则代表时间片的比例。可以通过ps-el命令查看系统中的进程列表,结果中标记NI的一列就是进程对应的nice值。
(2)
实时优先级值,默认0到99,数值越大优先级越高。任何实时进程的优先级都高于普通的进程。
4.公平调度——CFS中,任何进程所获得的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定。任何nice值对应的绝对时间是处理器的使用比。
5.调度的实现——CFS相关代码四个组成部分:时间记账、进程选择、调度器入口和睡眠和唤醒。
6.用户抢占(会检查need_ resched标志)发生时机:
- 从系统调用返回用户空间时;
- 从中断处理程序返回用户空间时。
7.内核抢占发生时机:
- 中断处理程序正在执行且返回内核空间之前;
- 内核代码再一次具有可抢占性的时候;
- 内核中的任务显式地调用schedule函数。
8.实时调度策略
linux 提供两种实时调度策略SCHED_FIFO和SCHED_RR。
- SCHED_FIFO实现了一种简单的、先入先出的调度算法:它不使用时间片.处于可运行状态的SCHED_FIFO 级的进程会比任何SCHED_NORMAL
级的进程都先得到调度。 - SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了.即 SC阻止RR 是带有时闹片的SCHED_FIFO-这是一种实时轮流调度算挂。
这两种算法实现的都是静态优先级。Linux实时调度算法是软实时工作方式——内核调度进程,尽量使进程在它的限定时间到来前运行,但内核不能保证能够总能满足。
实时优先级范围是0到MAX_RT_PRIO减1。默认情况下,MAX_RT_PRIO为100,nice值从-20到19直接对应的是100到139的实时优先级范围。
(五)第五章——系统调用
重点——Linux系统调用的规则和实现方法。
1.如何定义一个系统调用:asmlinkage
long sys_getpid(void)
- 首先,注意函数声明中的asmlinkage限定词,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。
- 其次函数返回long。为了保证32位和64位系统的兼容,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int在内核空间为long。
- 最后,注意系统调用get_pid()中的在内核中被定义成sys_getpid()。这是Linux中所有系统调用都应该遵守的命名规则,系统调用bar()在内核中也实现为sys_bar()函数。
2.Linux系统调用执行快,两个原因:
- 有很短的上下文切换时间。
- 系统调用处理程序和每个系统调用本身非常简洁。
3.通知内核的机制是靠软中断实现的
4.x86-32系统中,参数传递时ebx,ecx,edx,esi,edi按顺序存放前五个参数。给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。
5.系统调用上下文
• 内核在执行系统调用的时候处于进程上下文。
• 在进程上下文中,内核可以休眠并且可以被抢占。
• 当系统调用返回的时候,控制权仍在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去。
(六)第七章
链接
1.静态链接:链接器将重定位目标文件(relocatable
object files)组合成一个可执行目标文件。cpp(c previous processor,C预处理器);ccl(C编译器);as(汇编器)。
为了创建静态链接,链接器完成两个主要任务:
- 符号解析(symbol resolution):将每个符号引用和一个符号定义联系起来。
- 重定位(relocation):编译器和汇编器生成从0地址开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
2.目标文件有三种形式:
- 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件:包含二进制代码和数据,其形式可以直接拷贝到存储器并执行。
- 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或运行时被动态地加载到存储器并链接。
3.重定位由两步组成:
- 重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址。
- 重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。为了执行这一步,链接器依赖于称为重定位条目的可重定位目标模块中的数据结构。
4.注意:静态链接与动态链接的区别——静态链接是把程序所需要的库代码和数据拷贝和嵌入到引用它们的可执行文件中;而动态链接是所有引用该库的可执行文件文件共享这个.so(dll)文件中的代码和数据。
5.动态链接器通过执行下面的重定位完成链接任务:
- 重定位libc.so的文本和数据到某个存储器段
- 重定位libvector.so的文本和数据到另一个存储器段
- 重定位p2中所有对libc.so和libvector.so定义的符号的引用
- 最后动态链接器将控制传递给应用程序,此时共享库的位置已固定,并且在程序执行的过程中不会改变
学习感想与体会:
从这学期还没开学开始学习云课堂《Linux内核分析》,跟着孟老师一步一步了解Linux内核,到现在半个学期已经过去,又对课本《Linux内核设计与实现》的一些章节,搭配着视频进行学习巩固。时间感觉过得很快。但同时也学到了不少知识。
首先,通过这门课的学习,加深了我对操作系统理论的理解,知道了Linux系统是如何工作的,如何通过代码阅读、调试去跟踪验证Linux系统的运行机制。其次,Linux作为一个极其成功的操作系统,其内核纷繁复杂、博大精深,我个人学习起来也是相当困难。虽然完成了网课、看了课本,孟老师也讲得不错,但我还是感觉自己刚刚开始学习,而且需要在深入挖掘的东西还有很多很多。
通过半个学期的学习,我认为重要的不是学习到了多少内核代码(其实也很重要);但更重要重要的是学习方法,即从何处着手学习Linux内核,例如:如何调试内核、如何看懂内核中的汇编代码,如何分析系统调用等。总之,学习还没有结束,还有半个学期,继续加油~