linux 内核开发 常用的 数据结构及接口 源码分析


概览

  • task_struct: Linux 内核中描述一个进程或线程的所有信息,是进程调度的基本单位,通常被称为“进程描述符”。
  • mm_struct: 描述一个进程的完整内存地址空间,包括虚拟内存区域(VMA)、页表等。
  • vm_area_struct (VMA): 描述进程地址空间中一段连续的、具有相同属性的虚拟内存区域。
  • sk_buff (Socket Buffer): 在整个网络协议栈中用于传递网络数据的核心数据结构。
  • net_device: 在内核中代表一个网络接口(如 eth0),是连接协议栈和硬件驱动的桥梁。

下面我们将逐一进行源码层面的分析。


a. task_struck – 进程描述符

task_struct 是 Linux 内核中最为庞大的数据结构之一,它定义了一个进程/线程的全部上下文。

源码位置: include/linux/sched.h

核心作用:
task_struct 包含了内核管理一个进程所需的所有信息,可以分为以下几大类:

  1. 状态与标识: 进程当前的状态(运行、睡眠、僵尸等)和唯一标识(PID)。
  2. 调度信息: 调度器用来决定下一个运行进程的全部信息(优先级、调度策略、CPU 时间消耗等)。
  3. 内存管理: 指向该进程的地址空间描述符(mm_struct)。
  4. 文件系统信息: 进程当前的工作目录、根目录等。
  5. 文件描述符: 进程打开的所有文件。
  6. 信号处理: 待处理信号、信号处理函数等。
  7. 进程关系: 父进程、子进程、线程组等关系。

关键字段分析 (Linux 5.15):

// include/linux/sched.h
struct task_struct {
    // 1. 状态与调度
    volatile long               state;          /* -1 unrunnable, 0 runnable, >0 stopped */
    void                        *stack;         /* 内核栈指针 */
    unsigned int                flags;          /* 进程标志, 如 PF_KTHREAD (内核线程) */
    int                         prio;           /* 动态优先级 */
    int                         static_prio;    /* 静态优先级 */
    int                         normal_prio;
    unsigned int                rt_priority;    /* 实时优先级 */
    const struct sched_class    *sched_class;   /* 指向所属的调度器类 (如 CFS 的 fair_sched_class) */
    struct sched_entity         se;             /* CFS 调度实体 */
    struct sched_rt_entity      rt;             /* RT 调度实体 */

    // 2. 标识符
    pid_t                       pid;            /* 进程ID */
    pid_t                       tgid;           /* 线程组ID (主线程的PID) */

    // 3. 内存管理
    struct mm_struct            *mm;            /* 指向进程地址空间 */
    struct mm_struct            *active_mm;     /* 活动的地址空间 (内核线程会借用) */

    // 4. 进程关系
    struct task_struct __rcu    *real_parent;   /* 真正的父进程 (fork时创建) */
    struct task_struct __rcu    *parent;        /* 父进程 (可能因 ptrace 改变) */
    struct list_head            children;       /* 子进程链表头 */
    struct list_head            sibling;        /* 兄弟进程链表 */
    struct task_struct          *group_leader;  /* 线程组的领导者 */

    // 5. Credentials (权限)
    const struct cred __rcu     *cred;          /* 进程凭证 (uid, gid等) */

    // 6. 文件系统与文件描述符
    struct fs_struct            *fs;            /* 文件系统信息 (pwd, root) */
    struct files_struct         *files;         /* 打开的文件描述符表 */

    // 7. 信号
    struct signal_struct        *signal;
    struct sighand_struct       *sighand;
    sigset_t                    blocked;        /* 阻塞的信号掩码 */

    // 8. 其他
    struct nsproxy              *nsproxy;       /* 命名空间代理 */
    // ... 还有非常多其他字段
};

相关接口:

  • 创建: fork()vfork()clone() 系统调用最终都会调用内核函数 _do_fork(),其核心是 copy_process()copy_process() 负责分配一个新的 task_struct (alloc_task_struct()) 并复制父进程的上下文。
  • 销毁: exit() 系统调用触发 do_exit(),将进程状态置为 TASK_DEAD,并最终由 release_task() 释放 task_struct 结构体。
  • 访问当前进程: 内核中通过 current 宏来获取当前正在运行进程的 task_struct 指针。在 x86_64 架构下,它通常通过读取特殊的 per-cpu 变量来实现。

b. mm_struct – 内存描述符

mm_struct 描述了一个进程完整的虚拟地址空间。普通的进程有唯一的 mm_struct。同一进程的多个线程共享同一个 mm_struct。内核线程没有自己的 mm_struct (task->mmNULL),它们在运行时会临时借用上一个用户进程的 active_mm

源码位置: include/linux/mm_types.h

核心作用:
作为进程虚拟地址空间的总管,它组织了所有的 vm_area_struct,并持有顶层页表(PGD)的指针,是进行地址翻译的起点。

关键字段分析 (Linux 5.15):

// include/linux/mm_types.h
struct mm_struct {
    struct {
        struct vm_area_struct *mmap;            /* VMA链表头, 按地址升序排列 */
        struct rb_root mm_rb;                   /* VMA红黑树树根, 用于快速查找 */
        // ...
    } __randomize_layout;

    pgd_t *pgd;                                 /* 指向页全局目录 (顶级页表) */

    atomic_t mm_users;                          /* 使用此地址空间的用户(进程)数 */
    atomic_t mm_count;                          /* mm_struct 的主引用计数 */

    unsigned long mmap_base;                    /* mmap 区域的基地址 */
    unsigned long mmap_legacy_base;             /* 传统 mmap 区域的基地址 */

    unsigned long start_code, end_code;         /* 代码段的起始和结束地址 */
    unsigned long start_data, end_data;         /* 数据段的起始和结束地址 */
    unsigned long start_brk, brk;               /* 堆的起始和当前末尾地址 */
    unsigned long start_stack;                  /* 栈的起始地址 */
    // ...
};

字段解读:

  • mmapmm_rb: 这是管理 VMA 的核心。mmap 是一个单向链表,方便按地址顺序遍历所有 VMA。mm_rb 是一个红黑树,可以根据一个虚拟地址快速地(O(log n) 时间复杂度)查找到对应的 VMA。
  • pgd: 地址转换的入口。当发生缺页异常或进行任何地址翻译时,硬件/软件的 Page Walker 都从 pgd 开始。
  • mm_users vs mm_count: mm_users 记录了有多少个进程在使用这个地址空间(clone() 时不带 CLONE_VM 会增加 mm_users)。mm_countmm_struct 自身的引用计数。当 mm_users 降为0时,会递减 mm_count。当 mm_count 降为0时,mm_struct 才会被真正释放。

相关接口:

  • 分配与初始化: mm_alloc() 分配 mm_structmm_init() 进行初始化。
  • 复制: copy_mm()fork() 时被调用,用于为子进程创建新的地址空间。
  • 释放: mmput() 减少 mm_users 引用计数, mmdrop() 减少 mm_count 引用计数并可能触发最终的释放 (通过 free_mm())。
  • 关联: 在 copy_process 中,新的或共享的 mm_struct 会被关联到新创建的 task_struct->mm

c. vm_area_struct (VMA)

如果 mm_struct 是一个城市的地图,那么 vm_area_struct 就是地图上的一个特定区域,比如一个公园、一个住宅区或一个商业区。它描述了进程地址空间中一段拥有相同属性(如权限、是否映射文件等)的连续虚拟地址区域。

源码位置: include/linux/mm_types.h

核心作用:
定义一段虚拟内存区域的边界、权限以及处理方式。例如,代码段是一个只读的 VMA,堆是一个可读写的 VMA,内存映射的文件是另一个 VMA。

关键字段分析 (Linux 5.15):

// include/linux/mm_types.h
struct vm_area_struct {
    unsigned long vm_start;             /* 区域的起始虚拟地址 (包含) */
    unsigned long vm_end;               /* 区域的结束虚拟地址 (不包含) */

    struct mm_struct *vm_mm;            /* 指回所属的 mm_struct */

    struct vm_area_struct *vm_next, *vm_prev; /* VMA 链表中的前后指针 */
    struct rb_node vm_rb;               /* 在 mm_struct 红黑树中的节点 */

    pgprot_t vm_page_prot;              /* 区域中页面的访问权限 */
    unsigned long vm_flags;             /* 区域的标志, 如 VM_READ, VM_WRITE, VM_EXEC, VM_SHARED */

    /* 对于文件映射区域 */
    struct {
        struct file * vm_file;          /* 指向被映射的文件 */
        loff_t vm_pgoff;                /* 文件内的页偏移量 */
        // ...
    } __shared;

    /* VMA 的操作函数集 */
    const struct vm_operations_struct *vm_ops;

    /* 用于匿名 VMA (如堆、栈) 的私有数据 */
    void *private_data;
};

字段解读:

  • vm_start, vm_end: 定义了这段连续虚拟地址的范围。
  • vm_mm, vm_next, vm_prev, vm_rb: 这些字段将 VMA 组织进 mm_struct 的链表和红黑树中。
  • vm_flags: 非常重要,定义了区域的属性,例如 VM_READ (可读), VM_WRITE (可写), VM_EXEC (可执行), VM_SHARED (共享映射), VM_GROWSDOWN (栈区域,向下增长) 等。
  • vm_file, vm_pgoff: 如果 VMA 是通过 mmap 映射了一个文件,这两个字段会指向对应的 struct file 和文件内的偏移。
  • vm_ops: 这是一个函数指针表 (vm_operations_struct),定义了对这个 VMA 的特定操作,最重要的就是 fault()。当发生缺页异常时,内核会调用 vma->vm_ops->fault() 来处理这个缺页,决定是分配一个新的物理页(匿名映射),还是从文件中读取一页(文件映射)。

相关接口:

  • 查找: find_vma(mm, addr) 是最核心的接口,用于在 mm_struct 中根据一个虚拟地址快速查找其所在的 VMA。
  • 创建/修改: mmap() 系统调用最终会调用内核的 do_mmap()mmap_region(),这些函数负责创建和插入新的 vm_area_structmprotect() 用于修改一个已存在 VMA 的权限。
  • 销毁: munmap() 系统调用最终调用 do_munmap(),它会找到对应的 VMA,将其从链表和红黑树中移除并释放。
  • 缺页处理: 缺页异常处理程序 (handle_mm_fault()) 会首先调用 find_vma(),然后根据 VMA 的信息和 vm_ops 来处理缺页。

d. sk_buff – Socket Buffer

sk_buff 是 Linux 网络子系统的“通用货币”。从网卡驱动接收到一个数据包,到经过协议栈(IP 层、TCP/UDP 层),再到被用户程序读取,整个过程中数据都封装在 sk_buff 结构中。

源码位置: include/linux/skbuff.h

核心作用:
高效地管理网络数据包的缓冲区,并携带贯穿整个协议栈的元数据(metadata)。

关键字段分析 (Linux 5.15):

// include/linux/skbuff.h
struct sk_buff {
    // 链表指针
    struct sk_buff  *next;
    struct sk_buff  *prev;

    struct sock     *sk;            /* 指向关联的 socket */
    struct net_device *dev;         /* 报文关联的设备 (发送或接收) */

    // 核心数据指针 (非常重要)
    unsigned char   *head;          /* 已分配缓冲区的头部 */
    unsigned char   *data;          /* 协议数据的开始 */
    unsigned char   *tail;          /* 协议数据的结尾 */
    unsigned char   *end;           /* 已分配缓冲区的尾部 */

    // 长度
    unsigned int    len;            /* data 区域的长度 (tail - data) */
    unsigned int    data_len;       /* 分片(fragment)中的数据长度 */
    unsigned int    truesize;       /* skb 总大小 (包括 struct 和数据区) */

    // 协议头指针
    __u16           transport_header;
    __u16           network_header;
    __u16           mac_header;
    // ...
};

核心指针图解:

<-- headroom -->|<-- data -->|<-- tailroom -->
[ head |---------| data |-----| tail |---------| end ]
       ^         ^      ^     ^      ^          ^
       |         |      |     |      |          |
    缓冲区的开始   实际数据的开始  len  实际数据的结尾  |      缓冲区的结束
                                                 |
                                         (tail - data)
  • headroom (data - head): 头部空间。当数据包在协议栈中向下传递时(如从 TCP 层到 IP 层),需要在前面添加新的协议头(IP Header)。headroom 提供了预留空间,避免了数据拷贝。
  • tailroom (end - tail): 尾部空间。可以用于添加数据或者协议尾部。
  • len vs data_len: lensk_buff 主缓冲区中的数据长度。对于大的数据包,可能会使用 “Scatter-gather I/O”,即将数据存放在多个分散的内存页(fragments)中,data_len 就是这些 fragments 中的数据总长度。总长度为 skb->len + skb->data_len

相关接口:

  • 分配与释放:
    • alloc_skb(size, gfp_mask): 分配一个 sk_buff 和指定大小的数据区。
    • dev_alloc_skb(size): 在驱动程序中常用的分配函数,会预留一些 headroom
    • kfree_skb(skb) / dev_kfree_skb(skb): 释放 sk_buff
  • 数据区操作:
    • skb_put(skb, len): 在tail指针后增加len字节数据,tail指针后移,len增加。
    • skb_push(skb, len): 在data指针前增加len字节数据(在 headroom 中),data指针前移,len增加。
    • skb_pull(skb, len): 从data指针处移除len字节数据,data指针后移,len减少。
    • skb_reserve(skb, len): 在headdata之间保留len字节的 headroom。
  • 拷贝与克隆: skb_clone() 创建一个元数据相同但共享数据区的 sk_buffpskb_copy() 创建一个元数据和数据都独立的 sk_buff 副本。

e. net_device – 网络设备

net_device 是内核中对一个物理或虚拟网络接口的抽象。任何能收发包的实体(如 eth0, lo, wlan0)都在内核中表现为一个 net_device 结构。

源码位置: include/linux/netdevice.h

核心作用:
作为网络协议栈(L3+)和设备驱动(L2/L1)之间的接口层,它包含了设备的状态、属性,以及最重要的——一系列操作函数,供协议栈调用以控制设备。

关键字段分析 (Linux 5.15):

// include/linux/netdevice.h
struct net_device {
    char            name[IFNAMSIZ];         /* 设备名, 如 "eth0" */
    int             ifindex;                /* 接口的唯一索引号 */

    /* 核心: 设备操作函数集 */
    const struct net_device_ops *netdev_ops;

    unsigned int    mtu;                    /* 最大传输单元 */
    unsigned int    flags;                  /* 接口标志, 如 IFF_UP, IFF_RUNNING, IFF_PROMISC */

    unsigned char   dev_addr[MAX_ADDR_LEN]; /* MAC 地址 */
    unsigned char   broadcast[MAX_ADDR_LEN];/* 广播地址 */

    struct net_device_stats stats;          /* 流量统计 */

    // NAPI (New API) 相关,用于高效收包
    struct napi_struct  napi;

    // ... 队列规程 (TC)
    struct Qdisc __rcu *qdisc;

    // ... 私有数据,供驱动程序使用
    void            *ml_priv;
};

字段解读:

  • netdev_ops: 这是 net_device 的灵魂所在。它是一个指向 net_device_ops 结构的指针,该结构包含了一系列函数指针。设备驱动程序的核心任务之一就是实现这些函数,并将其赋值给 dev->netdev_ops
    • ndo_open(): 启动设备时调用(ifconfig eth0 up)。
    • ndo_stop(): 关闭设备时调用。
    • ndo_start_xmit(): 最重要的发送函数。协议栈通过调用此函数将一个 sk_buff 交给驱动程序去发送。
    • ndo_get_stats(): 获取设备统计信息。
    • …还有很多其他操作。
  • flags: 反映设备当前状态的位掩码,如 IFF_UP (设备已启用)、IFF_RUNNING (链路已连接)、IFF_MULTICAST (支持多播)、IFF_PROMISC (混杂模式)。
  • mtu: 决定了该接口一次能够发送的最大数据包大小(不含链路层头部)。
  • stats: 记录收发包数量、错误数等统计信息。

相关接口:

  • 创建与注册:
    • alloc_etherdev(sizeof_priv): 驱动程序通常调用此函数来分配一个以太网类型的 net_device 结构,sizeof_priv 是为驱动私有数据预留的空间。
    • register_netdev(dev): 将分配并初始化好的 net_device 注册到内核,使其对协议栈和用户空间可见。
    • unregister_netdev(dev): 注销设备。
  • 发送数据: 协议栈调用 dev_queue_xmit(skb) 来发送一个数据包。这个函数会处理一些通用逻辑(如队列规程),然后最终调用 dev->netdev_ops->ndo_start_xmit(skb, dev)
  • 接收数据: 驱动程序在中断处理或 NAPI poll 函数中,将接收到的数据封装成 sk_buff,然后调用 netif_rx(skb)napi_gro_receive() 将其递交给上层协议栈进行处理。

总结

  • 高度抽象: task_struct, mm_struct, net_device 等都是对复杂实体的精妙抽象。
  • 数据结构驱动: 内核的行为很大程度上是由这些核心数据结构及其关系定义的。
  • 钩子/回调机制: 通过函数指针(如 vm_ops, netdev_ops),实现了通用框架与具体实现(如文件系统、驱动)的解耦。

它们之间的关系也非常紧密:一个task_struct代表的进程拥有一个mm_struct来管理其地址空间,该空间由多个vm_area_struct组成。当这个进程通过网络通信时,数据被封装在sk_buff中,并通过代表网卡的net_device进行收发。

一些 其他核心结构的简介:

[task_struct] /include/linux/sched.h – 进程控制块
任务管理的核心结构。维护调度数据、信号状态,以及指向相关资源(如内存、凭证、文件和命名空间)的指针。

[mm_struct] /include/linux/mm_types.h – 内存描述符
描述任务的地址空间。包含内存布局、页表,以及跨线程共享的映射区域的引用。

[cred] /include/linux/cred.h – 进程凭证
保存用户和组ID、能力集合以及安全上下文。
用于整个内核的权限检查和身份验证。

[files_struct] /include/linux/fdtable.h – 文件描述符表
映射数字文件描述符到文件结构。支持跨线程共享,包含并发访问的锁和引用跟踪。

[file] /include/linux/fs.h – 文件实例(每次打开)
表示一个打开的文件句柄。存储访问标志、文件偏移量,以及指向inode及其相关文件操作的链接。

[inode] /include/linux/fs.h – 文件系统元数据
表示磁盘上的文件或目录。存储元数据(如权限、时间戳、所有权),以及指向数据块或设备接口的指针。

[super_block] /include/linux/fs.h – 已挂载文件系统状态
封装关于已挂载文件系统的全局信息。包含挂载标志、文件系统类型、块大小和根inode。

[dentry] /include/linux/dcache.h – 目录项缓存
通过缓存名称到inode的映射提供快速路径解析。
支持分层遍历并管理路径查找效率。

[signal_struct] /include/linux/sched/signal.h – 信号状态(每线程组)
保存信号掩码、队列和处理程序,用于一组线程。
协调信号的传递、阻塞和处理。

[nsproxy] /include/linux/nsproxy.h – 命名空间引用持有者
将任务链接到各种命名空间:mount、UTS、IPC、PID、NET、CGROUP。
定义隔离边界并控制资源可见性。

[vm_area_struct] /include/linux/mm_types.h – 虚拟内存区域
定义进程中内存映射的段。跟踪权限、映射类型,以及与文件或匿名页面的关系。

[msg_queue] /include/linux/msg.h – System V IPC消息队列
实现基于消息的IPC。管理消息列表、队列限制,以及发送和接收进程的同步。

[sk_buff] /include/linux/skbuff.h – 网络数据包缓冲区
封装用于传输和接收的数据包数据。包含协议头、路由元数据和套接字关联。

[net_device] /include/linux/netdevice.h – 网络接口描述符
描述网络设备。管理接口操作、统计信息、链路状态和数据传输队列。

[sock] /include/linux/sock.h – 协议层套接字
包含套接字的协议状态(TCP、UDP等)。管理队列、计时器、连接跟踪,以及指向相关文件描述符的链接。

[cgroup_subsys_state] /include/linux/cgroup-defs.h – Cgroup控制器状态
跟踪资源管理任务的每控制器状态。用于强制限制、收集指标和传播资源控制事件。

[cpu] /include/linux/cpu.h – 每CPU运行时上下文
保存每个处理器核心的本地数据。跟踪调度统计、任务计数、中断处理和CPU本地结构。

本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。

发表评论