二级指针到底在改什么?——从C语言基础到Linux内核文件系统注册机制

二级指针到底在改什么?------从C语言基础到Linux内核文件系统注册机制

一、引言

你是否曾在阅读Linux内核代码时,被 struct file_system_type **p 这样的写法劝退过?

明明可以用一级指针,为什么内核开发者偏爱这种看起来更复杂的二级指针?它到底解决了什么问题?如果没有它,代码会变成什么样?

如果你也有过类似的困惑,那么这篇文章就是为你准备的。

本文目标: 从C语言二级指针的核心原理入手,通过指针遍历、链表插入的核心逻辑,最终落地到 Linux 内核文件系统注册链表的源码。

我们将打通一条完整的学习链路:

指针基础 → 二级指针本质 → 链表优雅实现 → 内核源码实战

解决两大核心问题:

  1. 二级指针到底在改什么?
  2. Linux 内核中那种无判断、极简的链表写法是如何实现的?

二、前置核心:二级指针的本质

2.1 一级指针的局限性

一级指针 T *p 只能做两件事:

  • 读取指向的数据 *p
  • 修改指向的数据内容

它有一个致命缺陷无法修改指针本身的指向。

如果你想在函数内部,修改外部传入的指针变量所指向的地址,一级指针完全做不到。

2.2 二级指针的唯一核心作用

二级指针 T **p 的本质是:指针变量的地址

我们可以这样理解:

  • 一级指针:改数据
  • 二级指针:改指针

换句话说,二级指针让你"拿到"了一级指针本身,从而可以直接修改它的指向、赋值、甚至置空。

2.3 二级指针使用示例

c 复制代码
#include <stdio.h>

// 二级指针:修改外部一级指针的指向
void changePtr(int **p) {
    static int new_val = 999;
    *p = &new_val;  // 直接修改外部指针的指向
}

int main(void) {
    int val = 100;
    int *p = &val;

    changePtr(&p);  
    printf("%d\n", *p); // 输出 999
    return 0;
}

核心逻辑: *p = &new_val 直接覆盖了外部一级指针的指向。这是一级指针永远做不到的。


三、进阶:二级指针如何解决链表痛点

3.1 一级指针实现单向链表的问题

用一级指针实现单向链表的尾插,你必须面对两个问题:

  1. 必须判空,逻辑冗余

    • 空链表和非空链表是两套处理逻辑
    • 无法用统一的代码表达"找到末尾并插入"
  2. 必须用返回值传递新头,接口不优雅

    • 函数不能直接修改外部的头指针
    • 必须通过返回值传递新头
    • 调用方必须记得接收返回值

下面是一级指针的尾插实现:

c 复制代码
Node* tailInsert(Node *head, int val) {
    Node *newNode = malloc(sizeof(Node));
    newNode->val = val;
    newNode->next = NULL;
    
    // 情况1:空链表 ------ 必须判空
    if (head == NULL) {
        return newNode;  // 返回新头
    }
    
    // 情况2:非空链表
    Node *p = head;
    while (p->next != NULL) {
        p = p->next;
    }
    p->next = newNode;
    return head;  // 返回原头
}

// 调用时必须接收返回值
list = tailInsert(list, 10);
list = tailInsert(list, 20);

3.2 二级指针如何统一处理

二级指针可以直接操作 "指针本身" ,从而统一处理空链表和非空链表,完全不需要 if 判空。

核心思想:

  • 不遍历节点,而是遍历 节点指针的地址
  • 最终停留的位置,就是可以挂载新节点的那个 空指针地址

下面是二级指针的尾插实现:

c 复制代码
void tailInsert(Node **head, int val) {
    Node *newNode = malloc(sizeof(Node));
    newNode->val = val;
    newNode->next = NULL;
    
    Node **pp = head;
    while (*pp) {
        pp = &(*pp)->next;
    }
    *pp = newNode;  // 统一处理,无需判断
}

注意 while 循环中的 pp = &(*pp)->next 这一行------它不是在移动指针,而是在移动 指针的地址。这是理解二级指针遍历链表的关键。


四、内核源码实战:文件系统注册链表

下面我们基于 Linux 内核原生的 register_filesystem 源码,解析二级指针链表的实际用法。

4.1 源码整体结构

c 复制代码
// 全局链表头:所有文件系统的总链表
static struct file_system_type *file_systems;

// 核心:用二级指针遍历链表,查找/定位插入点
static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
    struct file_system_type **p;
    for (p = &file_systems; *p; p = &(*p)->next)
        if (strncmp((*p)->name, name, len) == 0 &&
            !(*p)->name[len])
            break;
    return p;
}

int register_filesystem(struct file_system_type * fs)
{
    int res = 0;
    struct file_system_type ** p;

    // 参数校验(省略)
    write_lock(&file_systems_lock);
    p = find_filesystem(fs->name, strlen(fs->name));
    if (*p)
        res = -EBUSY;   // 已存在,注册失败
    else
        *p = fs;        // 不存在,直接插入!
    write_unlock(&file_systems_lock);
    return res;
}

4.2 核心逻辑逐行解析

1. 全局链表头

c 复制代码
static struct file_system_type *file_systems;

内核用这个指针维护所有已注册的文件系统:ext2 -> ext4 -> vfat -> ntfs -> sysfs -> ...,形成一个单向链表。

2. 二级指针遍历的核心

c 复制代码
for (p = &file_systems; *p; p = &(*p)->next)
  • p = &file_systems:二级指针指向头指针的地址
  • *p:判断当前节点是否存在,为空则终止遍历
  • p = &(*p)->next:移动到下一个节点的 next 指针的地址

遍历结束后,p 只会指向两种位置:

  • 找到了同名文件系统p 指向该节点的指针地址,*p != NULL
  • 未找到p 指向链表末尾的那个 NULLnext 指针地址,*p == NULL

3. 注册插入

c 复制代码
p = find_filesystem(fs->name, strlen(fs->name));
if (*p)
    res = -EBUSY;   // 已经存在
else
    *p = fs;        // 不存在,直接插入!

极简且优雅:

  • 节点已存在:返回设备忙,注册失败
  • 节点不存在:*p = fs 直接挂载新节点,完成尾插

五、核心原理总结

5.1 二级指针的本质

  • 一级指针:操作数据
  • 二级指针:操作指针本身(地址指向)

核心价值: 统一空链表和非空链表的操作,消除冗余的 if 判断,让代码更简洁、更优雅。

5.2 内核链表的设计精髓

  • 遍历时不移动节点,只移动指针的地址
  • 最终精准定位到可以插入的位置
  • 实现效果:
    • 代码极简,无冗余分支
    • 时间复杂度 O(n),性能稳定
    • 工业级可靠,内核全局复用

六、小结

本文从C语言二级指针的核心概念出发,用对比的方式讲清楚了一级指针的局限性,以及二级指针如何优雅地解决链表尾插问题。最后,我们通过 Linux 内核 register_filesystem 的源码,看到了二级指针在实际工程中的落地应用。


七、写在最后

如今,AI 可以帮我们生成代码、解释语法、甚至自动补全整个函数。很多人问:那这些底层的基础知识,还有必要花力气去啃吗?

我的答案是:不仅有必要,而且比以往任何时候都更有必要。

AI 擅长的是"拼图"------从海量已知的代码中,拼出最可能的答案。但它背后的数据结构是什么?它在几十年的演进中解决了哪些问题,又带来了哪些新的挑战?这些,恰恰是基础知识的价值所在。

比如掌握了二级指针,就不会在看到内核代码中的 struct file_system_type **p 时一头雾水;理解了链表的本质,就能一眼看出 for (p = &file_systems; *p; p = &(*p)->next) 这行代码的精妙之处。

AI 是工具,基础是使用工具的能力。它什么都会,但那都不是你的东西。

所以,无论技术如何变迁,那些关于内存、指针、数据结构、算法本质的东西,永远值得静下心来,一行一行地啃、一遍一遍地想。碰到想不明白的,我们也可以把AI这位"老师"拉过来,和它讨论,让它帮我们加速理解。

------但前提是,你得先有自己的问题。

相关推荐
wanQQ3 小时前
在 KDE 中将 Nemo 设为默认文件管理器后,浏览器仍调用 Dolphin 的解决方案
linux
认真的薛薛3 小时前
Linux基础:GitOps发布流程
java·linux·运维
dislike_shuati3 小时前
Ubuntu18多用户情况一用户桌面卡死,鼠标能动但点击没用——解决办法
linux·运维·服务器
Yeats_Liao4 小时前
物联网接入层技术剖析(四):当epoll遇见MQTT
java·linux·服务器·网络·物联网·架构
zzzyyy5384 小时前
利用AI整理进程池创建的思路和细节
linux
zandy10115 小时前
2026 主流技术栈:hermes agent多环境安装配置:Windows/Mac/Linux
linux·windows·macos
s_w.h5 小时前
【 linux 】理解进程状态
linux·运维·服务器
Fcy6485 小时前
Linux下 动、静态库的制作、使用与原理和ELF文件解析
linux·elf·动、静态库
身如柳絮随风扬5 小时前
CentOS 7 搭建 MySQL 主从复制集群:从零到生产级高可用
linux·mysql·centos