linux内核结构和启动过程
(以下内容来自教学课件)
一、Linux内核结构
arch
与体系结构相关的代码。对应于每个支持的体系结构,有一个相应的子目录如x86、arm等与之对应,相应目录下有对应的芯片与之对应
drivers
设备驱动代码,占整个内核代码量的一半以上,里面的每个子目录对应一类驱动程序,如:char:字符设备、block:块设备、net:网络设备等
fs
文件系统代码,每个支持的文件系统有相应的子目录,如cramfs,yaffs,jffs2等
include
这里包括编译内核所需的大部分头文件
与平台无关的头文件include/linux
各类驱动或功能部件的头文件(/media、/mtd、/net等)
与体系相关的头文件arch/arm/include/
与平台相关的头文件arch/arm/mach-s5pv210/include/mach
lib
与体系结构无关的内核库代码
与体系结构相关的内核库代码在arch/arm/lib下
init
内核初始化代码,其中的main.c中的start_kernel函数是系统引导起来后运行的第1个函数,这是研究内核开始工作的起点
ipc
内核进程间通信代码
mm
与体系结构无关的内存管理代码
与处理器体系结构相关的代码在arch/arm/mm下
kernel
内核管理的核心代码
net
网络部分代码,其每个目录对应于网络的一个方面
scripts
存放一些脚本文件,如配置内核时用到的make menuconfig命令
Documentation
内核相关文档
crypto
常用加密及散列算法,和一些压缩及CRC校验算法
二、Linux编程风格
缩进
采用制表符(Tab)进行缩进,而不是空格
禁止制表符和空格混合使用
命名规范
Linux规定名称中不允许使用混合大小写字符
单词之间用下划线分隔
避免取有疑惑的简单名称,如pad(),应该写成platform_add_devices()
代码长度
每行尽量不超过80个字符(可进行有意义分行切割)
函数体代码长度尽量不超过两屏
函数局部变量尽量不超过十个
根据函数使用频率和函数体大小可以使用inline声明,以提高调用效率
注释
一般情况注释用于描述代码可以做什么和为什么要做,尽量不写实现方式
函数的修改和维护日志统一集中在文件开头
其他
指针中的"*"号应靠近变量名,而不是类型关键字
函数之间用空行隔开
函数导出申明紧跟在函数定义的下面
等等......
代码风格的事后修正
indent命令是大多数Linux系统中都带有的工具,当得到一段与内核编码风格大相径庭的代码时,可以通过这个工具进行调整:
#indent -kr -i8 -ts8 -sob -l80 -ss -bs -psl <filename>
三、Linux内核启动引导过程
在了解Linux启动流程之前,首先需要了解Linux镜像的格式及其产生过程
Image:直接生成并未压缩的内核,一般用于PC机
zImage:Image的压缩版,采用gzip进行压缩,比Image体积小,但启动时需要进行自解压,嵌入式系
统中一般采用此种方法
uImage:是u-boot专用的一种内核镜像格式,它是在zImage的基础上又添加了一个长度为0x40的标签头,在u-boot启动时会去掉此头信息,仍按zImage启动,头信息主要用于区分不同格式的内核镜像
xipImage:片上执行的未压缩内核,(如norflash等)
(ps:实际应用中由于norflash比较贵,所以大多使用NAND flash作为存储器件,而NAND flash不支持片上执行,故xipImage用的比较少)
bootpImage:将内核与根文件系统制作在一起的镜像
zImage产生过程:vmlinux(内核代码生成的镜像)->Image(objcopy对vmlinux二进制化处理)->compressed/vmlinux(压缩的Image加上head.S和misc.c)->zImage(vmlinux二进制化)
Linux内核的启动过程大体上可以分为3个阶段:
1、内核解压(汇编+C)
主要由arch/arm/boot/compressed/对zImage完成解压,并调用call_kernel跳转到下阶段代码
2、板级引导阶段(汇编)
主要进行cpu和体系结构的检查、cpu本身的初始化以及页表的建立等
3、通用内核启动阶段(C语言)
进入init/main.c文件,从start_kernel开始进行内核初始化工作,最后调用rest_init
rest_init()会创建内核第一个线程,并进入线程函数kernel_init()
在kernel_init()中会初始化各种驱动并建立起标准输入/标准输出/错误输出,最后调用init_post()
在init_post()中会释放初始化内存段,标志着内核启动完成,并努力寻找一个用户进程,通过kernel_execve()函数加载,将该进程作为系统的第一个用户进程init,进程号为1,至此内核启动完成
四、内核的配置与编译
make menuconfig完成Linux内核配置裁剪
根据配置裁剪的结果配合Makefile完成内核编译
linux2.6以后的版本是通过每层目录的Kconfig和Makefile实现了整个Linux内核的分布式配置
Kconfig:对应内核模块的配置菜单
Makefile:对应内核模块的编译选项
当执行make menuconfig时,配置工具会自动根据根目录下的ARCH变量读取arch/$(ARCH)/Kconfig文件来生成配置界面(引用其它目录的kconfig,通过改变宏定义的方式进行条件编译),这个文件是所有文件的总入口,其它目录的Kconfig都由它来引用
配置界面的内容来自于所有目录的Kconfig, 选择"*", " ", 或"M"进行条件编译,对应宏定义在顶层.config文件中
在读取配置界面的同时,系统会读取顶层目录下的.config文件生成所有配置选项的默认值
3、当修改完配置并保存后,系统会更新顶层目录下的.config文件
4、当执行make时,各层的Makefile会根据.config文件中的编译选项来决定哪些文件会被编译到内核中,或编译成模块
Kconfig语法格式可以参考具体文件,如:drivers/char/Kconfig
menu "Character devices"
config VT
bool "virtual terminal" if EMBEDDED
depends on !s390
select INPUT
default y
--help--
if you say Y here,you will get support.....
config代表一个选项的开始,最终会出现在.config中(会自动增加一个CONFIG_前缀)
bool代表此选项仅能选中或不选中,bool后面的字符串代表此选项在make menuconfig中的名字
tristate:代表可以选择编译、不编译、编译成模块
string:字符串; hex:16进制的数; int:10进制的数
depends on:依赖其余的选项
default:默认选项值
select:表示当前config被选中时,此选项也被选中
menu/endmenu:表示生成一个菜单
choice/endchoice:表示选择性的菜单条目
comment:注释信息,菜单和.config文件中都会出现
source:用于包含其它Kconfig
将自己开发的内核代码加入到Linux内核中,需要有3个步骤:
1. 确定把自己开发代码放入到内核合适的位置
2. 把自己开发的功能增加到Linux内核的配置选项中,使用户能够选择此功能
3. 构建或修改Makefile,根据用户的选择,将相应的代码编译到最终生成的Linux内核中去
比如要把一个key1*5键盘驱动添加进内核
步骤如下:
Step1:将s5pv210-key15-simple.c拷到drivers/char/目录下
Step2: vi driver/char/Kconfig,在Kconfig文件结尾,在endmenu的前面加入一个config选项
config UNSP210_KEY15
bool "key1*5 driver for sunplusedu unsp210 boards"
default y
help
this is GPIO driver for unsp210 boards.
Step3:make menuconfig(添加配置选项)
Device driver->
character devices->
[*] key1*5 driver for sunplusedu unsp210 boards
Step4: vi driver/char/Makefile添加内容如下:
obj-$(CONFIG_UNSP210_KEY15) += s5pv210-key15-simple.o
Step5:make (更新内核镜像到开发板)
Step6:交叉编译测试程序,并放到开发板运行
#arm-linux-gcc key15_test.c -o key15_test