u-boot 的主要的目的是启动内核
内核的主要目的是挂载文件系统并启动应用程序。
内核怎么启动第一个应用程序:
分析代码:
asmlinkage void __init start_kernel(void)
static void noinline __init_refok rest_init(void)
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); // 启动 kernel_init 内核线程
prepare_namespace(); // 挂载根文件系统
init_post(); // 启动应用程序
分析 init_post() 代码:
-------------------------------------------------
static int noinline init_post(void)
{
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) // 打开标准控制台 对应 printf
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0); // 拷贝标准控制台"/dev/console" 对应 scanf
(void) sys_dup(0); // 拷贝标准控制台"/dev/console" 对应 err
// 我们这里控制台用的是串口0 也可以是其他,比如显示屏和键盘的组合
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* 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) { // 搜索 execute_command 它对应的是 "init=" 后面的参数
run_init_process(execute_command); // 执行 u-boot 传入的参数 init=/linuxrc run_init_process 函数,成功执行便不会再返回
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.");
}
-------------------------------------------------
想要知道这个文件系统有哪些东西,我们就得跟着 /sbin/init 进程一路分析下去,看些这个进程需要哪些东西。
根文件系统中许多命令 ls cp ... 这些命令都是应用程序,但是Linux中有许多命令,我们不可能都要把这些命令重新写一遍,
在嵌入式系统里面,我们有一个东西叫做 busybox 它是 ls cp cd 这些命令的合集,我们去编译 busybox ,
会得到一个 busybox 应用程序,ls cp cd 这些命令都是 busybox 的一个链接,到我们执行这些命令时实际运行的是 busybox 应用程序。
我们可以执行如下命令来验证:
ls -l /bin/ls
ls -l /bin/cp
busybox ls 也能实现 ls 的功能
在嵌入式系统中,/sbin/init 也是到 busybox 的链接
ls -l /sbin/init
所以我们想要知道 /sbin/init 进程做了哪些事情,我们需要分析 busybox 的源码。
busybox 中每一个命令都对应一个 .c 文件,我们来分析 init.c
busybox -> init_main
parse_inittab(); // 解析 init 进程 表
file = fopen(INITTAB, "r"); //打开文件INITTAB 表 "/etc/inittab" /* inittab file location */ inittab 的格式请查看文档 /examples/inittab <id>:<runlevels>:<action>:<process>
文件打开失败,解析默认项
new_init_action(ASKFIRST, bb_default_login_shell, VC_2);
文件打开成功,解析配置文件
new_init_action // 解析配置文件 把解析的内容放到一个链表中
run_actions(SYSINIT); // 运行这一类动作
run_actions(WAIT);
run_actions(ONCE);
分析:
new_init_action(ASKFIRST, bb_default_login_shell, VC_2);
替换宏:
const char bb_default_login_shell[] ALIGN1 = LIBBB_DEFAULT_LOGIN_SHELL;
#define LIBBB_DEFAULT_LOGIN_SHELL "-/bin/sh"
#define ASKFIRST 0x004
# define VC_2 "/dev/tty2"
new_init_action(ASKFIRST, "-/bin/sh", /dev/tty2);
------------------------
static void new_init_action(int action, const char *command, const char *cons) // 参数 执行时机,命令或脚本,终端
{
struct init_action *new_action, *a, *last;
if (strcmp(cons, bb_dev_null) == 0 && (action & ASKFIRST))
return;
/* Append to the end of the list */
for (a = last = init_action_list; a; a = a->next) {
/* don't enter action if it's already in the list,
* but do overwrite existing actions */
if ((strcmp(a->command, command) == 0)
&& (strcmp(a->terminal, cons) == 0)
) {
a->action = action;
return;
}
last = a;
}
new_action = xzalloc(sizeof(struct init_action));
if (last) {
last->next = new_action;
} else {
init_action_list = new_action;
}
strcpy(new_action->command, command);
new_action->action = action;
strcpy(new_action->terminal, cons);
messageD(L_LOG | L_CONSOLE, "command='%s' action=%d tty='%s'\n",
new_action->command, new_action->action, new_action->terminal);
}
struct init_action {
struct init_action *next; // 链表指针
int action; // 执行时机
pid_t pid; // process ID 进程ID
char command[INIT_BUFFS_SIZE]; // 命令或脚本
char terminal[CONSOLE_NAME_SIZE]; // 终端
};
------------------------
inittab 格式: 详见源码下文档 /examples/inittab
<id>:<runlevels>:<action>:<process>
id : id=/dev/id,用做终端:stdin, stdout, stderr :printf , scanf , err 等
runlevels : 忽略
action : 执行时机 sysinit, respawn, askfirst, wait, once,restart, ctrlaltdel, and shutdown.
process : 应用程序或脚本
最小根文件系统需要:
1. dev/console、 dev/null // 控制台
2. init 应用程序 -> busybox // 第一个应用程序
3. 配置文件 /etc/inittab // 指定需要启动的程序表 指定ID 启动方式 和 时机
4. 配置文件中的应用程序 // 应用程序
5. 应用程序所需要的 C 库 // 库
配置和编译busybox
解压 tar xjf busybox-1.7.0.tar.bz2
查看 源文件目录下的 INSTALL 文件 查看编译方法
make menuconfig # This creates a file called ".config"
make # This creates the "busybox" executable
make install # or make CONFIG_PREFIX=/path/from/root install
问题 : make menuconfig 报错
Makefile:405: *** mixed implicit and normal rules: deprecated syntax
Makefile:1242: *** mixed implicit and normal rules: deprecated syntax
方法:
在makefile中将405行代码
config %config: scripts_basic outputmakefile FORCE
改为
%config: scripts_basic outputmakefile FORCE
在makefile中将1242行代码
/ %/: prepare scripts FORCE
改为
%/: prepare scripts FORCE
配置:参考韦老师书本 17.2.3 章
Busybox Setting -->
Busybox Library Tuning -->
[*、] Tab completion // 命令行 tab 补全
Build Options --->
[ ] Build BusyBox as static binary (no shared libs) // 静态链接, 我们用动态库
Archival Utilities --> // 压缩命令
// 保持默认
Linux Module Utilities ---> // 模块加载命令
// 保持默认
Linux System Utilities ---> // 支持mdev , 这可以很方便的构造 /dev 目录
// 保持默认
Networking Utilities ---> // 网络
// 保持默认
保存退出
make CROSS_COMPILE=arm-linux- // 指定工具链 编译 或者在 Makefile 中定义。
安装
mkdir -p /work/first_fs
// make CONFIG_PREFIX=/work/first_fs install //安装到指定目录 如果没有修改 Makefile 文件 的编译工具链 请使用下面的命令
make CROSS_COMPILE=arm-linux- CONFIG_PREFIX=/work/first_fs install
查看
cd /work/first_fs
ls -l
drwxrwxr-x 2 book book 4096 6月 18 08:20 bin
lrwxrwxrwx 1 book book 11 6月 18 08:20 linuxrc -> bin/busybox // u-boot 传递进来第一运行的程序 Linuxrc
drwxrwxr-x 2 book book 4096 6月 18 08:20 sbin
drwxrwxr-x 4 book book 4096 6月 18 08:20 usr
ls -l bin/ // 这些命令都是指向 busybox 的链接
构造最小根文件系统
创建设备节点:
先查看本机系统的控制台
ls /dev/console /dev/null -l
crw------- 1 root root 5, 1 6月 18 07:26 /dev/console
crw-rw-rw- 1 root root 1, 3 6月 18 07:26 /dev/null
c 字符设备 5 主设备号 1 次设备号
mkdir dev
cd dev/
sudo mknod console c 5 1
sudo mknod null c 1 3
ls -l
创建 inittab 表:
cd .. // 返回根目录
mkdir etc
vi etc/inittab 添加 console::askfirst:-/bin/sh 项 启动一个 -/bin/sh 并显示到终端 console
安装 C 库:
创建 lib 目录
mkdir lib
cd /work/tools/gcc-3.4.5-glibc-2.3.6/arm-linux/lib
cp *.so* /work/first_fs/lib/ -d // -d 参数表示保持链接 不加会把链接也拷贝成链接对应的文件,这样体积会很大
最小根文件系统已经做好了,怎么烧写到开发板?
我们需要做一个映像文件,怎么制作?
参考书本 17.4.4
编译 yaffs 工具 ,用编译后的 工具 打包制作根文件系统
cd /work/systems/
tar xjf yaffs_source_util_larger_small_page_nand.tar.bz2
cd Development_util_ok/yaffs2/utils/
make
sudo cp mkyaffs2image /usr/local/bin/
sudo chmod +x /usr/local/bin/mkyaffs2image
打包根文件系统
cd /work/
mkyaffs2image first_fs first_fs.yaffs2
烧写 first_fs.yaffs2
发现 不能执行 ps 命令
ps: can't open '/proc': No such file or directory
系统的当前状态保存在一个虚拟的 proc 文件系统,并且挂载在 /proc 目录
在单板上运行
mkdir proc
mount -t proc none /proc
然后再执行 ps 就可以查看进程了
proc 是一个虚拟的文件系统,由内核提供的
cd proc
cd 1
ls -l fd
lrwx------ 1 0 0 64 Jan 1 00:19 0 -> /dev/console // 标准输入
lrwx------ 1 0 0 64 Jan 1 00:19 1 -> /dev/console // 标准输出
lrwx------ 1 0 0 64 Jan 1 00:19 2 -> /dev/console // 标准错误
完善文件系统
mkdir proc
添加自动挂载功能
vi etc/inittab
添加启动项
::sysinit:/etc/init.d/rcS
创建
mkdir etc/init.d
vi etc/init.d/rcS
添加
mound -t proc none /proc
添加可执行权限
chmod +x etc/init.d/rcS
或者
vi etc/init.d/rcS
# mound -t proc none /proc
mount -a // 读取 /etc/fstab 中的内容
根据 etc/fstab 中的内容来挂载
vi etc/fstab
# <file system> <mount point> <type> <options> <dump> <pass>
proc /proc proc defaults 0 0
cat /proc/mounts // 查看系统已经挂在的文件系统
继续完善文件系统
在 /dev 目录下有两个设备,这是我们手动创建的
# ls dev/ -l
crw-r--r-- 1 0 0 5, 1 Jan 1 00:01 console
crw-r--r-- 1 0 0 1, 3 Jun 18 2018 null
如果一个系统里面有成千上万个设备,我们不可能全部手工来创建,这里介绍 udev
udev : 自动创建 /dev 下的设备节点,busybox 中有一个 udev 的简化版本 mdev
我们来介绍 mdev 的用法,查看文档 busybox-1.7.0\docs\mdev.txt 或书本 17.4.2.2
mkdir sys
vi /etc/fstab
添加
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
vi /etc/init.d/rcS
添加
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug // 热拔插 指向 /sbin/mdev 当系统设备有变化时,会调用 /sbin/mdev
mdev -s // 创建现有设备节点
重新烧录
ls dev/ 查看设备节点发现自动创建了很多
cat /proc/mounts // 查看系统已经挂载的文件系统
到此我们已经制作了一个较为完善的最小根文件系统了!
扩展知识:
果我们想使用其他的文件系统 如 jffs2 韦老师书本17.4.5
jffs2 文件系统适用于 NOR Flash ,也可以使用在 NAND Flash 上
cd work/GUI/xwindow/X/deps/
tar xzvf zlib-1.2.3.tar.gz
cd zlib-1.2.3/
./configure --shared --prefix=/usr // --shared 表示动态库 --prefix=/usr 表示安装目录
make
sudo make install
zlib 是一个压缩库,编译 jffs2 工具要用到
编译 mkfs.jffs2
cd /work/tools/
tar vxjf mtd-utils-05.07.23.tar.bz2
cd mtd-utils-05.07.23/util/
make
sudo make install
制作文件系统镜像
cd /work/
mkfs.jffs2 -n -s 2048 -e 128KiB -d first_fs -o first_fs.jffs2 // -s 页大小 -e 块大小 -d 目录 -o 输出文件名
烧写后不能正常启动,还是以yaffs2 方式识别文件系统,我们可以修改启动参数 强制以 jffs2 方式启动
set bootargs noinitrd root=/dev/mtdblock3 rootfstype=jffs2 init=/linuxrc console=ttySAC0
save
boot
我们每次修改文件系统都要重新烧写,有没有更好的办法?
答:有,NFS 网络文件系统
ifconfig eth0 up
ifconfig eth0 192.168.1.12 //同一网段
NFS:
a. 从 Flash 上启动根文件系统,在用命令挂接NFS
b. 直接从NFS 启动
挂接 NFS 条件:
1. 服务器允许那个目录可以被挂接
指定被挂接的目录
修改 sudo vi /etc/exports
/work/first_fs *(rw,sync,no_root_squash)
重启启动NFS服务
sudo /etc/init.d/nfs-kernel-server restart
本机挂接测试
sudo mount -t nfs 192.168.100.101:/work/first_fs /mnt
-----------------------------
a. 首先创建挂载点: mkdir /mnt/test1
b. 然后挂载nfs: mount -t nfs 192.168.1.3:/nfs_test /mnt/test1
c. 挂载成功之后通过 df -h 可以查看挂载的情况,nfs可用空间就是服务端/nfs_test目录所能使用的最大空间
d. 卸载nfs和普通文件系统一样,使用: umount /mnt/test1
-----------------------------
开发版挂接
mkdir mnt
mount -t nfs -o nolock 192.168.100.101:/work/first_fs /mnt
------------------------------------------------------------
直接从NFS 启动文件系统:
1. 服务器IP 根文件目录
2. 裸板IP
修改启动参数,参考内核源码\Documenttation\nfsroot.txt
bootargs=noinitrd root=/dev/mtdblock3 rootftype=jffs2 init=/linuxrc console=ttySAC0
set bootargs noinitrd root=/dev/nfs nfsroot=192.168.123.200:/work/first_fs ip=192.168.123.201:192.168.123.200:192.168.123.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0
save
也可以开启路由器的NFS
http://www.right.com.cn/forum/thread-182695-1-1.html
我的路由器可以直接在 USB应用程序 -> NFS 服务器
将ubuntu 下的busybox 创建的根文件系统拷贝到路由器的U盘上
sudo cp -r /work/first_fs/* first_fs/ -d
登陆路由器添加共享的文件夹
vi /etc/exports
/media/upan/first_fs 192.168.123.0/24(rw,async,insecure,no_root_squash,no_subtree_check)
串口登陆开发板,修改保存参数
set bootargs noinitrd root=/dev/nfs nfsroot=192.168.123.1:/media/upan/first_fs ip=192.168.123.201:192.168.123.1:192.168.123.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0
save
这样路由器下的电脑都可以把这个网络文件系统挂载在自己的电脑上,然后进行驱动调试,只需要把开发板网线连接路由器上然后通过telnet登陆开发板即可。
--------------------------------------------------------------
测试:在主机上编译文件,在开发板上运行。
vi hello.c
#include <stdio.h>
int main()
{
printf("xiaoci,hello!\n");
return 0;
}
arm-linux-gcc -o hello hello.c
在开发板对应目录下执行
./hello
xiaoci,hello!
文件系统更详细配置请看:http://bbs.100ask.net/forum.php?mod=viewthread&tid=17389
修改密码出现:
passwd:unknown uid 0
解决方法:
自动生成是使用了busybox提供的adduser工具和passwd工具。
在文件系统正常运行起来后,使用adduser命令,使用方法为:
#adduser root
然后就会在etc目录下自动生成passwd 、group和shadow3个文件。但是运行该命令后会打印出如下消息:
passwd:unknown uid 0
这表示不能为该用户设置密码,此时你会发现要passwd命令也无法使用。
解决的办法是,打开passwd文件,其内容为:
root:x:1000:1000:Linux User…:/home/root:/bin/sh
将用户ID和组ID均更改为0
打开group文件,其内容为:
root:x:1000:
同样将组ID改为0
然后,passwd命令就可以正常使用了。这时为root用户设置口令:
#passwd root
根据提示输入密码。其中,root用户登陆后的目录可以手动进行更改。
参考:https://www.cnblogs.com/liangwode/p/5710343.html?utm_source=itdadao&utm_medium=referral
telnet 远程登陆
/etc/init.d/rcS
中加上
telnetd& // 开机启动 telnetd 服务
就可以用 telnet IP 命令进行登陆 输入账号 密码
这样就不需要串口了
但是 telnet 无法显示内核打印信息
方法1:
可以通过 cat /proc/kmsg& 来查看 内核输出信息
ps
kill 11021
方法2:
dmesg -c //显示最新的信息 并清除
离线