看到飞凌嵌入式联合北京大学出版社推出《嵌入式Linux系统开发21天速成》这本书的消息,我第一反应是:这年头敢用“21天”命名技术书的出版社和作者,要么是真有底气,要么就是准备挨骂。作为一个在嵌入式Linux这行摸爬滚打了十多年、从单片机转Linux应用再转驱动、踩过无数坑的老工程师,我想借着这本新书的话题,聊聊我对嵌入式Linux学习路线、核心技术点的理解,也把我这些年积累的一些实操经验和排查技巧整理出来。不管你是刚入手开发板的新手,还是已经在做应用开发想往底层钻的工程师,这篇内容应该都能给你一些参考。
1. 关于“21天速成”这件事,我的真实看法
先说实话,我特别反感那种“X天精通XXX”的标题,但“21天速成”放在嵌入式Linux这个领域,倒不完全是标题党。嵌入式Linux的知识体系虽然庞杂,但它的核心链路其实是相对固定的:会C语言、懂Linux基础操作、能交叉编译、会看原理图和数据手册、懂点硬件寄存器操作,再加上设备树和驱动模型的理解,基本就能跑通一个完整的开发流程。21天能不能让你成为驱动开发专家?肯定不能。但21天足够让你把一个开发板从点灯带到跑起根文件系统、加载自己的驱动模块,这个过程一旦跑通,后面的学习就是加速度。
1.1 为什么很多人学嵌入式Linux半途而废
这些年我带过不少新人,也看过很多论坛求助帖。我发现大多数人学不下去不是因为笨,而是踩了三个坑:
第一个坑是环境问题劝退。很多人装个VMware、装个Ubuntu就折腾了两三天,然后卡在交叉编译工具链上,最后连hello world都没跑起来就放弃了。这个真不是技术问题,是教程写得太飘,缺了“保姆级”的细节。
第二个坑是知识点太散。今天看一篇讲文件系统的文章,明天看一篇讲中断的文章,没有一个主线把CPU、内存、存储、外设这些点串起来,学完就忘。
第三个坑是不重视根文件系统。很多新手把注意力全放在内核编译上,结果内核起来了,initramfs里啥都没有,shell都进不去,更别说跑自己的程序。实际上根文件系统挂载是整个嵌入式Linux启动流程中最容易出错、也最体现功力的环节。
1.2 这本书能帮你解决什么问题
飞凌嵌入式是做开发板起家的厂商,他们这本书的优势在于有真实的硬件平台作为依托。书里讲的不是悬空的原理,而是基于他们自家板子的实际操作流程。我翻过类似定位的书,这种“原厂工程师写实战”的路线,对于新手来说往往比大学教授写的理论书更友好,因为原厂工程师每天就是在解决“为什么我的板子起不来”这类问题。
这本书比较适合两类人:一类是刚买了开发板、想系统跑通整个流程的新手;另一类是在应用层写了好几年代码、想往底层驱动的方向转的工程师。它更像是一张地图,让你知道嵌入式Linux的版图是什么样的,哪些地方是主干道,哪些地方可以以后再去探索。
2. 学习嵌入式Linux必须打通的核心知识链路
如果你想把21天的效率最大化,首先要搞清楚嵌入式Linux开发的知识链路长什么样。它不是线性的,而是像一张网,但有一条主线:交叉编译 → Bootloader引导 → 内核启动 → 根文件系统挂载 → 驱动加载 → 应用运行。
2.1 交叉编译:嵌入式开发的第一道门槛
交叉编译这个概念,说白了就是在你的x86电脑上编译出ARM架构能运行的二进制文件。我们日常用的gcc编译出来的程序,只能在同架构的CPU上跑,但开发板是ARM架构,所以必须用arm-linux-gnueabihf-gcc这类工具链来编译。
这里有个关键参数需要理解:-march和-mfpu。这两个参数决定了编译器生成什么样的指令。比如飞凌的板子如果是Cortex-A7架构,你需要用-march=armv7-a来指定指令集架构,用-mfpu=neon来开启NEON SIMD指令支持。如果你编译时指定的架构比实际CPU高,编译出来的程序在板子上会报“Illegal instruction”错误,这个问题在初学者中非常常见。
举个例子,如果你在Ubuntu上安装了gcc-arm-linux-gnueabihf工具链,最简单的交叉编译命令是:
arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -o hello hello.c编译完用file hello命令可以看到输出:
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked...看到ARM字样就说明编译架构正确。很多新手在这一步会用错工具链,编译出来一个x86的程序,拿到板子上当然跑不了。
2.2 内核编译:别在配置选项里迷失方向
内核编译是新手最容易迷失的环节。make menuconfig里面有几千个选项,密密麻麻的。如果你在一块新板子上要从零配置内核,最佳实践是先从一个默认配置开始,然后逐步裁剪或增加。
飞凌这类厂商一般会提供官方内核源码和配置文件,通常在源码目录下的arch/arm/configs/里。比如:
make ARCH=arm xxx_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j4编译完成后在arch/arm/boot/目录下会生成zImage,这就是你要烧写到板子上的内核镜像。这里我想强调一个很多教程不会细说的点:DTS(设备树)编译。现在的ARM Linux内核,板级硬件描述全靠设备树文件(.dts),它会被编译成.dtb文件,然后由Bootloader传递给内核。如果你改了硬件配置,比如换了一个LED引脚,就需要修改dts并重新编译dtb:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs新手常犯的错误是:改完dts后只重新编译了zImage就烧写,忘记烧写dtb。结果板子启动到一半崩溃,或者外设不工作,排查半天才发现是dtb没更新。
2.3 Bootloader:U-Boot是连接硬件与内核的桥梁
U-Boot在整个启动链路中的作用,很多教程只是草草带过。实际开发中U-Boot是调试利器。飞凌的板子一般提供两种启动方式:SD卡启动和eMMC启动。SD卡启动是开发阶段的救命稻草,因为你可以通过修改SD卡上的环境变量来调整启动参数,而不需要频繁烧写eMMC。
U-Boot里最重要的环境变量有三个:
bootcmd:自动启动时执行的命令,通常是一条完整的启动脚本。bootargs:传递给内核的启动参数,内核命令行解析全靠它。loadaddr:内核镜像加载到内存的地址。
我见过最典型的坑就是bootargs里面没写root=参数,导致内核挂载根文件系统失败,内核panic。后面我会详细展开NFS挂载根文件系统的配置,那是开发阶段效率最高的做法。
3. 实操核心:根文件系统挂载与NFS开发环境的搭建
为什么把根文件系统单独拉出来说?因为这是从“内核跑起来”到“应用跑起来”的分水岭。内核如果启动成功,最后一步永远是尝试挂载根文件系统(rootfs)。如果这一步失败,你只能看到一个内核panic的界面,什么也做不了。
3.1 根文件系统里到底需要什么
一个能用的Linux根文件系统,至少需要这几个部分:
/bin、/sbin:基础的shell命令和系统管理工具,通常由BusyBox提供。/dev:设备节点,虽然现在很多是devtmpfs自动创建的,但/dev/console和/dev/null必须有。/etc:配置文件目录,至少要有一个inittab(SysVinit风格)或systemd相关的目录。/lib:C运行库,如libc.so.6,以及动态链接器ld-linux-armhf.so.3。/proc、/sys、/tmp:挂载点目录,后面要挂载procfs、sysfs和tmpfs。
新手如果从零制作根文件系统,最核心的工具是BusyBox。它可以让你用一条命令编译出大部分需要的命令行工具:
make menuconfig # 选择静态编译 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install注意,BusyBox强烈建议用静态编译(CONFIG_STATIC=y),这样你生成的文件系统不依赖目标板上的动态库,拷贝过去就能用。而你的应用程序(比如交叉编译的hello)如果依赖动态库,你可能需要把交叉工具链里的libc.so.6等文件一起拷贝到文件系统的/lib目录,并且用readelf -d hello查看程序依赖了哪些库,确保每个依赖都在/lib下能找到。
3.2 NFS挂载根文件系统:开发阶段的效率神器
这是我要重点讲的实操内容,也是热搜词里大家最关心的点之一:如何使用NFS v3挂载根文件系统。
NFS开发模式的基本思想是:你的Ubuntu主机上存放着完整的根文件系统目录,开发板通过以太网挂载这个目录作为自己的根文件系统。这样你改了主机上的应用程序、脚本甚至库文件,开发板重启后立即生效,完全不需要反复烧写SD卡或eMMC。
3.2.1 主机端配置步骤
假设你的Ubuntu主机上有一个目录/opt/rootfs,它就是你开发板的根文件系统。首先安装NFS服务器:
sudo apt install nfs-kernel-server然后编辑配置文件/etc/exports,添加共享目录:
/opt/rootfs *(rw,sync,no_subtree_check,no_root_squash)这里有几个重要的选项需要解释:
no_root_squash:允许开发板的root用户拥有主机的root权限,否则开发板上的root会被映射成nobody,很多操作会权限不足。这是新手最容易忽略的参数。rw:允许读写。sync:同步写入,保证数据一致性,调试阶段更安全。no_subtree_check:关闭子树检查,减少NFS协议限制,提高兼容性。
保存后重启服务:
sudo exportfs -ra sudo systemctl restart nfs-kernel-server3.2.2 内核配置NFS客户端支持
开发板的Linux内核必须支持NFS客户端和rootfs挂载。在make menuconfig中需要勾选:
File systems ---> Network File Systems ---> <*> NFS client support [*] NFS client support for NFS version 3 [*] Root file system on NFS注意这里要勾选**“Root file system on NFS”**这个选项,它不是一个文件系统格式,而是允许内核在启动时将NFS挂载为根文件系统的标志。少了它,即使你的bootargs写了NFS启动参数,内核也会报错不支持。
3.2.3 Bootloader中的启动参数配置
在U-Boot命令行中设置:
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/opt/rootfs,v3 ip=192.168.1.99:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off rw'这里每个参数的含义一次说透:
console=ttymxc0,115200:指定串口控制台。飞凌的板子基于NXP i.MX系列一般是ttymxc0,如果基于全志或瑞芯微芯片,名字会不一样,需要看板子原理图。root=/dev/nfs:告诉内核根文件系统使用NFS,并不是真的有/dev/nfs这个设备节点。nfsroot=192.168.1.100:/opt/rootfs,v3:NFS服务器IP和共享目录。v3参数很关键,它告诉内核使用NFS v3协议。原因很简单:NFS v4的mount协议实现更复杂,在嵌入式环境下权限和端口协商问题比较多,而v3在嵌入式领域兼容性极好,也是大多数开发板默认支持的版本。如果不写v3,内核会默认尝试v4,而你的主机NFS服务器即使开着,也可能因为版本协商失败导致挂载卡住,最终内核panic。ip=192.168.1.99:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off:这个格式依次是“开发板IP:服务器IP:网关:子网掩码::网卡名:autoconf”。如果off表示关闭自动配置,全部手动指定。也就是说,开发板的IP是192.168.1.99,主机的IP是192.168.1.100,网关是192.168.1.1。注意两个IP要在同一网段,网卡名要和自己板子匹配(有的板子是eth0,有的可能是usb_ether)。
设置完成后执行:
saveenv boot如果一切正常,你会在串口终端里看到内核挂载NFS的日志,然后进入shell提示符。
3.3 我踩过的NFS挂载坑
NFS挂载根文件系统的坑太多了,我把最典型的几个列出来:
坑一:挂载后文件系统只读,无法修改。现象是启动后touch文件提示Read-only file system。原因多半是/etc/exports里漏了rw参数,或者bootargs里漏了最后的rw关键字。内核启动参数里的rw告诉内核根文件系统以读写方式挂载,没写的话默认是ro。
坑二:内核启动卡在“VFS: Unable to mount root fs via NFS”。这是最常见的错误。排查顺序是:先ping通主机IP吗?如果在U-Boot阶段可以ping 192.168.1.100,确认网络通;然后确认主机上/opt/rootfs目录里有/sbin/init或者/bin/busybox;最后确认内核配置里勾了NFS v3和Root file system on NFS。90%的情况是内核配置漏了项。
坑三:启动后网络接口名不是eth0,导致IP配置失败。新版内核中,如果你没有在dts里固定网络接口别名,内核可能把网卡命名为enp0s3之类(系统d风格的命名)。解决方法是修改dts中ethernet节点的alias,确保chosen节点里或aliases节点中把ethernet0 = &fec1指向你的网卡控制器,或者在内核启动参数中简单的使用ip=dhcp先获取动态地址,再排查。
坑四:NFS挂载成功,但运行应用程序时报“No such file or directory”。这个一般不是文件真的不存在,而是动态链接器路径问题。检查文件系统里/lib/ld-linux-armhf.so.3是否存在,用readelf -l查看你的程序的interpreter路径是否和文件系统里的一致。如果工具链是arm-linux-gnueabihf-,interpreter路径就是/lib/ld-linux-armhf.so.3。
4. 理解设备树:从点灯到驱动开发的关键一步
在NFS根文件系统搞定之后,下一步就是驱动开发了。而驱动开发在现在的ARM Linux中,几乎绕不开设备树(Device Tree,DT)。我经常把设备树比作硬件的“说明书”,内核通过读取这份说明书才知道哪个地址有寄存器、哪个引脚接了LED、哪个中断是哪个外设发出来的。
4.1 设备树的基本结构和语法
设备树文件是一个.dts文本文件,经过dtc工具编译成二进制.dtb。它的基本结构是节点(node)和属性(property)。一个最简单的LED节点长这样:
/ { leds { compatible = "gpio-leds"; user-led { label = "user_led"; gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; default-state = "off"; }; }; };这段代码表示在gpio1的第4号引脚上接了一个LED,高电平点亮。compatible属性是内核用来匹配驱动程序的“身份证”,内核里的gpio-leds驱动会检查这个值是否匹配。
对于初学者来说,设备树最大的坑是GPIO编号的计算方式。在设备树里<&gpio1 4>并不直接等于Linux里的gpio编号4。它指的是gpio1控制器下的第4个引脚,实际的编号通常是(控制器基号-1) * 32 + 引脚号。比如gpio1的控制器的基号是0,那么&gpio1 4对应的Linux gpio编号是0 * 32 + 4 = 4;如果用的是&gpio3 19,而gpio3基号是2,那就是2 * 32 + 19 = 83。如果你在写驱动时对不上号,点灯操作可能会点亮一个完全不相干的引脚。
4.2 从零编写一个简单的字符设备驱动
理解了设备树之后,驱动本身的编写其实有固定的套路。我以最常见的miscdevice(杂项设备)为例,它比普通的字符设备驱动简单不少,不需要手动分配主设备号。
驱动代码的核心框架如下:
#include <linux/module.h> #include <linux/miscdevice.h> #include <linux/fs.h> static int demo_open(struct inode *inode, struct file *file) { printk("demo device opened\n"); return 0; } static const struct file_operations demo_fops = { .owner = THIS_MODULE, .open = demo_open, }; static struct miscdevice demo_dev = { .minor = MISC_DYNAMIC_MINOR, .name = "demo", .fops = &demo_fops, }; static int __init demo_init(void) { return misc_register(&demo_dev); } static void __exit demo_exit(void) { misc_deregister(&demo_dev); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL");这个驱动做的事非常少:注册一个名为demo的设备,当用户空间open("/dev/demo")时打印一条日志。但麻雀虽小五脏俱全,它展示了驱动的基本骨架:module_init和module_exit负责加载/卸载时的初始化和清理,file_operations结构体将系统调用映射到驱动函数。
编译这个驱动需要用到内核源码树,最简单的做法是写一个Makefile:
obj-m := demo.o KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean不过要在开发板上编译,你需要把KERNEL_DIR改成你开发板内核源码的路径,并在编译时指定架构和交叉编译器:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL_DIR=/path/to/kernel生成的demo.ko通过NFS拷到开发板(如果你用NFS根文件系统,直接把文件放进/opt/rootfs/home目录就行),然后加载:
insmod demo.ko ls /dev/demo echo "test" > /dev/demo dmesg | tail看到“demo device opened”日志,你的第一个驱动就算跑起来了。
4.3 驱动开发的进阶方向与面试重点
如果你把上面的demo驱动跑通了,就已经完成了从应用开发者到驱动开发者的“入门跃迁”。但面试和实际项目中,光会写miscdevice是不够的。我根据自己的经验和对行业常见面试题的分析,总结了以下几个必须掌握的驱动知识点:
第一,并发与竞态控制。这是驱动开发中最重要的基础。当多个进程同时打开设备文件,或者中断处理函数和进程上下文同时访问共享数据时,必须使用自旋锁(spinlock)、互斥体(mutex)或原子变量来保护临界区。面试官爱问:“自旋锁和睡眠锁的区别是什么?”标准回答是:自旋锁在等待时不会睡眠,而是忙等待,适合临界区执行时间极短的场景;互斥体在等待时会睡眠,适合长时间持锁,但只能在进程上下文使用。这个知识点基本是驱动岗必问的。
第二,阻塞与非阻塞I/O。比如读取一个没有数据的设备,是让进程睡眠等待,还是立即返回-EAGAIN?这涉及等待队列(wait queue)和poll机制。Android的input子系统、传感器驱动、串口驱动都大量使用这个模型。理解它之后,你就能看懂为什么cat /dev/input/event0会卡在那里,直到你有触摸动作才返回。
第三,中断下半部机制。中断处理函数里不能做耗时操作,所以Linux提供了软中断、tasklet(旧机制)、工作队列等下半部机制。现在主流推荐是使用request_irq申请中断,配合threaded_irq线程化中断处理,或者使用内核线程来处理中断上下文不能做的事情。这一块是驱动开发的深水区,也是拉开水平差距的地方。
第四,与设备树的匹配机制。驱动里必须要有of_match_table,否则设备树里的节点无法和你的驱动关联。比如:
static const struct of_device_id demo_of_match[] = { { .compatible = "vendor,demo-device" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_of_match);然后你要在device_driver结构体中引用它,比如在miscdevice之上再加一个platform_driver来绑定设备树节点:
static struct platform_driver demo_driver = { .probe = demo_probe, .remove = demo_remove, .driver = { .name = "demo", .of_match_table = demo_of_match, }, }; module_platform_driver(demo_driver);这样你的驱动就能和设备树里compatible = "vendor,demo-device"的节点自动绑定了。每次驱动加载的时候,probe函数就会被调用。这里的probe是驱动开发者的主战场,硬件资源的获取、中断申请、设备文件注册全都在这里完成。
5. 如何利用“21天”规划你的学习路径
前面说了很多技术细节,但我知道大部分读者更关心的是:我到底该怎么安排这21天?我结合飞凌这本书的定位和行业现状,给一套可执行的学习计划。
5.1 三周时间的学习节奏安排
第1周(Day 1-7)目标是“让板子跑起来”。前2天搭建开发环境,装好Ubuntu虚拟机(推荐16.04或18.04,不是越新越好,太新的Ubuntu反而容易遇到交叉工具链兼容问题),配置好NFS和TFTP。第3-4天玩转U-Boot,理解启动参数,学会从SD卡启动、网络启动。第5-7天学习内核编译和根文件系统制作,以NFS挂载方式启动到shell。这个阶段的关键就是“启动链路跑通”,不需要深究每个内核选项的含义。
第2周(Day 8-14)目标是“让外设动起来”。从GPIO点灯开始,学习设备树语法和GPIO子系统。然后依次学习按键中断(input子系统)、串口通信(tty子系统)、I2C和SPI总线协议。每个外设都配合一个实际模块来实验,比如用I2C读取温湿度传感器。这一周是建立“写驱动-编内核-设备树匹配-应用读写”完整链条的关键期。
第3周(Day 15-21)目标是“让项目落地”。这周我建议做一个综合小项目,比如一个基于字符设备的简易数据采集系统:设备树配置ADC引脚,驱动实现数据读取,应用层写个C程序把数据通过串口或者网络发出去。然后全程用GDB调试,学会看内核日志、trace点,掌握基本的系统性能分析工具(top、vmstat、strace)。如果你时间有余,再看一遍中断下半部和并发控制的进阶知识。
5.2 我的学习资源使用建议
书肯定要配合原厂的开发板资料学。飞凌的资料包一般包含原理图PDF、芯片数据手册、基础例程、内核源码。我认为最有价值的资料是原理图,因为所有寄存器的地址映射、引脚复用关系、电源域控制都能在上面找到线索。看原理图时重点关注芯片手册里的IOMUX章节,搞清楚PINMUX如何配置,这对驱动开发是硬功夫。
另一个容易被忽视的资源是内核源码里的Documentation目录。比如Documentation/devicetree/bindings/下面有所有设备树binding说明。查一个外设的compatible字符串怎么写、需要哪些属性、哪些是必需的,在这个目录里找到对应文档,比自己搜百度靠谱得多。
工具方面,我强推strace和busybox top。strace能追踪应用程序的系统调用,很多“为什么我的程序没反应”问题,用strace看一次就能定位是卡在读文件还是写设备。而busybox top能看CPU占用和内存情况,排查内存泄漏这些问题离不开它。
5.3 关于面试题的准备方向
看到热搜词里有“嵌入式Linux面试题”,我顺便给正在准备面试的读者提个醒。嵌入式Linux岗位面试一般分三块:C语言与数据结构基础、操作系统原理、Linux驱动与应用开发经验。
C语言方面,指针与内存管理是重灾区。面试官特别喜欢问“指针和数组的区别”“static关键字的作用”“volatile什么时候用”。这些问题看似简单,但真能答好的人不多。
操作系统方面,进程与线程的区别、上下文切换、同步互斥手段必须烂熟于心。嵌入式Linux岗位还常问段错误(Segmentation fault)是怎么产生的,涉及MMU和页表的知识。
Linux应用/驱动方面,常见问题包括:用户态和内核态如何通信(copy_to_user/copy_from_user)、字符设备和块设备的区别、select/poll/epoll的区别、发生死锁怎么办、如何排查内核crash(通过cat /proc/kallsyms结合addr2line定位函数)。
这里我给一个压箱底的排查技巧:当内核打印Oops信息时,找到PC is at xxx+0x1c/0x40这一行,后面跟的内存地址对应你的驱动函数。把这个函数地址记下来,在主机上执行:
arm-linux-gnueabihf-addr2line -e /path/to/your.ko 0x414 -f就能定位到源码里具体是哪一行出了问题。这个技巧我每次带人都会教,能省下大量的瞎猜时间。
6. 实战技巧:开发过程中的性能优化与调试方法
到了最后一个部分,我想分享一些开发过程中真正提升效率的实战技巧。这些内容不一定会写在书里,但确实是我这些年天天在用的土办法,非常管用。
6.1 内核日志等级与动态调试
内核的printk有很多日志等级,从KERN_EMERG到KERN_DEBUG。嵌入式开发中我推荐这样设置:
echo "7 4 1 7" > /proc/sys/kernel/printk这四个数字的含义分别是:控制台日志级别、默认消息日志级别、最小日志级别、最大日志级别。设为7 4 1 7意味着级别小于4的消息(也就是DEBUG级别)不会打印到控制台,避免刷屏干扰操作,但级别为4及以上的信息(KERN_WARNING等)都能看到。当你需要看驱动的详细调试信息时,动态调试比printk更好用:
echo 'file demo.c +p' > /sys/kernel/debug/dynamic_debug/control这个命令会打开demo.c里所有dev_dbg()调试输出,不需要重新编译内核。这些调试开关在生产环境中尤其好用,能极大地减少排查时间。
6.2 内存问题的快速定位方法
嵌入式开发中内存泄漏是最隐蔽的问题。我常用的排查工具是板子上的/proc/meminfo。如果怀疑某个内核驱动有泄漏,可以持续观察Slab和SReclaimable字段的变化:
watch -n 2 'cat /proc/meminfo | grep -E "Slab|SReclaimable|SUnreclaim"'如果SUnreclaim持续增长,基本可以断定某个内核驱动或文件系统缓存异常。用户态的内存泄漏可以用valgrind或者简单粗暴的“观察法”:跑一个持续申请内存的测试程序,一边跑一边top看RES内存是否线性增长。
6.3 中断风暴和CPU占用异常的定位
开发过程中如果发现系统响应突然变慢,top里si(软中断)占比很高,多半是发生了中断风暴。先通过cat /proc/interrupts看哪个中断号在疯狂增长,然后去dts里确认对应引脚,用示波器看波形。有一种常见情况是GPIO按键驱动没有做防抖,或者中断触发模式设成了IRQ_TYPE_EDGE_BOTH但驱动没有正确读取并清除中断状态,导致中断反复触发。
这类问题的排查思路基本是:先在dmesg里看有没有刷屏的错误日志,再缩小范围看是哪个外设,然后用perf或者ftrace跟踪中断处理函数耗时。在嵌入式Linux中,ftrace是最强大的跟踪工具之一,基本用法很简单:
echo function_graph > /sys/kernel/debug/tracing/current_tracer echo 'irq_handler_entry' > /sys/kernel/debug/tracing/set_event echo 1 > /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace虽然上面我给的命令不一定完全匹配所有内核版本(不同内核的trace接口命名略有差异),但思路是对的:追踪事件发生的时间线和调用关系,找到瓶颈函数。
6.4 网卡性能问题排查一例
我之前做过一个项目,板子上跑网络应用,吞吐率始终上不去。排查过程大致是:
先看一下链路速率协商结果:
ethtool eth0如果显示100Mbps而不是1000Mbps,说明网线或对端交换机协商有问题。接着用iperf3测吞吐,发现UDP能达到百兆,TCP只有十几兆,基本可以判断是网络栈参数问题。后来通过调整内核参数解决:
sysctl -w net.core.rmem_max=212992 sysctl -w net.core.wmem_max=212992以及确认网卡驱动中是否开启了TSO(TCP Segmentation Offload)或GSO,如果网卡驱动不支持而默认开启了,会导致CPU抛出大量“分段失败,内核回退”的错误,画面就会表现为网络吞吐极低。这类问题在真实项目中很常见,教科书上不会写,但排查思路是通用的:先看硬件协商,再测裸协议,最后调软件栈参数。
写在最后的一些大白话
我没有任何资格替你制定“必须这样做”的学习路线,毕竟每个人基础不同、目标也不同。但根据我多年的实际经验,如果非要说最重要的建议,我只会强调两点:第一,一定要把NFS根文件系统的开发模式跑通。这件事值得你花三天时间去折腾,它会让你后续的每一次代码修改都像在本地编辑器里改完就能立即看到效果,开发效率提升不是一个量级的。第二,不要害怕重新编译内核。很多人把内核编译想得太神圣,实际上编译一次也就十几分钟,而你在这个过程中会对启动流程、配置选项、设备树匹配有越来越深的肌肉记忆。
这本《嵌入式Linux系统开发21天速成》具体内容我现在还没通读,但如果它的核心脉络是围绕“从开发板到系统跑通再到驱动开发”这条链路展开的,那我认为它的定位是准确的。学习嵌入式Linux从来不是靠看书看会的,而是要在一个真实的硬件平台上反复折腾、反复出错、反复排查才能内化。希望这篇内容能帮你少走一些弯路。如果你在实操中碰到任何具体问题,欢迎按上面的思路先去排查一遍,大多数情况下你自己就能找到答案。