20135220谈愈敏Linux_总结

Linux_总结

具体博客链接

一、计算机是如何工作的

存储程序计算机工作模型

  • 冯诺依曼体系结构
    • 从硬件角度来看:CPU和内存,由总线连接,CPU中有一个名为IP的寄存器,总是指向内存的某一块:CS,代码段,执行命令时就取IP指向的一条指令,然后IP自加1,就指向下一条指令。
    • 从程序员角度来看:即存储程序计算机,内存存储数据和指令,CPU就是一个for循环,总是在执行下一条指令,CPU负责解释和执行这些指令。

X86汇编基础

  • CPU寄存器
    • 通用寄存器+段寄存器+标志寄存器
  • X86汇编指令
    • mov指令:b,w,l,q分别代表8位,16位,32位,64位
    • 寻址方式:寄存器寻址,立即数寻址,直接寻址,间接寻址,变址寻址
    • 汇编指令:push,pop,call,ret
    • leave
  • 分析汇编指令片段

汇编一个简单的C程序分析其汇编指令执行过程

  • 汇编一个简单的C程序
  • C程序和对应的汇编指令
  • 完整汇编程序执行过程分析

二、操作系统是如何工作的

函数调用堆栈

  • 计算机三大法宝
    • 存储程序计算机工作模型
    • 函数调用堆栈
    • 中断
  • 深入理解函数调用堆栈
    • 函数调用框架
    • 传递参数
    • 保存返回地址
    • 提供局部变量空间
  • 堆栈相关寄存器:esp堆栈指针ebp基址指针,记录当前函数调用基址
  • 堆栈操作:push,pop
  • cs:eip:总是指向下一条的指令地址,执行call的时候,保存下一条指令地址到栈顶,然后cs:eip指向调用函数入口地址。
  • 参数传递与局部变量

借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断

  • mykernel实验背后涉及的思想
  • 利用mykernel实验模拟计算机硬件平台
  • 由CPU和内核代码共同实现了保存现场和恢复现场

在mykernel基础上构造一个简单的操作系统内核

  • C代码中嵌入汇编代码的写法
  • 一个简单的操作系统内核源代码
    • mypcb.h:头文件
    • mymain.c: 内核初始化和进程的启动
    • myinterrupt.c:进程的切换
  • 运行这个精简的操作系统内核

三、构造一个简单的Linux系统MenuOS

Linux内核源代码简介

  • 操作系统两把宝剑
    • 中断上下文的切换
    • 进程上下文的切换
  • Linux内核源代码

构造一个简单的Linux系统

  • 构造一个简单的Linux系统MenuOS
    • qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
      • qemu:启动一个虚拟机平台
      • -kernel:给一个内核,操作系统
      • -initrd:驱动所需的硬盘
      • rootfs.img:放一个可执行文件由menuOS源代码编译成的init

跟踪调试Linux内核的启动过程

  • 使用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选项
    • gdb
    • (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之前,也可以在之后
      • 符号:全局函数名,全局变量名,汇编时没有,不需要符号只是一个地址,但调试时需要,所以有符号表:符号与在内存中地址的一一对应。
  • 简单分析一下start_kernel
    • 全局变量init_task,即手工创建的PCB,0号进程即最终的idle进程
    • trap_init涉及到中断,初始化一些中断向量
    • mm_init:内存管理模块初始化
    • sched_init:调用进程调度初始化
    • rest_init中:
      • kernel_init:创建1号进程
      • 第一个用户态进程:init_process,1号进程,找默认路径下的程序作为1号进程
      • kthreadd:内核线程,用来管理系统资源
      • 当系统没有进程需要执行时就调度idle进程,即0号进程,一直存在。

四、系统调用

用户态、内核态和中断

  • 用户态&内核态(CPU执行级别)
    • 内核态:高级别执行,可以使用特权指令,访问任意的物理地址。对应x86 0级
    • 用户态:低级别执行,代码范围受到限制。对应x86 3级(x86CPU有0-3四个级别)
  • 如何区分用户态&内核态?
    • 0xc0000000以上的地址只能在内核态下访问
    • 0x00000000-0xbfffffff两种状态都行
  • 中断处理是从用户态进入内核态主要的方式
    • 从用户态进入内核态:必须保存用户态的寄存器上下文
    • 中断/int指令在堆栈上保存寄存器的值:用户态/内核态栈顶地址(ss:esp)、状态字(eflags)、cs:eip值(内核态时指向中断服务程序入口)

系统调用概述

  • 系统调用的意义
    • 用户不管硬件编程
    • 提高系统安全性
    • 用户程序可移植
  • API和系统调用
    • API:应用编程接口,是一个函数定义
    • 系统调用:通过软中断向内核发出明确请求
    • 不是每个API都对应一个特定的系统调用
  • 应用程序、封装例程、系统调用处理程序、系统调用服务例程之间的关系
  • 系统调用三层皮:
    • API(xyz)
    • 中断向量(system_call)
    • 中断服务程序(sys_xyz)

使用库函数API和C代码中嵌入汇编代码触发同一个系统调用

  • 使用库函数API获取系统当前时间
  • 使用C代码中嵌入汇编代码触发系统调用获取系统当前时间
    • 传递了一个系统调用号 - eax
    • 传递了参数 - ebx

给MenuOS增加time和time-asm命令

  • 更新menu代码到最新版
  • 在main函数中增加MenuConfig(一个命令一行,与上面的格式一样)
  • 增加对应的time和time-asm函数(就是上周写的两个函数)
  • make rootfs (rootfs是一个脚本,可以自动生成,编译)

使用gdb跟踪系统调用内核函数sys_time

  • c:继续执行,停在断点处
  • n/s:单步运行,s进入函数,n不进入

系统调用在内核代码中的处理过程

  • int 0x80触发一个系统调用 ->执行系统调用处理函数system_call ->返回到用户态
    • 返回到用户态之前有一个进程调度的时机,会跟踪到schedule
  • 中断向量:int 0x80 ->system_call,初始化时绑定
  • 系统调用号:将xyz和中断服务程序sys_xyz关联起来
  • 系统调用机制的初始化
    • main.c中start_kernel函数:trap_init()
      • set_system_trap_gate(SYSCALL_VECTOR,&system_call)
      • SYSCALL_VECTOR:系统调用的中断向量
      • &system_call:汇编代码入口
  • 理解system_call到iret之间的主要代码
    • 从系统调用入口开始:ENTRY(system_call)
    • SAVE_ALL //保存现场
    • system_call中:call *system_ call_table(,%eax,4) //调用了系统调用处理函数,有系统调用号eax中,是实际的系统调用处理程序。
    • 当前任务syscall_ exit_ work里面有work_ pending里面有work_ notifysig //处理pending信号,不用管
    • 重要的是work_ resched:call schedule //决定了进程调度的代码,调用完会跳转到restore_all
    • restore_all //恢复现场
    • INTERRUPT_ RETURN //irp_return宏,中断处理过程在这结束

五、进程的描述和进程的创建

进程的描述

  • 操作系统的三大管理功能:
    • 进程管理(最重要的)
    • 内存管理
    • 文件系统
  • 进程控制块PCB task_struct:
    • 进程状态
    • 进程打开的文件
    • 进程优先级信息
  • 进程描述符task_struct数据结构

进程的创建

  • 进程的创建概览
    • 道生一(start_ kernel....cpu_idle)
    • 一生二(kernel_init和kthreadd)
    • 二生三(即前面0、1和2三个进程)
    • 三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
  • fork创建一个子进程
    • fork在父进程和子进程各返回一次
  • 创建一个新进程在内核中的执行过程
  • 新进程从哪里开始执行?
    • fork出来的子进程是从ret_from_fork开始执行的,然后跳转到syscall_exit,从系统调用中返回。

六、可执行程序的装载

预处理、编译、链接和目标文件的格式

  • 可执行程序是怎么得来的?
    • C代码
    • 编译器预处理
    • 编译成汇编代码
    • 汇编器编译成目标代码
    • 链接成可执行文件
    • 操作系统加载到内存执行
  • 目标文件的格式ELF
  • 静态链接的ELF可执行文件和进程的地址空间
    • 默认加载起始地址是0x8048000
    • ELF头部大小不同,程序入口点也将不同,程序入口在头部有定义:Entry point address,即是可执行文件加载到内存中开始执行的第一行代码

可执行程序、共享库和动态链接

  • 装载可执行程序之前的工作
    • 可执行程序的执行环境
    • 命令行参数和环境变量是如何保存和传递的?
    • 参数传递过程:shell程序 -> execve系统调用 -> sys_execve 内核处理函数在初始化新程序堆栈时拷贝进去
  • 装载时动态链接和运行时动态链接应用举例
    • 准备.so文件
    • 分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件

可执行程序的装载

  • 可执行程序的装载相关关键问题分析
    • 特殊系统调用:fork、execve
    • sys_execve内部会解析可执行文件格式
    • execve系统调用返回到用户态从哪里开始执行?load_ elf_ binary -> start_thread
  • sys_execve的内部处理过程
  • 使用gdb跟踪sys_execve内核函数的处理过程
  • 可执行程序的装载与庄生梦蝶的故事
  • 浅析动态链接的可执行程序的装载

七、进程的切换和系统的一般执行过程

进程切换的关键代码switch_to分析

  • 进程调度与进程调度的时机分析
    • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
    • 内核线程(只有内核态没有用户态的特殊进程)可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
    • 用户态进程无法实现主动调度,只能被动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
  • 进程上下文切换相关代码分析
    • 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;
    • 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
    • 进程上下文包含了进程执行需要的所有信息
      • 用户地址空间:包括程序代码,数据,用户堆栈等
      • 控制信息:进程描述符,内核堆栈等
      • 硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
    • schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
      • next = pick_ next_task(rq, prev);//进程调度算法都封装这个函数内部
      • context_switch(rq, prev, next);//进程上下文切换
      • switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程

Linux系统的一般执行过程

  • Linux系统的一般执行过程分析
    • 中断上下文的切换(中断和中断返回时CPU进行上下文切换)
    • 进程上下文的切换(进程调度过程中,从一个进程的内核堆栈切换到另一个进程的内核堆栈)
  • Linux系统执行过程中的几个特殊情况
    • 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
    • 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;//用户态进程不能主动调用
    • fork:创建子进程的系统调用在子进程中的执行起点(next_ ip = ret_ from_ fork)返回用户态,进程返回不是从标号1开始执行,直接跳转到ret_ from_fork执行然后返回到用户态;
    • 加载一个新的可执行程序后返回到用户态的情况,如execve,只是中断上下文在execve系统调用内部被修改了;

Linux系统架构和执行过程概览

  • Linux操作系统架构概览
  • 最简单也是最复杂的操作--执行ls命令
  • 从CPU和内存的角度看Linux系统的执行

学习心得

收获

还记得第一次看视频的时候是寒假,那时还在家,转眼我已经跟着网络课程学习了两个月,每周固定的看视频、做笔记、写博客、做实验,到现在已经有点习惯这种生活,突然没有视频可学,还有点空虚呢。依稀记得第一次视频老师您还说是除夕,听得到外面的鞭炮声,想想老师也是很辛苦的,很感谢老师能教给我们这么多知识。第一次亲身经历这种学习方式,也体验到了很多好处,比如每周的自评和互评,可以看到很多优秀的博客,加以学习,让自己变得更好,看见差的博客,会指出他的问题,也希望可以帮助到其他人。

通过八周的学习,第一次系统全面的理解了Linux系统:

  • 首先是进程的创建,由0号进程idle创建其子进程1号进程init和2号进程kthreadd,1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先。由fork来创建一个新的子进程
  • 系统调用其实就是一种中断,从用户态转到内核态,发生中断时关键点是中断上下文的切换(中断和中断返回时CPU进行上下文切换)。
  • 由中断返回到用户态之前有一个进程调度时机,可能就会发生进程切换,关键点是进程上下文的切换(进程调度过程中,从一个进程的内核堆栈切换到另一个进程的内核堆栈),调用了schedule(),关键切换由switch_to执行。
  • 进程是依附于程序存在的,可执行程序是通过预编译、编译、汇编、链接生成的。由execve装载一个可执行程序。

遗憾

虽然每周都在按部就班的学习,但还是在一些具体的问题上不求甚解,是一直跟着老师的思路在走,老师讲过的就知道,自己去看其他的就蒙圈了,没有更深的去理解,有一些游走于表面,特别是对代码啊堆栈的理解就有些模糊。还有就是总觉得老师的逻辑跟我的有点不太一样,每次跟着听都有些费劲,感觉太散了,自己要看几遍才能理解,然后总结按自己的想法去串一遍,自认效率比较低,所以花的时间就很多。

posted on 2016-04-21 21:46  20135220谈愈敏  阅读(218)  评论(0编辑  收藏  举报