核心内容摘要
“大雷擦狙”高清巨制:震撼视听,开启全新次元!
嵌入式现代C教程——侵入式容器设计你记得普通容器对数据做什么吗拷贝指针、分配节点、维护额外的内存布局然后在某个时刻默默地吃掉你的缓存局部性。
侵入式容器则更直白数据对象自己把手伸出来当链表节点——谁要付额外内存和间接访问不是我。
什么是侵入式容器为什么在嵌入式特别香侵入式intrusive容器的关键点节点信息next/prev/…直接放在用户对象内部而不是另外分配一个 node 包裹对象指针。
优点显而易见零额外分配 —— 不需要每次 push 都 malloc/new 一个 wrapper非常重要。
更佳缓存局部性 —— 对象和元信息在一起遍历更快。
更小的内存占用与确定性 —— 对于内存受限或实时系统非常友好。
缺点也很直接对象被耦合到容器接口侵入源码要修改对象结构。
一个对象要同时在多个链表中需要多个“hook”成员或多继承。
使用不当会出现悬挂指针/重复插入等问题需要更谨慎的生命周期管理。
适用场景任务调度器、空闲块链表、驱动链表、内核/RTOS 数据结构、内存池的 free-list 等。
两种常见实现策略基类 hookinheritance对象继承一个包含 next/prev 的 hook 基类。
类型安全转换容易。
成员 hookmember hook对象包含一个 hook 成员更灵活可同时有多个 hook 实例但是需要container_of技巧把 hook 指针转换回对象指针。
下面我们先实现一个干净的、可直接使用的“基类 hook”双向链表适合教程和嵌入式再说 member-hook 的思路与注意点。
代码简单、类型安全的侵入式双向链表继承式下面的实现目标小而清晰C11 可用适合嵌入式编译器。
// intrusive_list.h#pragmaonce#includecassert#includeiterator// Intrusive list node base — 继承它即可成为链表节点templatetypenameTstructIntrusiveListNode{T*prevnullptr;T*nextnullptr;};// Intrusive doubly linked listtemplatetypenameTclassIntrusiveList{public:IntrusiveList():head(nullptr),tail(nullptr){}boolempty()const{returnheadnullptr;}voidpush_front(T*node){assert(nodenode-prevnullptrnode-nextnullptr节点必须处于未链接状态);node-nexthead;if(head)head-prevnode;headnode;if(!tail)tailnode;}voidpush_back(T*node){assert(nodenode-prevnullptrnode-nextnullptr节点必须处于未链接状态);node-prevtail;if(tail)tail-nextnode;tailnode;if(!head)headnode;}T*pop_front(){if(!head)returnnullptr;T*nhead;headhead-next;if(head)head-prevnullptr;elsetailnullptr;n-nextn-prevnullptr;returnn;}voiderase(T*node){assert(nodeerase null);if(node-prev)node-prev-nextnode-next;elseheadnode-next;if(node-next)node-next-prevnode-prev;elsetailnode-prev;node-prevnode-nextnullptr;}voidclear(){T*curhead;while(cur){T*nxtcur-next;cur-prevcur-nextnullptr;curnxt;}headtailnullptr;}// 简单迭代器只读/可写structiterator{usingiterator_categorystd::forward_iterator_tag;usingvalue_typeT;usingpointerT*;usingreferenceT;explicititerator(T*p):p(p){}referenceoperator*()const{return*p;}pointeroperator-()const{returnp;}iteratoroperator(){pp-next;return*this;}iteratoroperator(int){iterator tmp*this;*this;returntmp;}booloperator(constiteratoro)const{returnpo.p;}booloperator!(constiteratoro)const{returnp!o.p;}private:T*p;};iteratorbegin(){returniterator(head);}iteratorend(){returniterator(nullptr);}private:T*head;T*tail;};如何使用// example.cpp#includeintrusive_list.h#includeiostreamstructTask:IntrusiveListNodeTask{intid;Task(inti):id(i){}};intmain(){IntrusiveListTaskrunq;Taska(
,b(
,c(
;runq.push_back(a);runq.push_back(b);runq.push_front(c);// 链表顺序 c, a, bfor(autot:runq){std::coutTask t.id\n;}runq.erase(a);if(autoprunq.pop_front()){std::coutpop p-id\n;}}这段代码可以直接编译到嵌入式支持的 C 编译器只要支持基础模板与nullptr。
成员 hook当对象需要在多个链表中出现时继承式简单但如果一个对象需要同时属于多个链表例如同时在 ready_list 和 wait_list你需要多个 hook 成员或使用成员 hook 的方式。
成员 hook 的关键是container_of—— 给定指向 hook 成员的指针计算回包含它的对象指针Linux 内核常用宏。
简单的宏版实现清晰且常用#includecstddef// offsetof#defineCONTAINER_OF(ptr,type,member)\((type*)((char*)(ptr)-offsetof(type,member)))示例结构structMyObject{IntrusiveListNodeMyObjectready_hook;// for ready listIntrusiveListNodeMyObjectwait_hook;// for wait listintdata;};// 操作 ready list 时将传入 obj-ready_hook然后用 CONTAINER_OF 转回 MyObject*成员 hook 比较灵活但使用时需特别注意offsetof要与实际成员名一致并且强烈要求插入前检查该 hook 是否已经链接避免重复插入。
设计建议与防坑指南对象生命周期必须明确链表中的节点在被销毁前必须先从所有链表中移除。
否则会出现野指针后果通常是难以定位的崩溃。
插入前检查状态给 hook 加一个bool linked字段或断言防止重复插入。
测试代码里多用assert。
多 hook 需求优先使用成员 hook若对象在多个容器间切换频繁成员 hook 更灵活。
并发场景小心内存屏障/原子性如果要在 ISR 或多核中操作链表必须采用锁、原子 CAS 或者专门的 lock-free 算法超出本篇范畴。
提供 RAII wrapper考虑提供一个小的IntrusiveListGuard或ScopedUnlink来保证异常或早返回时对象能安全注销。
嵌入式代码也许没有异常但 RAII 有助于写出更安全的释放代码。
调试信息在开发阶段把节点状态打印出来id/地址/prev/next能快速定位错误。
不要滥用侵入式容器并非万能工具。
若你不在乎每次分配开销或者对象不可修改第三方库就别侵入对象普通std::list/vector更简单、安全、易维护。
何时该选择侵入式容器在嵌入式 / 内核 / 实时系统资源与延迟是第一要务侵入式数据结构在这些场景是非常自然的选择。
特别适合要求确定性、避免堆分配的系统bootloader、RTOS kernel。
需要高性能的 free-list、task queue、timer wheel 等。
想要最小内存占用的场景。
如果你做的是普通应用层业务逻辑或者对象来自第三方库无法改结构侵入式方案的维护成本可能高于收益。
结语侵入式容器的思想不复杂让数据自己负责“站位”。
但这要求你对对象的责任更清晰——谁插它、谁删它、什么时候删它。
把责任写成代码再把代码写成规范。
对嵌入式系统而言这是一种非常“实在”的工程哲学省一分内存多一分确定性。