链表宏在linux内核、鸿蒙内核、rtos和一些开源代码中用的非常多。链表宏是双向链表的经典实现方式,总代码不超过50行,相当精炼。在一些开源框架中,它的数据结构,就是以链表宏为基础进行搭建(如shttpd,一个开源的轻量级、嵌入式服务器框架)。本篇文章将对llist.h文件中的链表宏进行逐个讲解。
1 源码(llist.h) llist.h文件的全部源码如下:
#ifndef llist_header_included#define llist_header_included/* * linked list macros. */struct llhead { struct llhead *prev; struct llhead *next;};#define ll_init(n) ((n)->next = (n)->prev = (n))#define ll_head(h) struct llhead h = { &h, &h }#define ll_entry(p,t,n) ((t *)((char *)(p) - offsetof(t, n)))#define ll_add(h, n) do { ((h)->next)->prev = (n); (n)->next = ((h)->next); (n)->prev = (h); (h)->next = (n); } while (0)#define ll_tail(h, n) do { ((h)->prev)->next = (n); (n)->prev = ((h)->prev); (n)->next = (h); (h)->prev = (n); } while (0)#define ll_del(n) do { ((n)->next)->prev = ((n)->prev); ((n)->prev)->next = ((n)->next); ll_init(n); } while (0)#define ll_empty(n) ((n)->next == (n))#define ll_foreach(h,n) for (n = (h)->next; n != (h); n = (n)->next)#define ll_foreach_safe(h,n,t) for (n = (h)->next, t = (n)->next; n != (h); n = (t), t = (n)->next)#endif /* llist_header_included */ 2 注解 在llist.h中,所用到的链表是双向链表,其节点结构定义如下。在此节点结构中,其只包含了两个指针域,一个指向直接前驱,一个指向直接后继,没有定义数据域。
struct llhead { struct llhead *prev; struct llhead *next;}; 2.1 ll_init(n) 宏ll_init的定义如下,其作用是将所传入指针n的两个指针域(n)->next和(n)->prev都指向n。目的是完成单个节点的初始化工作,如下图示意了该过程。
#define ll_init(n) ((n)->next = (n)->prev = (n)) 2.2 ll_head(h) 宏ll_head的定义如下,直接将宏ll_head展开,其意图很明显是定义一个新链表h(h表示为传入宏的参数名),并且将h的两个指针域,都初始化为h地址本身,如下图示意了该过程。
#define ll_head(h) struct llhead h = { &h, &h } 2.3 ll_entry(p,t,n) 宏ll_entry的定义如下,其依赖于宏offsetof。下面先对宏offsetof进行详细描述,其功能描述为:
c语言的offsetof()宏,是定义在stddef.h。用于求出一个struct或union数据类型的给定成员的size_t类型的字节偏移值(相对于struct或union数据类型的开头)。offsetof()宏有两个参数,分别是结构名与结构内的成员名。——维基百科
#define ll_entry(p,t,n) ((t *)((char *)(p) - offsetof(t, n)))#define offsetof(type, member) ((size_t) &((type *)0)->member) 为了更好的理解宏offsetof,下面按照宏的定义来进行拆解说明。
((type *)0):取整数零并将其强转换为指向type的指针。 ((type *)0)->member):引用指向结构成员member。 &((type *)0)->member):取出member的地址。 ((size_t) &((type *)0)->member):将结果转换为适当的数据类型。 由于该结构体是以0地址开头,所以最后该宏返回的结果就是该成员相对于结构体开头的偏移量。有了对宏offsetof的理解,再来看宏ll_entry就比较好理解了。宏ll_entry的功能是,根据结构体变量(t)中的域成员变量(n)的指针(p)来获取指向整个结构体变量的指针,下面来做拆解说明:
offsetof(t, n):计算成员n相对于其结构体t开头的偏移量。 ((char *)(p):将指针p强转为字符指针类型,保证其做+/-运算时是以字节为单位。 (char *)(p) - offsetof(t, n)):p为成员n的指针,减去偏移量,指针到了结构体开头位置。 ((t *)((char *)(p)- offsetof(t, n))):将指针强转,得到了整个结构体指针。 宏ll_entry的作用和linux中的宏container_of作用基本一样,该宏定义如下:
#define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type,member) );}) 2.4 ll_add(h, n) 宏ll_add的定义如下,其作用是向双向链表h的头部添加节点n。根据ll_add定义的语句顺序,对照着图片分析,会更加清晰。如下图,上面这张图片展示了添加节点n之前的结构,下图展示了添加节点n之后的结构。
#define ll_add(h, n) do { ((h)->next)->prev = (n); (n)->next = ((h)->next); (n)->prev = (h); (h)->next = (n); } while (0) 2.5 ll_tail(h, n) 宏ll_tail的定义如下,其作用是将节点n添加到双向链表h的尾部。宏ll_tail的定义如下,其作用是向双向链表h的头部添加节点n。根据ll_tail定义的语句顺序,对照着图片分析,会更加清晰。如下图,上面这张图片展示了添加节点n之前的结构,下图展示了添加节点n之后的结构,可以和ll_add的结果进行对照。
#define ll_tail(h, n) do { ((h)->prev)->next = (n); (n)->prev = ((h)->prev); (n)->next = (h); (h)->prev = (n); } while (0) 2.6 ll_del(n) 宏ll_del的定义如下,其作用是将节点n从双向链表中删除,并且节点n回到初始状态(其指针仅指向自身,不再指向其它地方)。
#define ll_del(n) do { ((n)->next)->prev = ((n)->prev); ((n)->prev)->next = ((n)->next); ll_init(n); } while (0) 2.7 ll_empty(n) 宏ll_empty的定义如下,其作用是判断链表n是否为空链表,返回布尔值false/true。如果节点的直接后继next指向其自身,就认为其为空节点。
#define ll_empty(n) ((n)->next == (n)) 2.8 ll_foreach(h,n) 宏ll_foreach的定义如下,其作用是在双向链表h中,循环遍历出节点。
#define ll_foreach(h,n) for (n = (h)->next; n != (h); n = (n)->next) 2.9 ll_foreach_safe(h,n,t) 宏ll_foreach_safe的定义如下,其作用是在双向链表h中,循环遍历出节点n,因为其有提前存储n的下一个节点t。即使n节点被清理掉,也不影响其下一个节点的遍历,所以该宏一般用来做循环清除双向链表中节点的操作,而宏ll_foreach仅用来遍历双向链表。
#define ll_foreach_safe(h,n,t) for (n = (h)->next, t = (n)->next; n != (h); n = (t), t = (n)->next) 3 使用案例 有人可能会有疑惑,这个双向链表定义如此简单,只有前驱和后继两个指针,甚至连数据域都没有,那实际该如何使用呢?这个可能就是这组双向链表宏的精妙之处。其在使用过程中并不需要数据域,而是通过指针将结构体串联成双向链表,并且通过该指针借助 ll_entry宏 能还原出该结构体指针,从而达到操作具体结构体的目的。
如下例子虽然不是完整能跑的程序,但是足够说明双向链表宏的关键用法。程序源码如下,现对照代码,描述双向链表宏的大致使用步骤:
定义一个结构体,结构体中必须包含struct llheadlink;双向链表节点,这是后续能通过遍历双向链表节点,还原出该结构体指针的关键; 通过ll_head(listeners);,创建一个双向链表的头为listeners; 在具体逻辑中,肯定有地方通过ll_tail(&listeners, &l->link);或者ll_add(h, n),向双向链表的头listeners添加节点; 在需要操作1.所定义的结构体时,通过ll_foreach(&listeners, lp)遍历出节点指针; 这是最精华的一步,通过4.遍历出来的节点,传入宏ll_entry(lp, struct listener, link);中,还原出节点所在的结构体指针,根据逻辑的需要对结构体进行具体相应的操作; 通过宏ll_foreach_safe来遍历双向链表,ll_del来删除遍历出来的节点,达到清空链表的作用。 struct llhead { struct llhead *prev; struct llhead *next;};struct listener { struct llhead link; struct shttpd_ctx *ctx; /* context that socket belongs */ int sock; /* listening socket */ int is_ssl; /* should be ssl-ed */};static ll_head(listeners); /* list of listening sockets */struct listener *l;ll_tail(&listeners, &l->link);struct llhead *lp;ll_foreach(&listeners, lp) { l = ll_entry(lp, struct listener, link); fd_set(l->sock, &read_set); if (l->sock > max_fd) max_fd = l->sock; dbg((fd_set(%d) (listening), l->sock));}struct llhead *lp, *tmp;ll_foreach_safe(&listeners, lp, tmp) { l = ll_entry(lp, struct listener, link); (void) closesocket(l->sock); ll_del(&l->link); free(l);} 4 总结 ll_entry(p,t,n)宏是这一组宏的核心,其在具体使用中的功能可以概括为,通过传入链表节点p,还原出节点所在结构体的指针,进而能对结构体进行相应操作; 这一组双向链表宏其实形成的是一个循环双向链表; 这些宏最初是极客写出的,后来在linux内核中被推广使用。
原文标题:嵌入式开发中100%会用的几个宏,建议收藏
文章出处:【微信公众号:小麦大叔】欢迎添加关注!文章转载请注明出处。
谷歌母公司Alphabet Q4营收863.1亿美元较上年同期增长13%
荣耀V30和荣耀V30 Pro之间的区别是什么
利用Minitab的图形功能进行库存优化
Stm32学了好久了,感觉独立做项目还是有力不从心的感觉?
亚马逊正研发升级版AI家庭机器人 内含类ChatGPT功能
linux内核中llist.h文件中的链表宏讲解
可靠、低延时互联网传输SRT协议解答
航天飞机中惯导系统的工作原理是什么
什么是物联网技术对于农业的影响呢?与之发展趋势
热电阻分度表如何看?是什么意思?
长电科技发公告称:公司涉及诉讼,遭索赔1.7亿
全球半导体资本支出将首次超过1000亿美元,巨额资本用在哪里?
全新高通骁龙888 5G移动平台为智能手机解锁全新拍摄体验
MCS-51单片机的存储空间解析
背光种类有哪些
家里的绿植都out了?一款来源自然设计灵感的“岩石立方灯”来了
电气防火限流式保护器的功能及应用介绍
预计到2024年时,折叠屏出货量将达到5000万台
恩智浦与芯朋微电子领跑智慧节能家电发展
工字型电感和工字电感的区别