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