嵌入式Linux面经-随意随缘不完整版
🌐 一、内核基础(驱动和 BSP 的地基)
Linux 内核启动流程?(从 U-Boot / ATF 到 init)
系统上电后,SoC 会从固化的启动介质(如 SPI NOR/NAND)加载第一阶段 Bootloader,通常是 SPL(Secondary Program Loader),接着加载第二阶段的 U-Boot。
在 U-Boot 中主要做这些事:
- 初始化 DRAM,准备运行内核的内存环境
- 初始化串口、网络、存储等基本外设(可选)
- 读取设备树(.dtb)、kernel 镜像(zImage/Image)、initrd
- 最后调用
bootm
或booti
命令,引导内核
可选阶段:ATF(Arm Trusted Firmware)或 OpenSBI(RISC-V)
如果是 RISC-V 架构,这一步通常由 OpenSBI 负责:
- 设置机器模式下的一些 CSR、trap handler
- 创建 S-mode 的运行环境(包括 timer 中断、console、PMP)
- 提供 SBI 接口(给 S-mode 的 Linux 用)
OpenSBI 初始化完毕后,跳转到 Linux 内核的 start_kernel()
。
简略版本
Linux 的启动流程可以分为三大阶段:Bootloader、内核、用户空间。上电后 U-Boot 加载 kernel 和 dtb,如果是 RISC-V,还会通过 OpenSBI 设置 S-mode 环境,最后跳转到内核的 start_kernel 函数。内核初始化内存、子系统、挂载 rootfs,然后启动第一个用户态程序 /sbin/init
。这整个流程我在适配某个 RK3588 平台时调过很多次,比如串口出不来我就调 console 初始化、MMC 识别不了我就看设备树和 U-Boot 的 log。
设备树是什么?它的作用?你怎么写/改过?
设备树就是内核用来描述硬件结构的数据结构,是 Linux 移植过程中很关键的一部分。我写过/改过很多设备树,比如在适配 RK3588 的时候,我给 LED 加了 GPIO-led 节点,调过串口波特率、4G 控制引脚,解决过设备不能识别、设备树 compatible
不匹配驱动等问题。
项目 | 修改内容 |
---|---|
4G 模块适配 | 加了 UART 节点、AT 控制用 GPIO、PWRKEY 控制 |
LED 灯适配 | 加了 GPIO-led 节点,测试开关状态 |
RS-232 | 开启对应 UART 节点,添加正确引脚复用 |
什么是 platform_device 和 platform_driver?怎么匹配?
这是 Linux 设备模型中用于描述“非自动枚举外设”(比如 GPIO 控制器、LED、SPI、I2C 设备等)的核心机制。
platform_device
:描述一个平台设备,包含设备的资源信息(内存地址、中断号、时钟等)platform_driver
:驱动这个设备的驱动程序,实现probe/remove
等回调
每个设备或驱动都会挂到一条总线上,platform_device/driver是挂到了虚拟的platform_bus上,当有驱动模块插入或者设备创建的时候,就会去调用bus->match,如果匹配就会调用到probe。
中断处理流程(从硬件中断到 ISR)
- 中断产生
- 响应中断
- 保存当前上下文
- 调转到中断入口(中断向亮表)
- 进入trap流程,riscv是stvec
- GIC/PLIC识别中断来源
- linux调用handle_irq
- 最终调用驱动注册的irq
内核空间和用户空间的通信方式?(ioctl、procfs、sysfs、netlink)
内核和用户通信方式很多,最常用的是 ioctl
,适合复杂控制命令;如果是属性类信息,比如设备模式、开关等,可以用 sysfs
;调试或状态类信息用 procfs
;而更复杂的场景,比如异步事件、结构体交换等,则会用 netlink
。我在做外设驱动时可能会ioctl 去debug,值的一提的是platform框架就提供了sysfs来修改probe相关内容。
Linux 的内存管理机制简要说一下?
- 每个进程都有独立的虚拟空间
- 页表管理
- 支持多级页表,例如4级,支持huge page
- 硬件通过MMU转换,缺页触发page fault
- 内存分配,kmalloc小块,vmalloc大块
- 页面回收
- 清理cold页面
- dirty-page写回磁盘(swap)
- slab/slub分配器
- 内存映射
slub / slab有什么区别
slab: /slæb/
slub: /slʌb/
slab
/ slub
分配器是 Linux 内核中非常核心的一部分,用于高效地分配内存,尤其是 频繁申请/释放的小对象。它们属于内核的“内存分配器”子系统,目标是比 kmalloc()
更快、更省碎片、更好对齐。
slab / slub 分配器是 Linux 内核里专门用于“频繁分配释放的小内核对象”的内存池系统。
比如你在内核里频繁创建 / 删除一个结构体,比如 struct task_struct
,那 slab/slub 会帮你更快、更安全地管理内存,不用每次都向物理页框要整块内存。
为啥不用 malloc()
?
- 用户空间有
malloc()
,但那是给用户程序的。 - 内核不能用它,因为内核对性能和碎片容忍度要求极高。
- 内核自己的通用分配器是
kmalloc()
,但kmalloc()
适合中等规模、一次性用途。 - 对于大量重复、固定大小的小结构体,
slab/slub
更快更稳。
slab/slub/slob 的关系(别搞混)
分配器 | 特点 | 使用场景 |
---|---|---|
slab | 最早期,带缓存和局部性优化,复杂 | 老系统,嵌入式也可能用 |
slub | 默认分配器(大多数内核默认),简单高效 | 推荐,支持 NUMA,锁粒度低 |
slob | 超简陋、给资源极少的设备用(内存极少) | 小内核、嵌入式 minimal 环境 |
你怎么看待设备驱动中的 probe 函数?调用流程是?
- name
- of_match_table
- acpi
什么是共享中断?
共享中断(Shared IRQ)是指 多个设备共享同一个中断线(IRQ 号)。这种情况在中断线资源有限或板子设计简化时很常见,尤其是 x86 架构 PCI 设备、某些 ARM SoC 外设 中。
在传统中断系统中,一个中断号通常只属于一个设备。
而共享中断允许多个设备注册在同一个 IRQ 上,只要它们能自己判断中断来源即可。
这样可以节省中断资源,提高硬件利用率。
使用:
- request的时候带SHARED标志位
- dev_id必须唯一并且为非NULL
- irq_handler必须能判断是不是自己需要处理的中断(比如读状态寄存器)
ioremap和IOMMU
ioremap是将io物理地址映射为虚拟地址的一种机制,让驱动中可以用虚拟地址来访问实际的物理IO寄存器。
IOMMU是用来让设备访问内存的一种硬件+驱动的机制,帮助外设将虚拟地址翻译成物理地址。比如DMA控制器,网卡等。
内存管理子系统
内存管理子系统在 Linux 中负责管理物理内存和虚拟内存,确保内存的高效分配与回收。Linux 使用虚拟内存技术为每个进程提供独立的内存空间,并通过页表映射虚拟地址到物理地址。内核使用伙伴系统管理物理内存,通过 slab 分配器提高内存分配效率。在内存不足时,系统会通过页面交换(swap)将不常用的内存页写入硬盘,释放物理内存给其他进程。内存管理还包括内存保护和隔离,防止进程越界访问内存。同时,内核提供了内存监控和调优机制,如 /proc/meminfo
查看内存使用情况,和 cgroups
用于限制进程的内存使用。
作用
内存管理子系统负责管理系统的所有内存资源,包括物理内存、虚拟内存、内存映射和内存保护等。其主要任务是高效地分配、使用和回收内存资源,并确保内存访问的安全性和稳定性。
虚拟内存与物理内存的区分
虚拟内存:每个进程有独立的虚拟内存空间,通过分页技术,将虚拟地址映射到物理地址,虚拟地址提供了进程隔离和内存保护。
物理内存:实际的硬件ram,Linux会跟踪物理ram的使用情况,通过页面管理高效利用内存
内存分配方式
- 伙伴系统: 用于管理物理内存块,分配时按大小分配,释放时合并空闲块。
- slab分配器: 高效的内存分配机制,用于频繁分配和回收内存块,减少内存碎片。
- kmalloc,vmalloc:
- 内核分配内存的两种方式,
kmalloc
用于小块内存分配,vmalloc
用于较大的内存块。
页面交换与交换空间(Swap)
页面交换:当物理内存不足时,将不常用的内存页交换到硬盘的交换空间,释放物理内存给其他进程使用。
交换空间:通常是硬盘的一块区域,用于存储交换出去的内存页,可以是交换分区(swap partition)或交换文件(swap file)。
内存映射
mmap:通过 mmap()
系统调用将文件或设备映射到进程的虚拟内存地址空间,支持文件共享和内存映射。
内存映射文件:文件可以映射到进程的虚拟内存中,允许直接在内存中操作文件内容,避免重复的磁盘 I/O 操作。
内存管理中的核心算法
页面置换算法:当内存不够用时,操作系统需要决定将哪些页面交换到硬盘,常用的算法有 LRU(最少使用)和 COW(写时复制)等。
内存压力管理:内存不足时,内核会采取措施,如杀死占用内存过多的进程,释放内存。
什么是伙伴系统
伙伴系统是 Linux 的内存管理算法,采用2的幂次方式分配内存。它通过将内存块成对分配,并在空闲时合并相邻的块,减少内存碎片,保证高效的内存使用。
举个例子,当有一个16K的空闲区域。
有一个进程申请了4K的区域,伙伴系统将这个16K切成两个8K,将其中一个8k切成2个4k。
此时将4k分配给这个进程。空闲区域变成了: 4k(used) 4k(free) 8k(free)
此时又有一个进程申请了2k的区域,伙伴系统将4k切成2个2k,然后给这个进程: 4k(used) 2k(used) 2k(free) 8k(free)
此时2k释放,变成了:4k(used) 4k(free) 8k(free)
将2个2k合并成更大的4k了。
此时4k释放,变成了16k(free)
COW
COW是Copy-On-Write,也就是写时复制。
COW 的基本思想是,在资源共享的初期,不做资源的复制,直到某个进程或线程修改了这些共享资源时,才会进行复制操作。也就是说,COW 在资源被写入时才会进行复制,避免了不必要的内存拷贝,节省了时间和空间。
- fork时:当进程产生fork,内核会标记RW的pte,当fork的进程修改这些RW的页表的时候才会触发复制然后修改。
insmod的时候发生了什么
由于内核模块不像应用程序,默认占有自己独立的地址空间。
所以内核模块编译的时候需要生成地址无关代码,也就是需要可重定位的特性。
这里给出一个假设的模块重定位表:
Relocation section '.rela.text' at offset 0x240 contains 3 entries: |
Offset:表示在模块中的某个位置需要进行重定位。
Type:重定位类型,这里是
R_X86_64_PC32
,表示需要修改一个 32 位的相对地址。Sym.Name:需要调整的符号的名称,例如
simple_init
、simple_exit
和printk
。Sym.Value:符号的值(地址),在模块加载前是未定义的。
在模块加载时,内核根据重定位表中的信息执行以下操作:
- 内核加载模块到内存
当 insmod
或 modprobe
加载模块时,内核会将模块的代码段(如 .text
)加载到内核地址空间中的某个位置。假设模块加载到地址 0x80000000
。
- 解析符号表
内核会解析模块中的符号表,找出所有需要重定位的符号(如 simple_init
、simple_exit
、printk
)。这些符号在编译时并没有对应的内存地址,而是使用了相对地址或符号名称。
- 根据重定位表调整地址
接下来,内核会遍历重定位表,为每个需要调整的符号计算并替换地址。假设模块被加载到 0x80000000
,我们可以按以下步骤调整符号:
- **重定位
simple_init
**:- 在重定位表中,
simple_init
的偏移量是0x20
。 - 假设
simple_init
在编译时的地址是相对地址,因此它需要调整为内存中的实际地址0x80002000
(假设simple_init
在模块的.text
段中的实际位置)。 - 内核会修改模块代码中的
0x20
位置,将该位置的地址调整为0x80002000
。
- 在重定位表中,
- **重定位
simple_exit
**:- 同理,
simple_exit
的偏移量是0x40
,需要调整为模块实际加载的地址0x80002020
。
- 同理,
- **重定位
printk
**:printk
是内核提供的函数,因此内核会根据内核符号表查找printk
的实际地址,并将模块中的printk
调用地址进行调整。
中断,同步,异步
中断:中断(Interrupt) 是一种硬件机制,用来打断处理器(CPU)当前正在执行的程序流,转而去响应某些紧急或重要的事件。
同步:同步是一种软件机制,用来等待一件事情的完成才能执行下一件事,也就是说发起方需要等待结果。
异步:是一种软件机制,发起操作的一方 在操作完成之前不用等待,可以先干别的事,操作完成后另有机制通知。
SoftIRQ, tasklet, workqueue
特性 | softirq | tasklet | workqueue |
---|---|---|---|
层级 | 最底层机制 | 基于 softirq 的封装 | 基于普通内核线程 |
执行上下文 | 中断上下文(不可阻塞) | 中断上下文(不可阻塞) | 进程上下文(可以睡眠) |
并发性 | 高(每 CPU 并行) | 自动串行化同一个 tasklet(同一 tasklet 不并发) | 有线程调度,可完全并发 |
适用场景 | 极快的、实时性要求高的处理 | 简单延后处理,写起来方便 | 需要可能阻塞的较重任务,比如磁盘IO、内存分配 |
例子 | 网络协议栈收包 | 网卡收发包的小处理,定时器回调 | 文件系统刷盘,网络协议栈底层处理 |
当调用触发softirq函数时,softirq会设置一个pending标志位(bitmap),中断处理器(硬中断)返回时,如果检测到 softirq_pending
非 0,就会处理。也可以主动调用 do_softirq()
来处理 pending 的 softirq,比如内核线程或者一些中断处理代码中。
如果软中断太多,do_softirq() 处理超时(通常是1ms左右的门限),就不会继续在中断上下文里做了,而是唤醒一个普通进程叫 **ksoftirqd/***,比如 ksoftirqd/0
(第0个CPU)。
ksoftirqd 是一个优先级较低的内核线程,它的 main loop 里就是不断检查 pending 的 softirq,然后慢慢处理。
tasklet 是基于 softirq 封装的更简单 API,它统一用 TASKLET_SOFTIRQ
这一项,把一堆小任务链在一起处理。
📟 二、驱动开发相关
设备树节点如何转换为设备
首先dts中的node会在linux内核中被解析为一个个的device node的结构体,然后生成struct device结构体,将device node挂到struct device下的of_node成员。
随后根据不同的总线转换成不同的设备。
以platform举例:
如果一个设备想变成platform设备,需要满足以下条件:
一般情况下,只对设备树中根的第1级节点(/xx)注册成platform device,也就是对它们的子节点(/xx/*)并不处理。
如果一个结点的compatile属性含有这些特殊的值(“simple-bus”,”simple-mfd”,”isa”,”arm,amba-bus”)之一,并且自己成功注册成了platform_device, 那么它的子结点(需含compatile属性)也可以转换为platform_device(当成总线看待)。
- 至于为什么请看simple-mfd
根节点(/)是例外的,生成platfrom_device时,即使有compatible属性也不会处理
节点的什么属性会被转换
在老版本的内核中,platform_device部分是静态定义的,其实最主要的部分就是resources部分,这一部分描述了当前驱动需要的硬件资源,一般是IO,中断等资源。
在设备树中,这一类资源通常通过reg属性来描述,中断则通过interrupts来描述;
所以,设备树中的reg和interrupts资源将会被转换成platform_device内的struct resources资源。
能不能不通过dts去match
有的兄弟,有的。
设备可以通过platform_device_alloc
和platform_device_add
这种手段手动注册设备到内核。
此时也可以完成match,因为dts最终就是转换为了device node节点,最终给了deivce结构体,然后再根据这个dts是什么bus下的来确定转换为什么设备。
驱动模块的状态
已加载 (Loaded)
已初始化 (Initialized)
已卸载 (Unloaded)
活动 (Active)
已禁用 (Disabled)
已移除 (Removed)
等待 (Pending)
正在加载 (Loading)
正在卸载 (Unloading)
加载失败 (Failed to Load)
字符设备驱动的核心结构?(cdev、file_operations)
- 设备号分配
- 初始化驱动
- 注册字符设备
- 实现ops
- copy_to_user
- copy_from_user
DMA 如何使用
首先在dts中进行配置,dma0为dma-controller:
uart0: serial@10000000 { |
驱动中获取dma:
struct dma_chan *rx_chan; |
配置传输参数:
- 源地址(Source Address):数据传输的起始地址。
- 目标地址(Destination Address):数据传输的目的地。
- 数据传输方向:数据是从外设传输到内存,还是从内存传输到外设。
- 传输模式:如是否支持循环缓冲区,数据传输是否是单次或连续的。
struct dma_slave_config config = { |
提交并准备传输
dma_cookie_t cookie; |
什么时候
需要大量数据传输或者时用DMA,可以让CPU转去做其他事情。
DMA cache一致性问题
由于主存访问对于cpu来说很慢,所以有cache在中间做缓存。
- 当DMA启动的时候,如果数据源是内存,但此时最新的数据在cache中,此时就会导致一致性问题,所以应当先将cache中的内容刷新到主存中,再启动传输。
- 如果目的地址是内存,传输完毕后如果cache中有这段内存的数据,cpu会直接从cache中取,而不是内存,此时就会导致不是最新的数据,造成一致性问题。此时应该先将cache中的这段内存数据标记为无效。
DMA映射
为什么需要DMA映射?在传统裸机的时候,我们配置好src,dts,width,mode其实就可以开始传输了。
为什么在这里需要多个DMA映射呢?因为在linux的世界里比较复杂,dma访问的是物理地址,不能访问虚拟地址。
如果有IOMMU,dma_map_xx做的其实就是将虚拟地址转换成经过IOMMU的地址,让dma能够正常访问。
如果没有IOMMU,dma_map_xx做的其实就是v2p,然后可能刷一下cache这样的操作。
但是注意,这里不一定是绝对的pa,不同平台的物理地址规则(比如总线地址有无偏移)也可能不同,
所以永远用标准的 dma_map_xxx(),不要自己偷懒直接拿 virt_to_phys()。
一致性映射
含义:分配一块CPU和设备都能直接访问的内存,不需要额外同步。
特点:
CPU和设备看到的数据始终是一致的。
通常是通过 dma_alloc_coherent() 分配的。
缺点:
这种内存可能带缓存属性限制(比如不可缓存),速度比普通内存慢。
用量不能太大,适合小块数据,比如描述符、控制块。
流式映射
含义:用普通内存,在要DMA之前做同步,DMA完成后再同步。
特点:
通过 dma_map_single() 或 dma_map_page() 把内存映射给设备。
在 CPU写完后,要 dma_sync_for_device(),让设备能读到。
在 设备写完后,要 dma_sync_for_cpu(),让CPU能读到。
优点:
可以用普通缓存内存,访问快。
缺点:
需要手动同步(Cache维护),操作多了容易出错。
扩展DMA映射
含义:标准 dma_map/dma_unmap 之外,Linux 还扩展了一些特殊情况的接口,比如:
dma_map_resource():直接映射 MMIO 区域。
dma_mmap_coherent():把 coherent memory 映射到用户态。
特点:
主要是为了适配不同硬件场景,比如 MMIO、用户态共享等。
总结一句话:扩展是补充标准API没法覆盖的用法。
散列DMA映射
含义:支持把一大块数据分散在不同物理页上,设备可以连续读写。
特点:
不是一大块连续内存,而是多个小块拼起来。
通过 dma_map_sg() 和 dma_unmap_sg() 完成映射。
设备要求:
必须支持SG list(scatter-gather list),就是可以一次性收一组地址。
典型场景:
网络收发大包(sk_buff 里的数据零拷贝)。
大文件DMA(比如SCSI设备传输文件)。
regmap 框架你用过没?做寄存器读写有什么坑?
regmap
是 Linux 内核提供的一种通用框架,用来简化设备寄存器的读写操作。它将底层硬件接口(如 I2C、SPI、MMIO 等)与高层驱动逻辑进行抽象和统一处理。使用者不需要再关心i2c msg的拼接,时序控制等。
坑:
- 寄存器缓存一致性问题:
regmap
提供了缓存机制来提高性能,但在一些特殊情况下,缓存与实际硬件状态可能不同步,导致读到过时的数据或写入数据未能立即反映到硬件。为了解决这个问题,可能需要手动刷新缓存,或禁用缓存(REGMAP_CACHE
)。- regmap_sync强制刷新缓存
- 读取硬件状态前确保缓存已经更新
- 增量访问:某些硬件设备不支持通过自动递增的方式来访问连续的寄存器。在这种情况下,使用
regmap
的某些操作可能会失败
syscon怎么使用
很多 SoC 芯片中,一个寄存器区域可能控制着多个不同模块,而这些模块又分别由不同的驱动来控制。为了避免多驱动重复映射同一段地址(导致资源冲突),Linux 引入了 syscon
来集中管理这些共享寄存器。
在驱动中如何使用?
有很多方法可以使用,前提是获得到syscon节点,例子:
struct regmap *regmap; |
simple-mfd是什么
simple-mfd
是一个“容器设备”,用于声明一个寄存器区域下还有多个子设备(比如 clock、reset、pinctrl 等)。
SoC 里经常有一些硬件模块的寄存器都集中在一块区域,比如:
[0x00]
是 reset 控制器的寄存器[0x10]
是 clock 控制器的寄存器[0x20]
是 pinctrl 控制器的寄存器
你不可能一个设备就注册三次这些地址,于是 Linux 内核提供了一个机制叫 **mfd
**,专门用来声明这种「一段地址包含多个设备」的场景。
告诉 Linux:
这不是单一功能的设备,而是一段地址区域里有多个子设备。
于是 Linux 会自动把子节点当成独立的 platform device 去注册,绑定各自的驱动。
设备驱动中你做过的调试手段有哪些?怎么查 kernel log、跟踪问题?
- kgdb:
- vmlinux,保存调是符号
- 开启config
- 通过串口调试
- ftrace: 打印系统调用
- strace: 用户空间跟踪
- ioctrl: 修改驱动的一些参数达到debug效果
- sysfs:跟ioctrl起的效果差不多
怎么写一个能挂到 sysfs 的驱动调试接口?
- 创建kobject(挂载到/sys/kernel/debug下)
- 添加sysfs属性到到kobject
- 实现show和store函数
pinctrl驱动
以pinctrl-k230为例。
概念
pinctrl有function,group,pin和conf的概念。
- function:功能,用来表示这些引脚包含了哪些组,一个function包含一个或多个组
- group:组,用来表明一个或多个引脚的集合,比如rxd和txd是传输组,cts和rts是控制组
- pin: 引脚,没什么好说的
- conf:引脚除了复用为某个功能,还需要一些配置,比如上拉,下拉,此时conf就有了作用
实现函数
首先需要在probe函数中parse dts,然后将dts中的节点按自己的想法转成function, group, pin, conf。
对于conf,目前pinctrl框架已经实现了通用的属性节点解析和还原。可以通过api一键解析,再也不用使用厂商自定义的字段了,除非你要求的conf没在pinctrl框架实现。
随后要填充ops:
pinctrl-ops
- pinctrl-ops
- get_groups_count
- get_groups_name
- get_groups_pins
- dt_node_to_map
- dt_free_map
前三个看名字就知道什么意思了,将probe阶段解析的dts返回回去就可以了。
重点是dt_node_to_map,这个函数中将pinmux和和config都转成maps。
很显然这是对于group来说的,这里要注意的是,maps的个数应该是:
for (i = 0; i < func->ngroups; ++i) { |
因为一个map只能对应pinmux或者conf,所以为每个引脚分配conf,然后多余的一个用来pinmux。
pinmux-ops
对于传进来的参数更新pinctrl的寄存器就可以了
pinconf-ops
跟pinmux相同
USB扫盲
USB中速率
Full-speed/high-speed/low-speed
其中full-speed能和任意一种模式结合,但high-speed和low-speed无法同时支持。
USB怎么检测连接和断开
USB HUB(root hub)的D-和D+都连接有15K下拉电阻。
- Full-speed设备的D+引脚连接1.5K上拉电阻,当空闲D+由低到高则连接,由高到低则断开。
- High-speed设备与Full-speed电气连接相同,断开相同。在连接的时候,当设备收到se0(复位)信号之后,会主动向hub发送K信号,表明支持High-speed
- Low-speed设备的D-引脚连接1.5K上拉电阻,当空闲D+由低到高则连接,由高到低则断开。
当工作于high-speed的时候会断开上拉电阻,只保留hub的下拉电阻。
对于工作在high-speed模式的hub和dev,D+和D-都有45欧姆的下拉电阻,用来消除反射信号
🧩 三、BSP 适配和系统裁剪
你怎么给一个新板子适配 Linux?
你移植过哪几个平台的 BSP?都做了哪些模块的适配?
- rk3568
- can
- 485
- rtl8125
- jl2101
- ec20
- intel 7260
- a33
- ap6212
- r8152
- k1
- pmic
- regulator
- rtc
- gpio
- i2c
- pmic
- k230
- npu
- clock
- eth
- i2c
- timer
- watchdog
- usb
- thermal
什么是 device tree overlay?你什么时候会用它?
在运行时或加载阶段对基础设备树(Base Device Tree)进行扩展或修改的机制。它可以在不更改原始设备树文件的前提下,动态地添加、修改、启用设备节点。
通常是dts文件编译成dtbo。
加载:
- uboot使用fdt apply xxx.dtbo加载
- 内核开启overlay
mount -t configfs none /sys/kernel/config
mkdir /sys/kernel/config/device-tree/overlays/my_overlay
cat my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/dtbo
Linux 启动卡在 early boot,你怎么排查?
- 检查uboot参数
- 检查镜像格式,内存地址设置
- 检查是否内存覆盖
- 加上earlycon参数
- 检查串口配置
怎么在系统裁剪中定位无用模块?怎么控制编译 size?
- GCOV / LCOV:编译内核/应用带上
CONFIG_GCOV
,运行后看哪些模块根本没被调用过。- 可以生成覆盖报告,检查哪些c根本没被调用过
- LTO:LTO,全称 Link Time Optimization(链接时优化),是一种 跨文件级别的编译优化技术。简单说,它可以让编译器在链接阶段看到整个程序(或整个内核),从而进行更深入的优化,比如函数内联、删除死代码、消除冗余符号等。
- 去除不需要的config
- nfs
- 多余的文件系统
- 多余的网络协议,如rndis等
- 未使用的CPU/架构相关选项(defcofig会开很多)
- GUI框架等
scripts/config --disable CONFIG_SOMETHING
- modprobe -r去除不需要的模块看看是否系统稳定
🧪 四、调试和实战经验
你调过哪些 tricky 的 bug?怎么解决的?
你怎么用串口/网口调试板子?
你有没有接触过 kgdb、crash、ftrace 或 perf 这些工具?
一次调不出网口/USB/WiFi 的问题,你会怎么一步步排查?
你适配过哪些外设?网卡、串口、I2C 设备、屏幕、按键、LED?分别遇到过什么问题?
🚀 五、进阶与加分项
- 熟悉 device tree 编写和调试工具,比如 dtc/dumpdtb?
- 了解 OpenWrt / Buildroot / Yocto 哪些?能快速出系统?
- 有没有做过 patch 提交或者分析 mainline kernel 的提交?