zoukankan      html  css  js  c++  java
  • Lab4——课程学习总结报告

    临近期末考试,把Linux操作系统分析的内容又重新梳理了一遍,把个人认为比较重要的知识点总结如下:

    这门课由孟老师和李老师共同授课,孟老师采用的参考书籍是《庖丁解牛Linux内核分析》,这本书我也看了电子版,书中的内容由浅入深、层层深入,环环相扣,而且还有配套的实验教程(在前面的博文中已经提交了相应的实验报告),作为Linux入门书籍是一个很好的选择;通过亲自操作每一个实验,也能够将书上讲的知识形成系统;李老师采用的书籍是《深入理解Linux内核》,个人认为这本书要更难一点,里面涉及到较多的代码分析。

    一、《庖丁解牛Linux内核分析》中提到计算机”三大法宝“即是:存储程序计算机、函数调用堆栈、中断;操作系统两大宝剑即:中断上下文和进程上下文

    1.1 首先是存储程序计算机

    将编写好的程序和数据先存⼊存储器中,然后启动计算机⼯作,这就是存储程序的基本含义。即冯·诺伊曼体系结构

     CPU、内存和I/O设备通过总线连接。

    内存中存放指令和数据。

    “计算机内部采⽤⼆进制来表示指令和数据”表明,指令和数据的功能和处理是不同的,但都可以⽤⼆进制的⽅式存储在内存中。

    1.2 函数调用堆栈

    在这一部分,我们从底层汇编语言的角度分析程序的运行;

    首先,是汇编语言的语法知识补充,对普通汇编语句和寄存器相关操作有所了解;接着深入到堆栈操作和函数的调用、返回,并采用gcc指令查看汇编代码;最后从整体上分析函数调用堆栈框架的形成过程

     最后做了一个实验  编写⼀个最精简的操作系统内核 ,通过这个实验对Linux系统的相关操作更加熟悉,并且对Linux内核的大致架构和C语言内嵌汇编代码也有了一定的概念

    1.3 中断——系统调用

    在前面的学习中,中断这部分我们主要学习了系统调用,

    宏观上 Linux 操作系统的体系架构分为⽤户态和内核态。计算机的硬件资源是有限的,为了减少有限资源的访问和使⽤冲突,CPU 和操作系统必须提供⼀些机制对⽤户程序进⾏权限划分。

    系统调⽤的意义是操作系统为⽤户态进程与硬件设备进⾏交互提供了⼀组接⼝,如下图所示:

         

     从⽤户态进⼊内核态是由中断触发的,可能是硬件中断,在⽤户态进程执⾏时,硬件中断信号到来,进⼊内核态,就会执⾏这个中断对应的中断服务例程。也可能是⽤户态程序执⾏过程中,调⽤了⼀个系统调⽤,陷⼊了内核态,叫作陷阱(trap)(系统调⽤是特殊的中断)。

    具体来说,1)在Linux中通过执⾏int $0x80或syscall指令来触发系统调⽤的执⾏,其中这条int$0x80汇编指令是产⽣中断向量为128的编程异常(trap)
     2)内核通过给每个系统调⽤⼀个编号来进行区分,即系统调⽤号,将API函数xyz()和系统调⽤内核函数sys_xyz()关联起来了;

    内核实现了很多不同的系统调⽤,⽤户态进程必须指明需要执⾏哪个系统调⽤,这需要使⽤EAX寄存器传递⼀个名为系统调⽤号的参数。除了系统调⽤号外,系统调⽤也可能需要传递参数
    这一部分,我们着重学习了从触发系统调用到系统函数处理任务完成返回后的过程,通过实验完成了系统调用内嵌汇编代码的编写,并且利用gdb调试跟踪了系统调用从触发到返回的过程;
    关于中断上下文切换部分,这里并未详细阐述,在李老师的课中着重分析了中断处理的全过程

    1.4 关于进程的描述和创建

    操作系统内核中最核⼼的功能是进程管理

    1)在Linux内核中⽤⼀个数据结构struct task_struct来描述进程

     struct list_head tasks把所有的进程⽤双向链表链起来,⽽且还会头尾相连把所有的进程⽤双向循环链表链起来

    2)进程的创建

    其中,init_task为第⼀个进程(0号进程)的进程描述符结构体变量,它的初始化是通过硬编码⽅式固定下来的。除此之外,所有其他进程的初始化都是通过do_fork复制⽗进程的⽅式初始化的。

    1号和2号进程的创建是start_kernel初始化到最后由rest_ init通kernel_thread创建了两个内核线程:⼀个是kernel_init,最终把⽤户态的进程init给启动起来,是所有⽤户进程的祖先;另⼀个是kthreadd内核线程,kthreadd内核线程是所有内核线程的祖先,负责管理所有内核线程。

     

     3)理解了进程的创建和描述后,自然回想到我们编写的可执行程序是如何作为一个进程工作的

    程序从源代码到可执⾏⽂件的编译步骤⼤致分为:预处理、编译、汇编、链接

    ELF(Executable and Linkable Format)即可执⾏的和可链接的格式, 是⼀个⽂件格式的标准。ELF 格式的⽂件⽤于存储 Linux 程序。ELF 是⼀ 种对象⽂件的格式,⽤于定义不同类型的对象⽂件中都有什么内容、以什么样的格式放这些内容。汇编后形成的.o格式的⽂件已经是ELF格式⽂件,程序编译后⽣成的⽬标⽂件⾄少含有3个节区(Section),分别为.text、.data和.bss。

    链接是将各种代码和数据部分收集起来并组合成为⼀个单⼀⽂件的过程,这个⽂件可被加载(或被复制)到内存中并执⾏。通俗地说,链接就是把多个⽂件拼接到⼀起,本质上是节的拼接。链接从过程上讲分为符号解析和重定位两部分;根据链接时机的不同,⼜分为静态链接和动态链接两种。

    Shell会调⽤execve系统调⽤接⼝函数将命令⾏参数和环境变量传递给可执⾏程序的main函数

    内核装载可执⾏程序的过程,实际上是执⾏⼀个系统调⽤execve,和前⾯分析的fork及其他的系统调⽤的主要过程是⼀样的
    二、李老师的课程内容包括:中断和异常、定时测量、进程管理、linux驱动程序基础、文件系统、关于内核的根文件系统挂载

    2.1 中断和异常

    中断可分为:

    1)外部中断(硬件中断)

    2)内部中断(软件中断)/异常:

    处理器探测异常:由CPU执行指令时探测到一个反常条件时产生,如溢出、除0错等。

    对于处理器探测异常,根据异常时保存在内核堆栈中的eip的值可以进一步分为

    • 故障(fault):eip=引起故障的指令的地址,通常可以纠正,比如缺页异常
    • 陷阱(trap):eip=随后要执行的指令的地址

    编程异常:由编程者发出的特定请求产生,通常由int类指令触发,比如系统调用

    中断执行过程:

    1)确定与中断或者异常关联的向量i(0~255)
    2)读idtr寄存器指向的IDT表中的第i项
    3)从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符
    4)确定中断是由授权的发生源发出的。

    • 中断:中断处理程序的特权不能低于引起中断的程

          序的特权(对应GDT表项中的DPL vs CS寄存器中的CPL)                               

    • 编程异常:还需比较CPL与对应IDT表项中的DPL

    5) 检查是否发生了特权级的变化,一般指是否由用户态陷入了内核态。如果是由用户态陷入了内核态,控制单元必须开始使用与新的特权级相关的堆栈

    • a,读tr寄存器,访问运行进程的tss段
    • b,用与新特权级相关的栈段和栈指针装载ss和esp寄存器。这些值可以在进程的tss段中找到
    • c,在新的栈中保存ss和esp以前的值,这些值指明了与旧特权级相关的栈的逻辑地址

    6)若发生的是故障,用引起异常的指令地址修改cs和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行(这是因为cs,eip默认保存吓一跳指令的地址,但是这个故障还要再次执行当前指令,因此要进行修改)
    7)在栈中保存eflags、cs和eip的内容(保存在被中断进程的内核栈中)
    8)如果异常产生一个硬件出错码,则将它保存在栈中
    9)装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这对寄存器值给出中断或者异常处理程序的第一条指定的逻辑地址

    从中断/异常返回

    中断/异常处理完后,相应的处理程序会执行一条iret汇编指令,这条汇编指令让CPU控制单元做如下事情:
    1)用保存在栈中的值装载cs、eip和eflags寄存器。如果一个硬件出错码曾被压入栈中,那么弹出这个硬件出错码
    2)检查处理程序的特权级是否等于cs中最低两位的值(这意味着进程在被中断的时候是运行在内核态还是用户态)。若是,iret终止执行;否则,转入3
    3)从栈中装载ss和esp寄存器。这步意味着返回到与旧特权级相关的栈
    4)检查ds、es、fs和gs段寄存器的内容,防止恶意用户程序利用这些寄存器访问内核空间

    初始化中断描述符

    1)初步初始化:即汇编初始化,setup_idt 用ignore_int()中断处理程序去填充256项中断描述符表,这是一个空处理程序

    2)Start_kernel中再次初始化,内核用有意义的陷阱和中断处理程序替换ignore_int()这个空程序:

      • trap_init():对0-31项异常进行初始化,把一些异常处理函数插入到IDT表项中,由set_trap_gate、set_system_gate完成
      • init_IRQ:32-255对外部中断进行初始化系统初始化时,调用init_IRQ()函数用新的中断门替换临时中断门来更新IDT

    2.2 定时测量

    定时测量是由基于固定频率振荡器和计数器的几个硬件电路完成的

    实时时钟RTC、时间戳计数器TSC、PIT

    两个时钟源:定时时钟源和计时时钟源
    定时时钟源:周期性的引起时钟中断,一般为PIT,读取计时时钟源的值更新时间。
    定时时钟源周期性的产生中断,使time_interrupt()运行,对下面各项进行更新:

    • 更新自系统启动以来所经过的时间
    • 更新时间和日期
    • 确定当前进程的执行时间,考虑是否要抢占
    • 更新资源使用统计计数
    • 检查到期的软定时器

    计时体系结构中的数据结构和变量

    1) 计时时钟源

    2) Jiffies变量:相对时间,记录从系统启动直到当前时刻的系统时钟产生的滴答数,由系统时钟中断进行维护,用来提醒内核或用户进程一段指定的时间已经过去了

    3) Xtime变量:墙上时间,即系统的当前时间,基本上每个tick更新一次,在系统启动过程中根据实时时钟(RTC)芯片保存数据进行初始化,该变量的值在系统运行过程中由系统时钟中断处理程序负责在每次时钟中断时进行更新

    注册好的时钟源链表clocksource_list:linux中所有被注册的时钟源,按照精度排列,精度最高的头部

    时钟初始化:
    time_init中调用choose_time_init;
    choose_time_init被定义为hpet_time_init,如果没有hpet,就去执行setup_pit_timer,
    setup_pit_time注册一个事件pit_clockevent为Clockevent 设备并赋给global_clock_event(global_clock_event = &pit_clockevent),这个设备数据结构为irq0
    最后执行time_init_hook()来设置系统中断处理程序,调用setup_irq(0, &irq0)把irq0代表的action注册到链表上

     2.3 Linux驱动程序基础

    Linux设备

    • 字符设备
    • 块设备
    • 网络设备

     内核模块的基本结构
    头文件声明:头文件module.h和init.h是必须的。 #include<linux/module.h>包含了加载模块需要的函数和符号的定义;#include<linux/init.h>包含了模块初始化和清理函数的定义;如果模块在加载时允许用户传递参数,模块还应该包含moduleparam.h头文件。
    模块许可声明:MODULE_LICENSE宏声明此模块的许可证
    初始化、清理函数声明:在2.6内核模块必须调用宏module_init和module_exit去注册初始化与清理函数,需要注意的是初始化清理函数必须在宏module_init和module_exit使用前定义

    直接在内核源码树下完成模块的构建
    以字符设备为例:
    1)首先在…/drivers/char目录下新建一个子目录examples,创建完这个目录后需要在其中创建两个文件,驱动程序模块的源代码文件和makefile
    2)接下来在内核的配置文件中增加新的选项:在…/driver/char/kconfig文件中增加针对example的配置选项,当设置完kconfig文件并且保存在…/driver/char/子目录下后,上述的代码片段最终在当前的操作系统内核配置选项中增加了新的一项config_examples,相应的配置程序需要通过下面的命令进行调用,然后进行相关配置完成相关配置选项的配置工作
    3)接下来修改字符设备总的makefile,添加example选项,按照(2)进行配置
    4)把驱动程序模块安装在文件系统相应的目录中

    linux基于文件系统管理驱动程序的实现方式

    linux对设备的访问是通过对文件的操作来实现的,要访问某个设备,除了要有驱动程序,还要有设备文件

    在驱动程序中主要完成以下工作:

    1)获取并注册设备号

    2)新建、初始化并添加cdev结构体

    3)进行其他初始化

    对于设备文件来说:

    1)根据设备号,创建设备文件

    2)创建完成就可以open了,open后返回fd,利用fd进行readwrite等操作

    2.4 进程管理

    1、Linux进程
    Linux是一个多任务多用户操作系统,
    一个任务(task)就是一个进程(process)。每一个进程都具有一定的功能和权限,它们都运行在各自独立的虚拟地址空间。
    在Linux中,进程是系统资源分配的基本单位,也是使用CPU运行的基本调度单位。
    存放在磁盘上的可执行文件的代码和数据的集合称为可执行映象(Executable Image)。
    当一个可执行映像装入系统中运行时,它就形成了一个进程 。

    2、进程控制块PCB
    Linux中每一个进程由一个task_struct数据结构来描述(进程控制块PCB)
    task仅包含了描述符的指针,而不是描述符本身,进程描述符放在动态内存中而且和内核态的进程栈放在一个独立的8KB的内存区中
    (好处:通过esp就能引用进程描述符)
    Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:进程描述符Thread_info和内核态的进程栈
    从内核堆栈获得thread_info,根据thread_info描述符和内核态堆栈之间的配对,内核可以很容易的从堆栈寄存器的值获得thread_info的指针
    current宏获得当前进程描述符
     
    3、进程状态
     

    4、进程切换

    本质上说进程切换由两步组成:
    切换页全局目录以安装一个新的地址空间;
    切换内核态堆栈和硬件上下文(硬件上下文提供了内核执行新进程所需要的所有信息,包括cpu寄存器)
    switch_to宏执行进程切换,schedule()函数调用这个宏调度一个新的进程在CPU上运行
    这个宏和函数的被调用关系:
    schedule() --> context_switch() --> switch_to --> __switch_to()
    切换全局页表项这个切换工作由context_switch()完成;切换内核堆栈和硬件上下文由switch_to和__switch_to()完成
     
    5、Linux进程调度策略
    SCHED_OTHER普通进程的时间片轮转算法
    SCHED_FIFO实时进程的先进先出算法
    SCHED_RR实时进程的时间片轮转算法
     
    SHCED_RR和SCHED_FIFO的不同:
    当采用SHCED_RR策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
    SCHED_FIFO一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃。
    如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而RR可以让每个任务都执行一段时间。
    相同点:
    RR和FIFO都只用于实时任务。
    创建时优先级大于0(1-99)。
    按照可抢占优先级调度算法进行。
    就绪态的实时任务立即抢占非实时任务。
     
    2.5 根文件系统挂载流程
    内核从 Start_kernal()函数开始启动,执行到 mnt_init()函数中,init_rootfs()实现注册一个 rootfs 文件系统,
    init_mount_tree()把rootfs挂载到 / 目录。
    当内核代码继续运行到 kernel_init()函数时,将执行一个非常关键的函数 do_basic_setup().这个函数启动内核中
    的模块。在这个过程中,和根文件系统相关的函数 populate_rootfs()将会被执行,它将判断虚文件系统是 cpio-initrd
    格式,还是 image-initrd 格式。
    如果是 cpio-initrd 格式,它会把 initrd 直接释放到根目录,执行/init 脚本;
    如果是 image-initrd 格式,它将会把内容保存到/initrd.image 文件中,然后把 initrd.image 的内容读入/dev/ram0 中,内核以可读写的方式把/dev/ram0 挂
    载为原始的根文件系统,最后内核执行 initrd 上的/linuxrc 脚本文件,加载内核访问根文件所必须的驱动以及加载根文件系统

    不管是 cpio-initrd 格式的/init 脚本,还是 image-initrd 格式的/linuxrc 脚本被执行到了,这两个脚本的功能都是
    类似的,他们都运行在用户空间,加载内核访问根文件系统必须的驱动,切换根目录,挂载真正的根文件系统。
    最后根文件系统会依次查找/sbin/init,/etc/init,/bin/init,/bin/sh 是否有自动运行的程序,如果有则第一个应用程序
    将会被启动起来。
     

    2.6 文件系统

    vfs向上提供服务,向下可以兼容不同的具体文件系统。

    进程打开文件时。会维护一个进程的文件打开表fd,fd指向一个系统的文件打开表。在系统文件打开表中,存在一个f_op,对应的是具体的文件系统操作函数(read/write)。

    进程是程序的一次活动。 代码和可执行程序就存储在文件系统中。(Linux设备驱动和代码文件、可执行文件都存储在文件系统中)

    2.7 打开文件的过程

    根据给定的文件路径名搜索目录结构
    文件的FCB被复制到内存里的系统打开文件表(还有文件计数)里
    在进程打开文件表里创建一个指针字段,指向系统打开文件表里相应的表项
    打开文件系统调用返回指向进程打开文件表相应项的指针,以后文件的操作都通过这个指针进行

     
    总的来说,两位老师的课都是干货满满,孟老师从大致流程进行了介绍,我们首先建立的大概的框架,然后跟着李老师,开始了解模块细节,往框架中填充东西,
    两位老师的授课风格相辅相成,相得益彰,课程体验非常好,学到了很多知识,再次感谢两位老师。

  • 相关阅读:
    utf8.php
    common.php
    ubuntu 12.04 下 Vim 插件 YouCompleteMe 的安装
    Linux 获取文件夹下的所有文件
    poj 1659 Frogs' Neighborhood Havel-Hakimi定理 可简单图定理
    Huffman Coding 哈夫曼编码
    hdu 4778 Gems Fight! 博弈+状态dp+搜索
    print neatly 整齐打印 算法导论
    poj 2533 Longest Ordered Subsequence 最长递增子序列
    poj 3783 Balls 动态规划 100层楼投鸡蛋问题
  • 原文地址:https://www.cnblogs.com/pghzl-123/p/13274000.html
Copyright © 2011-2022 走看看