韩玉琪 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

一、Linux内核源码简介

1. 源码主要结构

2. 我们关注的部分

  • arch/

      - 该目录中包含和硬件体系结构相关的代码,每种平台占一个相应的目录。
      - 和32位PC相关的代码存放在x86目录下。
      - 每种平台至少包含3个子目录:kernel(存放支持体系结构特有的特征实现)、lib(存放体系结构特有的对通用函数的实现)、mm(存放体系结构特有的内存管理程序的实现),除了这3个子目录之外,大多数体系结构在必要的情况下还有一个boot子目录,包含了在这种硬件平台上启动内核所使用的部分或全部平台特有代码。
    
  • init/

      - 内核启动相关代码 -> main.c
      - Linux内核启动初始化的起点就位于main.c中的函数start_kernel,相当于普通程序的main函数。
    
  • kernel/

      - 存放linux内核最核心的代码,用于实现系统的核心模块,包括进程管理、进程调度器、中断处理、系统时钟管理、同步机制等。
      - 该目录中的代码实现这些核心模块的主体框架,独立于具体的平台和系统架构。
      - 核心模块与平台相关的代码放在arch/中。
    

3. 其它

  • Documentation

    • 这个目录下面没有内核代码,只有很多质量参差不齐的文档,但往往能够给我们提供很多的帮助。
  • drivers

    • 显卡、网卡、SCSI适配器、PCI总线、USB总线和其他任何Linux支持的外围设备或总线的驱动程序都可以在这里找到。
  • fs

    • 虚拟文件系统(VFS,Virtual File System)的代码,和各个不同文件系统的代码都在这个目录中。
    • Linux支持的所有文件系统在fs目录下面都有一个对应的子目录。
  • include

    • 这个目录包含了内核中大部分的头文件。
  • ipc

    • IPC,即进程间通信(interprocess communication)。它包含了共享内存、信号量以及其他形式IPC的代码。
  • lib

    • 库代码,实现了一个标准C库的通用子集,包括字符串和内存操作的函数(strlen、mmcpy和其他类似的函数)以及有关sprintf和atoi的系列函数。
    • 与arch/lib下的代码不同,这里的库代码都是使用C编写的,在内核新的移植版本中可以直接使用。
  • mm

    • 包含了体系结构无关部分的内存管理代码,体系相关的部分位于arch/*/mm目录下。
  • net

    • 网络相关代码,实现了各种常见的网络协议,如TCP/IP、IPX等。
  • scripts

    • 该目录下没有内核代码,只包含了用来配置内核的脚本文件。
    • 当运行make menuconfig或者make xconfig之类的命令配置内核时,用户就是和位于这个目录下的脚本进行交互的。
  • block

    • block层的实现。
  • crypto

    • 内核本身所用的加密API,实现了常用的加密和散列算法,还有一些压缩和CRC校验算法。
  • security

    • 这个目录包括了不同的Linux安全模型的代码,比如NSA Security-Enhanced Linux。
  • sound

    • 声卡驱动以及其他声音相关的代码。
  • usr

    • 实现了用于打包和压缩的的cpio等。

4. README

  • 提供内核的各种编译方法、生成文件的查看方法。例如:

    • INSTALLING 如何安装内核源代码
    • make mrproper 清理安装时生成的中间代码

二、构造一个简单的Linux系统

1. 运行MenuOS系统

  • 在实验楼的虚拟机环境里,打击打开shell,使用下面的命令

      cd LinuxKernel/  
      qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
    
      # -initrd:指明一个根文件系统
    

  • 启动实验的Linux系统MenuOS,实际上就是一个在Linux内核的基础上,再运行一个简单菜单命令行程序。

  • 在MenuOS>的提示符下输入help,看到其全部支持的命令:help、version、quit

2. 使用gdb跟踪调试内核

  • 使用带参数命令启动MenuOS,使得系统在刚启动时,暂停等待调试器跟踪执行。

      qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
      # -S 在CPU初始化之前,冻结CPU
      # -s 1234端口上创建一个tcp接口。若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项  
    
  • 另开一个shell窗口,启动gdb。

      (gdb)file linux-3.18.6/vmlinux
      # 在gdb界面中targe remote之前加载符号表
      (gdb)target remote:1234 
      # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
      (gdb)break start_kernel 
      # 在start_kernel函数入口处设置断点
      (gdb)c 
      # 使得系统运行到start_kernel处停住
      (gdb)list
      # 显示当前行所在位置上下的代码
    

附:常用调试参数

常用调试参数用途
r(run)开始运行程序
c(continue)继续运行一直到断点停止
b(break)设置程序断点
l(list)显示当前行后面的源程序
bt (backtrace)查看堆栈信息
info查看各类gdb信息以及环境信息,比如:info break可以查看断点信息
p(print)打印出变量值;如 p var,会把var变量的值输出
s(step)单步跟踪,会进入函数内部
n(next)单步跟踪,不进入函数
finish跳出函数调试,并打印返回时的信息
clear清除全部已定义的断点
q(quit)退出gdb

三、简单分析start_kernel

1. init/main.c -> stat_kernel

asmlinkage void __init start_kernel(void)
  • 该函数是Linux内核的入口。
  • start_kernel()是内核的汇编与C语言的交接点,在该函数以前,内核的代码都是用汇编写的,完成一些最基本的初始化与环境设置工作。
  • 在start_kernel()中Linux将完成整个系统的内核初始化。内核初始化的最后一步就是启动init进程,这个所有进程的祖先。
  • 不管分析内核那一部分都会涉及到start_kernel,因为几乎所有模块在初始化时都会调用它。

2. 一些init函数

(1)全局变量 init_task

  • 全局变量init_task,即手工创建的PCB,0号进程初始化,0号进程就是最终的idle。

(2)初始化一些中断向量 trap_init()

  • 中断向量表的初始化函数,设置了很多中断门(Interrupt Gate)

  • set_intr_gate:设置中断门

(3)内存管理模块初始化 mm_init()

(4)调度模块初始化 sched_init()

  • 函数内做了很关键的一步初始化——对0号进程,即idle进程进行初始化。

(5)其它模块初始化 rest_init()

  • kernel_thread(kernel_init,NULL,CLONE_FS)中的kernel_init包含一个run_init_process,创建了一号进程,即第一个用户态进程。

  • 之后创建了kthreadd,一个内核线程来管理系统的资源。

  • 注:内核中并没有线程的概念,这里创建的init是一个任务(进程)。如果把系统当成是一个大进程,那么init进程就相当于大进程中的一个线程,因为内核中的进程调度会为每一个像init进程分配时间片来执行。

  • 各部分启动完毕后,调用static void cpu_idle_loop(void),当系统没有进程需要执行时就调度到idle进程中的cpu_idle_loop,其中while(1)无限循环,即rest_init的中0号进程会一直存在。

四、总结

  1. 内核启动过程:rest_init()就是一个0号进程,在start_kernel内核一启动时就一直存在;然后这个0号进程就创建了1号进程kernel_init,接下来还创建了其他的一些服务类的内核线程如kthreadd。这样整个系统就启动起来了。
  2. 使用qemu在自己的电脑上运行待续....​

参考资料

课程提供的Linux内核启动过程相关的参考资料

  • 计算机的启动过程概述

    • x86 CPU启动的第一个动作CS:EIP=FFFF:0000H(换算为物理地址为000FFFF0H,因为16位CPU有20根地址线),即BIOS程序的位置。
    • BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序。这里一般是把硬盘的第一个扇区MBR和活动分区的引导程序加载到内存(即加载BootLoader),加载完整后把控制权交给BootLoader。
    • 引导程序BootLoader开始负责操作系统初始化,然后起动操作系统。启动操作系统时一般会指定kernel、initrd和root所在的分区和目录,比如root (hd0,0),kernel (hd0,0)/bzImage root=/dev/ram init=/bin/ash,initrd (hd0,0)/myinitrd4M.img
    • 内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。
    • 一般分两阶段启动,先是利用initrd的内存文件系统,然后切换到硬盘文件系统继续启动。
    • initrd文件的功能主要有两个:
      • 一是提供开机必需的但kernel文件(即vmlinuz)没有提供的驱动模块(modules)
      • 二是负责加载硬盘上的根文件系统并执行其中的/sbin/init程序进而将开机过程持续下去
  • 道生一(start_kernel....cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先),新内核的核心代码已经优化的相当干净,都符合中国传统文化精神了。

其它

参考资料1:linux 源代码目录结构
参考资料2:内核源码目录结构
参考资料3:Linux内核中的init_task进程和idle进程
参考资料4:init进程详解