话说linux内核连载1

编程入门 行业动态 更新时间:2024-10-23 01:46:15

话说linux<a href=https://www.elefans.com/category/jswz/34/1769575.html style=内核连载1"/>

话说linux内核连载1

话说linux内核连载
目录
1.内核和发行版的区别
2.内核和驱动的关联
3.内核和应用程序、根文件系统的关联
4.linux内核的模块化设计
5.选择合适版本的内核
6.声明:
 
正文
回到顶部
1.内核和发行版的区别
到底什么是操作系统
linux、windows、android、ucos就是操作系统
操作系统本质上是一个程序,由很多个源文件构成,需要编译连接成操作系统程序(vmlinz、zImage)
操作系统的主要作用就是管理计算机硬件,给应用程序提供运行环境。
 
操作系统核心功能:
内存管理。如果没有操作系统,内存是需要程序自己来管理的。譬如在uboot中要使用内存的哪里是自己随便用的,没有注册也没有限制。这时候如果程序自己不小心把同一块内存重复用了就会出现程序逻辑错误。系统大了之后(内存多了)内存管理非常麻烦;有了操作系统之后,操作系统负责管控所有的内存,所有的应用程序需要使用内存时都要向操作系统去申请和注册,由操作系统的内存管理模块来分配内存给你使用,这样好处是可以保证内存使用不会冲突。
进程调度。操作系统下支持多个应用程序同时运行(所以可以一边聊QQ一边看电影···),这是宏观上的并行。实际上在单核心CPU上微观上是不能并行的,宏观上的并行就是操作系统提供的分时复用机制。操作系统的进程调度模块负责在各个进程之间进行切换。
硬件设备管理。没有操作系统时要控制任何硬件都要自己写代码,有了操作系统后操作系统本身会去控制各个硬件,应用程序就不用考虑硬件的具体细节了。操作系统的硬件设备管理模块就是驱动模块。
文件系统。文件系统是管理存储设备的一种方式。存储设备是由很多个扇区组成的,每个扇区有512/1024/2048/4096字节,存储设备要以扇区为单位进行读写。如果没有文件系统,程序要自己去读写扇区,就得记得哪个文件在哪个扇区。有了文件系统之后我们人不用再关注扇区,人只用关注文件系统中的目录和文件名,而不用管这个文件在物理磁盘的哪个扇区。
 
操作系统扩展功能:
协议栈
有用的应用程序包。应用程序本身不属于操作系统内核的一部分,应用程序是给人用的,面向某种功能的。譬如ping程序用来测试网络是否联通,ifconfig程序用来配置网卡。
 
内核和发行版的区别
区别:内核是操作系统内核的简称,内核负责实现操作系统的核心功能(资源管理模块,譬如内存管理、调度系统······),内核不包括应用程序。所以说只有内核人是没法用的,因为人做任何事情都是通过相应的应用程序来完成的。所以卖操作系统的人把内核和一些常用的应用程序打包在一起提供给普通用户,这就是操作系统的发行版(也就是普通意义上的操作系统)。
内核只有一个。www.kernel
发行版有很多。譬如ubuntu、redhat、suse、centos······
回到顶部
2.内核和驱动的关联
学习linux的思路
对庞大的整体要有个认识。学习路线就是先建立框架和整体,然后逐渐去学习各个细节部分,逐步细化。
对各分层的作用要清楚。
对层次间的关联和互相调用要理解。
 
驱动属于内核的一部分
驱动就是内核中的硬件设备管理模块
驱动工作在内核态。
驱动程序故障可能导致整个内核崩溃
驱动程序漏洞会使内核不安全
回到顶部
3.内核和应用程序、根文件系统的关联
应用和内核的关系
应用程序不属于内核,而是在内核之上的
应用程序工作在用户态,是受限制的。
应用程序故障不会导致内核崩溃
应用程序通过内核定义的API接口来调用内核工作
总结1:应用程序是最终目标
总结2:内核就是为应用程序提供底层资源管理的服务员
 
内核和根文件系统
根文件系统提供根目录。
进程1存放在根文件系统中
内核启动最后会去装载根文件系统。
总结:根文件系统为操作系统启动提供了很多必备的资源:根目录、进程1
回到顶部
4.linux内核的模块化设计
什么是模块化设计
因为linux内核很庞大,代码量很大、东西很多,如果设计时完全设计成一体(各个文件、各个函数之间紧耦合),复杂度超出了人所能理解的范围。所以模块化设计也是一种必要。
模块化设计就是内核中各个功能模块在代码上是彼此独立的,譬如说调度系统和内存管理系统之间并没有全局变量的互相引用,甚至函数互相调用也很少,就算有也是遵循一个接口规范的。模块化设计的目的就是实现功能模块的松耦合。
 
模块化设计的体现
配置时可裁剪。linux内核在编译之前可以进行配置,配置时可以选择将组成内核的成千上万个模块每一个要或者不要。要了之后还有更多的一些细节的配置。
模块化编译和安装。为了操作方便,逐渐从静态的升级变成了动态的升级(不需要重启系统,更不需要重新烧录系统)。这种动态的升级也是由模块化来支持的。
源码中使用条件编译。这种在uboot中已经见过了。
 
模块化设计的好处
功能可裁剪、灵活性
可扩展性(动态安装卸载、新硬件支持)
利于协作
 
模块化设计是一种普遍性的系统设计原则
 
回到顶部
5.选择合适版本的内核
linux内核版本变迁简史
linux0.01。初版
linux0.11。很多讲linux内核源代码解析的书都是以这个版本为原本来讲。《图解linux内核设计的艺术》
linux2.4。比较接近现代的版本,很多经典的书都是以2.4版本内核为参照的,譬如《LDD3》。linux2.4的晚期内核在前几年还会经常碰到有用的。
linux2.6早期。2.6的早期和2.4晚期内核挺像的。
linux2.6晚期。2.6的晚期内核较早期内核有一些改变,尤其是驱动相关的部分和一些头文件的位置。2.6的晚期内核目前还算是比较主流。
linux3.x 4.x
 
如何选择合适的内核版本
并不是越新版本的内核越好
选择SoC厂家移植版本会减少工作量
 
S5PV210适用的内核版本
2.6.35.7+android2.3/QT4.8.3
3.0.8+android4.0
 
本次程使用2.6.35.7版本内核讲解
2、内核的配置和移植
目录
1.linux内核源码目录结构1
 2.linux内核源码目录结构2
3.内核配置和编译体验
4.内核的配置原理1
5.menuconfig的使用和演示
6.menuconfig的工作原理
7.Kconfig文件详解1
8.Kconfig文件详解2
9.Kconfig和.config文件和Makefile三者的关联
10.menuconfig的实验学习思路
 
正文
回到顶部
1.linux内核源码目录结构1
源码从哪里来
之前讲过,我们使用2.6.35.7版本的内核。这个版本的内核有三种:第一种是kernel上的官方版本,第二种是三星移植过的,第三种是九鼎X210的移植版本。我们讲课时使用第三种内核来讲解,后面的移植实验使用第二种内核来移植。
源码在开发板光盘中有。可以自己去linux下解压然后make distclean清理然后再次打包传输到windows下去解压分析;也可以直接去我网盘中下载我打包好的。
解压后最终在windows下得到了一个kernel的源码目录树,这个源码目录就是九鼎以三星移植过的内核为原材料自己针对X210移植后的内核版本。
 
分析源码目录下的单个文件

1 .mailmap      //开发者的邮箱
2 COPYING       //版权
3 COPYING.txt    //版权    
4 CREDITS       //感谢
5 initrd.img.cpio  //设备树传参
6 Kbuild        //就是内核编译的意思。这个文件就是linux内核特有的内核编译体系需要用到的文件。
7 Makefile      //这个是linux内核的总makefile,整个内核工程用这个Makefile来管理的。
8 mk          //是九鼎在移植时自己添加的,不是linux内核本身的东西。九鼎添加这个文件的作用是用这个文件来整天管理kernel目录的配置和编译,也就是说这个文件有点类似于我们之前移植uboot时自己创建的那个cp.sh。
9 README       //说明

 
简单讲一下linux内核的配置体系。
linux内核很庞大,里面模块很多,而且可配置性非常高。所以linux源代码的配置是一个很复杂的事情,必须要有一套很复杂的机制来保证linux内核可以被正确的配置。(对比一下uboot,uboot的配置项都是在xxx.h中,用宏定义来表示的。uboot的这种方式很依赖于人的水平,因为uboot的配置体系很简单。)
linux内核本身配置项有上千个,光靠人眼睛去看脑袋去记根本不可能,所以内核发明了一种体系用来帮助人进行简单化的配置。这种体系就是我们本次重点要研究的东西。
Kbuild、Kconfig等文件,都是和内核的配置体系有关的。
回到顶部
 2.linux内核源码目录结构2
 linux目录结构

 1 arch      //arch是architecture的缩写,意思是架构。arch目录下是好多个不同架构的CPU的子目录,譬如arm这种cpu的所有文件都在arch/arm目录下,X86的CPU的所有文件都在arch/x86目录下。
 2 block      //在linux中block表示块设备(以块(多个字节组成的整体,类似于扇区)为单位来整体访问),譬如说SD卡、iNand、Nand、硬盘等都是块设备。你几乎可以认为块设备就是存储设备。block目录下放的是一些linux存储体系中关于块设备管理的代码。   
 3 crypto     //英文意思是加密。这个目录下放了一些各种常见的加密算法的C语言代码实现。譬如crc32、md5、sha1等。
 4 Documentation //里面放了一些文档。
 5 drivers    //驱动目录,里面分门别类的列出了linux内核支持的所有硬件设备的驱动源代码。
 6 firmware  //固件。什么是固件?固件其实是软件,不过这个软件是固化到IC里面运行的叫固件。就像S5PV210里的iROM代码。
 7 fs       //fs就是file system,文件系统,里面列出了linux支持的各种文件系统的实现。
 8 include  //头文件目录,公共的(各种CPU架构共用的)头文件都在这里。每种CPU架构特有的一些头文件在arch/arm/include目录及其子目录下
 9 init    //init是初始化的意思,这个目录下的代码就是linux内核启动时初始化内核的代码。
10 ipc    //ipc就是inter process commuication,进程间通信,里面都是linux支持的IPC的代码实现。
11 kernel    //kernel就是内核,就是linux内核,所以这个文件夹下放的就是内核本身需要的一些代码文件。
12 lib    //lib是库的意思,这里面都是一些公用的有用的库函数,注意这里的库函数和C语言的库函数不一样的。在内核编程中是不能用C语言标准库函数,这里的lib目录下的库函数就是用来替代那些标准库函数的。譬如在内核中要把字符串转成数字用atoi,但是内核编程中只能用lib目录下的atoi函数,不能用标准C语言库中的atoi。譬如在内核中要打印信息时不能用printf,而要用printk,这个printk就是我们这个lib目录下的。
13 mm     //mm是memory management,内存管理,linux的内存管理代码都在这里
14 net    //该目录下是网络相关的代码,譬如TCP/IP协议栈等都在这里。
15 samples  //一些实例代码
16 scripts   //脚本,这个目录下全部是脚本文件,这些脚本文件不是linux内核工作时使用的,而是用来辅助对linux内核进行配置编译生产的。我们并不会详细进入分析这个目录下的脚本,而是通过外围来重点学会配置和编译linux内核即可
17 security  //安全相关的代码。不用去管。
18 sound   //音频处理相关的。
19 tools    //linux中用到的一些有用工具
20 usr    //目录下是initramfs相关的,和linux内核的启动有关,暂时不用去管
21 virt   //内核虚拟机相关的,暂时不用管。

总结:这么多目录跟我们关系很紧密的就是arch和drivers目录,然后其他有点相关的还有include、block、mm、net、lib等目录。
回到顶部
3.内核配置和编译体验
先确认Makefile
主要是检查交叉编译工具链有没有设置对。CROSS_COMPILE ?= /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
确认ARCH = arm。主要目的是为了编译时能找到arch/arm目录。 

make x210ii_qt_defconfig
最后只要出现:configuration written to .config这句话,就证明我们的操作是正确的。如果没有出现这句话,就有错误。
可能出现的错误1:名字敲错了。名字是字符串匹配的,一定要正确。
注意:如果这一步配置没有得到.config文件,是不能进行到下一步的。实际测试时没有.config也可以make menuconfig,但是这样做出来的内核编译和烧写运行应该是有问题的。

make menuconfig


可能出现的错误1:ncurses库没装
错误信息:
*** Unable to find the ncurses libraries or the
*** required header files.
*** 'make menuconfig' requires the ncurses libraries.
***
*** Install ncurses (ncurses-devel) and try again.
解决方案: apt-get install libncurses5-dev (参考了:)
可能出现的错误2:屏幕太小
错误信息:
Your display is too small to run Menuconfig!
It must be at least 19 lines by 80 columns.
解决方案:全屏,或者是把字体调小。
 
总结:make menuconfig是第二步配置,具体的用法和配置意义在后面课程讲。我们这里因为是九鼎已经移植过的,所以第二步配置是可以不做的,直接退出即可。
用键盘的向右方向键移动到EXIT,按回车退出。
 
make
可能出现的错误1:莫名其妙的错误,可以试试先make distclean
代码本身的错误:具体问题具体分析
编译完成后得到的内核镜像不在源码树的根目录下,在arch/arm/boot这个目录下。得到的镜像名是zImage
回到顶部
4.内核的配置原理1
烧写测试
配置的关键是得到.config文件
.config以.开头,是一个隐藏文件,因此平时是看不到的,需要ls -a来看
当我们make distclean后(也就是说默认情况下)是没有.config文件的,我们配置的两步过程就是为了得到内容合适的.config文件
.config文件是linux内核在编译过程中很重要的一个文件,其作用类似与uboot中的include/configs/x210_sd.h,内核在编译过程中会读取.config中的配置项,并且用这些配置项去指导整个编译链接过程。
.config文件的格式类似于脚本文件,其中内容为类似于于:CONFIG_ARM=y的一个一个的配置项。这些配置项就类似于脚本文件中定义的一个一个变量,所以这一行可以被理解为定义了一个变量CONFIG_ARM,这个变量的值为y。
.config文件中每一行都是一个配置项,从.config文件的规模可以看出linux内核的可配置项有两三千个。所以linux内核是高度可配置的,而且linux内核的所有配置项很难全部搞明白。因为linux内核的配置项太多太繁杂超出了人的大脑能够记忆和处理的数量级,因此linux内核不像uboot那样直接手工配置,而是发明了一个图形化的配置工具menuconfig。
 
make xx_defconfig 和 make menuconfig相配合
我们为了对.config文件中的两三千个配置项做逐一合适的配置,专门发明了两步结合的配置方式。
其实只要人的记忆足够好,大脑足够厉害,完全可以手工去书写/修改.config文件完成内核配置,最终只要.config中内容是正确的,就不影响编译过程。
第一步:make xxx_defconfig解决的问题是大部分的配置项(这一步结束后99%的配置项就已经正确了),下来就是对个别不同的针对我们的开发板进行细节调整,细节调整就通过make menuconfig来完成。
make xxx_defconfig这一步其实是参考别人已经做好的,这样做有很多好处:减少很多工作量,避开了很多自己不懂的配置项(譬如对内存管理的、调度系统的等模块的配置项),我们只用管自己需要管的。
make menuconfig其实就是读取第一步得到的 .config,然后给我们一个图形化的界面,让我们可以更加容易的找到自己想要修改的配置项,然后更改配置他。
 
make xx_defconfig到底做了什么?
make x210ii_qt_defconfig  其实相当于:cp arch/arm/configs/x210ii_qt_defconfig .config
arch/arm/configs目录下的这么多个xxx_defconfig哪里来的?其实这些文件都是别人手工配置好适合一定的开发板的.config文件后自己把.config文件保存过去的。譬如说我们用S5PV210这个SoC,针对这个SoC的开发板的最初配置肯定是三星的工程师去做的。
 
回到顶部
5.menuconfig的使用和演示
使用说明解释
make ,menuconfig中本身自带的提示就有所有的用法,这里只要全部理解就可以了。
menuconfig中间的选择区中有很多个选择项,menuconfig中每个选择项对应.config文件中的一个配置项,每一个选择项都可以被选择和配置操作,选择区中的每一项都是有子目录的,将光标放在选择项上按Enter键可以进入子目录(子目录可能还会有子目录)。选择区太短放不下所有的一个目录层级的选项,可以用箭头按键的向上箭头和向下箭头来上翻和下翻。
 
注:在menuconfig中操作相关的几个键盘按键,主要是;Enter、ESC、四个方向箭头按键。还有一些特殊字符按键,如/ ?
向上和向下箭头,主要用来在选择项菜单中目录浏览时上下翻
回车,主要作用是选中并且执行select/exit/help。
ESC,主要作用是返回上一层
向左和向右箭头,主要作用是在菜单选项(select、exit、help)间切换。
 
用法翻译:
箭头按键导航整个菜单,回车按键选择子菜单(注意选项后面有 --->的选项才是有子菜单的,没有这个标识的没有子菜单),高亮的字母是热键(快捷键),键盘按键Y、N、M三个按键的作用分别是将选中模块编入、去除、模块化。双击ESC表示退出,按下?按键可以显示帮助信息,按下/按键可以输入搜索内容来全局搜索信息(类似于vi中的搜索),[]不可以模块化,<>的才可以模块化。
 
注:linux内核中一个功能模块有三种编译方法:编入、去除、模块化。
编入就是将这个模块的代码直接编译连接到zImage中去,用 * 号表示,快捷键:Y
去除就是将这个模块不编译链接到zImage中,用 空白 表示, 快捷键:N
模块化是将这个模块仍然编译,但是不会将其链接到zImage中,用 M 表示,快捷键:M
模块化会将这个模块单独链接成一个内核模块.ko文件,将来linux系统内核启动起来后可以动态的加载或卸载这个模块。
在menuconfig中选项前面的括号里,*表示编入,空白表示去除,M表示模块化, < > 表示可模块化,[ ] 表示不可模块化。
回到顶部
6.menuconfig的工作原理
menuconfig本身由一套软件支持的
linux为了实现图形化界面的配置,专门提供了一套配置工具menuconfig。
ncurses库是linux中用来实现文字式的图形界面,linux内核中使用了ncurses库来提供 menuconfig
scripts\kconfig\lxdialog目录下的一些c文件就是用来提供menuconfig的那些程序源代码。
 
menuconfig读取Kconfig文件
menuconfig本身的软件只负责提供menuconfig工作的这一套逻辑(譬如在menuconfig中通过上下左右箭头按键来调整光标,Enter ESC键等按键按下的响应),而并不负责提供内容(菜单里的项目)。
menuconfig显示的菜单内容(一方面是菜单的目录结构,另一方面是每一个菜单项目的细节)是由内核源码树各个目录下的 Kconfig 文件来支持的。Kconfig文件中按照一定的格式包含了一个又一个的配置项,每一个配置项在make menuconfig中都会成为一个菜单项目。而且menuconfig中显示的菜单目录结构和源码目录中的Kconfig的目录结构是一样的。
在相应的Kconfig文件中删除一个config项,则再次make menuconfig时这个项目已经看不到了。
 
menuconfig读取/写入.config文件
刚才已经知道menuconfig的菜单内容来自于Kconfig文件,但是每一个菜单的选择结果(Y、N、M)却不是保存在Kconfig文件中的。Kconfig文件是不变的,Kconfig文件只是决定有没有这个菜单项,并不管这个菜单项的选择结果。
menuconfig工作时在我们make menuconfig打开时,他会读取.config文件,并且用.config文件中的配置选择结果来初始化menuconfig中各个菜单项的选择值。
 
总结:菜单项的项目内容从 Kconfig 文件来,菜单项的选择值从 .config 文件来
 
当我们每次退出make menuconfig时,menuconfig机制会首先检查我们有没有更改某些配置项的值,如果我们本次没有更改过任意一个配置项目的值那直接退出;如果我们有改动配置项的值则会提示我们是否保存。此时如果点保存,则会将我们更改过的配置重新写入.config文件中记录,下一次再次打开make menuconfig时会再次加载.config,最终去编译内核时编译连接程序会考虑.config中的配置值指导整个编译连接过程。
 
总结:本次主要内容就是讲:menuconfig和Kconfig和.config的关系。
回到顶部
7.Kconfig文件详解1
Kconfig的格式
Kconfig按照一定的格式来书写,menuconfig程序可以识别这种格式,然后从中提取出有效信息组成menuconfig中的菜单项。
将来在做驱动移植等工作时,有时需要自己添加Kconfig中的一个配置项来将某个设备驱动添加到内核的配置项目中,这时候就需要对Kconfig的配置项格式有所了解,否则就不会添加。
#开头的行是注释行
menuconfig表示菜单(本身属于一个菜单中的项目,但是他又有子菜单项目)、.config表示菜单中的一个配置项(本身并没有子菜单下的项目)。


menuconfig或者config后面空格隔开的大写字母表示的类似于 NETDEVICES 的就是这个配置项的配置项名字,这个字符串前面添加CONFIG_后就构成了.config中的配置项名字。
一个menuconfig后面跟着的所有config项就是这个menuconfig的子菜单。这就是Kconfig中表示的目录关系。
内核源码目录树中每一个Kconfig都会source引入其所有子目录下的Kconfig,从而保证了所有的Kconfig项目都被包含进menuconfig中。这个也告诉我们:如果你自己在linux内核中添加了一个文件夹,一定要在这个文件夹下创建一个Kconfig文件,然后在这个文件夹的上一层目录的Kconfig中source引入这个文件夹下的Kconfig文件。


 
tristate和bool的含义
tristate 意思是三态 < >(3种状态,对应Y、N、M三种选择方式),bool 是要么真要么假 [ ](对应Y和N)。所以tristate的意思就是这个配置项可以被三种选择,bool的意思是这个配置项只能被2种选择。




回到顶部
8.Kconfig文件详解2
depends的含义
depends中文意思是“取决于”或者“依赖于”,所以depends在这里的意思是:本配置项依赖于另一个配置项。依赖的配置项为Y或者M,则本配置项才有意义;如果依赖的哪个配置项本身被设置为N,则本配置项根本没有意义。
depends项目会导致make menuconfig的时候找不到一些配置项。所以你在menuconfig中如果找不到一个选项,但是这个选项在Kconfig中却是有的,则可能的原因就是这个配置项依赖的一个配置项是不成立的。
depends并不要求依赖的配置项一定是一个,可以是多个,而且还可以有逻辑运算。这种时候只要依赖项目运算式子的裸机结果为真则依赖就成立。


help
帮助信息,告诉我们这个配置项的含义,以及如何去配置他。



回到顶部
9.Kconfig.config文件和Makefile三者的关联
配置项XXX被配置成Y、N、M会影响.config文件中的CONFIG_XXX变量的配置值。
这个.config中的配置值(=y、=m、没有)会影响最终的编译链接过程。如果=y则会被编入(built-in),如果=m会被单独连接成一个ko模块,如果没有则对应的代码不会被编译。那么这么是怎么实现的?都是通过makefile实现的。
obj-$(CONFIG_DM9000) += dm9000.o
如果CONFIG_DM9000变量值为y,则obj += dm9000.o,因此dm9000.c会被编译;如果CONFIG_DM9000变量未定义,则dm9000.c不会被编译。如果CONFIG_DM9000变量的值为m则会被连接成ko模块(这个是在linux内核的Makefile中定义的规则)


 
总结:把menuconfig中的菜单项、Kconfig中的配置项、.config中的一行、 Makefile中的一行,这4个东西结合起来理解,则整个linux内核的配置体系就明了了。
menuconfig中的菜单项取决于Kconfig中配置项,我们在配置Kconfig后,会更改.config 中变量的值,在编译的时候,Makefile会根据变量的值来决定哪些文件被编译。
回到顶部
10.menuconfig的实验学习思路
验证menuconfig和.config的关系
make menuconfig时,会读取.config中的配置值来初始化menuconfig中的配置项。
验证:如果理论正确的,那么我自己手工修改了.config的配置后,再次make menuconfig时看到的初始值就应该是我手工修改的。
menuconfig中修改了(按Y、N、M)配置项的值,然后退出时保存,则这个保存结果会修改.config文件中的相应行。
验证:如果结论是正确的,那么在menucofig中修改了配置后保存退出,再次去手工打开.config文件则可以看到相应配置的一行内容被修改了。
 
验证menuconfig和Kconfig的关系
menuconfig读取Kconfig的内容作为菜单项目内容。
验证1:在Kconfig中删除一个config项,则再次make menuconfig时就看不到这个项目了。(上课时已经验证过了)
验证2:在Kconfig中自己添加创建一个config项,则再次make menuconfig时就能看到多了一个项目。
 
验证验证menuconfig和Makefile的关系
我找一个模块,把他配制成y,然后去make编译连接,最后得到的zImage中这个模块就应该被编译连接进去到zImage中了。
验证:
方法一:去这个模块对应的源代码目录看一下这个源码有没有被编译
方法二:去zImage对应的elf格式的vmlinux中查看符号
方法三:将vmlinux反编译(objdump)后得到的文件中找模块对应的符号
方法四:将zImage下载到开发板中启动,启动后看你的模块能不能工作
3、内核的启动过程
目录
 1.内核分析准备
链接脚本分析
2.head.S文件分析1
内核的真正入口
内核运行的硬件条件
3.内核启动的汇编阶段
4.内核启动的C语言阶段1
5.内核启动的C语言阶段2
打印内核版本信息
6.内核启动的C语言阶段3
7.内核启动的C语言阶段4
8.内核启动的C语言阶段5
9.内核启动的C语言阶段6
10.init进程详解1
init进程构建了用户交互界面
11.init进程详解2
打开控制台
12.cmdline常用参数
13.内核中架构相关代码简介
 
正文
回到顶部
 1.内核分析准备
删除无用文件
官方版本的kernel中是支持各种硬件架构、各种开发板的,因此有很多文件夹和文件和我们无关,在建立SI工程前应该删掉这些家伙。
我们现在分析的是开发板厂商九鼎科技移植好的针对X210开发板的kernel,因此其中一些无用文件已经被删掉了。
 
建立SI工程并解析
建立方法和uboot中当时讲的是一样的。
 
Makefile分析
kernel的Makefile写法和规则等和uboot的Makefile是一样的,甚至Makefile中的很多内容都是一样的。
kernel的Makefile比uboot的Makefile要复杂,这里我们并不会一行一行的详细分析。
Makefile中只有一些值得关注的我会强调一下,其他不强调的地方暂时可以不管。
Makefile中刚开始定义了kernel的内核版本号。这个版本号挺重要(在模块化驱动安装时会需要用到),要注意会查,会改。
在make编译内核时,也可以通过命令行给内核makefile传参(跟uboot配置编译时传参一样)。譬如make O=xxx可以指定不在源代码目录下编译,而到另外一个单独文件夹下编译。
kernel的顶层Makefile中定义了2个很重要的变量,一个是ARCH,一个是CROSS_COMPILE。ARCH决定当前配置编译的路径,譬如ARCH = arm的时候,将来在源码目录下去操作的arch/arm目录。CROSS_COMPILE用来指定交叉编译工具链的路径和前缀。
CROSS_COMPILE = xxx和ARCH = xxx和O=xxx这些都可以在make时通过命令行传参的方式传给顶层Makefile。
所以有时候你会看到别人编译内核时:make O=/tmp/mykernel ARCH=arm CROSS_COMPILE=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
链接脚本分析
分析连接脚本的目的就是找到整个程序的entry(入口)
kernel的连接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds。
vmlinux.lds.S 在 arch/arm/kernel/目录下。
思考:为什么linux kernel不直接提供vmlinux.lds而要提供一个vmlinux.lds.S然后在编译时才去动态生成vmlinux.lds呢?
猜测:.lds文件中只能写死,不能用条件编译。但是我们在kernel中链接脚本确实有条件编译的需求(但是lds格式又不支持),于是乎kernel工作者找了个投机取巧的方法,就是把vmlinux.lds写成一个汇编格式,然后汇编器处理的时候顺便条件编译给处理了,得到一个不需要条件编译的vmlinux.lds。
入门在哪里?从vmlinux.lds中ENTRY(stext)可以知道入口符号是stext,在SI中搜索这个符号,发现在arch/arm/kernel/head.S 和head-nommu.S中都有。
head.S是启用了MMU情况下的kernel启动文件,相当于uboot中的start.S。head-nommu.S是未使用mmu情况下的kernel启动文件。

回到顶部
2.head.S文件分析1
内核运行的物理地址与虚拟地址
KERNEL_RAM_VADDR(VADDR就是virtual address),这个宏定义了内核运行时的虚拟地址。值为0xC0008000
KERNEL_RAM_PADDR(PADDR就是physical address),这个宏定义内核运行时的物理地址。值为0x30008000
总结:内核运行的物理地址是0x30008000,对应的虚拟地址是0xC0008000。
#define KERNEL_RAM_VADDR    (PAGE_OFFSET + TEXT_OFFSET)    //内核运行的虚拟地址 c0000000+8000
#define KERNEL_RAM_PADDR    (PHYS_OFFSET + TEXT_OFFSET)    //内核运行的物理地址 30000000+8000

内核的真正入口
内核的真正入口就是ENTRY(stext)处
前面的__HEAD定义了后面的代码属于段名为.head.text的段
 __HEAD                /* .section ".head.text","ax"  用户自定义段 */
ENTRY(stext)        //内核入口
    setmode    PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode    //把中断快速中断禁掉,设置SVC模式

内核运行的硬件条件
内核的起始部分代码是被解压代码调用的。回忆之前讲zImage的时候,uboot启动内核后实际调用运行的是zImage前面的那段未经压缩的解压代码,解压代码运行时先将zImage后段的内核解压开,然后再去调用运行真正的内核入口。
内核启动是有一定的先决条件,这个条件由启动内核的bootloader(我们这里就是uboot)来构建保证。(The requirements * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0, * r1 = machine nr, r2 = atags pointer.)

/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * This is normally called from the decompressor code.  The requirements
 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
 * r1 = machine nr, r2 = atags pointer.
 *
 * This code is mostly position independent, so if you link the kernel at
 * 0xc0008000, you call this at __pa(0xc0008000).
 *
 * See linux/arch/arm/tools/mach-types for the complete list of machine
 * numbers for r1.
 *
 * We're trying to keep crap to a minimum; DO NOT add any machine specific
 * crap here - that's what the boot loader (or in extreme, well justified
 * circumstances, zImage) is for.
 */


ARM体系中,函数调用时实际是通过寄存器传参的(函数调用时传参有两种设计:一种是寄存器传参,另一种是栈内存传参)。所以uboot中最后 theKernel (0, machid, bd->bi_boot_params);执行内核时,运行时实际0放入r0machid放入到了r1bd->bi_boot_params放入到了r2。ARM的这种处理技巧刚好满足了kernel启动的条件和要求。


 
kernel启动时MMU是关闭的,因此硬件上需要的是物理地址。但是内核是一个整体(zImage)只能被连接到一个地址(不能分散加载),这个连接地址肯定是虚拟地址。因此内核运行时前段head.S中尚未开启MMU之前的这段代码就很难受。所以这段代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。__pa()自动将虚拟地址转变成物理地址
 
内核启动要求的传参方式
回到顶部
3.内核启动的汇编阶段
s内核启动汇编部分程序如下

 __HEAD                /* .section ".head.text","ax"  用户自定义段 */
ENTRY(stext)        //内核入口
    setmode    PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode    //把中断快速中断禁掉,设置SVC模式
                        @ and irqs disabled
    mrc    p15, 0, r9, c0, c0        @ get processor id
    bl    __lookup_processor_type        @ r5=procinfo r9=cpuid 校验处理器ID
    movs    r10, r5                @ invalid processor (r5=0)?
    beq    __error_p            @ yes, error 'p'
    bl    __lookup_machine_type        @ r5=machinfo                         校验机器码
    movs    r8, r5                @ invalid machine (r5=0)?
    beq    __error_a            @ yes, error 'a'
    bl    __vet_atags        @ 校验传参
    bl    __create_page_tables        //建立段式页表(粗页表)
    /*
     * The following calls CPU specific code in a position independent
     * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
     * xxx_proc_info structure selected by __lookup_machine_type
     * above.  On return, the CPU will be ready for the MMU to be
     * turned on, and r0 will hold the CPU control register value.
     */
    ldr    r13, __switch_data        @ address to jump to after        @
                        @ mmu has been enabled
    adr    lr, BSYM(__enable_mmu)        @ return (PIC) address
 ARM(    add    pc, r10, #PROCINFO_INITFUNC    )
 THUMB(    add    r12, r10, #PROCINFO_INITFUNC    )
 THUMB(    mov    pc, r12                )
ENDPROC(stext)

__lookup_processor_type
我们从cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用这个函数来进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。
该函数检验cpu id的合法性方法是:内核会维护一个本内核支持的CPU ID号码的数组,然后该函数所做的就是将从硬件中读取的cpu id号码和数组中存储的各个id号码依次对比,如果没有一个相等则不合法,如果有一个相等的则合法。
内核启动时设计这个校验,也是为了内核启动的安全性着想。

__lookup_processor_type:
    adr    r3, 3f
    ldmia    r3, {r5 - r7}
    add    r3, r3, #8
    sub    r3, r3, r7            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space
1:    ldmia    r5, {r3, r4}            @ value, mask
    and    r4, r4, r9            @ mask wanted bits
    teq    r3, r4
    beq    2f
    add    r5, r5, #PROC_INFO_SZ        @ sizeof(proc_info_list)
    cmp    r5, r6
    blo    1b
    mov    r5, #0                @ unknown processor
2:    mov    pc, lr
ENDPROC(__lookup_processor_type)


__lookup_machine_type
该函数的设计理念和思路和上面校验cpu id的函数一样的。不同之处是本函数校验的是机器码。

__lookup_machine_type:
    adr    r3, 4b
    ldmia    r3, {r4, r5, r6}
    sub    r3, r3, r4            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space
1:    ldr    r3, [r5, #MACHINFO_TYPE]    @ get machine type
    teq    r3, r1                @ matches loader number?
    beq    2f                @ found
    add    r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    cmp    r5, r6
    blo    1b
    mov    r5, #0                @ unknown machine
2:    mov    pc, lr
ENDPROC(__lookup_machine_type)

__vet_atags
该函数的设计理念和思路和上面2个一样,不同之处是用来校验uboot给内核的传参ATAGS格式是否正确。这里说的传参指的是uboot通过tag给内核传的参数(主要是板子的内存分布memtag、uboot的bootargs)
内核认为如果uboot给我的传参格式不正确,那么我就不启动。
uboot给内核传参的部分如果不对,是会导致内核不启动。譬如uboot的bootargs设置不正确内核可能就会不启动。

__vet_atags:
    tst    r2, #0x3            @ aligned?
    bne    1f
    ldr    r5, [r2, #0]            @ is first tag ATAG_CORE?
    cmp    r5, #ATAG_CORE_SIZE
    cmpne    r5, #ATAG_CORE_SIZE_EMPTY
    bne    1f
    ldr    r5, [r2, #4]
    ldr    r6, =ATAG_CORE
    cmp    r5, r6
    bne    1f
    mov    pc, lr                @ atag pointer is ok
1:    mov    r2, #0
    mov    pc, lr
ENDPROC(__vet_atags)

__create_page_tables
顾名思义,这个函数用来建立页表。
linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。kernel想了一个好办法
kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。
内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。
__switch_data
建立了段式页表后进入了 __switch_data 部分,这东西是个函数指针数组。
分析得知下一步要执行__mmap_switched函数
复制数据段、清除bss段(目的是构建C语言运行环境)
保存起来cpu id号、机器码、tag传参的首地址。
b start_kernel跳转到C语言运行阶段。

__switch_data:
    .long    __mmap_switched                            @构建c语言运行环境
    .long    __data_loc            @ r4
    .long    _data                @ r5
    .long    __bss_start            @ r6
    .long    _end                @ r7
    .long    processor_id            @ r4
    .long    __machine_arch_type        @ r5
    .long    __atags_pointer            @ r6
    .long    cr_alignment            @ r7
    .long    init_thread_union + THREAD_START_SP @ sp


总结:汇编阶段其实也没干啥,主要原因是uboot干了大部分活。汇编阶段主要就是:校验cpu_id,机器码和uboot传参、建立段式映射的页表并开启MMU以方便使用内存、跳入C阶段。
回到顶部
4.内核启动的C语言阶段1
这一块的学习思路
抓大放小,不深究.
感兴趣可以就某个话题去网上搜索资料学习
重点局部深入分析
具体学习方法
顺着代码执行路径抓全。这是我们的学习主线。
对照内核启动的打印信息进行分析。
几条学习线路
分析uboot给kernel传参的影响和实现
硬件初始化与驱动加载
内核启动后的结局与归宿
回到顶部
5.内核启动的C语言阶段2
杂碎
smp。smp就是对称多处理器(其实就是我们说的多核心CPU)
lockdep。锁定依赖,是一个内核调试模块,处理内核自旋锁死锁问题相关的。
cgroup。control group,内核提供的一种来处理进程组的技术。
打印内核版本信息
代码位于:kernel/init/main.c中的572行
printk函数是内核中用来从console打印信息的,类似于应用层编程中的printf。内核编程时不能使用标准库函数,因此不能使用printf,其实printk就是内核自己实现的一个printf。
printk函数的用法和printf几乎一样,不同之处在于可以在参数最前面用一个宏来定义消息输出的级别。为什么要有这种级别?主要原因是linux内核太大了,代码量太多,里面的printk打印信息太多了。如果所有的printk都能打印出来而不加任何限制,则最终内核启动后得到海量的输出信息。
为了解决打印信息过多,无效信息会淹没有效信息这个问题,linux内核的解决方案是给每一个printk添加一个打印级别。级别定义0-7(注意编程的时候要用相应的宏定义,不要直接用数字)分别代表8种输出的重要性级别,0表示最重要,7表示最不重要。我们在printk的时候自己根据自己的消息的重要性去设置打印级别。
linux的控制台监测消息的地方也有一个消息过滤显示机制,控制台实际只会显示级别比我的控制台定义的级别高的消息。譬如说控制台的消息显示级别设置为4,那么只有printk中消息级别为0-3(也可能是0-4)的才可以显示看见,其余的被过滤掉了。

  lock_kernel();                  //大内核锁
  tick_init();                   //初始化滴答时钟
  boot_cpu_init();            
  page_address_init();
  printk(KERN_NOTICE "%s", linux_banner);   //打印kernel版本信息和编译环境信息
  setup_arch(&command_line);                //创建硬件平台
  mm_init_owner(&init_mm, &init_task);
  setup_command_line(command_line);         //保存命令号
  setup_nr_cpu_ids();                      //求CPU的个数
  setup_per_cpu_areas();                  //初始化per_cpu数据
  smp_prepare_boot_cpu();    /* arch-specific boot-cpu hooks  注册SMP上的启动进程 */
  build_all_zonelists(NULL);             //查看build_all_zonelists结构
  page_alloc_init();                   //处理用于热插拔CPU的页
  printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);    //正式打印
  parse_early_param();                     //解析传参和其他传参
  parse_args("Booting kernel", static_command_line, __start___param,    //处理特殊参数
           __stop___param - __start___param,
           &unknown_bootoption);

linux_banner的内容解析:存放linux版本信心和编译环境信息
/* FIXED STRINGS! Don't touch! */
const char linux_banner[] =
    "Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
    LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";
回到顶部
6.内核启动的C语言阶段3
setup_arch()函数简介
从名字看,这个函数是CPU架构相关的一些创建过程。
实际上这个函数是用来确定我们当前内核的机器(arch、machine)的。我们的linux内核会支持一种CPU的运行,CPU+开发板就确定了一个硬件平台,然后我们当前配置的内核就在这个平台上可以运行。之前说过的机器码就是给这个硬件平台一个固定的编码,以表征这个平台。
当前内核支持的机器码以及硬件平台相关的一些定义都在这个函数中处理。
 
Machine查找
setup_processor() 函数用来查找CPU信息,可以结合串口打印的信息来分析。
setup_machine() 函数的传参是机器码编号,machine_arch_type符号在include/generated/mach-types.h的32039-32050行定义了。经过分析后确定这个传参值就是2456.

arch_initcall(customize_machine);
void __init setup_arch(char **cmdline_p)
{
    struct tag *tags = (struct tag *)&init_tags;
    struct machine_desc *mdesc;
    char *from = default_command_line;
    unwind_init();
    setup_processor();    //查找CPU信息
    mdesc = setup_machine(machine_arch_type);    //查找机器码
    machine_name = mdesc->name;
    if (mdesc->soft_reboot)
        reboot_setup("s");


setup_machine() 函数的作用是通过传入的机器码编号,找到对应这个机器码的machine_desc描述符,并且返回这个描述符的指针。
其实真正干活的函数是lookup_machine_type(),找这个函数发现在head-common.S中,真正干活的函数是__lookup_machine_type

static struct machine_desc * __init setup_machine(unsigned int nr)
{
    struct machine_desc *list;
    /*
     * locate machine in the list of supported machines.
     */
    list = lookup_machine_type(nr);
    if (!list) {
        printk("Machine configuration botched (nr %d), unable "
               "to continue.\n", nr);
        while (1);
    }
    printk("Machine: %s\n", list->name);
    return list;
}


__lookup_machine_type()函数的工作原理:内核在建立的时候就把各种CPU架构的信息组织成一个一个的machine_desc结构体实例,然后都给一个段属性 .arch.info.init,链接的时候会保证这些描述符会被连接在一起。__lookup_machine_type就去那个那些描述符所在处依次挨个遍历各个描述符,比对看机器码哪个相同。

    .align    2
3:    .long    __proc_info_begin
    .long    __proc_info_end
4:    .long    .
    .long    __arch_info_begin
    .long    __arch_info_end
__lookup_machine_type:
    adr    r3, 4b
    ldmia    r3, {r4, r5, r6}
    sub    r3, r3, r4            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space
1:    ldr    r3, [r5, #MACHINFO_TYPE]    @ get machine type
    teq    r3, r1                @ matches loader number?
    beq    2f                @ found
    add    r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    cmp    r5, r6
    blo    1b
    mov    r5, #0                @ unknown machine
2:    mov    pc, lr
ENDPROC(__lookup_machine_type)
@ vmlinux.lds.S
__arch_info_begin = .;
    *(.arch.info.init)
__arch_info_end = .;

setup_arch()函数进行了基本的 cmdline 处理
这里说的cmdline指的uboot给kernel传参时传递的命令行启动参数,也就是uboot的bootargs。
有几个相关的变量需要注意:
default_command_line:看名字是默认的命令行参数,实际是一个全局变量字符数组,这个字符数组可以用来存东西。
//\kernel\kernel-jiuding\arch\arm\kernel\setup.c
static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;

CONFIG_CMDLINE:在.config文件中定义的(可以在make menuconfig中去更改设置),这个表示内核的一个默认的命令行参数。
内核对cmdline的处理思路是:内核中自己维护了一个默认的cmdline(就是.config中配置的这一个),然后uboot还可以通过tag给kernel再传递一个cmdline。如果uboot给内核传cmdline成功则内核会优先使用uboot传递的这一个;如果uboot没有给内核传cmdline或者传参失败,则内核会使用自己默认的这个cmdline。以上说的这个处理思路就是在setup_arch函数中实现的。
回到顶部
7.内核启动的C语言阶段4
实验验证内核的cmdline确定
验证思路:首先给内核配置时配置一个基本的cmdline,然后在uboot启动内核时给uboot设置一个bootargs,然后启动内核看打印出来的cmdline和uboot传参时是否一样。
在uboot中去掉bootargs,然后再次启动内核看打印出来的cmdline是否和内核中设置的默认的cmdline一样。
 
注意:uboot给内核传递的cmdline非常重要,会影响内核的运行,所以要谨慎。有时候内核启动有问题,可以分析下是不是uboot的bootargs设置不对。
注意:这个传参在这里确定出来之后,还没完。后面还会对这个传参进行解析。解析之后cmdline中的每一个设置项都会对内核启动有影响。
思考:内核为什么要这样设计?
回到顶部
8.内核启动的C语言阶段5
setup_command_line()函数
也是在处理和命令行参数cmdline有关的任务。

static void __init setup_command_line(char *command_line)
{
    saved_command_line = alloc_bootmem(strlen (boot_command_line)+1);
    static_command_line = alloc_bootmem(strlen (command_line)+1);
    strcpy (saved_command_line, boot_command_line);
    strcpy (static_command_line, command_line);
}

parse_early_param() & parse_args()
解析cmdline传参和其他传参
这里的解析意思是把cmdline的细节设置信息给解析出来。譬如cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3,则解析出的内容就是就是一个字符串数组,数组中依次存放了一个设置项目信息。
console=ttySAC2,115200 一个 // 串口2,波特率为 115200
root=/dev/mmcblk0p2 rw 一个 //
init=/linuxrc 一个 //
rootfstype=ext3 一个 //
这里只是进行了解析,并没有去处理。也就是说只是把长字符串解析成了短字符串,最多和内核里控制这个相应功能的变量挂钩了,但是并没有去执行。执行的代码在各自模块初始化的代码部分。
 
杂碎
trap_init() 设置异常向量表
mm_init() 内存管理模块初始化
sched_init() 内核调度系统初始化
early_irq_init() & init_IRQ() 中断初始化
console_init() 控制台初始化
 
总结:start_kernel函数中调用了很多的xx_init函数,全都是内核工作需要的模块的初始化函数。这些初始化之后内核就具有了一个基本的可以工作的条件了。
如果把内核比喻成一个复杂机器,那么start_kernel函数就是把这个机器的众多零部件组装在一起形成这个机器,让他具有可以工作的基本条件。
 
rest_init()
这个函数之前内核的基本组装已经完成。
剩下的一些工作就比较重要了,放在了一个单独的函数中,叫rest_init。
 
总结:start_kernel函数做的主要工作:打印了一些信息、内核工作需要的模块的初始化被依次调用(譬如内存管理、调度系统、异常处理···)、我们需要重点了解的就是setup_arch中做的2件事情:机器码架构的查找并且执行架构相关的硬件的初始化、uboot给内核的传参cmdline。
回到顶部
9.内核启动的C语言阶段6
操作系统去哪了
rest_init()中调用kernel_thread()函数启动了2个内核线程,分别是:kernel_init 和 kthreadd
调用schedule()函数开启了内核的调度系统,从此linux系统开始转起来了。
rest_init()最终调用cpu_idle()函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle。这个函数里面肯定是死循环。
简单来说,linux内核最终的状态是:有事干的时候去执行有意义的工作(执行各个进程任务),实在没活干的时候就去死循环(实际上死循环也可以看成是一个任务)。

static noinline void __init_refok rest_init(void)
    __releases(kernel_lock)
{
    int pid;
    rcu_scheduler_starting();
    /*
     * We need to spawn init first so that it obtains pid 1, however
     * the init task will end up wanting to create kthreads, which, if
     * we schedule it before we create kthreadd, will OOPS.
     */
    kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); //启动1号进程,也称为初始化进程,是第一个用户进程 
    numa_default_policy();
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);//kthreadd为进程2,这个进程是linux内核的守护进程
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    rcu_read_unlock();
    complete(&kthreadd_done);
    unlock_kernel();
    /*
     * The boot idle thread must execute schedule()
     * at least once to get things moving:
     */
    init_idle_bootup_task(current);
    preempt_enable_no_resched();
    schedule();
    preempt_disable();
    /* Call into cpu_idle with preempt disabled */
    cpu_idle();
}


之前已经启动了内核调度系统,调度系统会负责考评系统中所有的进程,这些进程里面只有有哪个需要被运行,调度系统就会终止cpu_idle死循环进程(空闲进程)转而去执行有意义的干活的进程。这样操作系统就转起来了。
 
什么是内核线程
进程和线程。简单来理解,一个运行的程序就是一个进程。所以进程就是任务、进程就是一个独立的程序。独立的意思就是这个程序和别的程序是分开的,这个程序可以被内核单独调用执行或者暂停。
在linux系统中,线程和进程非常相似,几乎可以看成是一样的。实际上我们当前讲课用到的进程和线程的概念就是一样的。
进程/线程就是一个独立的程序。应用层运行一个程序就构成一个用户进程/线程,那么内核中运行一个函数(函数其实就是一个程序)就构成了一个内核进程/线程。
所以我们 kernel_thead 函数运行一个函数,其实就是把这个函数变成了一个内核线程去运行起来,然后他可以被内核调度系统去调度。说白了就是去调度器注册了一下,以后人家调度的时候会考虑你。
 
进程0、进程1、进程2
截至目前为止,我们一共涉及到3个内核进程/线程。
操作系统是用一个数字来表示/记录一个进程/线程的,这个数字就被称为这个进程的进程号。这个号码是从0开始分配的。因此这里涉及到的三个进程分别是linux系统的进程0、进程1、进程2.
在linux命令行下,使用ps命令可以查看当前linux系统中运行的进程情况。
我们在ubuntu下ps -aux可以看到当前系统运行的所有进程,可以看出进程号是从1开始的。为什么不从0开始,因为进程0不是一个用户进程,而属于内核进程。
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); //启动1号进程,也称为初始化进程,是第一个用户进程 
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);//kthreadd为进程2,这个进程是linux内核的守护进程
......
cpu_idle();
三个进程
进程0:cpu_idle进程0其实就是刚才讲过的idle进程,叫空闲进程,也就是死循环。
进程1:kernel_init 函数就是进程1,这个进程被称为  init进程。
进程2:kthreadd 函数就是进程2,这个进程是linux内核的守护进程。这个进程是用来保证linux内核自己本身能正常工作的。
 
总结1:重点在于理解linux内核启动后达到的一个稳定状态。注意去对比内核启动后的稳定状态和uboot启动后的稳定状态的区别。
总结2:本节课的第二个重点就是初步理解进程/线程的概念。
总结3:你得明白每个进程有个进程号,进程号从0开始依次分配的。明白进程0是idle进程(idle进程是干嘛的);进程2是ktheadd进程(基本明白干嘛的就行)
总结4:分析到此,发现后续的料都在进程1.所以后面课程会重点从进程1出发,分析之后发生的事情。
回到顶部
10.init进程详解1
init进程完成了从内核态向用户态的转变
一个进程2种状态。init 进程刚开始运行的时候是内核态,它属于一个内核线程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。因为init进程自身完成了从内核态到用户态的过度,因此后续的其他进程都可以工作在用户态下面了。
内核态下做了什么?重点就做了一件事情,就是挂载根文件系统并试图找到用户态下的那个init程序。init进程要把自己转成用户态就必须运行一个用户态的应用程序(这个应用程序名字一般也叫init),要运行这个应用程序就必须得找到这个应用程序,要找到它就必须得挂载根文件系统,因为所有的应用程序都在文件系统中。
内核源代码中的所有函数都是内核态下面的,执行任何一个都不能脱离内核态。应用程序必须不属于内核源代码,这样才能保证自己是用户态。也就是说我们这里执行的这个init程序和内核不在一起,他是另外提供的。提供这个init程序的那个人就是根文件系统。
 
用户态下做了什么?init进程大部分有意义的工作都是在用户态下进行的。init进程对我们操作系统的意义在于:其他所有的用户进程都直接或者间接派生自init进程。
如何从内核态跳跃到用户态?还能回来不?这个过程是不可逆的
init进程在内核态下面时,通过一个函数 kernel_execve 来执行一个用户空间编译连接的应用程序就跳跃到用户态了。注意这个跳跃过程中进程号是没有改变的,所以一直是进程1.这个跳跃过程是单向的,也就是说一旦执行了init程序转到了用户态下整个操作系统就算真正的运转起来了,以后只能在用户态下工作了,用户态下想要进入内核态只有走API这一条路了。
init进程构建了用户交互界面
init进程是其他用户进程的老祖宗。linux系统中一个进程的创建是通过其父进程创建出来的。根据这个理论只要有一个父进程就能生出一堆子孙进程了。
init启动了login进程(用于用户登录)、命令行进程(提供命令行环境)、shell进程
shell进程启动了其他用户进程。命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序,每一个应用程序的运行就是一个进程。
 
总结:本节的主要目的是让大家认识到init进程如何一步步发展成为我们平时看到的那种操作系统的样子。
回到顶部
11.init进程详解2
打开控制台
linux系统中每个进程都有自己的一个文件描述符表,表中存储的是本进程打开的文件。
linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。
这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。
进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符。

* Open the /dev/console on the rootfs, this should never fail */
    if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)            //打开控制台文件,得到一个文件描述符
        printk(KERN_WARNING "Warning: unable to open an initial console.\n");
    (void) sys_dup(0);    //复制2次文件描述符,一共得到3个文件描述符
    (void) sys_dup(0);

挂载根文件系统
prepare_namespace 函数中挂载根文件系统
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
        ramdisk_execute_command = NULL;
        prepare_namespace();        //挂载根文件系统
    }
根文件系统在哪里?根文件系统的文件系统类型是什么? uboot通过传参来告诉内核这些信息。
uboot传参中的 root=/dev/mmcblk0p2 rw 这一句就是告诉内核根文件系统在哪里
uboot传参中的 rootfstype=ext3 这一句就是告诉内核rootfs的类型。
如果内核挂载根文件系统成功,则会打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.
如果挂载根文件系统失败,则会打印:No filesystem could mount root, tried: yaffs2
如果内核启动时挂载rootfs失败,则后面肯定没法执行了,肯定会死。内核中设置了启动失败休息5s自动重启的机制,因此这里会自动重启,所以有时候大家会看到反复重启的情况。
 
如果挂载rootfs失败,可能的原因有:
最常见的错误就是uboot的bootargs设置不对。
rootfs烧录失败(fastboot烧录不容易出错,以前是手工烧录很容易出错)
rootfs本身制作失败的。(尤其是自己做的rootfs,或者别人给的第一次用)
执行用户态下的进程1程序
上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,这个程序就是用户空间的进程1.找到后用run_init_process去执行他
我们如果确定init程序是谁?方法是:
先从uboot传参cmdline中看有没有指定,如果有指定先执行cmdline中指定的程序。cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序。这里的指定方式就表示我们rootfs的根目录下面有个名字叫linuxrc的程序,这个程序就是init程序。
如果uboot传参cmdline中没有init=xx或者cmdline中指定的这个xx执行失败,还有备用方案。第一备用:/sbin/init,第二备用:/etc/init,第三备用:/bin/init,第四备用:/bin/sh。
如果以上都不成功,则认命了,死了。

    /*
     * We try each of these until one succeeds.
     *
     * The Bourne shell can be used instead of init if we are
     * trying to recover a really broken machine.
     */
    if (execute_command) {
        run_init_process(execute_command);
        printk(KERN_WARNING "Failed to execute %s.  Attempting "
                    "defaults...\n", execute_command);
    }
    run_init_process("/sbin/init");
    run_init_process("/etc/init");
    run_init_process("/bin/init");
    run_init_process("/bin/sh");
    panic("No init found.  Try passing init= option to kernel. "
          "See Linux Documentation/init.txt for guidance.");
}

回到顶部
12.cmdline常用参数
格式简介
格式就是由很多个项目用空格隔开依次排列,每个项目中都是:项目名=项目值
整个cmdline会被内核启动时解析,解析成一个一个的项目名=项目值的字符串。这些字符串又会被再次解析从而影响启动过程。
 
root=
这个是用来指定根文件系统在哪里的
一般格式是root=/dev/xxx(一般如果是nandflash上则 /dev/mtdblock2,如果是inand/sd的话则 /dev/mmcblk0p2)
如果是nfs的rootfs,则root=/dev/nfs。
 
rootfstype=
根文件系统的文件系统类型,一般是jffs2、yaffs2、ext3、ubi
 
console=
控制台信息声明,譬如:console=/dev/ttySAC0,115200表示控制台使用串口0,波特率是115200.
正常情况下,内核启动的时候会根据console=这个项目来初始化硬件,并且重定位console到具体的一个串口上,所以这里的传参会影响后续是否能从串口终端上接收到内核的信息。
 
mem=
mem=用来告诉内核当前系统的内存有多少
 
init=
init=用来指定进程1的程序pathname,一般都是init=/linuxrc
 
常见cmdline介绍
console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
第一种这种方式对应rootfs在SD/iNand/Nand/Nor等物理存储器上。这种对应产品正式出货工作时的情况。
 
root=/dev/nfs nfsroot=192.168.1.141:/root/s3c2440/build_rootfs/aston_rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200
第二种这种方式对应rootfs在nfs上,这种对应我们实验室开发产品做调试的时候。
回到顶部
13.内核中架构相关代码简介
内核代码基本分为3块
arch:目录下全是cpu架构有关的代码
drivers :目录下全是硬件的驱动
其他:相同点是这些代码都和硬件无关,因此系统移植和驱动开发的时候这些代码几乎都是不用关注的。
 
架构相关的常用目录名及含义
mach(mach就是machine architecture)。arch/arm目录下的一个mach-xx目录就表示一类machine的定义,这类machine的共同点是都用xx这个cpu来做主芯片。(譬如mach-s5pv210这个文件夹里面都是s5pv210这个主芯片的开发板machine);mach-xx目录里面的一个mach-yy.c文件中定义了一个开发板(一个开发板对应一个机器码),这个是可以被扩展的。
plat(plat是platform的缩写,含义是平台)。plat在这里可以理解为SoC,也就是说这个plat目录下都是SoC里面的一些硬件(内部外设)相关的一些代码。
在内核中把SoC内部外设相关的硬件操作代码就叫做平台设备驱动。
include这个include目录中的所有代码都是架构相关的头文件。(linux内核通用的头文件在内核源码树根目录下的include目录里)
 
补充
(内核中的文件结构很庞大、很凌乱(不同版本的内核可能一个文件存放的位置是不同的),会给我们初学者带来一定的困扰。
头文件目录include有好几个,譬如:
kernel/include 内核通用头文件
kernel/arch/arm/include 架构相关的头文件
kernel/arch/arm/include/asm
kernel\arch\arm\include\asm\mach
kernel\arch\arm\mach-s5pv210\include\mach
kernel\arch\arm\plat-s5p\include\plat
 
内核中包含头文件时有一些格式
#include <linux/kernel.h>         kernel/include/linux/kernel.h
#include <asm/mach/arch.h>        kernel/arch/arm/include/asm/mach/arch.h
#include <asm/setup.h>                kernel/arch/arm/include/asm/setup.h
#include <plat/s5pv210.h>            kernel/arch/arm/plat-s5p/include/plat/s5pv210.h
 
有些同名的头文件是有包含关系的,有时候我们需要包含某个头文件时可能并不是直接包含他,而是包含一个包含了他的头文件。
4、移植三星官方内核
目录
1.内核移植初体验
2.初步移植以看到启动信息
3.内核中机器码的确定
4.解决内核启动中的错误
5.iNand的问题和安排
6.网卡驱动的移植和添加实验
7.内核启动第一阶段的调试方法
 
正文
回到顶部
1.内核移植初体验
三星官方移植版内核获取
从网盘下载源码包。
这个文件最初是来自于三星的SMDKV210开发板附带的光盘资料
构建移植环境
将kernel包放到Ubuntu中,使用“tar zxvf android_kernel_2.6.35_smdkv210.tar.bz2”进行解压,然后“make distclean”,完成后就可以copy到Windows下了;
在kernel目录下,删除与硬件无关的文件,集中在arch目录下,进入arch目录,先删除无关的架构文件,然后进入arm目录,删除与CPU无关的文件“mach-xxx”,留下三星相关的备用,再删除与平台无关的文件“plat”,留下三星相关的备用;
Windows下建立SI工程
 
配置编译下载尝试
检查Makefile中ARCH和CROSS_COMPILE
make xx_defconfig
make menuconfig
make -j4:修改错误
Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.
/root/kernel-porting/kernel-samsung/kernel/Makefile:138: recipe for target 'kernel/timeconst.h' failed
// 解决方法:直接将 “defined()”去掉,去掉后为“if (!@val) {”
默认情况下直接make则会直接单线程编译。但是如果make -j4则会4线程编译。
编译得到的zImage去下载运行,看结果
根据结果去分析问题原因,然后去尝试解决这些问题。
x210 # bootm 0x30008000
Boot with zImage
Starting kernel ...
回到顶部
2.初步移植以看到启动信息
分析问题
根据运行结果,分析发现:linux内核的自解压代码都没有运行(因为没有看到:Uncompressing Linux... done, booting the kernel.)
说明zImage根本没有被解压成功,内核代码根本就没有被运行,当然没有输出信息了。所以问题出在解压相关的部分。
问题出在内核配置的解压后代码放置的内存地址处。
内核配置的解压地址应该等于连接地址,否则自解压之后内核无法运行。现在问题变成:第一,内核的连接地址等于多少?第二,内核中配置的解压地址是多少?
这里面还有个问题:内核的连接地址是一个虚拟地址,而自解压代码解压内核时需要物理地址,因此上面说的等于,其实是连接地址对应的物理地址等于自解压地址。
连接地址和他对应的物理地址在head.S中可以查到,分别是0xC0008000和0x30008000。那么自解压代码配置的解压地址应该是30008000.
自解压代码对应的自解压地址在mach/Makefile.boot文件中。在其中修改,加入两行:
//arch/arm/mach-s5pv210/Makefile.boot
# override for SMDKV210
zreladdr-$(CONFIG_MACH_SMDKV210)    := 0x30008000
params_phys-$(CONFIG_MACH_SMDKV210)    := 0x30000100
 
同步代码,并且编译,得到的zImage复制到/tftpboot,然后重新下载运行查看结果。
结果就是:还是没运行,但是有效果。自解压代码解压打印信息已经出来了。但是内核还没运行

x210 # bootm 0x30008000
Boot with zImage
Starting kernel ...
Uncompressing Linux... done, booting the kernel.  //自解压代码打印出的信息

 
问题分析
定义的物理地址不对,从20000000改到30000000即可

//arch/arm/mach-s5pv210/include/mach/memory.h
#if defined(CONFIG_MACH_SMDKV210)
#define PHYS_OFFSET        UL(0x20000000)
//#define PHYS_OFFSET        UL(0x30000000)
#else
#define PHYS_OFFSET        UL(0x30000000)
#endif

更改物理地址之后再编译,出现如下错误:

...
  LD      init/built-in.o
  LD      .tmp_vmlinux1
/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-ld:arch/arm/kernel/vmlinux.lds:293: ignoring invalid character `#' in expression
/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-ld:arch/arm/kernel/vmlinux.lds:293: syntax error
Makefile:815: recipe for target '.tmp_vmlinux1' failed
make: *** [.tmp_vmlinux1] Error 1

解决方法:
定位在arch\arm\mach-s5pv210\include\mach\memory.h中,
把 “//#define PHYS_OFFSET   UL(0x30000000)”  这句话拿掉,因为这句话在配置的时候,会在vmlinux.lds中生成,并导致编译结果出错
回到顶部
3.内核中机器码的确定
MACHINE_START宏
这个宏用来定义一个机器码的数据结构的。这个宏的使用其实是用来定义一个结构体类型为machine_desc类型的结构体变量,名为__mach_desc_SMDKV210。这个结构体变量会被定义到一个特定段.arch.info.init,因此这个结构体变量将来会被链接器链接到这个.arch.info.init段中。

//arch\arm\mach-s5pv210\mach-smdkv210.c
MACHINE_START(SMDKV210, "SMDKV210")
    /* Maintainer: Kukjin Kim <kgene.kim@samsung> */
    .phys_io    = S3C_PA_UART & 0xfff00000,
    .io_pg_offst    = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S5P_PA_SDRAM + 0x100,
    .init_irq    = s5pv210_init_irq,
    .map_io        = smdkv210_map_io,
    .init_machine    = smdkv210_machine_init,
#ifdef CONFIG_S5P_HIGH_RES_TIMERS
    .timer        = &s5p_systimer,
#else
    .timer        = &s3c24xx_timer,
#endif
MACHINE_END

宏替换之后,如下所示:

static const struct machine_desc __mach_desc_SMDKV210    \
__used    \
__attribute__((__section__(".arch.info.init"))) = {    \
.nr    = MACH_TYPE_SMDKV210,    \
.name    = "SMDKV210",
.phys_io    = S3C_PA_UART & 0xfff00000,
.io_pg_offst    = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
.boot_params    = S5P_PA_SDRAM + 0x100,
.init_irq    = s5pv210_init_irq,
.map_io    = smdkv210_map_io,
.init_machine    = smdkv210_machine_init,
.timer    = &s5p_systimer,
};

经过分析,发现一个mach-xxx.c文件中定义了一个机器码的开发板的machine_desc结构体变量,这个结构体变量放到.arch.info.init段中后,那么就表示当前内核可以支持这个机器码的开发板。
落实到当前开发板和当前内核中来分析,当前我们移植的目标开发板使用S5PV210的CPU,开发板名字叫X210.我们在三星官方版本的内核中是找不到mach-x210.c的,所以我们又不想从零开始去移植,因此我们的思路是在三星移植的mach-s5pv210目录下找一个mach-xx.c,这个开发板和我们的X210开发板最为接近,然后以此为基础来移植。
经过查看,发现mach-s5pc110.c和mach-s5pv210.c和我们的X210开发板最为接近。我们一般确定的一个原则是:看我们的开发板和三星官方的哪个开发板最为相似。我们的X210开发板抄的是三星的SMDKV210,因此要找这个对应的那个文件。
结合mach-s5pv210目录下的Makefile来分析,得知.config中定义了CONFIG_MACH_SMDKV210后,实际绑定的是mach-smdkc110.c这个文件。所以实际上mach-smdkv210.c这个文件根本没用到。启示就是不要光看名字。

#ifdef CONFIG_MACH_SMDKC110
MACHINE_START(SMDKC110, "SMDKC110")
#elif CONFIG_MACH_SMDKV210
MACHINE_START(SMDKV210, "SMDKV210")
#endif
    /* Maintainer: Kukjin Kim <kgene.kim@samsung> */
    .phys_io    = S3C_PA_UART & 0xfff00000,
    .io_pg_offst    = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S5P_PA_SDRAM + 0x100,
    .init_irq    = s5pv210_init_irq,
    .map_io        = smdkc110_map_io,
    .init_machine    = smdkc110_machine_init,
    .timer        = &s5p_systimer,
MACHINE_END

硬件驱动的加载和初始化函数执行
.init_machine = smdkc110_machine_init,
这个元素定义了一个机器硬件初始化函数,这个函数非常重要,这个函数中绑定了我们这个开发板linux内核启动过程中会初始化的各种硬件的信息。
回到顶部
4.解决内核启动中的错误
认识内核启动OOPS
OOPS:内核错误的致命信息;
内核启动后会有打印信息,打印信息中隐藏了问题所在。认真的去分析这个打印信息,从中找到对的或者错误的一些信息片段,才能帮助我们找到问题,从而解决问题。
内核启动中的错误信息有一些特征:

[    0.721181] Unable to handle kernel NULL pointer dereference at virtual address 00000060
[    0.729179] pgd = c0004000
[    0.731863] [00000060] *pgd=00000000
[    0.735411] Internal error: Oops: 5 [#1] PREEMPT
[    0.740000] last sysfs file:
[    0.742947] Modules linked in:
[    0.745983] CPU: 0    Not tainted  (2.6.35.7 #1)
[    0.750580] PC is at dev_driver_string+0xc/0x44
[    0.755084] LR is at max8698_pmic_probe+0x150/0x32c
[    0.759935] pc : [<c01e34e8>]    lr : [<c03920c8>]    psr: 20000013
[    0.759940] sp : dfc2fb60  ip : dfc2fb70  fp : dfc2fb6c
[    0.771371] r10: fffffffa  r9 : dfc74bc0  r8 : dfc74bc0
[    0.776571] r7 : 00000000  r6 : dfc74980  r5 : dfc74b80  r4 : c0519c24
[    0.783070] r3 : 00000000  r2 : dfc2faa0  r1 : dfc74c80  r0 : 00000000
[    0.789571] Flags: nzCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment kernel
[    0.796849] Control: 10c5387f  Table: 30004019  DAC: 00000017

从以上错误信息中的PC和LR的值可以看出,程序是执行到dev_driver_string或者max8698_pmic_probe(这两个是函数或者汇编中的标号)符号部分的时候出错了。我们就从这两个符号出发去寻找、思考可能出错的地方然后试图去解决。
 
错误追溯及问题解决
max8698_pmic_probe看名字是max8698这个电源管理IC的驱动安装函数部分出错了,应该是我们的开发板系统中配置了支持这个电源管理IC,于是乎启动时去加载他的驱动,结果驱动在加载执行的过程中出错了OOPS了。
我们为什么要配置支持这个驱动?这个驱动加载为什么要出错?
结合X210开发板的硬件实际情况来分析:X210开发板上根本就没有max8698这个电源管理IC,既然硬件都没有驱动执行了肯定会出错。
回忆当时从三星版本的uboot移植的时候,在uboot的lowlevel_init.S中也有调用个电源管理IC初始化函数(PMIC_init),后来解决的办法就是屏蔽掉了这个函数的调用,uboot就成功运行下去了。
为什么我们的uboot和内核中默认都调用了这个电源管理IC的初始化代码?原因就是三星的SMDKV210开发板中是用了max8698这个电源管理IC的,所以三星的uboot和kernel中都有默认支持这个。但是X210中是没用的,因此都需要去掉。
怎么解决?在uboot中是直接改源代码屏蔽掉那个初始化函数解决的;在内核中不能这么干?因为linux kernel是高度模块化高度可配置的,内核中每一个模块都是被配置项条件编译了的,因此要去掉某个模块的支持,只需要重新配置去掉选项即可,不用改源代码。所以我们的关键就是要找它对应的配置项。
我们做法:make menuconfig,然后/搜索"MAX8698"这几个关键字,然后看到这个配置项的路径,然后到路径下去按N键去掉这个模块的支持,保存,重新编译即可。
实践证明问题被解决了,而且内核再次启动后直接运行到挂载rootfs才出错。
分析及总结
分析:问题究竟是怎么被解决的?涉及哪几个方面
根本原因在于CONFIG_MFD_MAX8698这个配置宏。这个配置宏决定了很多东西
第一:这个配置宏决定了drivers目录下的max8698对应的驱动程序源代码是否被编译
第二:这个配置宏决定了kernel启动过程中是否会调用一些max8698的相关的代码
总结:kernel是高度模块化和可配置化的,所以在内核中做任何事情(添加一个模块、更改一个模块、去掉一个模块)都必须按照内核设定的方案和流程来走。
回到顶部
5.iNand的问题和安排
错误分析
得到的内核错误信息:Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)。从错误信息字面意思来分析,就是内核试图挂载根文件系统时失败,失败的原因是unknown-block(不能识别的块设备)
backstrace分析,可以得知错误信息的来源,再结合之前的内核启动流程分析,就更加确定了出错的地方。
下一个问题:分析这个错误出现的原因。unknown-block(0,0)。在kernel启动时uboot会传给内核一个cmdline,其中用root=xx来指定了rootfs在哪个设备上,内核就会到相应的地方去挂载rootfs。譬如我们传参中:root=/dev/mmcblk0p2,这里的/dev/mmcblk0p2就是rootfs的设备地址,这个设备文件编号的含义就是mmc设备0的第2个分区(设备0就是在SD0通道上的设备,也就是iNand),这里的问题就是没找到mmc设备0的第2分区。
下一步问题:为什么没找到mmc设备0的第2分区。一定是因为kernel启动过程中加载mmc驱动的时候有问题,驱动没有发现mmc设备0.问题定位在MMC相关的驱动方面。
对比九鼎版本的内核启动信息,即可发现我们的内核启动并没有找到MMC设备(内置的iNand和外置的SD卡都没找到),没找到肯定是驱动的问题,这就要去移植MMC驱动了。
问题阐述
SD/iNand本身都是由一个一个的扇区组成的,回忆裸机中讲到的210的启动时,BL1在SD卡的1扇区开始往后存放,SD卡的0扇区是不用的。SD卡的0扇区是用来放置MBR的。
MBR就是用来描述块设备的分区信息的,事先定义了一个通用的数据结构来描述块设备的分区,我们只要按照这个标准将分区信息写入MBR中即可对该设备完成分区。MBR默认就是在块设备的第0个扇区上存放的。
我们内核中读到iNand分4个分区,我们哪里分区的?uboot中有一个命令fdisk -c 0时就对iNand进行了分区。uboot的fdisk命令内部已经写死了iNand的分区表,到内核中时内核直接读取MBR就知道了分区。所以在uboot和内核之间iNand设备的分区信息是靠iNand自己传递的,所以uboot不用给内核传参时传递分区表信息。
如果开发板用的是nandFlash的话,分区表一般是在内核中自己用代码构建的。所以nand版本的内核移植的时候一般都需要去移植更改nand分区表。
解决安排
暂时解决不了这个问题。
后续课程安排
一节课搞定网卡驱动的移植,一节课讲述一些内核移植的小方法和技巧,然后课程结束
整体移植的课程结束,进入根文件系统部分。
回到顶部
6.网卡驱动的移植和添加实验
移植标准
网卡驱动移植ok时,启动信息为:
[ 1.452008] dm9000 Ethernet Driver, V1.31
[ 1.455870] eth0: dm9000c at e08f4300,e08f8304 IRQ 42 MAC: 00:09:c0:ff:ec:48 (platform data)
当前内核中网卡驱动尚未移植,因此内核启动时有错误的打印信息:

[ 1.130308] dm9000 Ethernet Driver, V1.31
[ 1.133113] ERROR : resetting
[ 1.135700] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.140915] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.145941] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.150963] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.155992] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.161018] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.166041] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.171070] dm9000 dm9000.0: read wrong id 0x2b2a2928
[ 1.176092] dm9000 dm9000.0: wrong id: 0x2b2a2928
[ 1.180774] dm9000 dm9000.0: not found (-19).

移植的目标就是让我们的版本的内核可以打印出正确情况下的启动信息,那我们就相信内核启动后网卡是可以工作的。
 
make menuconfig中添加DM9000支持
menuconfig中选择Y
其实这一步本来就是Y,所以在我们这里是不用管的。但是你自己遇到的一个内核可能默认不是Y,因此要设置。
mach-smdkc110.c中逻辑分析
mach-smdkc110.c中的smdkc110_machine_init是整个开发板的所有硬件的初始化函数,在这里加载了的硬件将来启动时就会被初始化,在这里没有的将来启动时就不管。
smdkc110_devices和smdkc110_dm9000_set()这两个地方是和DM9000有关的,要分别去做移植。
smdkc110_dm9000_set这个函数就是DM9000相关的SROM bank的寄存器设置,相当于uboot中dm9000移植时的dm9000_pre_init函数。只是读写寄存器的函数名称不同了。
修改相应的配置参数
DM9000相关的数据配置在arch/arm/plat-s5p/devs.c中更改
在arch/arm/mach-s5pv210/include/mach/map.h中定义了DM9000的IO基地址,和DM9000接在哪个bank有关。
还有+2改成+4,IRQ_EINT9改成10即可。
代码实践
同步代码、编译生成zImage
下载启动后看启动信息。
回到顶部
7.内核启动第一阶段的调试方法
问题点描述
内核启动在head.S中首先进行了三个校验(CPU id的校验、机器码的校验、tag的校验),然后创建页表,然后做了一些不太会出错的事情,然后b start_kernel。基本上能运行到start_kernel内核移植就不太会出问题了。
有时候移植的内核启动后的现象是:根本没有启动信息出来。这时候有可能是内核启动运行了但是运行出错了没启动起来所以没有打印信息;也有可能是内核根本没得以运行。都有可能但是没法确定。我们希望能有一种调试手段来确定问题所在。
 
调试方法和原理
调试方法:在内核启动的第一阶段添加汇编操作led点亮/熄灭的方法来标明代码运行的轨迹。
我们找之前裸机中汇编操作led点亮/熄灭的代码过来,复制粘贴到head.S中合适位置。然后内核启动后根据led的表现来标明代码有无运行。
动手测试
整理好led操作的代码段,在head.S中合适的地方添加led这个函数,然后在head.S的内核起始运行阶段添加调用led函数,然后重新编译内核,运行内核看这段代码有无被运行。
如果被运行了,证明在这个调用led的步骤之前的部分都是没问题的,那么如果有错肯定错误在后边;如果没有被运行则证明错误在之前,那么就要去之前的部分debug。
5、根文件系统原理
目录
1.根文件系统概述
2.根文件系统的形式
3.自己制作ext3格式的根文件系统
4.nfs方式启动自制简易文件夹形式的rootfs
5.到底什么是linuxrc
6.rootfs中还应该有什么
7.VFS简介
 
正文
回到顶部
1.根文件系统概述
为什么需要根文件系统
init进程的应用程序在根文件系统上
根文件系统提供了根目录 ‘ / ’ 
内核启动后的应用层配置(etc目录)在根文件系统上。几乎可以认为:发行版=内核+rootfs
shell命令程序在根文件系统上。譬如ls、cd等命令
总结:一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。
根文件系统的实质是什么
根文件系统是特殊用途的文件系统。
根文件系统也必须属于某种文件系统格式。rootfstype=
究竟文件系统是用来干嘛的。ZnFAT(振南文件系统)
首先,存储设备(块设备,像硬盘、flash等)是分块(扇区)的,物理上底层去访问存储设备时是按照块号(扇区号)来访问的。这就很麻烦。
其次,文件系统是一些代码,是一套软件,这套软件的功能就是对存储设备的扇区进行管理,将这些扇区的访问变成了对目录和文件名的访问。我们在上层按照特定的目录和文件名去访问一个文件时,文件系统会将这个目录+文件名转换成对扇区号的访问。
最后,不同的文件系统的差异就在于对这些扇区的管理策略和方法不同,譬如坏块管理、碎片管理。
回到顶部
2.根文件系统的形式
镜像文件形式
使用专用工具软件制作的可供烧录的镜像文件
镜像中包含了根文件系统中的所有文件
烧录此镜像类似于对相应分区格式化。
镜像文件系统具有一定的格式,格式是内化的,跟文件名后缀是无关的。
文件夹形式
根文件系统其实就是一个包含特定内容的文件夹而已
根文件系统可由任何一个空文件夹添加必要文件构成而成
根文件系统的雏形就是在开发主机中构造的文件夹形式的
总结
镜像文件形式的根文件系统主要目的是用来烧录到块设备上,设备上的内核启动后去挂载它。镜像文件形式的根文件系统是由文件夹形式的根文件系统使用专用的镜像制作工具制作而成的。
最初在开发主机中随便mkdir创建了一个空文件夹,然后向其中添加一些必要的文件(包括etc目录下的运行时配置文件、/bin等目录下的可执行程序、/lib目录下的库文件等···)后就形成了一个文件夹形式的rootfs。然后这个文件夹形式的rootfs可以被kernel通过nfs方式来远程挂载使用,但是不能用来烧录块设备。我们为了将这个rootfs烧录到块设备中于是用一些专用的软件工具将其制作成可供烧录的一定格式的根文件系统镜像。
文件夹形式的rootfs是没有格式的,制作成镜像后就有了一定的rootfs格式了,格式是由我们的镜像制作过程和制作工具来决定的。每一种格式的镜像制作工具的用法都不同。
回到顶部
3.自己制作ext3格式的根文件系统
mke2fs介绍
mke2fs是一个应用程序,在ubuntu中默认是安装了的。这个应用程序就是用来制作ext2、ext3、ext4等格式的根文件系统的。
一般用来制作各种不同格式的rootfs的应用程序的名字都很相似,类似于mkfs.xxx(譬如用来制作ext2格式的rootfs的工具叫mkfs.ext2、用来制作jffs2格式的rootfs的工具就叫mkfs.jffs2)
ubuntu14.04中的mkfs.ext2等都是mke2fs的符号链接而已。
动手制作ext3格式的根文件系统
创建rootfs.ext2文件并且将之挂载到一个目录下方便访问它
//《参考资料:》
dd if=/dev/zero of=rootfs.ext2 bs=1024 count=2048
losetup /dev/loop1 rootfs.ext2
mke2fs -m 0 /dev/loop1 2048
mount -t ext2 /dev/loop1 ./rootfs/
我们向镜像中写入一个普通文件linuxrc。这个文件就会成为我们制作的镜像中的/linuxrc。内核挂载了这个镜像后就会尝试去执行/linuxrc。然后执行时必然会失败。我们将来实验看到的现象就应该是:挂载成功,执行/linuxrc失败。
将来真正去做有用的rootfs时,就要在这一步添加真正可以执行的linuxrc程序,然后还要添加别的/lib目录下的库文件,/etc目录下的配置文件等。
卸载掉,然后镜像就做好了。
umount /dev/loop1
losetup -d /dev/loop1
 
烧录制作的rootfs.ext3
烧录过程参考裸机中第三部分的刷机过程。注意bootargs传参设置
分析和总结
回到顶部
4.nfs方式启动自制简易文件夹形式的rootfs
什么是nfs
nfs(network file system)是一种网络通讯协议,由服务器和客户端构成。
nfs的作用。利用nfs协议可以做出很多直接性应用,我们这里使用nfs主要是做rootfs挂载。开发板中运行kernel做nfs客户端,主机ubuntu中搭建nfs服务器。在主机ubuntu的nfs服务器中导出我们制作的文件夹形式的rootfs目录,则在客户端中就可以去挂载这个文件夹形式的rootfs进而去启动系统。
搭建nfs服务器。
配置内核以支持nfs作为rootfs
设置nfs启动方式的bootargs:setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/rootfs/rootfs ip=192.168.1.7:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off  init=/linuxrc console=ttySAC2,115200 
启动之后出现信息
[    5.567524] VFS: Cannot open root device "nfs" or unknown-block(0,255)
[    5.572671] Please append a correct "root=" boot option; here are the available partitions:
原因是:在menuconfig中没有支持nfs,需要配置支持nfs启动方式
总结
nfs方式启动相当于开发板上的内核远程挂载到主机上的rootfs
nfs方式启动不用制作rootfs镜像
nfs方式不适合真正的产品,一般作为产品开发阶段调试使用
回到顶部
5.到底什么是linuxrc
/linuxrc是一个可执行的应用程序
/linuxrc是应用层的,和内核源码一点关系都没有
/linuxrc在开发板当前内核系统下是可执行的。因此在ARM SoC的linux系统下,这个应用程序就是用arm-linux-gcc编译链接的;如果是在PC机linux系统下,那么这个程序就是用gcc编译连接的。
/linuxrc如果是静态编译连接的那么直接可以运行;如果是动态编译连接的那么我们还必须给他提供必要的库文件才能运行。但是因为我们/linuxrc这个程序是由内核直接调用执行的,因此用户没有机会去导出库文件的路径,因此实际上这个/linuxrc没法动态连接,一般都是静态连接的。
/linuxrc执行时引出用户界面
操作系统启动后在一系列的自己运行配置之后,最终会给用户一个操作界面(也许是cmdline,也许是GUI),这个用户操作界面就是由/linuxrc带出来的。
用户界面等很多事并不是在/linuxrc程序中负责的,用户界面有自己专门的应用程序,但是用户界面的应用程序是直接或者间接的被/linuxrc调用执行的。用户界面程序和其他的应用程序就是进程2、3、4·····,这就是我们说的进程1(init进程,也就是/linuxrc)是其他所有应用程序进程的祖宗进程。
/linuxrc负责系统启动后的配置
就好像一个房子建好之后不能直接住,还要装修一样;操作系统启动起来后也不能直接用,要配置下。
操作系统启动后的应用层的配置(一般叫运行时配置,英文简写etc)是为了让我们的操作系统用起来更方便,更适合我个人的爱好或者实用性。
/linuxrc在嵌入式linux中一般就是busybox
busybox是一个C语言写出来的项目,里面包含了很多.c文件和.h文件。这个项目可以被配置编译成各个平台下面可以运行的应用程序。我们如果用arm-linux-gcc来编译busybox就会得到一个可以在我们开发板linux内核上运行的应用程序。
busybox这个程序开发出来就是为了在嵌入式环境下构建rootfs使用的,也就是说他就是专门开发的init进程应用程序。
busybox为当前系统提供了一整套的shell命令程序集。譬如vi、cd、mkdir、ls等。在桌面版的linux发行版(譬如ubuntu、redhat、centOS等)中vi、cd、ls等都是一个一个的单独的应用程序。但是在嵌入式linux中,为了省事我们把vi、cd等所有常用的shell命令集合到一起构成了一个shell命令包,起名叫busybox。
回到顶部
6.rootfs中还应该有什么
最重要的就是上节课讲过的/linuxrc
dev目录下的设备文件。在linux中一切皆是文件,因此一个硬件设备也被虚拟化成一个设备文件来访问,在linux系统中/dev/xxx就表示一个硬件设备,我们要操作这个硬件时就是open打开这个设备文件,然后read/write/ioctl操作这个设备,最后close关闭这个设备。
在最小rootfs/dev目录也是不可少的,这里面有一两个设备文件是rootfs必须的。
sysproc目录。在最小rootfs中也是不可省略的,但是这两个只要创建了空文件夹即可,里面是没东西的,也不用有东西。这两个目录也是和驱动有关的。属于linux中的虚拟文件系统。
usr是系统的用户所有的一些文件的存放地,这个东西将来busybox安装时会自动生成。
etc目录是很关键很重要的一个,目录中的所有文件全部都是运行时配置文件。/etc目录下的所有配置文件会直接或者间接的被/linuxrc所调用执行,完成操作系统的运行时配置。etc目录是制作rootfs的关键,所以后面下一个课程专门讲这个etc目录。
lib目录也是rootfs中很关键的一个,不能省略的一个。lib目录下放的是当前操作系统中的动态和静态链接库文件。我们主要是为了其中的动态链接库。
回到顶部
7.VFS简介
什么是VFS
VFS是linux内核的一种设计理念、设计机制。VFS就是vitrual file system,叫虚拟文件系统。
具体的一些文件系统如FAT、NTFS、ext2、ext3、jffs2、yaffs2、ubi等主要设计目的是为了管理块设备(硬盘、Nand···)
VFS是借鉴了文件系统的设计理念(通过文件系统将底层难以管理的物理磁盘扇区式访问,转换成目录+文件名的方式来访问),将硬件设备的访问也虚拟化成了对目录+文件的访问。所以有了VFS后我们可以通过设备文件(目录+文件名,譬如/dev/mmcblk0p2)的方式来访问系统中的硬件设备。
以上可以初步看出VFS的一些厉害之处,但是VFS不止于此。
VFS的意义
上面说过的对硬件设备的访问,将对硬件设备的访问和对普通文件的访问给接口统一化了(linux中一切届是文件)。
将操作系统上层(应用层)对下层不同文件系统类型的访问细节给屏蔽掉了。因此如果没有VFS那我们写cp命令(其他命令也一样)的时候就不得不去考虑你cp的这个文件在什么文件系统类型下。所以cp命令非常复杂,因此要考虑具体的文件系统类型。有了VFS后情况就不同了。VFS成了一个隔离层,隔离了下层的不同文件系统的差异性,对上层应用提供一个统一的接口。
VFS将不同文件系统和下层硬件设备(块设备)驱动之间的细节也给屏蔽了。不同类型的文件系统在本身设计时是不用考虑各种不同的硬件设备的具体操作差异的,这里有一个类似于VFS的设计理念。
VFS和我们学习的关系
VFS机制和rootfs挂载,和其他文件系统的挂载都是有关联的。
内核中有一些sys proc这种虚拟文件系统,这东西也是和VFS机制有关。
/dev/目录下的设备文件都和VFS有关,所以学习驱动绕不开VFS。
 

更多推荐

话说linux内核连载1

本文发布于:2024-02-05 09:08:18,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1674012.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:内核   linux

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!