作者:张华 发表于:2013-09-21
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明
( http://blog.csdn.net/quqi99 )
本文是《编译linuxkernel及制作initrd( by quqi99 ) 》的续集,将采用busybox实践如何定制自己的Linux,内容包括:定制initramfs,定制内核,定制镜像。从中你可以学到:
-
Linux的启动过程原理
-
initramfs的本质,如何编译内核,制作OpenStack镜像的根本原理
-
initramfs, 内核,镜像之间的关系
-
chroot的根本原理,以及网上将linux安装在arm的安卓手机的基本原理
1, 创建工作目录:
export MYLINUX=/bak/images/mylinux && mkdir $ MYLINUX && cd $ MYLINUX
2, 使用busybox生成根文件系统中的bin等目录
wget -c http://www.busybox.net/downloads/busybox-1.21.1.tar.bz2
tar -xf busybox-1.21.1.tar.bz2 && cd busybox-1.21.1
make menuconfig
然后将里面的CONFIG_BUSYBOX_EXEC_PATH参数(BusyboxSettings -> General Configuration -> Path to BusyBox executable)配置成”/bin/bushbox”,否则在执行chroot命令时会报错:“chroot:failed to run command `/bin/bash': No such file or directory”。
make && make CONFIG_PREFIX=$MYLINUXinstall
3,用下列脚本创建lib或lib64目录解决共享库的依赖问题,处此配置不对在执行chroot命令时也会报错:“chroot:failed to run command `/bin/bash': No such file or directory”。
sudo ./create_lib.sh$MYLINUX
$ cat create_lib.sh
ROOT=$1
libs=`find $ROOT/bin-type f -perm /111 -exec "ldd" {} \;|cut -d \> -f 2|cut-d \( -f 1|sort |uniq`
echo $libs
for lib in $libs
do
if [ -f $lib ];then
if [ !-f $ROOT/$lib ] ;then
dir=`dirname $ROOT$lib`
if [ ! -d $dir ];then
mkdir -pv $dir
fi
cp -av $lib $ROOT$lib
if [ -h $lib ]; then
source=`dirname $lib`/`readlink $lib`
cp -av $source $ROOT$source
echo $source >> liblist
fi
fi
fi
done
echo "Done!"
4, 创建服务启动脚本与inittab文件与fstab文件
$ mkdir -p etc/init.d
$ cat $MYLINUX/etc/init.d/rcS
#!/bin/sh
mount -t proc /proc /proc
syslogd
klogd
ifconfig lo 127.0.0.1
xinetd
busybox sh
busybox支持init功能,当系统没有/etc/inittab文件时,它按下列缺省默认配置执行:
::sysinit:/etc/init.d/rcS
::askfirst:/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
如果检测到/dev/console不是串口控制台,init还会执行,这样系统运行后可用ALT+F1~F6键在6个终端之间切换:
tty1::askfirst:/bin/sh
tty2::askfirst:/bin/sh
tty3::askfirst:/bin/sh
tty4::askfirst:/bin/sh
busybox的inittab文件与通常的inittab不同,它没有runlevel的概念,语句功能上也有限制。Inittab语句的标准格式是:
<id>:<runlevels>:<action>:<process>
各字段的含义如下:
-
<id>:id字段与通常的inittab中的含义不同,它代表的是这个语句中process执行所在的tty设备,内容就是/dev目录中tty设备的文件名。由于是运行process的tty设备的文件名,所以也不能象通常的inittab那样要求每条语句id的值唯一。
-
<runlevels>:busybox不支持runlevel,所以此字段完全被忽略。
-
<action>:为下列这些值之一:sysinit, respawn, askfirst, wait,once, restart,ctrlaltdel, shutdown
-
其含义与通常的inittab的定义相同。特别提一下askfirst,它的含义与respawn相同,只是在运行process前,会打出一句话“please press Enter to active thisconsole”,然后等用户在终端上敲入回车键后才运行process。
所以在理解了busybox的initab格式之后,创建最小化的inittab文件如下:
$ cat$MYLINUX/etc/inittab
::sysinit:/etc/init.d/rcS
tty1::askfirst:/bin/sh
tty2::askfirst:/bin/sh
tty3::askfirst:/bin/sh
tty4::askfirst:/bin/sh
tty5::askfirst:/bin/sh
tty6::askfirst:/bin/sh
::askfirst:/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount-a -r
为了避免在reboot时报找不着fstab文件,可:touch$MYLINUX/etc/fstab
5,创建根文件系统的init脚本(initrc使用linuxrc,而initramfs使用init脚本,所以先删除busybox为initrc生成的默认的linuxrc文件,rm-rf$LINUX/linuxrc),init文件可以是任何ELF格式的可执行性文件,例如如果是一个编译过的C程序也是可以的,记得安装编译c文件要用到的库:sudoyum install -y glibc-static libstdc++-static。这里使用bash脚本:
cat $MYLINUX/init
#!/bin/sh
mount -vt proc proc/proc
mount -vt sysfs sysfs/sys
mdev -s
/bin/sh
其中mdev -s命令是使用busybox来创建dev目录里的udev设备,注意,它必须在加载内核文件系统sysfs和proc(很多工具都要从proc里读数据)之后再执行。接着,再创建两个基本的静态设备:
sudo mknod -m 600$MYLINUX/dev/console c 5 1
sudo mknod -m 600$MYLINUX/dev/null c 1 3
另外,mdev命令需要读取/etc/mdev.conf文件,为避免出错信息,我们创建一个空文件:
touch$MYLINUX/etc/mdev.conf
chmod +x $MYLINUX/init
6,使用chroot在未make内核之前就可以先测试initramfs:
sudo mount -v -o bind/dev $MYLINUX/dev
$ sudo chroot $MYLINUX/bin/sh
/ # ls
bin dev etc init liblist proc sys usr
create_lib.sh download hello.c lib64 linuxrc sbin sysfs
/ #
如果执行sudochroot $MYLINUX /bin/bash报错:bash:applet not found,那是正常现象,因为busybox只支持ash和sh,不支持bash。
7,构建initramfs,先配置支持Generalsetup -> Initial RAM filesystem and RAM disk (initramfs/initrd)support (默认就是支持的),再将linux内核的CONFIG_INITRAMFS_SOURCE(Generalsetup -> initramfs sourcefile(s))指向这个目录,然后执行make命令内核就会自动构建生成initramfs了(是一个压缩过的cpio档案文件),并且将它链接到内核镜像中。
其实我们也可以用cpio命令生成单独的initramfs,与内核编译脱沟,在内核运行时以initrd的形式加载到内核。我们先配置这种使用单独的initrd的方式。先将上述INIRAMFS_SOURCE内空清空,然后用内核源码树usr目录下已由内核编译生成的initramfs文件/bak/images/download/linux-2.6/usr/initramfs_data.cpio即可。
注意:不要把linux源码放在之前的mylinux目录下了,这样会造成生成的initrd文件很大的
cd /bak/images/download#
git clonegit://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
git tag
git checkout -b v3.9
#umount之前测试加载的proc,sys, dev目录
sudo umount$MYLINUX/proc
sudo umount$MYLINUX/sys
sudo umount$MYLINUX/dev
make menuconfig ,然后确保CONFIG_INIRAMFS_SOURCE=/bak/images/mylinux
sudo yum installncurses-devel
make, make实际包含了(确定依赖性make dep,清理编译中间文件,makeclean,编译内核,make bzImage,生成模块,make modules)
8,调试验证。可以使用物理机验证,也可以采用qemu虚机验证。如果使用物理机来验证的话,本来可以直接使用makeinstall命令自动安装,但最好不要这么做,因为它会自动执行updategrub命令来破坏你的grub文件,可以手工做:
sudocp arch/x86_64/boot/bzImage /boot/vmlinuz-3.9
sudo chmod a+xvmlinuz-3.9
注意:以vmlinuz-<version>这样命名它。
安装模块,sudomake modules_install命令会将模块自动安装到/lib/modules/3.9/目录下.
安装符号表[可选],sudocp System.map/boot/System.map-3.9,只有调试时才需要用到。符号表System.map用以将内核符号和它们的起始地址对应起来,调试的时候,如果需要把内存地址翻译成容易理解的函数名和以及变量名,就会很有用。
将initrd文件(/bak/images/download/linux-2.6/usr/initramfs_data.cpio)也可以拷到/boot目录下。
最后在GRUB(menu.lst)中添加的启动项可以是:
#test forinitramfs
title test for initramfs (on /dev/sda)
root(hd0,0)
kernel /boot/vmlinuz-3.9 (注意:并没有向内核传递root参数信息)
initrd/boot/ initramfs_data.cpio
这里我们使用qemu命令(qemu-system-x86_64-kernel /bak/images/download/linux-2.6/arch/x86_64/boot/bzImage)验证刚刚生成的内核和initramfs,如下图:
如果还想调试内核的话可以加-S参数(qemu-system-x86_64 -kernel/bak/images/download/linux-2.6/arch/x86_64/boot/bzImage-S),这时系统会跳出一个黑显示界面,无任何内容。此时通过ctrl+alt+1与ctrl+alt+2可以切换,一个是屏幕输出,一个是qemu控制台。切换到qemu控制台,输入命令gdbserver1234,同时打开gdb程序(当然前提是你的initramfs这个根文件系统得安装gdb),通过qemu说明中提供的方法既可进行调试:
Thenlaunch gdb on the 'vmlinux' executable:
> gdb vmlinux
Ingdb, connect to QEMU:
(gdb) target remote localhost:1234 #如设置断点在内核的第一个C程序上breakstart_kernel
9,initramfs是一个最小化的根文件系统,它的根本目的是为了为切换到新的根文件系统做准备。initrd方式使用pivot_root命令切换到新的根文件系统,然后卸载ramdis,但是initramfs方式由于它是本身就是rootfs(rootfs是ramfs的一个实例)位于内存中,所以它既不能pivot_root,也不能umount,是常驻内存的。所以为了从initramfs中切换到新根文件系统,需做如下处理:
-
删除rootfs的全部内容,释放空间,find -xdev / -exec rm '{}' ';'
-
安装新的根文件系统,并切换,cd/newmount; mount --move . /; chroot .
-
把stdin/stdout/stderr附加到新的/dev/console,然后执行新文件系统的init程序
上述步骤比较麻烦,而且要解决一个重要的问题:第一步删除rootfs的所有内容也删除了所有的命令,那么后续如何再使用这些命令完成其他步骤?busybox的解决方案是,提供了switch_root命令,完成全部的处理过程,使用起来非常方便。switch_root命令的格式是:
switch_root[-c /dev/console] NEW_ROOT NEW_INIT[ARGUMENTS_TO_INIT]
其中NEW_ROOT是实际的根文件系统的挂载目录,执行switch_root命令前需要挂载到系统中;NEW_INIT是实际根文件系统的init程序的路径,一般是/sbin/init;-c/dev/console是可选参数,用于重定向实际的根文件系统的设备文件,一般情况我们不会使用;ARGUMENTS_TO_INIT则是传递给实际的根文件系统的init程序的参数,也是可选的。需要特别注意的是:switch_root命令必须由PID=1的进程调用,也就是必须由initramfs的init程序直接调用,不能由init派生的其他进程调用,否则会出错,提示:switch_root:not rootfs
也是同样的原因,init脚本调用switch_root命令必须用exec命令调用,否则也会报同样的错。
这里我们先用的linux-0.2.img(http://wiki.qemu.org/download/linux-0.2.img.bz2)作为新根文件系统。所以我们将上面initramfs的init脚本添加两句。注意,switch_root命令由于是通过exec命令调用的,这样新根文件系统的/sbin/init进程会完全替换到initramfs的init进程空间,如果失败也会接着调用/bin/sh而不至于panic:
mount /dev/sda /mnt #为了简单,我们直接把CLFS分区写死在init脚本中,也可使用GRUB选择
exec switch_root /mnt/sbin/init
最终变成:
cat $MYLINUX/init
#!/bin/sh
mount -vt proc proc/proc
mount -vt sysfs sysfs/sys
mdev -s
mount /dev/hda /mnt
exec switch_root/mnt /sbin/init
/bin/sh
在重新编译内核生成新的intramfs之后(直接在内核目录执行makeclean && make命令即可),在qemu命令中添加参数“-hda /bak/images/linux-0.2.img -append"root=/dev/hda" ”进行测试,完整的命令如下,:
qemu-system-x86_64 -kernel/bak/images/download/linux-2.6/arch/x86_64/boot/bzImage -hda/bak/images/linux-0.2.img -append "root=/dev/hda"
注意此处使用的/dev/hda应与上面init脚本中的”mount/dev/hda /mnt”一致,它将新根文件系统mount到了/mnt目录,当然,也可以直接mount到根目录/下。现在网上很多linux系统安装到arm上实际上就是这个原理。
10,对模块的支持,到目前为止,我们在构建initramfs时还没有涉及内核模块的支持,所用到的硬件驱动程序都是直接编译到内核中。现在我们就看看如何使initramfs支持内核模块。
首先,内核配置要支持模块,并支持内核模块的自动加载功能:在内核配置菜单中的激活下面的配置项,编译进内核Load module support / Enable loadable modulesupport / Automatic kernel loading;然后把需要的硬件驱动程序配置模块形式,比如把硬盘控制器的驱动编译成模块,则选择:
DeviceDriver
|---->SCSI device support
|---->SCSI disksupport
|----->verbose SCSI error reporting(不是必须的,但可方便问题定位)
|----->SCSIlow-level drivers
|---->Serial ATA (SATA) support
|---->intel PIIX/ICH SATA support
通过makeclean && make命令编译内核后,通过INSTALL_MOD_PATH参数把编译好的内核模块安装批定目录:makeINSTALL_MOD_PATH=$MYLINUX/image modules_install
命令执行完毕后,在$MYLINUX/image/lib/modules/3.9/kernel/drivers/scsi目录下安装了4个内核模文件:scsi_mod.ko、sd_mod.ko、ata_piix.ko、libata.ko,它们就是所需的硬盘控制器的驱动程序。
在initramfs的init脚本中使用它的时候注意顺利:
insmodscsi_mod
insmod libata
insmod ata_piix
insmod sd_mod
mdev -s 或者echo/sbin/mdev > /proc/sys/kernel/hotplug
mount /dev/sda8/mnt
exec switch_root /mnt /sbin/init
我们在加载完驱动模块后调用了mdev-s命令来生成硬盘的设备文件。其实,可以使用mdev的hotplug模式在加载内核时自动生成对应的设备文件。在执行insmod命令前,用echo/sbin/mdev >/proc/sys/kernel/hotplug命令设置系统的hotplug程序为mdev。后续使用insmod命令加载模块时,系统自动调用mdev生成相应的设备文件。注意:内核必须配置支持hotplug功能。
总结:
整个过程是,initrc或者initramfs都是一个运行在内存的小根文件系统,它有一个叫init的脚本,做完一些准备工作之后,如加载硬件的驱动,然后会切换到镜像所在的新根文件系统上,下面就是一个intramfs中init脚本的例子:
#!/bin/sh
mount -vtproc proc /proc #很多工具都读proc的数据,故先加载
mount-vt sysfs sysfs /sys #加载内核文件系统
insmodscsi_mod
insmod libata
insmod ata_piix
insmodsd_mod
mdev -s 或者echo/sbin/mdev > /proc/sys/kernel/hotplug
#加载硬盘,或者直接加到根目录/中
mount/dev/sda /mnt
#通过exec会让镜像中的init进程完全替换initramfs中的init进程的空间来切换根文件分区
exec switch_root /mnt /sbin/init
#如果上述切换根文件分区失败,还可以使用initramfs的sh进程,否则会panic
/bin/sh
所以说,这个镜像应该是linux内核直接可以认的文件系统格式,如ext4,虚机使用的文件格式像raw,qrow2等转换成ext4等格式(方法见:编译linuxkernel及制作initrd
)。这样也就可以直接通过dd命令将镜像拷到/dev/sda硬盘中了(gunzip -c /mnt/sda1/hda.img.gz | dd of=/dev/hda conv=sync,noerrorbs=64K )
使用dd命令将硬盘备份到外部存储移动硬盘中的具体过程,显然,dd不管硬盘是不是都用了备份的是整个分区。有个叫再生龙(Clonezilla)的工具就是来克服这个缺点的:
-
加载移动硬盘,mount-t vfat /dev/sda1 /mnt/sda1
-
dd备份,ddif=/dev/hda conv=sync,noerror bs=64K | gzip -c >/mnt/sda1/hda.img.gz
-
恢复,gunzip-c /mnt/sda1/hda.img.gz | dd of=/dev/hda conv=sync,noerror bs=64K
参考:
http://blog.csdn.net/quqi99/article/details/8546687
http://www.360doc.com/content/13/0414/21/7044580_278272334.shtml