0%

Linux驱动学习日记(10) Linux Kernal学习

文件结构

  编译后的文件夹结构如下表所示:

图 1 文件结构

arch 目录

  这个目录是和架构有关的目录,比如 arm、arm64、avr32、x86 等等架构。每种架构都对应 一个目录,在这些目录中又有很多子目录,比如 boot、common、configs 等等。

  arch/arm/configs 中就包含有 I.MX6U-ALPHA 开发板的默认配置文件:imx_v7_defconfi g,执行“make imx_v7_defconfig”即可完成配置。arch/arm/boot/dts 目录里面是对应开发平台的 设备树文件。

  arch/arm/boot 目录下会保存编译出来的 Image 和 zImage 镜像文件,而 zImage 用的 linux 镜像文件。

  arch/arm/mach-xxx 目录分别为相应平台的驱动和初始化文件,比如 mach-imx 目录里面就是 I.MX 系列 CPU 的驱动和初始化文件。

block 目录

  block 是 Linux 下块设备目录,像 SD 卡、EMMC、NAND、硬盘等存储设备就属于块设备, block 目录中存放着管理块设备的相关文件

crypto 目录

  crypto 目录里面存放着加密文件,比如常见的 crc、crc32、md4、md5、hash等加密算法。

drivers 目录

  驱动目录文件,此目录根据驱动类型的不同,分门别类进行整理,比如 drivers/i2c 就是 I2 C 相关驱动目录,drivers/gpio 就是 GPIO 相关的驱动目录。

fs 目录

  此目录存放文件系统,比如 fs/ext2、fs/ext4、fs/f2fs 等,分别是 ext2、ext4 和 f2fs 等文件系统。

.config 文件

  .config 保存着 Linux 最终的配置信息,编译 Linux 的时候会读取此文件中 的配置信息。最终根据配置信息来选择编译 Linux 哪些模块,哪些功能。

顶层 Makefile

  Linux 的顶层 Makefile 和 uboot 的顶层 Makefile 非常相似,因为 uboot 参考了 Linux,前 602 行几乎一样,所以前面部分我们大致看一下就行了。

  起手版本号,然后是MAKEFLAGS变量。接着是命令输出控制(V=1/0),也可以设置 -s 参数实现编译静默。可以使用 O=xxx 将编译产生的过程文件输出到指定目录中。在252、253行之后可以自行设置目标架构编译器。接下来就是调用文件 scripts/Kbuild.include,然后设置交叉编译工具变量、头文件路径变量,然后导出变量给子文件夹的Makefile使用。

make xxx_defconfig 过程

  make xxx_defconfig匹配的是下面这条模式规则:

%config:
$(MAKE) $(build)=scripts/kconfig $@

  在 解析顶层 Makefile 阶段,make 根据 MAKECMDGOALS=xxx_defconfig计算一组控制变量(是否 include .config、是否需要编译器、是否 mixed build),然后才进入目标匹配与执行阶段。结构框图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
make xxx_defconfig


顶层 Makefile
├─ 判断:这是 %config
├─ 禁止 include .config
└─ 命中规则 %config


make -f scripts/Makefile.build obj=scripts/kconfig xxx_defconfig


scripts/kconfig
├─ 读取 arch/arm/configs/xxx_defconfig
└─ 生成 .config

Makefile.build 脚本分析

  Makefile.build 是 Kbuild 的通用构建脚本,在 make xxx_defconfig 阶段首先被用于构建 scripts/basic(fixdep、bin2c 等基础工具),框图如下:

1
2
3
4
5
6
7
8
9
10
make xxx_defconfig

顶层 Makefile 判断:这是 config 目标

先调用 Makefile.build 构建 scripts/basic

scripts/basic/Makefile 告诉系统:
必须生成 fixdep 和 bin2c

这两个工具被编译出来

make 过程

  这个过程把所有 built-in.o / .a 链接成一个 vmlinux。vmlinux 的依赖为:scripts/link-vmlinux.sh、$(head-y) 、$(init-y)、$(core-y) 、$(libs-y) 、$(drivers-y) 、$(net-y)、arch/arm/kernel/vmlinux.lds 和 FORCE。经过分析可知,这些变量最终的对应值如下:

head-y = arch/arm/kernel/head.o
init-y = init/built-in.o 
drivers-y = drivers/built-in.o  sound/built-in.o  firmware/built-in.o 
net-y = net/built-in.o
libs-y = arch/arm/lib/lib.a  lib/lib.a  arch/arm/lib/built-in.o  lib/built-in.o
core-y = usr/built-in.o arch/arm/vfp/built-in.o \ 
    arch/arm/vdso/built-in.o arch/arm/kernel/built-in.o \ 
    arch/arm/mm/built-in.o arch/arm/common/built-in.o \ 
    arch/arm/probes/built-in.o arch/arm/net/built-in.o \ 
    arch/arm/crypto/built-in.o arch/arm/firmware/built-in.o \ 
    arch/arm/mach-imx/built-in.o kernel/built-in.o\ 
    mm/built-in.o fs/built-in.o \ 
    ipc/built-in.o  security/built-in.o \ 
    crypto/built-in.o block/built-in.o

make zImage 过程

  vmlinux 是 ELF 格式的文件,但是在实 际中我们不会使用 vmlinux,而是使用 zImage 或 uImage 这样的 Linux 内核镜像文件。那么 vmlinux、zImage、uImage 他们之间有什么区别呢?

①、vmlinux 是编译出来的最原始的内核文件,是未压缩的,比如正点原子提供的 Linux 源 码编译出来的 vmlinux 差不多有 16MB。

②、Image 是 Linux 内核镜像文件,但是 Image 仅包含可执行的二进制数据。Image 就是使 用 objcopy 取消掉 vmlinux 中的一些其他信息,比如符号表什么的。但是 Image 是没有压缩过 的,Image 保存在 arch/arm/boot 目录下,其大小大概在 12MB

③、zImage 是经过 gzip 压缩后的 Image,经过压缩以后其大小大概在 6MB 左右

④、uImage 是老版本 uboot 专用的镜像文件,uImag 是在 zImage 前面加了一个长度为 64 字节的“头”,这个头信息描述了该镜像文件的类型、加载位置、生成时间、大小等信息。但是 新的 uboot 已经支持了 zImage 启动!所以已经很少用到 uImage 了,除非你用的很古老的 uboot。

启动流程

连接脚本 vmlinux.lds

  要分析 Linux 启动流程,先打开 vmlinux.lds 连接脚本,找到程序入口为 stext,stext 定义在文件 arch/arm/ke rnel/head.S 中,因此要分析 Linux 内核的启动流程,就得先从文件 arch/arm/kernel/head.S 的 stext 处开始分析。

1
2
3
OUTPUT_ARCH(arm) 
ENTRY(stext)
jiffies = jiffies_64

程序分析

Linux 内核入口 stext

  根据注释可以得知,Linux内核启动前要求如下:
①、关闭 MMU。
②、关闭 D-cache。
③、r0 = 0。
④、r1 = machine nr(机器ID)。
⑤、r2 = atags或者dft首地址。

  Linux 内核将每种处理器都抽象为一个 proc_info_list 结构体,每种处理器都对应一个 procinfo。因此可以通过处理器 ID 来找到对应的 procinfo 结构,__lookup_processor_type 函数找到对 应处理器的 procinfo 以后会将其保存到 r5 寄存器中。

  先检查 CPU 是否在 SVC 模式,关闭中断,读取设备ID,检查当前系统是否支持此 CPU 。然后调用函数 __create_page_tables 创建页表,将函数__mmap_switched 的地址保存到 r13 寄存器中。调用__enable_mmu 函数使能 MMU,__enable_mmu 定义在文件 arch/arm/kernel/head.S 中。__enable_mmu 最终会通过调用__turn_mmu_on 来打开 MMU,__turn_mmu_on 最 后会执行 r13 里面保存的__mmap_switched 函数。

__mmap_switched 函数

  这个函数干了以下几件事情:复制.data段、清零.bss段,保存启动参数并切换内核栈,跳转到start_kernal。时间轴如下

1
2
3
4
5
6
7
8
9
reset

head.S

__enable_mmu

__mmap_switched ← 你现在看的函数

start_kernel() ← C 语言世界

rest_init 函数

  start_kernel() 通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作。在这个函数的最后调用了 rest_init,此函数做了以下几件事
①、创建 PID=1 的 init 进程(kernel_init)
②、创建 PID=2 的 kthread 内核线程管理者
③、把当前执行流变成 idle 进程(PID=0),并正式进入调度器

1
2
3
4
5
6
7
8
9
start_kernel()
|
|-- 各种子系统初始化(内存、调度、IRQ、VFS…)
|
|-- rest_init() ← 你现在看的函数
|
|-- kernel_init() (PID 1, init)
|-- kthreadd() (PID 2)
|-- idle loop (PID 0)

init 进程

  init 进程把 Linux 从“内核态的世界”,带进“可用的用户态系统”。主要职责:
①、完成所有“只需要做一次”的内核初始化
②、释放 init 阶段占用的内存
③、准备好用户态生存环境
④、exec 用户态 init(/sbin/init 等)

kernel_init() 的核心目标是:在完成内核阶段的最后收尾工作后,尝试 exec 一个用户态 init 程序(如 /sbin/init),一旦 exec 成功,PID 1 即转为用户态 init 进程。

1
2
3
4
5
6
7
8
9
kernel_thread(kernel_init)
|
| (收尾释放 initmem准备 console驱动 ready)
v
exec("/sbin/init")
|
| 这里是不可逆点
v
用户态 init (PID 1)