uclinux启动过程详细分析

嵌入式操作系统

57人已加入

描述

  uclinux表示micro-control linux.即“微控制器领域中的Linux系统”,是Lineo公司的主打产品,同时也是开放源码的嵌入式Linux的典范之作。uCLinux主要是针对目标处理器没有存储管理单元MMU(Memory Management Unit)的嵌入式系统而设计的。它已经被成功地移植到了很多平台上。由于没有MMU,其多任务的实现需要一定技巧。

  uClinux启动过程

  uCinux的启动主要经历三个阶段。首先,必须完成CPU和存储器的硬件初始化,在系统RAM中建立程序堆栈和数据段,建立程序的运行时的环境。初始化完成之后,uClinux内核就取得了CPU的控制权,开始操作系统自身的初始化,这包括建立RAM中断矢量表、加载设备驱动程序、内存管理模块等等。这一切完成后,uClinux启动一个最初的init线程,进入到第三阶段,这时内核已经正常运行,外围模块也都就绪,开始执行一些脚本文件(如/etc/rc脚本文件)。

  一.kernel代码段之前的系统初始化

  1. uClinux-dist/linux-2.4.x/arch/armnommu/boot/compressed/head.S

  开发板从上电开始,最开始执行的程序放在uClinux-dist/linux-2.4.x/arch/armnommu/boot/compressed/head.S中。

  (1) 切换模式,关闭中断。 (line 96 )

  (2) 首先程序要先给SYSCFG,EXTDBWTH,ROMCON0等一系列系统控制寄存器赋值,此时flash地址在 0X0,DRAM地址在0X1000000.(line 141 )

  (3) 点亮I/O口的指示灯。 (line 152 )

  (4) 把在flash上的image复制到DRAM上。(line 161 )

  (5) 执行remap,把flash地址映射为0X1000000,DRAM地址映射为0.(line 172 )

  (6) 打开cache和write buffer.(line 196 )

  (7) 设置好64K堆栈。(line 204 )

  (8) 跳转到decompress_kernel函数(line 217 ),此处的跳转为带返回的跳转,以便于执行完此函数跳转回来。

  2. uClinux-dist/linux-2.4.x/arch/armnommu/boot/compressed/misc.c

  此时的函数decompress_kernel是用C语言写的,line 297 。

  (1) makecrc();进行crc校验。

  (2) puts(“Uncompressing Linux.。.”); 输出linux起动后的第一句话。

  (3) gunzip();解压缩kernel.

  (4) puts(“ done, booting the kernel./n”);

  3. uClinux-dist/linux-2.4.x/arch/armnommu/boot/compressed/head.S

  执行完decompress_kernel函数后,kernel又跳转回head.S中,因为此时我们还要检验解压缩之后的kernel起始地址是否紧接着kernel image,如果是,beq call_kernel(line 220),执行解压后的kernel.

  如果解压缩之后的kernel起始地址不是紧接着kernel image,执行relocate(line 236),将其拷贝到紧接着kernel image的地方,然后跳转,执行解压后的kernel.

  二.kernel执行

  1.uClinux-dist/linux-2.4.x/init/main.c中的start_kernel() (line 352)

  系统启动过程到此,转入体系结构无关的通用C代码中,start_kernel() 中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。

  (1) 输出Linux版本信息(printk(linux_banner))

  (2) 设置与体系结构相关的环境(setup_arch())

  (3) parse_options(command_line);解析command_line,将其转化为环境变量。

  (4) 初始化系统IRQ(init_IRQ())

  (5) 核心进程调度器初始化(sched_init())

  (6) 软中段初始化softirq_init();

  (7) 时间、定时器初始化(包括估测主频、初始化定时器中断等,time_init())

  (8) 控制台初始化console_init();

  (9) 核心CACHE初始化kmem_cache_init();

  (10)延迟校准calibrate_delay();

  (11)内存初始化(设置内存上下界和页表项初始值,mem_init())

  (12)文件,目录,块设备读写缓冲区初始化

  (13)检查体系结构漏洞(check_bugs())

  (14)启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init())

  至此start_kernel()结束,基本的核心环境已经建立起来了。

  2.uClinux-dist/linux-2.4.x/init/main.c中的init() (line 548)

  现在我们进入内核引导第二部分,init()函数作为核心线程,首先锁定内核(仅对SMP机器有效,我们为空函数),然后调用 do_basic_setup() (line 551)完成外设及其驱动程序的加载初始化。

  过程如下:

  * 网络初始化(初始化网络数据结构,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,将调用protocols结构中包含的所有协议的初始化过程,sock_init())

  * 创建事件管理核心线程(start_context_thread()函数,这是系统创建的第二个内核线程,名叫“keventd”。其代码context_thread()也在kernel/context.c中,)

  启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,此时由do_initcalls()函数启动)。

  此时系统开始加载外部设备的初始化程序,如:在linux-2.4.x/driver/block/genhd.c中的device_init()函数,在genhd.c中由__initcall(device_init)标识在此时调用,device_init()函数是所有外部设备初始化的总入口,包括了块设备的初始化blk_dev_init,网络设备的初始化net_dev_init()和atmdev_init()等。

  至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开/dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。(line 576) 。

  init()函数到此结束,内核的引导部分也到此结束了,

  3. uClinux-dist/linux-2.4.x/init/main.c中的execve(“/etc/init”,argv_init,envp_init); (line 579)

  init进程是系统所有进程的起点,内核在完成核内引导以后,即在本线程(进程)空间内加载init程序,它的进程号是1。

  init程序需要读取/vendors/SAMSUNG/4510B/inittab文件作为其行为指针,然后执行。

  4.系统执行rc脚本。

  hostname Samsung

  /bin/expand /etc/ramfs.img /dev/ram0

  /bin/expand /etc/ramfs2048.img /dev/ram1

  mount -t proc proc /proc

  mount -t ext2 /dev/ram0 /var

  mount -t ext2 /dev/ram1 /ramdisk

  chmod 777 /ramdisk

  mkdir /var/config

  mkdir /var/tmp

  mkdir /var/log

  mkdir /var/run

  mkdir /var/lock

  ifconfig lo 127.0.0.1

  route add -net 127.0.0.0 netmask 255.255.255.0 lo

  dhcpcd &

  cat /etc/motd

  rc程序执行完毕后,系统环境已经设置好了,下面就该用户登录系统了。

  5.运行Sash command shell

  uclinux启动的详细过程有着诸多的信息可以给我们巨大的启发,我们在这里讨论的就是要对这些信息做一个具体细致的分析,通过我们的讨论,大家会对uclinux启动过程中出现的、以前感觉熟悉的、但却又似是而非的东西有一个确切的了解,并且能了解到这些输出信息的来龙去脉。

  uclinux的启动过程,它是一幅缩影图,对它有了一个详细的了解后,有助于指导我们更加深入地了解uclinux的核心。

  大家对uclinux的启动应该都比较熟悉,作为一名嵌入系统开发者,你一定遇到过下面的情景:在某论坛上看到一篇帖子,上面贴着uclinux开发板启动时的一堆信息,然后大家在帖子里讨论着这个启动过程中出现的问题,随机举例如下:

  Linux version 2.4.20-uc1 (root@Local) (gcc version 2.95.3

  20010315 (release)(ColdFire patches - 20010318 from http://f

  (uClinux XIP and shared lib patches from http://www.snapgear.com/)) #20 三 6月 1

  8 00:58:31 CST 2003

  Processor: Samsung S3C4510B revision 6

  Architecture: SNDS100

  On node 0 totalpages: 4096

  zone(0): 0 pages.

  zone(1): 4096 pages.

  zone(2): 0 pages.

  Kernel command line: root=/dev/rom0

  Calibrating delay loop.。. 49.76 BogoMIPS

  Memory: 16MB = 16MB total

  Memory: 14348KB available (1615K code, 156K data, 40K init)

  Dentry cache hash table entries: 2048 (order: 2, 16384 bytes)

  Inode cache hash table entries: 1024 (order: 1,

  Mount-cache hash table entries: 512 (order: 0, 4096 bytes)

  Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes)

  Page-cache hash table entries: 4096 (order: 2, 16384 bytes)

  POSIX conformance testing by UNIFIX

  Linux NET4.0 for Linux 2.4

  Based upon Swansea University Computer Society NET3.039

  Initializing RT netlink socket

  Starting kswapd

  Samsung S3C4510 Serial driver version 0.9 (2001-12-27) with no serial options en

  abled

  ttyS00 at 0x3ffd000 (irq = 5) is a S3C4510B

  ttyS01 at 0x3ffe000 (irq = 7) is a S3C451

  Blkmem copyright 1998,1999 D. Jeff Dionne

  Blkmem copyright 1998 Kenneth Albanowski

  Blkmem 1 disk images:

  0: BE558-1A5D57 [VIRTUAL BE558-1A5D57] (RO)

  RAMDISK driver initialized: 16 RAM disks of 1024K size 1024 blocksize

  Samsung S3C4510 Ethernet driver version 0.1 (2002-02-20) 《mac@os.nctu.edu.tw》

  eth0: 00:40:95:36:35:34

  NET4: Linux TCP/IP 1.0 for NET4.0

  IP Protocols: ICMP, UDP, TCP

  IP: routing cache hash table of 512 buckets, 4Kbytes

  TCP: Hash tables configured (established 1024 bind 1024)

  VFS: Mounted root (romfs

  Freeing init memory: 40K

  上面的这些输出信息,也可能包括你自己正在做的uclinux开发板的输出信息,其中的每一行,每一个字的含义,你是否深究过,或者说大部分的含义你能确切地知道的?本人想在这里结合本人在实践中一些体会来和广大uclinux的开发者一起读懂这些信息。

  我们在这里将以一个真实的uclinux系统的启动过程为例,来分析这些输出信息。启动信息的原始内容将用标记标出,以区别与注释。

  uclinux的启动主要分为两个阶段:

  ① 第一部分bootloader启动阶段

  ② ② 第二部分linux 内核初始化和启动阶段

  第一节:start_kernel

  第二节:用户模式( user_mode )开始,start_kernel结束

  第三节:加载linux内核完毕,转入cpu_idle进程

  第一部分 : bootloader启动

  图 1:uclinux启动状态转移示意图

  *****************************************************

  Boot loader v0.12NOTE: this boot loader is designed to boot kernels made with the2.4.xx releasesbootloader for XVBuilt at Nov 20 2005 10:12:35

  Bootloader头信息,版本,编译时间等,这个因不同的bootloader的设计而有所不同,由此你能看出bootloader的版本信息,有很多使用的是通用的bootloader,如u-boot,redboot等。

  Loaded to 0x90060000

  将bootloader加载到内存ram中的0x90060000处,即将bootloader加载到内存的高端地址处。

  Linux内核将被bootloader加载到0x90090000处。

  Found boot configuration

  查找到了启动boot的配置信息。

  Booted from parallel flash

  从flash中启动代码,此处的flash为并行闪存。

  注意:任何flash器件的写入操作只能在空或已擦除的单元内进行,所以大多数情况下,在进行写入操作之前必须先执行擦除。NAND器件执行擦除操作是十分简单的,而NOR则要求在进行擦除前先要将目标块内所有的位都写为0。

  从上面的信息,我们可以对flash类型特点有个比较明确的了解。

  CPU clock rate: 200 MHz

  开发板上所使用的CPU的主频为200MHZ。

  DRAM size is 128MB (128MB/0MB)

  动态内存ram大小为128M。

  在嵌入式系统中使用DRAM内存的设计比较广泛。

  在uclinux的系统中,系统运行时间较长后,会出现内存碎片的问题,导致再分配大块内存时会失败。这是在uclinux系统中经常遇到的问题,解决的办法通常有使用静态内存、应用程序启动时预先分配大内存、使用内存池等。

  地址辅助说明:

  先说明一下内存地址数字情况,主要是为了方便记忆。

  可以访问的内存为4G。0x40000000是1GB处;0x00040000是256K处,0x00020000是128K处,0x90000000是2GB多的地方。1M-》0x00100000, 2M-》0x00200000,8M-》0x00800000,16M-》0x01000000, 32M-》0x02000000,256M-》0x10000000,64K-》0x00010000,注意:rootfs并不是一个具体的文件系统类型,如jffs。它只是一个理论上的概念。在具体的嵌入系统实例中,可以将某种具体的文件系统设置为根文件系统rootfs,如我们可以设置romfs为根文件系统,也可以设置jffs为根文件系统。

  这里的ROMFS只读文件系统只是一种具体的文件系统类型,也是在嵌入系统中经常使用到的类型。

  看完了上面的内容,以后你对出现的类似“kernel Panic:VFS:Unable to mount root fs on 0:00”的含义应该已经了解了。其中“VFS:”就是虚拟文件系统管理器操作时的输出信息了。

  File linux.bin.gz found

  linux kernel内核文件名,它是在只读文件系统romfs上的一个组成部分。

  Unzipping image from 0x4639DE60 to 0x90090000, size = 1316021

  将romfs中的linux kernel解压缩到0x90090000,之后会从这个内存地址启动内核。romfs为压缩格式文件,使用压缩的只读文件系统,是为了保持制作出来的整 个系统所占用的flash空间减小。这个内核的大小为1.3M左右,这也是目前大多数嵌入系统所使用的方法。

  Inptr = 0x00000014(20)Inflating……

  释放……

  Outcnt = 0x0030e7c8(3205064)Final Inptr = 0x001414ad(1316013)Original CRC = 0xcbd73adbComputed CRC = 0xcbd73adb

  做释放后的CRC检查。

  Boot kernel at 0x90090000 with ROMFS at 0x46040000

  kernel已经被从romfs中释放到内存地址0x90090000处,可以跳转到此处启动kernel了,这里是指定的kernel的起始地址。

  Press ‘enter’ to boot

  系统等待启动,后面将看到linux kernel的启动过程了。

  4K-》0x00001000这个是个快速记忆的方法,你可以根据地址中1的位置和其后0的个数来快速知道换算后的地址是在多少兆的地方。比如,1的后面5个0,代表1M的大小,6个0,代表16M,以此类推。

  ROMFS found at 0x46040000, Volume name = rom 43f291aa

  romfs,只读文件系统所在的地址为:0x46040000 (flash映射后的第3分区)。卷名为rom。

  romfs 和rootfs概念上有所区别。flash在内存中的的起始地址为0x46000000,而ROMFS在flash分区上的起始位置为0x00040000,所以ROMFS在内存地址中的位置就为0x46040000。这个细节的部分可以参考flash分区时的地方,Creating 3 MTD partitions。

  romfs中包括kernel和app应用,不包括bootloader和firmware信息头。romfs只读文件系统里的内容有很多种分类方法,我们可以将kernel和app同时放里面,作为根文件系统下的一个文件,也可以在flash上另外划分区域来分别存放。

  *****************************************************************************

  第一节:start_kernel

  Linux的源代码可以从 www.kernel.org 得到,或者你可以查看linux代码交叉引用网站:http://lxr.linux.no/ 进行在线的代码查看,这是一个很好的工具网站。

  在start_kernel中将调用到大量的init函数,来完成内核的各种初始化。如:

  图 2:kernel start up初始化过程

  具体内容可以参考[http://lxr.linux.no/source/init/main.c]

  Linux version 2.4.22-uc0 (root@local) (gcc version 2.95.3 20010315 (release)) #33 。?1…… 20 12:09:106

  上面的代码输出信息,是跟踪linux代码分析后得到的,进入init目录下的main.c的start_kernel启动函数。

  uclinux使用的是linux内核版本为2.4.22。linux source code代码中start_kernel中输出的linux_banner信息。这个信息是每个linux kernel都会打印一下的信息,如果你没有把这句去掉的话。

  Found bootloader memory map at 0x10000fc0.

  bootloader经过内存映射后的地址为:0x10000fc0, 按上面的地址换算方法,1后面有7个0,那么虚拟地址256M左右处。

  Processor: ARM pt110 revision 0

  pT110是ARM微处理器arm核的一种,另一种为pT100。此处为显示ARM的类型。

  On node 0 totalpages: 20480

  zone(0): 20480 pages.

  zone(0): Set minimum memory threshold to 12288KB

  Warning: wrong zone alignment (0x90080000, 0x0000000c, 0x00001000)

  zone(1): 0 pages.

  zone(2): 0 pages.

  预留内存大小,在节点0上总共20页, zone(0) 设置最小内存为12MB, zone(1)和zone(2)为0页。警告:对齐不正确。

  Kernel command line: root=/dev/mtdblock3

  Kernel 启动命令设为:/dev/mtdblock3(在后面的说明中会看到mtdblock3是指的flash上的romfs分区。),用来指定根文件系统所在的位置,kernel会将块设备mtdblock3当作文件系统来处理。也就是说,内核会根据上面的kernel命令行,知道只读文件系统romfs将是 根文件系统rootfs。

  start_kernel(void) 中输出的上面的这句信息。这行命令是在linux内核启动过程中都会输出的一句。

  Console: colour dummy device 80x30

  代码中console_init()的输出信息, 显示控制台属性:一般使用VGA text console,标准是80 X 25行列的文本控制台,这里是对属性进行了设置。

  serial_xx: setup_console @ 115

  串口设置值为115200,此为波特率输出信息。对串口设置的信息做一个打印的动作,在调试时会非常有用。

  Calibrating delay loop…… 82.94 BogoMIPS Calibrate:校准, 进入时延校准循环。检查CPU的MIPS(每秒百万条指令),Bogo是Bogus(伪)的意思。这里是对CPU进行一个实时测试,来得到一个大体的MIPS数值。

  上面这个输出,在所有的linux系统启动中都会打印出来。

  进入内存初始化:mem_init(void), [arch/i386/mm/init.c]

  Memory: 80MB = 80MB totalMemory: 76592KB available (1724K code, 2565K data, 72K init)

  当前内存使用情况,将列出总的内存大小, 及分配给内核的内存大小:包括代码部分,数据部分,初始化部分,总共刚好4M。请留意此处的内核的内存大小的各个值。

  进入虚拟文件系统VFS初始化:vfs_caches_init()

  Dentry cache hash table entries: 16384 (order: 5, 131072 bytes)

  Inode cache hash table entries: 8192 (order: 4, 65536 bytes)

  Mount cache hash table entries: 512 (order: 0, 4096 bytes)

  Buffer cache hash table entries: 4096 (order: 2, 16384 bytes)

  Page-cache hash table entries: 32768 (order: 5, 131072 bytes)

  在内存中建立各个缓冲hash表,为kernel对文件系统的访问做准备。

  VFS(virtual filesystem switch)虚拟文件切换目录树有用到类似这样的结构表。

  上面的输出信息,在一般的linux启动过程中都会看到。

  POSIX conformance testing by UNIFIX

  conformance:顺应, 一致。即POSIX适应性检测。UNIFIX是一家德国的技术公司,Linux 原本要基于 POSIX.1 的, 但是POSIX 不是免费的, 而且 POSIX.1 证书相当昂贵。 这使得 Linux 基于 POSIX 开发相当困难。 Unifix公司(Braunschweig, 德国) 开发了一个获得了 FIPS 151-2 证书的 Linux 系统。 这种技术用于 Unifix 的发行版 Unifix Linux 2.0 和 Lasermoon的 Linux-FT。

  在2.6的内核中就将上面的这句输出给拿掉了。

  *************************************************************************

  第二节:用户模式( user_mode )开始,start_kernel结束

  图 3:用户模式初始化

  PCI: bus0: Fast back to back transfers disabled

  PCI: Configured XX as a PCI slave with 128MB PCI memory

  PCI: Each Region size is 16384KB

  PCI: Reserved memory from 0x10080000 to 0x15080000 for DMA and mapped to 0x12000000

  设备的初始化 init()——》do_basic_init()——》pci_init(),初始化PCI,检测系统的PCI设备。

  Linux NET4.0 for Linux 2.4Based upon Swansea University Computer Society NET3.039

  英国威尔士,斯旺西大学的NET3.039, TCP/IP 协议栈。

  此信息,在linux启动过程中都会出现。

  Initializing RT netlink socket

  对Socket的初始化, socket_init(),Netlink 一种路由器管理协议(linux-2.4.22/net/core/Rtnetlink.c,Routing netlink socket interface: protocol independent part。 其中RT是route路由的意思。这句输出是在create产生rtnetlink的socket套接字时的一个调试输出。)

  此信息,在linux启动过程中都会出现。

  Starting kswapd

  启动交换守护进程kswapd,进程IO操作例程kpiod。

  kswapd 可以配合kpiod运行。进程有时候无事可做,当它运行时也不一定需要把其所有的代码和数据都放在内存中。这就意味着我们可以通过把运行中程序不用的内容切换到交换分区来更好的是利用内存。大约每隔1秒,kswapd醒来并检查内存情况。如果在硬盘的东西要读入内存,或者内存可用空间不足,kpiod就会 被调用来做移入/移出操作。kswapd负责检查,kpiod负责移动。

  Journalled Block Device driver loaded

  加载日志块设备驱动。

  日志块设备是用来对文件系统进行日志记录的一个块设备。日志文件系统是在传统文件系统的基础上,加入文件系统更改的日志记录。

  它 的设计思想是:跟踪记录文件系统的变化,并将变化内容记录入日志。日志文件系统在磁盘分区中保存有日志记录,写操作首先是对记录文件进行操作,若整个写操作由于某种原因(如系统掉电)而中断,系统重启时,会根据日志记录来恢复中断前的写操作。在日志文件系统中,所有的文件系统的变化都被记录到日志,每隔一定时间,文件系统会将更新后的元数据及文件内容写入磁盘。在对元数据做任何改变以前,文件系统驱动程序会向日志中写入一个条目,这个条目描述了它将要做些 什么,然后它修改元数据。

  devfs: v1.12c (20020818) Richard Gooch (rgooch@atnf.csiro.au)devfs: boot_options: 0x1

  Devfs模块的输出信息。设备文件系统devfs,版本1.12c

  pty: 256 Unix98 ptys configured

  Pty模块的输出信息,与控制台操作有关的设置。

  将通过 devpts 文件系统使用 Unix98 PTYs,(Pseudo-ttys (telnet etc) device是伪ttys设备的缩写。

  ① TTY(/dev/tty)是TeleTYpe的一个老缩写,为用户输入提供不同控制台的设备驱动程序。它的名字来源于实际挂接到UNIX系统的、被称为电传打字机(teletype)的终端。在Linux下,这些文件提供对虚拟控制台的支持,可以通过按<Alt-F1>到<Alt -F6>键来访问这些虚拟控制台。这些虚拟控制台提供独立的、同时进行的本地登录对话过程② ttys(/dev/ttys)是计算机终端的串行接口。/dev/ttyS0对应MS-DOS下的 COM1。

  使 用 make dev脚本MAKEDEV来建立pty文件。这样系统内核就支持Unix98风格的pty了。在进行Telnet登录时将要用到/dev/pty设备。 pty是伪终端设备,在远程登录等需要以终端方式进行连接,但又并非真实终端的应用程序中必须使用这种设备,如telnet或xterm等程序。 Linux 2.2以后增添了UNIX98风格的Pty设备,它使用一个新的文件系统(devpts针对伪终端的文件系统)和一个克隆的设备cloning device来实现其功能。

  linux-2.4.22/drivers/char/Pty.c, 在devfs_mk_dir (NULL, “pts”, NULL);时会输出上面的信息。

  loop: loaded (max 8 devices)

  加载返还块设备驱动,最多支持8个设备。

  8139too Fast Ethernet driver 0.9.27

  eth0: RealTek RTL8139 at 0x60112000, 00:10:0d:42:a0:03, IRQ 14

  eth0: Identified 8139 chip type ‘RTL-8100B/8139D’

  网卡驱动,基地址为:0x60112000, MAC地址:00:10:0d:42:a0:03, 中断号:14

  从 2.2 版内核升级到 2.4 版时, RTL-8139 支持模块已不再叫 rtl8139,而叫它 8139too,现在你再看到8139too就不会不明白它的来由了吧。

  SCSI subsystem driver Revision: 1.00

  USB设备信息,USB会被当做SCSI来处理。

  mumk_register_tasklet: (1) tasklet 0x905bf9c0 status @0x9025e974

  软中断信息输出。Tasklet是在2.4中才出现,它是为了更好地利用多CPU。

  Probing XX Flash Memory

  探测 XX的闪存(Flash Memory),“NOR NAND Flash Memory Technology”。

  Amd/Fujitsu Extended Query Table v1.3 at 0x0040

  number of CFI chips: 1

  *************************************************************************

  AMD与富士通合资设立的Flash供货 商Spansion。AMD因获利不佳,已经退出Flash市场,后续由Spansion合资公司经营。主要生产NOR类型的flash,特点是容量小,速度快。Spansion商标的flash,在我们开发中会经常看到。以后大家看到Spansion的芯片,就能了解到它和AMD还有富士通的来龙去脉 了。

  Common flash Interface (CFI)是指一个统一的flash访问接口,表示这种flash是这种接口类型的。

  Using buffer write method

  使用flash写缓冲方式。

  flash 提供了写BUFFER的命令来加快对flash上块的操作。对Flash擦除和写数据是很慢的。如果用写BUFFER的命令会快一点。据手册上说,会快 20倍。Buffer Size :5 bytes的buffer缓冲不是每个块都有,是整个flash只有一个5 bytes的buffer,用写BUFFER命令对所有的块进行写操作,都要用同一个buffer,写Buffer是主要检查buffer是否 available,其实buffer起缓冲作用,来提高工作效率。

  比如某flash有128个128K字节块。允许用户对任意块进行字节编程和写缓冲器字节编程操作,每字节编程时间为210μs;若采用写缓冲器字节编程方式,32字节编程共需218μs,每字 节编程时间仅为6.8μs。芯片的块擦除时间为1s,允许在编程或块擦除操作的同时进行悬挂中断去进行读操作,待读操作完成后,写入悬挂恢复命令,再继续编程或块擦除。

  Creating 3 MTD partitions on “XX mapped flash”:

  0x00000000-0x00020000 : “BootLoader”

  0x00020000-0x00040000 : “Config”

  0x00040000-0x01000000 : “Romfs”

  此处为重要信息部分,需要特别留意。在内存中映射过的flash,创建三个MTD分区:

  flash上的内容将被映射到内存中的对应地址

  前128K为BootLoader——》0x00000000-0x00020000接着的128K为系统配置信息Config存放的位置——》0x00020000-0x00040000再后面的 16M - 2X128K 为romfs的存放处。——》0x00040000-0x01000000上面的内容,大家可以根据前面的换算公式得到。

  A》 编译的bootloader一般大小约50K左右;

  B》 在此处就知道了配置信息config是放在第2分区中的;

  C》 制作的romfs的大小,一般为8M或10M左右,所以能放得下;

  NET4: Linux TCP/IP 1.0 for NET4.0

  调用inet_init [ linux-2.4.22/net/ipv4/Af_inet.c ]时的输出信息, 在启动过程中被socket.c调用到。

  IP Protocols: ICMP, UDP, TCP, IGMP

  列出可以支持的IP协议,此处为kernel源代码inet_add_protocol(p);的输出。在linux启动过程中,都会看到这句的输出。

  IP: routing cache hash table of 512 buckets, 4Kbytes

  IP路由代码的输出信息。

  ip_rt_init [ linux-2.4.22/net/ipv4\Route.c ],设置 IP module,路由缓冲hash表

  TCP: Hash tables configured (established 8192 bind 8192)

  TCP协议初始化输出信息。tcp_init [ linux-2.4.22/net/ipv4/Tcp.c ],

  NET4: Unix domain sockets 1.0/SMP for Linux NET4.0.

  UNIX网络协议信息。

  af_unix_init[ linux-2.4.22/net/unix/Af_unix.c ], 多种连接的一种(IPv4, UNIX domain sockets, IPv6和IrDA)。 SMP 对称多处理器—Symmetrical Multi Processing,这里主要是指UNIX的一些网络协议。

  上面的关于网络的输出信息是在linux启动信息中都会出现的。

  cramfs: wrong magic

  加载各种文件系统。

  会出现“cramfs: wrong magic”,别担心这没有什么害处,这个是kernel的书写bug,在2.6中有修改之,它是一个警告信息,用来检查cramfs的superblock超级块的。superblock也是VFS要用到的数据结构。

  代码linux-2.4.22/fs/cramfs/Inode.c:

  2.4

  cramfs_read_super(。。。)

  /* Do sanity checks on the superblock */

  if (super.magic != CRAMFS_MAGIC) {

  /* check at 512 byte offset */

  memcpy(&super, cramfs_read(sb, 512, sizeof(super)), sizeof(super));

  if (super.magic != CRAMFS_MAGIC) {

  printk(KERN_ERR “cramfs: wrong magic/n”);

  goto out;

  }

  }

  2.6

  if (super.magic != CRAMFS_MAGIC) {

  if (!silent)

  printk(KERN_ERR “cramfs: wrong magic/n”);

  goto out;

  }

  超级块是文件系统的“头部”。它包含文件 系统的状态、尺寸和空闲磁盘块等信息。如果损坏了一个文件系统的超级块(例如不小心直接将数据写到了文件系统的超级块分区中),那么系统可能会完全不识别该文件系统,这样也就不能安装它了,即使采用e2fsck 命令也不能处理这个问题。

  **************************************************************************

  RamDisk有三种实现方式。

  在Linux中可以将一部分内存mount为分区来使用,通常称之为RamDisk,分为:

  Ramdisk, ramfs, tmpfs。

  ① 第一种就是传统意义上的,可以格式化,然后加载。这在Linux内核2.0/2.2就已经支持,其不足之处是大小固定,之后不能改变。为了能够使用 Ramdisk,我们在编译内核时须将block device中的Ramdisk支持选上,它下面还有两个选项,一个是设定Ramdisk的大小,默认是4096k;另一个是initrd的支持。

  如果对Ramdisk的支持已经编译进内核,我们就可以使用它了:首先查看一下可用的RamDisk,使用 ls /dev/ram*;首先创建一个目录,比如test,运行 mkdir /mnt/test;然后对/dev/ram0 创建文件系统,运行 mke2fs /dev/ram0;最后挂载/dev/ram0,运行mount /dev/ram /mnt/test,就可以象对普通硬盘一样对它进行操作了。

  ② 另两种则是内核2.4才支持的,通过Ramfs或者Tmpfs来实现:它们不需经过格式化,用起来灵活,其大小随所需要的空间而增加或减少。

  Ramfs顾名思义是内存文件系统,它处于虚拟文件系统(VFS)层,而不像ramdisk那样基于虚拟在内存中的其他文件系统(ex2fs)。因而,它无需格式化,可以创建多个,只要内存足够,在创建时可以指定其最大能使用的内存大小。

  如果你的Linux已经将Ramfs编译进内核,你就可以很容易地使用Ramfs了。创建一个目录,加载Ramfs到该目录即可:

  # mkdir /testRam # mount -t ramfs none /testRAM缺省情况下,Ramfs被限制最多可使用内存大小的一半。可以通过maxsize(以kbyte为单位)选项来改变。

  # mount -t ramfs none /testRAM -o maxsize=2000 (创建了一个限定最大使用内存为2M的ramdisk)③ Tmpfs是一个虚拟内存文件系统,它不同于传统的用块设备形式来实现的Ramdisk,也不同于针对物理内存的Ramfs。

  Tmpfs 可以使用物理内存,也可以使用交换分区。在Linux内核中,虚拟内存资源由物理内存(RAM)和交换分区组成,这些资源是由内核中的虚拟内存子系统来负 责分配和管理。Tmpfs向虚拟内存子系统请求页来存储文件,它同Linux的其它请求页的部分一样,不知道分配给自己的页是在内存中还是在交换分区中。同Ramfs一样,其大小也不是固定的,而是随着所需要的空间而动态的增减。

  使用tmpfs,首先你编译内核时得选择“虚拟内存文件系统支持(Virtual memory filesystem support)”。然后就可以加载tmpfs文件系统了:

  # mkdir -p /mnt/tmpfs

  # mount tmpfs /mnt/tmpfs -t tmpfs

  同样可以在加载时指定tmpfs文件系统大小的最大限制:

  # mount tmpfs /mnt/tmpfs -t tmpfs -o size=32m

  FAT: bogus logical sector size 21072

  具体的文件系统FAT格式。虚拟逻辑扇区大小为20K,linux-2.4.22/fs/fat/Inode.c。

  在初始化MS-DOS文件系统时,读MS-DOS文件系统的superblock,函数fat_read_super中输出的上面的信息。

  UMSDOS: msdos_read_super failed, mount aborted.

  UMSDOS:一种文件系统,特点容量大 但相对而言不大稳定。是Linux 使用的扩展了的DOS文件系统。它在 DOS 文件系统下增加了长文件名、 UID/GID、POSIX 权限和特殊文件 (设备、命名管道等)功能,而不牺牲对 DOS 的兼容性。允许一个普通的msdos文件系统用于Linux,而且无须为它建立单独的分区,特别适合早期的硬盘空间不足的硬件条件。

  VFS: Mounted root (romfs filesystem) readonly

  虚拟文件系统VFS(Virtual Filesystem Switch)的输出信息。

  再 次强调一下一个概念。VFS 是一种软件机制,也可称它为 Linux 的文件系统管理者,它是用来管理实际文件系统的挂载点,目的是为了能支持多种文件系统。kernel会先在内存中建立一颗 VFS 目录树,是内存中的一个数据对象,然后在其下挂载rootfs文件系统,还可以挂载其他类型的文件系统到某个子目录上。

  Mounted devfs on /dev

  加载devfs设备管理文件系统到dev安装点上。/dev是我们经常会用到的一个目录。在2.4的kernel中才有使用到。每次启动时内核会自动挂载devfs。

  devfs 提供了访问内核设备的命名空间。它并不是建立或更改设备节点,devfs只是为你的特别文件系统进行维护。一般我们可以手工mknod创件设备节点。 /dev目录最初是空的,里面特定的文件是在系统启动时、或是加载模组后驱动程序载入时建立的。当模组和驱动程序卸载时,文件就消失了。

  Freeing init memory: 72K

  释放1号用户进程init所占用的内存

  *************************************************************

  第三节:加载linux内核完毕,转入cpu_idle进程

  系统启动过程中进程情况:

  ① init进程

  一 般来说, 系统在跑完 kernel bootstrapping 内核引导自举后(被装入内存、已经开始运行、已经初始化了所有的设备驱动程序和数据结构等等), 就去运行 init『万process之父』, 有了它, 才能开始跑其他的进程,因此,init进程,它是内核启动的第一个用户级进程,它的进程号总是1。你可以用进程查看命令来验证:

  # ps aux

  PID Uid VmSize Stat Command

  1 0 SW init

  2 0 SW [keventd]

  3 0 SWN [ksoftirqd_CPU0]

  4 0 SW [kswapd]

  5 0 SW [bdflush]

  6 0 SW [kupdated]

  7 0 SW [rbwdg]

  9 0 SW [mtdblockd]

  10 0 SW [khubd]

  80 0 SW [loop0]

  另外 Linux 有两个 kernel 类的 process 也开始跑了起来,一个是 kflushd/bdflush,另一个是 kswapd。只有这个init 是完全属于 user 类的进程, 后两者是 kernel假借 process 进程之名挂在进程上。

  init 有许多很重要的任务,比如象启动getty(用于用户登录)、实现运行级别、以及处理孤立进程。init 一开始就去读 /etc/inittab (init初始化表),初始化表是按一定格式排列的关于进程运行时的有关信息的。init程序需要读取/etc/inittab文件作为其行为指针。这个 inittab 中对于各个runlevel运行级别要跑哪些 rc 或 spawn 生出什么有很清楚的设定。

  一 般, 在Linux中初始化脚本在/etc/inittab 文件(或称初始化表)中可以找到关于不同运行级别的描述。inittab是以行为单位的描述性(非执行性)文本,每一个指令行都是固定格式。 inittab中有respawn项,但如果一个命令运行时失败了,为了避免重运行的频率太高,init将追踪一个命令重运行了多少次,并且如果重运行的 频率太高,它将被延时五分钟后再运行。

  ② kernel进程

  A》 请注意init是1号进程,其他进程id分别是kflushd/ bdflush, kupdate, kpiod and kswapd。这里有一个要指出的:你会注意到虚拟占用(SIZE)和实际占用(RSS)列都是0,进程怎么会不使用内存呢?

  这些进程就是内核守护进程。大部分内核并不显示在进程列表里。守护进程在init之后启动,所以他们和其他进程一样有进程ID,但是他们的代码和数据都存放在内核占有的内存中。在列表中使用中括号来区别与其他进程。

  B》 输入和输出是通过内存中的缓冲来完成的,这让事情变得更快,程序的写入会存放在内存缓冲中,然后再一起写入硬盘。守护进程kflushd和kupdate 管理这些工作。kupdate间断的工作(每5秒)来检查是否有写过的缓冲,如过有,就让kflushd把它们写入磁盘。

  C》 进程有时候无事可做,当它运行时也不一定需要把其所有的代码和数据都放在内存中。这就意味着我们可以通过把运行中程序不用的内容切换到交换分区来更好的是利用内存。把这些进程数据移入/移出内存通过进程IO管理守护进程kpiod和交换守护进程kswapd,大约每隔1秒,kswapd醒来并检查内存情 况。如果在硬盘的东西要读入内存,或者内存可用空间不足,kpiod就会被调用来做移入/移出操作。

  D》 bdflush - BUF_DIRTY, 将dirty缓存写回到磁盘的核心守护进程。对于有许多脏的缓冲区(包含必须同时写到磁盘的数据的缓冲区)的系统提供了动态的响应。它在系统启动的时候作为一个核心线程启动,它叫自己为 “kflushd”,而这是你用ps显示系统中的进程的时候你会看得的名字。即定期(5秒)将脏(dirty)缓冲区的内容写入磁盘,以腾出内存;

  E》 ksoftirqd_CPUx 是一个死循环, 负责处理软中断的。它是用来对软中断队列进行缓冲处理的进程。当发生软中断时,系统并不急于处理,只是将相应的cpu的中断状态结构中的active 的相应的位,置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来临,再来处理。

  ksoftirqd_CPUx是由 cpu_raise_softirq() 即cpu触发中断,唤醒的内核线程,这涉及到软中断,ksoftirqd的代码参见[kernel/softirq.c]。

  F》 keventd,它的任务就是执行 scheduler 调度器队列中的任务,keventd 为它运行的任务提供了可预期的进程上下文。

  G》 khubd, 是用来检测USB hub设备的,当usb有动态插拔时,将交由此内核进程来处理。在检测到有hub事件时会有相应的动作(usb_hub_events())

  H》 mtdblockd是用来对flash块设备进行写操作的守护进程。NAND类型的Flash需要MTD(Memory Technology Devices 内存技术驱动程序)驱动的支持才能被linux所使用。NAND的特点是不能在芯片内执行(XIP,eXecute In Place),需要把代码读到系统RAM中再执行,传输效率不是最高,最大擦写次数量为一百万次,但写入和擦除的速度很快,擦除单元小,是高数据存储密度 的最佳选择。NAND需要I/O接口,因此使用时需要驱动程序。

  I》 loop0 是负责处理loop块设备的(回环设备)。loopback device指的就是拿文件来模拟块设备, 在我们这里,loop设备主要用来处理需要mount到板上的文件系统,类似mount /tmp/rootfs /mnt -o loop。。我们的实例有:mount -o loop -t cramfs /xxx.bin /xxx 也就是将xxx.bin这个文件mount到板上来模拟cramfs压缩ram文件系统。loop0进程负责对loop设备进行操作。

  loopback设备和其他的块设备的使用方法相同。特别的是,可以在该设备上建立一个文件系统,然后利用mount命令把该系统映射到某个目录下以便访问。这种整个建立在一个普通磁盘文件上的文件系统,就是虚拟文件系统 (virtual file system)。

  总结

  上面的内容是本人为了在实际开发中更加清楚地了解uclinux的启动过程而做的一个总结性的文章。在对uclinux的启动过程做了一个详细注释后,大家 会对涉及到嵌入系统的各个概念有了一个更加明确的认识,并能对嵌入系统的软硬件环境的有关设置更加清楚。当你自己动手结合linux源代码来分析时,将会有一个清楚的全局观。

  =============================================================

  1. 运行bootloader初始化程序

  SRAM 、SDRAM等存储设备属于挥发性的存储器,掉电以后其中的内容就会全部丢失,所以必须把操作系统的内核镜像存放在Flash等不挥发性存储介质上。但是操作系统在运行时,需要动态的创建一些如数据段、堆栈、页表(针对使用虚拟地址的操作系统)等内容,所以需要在RAM中运行操作系统。

  因此,就需要一个引导程序把操作系统的内核镜像从Flash存储器拷贝到RAM中,然后再从RAM中执行操作系统的内核。Bootloader就是可以完成这样一种功能的程序。

  从本质上来讲,bootloader不属于操作系统内核。它采用汇编语言编写,因此针对不同的CPU体系结构,这一部分代码不具有可移植性。在移植操作系统时,这部分代码必须加以改写

  具体来讲,bootloader在系统启动时主要完成以下几项工作:

  (1) 将操作系统内核从Flash拷贝到SDRAM中,如果是压缩格式的内核,还要将之解压缩。

  (2) 改写系统的memory map,原先flash起始地址映射为0地址,这时需要将RAM的起始地址映射为0。

  (3) 设置堆栈指针并将bss段清零。

  将来执行C语言程序和调用子函数时要用到

  (4) 改变pc值,使得CPU开始执行真正的操作系统内核。

  2. 运行操作系统内核

  bootloader程序执行完上述的各项工作后,通过一条跳转指令,转而执行init目录下C语言源文件main.c中的函数start_kernel()。

  因为在此之前bootloader已经创建好一个初始化环境,C函数可以开始执行了。

  整个操作系统内核的初始化工作从这里才算是真正开始。这个函数的长度比较短,代码如下:

  void __init start_kernel(void)

  {

  char * command_line;

  unsigned long mempages;

  extern char saved_command_line[];

  /*

  * Interrupts are still disabled. Do necessary setups, then

  * enable them

  */

  lock_kernel();

  printk(linux_banner);

  setup_arch(&command_line);

  printk(“Kernel command line: %s/n”, saved_command_line);

  parse_options(command_line);

  trap_init();

  init_IRQ();

  sched_init();

  softirq_init();

  time_init();

  /*

  * HACK ALERT! This is early. We‘re enabling the console before

  * we’ve done PCI setups etc, and console_init() must be aware of

  * this. But we do want output early, in case something goes wrong.

  */

  console_init();

  #ifdef CONFIG_MODULES

  init_modules();

  #endif

  if (prof_shift) {

  unsigned int size;

  /* only text is profiled */

  prof_len = (unsigned long) &_etext - (unsigned long) &_stext;

  prof_len 》》= prof_shift;

  size = prof_len * sizeof(unsigned int) + PAGE_SIZE-1;

  prof_buffer = (unsigned int *) alloc_bootmem(size);

  }

  kmem_cache_init();

  sti();

  calibrate_delay();

  #ifdef CONFIG_BLK_DEV_INITRD

  if (initrd_start && !initrd_below_start_ok && initrd_start 《 min_low_pfn 《《 PAGE_SHIFT)

  {

  printk(KERN_CRIT “initrd overwritten (0x%08lx 《 0x%08lx) - ”

  “disabling it./n”,initrd_start,min_low_pfn 《《 PAGE_SHIFT);

  initrd_start = 0;

  }

  #endif

  mem_init();

  kmem_cache_sizes_init();

  pgtable_cache_init();

  mempages = num_physpages;

  fork_init(mempages);

  proc_caches_init();

  vfs_caches_init(mempages);

  buffer_init(mempages);

  page_cache_init(mempages);

  #if defined(CONFIG_ARCH_S390)

  ccwcache_init();

  #endif

  signals_init();

  #ifdef CONFIG_PROC_FS

  proc_root_init();

  #endif

  #if defined(CONFIG_SYSVIPC)

  ipc_init();

  #endif

  check_bugs();

  printk(“POSIX conformance testing by UNIFIX/n”);

  /*

  * We count on the initial thread going ok

  Like idlers init is an unlocked kernel thread,which will

  * make syscalls (and thus be locked)。

  */

  smp_init();

  rest_init();

  }

  内核启动之后需要执行的第一个函数是start_kernel()(在linux/init/main.c文件中)。

  start_kernel()

  完成下面一系列初始化的工作。

  ◆printk(1inux_banner),显示Linux内核的版本信息。

  ◆ setup_arch(&command_line),做与体系结构相关的初始化工作。

  ◆ parse_options(command_line),解释系统参数。

  ◆ trap_init(),设置系统异常的入口点。

  ◆ init_IRQ(),初始化系统中断服务。

  ◆ sched_init(),系统调度器的初始化。

  ◆ time_init(),时钟、定时器初始化。

  ◆ softirq_init(),系统软中断的初始化。

  ◆console_init(),控制台初始化。

  ◆kmem_cache_init(),内核cache的初始化。

  ◆calibrate_delay(),校准时钟。

  ◆mem_init(),内存初始化。

  ◆kmem_cache_sizes_init(),创建及设置通用cache。

  ◆fork_init(mempages),建立uidcache,并且根据系统内存大小来确定最大进程数目。

  ◆buffer_init(mempages),块设备缓冲区的初始化。初始化一系列的cache。

  ◆check_bugs(),检查体系结构漏洞。

  ◆kernel_thread(init NULL,CLONE_FS | CLONE_FILES | CLONE_SIGNAL),创建第一个核心进程,启动init进程。

  ◆cpu_idle(),运行idle进程

  接下去做的工作由init()函数来完成。init()首先要锁定内核,然后调用do_basic_setup( )来完成外部设备以及驱动程序的初始化。外设的初始化要根据内核的配置来决定,一般需要做下面的初始化工作:

  ◆PCI总线初始化。

  ◆网络初始化。

  ◆一系列其他设备的初始化。

  ◆start_context_thread()创建事件管理核心进程keventd。

  ◆通过do_initcalls()函数来启动任何使用__initcall标识的函数。

  ◆文件系统初始化。

  ◆加载文件系统。

  在do_basic_setup()调用完成之后,init()会释放初始化函数所用的内存,并且打开/dev/console设备重新定向控制台,让系统调用execve来执行程序init。

  到这里为止,Linux 内核的初始化工作已经完成。然后开始用户态进程的初始化。

  =============================================================

  uClinux中内存模块的启动初始化

  arch/armnommu/kernel/entry_armv.S是一个汇编文件,他包含了一个kernel_entry的定义,这是整个内核的进入点。在完成某些平台相关的初始化工作之后,执行流程跳转到start_kerne()处。从这里开始考察uClinux的内存模块启动初始化是如何实现的。

  start_kernel()中与内存模块相关的函数调用流程如下:

  setup_arch() paging_init() free_area_init() mem_init()

  下面分别分析这些函数各自的功能以及uClinux对他们的改造。

  (1) setup_arch()

  setup_arch()首先根据目前内核所配置的平台向某些特定地址写入特殊字符序列,以完成对特定硬件的初始化,比如工作状态发光二极管、错误和报警发光二极管。

  存储了可用物理内存起始地址的变量memory_start的初始化是通过一个ld脚本中定义的变量_end进行的。ld脚本是用来控制GNU ld连接器在连接内核各个目标文件部分的时候的配置动作,比如这样一个脚本:

  SECTION

  {

  。=0x10000;

  .text:{*(.text)}

  。=0x8000000;

  .data:{*(.data)}

  .bss :{*(.bss)}

  }

  用来配置ld连接目标文件的时候将所有的目标文件中的存储程序正文的.text段(section)连接到一起,并且映射到输出文件的地址0x10000处,将所有目标文件中已初始化的数据.data段连接到一起并放置到输出可执行文件的0x8000000地址处,而所有目标文件中还未初始化的数据段.bss连接起来后影射到输出文件中紧跟在.data段之后的位置。

  这个ld配置脚本文件对每个平台都是不同的。如为MICETEK上所使用的uClinux版本使用的ld配置文件为arch/armnomm/vmLinux.lds。可以通过修改某个平台上的ld脚本配置文件中的_end变量来达到配置其可用物理内存起始地址的目的。

  setup_arch()在完成对memory_start变量的初始化之后,通过某些特定手段检测不同类型的内存分布情况。比如为检测某段地址范围是否为RAM的方法是通过将某个地址的数据读出来,将它加1后写回内存地址中,然后再读出来和原始数据比较看看其值是否成功增加了1,这样反复操作两次,最后将数据恢复。如果是可读可写的RAM,那么这个测试的结果就是每次比较都是成功的,否则就不能将这个地址当作RAM。

  在setup_arch()中还可能根据所用平台进行对flash memory和ROM的测试。在这些平台相关的工作完成之后,setup_arch()将对系统运行的第一个进程init_task的mm_struct结构中描述地址空间分布的变量start_code,end_code,end_data和brk进行初始化,start_code为0,其他三个数值分别为来自于ld脚本配置文件中定义的相关变量_etext、_edata和_end。

  此后setup_arch()将根据Linux中为系统中的第一块rom/flash memory card所分配的固定的主/从设备号(可以从Document/devices.txt中得到)来创建根文件系统的设备号,并存储在后来将要用到的全局变量ROOT_DEV中。

  setup_arch()最后完成对系统启动参数的保存。

  在调用setup_arch()返回之后,start_kernel()中得到了系统可用物理内存的起始和结束地址,以及命令启动时的命令行参数。

  (2) paging_init()

  在Linux中,paging_init()的一项主要功能是建立页目录和页表,而且将Linux移植到不同平台的过程中非常重要的一个步骤就是修改这个函数来适应新的硬件平台的虚拟内存体系。但是由于在uClinux中不再使用虚拟内存机制,也就不再需要维护页目录和页表数据结构了,所以paging_init()在这里只是为系统启动的时候保留一部分特殊用途的内存区间。它返回后,从可以使用的内存空间开始,依次是如下的数据结构:

  empty_bad_page_table 占用1页(4KB)

  empty_bad_page 占用1页(4KB)

  empty_zero_page 占用1页,并初始化为全0

  mem_map

  bitmap

  paging_init()函数在返回前通过调用free_area_init(start_mem,end_mem)进行建立buddy system的映射位图关系,以及建立空闲物理页面链表的操作。

  (3)free_area_init()

  这个函数用于建立管理物理页帧的数据结构mem_map,有多少物理页帧就有多少mem_map_t类型的结构体与之相对应。每个页面的mem_map_t结构中的flags被标明为PG_DMA和PG_reserved,并且页帧号被赋给相应的数值。同时建立了管理空闲页面的bitmap映射表,并且所有的位都被清零。

  (3) mem_init()

  mem_init()函数遍历整个可用物理内存地址空间,将每个页面相对应的struct page结构中flags的PG_reserved 标志位清除,标志用户个数的count计数器置1,并同时统计可用物理页面数量,然后打印系统的各个内存参数,如可用RAM和ROM的大小、内核代码段和数据段大小等。

  ======================================================

  摘 要:本文采用三星公司的S3C44B0微处理器,对uClinux操作系统内核的引导过程进行了剖析。

  关键字:S3C44B0X;uClinux;嵌入式系统;内核引导

  1 前言

  伴随着微电子的发展,用于嵌入式设备的处理器速度越来越快,功能也越来越强大。三星公司生产的S3C44B0微处理器,采用的是ARM7TDMI内核。该内核因为有着功耗小、成本低等特点,因此非常适合作为移动手持终端的处理器核心。Linux操作系统因为它的开放性,使得它不断的被应用到各个领域。在嵌入式领域同样也出现了各种各样的Linux变体,最常用的是uClinux。也正是因为uClinux操作系统支持不带MMU单元的ARM处理器,因此该系统可以对S3C44B0微处理器有很好的支持。

  在嵌入式系统开发中,第一个部分便是系统的引导。而系统的引导过程是通过BootLoader来完成的。BootLoader程序是与硬件紧密相关的一段代码,而且编写的时候比较复杂,它主要的功能是初始化微处理器以及周边的硬件资源,并且引导操作系统的启动。下面我将以S3C44B0微处理器来作为例子,对uClinux操作系统内核的引导过程进行一个剖析。

  2 BootLoader程序概念

  简单的说Boot Lodaer就是在操作系统内核运行之前运行的一段小程序,通过这段小程序,可以初始化硬件设备、建立系统的内存空间映射图,从而将系统的软硬件环境设置成一个适合的状态,以便为最终调用操作系统内核准备好正确的环境。最终,BootLoader把操作系统内核映象加载到RAM中,并将系统控制权传递给它。

  2.1 典型的BootLoader程序框架

  操作系统角度来说,Boot Loader的总目标就是正确的调用内核来执行。

  由于Boot Loader的实现依赖于CPU的体系结构,因此大多数Boot Loader都分为Stage1和Stage2两大部分。依赖于CPU体系结构的代码,例如设备初始化代码等,通常都放在Stage1中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而Stage2通常用C语言来实现,这样可以实现更加复杂的功能,而且代码会具有更好的可读性和可移植性。

  Boot Loader的Stage1通常包括如下步骤:

  1) 硬件设备初始化

  2) 为加载Boot Loader的Stage2准备RAM空间

  3) 复制Boot Loader的Stage2到RAM空间中

  4) 设置好堆栈

  5) 跳转到Stage2的C入口点

  Boot Loader的Stage2通常包括如下步骤:

  1) 始化本阶段要使用的硬件设备

  2) 检测系统内存映射(Memory Map)

  3) 将Kernel映象和根文件系统映象从FLASH上读取到RAM空间中

  4) 为内核设置启动参数

  5) 调用内核

  2.2 系统内存组织

  由于嵌入式设备具有很好的制定性,因此通常硬件环境会变的千差万别。就算是用户使用了相同的处理器芯片,但是也很有可能因为外围设备电路设计的不同,而存在差异。对于BootLoader程序来说,存储设备的与处理器的连接方式,与其息息相关。对于我们采用的S3C44B0微处理器来说,在系统加电之后,指令指针是指向0x00000000的,也就是说系统是从0x00000000开始之行。正是因为这个原因,通常这个地址空间我们会安排给FLASH存储器。这样我们可以将BootLoader启动代码以及我们之后将会要启动的uClinux操作系统映像烧写到Flash里。对于RAM地址空间,S3C44B0芯片将其设定为从0x0C000000到0x0FFFFFFF一共64MB的范围里。我们可以通过设定存储器控制寄存器来重新设定RAM的大小。例如我们试验采用的存储设备安排如下:

  0x00000000 – 0x003FFFFF 4MB Flash

  0x0C000000 – 0x0C7FFFFF 8MB RAM

  通常来说对于系统的引导和操作系统的启动,可以完全都在Flash中进行,但是Flash存储器的速度相对于RAM来说会慢很多,因此出于速度上的考虑,我们通常会将启动代码和uClinux操作系统的内核映像文件拷贝到RAM中之行。

  下面我将对典型的BootLoader程序框架进行分析。

  2.3 Stage1阶段

  该阶段的主要工作是完成对系统中断向量的设置,初始化微处理器内部寄存器,初始化堆栈,初始化RAM地址空间,并且将Stage2部分的C代码拷贝到RAM空间的指定地点,然后跳转到C代码入口点继续执行。对于这段代码来说,做的都是一些准备工作,因此为了提高效率,这段代码通常都是使用汇编语言来完成的。下面我将结合具体的代码来分析一下Stage1的启动过程。

  1)设置中断向量

  设置S3C44B0处理器定义的8种系统中断的中断向量地址。这八种系统中断分别是复位中断、未定义指令中断、软件中断、指令预取异常中断、数据异常中断、地址异常中断、IRQ中断和FIQ中断。这8个中断通常是通过无条件跳转的方式来实现的。具体的代码如下。

  __entry :

  b ResetHandler /* Reset vector */

  b HandlerUndef /* Undefined instruction */

  b HandlerSWI /* SWI */

  b HandlerPabort /* Prefetch abort */

  b HandlerDabort /* Data abort */

  b 。 /* Address exception */

  b HandlerIRQ /* IRQ */

  b HandlerFIQ /* FIQ */

  2)初始化微处理器内部寄存器

  这段代码主要是要完成硬件部分的初始化,包括关闭中断响应、初始化微处理器通用端口、设置CPU频率等操作。不过需要注意的是,在进行硬件初始化之前需要将微处理器的运行状态转换到SVC模式下。

  MRS a1,CPSR /*; Pickup current CPSR*/

  BIC a1,a1,#MODE_MASK /*; Clear the mode bits*/

  ORR a1,a1,#SUP_MODE /*; Set the supervisor mode bits*/

  ORR a1,a1,#LOCKOUT /*; Insure IRQ and FIQ intr are locked out*/

  MSR CPSR_cxsf,a1 /*; Setup the new CPSR*/

  3)初始化系统RAM空间

  这个部分的工作主要是为之后启动代码和内核映像的拷贝操作做准备,并且也为之后的C代码的执行初始化堆栈。这部分的工作主要可以分成两个部分来处理。首先,根据系统配置的存储器特性来初始化相关的存储器控制寄存器。在我们使用的S3C44B0处理器中,存储空间被分成了BANK0-BANK7一共8个块,分别由BANKCON0-BANKCON7控制各个块存储器的读写时钟和片选时钟等信号参数。具体代码如下:

  ldr r0,=rBANKCON0

  ldr r1,=0x700

  str r1,[r0]

  ldr r0,=rBANKCON1

  ldr r1,=0x700 /* 0x7ffc */

  str r1,[r0]

  ldr r0,=rBANKCON2

  ldr r1,=0x700 /* 0x7ffc */

  str r1,[r0]

  ldr r0,=rBANKCON3

  ldr r1,=0x7568

  str r1,[r0]

  ldr r0,=rBANKCON4

  ldr r1,=0x700 /* 0x7ffc */

  str r1,[r0]

  ldr r0,=rBANKCON5

  ldr r1,=0x700 /* 0x7ffc */

  str r1,[r0]

  ldr r0,=rBANKCON6

  ldr r1,=0x18008

  str r1,[r0]

  ldr r0,=rBANKCON7

  ldr r1,=0x18000

  str r1,[r0]

  ldr r0,=rREFRESH

  ldr r1,=0xac03e1

  str r1,[r0]

  ldr r0,=rBANKSIZE

  ldr r1,=0x16

  str r1,[r0]

  ldr r0,=rMRSRB6

  ldr r1,=0x020

  str r1,[r0]

  ldr r0,=rMRSRB7

  ldr r1,=0x020

  str r1,[r0]

  初始化RAM空间的第二个部分就是初始化连接脚本文件中指定的需要清0的地址空间,将该断地址空间的内容清0。该部分地址空间主要是用来存放C语言代码中的全局变量等内容的。实现代码如下:

  LDR a1,=Image_ZI_Base /* Pickup the start of the BSS area */

  MOV a3,#0 /* Clear value in a3 */

  LDR a2,=Image_ZI_Limit /* Pickup the end of the BSS area */

  CMP a1,a2

  BEQ move_data

  clear_loop :

  STR a3,[a1],#4 /* Clear a word, a1 += 4 */

  CMP a1,a2 /* end of ZI ? */

  BNE clear_loop

  4)为Stage2的C语言代码的执行准备必要的堆栈

  因为在Stage2阶段一般都是采用C语言代码来完成的,因此必须在使用C语言代码之前先建立起必要的堆栈信息。通常为了避免堆栈数据被执行代码破坏,通常都是放在RAM的高端地址,并且使得堆栈指针的增长方向是向下增长的。

  5)将初始化代码拷贝到RAM中,并且跳转到RAM中执行。因为在我们采用的S3C44B0微处理器里对于FLASH和RAM地址空间是使用的统一编址的,因此我们可以直接使用一个简单循环来完成拷贝。

  ldr r3, =0x10000 /* 64K Bytes */

  ldr r2, =0xc700000

  ldr r1, =0

  next :

  ldr r0,[r1],#4

  str r0,[r2],#4

  cmp r1,r3

  bne next

  6)跳转到C代码执行(即Stage2阶段)

  这个过程是直接给指令指针赋值于跳转的C代码的入口地址,在我们的试验中该入口地址是Main。

  LDR pc,=Main

  2.4 Stage2阶段

  该阶段的代码主要使用C语言来实现的。该阶段的工作主要是建立开发板与宿主机之间的通信,加载uClinux内核映像文件和配置内核启动参数,并且启动内核。

  嵌入式设备与宿主机的通讯方式有多种,最常用的是使用串口方式进行数据交换。本试验采用的S3C44B0微处理器提供了两个UART口,因此我们可以任选其中一个来初始化并且使用它来与宿主机交互。对于串口的初始化主要是波特率、奇偶校验、停止位、数据位等内容。

  对于串口的波特率和波特因子的计算采用如下公式

  Iubrd =((int(mclk/16 / baud + 0.5) – 1)

  mclk是频率、baud为波特率

  2.4.1 检测内存

  该部分的功能主要是检测系统在进行硬件初始化的时候是否发生了内存映射错误,即是否物理地址是否被映射到不存在的地址空间。通常是使用读写方式来检测的,即以内存页为单位,在每个页头进行读写操作,比较读写结果。因为S3C44B0处理器并不支持内存映射,因此我们在Stage2过程中并没有包含该部分功能函数。

  2.4.2 加载uClinux内核映像

  该过程其实只是一个从Flash的指定位置(该位置是uClinux烧写的起始地址)拷贝到RAM中指定的地址空间里。在拷贝之前必须要为uClinux的全局变量结构,即启动参数、内核页表、RAM的页目录等信息预留一定的空间。如果我们将FLASH和RAM看成连在一起的线性地址,则系统的空间分配会如下图:

  。..。..

  Boot

  初始化代码

  uClinux

  未用

  中断向量表

  初始化映像代码

  启动参数

  内核映像

  未用

  堆栈

  

  2.4.3 配置内核启动参数

  我们采用的uClinux是2.4.x内核版本,该版本的内核支持参数启动过程。在嵌入式系统中,启动参数的传入主要是依靠bootloader程序向标记列表(tagged list)的相关域中填写相应的值来完成的。

  2.5 uClinux内核引导

  当我们初始化完毕uClinux的启动参数之后,控制权就可以交给uClinux内核了,uClinux系统调用内核解压函数(decompress_kernel)来对上一个阶段拷贝的uClinux内核在RAM空间里进行解压(当然如果系统内核在建立的时候没有配置成压缩格式,则解压过程略去)。在解压完毕后,跳转到内核调用函数(call_kernel),该函数实际上执行的是start_kernel(),这个函数包含了有关处理器初始化、中断初始化、进程初始化等操作。最后,将控制权完全的交与uClinux操作系统来执行。

  伪处理过程如下:

  IF(启动参数正确)

  CALL decmporess_kernel()

  CALL call_kernel()

  ELSE

  启动失败

  decompress_kernel()

  {

  解压内核映像

  }

  call_kernel()

  {

  。..

  start_kernel()

  。..。

  }

  3 总结

  本文是对S3C44B0的启动过程进行了一次分析,启动部分的代码可以说是嵌入式设备开发比较重要的部分。而且该部分的处理工作往往又比较麻烦,因此在这里我只是想起到抛砖引玉的作用。因为成文时间比较仓促,难免有错误,请大家批评指正。

  ==========================================================

  《ARM7 uClinux开发实验与实践》P130

  Bootloader完成系统初始化工作后,将运行控制权交给uClinux内核。根据内核是否压缩以及内核是否在本地执行,uClinux通常有以下两种可选的启动方式:

  (1)Flash本地运行方式。内核中未经压缩的可执行映像固化在Flash中,系统启动时,内核在Flash中开始逐句执行。

  (2)压缩内核加载方式。内核的压缩映像固化在Flash上,系统启动时,由附加在压缩映像前的解压复制程序读取压缩映像,并在内存中解压后执行。这种方式相对复杂,但是运行速度更快。

  首先介绍内核的Flash本地运行方式。

  本地运行时,内核的启动包括特定体系结构设置和uClinux系统初始化两步,内核启动的入口文件是head-armv.s。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分