【linux kernel 常用数据结构和设计模式】【数据结构 2】【通过一个案例属性list、hlist、rbtree、xarray数据结构使用】

发布于:2025-09-08 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、背景

我在前面的例子中讲解了 内核常用的数据结构, 本机我们就拿 上节中 讲解到的 list、hlist、rbtree、xarray 来写一个 有意义的测试案例。

  • 在汽车电子里,MCU(比如车身域控制器里的 STM32)通过 SPI 向车机 SOC 定期上传各种信号和事件

  • 这些信号类型包括:

    1. 传感器数据(温度、车速、油门位置等)

    2. 诊断消息(MCU 自检、故障码上报)

    3. 控制命令(开关灯、空调风速等)

    4. 异步事件(按钮按下、CAN 报文转发)

  • 驱动需要缓存、分类、调度这些消息,以便车机上层应用(HMI 或中间件)读取。

1. 该如何设计我们的测试demo

  • 驱动模拟:车内 MCU 通过 SPI 向车机发送消息(本 demo 不直接与真实 SPI 硬件交互,而是通过 /proc/spi_demo 写入模拟消息)。

  • 写入格式(ASCII):SEQ:MSGID:DELAY_MS:PAYLOAD,示例:42:7:100:HELLO

    • SEQ:消息序列号 —— 存储 payload 的索引(用于 xarray)。

    • MSGID:消息类型 ID —— 在哈希表(hlist)中查找 handler。

    • DELAY_MS:延迟毫秒 —— 若 >0 则用 rbtree 排序后延迟处理。

    • PAYLOAD:消息的文本数据(不包含 :)。

  • 读取 /proc/spi_demo 可以查看内部状态:已注册设备(list),handlers(hlist/hash),已排期事件(rbtree),已存储消息(xarray)。


  • list_head:MCU 数量不多,但需要遍历,双向链表 O(1) 插删最合适。

  • hlist/hash:消息类型很多(几十/上百种),但分发必须快,hash 平均 O(1) 查找是最佳选择。

  • xarray:MCU 上报数据可能有几万条 seq,稀疏且需要随机访问,xarray 比数组节省内存,比链表查找更快。

  • rbtree:事件调度必须严格时间有序,红黑树保证插入 O(log n),且 rb_first() 总能拿到最近到期任务,适合 timer 驱动。

2. 如何映射到四种数据结构

  1. list_head:SPI 设备列表

    • 车上可能有多个 SPI MCU(例如 MCU-A 管理车身、MCU-B 管理空调),驱动维护一个链表存放当前挂接的 SPI 从设备。

    • 一对多关系:总线 → 多个 SPI 设备。

  2. hlist(哈希表):消息处理器表

    • 每个消息类型(msg_id)绑定一个 handler。

    • 例如 msg_id=1 → 温度数据解析函数,msg_id=2 → 车速解析函数。

    • 查找快,适合 MCU 发来的消息要立刻分发。

  3. xarray:按序号存储原始消息

    • 每条 MCU 上报的消息都有一个 seq 序号。

    • 存入 xarray 后,可以快速随机访问(比如 HMI 想读取最近 1000 条中第 500 条)。

    • 稀疏友好(如果 MCU 序号不是连续的,也没问题)。

  4. rbtree:延迟处理的事件队列

    • 某些 MCU 消息需要在未来某个时间点处理(例如“延迟 2 秒开灯”,“500ms 后触发蜂鸣器”)。

    • 驱动把这些事件按到期时间插入红黑树,timer 定时扫描最近要到期的节点,保证事件按时间顺序触发。

3. 场景

MCU-A 通过 SPI 发来以下报文:

  • 报文 1seq=101, msg_id=1, delay=0, payload=TEMP=25.5
    → 立即处理,handler=温度解析,结果更新到车机的传感器缓存。

  • 报文 2seq=102, msg_id=8, delay=500ms, payload=BUZZER_ON
    → 插入 rbtree,定时器 500ms 后执行蜂鸣器打开动作。

  • 报文 3seq=103, msg_id=7, delay=0, payload=DIAG:0xE101
    → handler 解析诊断信息,存入日志。

最终:

  • /proc/spi_demo 能看到:

    • 已注册设备列表(list_head)

    • handler 哈希表(hlist)

    • 已存消息(xarray)

    • 等待执行的定时任务(rbtree)

二、demo

1. code

/*
 * spi_ds_demo.c - demo kernel module combining list_head, hlist, rbtree, xarray
 *
 * Scenario: 車载 MCUs 通过 SPI 发送消息到车机的 SPI 驱动,驱动负责接收消息,
 * 存储原始 payload(按 sequence 使用 xarray),根据消息 ID 查找 handler(使用 hlist/hash),
 * 可为消息安排延迟处理(使用 rbtree 以 expires 排序),并维护已注册的 SPI 设备列表(list_head)。
 *
 * 交互接口:
 *  - /proc/spi_demo      : 读出当前内部状态(devices, handlers, scheduled events, stored messages)
 *  - 写入 /proc/spi_demo:以 ASCII 写入模拟 MCU 消息,格式为:
 *        SEQ:MSGID:DELAY_MS:PAYLOAD
 *    例如: 42:7:100:HELLO
 *    - SEQ: 序列号(整数,用于存储 payload 到 xarray)
 *    - MSGID: 消息 ID(用于哈希查找 handler)
 *    - DELAY_MS: 延迟毫秒(0 表示立即处理)
 *    - PAYLOAD: 不包含 ':' 的字符串负载(最大 200 字节)
 *
 * 数据结构展示映射:
 *  - list_head: 管理已注册的 spi 设备实例(one-to-many: bus -> devices)
 *  - hlist/hashtable: map msg_id -> handler (hash 桶)
 *  - rbtree: 按过期时间排序的延迟事件调度(支持按时间先后顺序处理)
 *  - xarray: 按序列号存储 payload,支持大稀疏序列空间
 *
 * 适用于学习/测试数据结构的驱动 demo(不与真实 SPI 硬件通信)。
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/hashtable.h>
#include <linux/rbtree.h>
#include <linux/xarray.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>
#include <linux/jiffies.h>
#include <linux/timer.h>
#include <linux/spinlock.h>

#define PROC_NAME "spi_demo"
#define MAX_PAYLOAD 200
#define HANDLER_BITS 4 /* 16 buckets */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("leo");
MODULE_DESCRIPTION("Demo SPI driver showing list_head, hlist, rbtree, xarray");

/* ---------- list_head: simulated SPI device instances ---------- */
struct spi_dev_entry {
    int id; /* device id */
    char name[32];
    struct list_head list; /* list_head 用于把设备挂到全局 device_list */
};
static LIST_HEAD(device_list);
static spinlock_t device_lock;

/* helper to create/register a fake device */
static struct spi_dev_entry *spi_dev_create(int id, const char *name)
{
    struct spi_dev_entry *e;
    e = kzalloc(sizeof(*e), GFP_KERNEL);
    if (!e)
        return NULL;
    e->id = id;
    strlcpy(e->name, name, sizeof(e->name));
    INIT_LIST_HEAD(&e->list);
    spin_lock(&device_lock);
    list_add_tail(&e->list, &device_list);
    spin_unlock(&device_lock);
    return e;
}

static void spi_dev_destroy_all(void)
{
    struct spi_dev_entry *pos, *tmp;
    spin_lock(&device_lock);
    list_for_each_entry_safe(pos, tmp, &device_list, list) {
        list_del(&pos->list);
        kfree(pos);
    }
    spin_unlock(&device_lock);
}

/* ---------- hlist/hashtable: message id -> handler mapping ---------- */
struct msg_handler {
    u32 msg_id;
    char name[32];
    struct hlist_node hnode;
};
static DEFINE_HASHTABLE(handler_ht, HANDLER_BITS);
static spinlock_t handler_lock;

static void handler_register(u32 id, const char *name)
{
    struct msg_handler *h;
    h = kzalloc(sizeof(*h), GFP_KERNEL);
    if (!h)
        return;
    h->msg_id = id;
    strlcpy(h->name, name, sizeof(h->name));
    spin_lock(&handler_lock);
    hash_add(handler_ht, &h->hnode, h->msg_id);
    spin_unlock(&handler_lock);
}

static void handler_unregister_all(void)
{
    int b;
    struct msg_handler *h;
    struct hlist_node *tmp;
    spin_lock(&handler_lock);
    hash_for_each_safe(handler_ht, b, tmp, h, hnode) {
        hash_del(&h->hnode);
        kfree(h);
    }
    spin_unlock(&handler_lock);
}

static struct msg_handler *handler_find(u32 id)
{
    struct msg_handler *h;
    spin_lock(&handler_lock);
    hash_for_each_possible(handler_ht, h, hnode, id) {
        if (h->msg_id == id) {
            spin_unlock(&handler_lock);
            return h;
        }
    }
    spin_unlock(&handler_lock);
    return NULL;
}

/* ---------- xarray: store raw payloads by sequence number ---------- */
static DEFINE_XARRAY(msg_xa); /* seq -> *struct stored_msg */

struct stored_msg {
    u32 seq;
    u32 msg_id;
    size_t len;
    char *data; /* allocated payload */
};

static void stored_msg_free(struct stored_msg *m)
{
    if (!m) return;
    kfree(m->data);
    kfree(m);
}

/* ---------- rbtree: scheduled events sorted by expires (jiffies) ---------- */
struct ev_node {
    unsigned long expires; /* jiffies */
    u32 seq; /* sequence number associated */
    struct rb_node node;
};
static struct rb_root ev_tree = RB_ROOT;
static spinlock_t ev_lock; /* protect ev_tree */
static struct timer_list ev_timer;
static unsigned long timer_interval_jiffies = HZ / 10; /* 100ms */

/* insert event into rbtree (sorted by expires then seq) */
static void ev_insert(struct ev_node *en)
{
    struct rb_node **link = &ev_tree.rb_node, *parent = NULL;
    struct ev_node *entry;

    spin_lock(&ev_lock);
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct ev_node, node);
        if (en->expires < entry->expires ||
           (en->expires == entry->expires && en->seq < entry->seq))
            link = &(*link)->rb_left;
        else
            link = &(*link)->rb_right;
    }
    rb_link_node(&en->node, parent, link);
    rb_insert_color(&en->node, &ev_tree);
    spin_unlock(&ev_lock);
}

static struct ev_node *ev_first_locked(void)
{
    struct rb_node *n = rb_first(&ev_tree);
    if (!n) return NULL;
    return rb_entry(n, struct ev_node, node);
}

static void ev_remove_node_locked(struct ev_node *en)
{
    rb_erase(&en->node, &ev_tree);
}

/* process expired events: pop all with expires <= jiffies */
static void process_expired_events(void)
{
    struct rb_node *n;
    struct ev_node *en;
    unsigned long now = jiffies;

    for (;;) {
        spin_lock(&ev_lock);
        n = rb_first(&ev_tree);
        if (!n) {
            spin_unlock(&ev_lock);
            break;
        }
        en = rb_entry(n, struct ev_node, node);
        if (time_after(en->expires, now)) {
            spin_unlock(&ev_lock);
            break;
        }
        /* remove then process */
        rb_erase(&en->node, &ev_tree);
        spin_unlock(&ev_lock);

        /* handle event: in demo just print and free */
        pr_info("[ev] processing seq=%u (expires at %lu, now=%lu)\n", en->seq, en->expires, now);
        kfree(en);
    }
}

static void ev_timer_fn(struct timer_list *t)
{
    process_expired_events();
    /* re-arm timer if tree still has nodes */
    spin_lock(&ev_lock);
    if (ev_tree.rb_node)
        mod_timer(&ev_timer, jiffies + timer_interval_jiffies);
    spin_unlock(&ev_lock);
}

/* schedule an event for seq after delay_ms */
static void schedule_event(u32 seq, unsigned int delay_ms)
{
    struct ev_node *en;
    en = kzalloc(sizeof(*en), GFP_KERNEL);
    if (!en) return;
    en->seq = seq;
    en->expires = jiffies + msecs_to_jiffies(delay_ms);
    ev_insert(en);
    /* ensure timer running */
    mod_timer(&ev_timer, jiffies + timer_interval_jiffies);
}

/* ---------- procfs interface: read status, write to inject messages ---------- */
static int spi_proc_show(struct seq_file *m, void *v)
{
    struct spi_dev_entry *d;
    int b;
    struct msg_handler *h;
    struct stored_msg *sm;
    unsigned long idx = 0;
    struct rb_node *n;
    struct ev_node *en;

    seq_puts(m, "=== spi_demo status ===\n");

    seq_puts(m, "-- devices (list_head) --\n");
    spin_lock(&device_lock);
    list_for_each_entry(d, &device_list, list) {
        seq_printf(m, "dev id=%d name=%s\n", d->id, d->name);
    }
    spin_unlock(&device_lock);

    seq_puts(m, "-- handlers (hashtable/hlist) --\n");
    spin_lock(&handler_lock);
    hash_for_each(handler_ht, b, h, hnode)
        seq_printf(m, "handler msg_id=%u name=%s\n", h->msg_id, h->name);
    spin_unlock(&handler_lock);

    seq_puts(m, "-- scheduled events (rbtree) --\n");
    spin_lock(&ev_lock);
    for (n = rb_first(&ev_tree); n; n = rb_next(n)) {
        en = rb_entry(n, struct ev_node, node);
        seq_printf(m, "seq=%u expires(in jiffies)=%lu now=%lu\n", en->seq, en->expires, jiffies);
    }
    spin_unlock(&ev_lock);

    seq_puts(m, "-- stored messages (xarray) --\n");
    xa_for_each(&msg_xa, idx, sm) {
        seq_printf(m, "seq=%lu msg_id=%u len=%zu data=%s\n", idx, sm->msg_id, sm->len, sm->data ? sm->data : "<null>");
    }

    return 0;
}

static int spi_proc_open(struct inode *inode, struct file *file)
{
    return single_open(file, spi_proc_show, NULL);
}

/* parse a line like: SEQ:MSGID:DELAY_MS:PAYLOAD */
static ssize_t spi_proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
    char buf[256];
    char *bufp = buf;
    unsigned int seq, msgid, delay;
    char payload[MAX_PAYLOAD + 1];
    char *p1, *p2, *p3;
    int ret;
    void *stored;
    struct stored_msg *sm;
    struct msg_handler *h;

    if (count == 0 || count >= sizeof(buf))
        return -EINVAL;
    if (copy_from_user(buf, ubuf, count))
        return -EFAULT;
    buf[count] = '\0';

    /* simple parse by tokens */
    p1 = strsep(&bufp, ":");
    p2 = strsep(&bufp, ":");
    p3 = strsep(&bufp, ":");
    if (!p1 || !p2 || !p3)
        return -EINVAL;
    /* the rest is payload (trim newline) */
    strncpy(payload, buf ? buf : "", MAX_PAYLOAD);
    payload[MAX_PAYLOAD] = '\0';
    /* strip trailing newline */
    ret = strlen(payload);
    if (ret && payload[ret-1] == '\n') payload[ret-1] = '\0';

    if (kstrtouint(p1, 10, &seq) || kstrtouint(p2, 10, &msgid) || kstrtouint(p3, 10, &delay))
        return -EINVAL;

    pr_info("[write] got seq=%u msgid=%u delay=%u payload=%s\n", seq, msgid, delay, payload);

    /* store payload in xarray */
    sm = kzalloc(sizeof(*sm), GFP_KERNEL);
    if (!sm) return -ENOMEM;
    sm->seq = seq;
    sm->msg_id = msgid;
    sm->len = strlen(payload) + 1;
    sm->data = kzalloc(sm->len, GFP_KERNEL);
    if (!sm->data) { kfree(sm); return -ENOMEM; }
    strncpy(sm->data, payload, sm->len);

    stored = xa_store(&msg_xa, seq, sm, GFP_KERNEL);
    if (stored) {
        /* existing entry replaced: free old */
        stored_msg_free(stored);
    }

    /* find handler, if none just log; if delay then schedule */
    h = handler_find(msgid);
    if (!h) {
        pr_info("[write] no handler for msgid=%u\n", msgid);
    } else {
        pr_info("[write] found handler %s for msgid=%u\n", h->name, msgid);
    }

    if (delay == 0) {
        pr_info("[write] immediate process seq=%u\n", seq);
        /* immediate processing: in demo just print */
        pr_info("[process] seq=%u payload=%s handler=%s\n", seq, sm->data, h ? h->name : "<none>");
    } else {
        schedule_event(seq, delay);
    }

    return count;
}

static const struct proc_ops spi_proc_fops = {
    .proc_open = spi_proc_open,
    .proc_read = seq_read,
    .proc_lseek = seq_lseek,
    .proc_release = single_release,
    .proc_write = spi_proc_write,
};

/* ---------- module init/exit ---------- */
static struct proc_dir_entry *proc_entry;

static int __init spi_demo_init(void)
{
    pr_info("spi_ds_demo init\n");
    spin_lock_init(&device_lock);
    spin_lock_init(&handler_lock);
    spin_lock_init(&ev_lock);

    /* create some fake spi devices (list_head usage) */
    spi_dev_create(1, "spi_mcu_a");
    spi_dev_create(2, "spi_mcu_b");

    /* register some handlers (hashtable/hlist usage) */
    handler_register(1, "power_btn");
    handler_register(2, "sensor_update");
    handler_register(7, "diag_msg");

    /* create proc entry for interaction */
    proc_entry = proc_create(PROC_NAME, 0666, NULL, &spi_proc_fops);
    if (!proc_entry) {
        pr_err("failed to create /proc/%s\n", PROC_NAME);
        handler_unregister_all();
        spi_dev_destroy_all();
        return -ENOMEM;
    }

    /* setup timer for rbtree event polling */
    timer_setup(&ev_timer, ev_timer_fn, 0);

    pr_info("spi_ds_demo ready. write to /proc/%s lines like SEQ:MSGID:DELAY_MS:PAYLOAD\n", PROC_NAME);
    return 0;
}

static void __exit spi_demo_exit(void)
{
    unsigned long idx = 0;
    struct stored_msg *sm;
    struct rb_node *n;
    struct ev_node *en;

    pr_info("spi_ds_demo exit\n");
    /* remove proc entry */
    if (proc_entry)
        proc_remove(proc_entry);

    /* cancel timer and free rbtree events */
    del_timer_sync(&ev_timer);
    spin_lock(&ev_lock);
    for (n = rb_first(&ev_tree); n;) {
        en = rb_entry(n, struct ev_node, node);
        n = rb_next(n);
        rb_erase(&en->node, &ev_tree);
        kfree(en);
    }
    spin_unlock(&ev_lock);

    /* free xarray stored messages */
    xa_for_each(&msg_xa, idx, sm) {
        xa_erase(&msg_xa, idx);
        stored_msg_free(sm);
    }

    /* unregister handlers and fake devices */
    handler_unregister_all();
    spi_dev_destroy_all();

    pr_info("spi_ds_demo cleaned up\n");
}

module_init(spi_demo_init);
module_exit(spi_demo_exit);

2.测试

# write immediate
echo -n "1:2:0:hello_from_mcu" > /proc/spi_demo

# write delayed by 500 ms
echo -n "2:7:500:diag_message" > /proc/spi_demo


cat /proc/spi_demo

3. 代码要点说明(更详细)

  • list_head (device_list):

    • 初始化:LIST_HEAD(device_list),并在 init 中调用 spi_dev_create(1, "spi_mcu_a") 等模拟注册。

    • 读取 proc 时遍历:list_for_each_entry(...) 打印设备信息。

    • 清理时使用 list_for_each_entry_safe 并调用 kfree

  • hlist/hashtable (handler_ht):

    • 使用 DEFINE_HASHTABLE(handler_ht, HANDLER_BITS) 创建哈希表。

    • handler_register(id, name) 添加 handler(hash_add),查找时用 hash_for_each_possible

    • 适合 msg_id -> handler 的映射,查找平均 O(1)。

  • xarray (msg_xa):

    • 存储 struct stored_msg *,通过 xa_store(&msg_xa, seq, sm, GFP_KERNEL) 存放。

    • 遍历使用 xa_for_each(&msg_xa, idx, sm) 来在 proc 中列出。

    • 适合按 seq 随机访问与稀疏序号空间。

  • rbtree (ev_tree):

    • 用于存储延迟事件节点 struct ev_node { expires, seq, rb_node },插入通过比较 expires(若相同再比较 seq)。

    • 采用 rb_link_node/rb_insert_color 插入,rb_first/rb_next 顺序遍历。

    • Timer 周期性调用 process_expired_events(),将已过期事件弹出并处理。

    • 适合需要按时间顺序调度与弹出的场景(例如延迟命令、timeout 管理)。

  • 同步:

    • 每个容器(device_list, handler_ht, ev_tree)都由各自的 spinlock 保护。

    • xarray 内部有自己的同步约束;写时用 xa_store(带锁),读时用 xa_for_each(用于 safe 遍历)。

  • 内存管理:

    • 所有 kmalloc 的对象在模块卸载时清理(xarray 元素、rbtree 节点、handler、devices)。

4. 一对多关系的表达方式(在 demo 中体现)

  • bus (implicit) -> 多个 spi devices:用 list_head 把多个 spi_dev_entry 挂在 device_list 上(父对象通过维护 list_head 表示它拥有多个孩子)。

  • msg_id -> 多个消息:哈希桶 hashtable(每个桶是 hlist)里可能包含相同 msg_id 的多个 handler(在本 demo 中一对一,但数据结构支持一对多)。

  • seq -> payloads:xarray 用索引 seq 映射到单个 payload;若需要每个 seq 对应多个子项,可以把 xarray 的 v


网站公告

今日签到

点亮在社区的每一天
去签到