ARM-Linux内核启动分析

ARM Linux启动过程分析

  1. 引 言
    Linux
    最初是由瑞典赫尔辛基大学的学生LinusTorvalds1991年开发出来的,之后在 GNU的支持下,Linux获得了巨大的发展。虽然Linux在桌面 PC机上的普及程度远不及微软的 Windows操作系统,但它的发展速度之快、用户数量的日益增多,也是微软所不能轻视的。而近些年来Linux在嵌入式领域的迅猛发展,更是给Linux注入了新的活力。

一个嵌入式Linux系统从软件角度看可以分为四个部分:

1:引导加载程序(bootloader2:linux内核 3:文件系统 4:应用程序


其中bootloader是系统启动或复位以后执行的第一段代码,它主要用来初始化处理器以及外设,然后调用linux内核。linux内核在完成系统的初始化之后需要挂载某个文件系统作为根系统(rootfilesystem)。根文件系统是linux系统的核心组成部分,它可以作为linux系统中文件和数据的存储区域,通常它还包括系统配置文件和应用程序软件所需要的库。应用程序可是说是嵌入式系统的“灵魂“

它所实现的功能通常就是设计该嵌入式系统所要的目标,如果没有应用程序的支持,任何硬件上设计精良的嵌入式系统都没有意义。


从以上分析我们可以看到bootloaderlinux内核在嵌入式系统中的关系和作用。bootloader在运行过程中虽然具有初始化系统和执行用户输入的命令等作用,但它最根本的功能就是为了启动linux内核。在嵌入式系统开发的过程中,很大一部分精力都是花在bootloaderLinux内核的开发或移植上。如果能清楚的了解bootloader执行流程和Linux的启动过程,将有助于明确开发过程中所需的工作,从而加速嵌入式系统的开发过程。而这正是本文的所要研究内容


Bootloader


1bootlader的概念和作用。Bootloader是嵌入式系统的引导加载程序,它是系统上电后运行的第一段小程序。器作用类似于PC机上的BIOS。在完成对系统的初始化任务之后,它会将非易失性存储器,它会将非易失性存储器(通常是FlashDOC)中的linux内核拷贝到RAM中去,然后跳到内核的第一条指令出继续执行,从而启动linux内核。由此可见,bootloaderlinux内核有着密不可分的联系,要想清楚的了解linux内核的启动过程,我们必须先的认识bootloader的执行过程,这样才能对嵌入式系统的整个启动过程有个清晰的了解。


2bootloader的执行过程不同的处理器上电或复位后执行的第一条指令地址并不相同,对于RAM处理器来说,该地址为0*00000000。对于一般的嵌入式系统,通常把flash等非易失性存储器映射到这个地址处,而bootloader就位于该存储器的最前端,所以系统上电或复位后执行的第一段程序便是bootloader。而因为存储器bootloader的存储器不同,bootloader的执行过程也并不相同。


嵌入式系统中广泛采用的非易失性存储器通常是Flash,而Flash又分为 NorFlash NandFlash 两种。 它们之间的不同在于:NorFlash 支持芯片内执行(XIPeXecuteIn Place),这样代码可以在Flash上直接执行而不必拷贝到RAM中去执行。而NandFlash并不支持XIP,所以要想执行NandFlash 上的代码,必须先将其拷贝到RAM中去,然后跳到RAM中去执行。实际应用中的bootloader根据所需功能的不同可以设计得很复杂,除完成基本的初始化系统和调用Linux内核等基本任务外,还可以执行很多用户输入的命令,比如设置Linux启动参数,给 Flash分区等;也可以设计得很简单,只完成最基本的功能。但为了能达到启动Linux内核的目的,所有的bootloader都必须具备以下功能[2]


1:初始化RAM

因为linux内核一般都会在RAM中运行,所以在调用linux内核之前boodloader必须设置和初始化RAM,为调用linux内核做好准备,初始化RAM的任务包括CPU的控制寄存器参数,以便能正常使用RAM以及检测RAM大小等。


2:初始化串口:

串口在linux的启动过程中有着非常重要的作用,它是linux内核和用户交互的方式之一。linux在启动过程中可以将信息通过串口输出,这样便可清楚的了解linux的启动过程。虽然它并不是bootloader必须要完成的工作,但是通过串口输出信息是调试bootloaderlinux内核的强有力的工具所以一般的bootloader都会在执行过程中初始化一个串口作为调试端口。


3:检测处理器类型

bootloaer在调用linux内核前必须检测系统的处理器类型,并将其保存到某个常量中提供给linux内核。Linux内核在启动过程中会根据该处理器类型调用相应的初始化程序。


4:设置linux启动参数

bootloader在执行过程中必须设置和初始化linux的内核启动参数。目前传递启动参数主要采用两种方式:即通过structparam_struct structtag(标记列表,taggedlist)两种结构传递。structparam_struct 是一种比较老的参数传递方式,在2.4版本以前的内核中使用较多。从 2.4版本以后 Linux内核基本上采用标记列表的方式。但为了保持和以前版本的兼容性,它仍支持structparam_struct 参数传递方式,只不过在内核启动过程中它将被转换成标记列表方式。
标记列表方式是种比较新的参数传递方式,它必须以ATAG_CORE开始,并以ATAG_NONE结尾。中间可以根据需要加入其他列表。Linux内核在启动过程中会根据该启动参数进行相应的初始化工作。


5:调用linux内核映像

bootloader完成的最后一项工作便是调用linux内核。如果linux内核存放在flash中,并且可直接在上面运行(这里的FlashNorFlash),那么可直接跳转到内核中去执行,但由于在flash中执行代码执行代码会有种种限制,而且速度也远不及RAM快,所以一般的嵌入式系统都是将linux内核拷贝到RAM中,然后跳转到RAM中去执行。不论那种情况,在在跳到Linux内核执行之前CUP的寄存器必须满足以下条件:r00r1=处理器类型,r2=标记列表在RAM中的地址


三:linux内核的启动过程


bootloaderlinux内核映像拷贝到RAM以后,可以通过下例代码启动linux内核:

call_linux(0,machine_type, kernel_params_base)

其中,machine_tpyebootloader检测出来的处理器类型,kernel_params_base是启动参数在 RAM的地址。通过这种方式将 Linux启动需要的参数从 bootloader传递到内核。

linux内核有两种映像:一种是非压缩内核,叫Image,另一种是它的压缩版本,叫zImage。根据内核映像的不同,linux内核的启动在开始阶段也有所不同。zImageImage经过压缩形成的,所以它的大小比Image小。但为了能使用zImage,必须在它的开头加上解压缩的代码,将zImage解压缩之后才能执行,因此它的执行速度比Image要慢。但考虑到嵌入式系统的存储空容量一般比较小,采用zImage可以占用较少的存储空间,因此牺牲一点性能上的代价也是值得的。所以一般的嵌入式系统均采用压缩内核的方式。

对于ARM系列处理器来说,zImage的入口程序即为arch/arm/boot/compressed/head.s。它依次完成一下工作:开启MMUcache,调用decompress_kernel()解压内核,最后通过调用call_kernel()进入非压缩内核Image的启动。下面将具体分析在此之后Linux内核的启动过程。

2linux内核入口

Linux非压缩内核的入口位于文件/arch/arm/kernel/head-armv.S中的 stext段。该段的基地址就是压缩内核解压后的跳转地址。如果系统中加载的内核是非压缩的Image,那么bootloader将内核从Flash中拷贝到RAM后将直接跳到该地址处,从而启动Linux内核。不同体系结构的 Linux系统的入口文件是不同的,而且因为该文件与具体体系结构有关,所以一般均用汇编语言编写[3]。对基于ARM处理的 Linux系统来说,该文件就是head-armv.S。该程序通过查找处理器内核类型和处理器类型调用相应的初始化函数,再建立页表,最后跳转到start_kernel()函数开始内核的初始化工作。
检测处理器内核类型是在汇编子函数__lookup_processor_type中完成的。通过以下代码可实现对它的调用:bl__lookup_processor_type__lookup_processor_type调用结束返回原程序时,会将返回结果保存到寄存器中。其中r8保存了页表的标志位,r9保存了处理器的 ID号,r10保存了与处理器相关的struproc_info_list结构地址。
检测处理器类型是在汇编子函数__lookup_architecture_type中完成的。与__lookup_processor_type类似,它通过代码:“bl__lookup_processor_type”来实现对它的调用。该函数返回时,会将返回结构保存在r5r6r7三个寄存器中。其中 r5保存了 RAM的起始基地址,r6保存了 I/O基地址,r7保存了I/O的页表偏移地址。当检测处理器内核和处理器类型结束后,将调用__create_page_tables子函数来建立页表,它所要做的工作就是将RAM基地址开始的 4M空间的物理地址映射到 0xC0000000开始的虚拟地址处。对笔者的 S3C2410开发板而言,RAM连接到物理地址 0x30000000处,当调用 __create_page_tables结束后 0x300000000x30400000物理地址将映射到0xC00000000xC0400000虚拟地址处。

当所有的初始化结束之后,使用如下代码来跳到C程序的入口函数start_kernel()处,开始之后的内核初始化工作:
bSYMBOL_NAME(start_kernel)


3start_kernel函数

start_kernel是所有的linux平台进入系统内核初始化的入口函数,它主要完成剩余的与硬件平台相关的初始化工作,在进行一系列与内核相关的初始化之后,调用第一个用户进程—init进程并等待用户进程的执行,这样整个linux内核便启动完毕该函数所做的具体工作有[4][5]

1)
调用setup_arch()函数进行与体系结构相关的第一个初始化工作;
对不同的体系结构来说该函数有不同的定义。对于ARM平台而言,该函数定义在arch/arm/kernel/Setup.c。它首先通过检测出来的处理器类型进行处理器内核的初始化,然后通过bootmem_init()函数根据系统定义的meminfo结构进行内存结构的初始化,最后调用paging_init()开启MMU,创建内核页表,映射所有的物理内存和IO空间。
2)
创建异常向量表和初始化中断处理函数;
3)
初始化系统核心进程调度器和时钟中断处理机制;
4)
初始化串口控制台(serial-console);

ARM-Linux在初始化过程中一般都会初始化一个串口做为内核的控制台,这样内核在启动过程中就可以通过串口输出信息以便开发者或用户了解系统的启动进程。
5)
创建和初始化系统cache,为各种内存调用机制提供缓存,包括;动态内存分配,虚拟文件系统(VirtualFileSystem)及页缓存。
6)
初始化内存管理,检测内存大小及被内核占用的内存情况;
7)
初始化系统的进程间通信机制(IPC);
当以上所有的初始化工作结束后,start_kernel()函数会调用rest_init()函数来进行最后的初始化,包括创建系统的第一个进程-init进程来结束内核的启动。Init进程首先进行一系列的硬件初始化,然后通过命令行传递过来的参数挂载根文件系统。最后init进程会执行用户传递过来的“init=”启动参数执行用户指定的命令,或者执行以下几个进程之一:
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init)

当所有的初始化工作结束后,cpu_idle()函数会被调用来使系统处于闲置(idle)状态并等待用户程序的执行。至此,整个Linux内核启动完毕。


4.结论
Linux
内核是一个非常庞大的工程,经过十多年的发展,它已从从最初的几百KB大小发展到现在的几百兆。清晰的了解它执行的每一个过程是件非常困难的事。但是在嵌入式开发过程中,我们并不需要十分清楚linux的内部工作机制,只要适当修改 linux内核中那些与硬件相关的部分,就可以将linux移植到其它目标平台上。通过对 linux的启动过程的分 析,我们可以看出哪些是和硬件相关的,哪些是linux内核内部已实现的功能,这样在移植linux的过程中便有所针对。而 linux内核的分层设计将使linux的移植变得更加容易。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值