25、Linux内核链表(list.h)实战:实现人员信息管理系统
·
Linux内核链表(list.h)实战:实现人员信息管理系统
一、内核链表的核心优势
传统链表通常将数据直接封装在链表节点中(如 struct node { data; struct node *next; }),这种设计的问题是:每一种数据类型都要重新实现一套链表操作(比如Person链表、Student链表各写一套add/del)。
而Linux内核链表的核心优势在于:
- 通用性:链表节点(
struct list_head)嵌入到业务数据结构体中,一套链表操作适配所有数据类型; - 低侵入性:无需修改业务数据结构的核心字段,仅需添加一个
list_head成员; - 高效性:提供丰富的遍历、增删宏,底层由内核优化,兼顾性能与安全性;
- 安全性:提供
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
四、内核链表使用注意事项
- 内存管理:
list_del仅从链表中移除节点,不会释放业务数据内存,需手动free(核心:避免内存泄露); - 安全遍历:遍历中删除节点必须用
list_for_each_entry_safe,否则会因next指针失效导致程序崩溃; - 链表头初始化:必须通过
LIST_HEAD或INIT_LIST_HEAD初始化链表头,否则会出现野指针; - 字符串操作:使用
strcpy时需确保目标数组足够大(如name[30]),避免缓冲区溢出(进阶可改用strncpy)。
五、总结
Linux内核链表(list.h)是“通用数据结构”的典范,其核心设计思想是将链表与业务数据解耦——通过嵌入list_head节点,实现一套操作适配所有数据类型。本文通过人员信息管理系统的实战,覆盖了内核链表的核心操作:
- 嵌入链表节点到业务结构体;
- 用
list_add_tail添加节点、list_del删除节点; - 用
list_for_each_entry_safe安全遍历; - 用
container_of/list_entry实现链表节点到业务数据的转换。
掌握内核链表的使用,不仅能大幅减少重复代码,更能理解Linux内核的设计哲学——通用、高效、低耦合,这也是嵌入式开发和内核开发的核心思维之一。
更多推荐


所有评论(0)