20135125陈智威 

原创作品转载请注明出处

《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

 

    这学期开始学习Linux,刚刚接触,也是不容易的坚持了下来。第一次学习这种网上课堂,形式非常的新颖,图文结合,并且孟老师教授的也很详细,一些概念也说的通俗易懂,这个课程整个学习下来还是受益匪浅。

    现在整个网课的学习过程已经结束,按照孟老师的要求写一份关于Linux的总结和报告,有许多不足之处,欢迎大家指正,希望能一起进步。

(一)计算机是如何工作的

 

  • 总是通过EIP取得下一段要执行的代码,然后执行该段代码,即总是取指执行
  • 当进行函数调用时,堆栈会保存调用函数之前的程序状态,同时堆栈指针bp和sp会在一个伪初始位置
  • 每次函数调用结束,堆栈指针bp和sp回复到调用之前的状态

(二)操作系统是如何工作的

  

  • 三个法宝——存储程序计算机、函数调用堆栈、中断机制;
  • 在my_schedule函数中,完成进程的切换。进程的切换分两种:

 

     1.下一个进程没有被调度过;
     2.下一个进程被调度过,可以通过下一个进程的state知道其状态。

 

(三) Linux内核源代码简介

     start_kernel()是内核的汇编与C语言的交接点,在该函数以前,内核的代码都是用汇编写的,完成一些最基本的初始化与环境设置工作。start_kernel就像是c代码中的main函数。不管你关注Linux的内核模块,总是离不开start_kernel函数的,因为大部分模块的初始化工作都是在start_kernel中完成的。按照这节课的实验步骤,我们可以跟踪Linux内核的启动过程。

(四) 扒开系统调用的三层皮(上)

  

  • getpid的函数很简单,就是获取当前进程的进程号

      1.系统调用号放在eax中。
      2.系统调用的参数,按照顺序分别放在ebxecxedxesiediebp
      3.返回值使用eax传递

  • fork函数同样不需要参数,只有输出,
  • fork这个函数有个特点,就是调用一次返回两次,原因在于它复制出了一个子进程,执行同样地代码段。
  • 区分子进程和父进程的手段就是检查返回值。
  • read函数需要三个参数。参数保存在ebx、ecx等寄存器中,这里的三个参数就是放在这三个寄存器中。

(五) 扒开系统调用的三层皮(下)

 

  • set_system_trap_gate,设置系统陷阱门,即系统调用。
  •  使用gdb跟踪

 

  • make rootfs:自动编译,生成根文件系统,自动启动.
  • (gdb)list 查看代码.
  • (gdb)s 单步调试进入函数体.
  • (gdb)n 单步调试不进入函数体.

 

  • 给MenuOS增加time和time-asm命令、添加了fork

 (六) 进程的描述和进程的创建

  • Linux中一般进程都是由现有的一个进程创建的,也就是我们所说的父进程
  • 具体的创建是通过fork()实现的
  • fork()的大体工作过程:   

 

  1. 在内存中申请一页内存存放进程控制块task_struct,并返回进程号nr,并在task数组的nr处存放task_struct的指针,还要将task的当前指针current指到nr处;  
  2. 将父进程的task_struct的内容复制到新进程的task_struct中作为模版  
  3. 对task_struct中的信息进行修改,主要进行一下工作:设置父进程、清除信号位图、时间片、运行时间、根据当前环境设置tss(内核态指针esp0指向task_struct所在页的顶端)、设置LDT的选择子等(根据nr指向GDT中相应的ldt描述符)。  
  4. 设置新进程的代码段、数据段的基地址和段长:更新task_struct中的代码开始地址:进程号(nr)×64M,更新task_struct中局部描述符表中的代码段和数据段描述符。   
  5.  复制父进程的页表目录项和页表:在页目录表中,复制父进程的页表目录项,目的地址由新进程的线性地址计算出来;对每个对应的页表目录项申请一个空闲页,并用页表地址更新页表目录项,最后将父进程页表中各项复制到新进程对应的页表中,也就是说,这个时候,子进程与父进程共享物理内存。   
  6. 更新task_struct中的文件信息:文件打开次数加1,父进程的当前目录引用数加1。   
  7. 设置TSS和LDT描述符项:在全局描述符表(GDT)中设置新任务的TSS描述符项和LDT段的描述符项,使TSS描述符项和LDT描述符项分别指向task_struct的TSS结构和LDT结构。 
  •  将任务设置为就绪状态,向当前进程(父进程)返回新进程号。​

  • fork()中,内核并不立刻为新进程分配代码和数据物理内存页,新进程与父进程共同使用父进程已有的代码和数据物理内存页面。只有当以后执行过程中由一个进程一写方式访问内存时候被访问的内存页面才会在写操作之前被复制到新申请的内存页面中。
  • 另外在fork的最后是将任务设置成了就绪状态,由于fork()是一个系统调用,在系统调用部分system_call.s,可以看到在系统函数返回后,会调用调度函数schedule(),在schedule()中,就会检测到新进程的就绪状态,并用switch_to()切换到新进程进行执行。

 (七) 可执行程序的装载

  • Linux内核装载和启动一个可执行程序

     1.创建新进程
     2.新进程调用execve()系统调用执行指定的ELF文件
     3.调用内核的入口函数sys_execve(),sys_execve()服务例程修改当前进程的执行上下文;
    (以上系统调用终止后,新进程开始执行放在可执行文件中的代码。)

 

  • 当ELF被load_elf_binary()装载完成后,函数返回至do_execve()在返回至sys_execve()。ELF可执行文件的入口点取决于程序的链接方式:

     1.静态链接:elf_entry就是指向可执行文件里边规定的那个头部,即main函数处。
     2.动态链接:可执行文件是需要依赖其它动态链接库,elf_entry就是指向动态链接器的起点。

 

 (八) 进程的切换和系统的一般执行过程

       Linux系统的一般执行过程,最一般的情况是:正在运行的用户态进程X切换到运行用户态进程Y的过程要经过以下步骤

  • 正在运行的用户态进程X
  • 发生中断:save cs:eip/esp/eflags(current) to kernel stack, then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
  • SAVE_ALL //保存现场,这里是已经进入内核中断处里过程
  • 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
  • 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
  • restore_all //恢复现场
  • iret - pop cs:eip/ss:esp/eflags from kernel stack
  •  继续运行用户态进程Y