imx6ull linux内核
linux内核编译
编译
着重看一下Makefile,了解linux内核的编译流程
和uboot一样,NXP原厂会下载linux社区的最新版本,然后移植到IMX的cpu上,然后我们再下载NXP的内核,移植到我们自己的开发板子上。
编译linux需要安装这个库
sudo apt-get install lzop |
编译指令为
#!/bin/sh |
但是实际编译运行后,报错
CALL scripts/checksyscalls.sh |
会出现重复定义,原因是因为编译工具gcc版本更新了,新版本的编译工具不支持合并变量,导致重复声明。解决办法就是手动添加编译参数
make KCFLAGS="-fcommon" HOSTCFLAGS="-fcommon" |
目录结构



我们真正要关心的文件夹是
- arch/
- 架构相关,比如arm, arm64, x86等架构
arch/ |
以arch/arm架构为例,针对这个架构,有
这些子目录,用于
- 系统引导
- 系统调用
- 动态调频
- 主频设置等
- arch
- arm/
- boot/
- zImage
- Image
- boot/
- arm64/
- x86/
- arm/
arch/arm/boot目录会保存编译出来的Image,zImage镜像文件,zImage就是我们需要的linux镜像文件
此外还有
- block/
block/是linux下块设备目录。比如SD卡,EMMC,NAND,硬盘这些存储设备,都是块设备。- 该目录下存放管理块设备的相关文件
- crypto
- 加密文件,比如
crc,crc32,md4,md5,hash这些加密算法
- 加密文件,比如
- Documentation
- 相关文档,了解linux某个功能模块/驱动框架的功能,可以在里面查找
- drivers/
- 驱动目录文件,学习重点
- firmware/
- 存放固件
- fs/
- 存放文件系统,比如:
- ext2
- ext4
- f2fs
- 存放文件系统,比如:
- include/
- 头文件目录
- init/
- 存放linux内核启动的时候初始化代码
- ipc/
- 进程间通信
- kernel/
- 内核代码
- lib/
- 公用的库函数
- mm/
- 内存管理相关代码
- net/
- 网络相关代码
- samples/
- 示例代码
- scripts/
- 脚本目录,linux编译用到的脚本文件
- security/
- 安全相关文件
- sound/
- 音频相关驱动文件,音频驱动没有放到drivers中,单独一个目录
- tools/
- 编译时的工具
- usr/
- 存放和inittramfs有关代码
- virt/
- 虚拟机相关文件
- .config
- 和uboot一样,.config保存linux最终的配置信息。编译linux会读取这个文件,根据这个配置来选择编译哪些模块功能
- Kbuild
- 一些Makefile会读取这个文件
- Kconfig
- 图形化配置界面的配置文件
- Makefile
- 顶层Makefile文件
- README
- 介绍如何编译linux源码,以及目录的信息
Makefile分析
Makefile和uboot的Makefile几乎差不多
版本号
定义一些变量,表示linux内核版本号:
- 4.1.15
MAKEFLAGS变量
老样子,不停的加入当前目录
MAKEFLAGS += -rR --include-dir=$(CURDIR) |
命令输出
和uboot一样
make V=1 //完整命令 |
静默输出
和uboot一样
make -s //静默编译,不打印任何信息 |
输出目录
make O=dir //指定编译产生的过程文件输出到指定目录 |
代码检查
make C=1 //使能代码检查,检查哪些需要重新编译的文件 |
模块编译
和uboot一样,
make M=dir //单独编译某个模块 |
设置目标架构,交叉编译器
同uboot
ARCH = XXX |
调用scripts/Kbuild.include
同uboot, 使用这个文件来导入一些变量
comma := |
交叉编译工具链变量设置
同uboot
头文件路径变量
顶层Makefile定义了两个变量保存头文件路径:
- USERINCLUDE
- UAPI相关的头文件路径
- LINUXINCLUDE
- linux内核源码的头文件路径
展开后为:
USERINCLUDE := \ |
导出变量
同uboot,顶层的Makefile也会导出很多变量给子Makefile使用。

以上都和uboot类似,准备环境变量,导出变量,准备编译环境
make xxx_defconfig 过程
这里开始配置linux内核,和uboot类似,都是设置定义变量:
- config-targets
- =1
- mixed-targets
- =0
- dot-config
- =1
然后引用 arch/arm/Makefile 这个文件
zImage,uImage这些文件,都是由arch/arm/Makefile生成的。
最终命令展开就是:
@make -f ./scripts/Makefile.build obj=scripts/kconfig xxx_defconfig |
总结,
make xxx_defconfig来进行配置linux,最终依赖的时scripts/Makefile.build来实现的
Makefile.build 分析
前面得知,,“make xxx_defconfig“配置 Linux 的时候如下两行命令会执行脚本scripts/Makefile.build
@make -f ./scripts/Makefile.build obj=scripts/basic |
这一块比较复杂,后面有时间再分析,总之
make xxx_defconfig之后,会在linux kernel 根目录下生成.config文件。
make过程
前面我们用make xxx_defconfig配置好linux内核后,就开始用make 或者make all进行编译了。

所以make all, 就是要依赖编译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/Makefile中
head-y := arch/arm/kernel/head$(MMUEXT).o |
init-y, drivers-y和net-y

可以看到,是把
- init/
- drivers/
- net/
这3个文件夹里面的代码全部编译链接起来了。
libs-y
libs-y = arch/arm/lib/lib.a lib/lib.a arch/arm/lib/built-in.o lib/built-in.o |
这些是使用的库函数,最终使用到的库如上
core-y
和init-y一样,也在Makefile中+=了
最终,core-y的值
相当于把linux的源码,比如kernel里面的,ipc进程通信,加密,安全,块设备这些linux内核相关的代码全部放到core里面。
make 目标是
vmlinux, 依赖的head-y,init-y,core-y,libs-y,drivers-y,net-y, 都是由built-in.o/.a等文件。这个和 uboot 一样,都是将相应目录中的源码文件进行编译,然后在各自目录下生成
built-in.o文件,有些生成了.a库文件。最终将这些
built-in.o和.a文件进行链接(链接脚本:arch/arm/kernel/vmlinux.lds)即可形成 ELF 格式的可执行文件,也就是vmlinux!
vmliux_link就是最终链接出vmlinux的函数

各个子目录的build-in.o, .a编译产生过程
vmlinux <- vmlinux-deps = $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) |
- KBUILD_LDS
- 链接脚本
- (KBUILD_VMLINUX_INIT)
- 子目录下的built-in.o/.a
- (KBUILD_VMLINUX_MAIN)
- 子目录下的built-in.o/.a

//顶层Makefile中 |
最终,vmlinux所需要涉及的源码目录如下
我们以编译init目录的指令为例,分析一下
@ make -f ./scripts/Makefile.build obj=init |
这里又需要用到Makefile.build这个脚本了。
该脚本默认使用__build作为默认目标。
94 __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \ |
- 当只编译Linux内核镜像文件,使用
make zImage
- KBUILD_BUILTIN=1,KBUILD_MODULES空
- 编译所有的东西, 保存linux内核镜像 + 一些模块文件,使用
make
我们重点看一下builtin-target, 他依赖
- obj-y
- obj-m
- obj-
- subdir-m
- lib-target
里面具体会调用if_changed来判断是否发生改变。
最终将某个目录下的所有.o文件链接在一起,最终形成
built-in.o
以上,我们已经完成了defconfig生成.
config,来配置linux内核,
然后分析了make的过程,知道如何从所有相关的文件夹中,编译出各自的
built-in.o,然后把他们链接起来,生成elf的vmlinux
make zImage过程
前面分析了如何制作.config, 然后根据.config来制作vmlinux(elf),但是最终我们要用的是zImage这样的linux内核镜像文件。
vmlinux, zImage, uImage区别
- vmlinux
- 编译出来的最原始的内核文件,elf,未压缩,16MB左右
- Image
- linux内核镜像文件,仅含可执行二进制数据
- 使用
objcopy取消掉vmlinux的其他信息,也没有压缩,12MB左右 - 保存在
arch/arm/boot中
- zImage
- 经过
gzip压缩后的Image, 6MB
- 经过
- uImage
- 老版本uboot专用的镜像文件
- 相当于在zImage前面加上一个64字节的头,描述该镜像的文件类型,加载位置,生成时间,大小等。
- 新版uboot已经支持zImage启动了
vmlinux 转换成 zImage
这个又是依赖 scripts/Makefile.build
@ make -f ./scripts/Makefile.build obj=arch/arm/boot MACHINE=arch/arm/boot/zImage |
具体细节不展示,主要理清楚编译思路。
linux内核启动过程
前一节,已经分析了,linux内核的编译过程,知道如何生成配置linux内核的.config文件,如何编译出vmlinux,如何生成我们需要的zImage
现在开始分析linux内核这个代码的启动流程,比uboot复杂得多。
老规矩,一段代码要运行,和裸机程序一样,肯定要有链接脚本,start.S启动脚本,用户代码这些。
链接脚本 vmlinux.lds
首先要分析的就是linux内核代码的链接脚本,这样我们才知道,第一行代码在哪里执行。
linux内核的链接脚本是 arch/arm/kernel/vmlinux.lds, 通过ENTRY就知道第一句是哪里
ENTRY(stext) |
所有,入口时stext。定义在arch/arm/kernel/head.S
启动流程分析
入口 stext
这个head.S的开头注释要求
- 关闭MMU
- 关闭D-cache
- 无所谓I-Cache
- R0=0
- R1=machine nr(机器ID)
- R2=atags/dtb首地址
所以,stext,就相当于内核的入口函数
ENTRY(stext) |
整理以下内容:
- stext
- 调用
safe_svcmode_maskall- 定义在
arch/arm/include/asm/assembler.h - 确保 CPU 处于 SVC 模式
- 关闭所有中断
- 定义在
- 读取处理器ID,保存在R9
__lookup_processor_type检查当前系统是否支持此CPU- 支持:获取
procinfo信息 (
proc_info_list类型, 定义在arch/arm/include/asm/procinfo.h)
linux内核,将每种处理器都抽象成一个proc_info_list结构体,每种处理器对应一个procinfo。因此可以通过处理器 ID 来找到对应的 procinfo 结构
__lookup_processor_type返回找到的对应处理器的procinfo,保存到R5中
- 支持:获取
__vet_atags- 验证atags/dtb的合法性
- 定义在
arch/arm/kernel/head-common.S
__create_page_tables, 创建页表R13=__mmap_switched- 定义在
arch/arm/kernel/head-common.S __mmap_switched最终调用start_kernel
- 定义在
__enable_mmu使能MMU- 定义在
arch/arm/kernel/head.S __turn_mmu_on,打开MMU- 调用
R13中的__mmap_switched
- 调用
- 定义在
- 调用
__mmap_switched 函数
arch/arm/kernel/head-common.S
__mmap_switched: |
可以看到,就是写拷贝数据段,清零BSS,保存处理器ID,机器类型,dtb的指针这些,以及一些控制寄存器的值,
随后跳转
start_kernel,再也不回来有点类似
start.S里面的reset_handler
start_kernel 函数
最关键的时start_kernel,定义在 init/main.c
代码很长,我这里只做简要的整理
asmlinkage __visible void __init start_kernel(void) |
每一个函数都是一个庞大的知识点,如果想要学习Linux 内核,那么这些函数就需要去详细的研究。
本教程注重于嵌入式 Linux 入门,因此不会去讲太多关于 Linux 内核的知识。
start_kernel函数最后调用了rest_init,接下来简单看一下 rest_init函数
rest_init 函数
init/main.c中定义
static noinline void __init_refok rest_init(void) |
- rest_init
- rcu_scheduler_starting, 启动RCU 锁调度器
- 调用
kernel_thread创建kernel_init内核进程,- 这就是大名鼎鼎的
init内核进程,init进程的 PID 为1 init 进程一开始是内核进程(也就是运行在内核态, SVC模式)- 后面
init 进程会在根文件系统中查找名为“init”这个程序,这个“init”程序处于用户态,通过运行这个“init”程序,init 进程就会实现从内核态到用户态的转变
- 这就是大名鼎鼎的
- 调用
kernel_thread创建kthreadd内核进程- 此内核进程的 PID 为 2
kthreadd进程负责所有内核进程的调度和管理
- 调用
cpu_startup_entry来进入 idle 进程- 调用
cpu_idle_loop- while循环,也就是
idle进程代码, 也就是空闲进程 idle进程的PID为0- 就是freertos里面的空闲进程概念,idle进程并没有由kernel_thread或者fork函数创建,而是从主进程演变而来
- while循环,也就是
- 调用
指令
ps -A可以打印出当前系统中的所有进程, 其中就能看到init进程和kthreadd进程
init进程
前面分析的函数,创建内核进程kernel_init,然后他会调用rootfs的init代码,实现从内核态到用户态的转变。
下面先来看看内核进程kernel_init的具体内容。定义在init/main.c中
static int __ref kernel_init(void *unused) |
kernel_initkernel_init_freeable(),完成init进程的一些其他初始化工作,后面具体分析ramdisk_execute_command- 是全局的char * 指针变量,值是
/init, 就是放在根目录下的init程序 - 该char指针,可以通过uboot传递参数:bootargs中用
rdinit=xxx来指定具体的init程序 - 如果存在
/initrun_init_process执行
- 如果不存在,查看
execute_command是否为空, 总之一定要在rootfs中找到一个可以运行的init程序execute_command可以通过bootargs适应init=xxx指定
- 如果都是空的,就依次执行
/sbin/init,/etc/init,/bin/init,/bin/sh这4个备用的init程序。- 如果还是不存在,那么Linux启动失败。
- 是全局的char * 指针变量,值是
再来看看kernel_init_freeable是做什么初始化工作的
static noinline void __init kernel_init_freeable(void) |
简单总结一下:
kernel_init_freeabledo_basic_setup完成linux下的设备驱动初始化工作,非常重要driver_init完成linux下驱动模型子系统的初始化
sys_open(“/dev/console”), 打开根文件系统的console设备- linux中一切皆文件,此文件为控制台设备
- 每个文件都有一个文件描述符,此处的/dev/console的描述符就是
0, 标准输入0
sys_dup, 将标准输入0的文件描述符复制了2次,一个作为标准输出1,一个作为标准错误2- 这样,stdi,stdo,stderr,都是/dev/console
还记得
uboot里面,通过bootargs指定了console=ttymxc0,115200, 将串口设备设置成console,也就是uart1当然你也可以把别的设备设置成交互的console, 比如虚拟控制台tty1, z这样就可以在LCD屏幕上看到系统的提示信息了
prepare_namespace(), 挂载根文件系统。- 根文件系统也是bootargs指定,
root=/dev/mmcblk1p2 rootwait rw, 即emmc的分区2中
- 根文件系统也是bootargs指定,
linux内核的启动分析就到这里了
linux内核最终是需要和rootfs打交道的,也就需要挂载根文件系统,并且执行rootfs总的/init程序。以此来进入用户态。这里就引出了我们的根文件系统。
所以,总结:
linux系统移植3部分:
- uboot
- linux kernel + dtb
- rootfs(根文件系统), 没有这个,就没有init进程,无法进入用户态。
linux内核移植
将NXP芯片原厂的linux内核移植到我们正点原子的开发板上,就是主要掌握如何将芯片原厂的linux BSP包移植到我们自己的平台上
先编译原厂的linux内核
make distclean |
烧录测试,因为我们以及默认指定了rootfs在emmc分区2,所以直接下载启动就行
添加我们自己的开发板
arch/arm/configs/下复制defconfig配置文件cp imx_v7_mfg_defconfig imx_alientek_emmc_defconfig- 屏蔽CONFIG_ARCH_MULTI_V6=y, 因为这个可能影响我们自己的驱动,我们是armv7架构
arch/arm/boot/dts, 复制dtscp imx6ull-14x14-evk.dts imx6ull-alientek-emmc.dtsarch/arm/boot/dts/Makefile加入编译选项
修改主频
这里主要就是尝试使用虚拟文件系统
cat /proc/cpuinfo //BogoMIPS越大,主频越高 |


修改EMMC驱动
默认的linux内核驱动+dts,使用emmc是4线,我们的支持8线,所以改成8线
修改以太网驱动
切换驱动,配置复位引脚,复位fec1,fec2, 先不要纠结,能用就行



