Linux内核链表(list.h)实战:实现人员信息管理系统

一、内核链表的核心优势

传统链表通常将数据直接封装在链表节点中(如 struct node { data; struct node *next; }),这种设计的问题是:每一种数据类型都要重新实现一套链表操作(比如Person链表、Student链表各写一套add/del)。

而Linux内核链表的核心优势在于:

  1. 通用性:链表节点(struct list_head)嵌入到业务数据结构体中,一套链表操作适配所有数据类型;
  2. 低侵入性:无需修改业务数据结构的核心字段,仅需添加一个list_head成员;
  3. 高效性:提供丰富的遍历、增删宏,底层由内核优化,兼顾性能与安全性;
  4. 安全性:提供list_for_each_entry_safe等安全遍历宏,避免遍历中删除节点导致的崩溃。

二、内核链表核心原理

2.1 核心结构体定义

list.h 的核心是双向链表节点结构体,仅包含前驱/后继指针,不耦合任何业务数据:

struct list_head {
  struct list_head *next, *prev; // 前驱、后继指针
};

2.2 关键宏解析

内核链表的“通用魔法”依赖以下核心宏,是理解链表操作的关键:

作用 核心逻辑
offsetof(TYPE, MEMBER) 计算结构体成员MEMBER相对于结构体起始地址的偏移量 通过(TYPE*)0虚拟结构体指针,取MEMBER的地址即为偏移量
container_of(ptr, type, member) 通过成员指针反向获取结构体指针 用成员地址 - 偏移量 = 结构体起始地址
list_entry(ptr, type, member) 封装container_of,通过list_head指针获取业务结构体指针 内核链表遍历的核心宏
list_for_each_entry_safe(pos, n, head, member) 安全遍历链表(允许遍历中删除节点) 提前保存下一个节点的指针,避免删除当前节点后遍历中断

2.3 核心操作接口

list.h 提供了链表的基础操作接口,无需重复实现:

接口 功能
LIST_HEAD(name) 定义并初始化链表头
list_add_tail(new, head) 将新节点添加到链表尾部(适合队列)
list_del(entry) 从链表中删除节点
list_empty(head) 判断链表是否为空

三、实战:人员信息管理系统

基于 list.h 实现人员(Person)信息的增、删、改、查、展示,完整代码可直接编译运行。

3.1 业务数据结构定义

struct list_head嵌入到Person结构体中,作为链表节点:

#include "list.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 人员信息结构体:核心业务数据 + 链表节点
typedef struct 
{
    char name[30];    // 姓名
    int age;          // 年龄
    struct list_head node; // 内核链表节点(嵌入)
    char addr[50];    // 地址
} Person;

// 定义并初始化链表头(全局链表,管理所有Person节点)
LIST_HEAD(PerHead);

3.2 核心功能实现

(1)添加人员(尾插法)
/**
 * @brief 添加人员到链表
 * @param name 姓名
 * @param age 年龄
 * @param addr 地址
 * @return 0-成功,1-失败
 */
int add_per(char* name, int age, char* addr)
{
    // 1. 分配Person内存
    Person* p = malloc(sizeof(Person));
    if (NULL == p)
    {
        perror("add_per malloc error");
        return 1;
    }
    // 2. 初始化业务数据
    strcpy(p->name, name);
    strcpy(p->addr, addr);
    p->age = age;
    // 3. 将节点添加到链表尾部(list_add_tail:尾插,符合队列顺序)
    list_add_tail(&p->node, &PerHead);
    printf("添加成功:%s(%d岁,地址:%s)\n", name, age, addr);
    return 0;
}
(2)删除人员(按姓名)

使用list_for_each_entry_safe安全遍历,避免删除节点导致遍历崩溃:

/**
 * @brief 按姓名删除人员
 * @param name 要删除的姓名
 * @return 0-成功,1-失败
 */
int del_per(char *name)
{
    Person* p, *n; // p:当前节点,n:下一个节点(安全遍历用)
    // 安全遍历链表:&PerHead是链表头,node是Person中的链表节点成员
    list_for_each_entry_safe(p, n, &PerHead, node)
    {
        if (0 == strcmp(p->name, name))
        {
            printf("删除成功:%s\n", name);
            list_del(&p->node); // 从链表中删除节点
            free(p);           // 释放Person内存(关键:避免内存泄露)
            return 0;
        }
    }
    printf("删除失败:未找到姓名%s\n", name);
    return 1;
}
(3)修改人员信息(按姓名)
/**
 * @brief 按旧姓名修改人员信息
 * @param oldname 原姓名
 * @param name 新姓名
 * @param age 新年龄
 * @param addr 新地址
 * @return 0-成功,1-失败
 */
int mod_per(char* oldname, char *name, int age, char* addr)
{
    Person* p, *n;
    list_for_each_entry_safe(p, n, &PerHead, node)
    {
        if (0 == strcmp(oldname, p->name))
        {
            // 更新数据
            strcpy(p->name, name);
            strcpy(p->addr, addr);
            p->age = age;
            printf("修改成功:%s → %s(%d岁,地址:%s)\n", oldname, name, age, addr);
            return 0;
        }
    }
    printf("修改失败:未找到姓名%s\n", oldname); // 修复原代码:提示旧姓名而非新姓名
    return 1;
}
(4)按姓名查询人员

补充原代码中空实现的find_per

/**
 * @brief 按姓名查询人员
 * @param name 要查询的姓名
 * @return 0-成功,1-失败
 */
int find_per(char *name)
{
    Person* p, *n;
    list_for_each_entry_safe(p, n, &PerHead, node)
    {
        if (0 == strcmp(p->name, name))
        {
            printf("查询到:姓名=%s,年龄=%d,地址=%s\n", p->name, p->age, p->addr);
            return 0;
        }
    }
    printf("查询失败:未找到姓名%s\n", name);
    return 1;
}
(5)展示所有人员
/**
 * @brief 展示链表中所有人员信息
 * @return 0-成功
 */
int show_per()
{
    if (list_empty(&PerHead)) // 判断链表是否为空
    {
        printf("当前无人员信息\n");
        return 0;
    }
    Person* p, *n;
    printf("===== 人员列表 =====\n");
    list_for_each_entry_safe(p, n, &PerHead, node)
    {
        printf("姓名:%s,年龄:%d,地址:%s\n", p->name, p->age, p->addr);
    }
    printf("====================\n");
    return 0;
}

3.3 测试主函数

int main(int argc, char **argv)
{
    // 1. 添加人员
    add_per("zhangsan", 20, "北京市海淀区101号");
    add_per("lisi", 21, "上海市浦东新区102号");
    add_per("wangmaizi", 23, "广州市天河区103号");

    // 2. 展示所有人员
    show_per();

    // 3. 删除不存在的人员(测试失败场景)
    printf("----- 测试删除 -----\n");
    del_per("li1si");
    show_per();

    // 4. 修改人员信息
    printf("----- 测试修改 -----\n");
    mod_per("lisi", "aaa", 11, "深圳市南山区201号");
    show_per();

    // 5. 查询人员
    printf("----- 测试查询 -----\n");
    find_per("aaa");
    find_per("zhangsan123");

    return 0;
}

3.4 编译与运行结果

编译命令
# 将list.h和主代码放在同一目录,编译
gcc person_list.c -o person_list -Wall -g
运行输出
添加成功:zhangsan(20岁,地址:北京市海淀区101号)
添加成功:lisi(21岁,地址:上海市浦东新区102号)
添加成功:wangmaizi(23岁,地址:广州市天河区103号)
===== 人员列表 =====
姓名:zhangsan,年龄:20,地址:北京市海淀区101号
姓名:lisi,年龄:21,地址:上海市浦东新区102号
姓名:wangmaizi,年龄:23,地址:广州市天河区103号
====================
----- 测试删除 -----
删除失败:未找到姓名li1si
===== 人员列表 =====
姓名:zhangsan,年龄:20,地址:北京市海淀区101号
姓名:lisi,年龄:21,地址:上海市浦东新区102号
姓名:wangmaizi,年龄:23,地址:广州市天河区103号
====================
----- 测试修改 -----
修改成功:lisi → aaa(11岁,地址:深圳市南山区201号)
===== 人员列表 =====
姓名:zhangsan,年龄:20,地址:北京市海淀区101号
姓名:aaa,年龄:11,地址:深圳市南山区201号
姓名:wangmaizi,年龄:23,地址:广州市天河区103号
====================
----- 测试查询 -----
查询到:姓名=aaa,年龄=11,地址:深圳市南山区201号
查询失败:未找到姓名zhangsan123

四、内核链表使用注意事项

  1. 内存管理list_del仅从链表中移除节点,不会释放业务数据内存,需手动free(核心:避免内存泄露);
  2. 安全遍历:遍历中删除节点必须用list_for_each_entry_safe,否则会因next指针失效导致程序崩溃;
  3. 链表头初始化:必须通过LIST_HEADINIT_LIST_HEAD初始化链表头,否则会出现野指针;
  4. 字符串操作:使用strcpy时需确保目标数组足够大(如name[30]),避免缓冲区溢出(进阶可改用strncpy)。

五、总结

Linux内核链表(list.h)是“通用数据结构”的典范,其核心设计思想是将链表与业务数据解耦——通过嵌入list_head节点,实现一套操作适配所有数据类型。本文通过人员信息管理系统的实战,覆盖了内核链表的核心操作:

  1. 嵌入链表节点到业务结构体;
  2. list_add_tail添加节点、list_del删除节点;
  3. list_for_each_entry_safe安全遍历;
  4. container_of/list_entry实现链表节点到业务数据的转换。

掌握内核链表的使用,不仅能大幅减少重复代码,更能理解Linux内核的设计哲学——通用、高效、低耦合,这也是嵌入式开发和内核开发的核心思维之一。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐