经过一番折腾,终于弄清了树莓派Linux是如何启动的。
过去一直搞不明白的一个问题就是,树莓派第一阶段的bootloader在哪里?为什么会有这个疑问呢?是因为,安装树莓派Linux的时候,只需要用dd命令把SD卡镜像刷入SD卡中,插入树莓派,上电即可运行。而SD卡上,没什么特别的地方,无非是两个分区,第一个分区是VFAT文件系统,作为/boot目录,第二个分区是EXT4文件系统,作为/目录。要启动树莓派的Linux内核(在/boot/kernel.img),至少必须要先挂载/boot目录,然后把kernel.img放入RAM里面执行才行。那么,挂载和加载内核这件事,是谁做的?
后来看了一个帖子,终于明白了。先贴上截图:
翻译一下:
1、当树莓派刚刚上电时,ARM内核(也就是CPU)是关闭的,而GPU内核是开启的。此时SDRAM也是不可用的;
2、GPU开始执行第一阶段的bootloader,这段代码被固化在ROM中。第一阶段bootloader读取SD卡,并且加载第二阶段bootloader——bootcode.bin——放入二级缓存,并运行之;
3、bootcode.bin启用SDRAM,并从SD卡中读取第三阶段的bootloader——loader.bin——放入RAM,并运行之;
4、loader.bin读取GPU固件——start.elf;
5、start.elf读取config.txt、cmdline.txt和kernel.img。
loader.bin并不做很多事。它能够处理.elf文件,这能够把start.elf加载到内存的顶端(CPU是从地址0开始使用内存的)。有一个计划想要把对elf文件的加载支持放入bootcode.bin中,这样就不再需要loader.bin了。不过这个计划的优先级很低(我猜这可能可以节约100ms的启动时间)。
OK,翻译结束~~~~~~~~~~~~~~~
事实上,现在的树莓派确实已经没有了loader.bin了,下图就是一个最简化的/boot目录所需的文件,只有5个:
其中bootcode.bin和start.elf是属于firmware(固件),可以自己下源码编译,也可以下载已经编译完了的二进制文件,还可以,呵呵,就是直接从已经存在的树莓派系统里面复制过来~他们的功能上面已经讲解过了。而config.txt和cmdline.txt就是两个文本文件,config.txt是start.elf的配置文件,而cmdline.txt是传给linux内核的参数。kernel.img就是我们自己定制编译的linux内核,当然,要通过一个工具加工一下。
config.txt说简单可以很简单,留一个空文件都可以,说复杂可以很复杂,因为可以设置很多很多参数。这里贴一个树莓派官方的文档,专门讲解这个config.txt的:《config.txt》。
=======================阶段一:SD卡分区==========================
为了演示确实是从“零”开始构建的,我不得不格式化掉SD卡。。。
树莓派要求SD卡的第一个分区必须是fat32文件系统,并把它挂载到/boot目录。而之后分几个区可以自由发挥,只要在cmdline.txt中指定哪个分区挂载为/目录,那么系统就能启动起来。一般来说,SD卡被分为两个区,第一个是fat32文件系统作为/boot,第二个是ext4文件系统作为/。
首先,把SD卡插入读卡器,然后再把读卡器插入电脑。
然后,查看以下当前有哪些存储设备,以确定哪个是SD卡。(其实最简单的办法就是在未插SD卡时“ls /dev | grep sd”看看有那些存储器,然后插入SD卡再“ls /dev | grep sd”,多出来的那个就是SD卡了)。我这里是/dev/sdb。
使用fdisk分区:
sudo fdisk /dev/sdb
之后就是交互式的操作了:
1、输入p,回车,看看当前SD卡有哪些分区:
2、输入d,回车,输入1,回车,即可删除/dev/sdb1分区;同理,输入d,回车,输入2,回车,即可删除/dev/sdb2。以此类推,直到把所有分区删除完。
3、此时再输入p,回车,会发现已经没有分区了:
4、输入n,回车,输入p,回车,输入1,回车,回车,输入+40M,回车,于是就新建了一个/dev/sdb1分区,大小40MB;
5、输入n,回车,输入p,回车,输入2,回车,回车,输入+4G,回车,于是就新建了一个/dev/sdb2分区,大小4GB;
6、输入p,回车,可以看一下分区表是否正确:
看来除了/dev/sdb1的文件系统类型不对以外,其他都对了(id=83就是EXT4);
7、输入t,回车,输入1,回车,输入c,回车,于是/dev/sdb1被设置为了W95 FAT32 (LBA);
8、再输入p,回车,再次看一下分区是否正确:
看来已经正确了;
9、输入w,将新的分区表写入SD卡。
分区完以后,就要格式化文件系统了:
sudo umount /dev/sdb1 sudo umount /dev/sdb2 sudo mkfs.vfat /dev/sdb1 sudo mkfs.ext4 /dev/sdb2
=======================阶段二:编译linux内核========================
在之前一篇博客《树莓派linux内核编译与升级》的阶段二已经讲解了怎么编译树莓派linux的内核了,可以作为一个参考。
首先新建一个目录diy_rpi_linux并进入目录:
cd ~ mkdir diy_rpi_linux cd diy_rpi_linux
下载树莓派linux的内核源码并解压:
wget http://github.com/raspberrypi/linux/archive/rpi-4.1.y.zip jar xvf rpi-4.1.y.zip
下载制作内核镜像的工具,其中包括交叉编译工具:
wget http://github.com/raspberrypi/tools/archive/master.zip -O tools.zip unzip tools.zip
设置好环境变量,把交叉编译器的路径加入PATH中:
PATH=$PATH:~/diy_rpi_linux/tools-master/arm-bcm2708/arm-bcm2708hardfp-linux-gnueabi/bin
然后开始编译工作!
cd linux-rpi-4.1.y/ make ARCH=arm CROSS_COMPILE=arm-bcm2708hardfp-linux-gnueabi- bcmrpi_defconfig make ARCH=arm CROSS_COMPILE=arm-bcm2708hardfp-linux-gnueabi- -j4
完成后,要来制作内核镜像了。因为编译出来的linux内核的格式不是树莓派能够识别的格式(树莓派用的kernel.img是在linux内核前加了一些信息),所以需要另外处理一下:
cd ../tools-master/mkimage/ ./imagetool-uncompressed.py ../../linux-rpi-4.1.y/arch/arm/boot/Image
此时,在~/diy_rpi_linux/tools-master/mkimg目录下,就多了一个kernel.img的文件。这个就是待会儿要放入SD卡的boot目录的内核镜像。
=====================阶段三:构建/boot目录======================
有了内核,就可以来构建/boot目录了。上面也说了,只需要bootcode.bin、start.elf、config.txt、cmdline.txt和kernel.img这么五个文件。
bootcode.bin和start.elf可以从树莓派的firmware中得到。
首先下载firmware-master.zip并解压:
cd ~/diy_rpi_linux wget https://github.com/raspberrypi/firmware/archive/master.zip -O firmware.zip unzip firmware.zip
而后就可以在~/diy_rpi_linux/firmware-master/boot目录下找到我们要的东西了:
新建一个目录,用来存放这5个文件:
mkdir sd_boot cd firmware-master/boot cp bootcode.bin start.elf ../../sd_boot/
接着,创建一个空白的config.txt和一个只有一行参数的cmdline.txt:
cd ../../sd_boot touch config.txt echo "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait" > cmdline.txt
最后,把kernel.img复制进来:
cp ../tools-master/mkimage/kernel.img .
至此,该有的都有了,挂载/dev/sdb1,全部复制进去,卸载SD卡:
sudo mkdir /mnt/sd_boot sudo mount /dev/sdb1 /mnt/sd_boot sudo cp * /mnt/sd_boot/ sync sudo umount /dev/sdb1
就是还差/目录。先不管了,看看内核能不能运行起来再说把!!
拔出SD卡,插入树莓派,启动minicom:
能够启动并打印一大堆启动信息,但是最后因为找不到init程序而挂起。但至少,内核启动起来了!!
=======================阶段三:构建/目录==========================
之所以出现上面的原因,是因为,内核找不到init程序。init进程是内核启动后第一个启动的进程,而所有的进程都是init的子进程。
构建/目录,大致就是构建bin、sbin、etc、var、home等目录,以及proc、dev等特殊目录。在《从零开始构建linux(一)——编译linux内核》的阶段三和阶段四已经讲述了怎么编译busybox和构建根目录了。这里依旧使用busybox。
首先要下载busybox的源码包并解压:
cd ~/diy_rpi_linux wget http://busybox.net/downloads/busybox-1.20.2.tar.bz2 tar xvf busybox-1.20.2.tar.bz2
这时就有了busybox-1.20.2目录了,进入目录并开始配置编译选项:
cd busybox-1.20.2 make defconfig make menuconfig
出现如下画面:
首先设置静态编译:Busybox Settings–>Build Options下的第一个选项Build BusyBox as a static binary(no shared libs)选上。因为我们的linux环境暂时还没有加入各种运行时库,所以必须静态链接。
然后是设置交叉编译工具:Busybox Settings–>Build Options下的第四个选项Cross Compiler prefix填写为“arm-bcm2708hardfp-linux-gnueabi-”,如图:
退出,保存。开始编译:
make -j4 make install
make install之后,目录下就多了一个_install目录,看看里面的结构,就是一个linux的/目录需要的了:
接下来,再创建几个常用的目录:
cd ~/diy_rpi_linux mkdir sd_root cp -r busybox-1.20.2/_install/* sd_root/ cd sd_root mkdir proc mnt var tmp dev sys etc
同时在/下还必须要有一个init文件,这个init文件可以是一个可执行的二进制文件,也可以是一个shell脚本,或者是指向前面两者的链接。init文件会在linux内核初始化就绪后被执行。方便起见,我们就把init做成一个指向bin/sh的软连接:
ln -s bin/sh init
dev目录下还必须有几个必要的设备console,null,tty,tty1,tty2,tty3,tty4:
cd dev mknod console c 5 1 mknod null c 1 3 mknod tty c 5 0 mknod tty1 c 4 1 mknod tty2 c 4 2 mknod tty3 c 4 3 mknod tty4 c 4 4
把sd_root中的内容复制到/dev/sdb2中:
sudo mkdir /mnt/sd_root sudo mount /dev/sdb2 /mnt/sd_root cd .. sudo cp -a * /mnt/sd_root/ sync sudo umount /dev/sdb2
OK,拔出SD卡,插入树莓派,启动minicom,欣赏一下吧:
注意最后一句,“Please press Enter to activate this console”,就是说按回车键激活终端。那就照做呗,敲一下Enter,居然出现了命令提示符“#”,执行命令试试:
不仅可以打印目录列表,还能读写文件~~~
你可能很好奇,为什么不需要用户登录?因为我把init直接指向了/bin/sh,而不是/bin/login,所以直接跳过了用户登录,于是乎,一切畅行无阻,再也没有权限管理的概念了~~~
但是目前,文件系统上还没有内核模块。