概览
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
包含了内核管理一个进程所需的所有信息,可以分为以下几大类:
- 状态与标识: 进程当前的状态(运行、睡眠、僵尸等)和唯一标识(PID)。
- 调度信息: 调度器用来决定下一个运行进程的全部信息(优先级、调度策略、CPU 时间消耗等)。
- 内存管理: 指向该进程的地址空间描述符(
mm_struct
)。 - 文件系统信息: 进程当前的工作目录、根目录等。
- 文件描述符: 进程打开的所有文件。
- 信号处理: 待处理信号、信号处理函数等。
- 进程关系: 父进程、子进程、线程组等关系。
关键字段分析 (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->mm
为 NULL
),它们在运行时会临时借用上一个用户进程的 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; /* 栈的起始地址 */
// ...
};
字段解读:
mmap
和mm_rb
: 这是管理 VMA 的核心。mmap
是一个单向链表,方便按地址顺序遍历所有 VMA。mm_rb
是一个红黑树,可以根据一个虚拟地址快速地(O(log n) 时间复杂度)查找到对应的 VMA。pgd
: 地址转换的入口。当发生缺页异常或进行任何地址翻译时,硬件/软件的 Page Walker 都从pgd
开始。mm_users
vsmm_count
:mm_users
记录了有多少个进程在使用这个地址空间(clone()
时不带CLONE_VM
会增加mm_users
)。mm_count
是mm_struct
自身的引用计数。当mm_users
降为0时,会递减mm_count
。当mm_count
降为0时,mm_struct
才会被真正释放。
相关接口:
- 分配与初始化:
mm_alloc()
分配mm_struct
,mm_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_struct
。mprotect()
用于修改一个已存在 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
vsdata_len
:len
是sk_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)
: 在head
和data
之间保留len
字节的 headroom。
- 拷贝与克隆:
skb_clone()
创建一个元数据相同但共享数据区的sk_buff
。pskb_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本地结构。