Linux中内核和用户空间通信send_uevent函数的实现

Netlink 是 Linux 内核中用于内核空间与用户空间通信的机制,特别适合事件通知 和配置管理

核心组件总结

1. 初始化阶段 (kobject_uevent_init)

功能: 建立内核与用户空间的通信基础

  • 创建专用的 Netlink socket (NETLINK_KOBJECT_UEVENT)
  • 初始化全局 uevent_sock 变量
  • 为后续 uevent 通知建立通道

功能 : 创建和配置内核端 Netlink socket

关键步骤:

  • 协议验证和范围检查
  • 分配 socket 核心数据结构
  • 设置 Netlink 特定选项
  • 初始化锁和等待队列
  • 注册到全局 Netlink 表

功能: 管理 socket 的寻址和路由

核心机制:

  • 基于 PID 的哈希表管理
  • 冲突检测和避免
  • 并发安全保护
  • 哈希表性能优化(稀释机制)

功能: 实现高效可靠的消息广播

初始化 kobject uevent 子系统kobject_uevent_init

c 复制代码
static int __init kobject_uevent_init(void)
{
	uevent_sock = netlink_kernel_create(NETLINK_KOBJECT_UEVENT, NULL);

	if (!uevent_sock) {
		printk(KERN_ERR
		       "kobject_uevent: unable to create netlink socket!\n");
		return -ENODEV;
	}

	return 0;
}
struct sock *
netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len))
{
	struct socket *sock;
	struct sock *sk;

	if (!nl_table)
		return NULL;

	if (unit<0 || unit>=MAX_LINKS)
		return NULL;

	if (sock_create_lite(PF_NETLINK, SOCK_DGRAM, unit, &sock))
		return NULL;

	if (netlink_create(sock, unit) < 0) {
		sock_release(sock);
		return NULL;
	}
	sk = sock->sk;
	sk->sk_data_ready = netlink_data_ready;
	if (input)
		nlk_sk(sk)->data_ready = input;

	if (netlink_insert(sk, 0)) {
		sock_release(sock);
		return NULL;
	}
	return sk;
}

代码逐行解释

kobject_uevent_init 函数

函数声明

c 复制代码
static int __init kobject_uevent_init(void)
  • __init: 表示这是内核初始化函数,初始化完成后会释放其内存
  • 无参数,返回整型状态码

创建 Netlink Socket

c 复制代码
uevent_sock = netlink_kernel_create(NETLINK_KOBJECT_UEVENT, NULL);
  • uevent_sock: 全局变量,存储创建的 netlink socket
  • NETLINK_KOBJECT_UEVENT: Netlink 协议类型,专门用于 kobject uevent
  • NULL: 输入回调函数,这里设为 NULL 表示这是纯发送端socket

错误检查

c 复制代码
if (!uevent_sock) {
    printk(KERN_ERR
           "kobject_uevent: unable to create netlink socket!\n");
    return -ENODEV;
}
  • 检查 socket 创建是否成功
  • 如果失败,打印错误信息到内核日志
  • 返回 -ENODEV 表示没有这样的设备

成功返回

c 复制代码
return 0;
  • 成功创建 socket,返回 0 表示初始化成功

函数声明和参数

c 复制代码
struct sock *netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len))
  • unit: Netlink 协议类型(如 NETLINK_KOBJECT_UEVENT)
  • input: 数据到达时的回调函数指针
  • 返回创建的 struct sock 指针

变量声明

c 复制代码
struct socket *sock;
struct sock *sk;
  • sock: 套接字结构,包含协议相关的操作
  • sk: 套接字核心结构,包含网络层状态

初始检查

c 复制代码
if (!nl_table)
    return NULL;

if (unit<0 || unit>=MAX_LINKS)
    return NULL;
  • 检查全局 netlink 表是否已初始化
  • 验证协议类型是否在有效范围内

创建轻量级 Socket

c 复制代码
if (sock_create_lite(PF_NETLINK, SOCK_DGRAM, unit, &sock))
    return NULL;
  • PF_NETLINK: 协议族,表示使用 Netlink
  • SOCK_DGRAM: 套接字类型,数据报套接字
  • unit: 协议类型
  • &sock: 输出参数,返回创建的 socket
  • 如果创建失败返回 NULL

创建 Netlink 特定结构

c 复制代码
if (netlink_create(sock, unit) < 0) {
    sock_release(sock);
    return NULL;
}
  • netlink_create(): 初始化 netlink 特定的数据结构
  • 如果失败,释放之前创建的 socket 并返回 NULL

设置回调函数

c 复制代码
sk = sock->sk;
sk->sk_data_ready = netlink_data_ready;
if (input)
    nlk_sk(sk)->data_ready = input;
  • sk = sock->sk: 获取 socket 的核心结构
  • sk->sk_data_ready = netlink_data_ready: 设置通用的数据就绪回调
  • 如果提供了自定义 input 回调,设置到 netlink 特定结构中

插入 Netlink 表

c 复制代码
if (netlink_insert(sk, 0)) {
    sock_release(sock);
    return NULL;
}
  • netlink_insert(): 将 socket 插入全局 netlink 表
  • 第二个参数 0 表示协议号
  • 如果插入失败,释放 socket 并返回 NULL

成功返回

c 复制代码
return sk;
  • 返回创建的 socket 核心结构指针

函数功能总结

kobject_uevent_init

主要功能 :初始化 kobject uevent 子系统,创建用于发送 uevent 的 netlink socket

关键作用:

  • 为内核对象事件通知建立通信通道
  • 初始化全局变量 uevent_sock,供后续 send_uevent 函数使用

主要功能 :创建内核态的 netlink socket,用于内核与用户空间的通信

关键特性:

  • 创建的是内核端 socket,不同于用户空间的 socket
  • 支持设置数据到达回调函数(input 参数)
  • 管理 netlink 协议族的内部数据结构
  • 维护全局的 netlink socket 表

通信机制:

  • 使用 NETLINK_KOBJECT_UEVENT 协议类型(值为 15)
  • 基于数据报(SOCK_DGRAM)的通信方式
  • 支持广播机制,允许一个内核事件被多个用户空间进程接收
c 复制代码
static int netlink_create(struct socket *sock, int protocol)
{
	struct sock *sk;
	struct netlink_opt *nlk;

	sock->state = SS_UNCONNECTED;

	if (sock->type != SOCK_RAW && sock->type != SOCK_DGRAM)
		return -ESOCKTNOSUPPORT;

	if (protocol<0 || protocol >= MAX_LINKS)
		return -EPROTONOSUPPORT;

	sock->ops = &netlink_ops;

	sk = sk_alloc(PF_NETLINK, GFP_KERNEL, 1, NULL);
	if (!sk)
		return -ENOMEM;

	sock_init_data(sock,sk);
	sk_set_owner(sk, THIS_MODULE);

	nlk = sk->sk_protinfo = kmalloc(sizeof(*nlk), GFP_KERNEL);
	if (!nlk) {
		sk_free(sk);
		return -ENOMEM;
	}
	memset(nlk, 0, sizeof(*nlk));

	spin_lock_init(&nlk->cb_lock);
	init_waitqueue_head(&nlk->wait);
	sk->sk_destruct = netlink_sock_destruct;

	sk->sk_protocol = protocol;
	return 0;
}

代码逐行解释

函数声明和参数

c 复制代码
static int netlink_create(struct socket *sock, int protocol)
  • sock: 已经创建的 socket 结构指针
  • protocol: Netlink 协议类型
  • 返回整型状态码(0表示成功,负值表示错误)

变量声明

c 复制代码
struct sock *sk;
struct netlink_opt *nlk;
  • sk: socket 的核心网络结构
  • nlk: Netlink 特定的选项和状态结构

Socket 状态初始化

c 复制代码
sock->state = SS_UNCONNECTED;

将 socket 状态设置为未连接状态,因为 Netlink socket 通常是无连接的

Socket 类型检查

c 复制代码
if (sock->type != SOCK_RAW && sock->type != SOCK_DGRAM)
    return -ESOCKTNOSUPPORT;

检查 socket 类型是否支持:

  • SOCK_RAW: 原始套接字
  • SOCK_DGRAM: 数据报套接字
  • 如果不支持这两种类型,返回"socket 类型不支持"错误

协议范围检查

c 复制代码
if (protocol<0 || protocol >= MAX_LINKS)
    return -EPROTONOSUPPORT;

验证协议号是否在有效范围内:

  • protocol 必须大于等于 0 且小于 MAX_LINKS
  • 如果超出范围,返回"协议不支持"错误

设置 Socket 操作

c 复制代码
sock->ops = &netlink_ops;

设置 socket 的操作函数表为 netlink_ops,这定义了 Netlink socket 的各种操作(绑定、发送、接收等)

分配 Socket 核心结构

c 复制代码
sk = sk_alloc(PF_NETLINK, GFP_KERNEL, 1, NULL);
if (!sk)
    return -ENOMEM;
  • sk_alloc(): 分配 struct sock 内存
  • PF_NETLINK: 协议族标识
  • GFP_KERNEL: 内存分配标志(可睡眠)
  • 1: 把struct sock结构体清零
  • 如果分配失败,返回内存不足错误

初始化 Socket 数据

c 复制代码
sock_init_data(sock, sk);

初始化 socket 和 sock 结构之间的关联关系,设置各种初始状态和回调函数

设置模块所有者

c 复制代码
sk_set_owner(sk, THIS_MODULE);

设置 sock 结构的所有者为当前内核模块,用于模块引用计数管理。

分配 Netlink 特定结构

c 复制代码
nlk = sk->sk_protinfo = kmalloc(sizeof(*nlk), GFP_KERNEL);
if (!nlk) {
    sk_free(sk);
    return -ENOMEM;
}
  • 为 Netlink 特定选项分配内存
  • sk->sk_protinfo: 指向协议特定数据的指针
  • 如果分配失败,释放之前分配的 sock 结构并返回内存不足错误

初始化 Netlink 结构

c 复制代码
memset(nlk, 0, sizeof(*nlk));

将 Netlink 结构清零,确保所有字段初始化为 0。

初始化自旋锁

c 复制代码
spin_lock_init(&nlk->cb_lock);

初始化回调锁,用于保护 Netlink 回调相关的数据结构的并发访问

初始化等待队列

c 复制代码
init_waitqueue_head(&nlk->wait);

初始化等待队列头,用于进程在 Netlink socket 上等待数据时的睡眠/唤醒机制

设置 Socket 析构函数

c 复制代码
sk->sk_destruct = netlink_sock_destruct;

设置 socket 的析构函数,当 socket 被释放时会调用此函数进行清理工作。

设置协议类型

c 复制代码
sk->sk_protocol = protocol;

将协议类型保存到 sock 结构中

成功返回

c 复制代码
return 0;

返回 0 表示 Netlink socket 创建成功

函数功能总结

主要功能 :初始化 Netlink socket 的协议特定数据结构和状态

关键作用:

  1. 参数验证:

    • 验证 socket 类型兼容性
    • 检查协议号有效性
  2. 内存管理:

    • 分配 sock 核心结构
    • 分配 Netlink 特定选项结构
    • 设置适当的析构函数
  3. 并发控制:

    • 初始化自旋锁保护回调数据
    • 初始化等待队列管理阻塞操作
  4. 协议设置:

    • 设置 Netlink 特定的操作函数表
    • 保存协议类型标识
  5. 模块管理:

    • 设置模块所有者关系
    • 确保正确的引用计数管理
c 复制代码
static int netlink_insert(struct sock *sk, u32 pid)
{
	struct nl_pid_hash *hash = &nl_table[sk->sk_protocol].hash;
	struct hlist_head *head;
	int err = -EADDRINUSE;
	struct sock *osk;
	struct hlist_node *node;
	int len;

	netlink_table_grab();
	head = nl_pid_hashfn(hash, pid);
	len = 0;
	sk_for_each(osk, node, head) {
		if (nlk_sk(osk)->pid == pid)
			break;
		len++;
	}
	if (node)
		goto err;

	err = -EBUSY;
	if (nlk_sk(sk)->pid)
		goto err;

	err = -ENOMEM;
	if (BITS_PER_LONG > 32 && unlikely(hash->entries >= UINT_MAX))
		goto err;

	if (len && nl_pid_hash_dilute(hash, len))
		head = nl_pid_hashfn(hash, pid);
	hash->entries++;
	nlk_sk(sk)->pid = pid;
	sk_add_node(sk, head);
	err = 0;

err:
	netlink_table_ungrab();
	return err;
}

代码逐行解释

函数声明和参数

c 复制代码
static int netlink_insert(struct sock *sk, u32 pid)
  • sk: 要插入的 socket 结构指针
  • pid: 进程ID(在Netlink中用作地址标识)
  • 返回整型状态码(0表示成功,负值表示错误)

变量声明

c 复制代码
struct nl_pid_hash *hash = &nl_table[sk->sk_protocol].hash;
struct hlist_head *head;
int err = -EADDRINUSE;
struct sock *osk;
struct hlist_node *node;
int len;
  • hash: 获取对应协议号的Netlink哈希表
  • head: 哈希桶头指针
  • err: 错误码,初始化为地址已使用
  • osk: 用于遍历的临时socket指针
  • node: 哈希链表节点
  • len: 哈希桶链表的长度

获取表锁

c 复制代码
netlink_table_grab();

获取Netlink表的自旋锁,保护对全局Netlink数据结构的并发访问

计算哈希桶位置

c 复制代码
head = nl_pid_hashfn(hash, pid);

根据pid计算在哈希表中的桶位置,nl_pid_hashfn是哈希函数

初始化长度计数

c 复制代码
len = 0;

初始化链表长度计数器为0

遍历哈希桶查找冲突

c 复制代码
sk_for_each(osk, node, head) {
    if (nlk_sk(osk)->pid == pid)
        break;
    len++;
}

遍历哈希桶中的socket链表:

  • sk_for_each: 宏定义,遍历链表中的每个socket
  • 检查每个socket的pid是否与要插入的pid冲突
  • 如果找到相同pid,跳出循环
  • 同时统计链表长度

检查pid冲突

c 复制代码
if (node)
    goto err;

如果node不为NULL,说明在链表中找到了相同pid的socket,存在冲突,跳转到错误处理

检查socket是否已分配pid

c 复制代码
err = -EBUSY;
if (nlk_sk(sk)->pid)
    goto err;
  • 将错误码改为"设备忙"
  • 检查要插入的socket是否已经分配了pid
  • 如果已经分配了pid,跳转到错误处理

检查哈希表条目数限制

c 复制代码
err = -ENOMEM;
if (BITS_PER_LONG > 32 && unlikely(hash->entries >= UINT_MAX))
    goto err;
  • 将错误码改为"内存不足"
  • 在64位系统上检查哈希表条目数是否超过32位无符号整型最大值
  • 如果超过限制,跳转到错误处理

哈希表稀释检查

c 复制代码
if (len && nl_pid_hash_dilute(hash, len))
    head = nl_pid_hashfn(hash, pid);
  • 如果当前桶的链表不为空且需要稀释(链表过长)
  • 调用nl_pid_hash_dilute检查是否需要重新哈希
  • 如果需要,重新计算哈希桶位置

更新哈希表统计

c 复制代码
hash->entries++;

增加哈希表的条目计数器

设置socket的pid

c 复制代码
nlk_sk(sk)->pid = pid;

将pid分配给要插入的socket的Netlink特定数据

将socket插入链表

c 复制代码
sk_add_node(sk, head);

将socket添加到哈希桶链表的头部

设置成功状态

c 复制代码
err = 0;

将错误码设置为0,表示插入成功

错误处理标签

c 复制代码
err:
    netlink_table_ungrab();
    return err;

释放Netlink表的自旋锁,并返回相应的错误码

函数功能总结

主要功能 :将Netlink socket插入到全局哈希表中,管理socket的注册和寻址

  1. 并发安全:

    • 使用自旋锁保护全局Netlink表
    • 确保多CPU环境下的安全操作
  2. 冲突检测:

    • 检查pid是否已被其他socket使用
    • 防止地址冲突
  3. 资源管理:

    • 检查哈希表容量限制
    • 管理哈希表条目计数
  4. 性能优化:

    • 实现哈希稀释机制防止链表过长
    • 动态重新哈希保持性能

向用户空间发送 uevent通知send_uevent

c 复制代码
/**
 * send_uevent - notify userspace by sending event trough netlink socket
 *
 * @signal: signal name
 * @obj: object path (kobject)
 * @envp: possible hotplug environment to pass with the message
 * @gfp_mask:
 */
static int send_uevent(const char *signal, const char *obj,
		       char **envp, int gfp_mask)
{
	struct sk_buff *skb;
	char *pos;
	int len;

	if (!uevent_sock)
		return -EIO;

	len = strlen(signal) + 1;
	len += strlen(obj) + 1;

	/* allocate buffer with the maximum possible message size */
	skb = alloc_skb(len + BUFFER_SIZE, gfp_mask);
	if (!skb)
		return -ENOMEM;

	pos = skb_put(skb, len);
	sprintf(pos, "%s@%s", signal, obj);

	/* copy the environment key by key to our continuous buffer */
	if (envp) {
		int i;

		for (i = 2; envp[i]; i++) {
			len = strlen(envp[i]) + 1;
			pos = skb_put(skb, len);
			strcpy(pos, envp[i]);
		}
	}

	return netlink_broadcast(uevent_sock, skb, 0, 1, gfp_mask);
}

函数功能概述

这个函数用于通过 netlink socket 向用户空间发送 uevent(用户事件)通知,通常用于内核对象的热插拔事件通知

代码详细解释

函数声明和参数

c 复制代码
static int send_uevent(const char *signal, const char *obj,
		       char **envp, int gfp_mask)
  • signal: 事件信号名称(如 "add", "remove", "change")
  • obj: 内核对象路径
  • envp: 环境变量数组,包含要传递的附加信息
  • gfp_mask: 内存分配标志

变量声明

c 复制代码
struct sk_buff *skb;
char *pos;
int len;
  • skb: socket buffer 指针,用于存储要发送的网络数据
  • pos: 指向 skb 数据区当前写入位置的指针
  • len: 用于计算各种字符串长度

初始检查

c 复制代码
if (!uevent_sock)
    return -EIO;

检查全局的 uevent netlink socket 是否已初始化。如果没有,返回 IO 错误

计算基本长度

c 复制代码
len = strlen(signal) + 1;
len += strlen(obj) + 1;

计算信号名称和对象路径的总长度,每个字符串都包含终止符 \0。

分配 SKB 缓冲区

c 复制代码
skb = alloc_skb(len + BUFFER_SIZE, gfp_mask);
if (!skb)
    return -ENOMEM;

分配 socket buffer:

  • len + BUFFER_SIZE: 为环境变量预留额外空间
  • gfp_mask: 控制内存分配行为(如是否可睡眠)
  • 如果分配失败,返回内存不足错误

构建基本消息

c 复制代码
pos = skb_put(skb, len);
sprintf(pos, "%s@%s", signal, obj);
  • skb_put(): 在 skb 尾部扩展指定长度的空间,返回写入位置
  • sprintf(): 将信号和对象路径格式化为 signal@obj 的形式

添加环境变量

c 复制代码
if (envp) {
    int i;

    for (i = 2; envp[i]; i++) {
        len = strlen(envp[i]) + 1;
        pos = skb_put(skb, len);
        strcpy(pos, envp[i]);
    }
}
  • 从 envp[2] 开始遍历环境变量数组(跳过前两个元素)
  • 对每个环境变量字符串:
    • 计算长度(包含终止符)
    • 扩展 skb 空间
    • 复制字符串到 skb

广播消息

c 复制代码
return netlink_broadcast(uevent_sock, skb, 0, 1, gfp_mask);

通过 netlink socket 广播消息:

  • uevent_sock: 发送用的 netlink socket
  • skb: 包含消息数据的 socket buffer
  • 0: 目标组(0 表示所有组)
  • 1: 分配失败时是否克隆 skb
  • gfp_mask: 内存分配标志

函数功能总结

主要功能 :通过 netlink 机制向用户空间发送内核对象事件通知

典型应用场景:

  • 设备热插拔(device add/remove)
  • 模块加载/卸载
  • 文件系统挂载/卸载
  • 电源管理事件

消息格式:

复制代码
signal@obj
env_var1=value1
env_var2=value2
...
c 复制代码
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
		      u32 group, int allocation)
{
	struct netlink_broadcast_data info;
	struct hlist_node *node;
	struct sock *sk;

	info.exclude_sk = ssk;
	info.pid = pid;
	info.group = group;
	info.failure = 0;
	info.congested = 0;
	info.delivered = 0;
	info.allocation = allocation;
	info.skb = skb;
	info.skb2 = NULL;

	netlink_trim(skb, allocation);

	/* While we sleep in clone, do not allow to change socket list */

	netlink_lock_table();

	sk_for_each_bound(sk, node, &nl_table[ssk->sk_protocol].mc_list)
		do_one_broadcast(sk, &info);

	netlink_unlock_table();

	if (info.skb2)
		kfree_skb(info.skb2);
	kfree_skb(skb);

	if (info.delivered) {
		if (info.congested && (allocation & __GFP_WAIT))
			yield();
		return 0;
	}
	if (info.failure)
		return -ENOBUFS;
	return -ESRCH;
}

代码逐行解释

函数声明和参数

c 复制代码
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
		      u32 group, int allocation)
  • ssk: 发送源socket(通常为内核socket)
  • skb: 要广播的socket缓冲区
  • pid: 目标进程ID(0表示广播给所有)
  • group: 目标组ID
  • allocation: 内存分配标志
  • 返回整型状态码

变量声明和初始化广播数据结构

c 复制代码
struct netlink_broadcast_data info;
struct hlist_node *node;
struct sock *sk;

info.exclude_sk = ssk;
info.pid = pid;
info.group = group;
info.failure = 0;
info.congested = 0;
info.delivered = 0;
info.allocation = allocation;
info.skb = skb;
info.skb2 = NULL;
  • info: 广播过程中使用的状态数据结构
  • node, sk: 用于遍历socket列表
  • 初始化info结构的所有字段:
    • exclude_sk: 排除的socket(不向自己发送)
    • pid, group: 目标过滤条件
    • failure, congested, delivered: 状态计数器
    • allocation: 内存分配标志
    • skb: 原始数据包
    • skb2: 克隆的数据包

调整SKB大小

c 复制代码
netlink_trim(skb, allocation);

根据内存分配标志调整skb的大小,可能释放不必要的尾部空间

锁定Netlink表

c 复制代码
netlink_lock_table();

获取Netlink表的读写锁,防止在广播过程中socket列表发生变化

遍历多播socket列表并广播

c 复制代码
sk_for_each_bound(sk, node, &nl_table[ssk->sk_protocol].mc_list)
    do_one_broadcast(sk, &info);
  • sk_for_each_bound: 宏定义,遍历绑定到指定协议的多播socket列表
  • nl_table[ssk->sk_protocol].mc_list: 获取对应协议的多播socket链表
  • 对每个socket调用do_one_broadcast函数发送数据

释放表锁

c 复制代码
netlink_unlock_table();

释放Netlink表的读写锁,允许其他操作修改socket列表

清理克隆的SKB

c 复制代码
if (info.skb2)
    kfree_skb(info.skb2);

如果创建了克隆的skb(用于重试等情况),释放它

释放原始SKB

c 复制代码
kfree_skb(skb);

释放原始的socket缓冲区

处理成功投递情况

c 复制代码
if (info.delivered) {
    if (info.congested && (allocation & __GFP_WAIT))
        yield();
    return 0;
}
  • 如果有数据成功投递:
    • 检查是否有拥塞且允许等待,如果有则让出CPU
    • 返回0表示成功

处理失败情况

c 复制代码
if (info.failure)
    return -ENOBUFS;
return -ESRCH;
  • 如果有发送失败,返回"无缓冲区空间"错误
  • 如果没有找到任何接收者,返回"无此进程"错误

函数功能总结

主要功能 :向多个Netlink socket广播消息,实现一对多的通信模式

关键特性:

  1. 多播机制:

    • 遍历协议的多播socket列表
    • 向所有符合条件的socket发送消息
  2. 过滤条件:

    • pid: 可以指定特定进程
    • group: 可以指定组播组
    • exclude_sk: 排除发送者自身
  3. 状态跟踪:

    • 记录成功投递数量
    • 检测拥塞情况
    • 统计失败次数
c 复制代码
static inline int do_one_broadcast(struct sock *sk,
				   struct netlink_broadcast_data *p)
{
	struct netlink_opt *nlk = nlk_sk(sk);
	int val;

	if (p->exclude_sk == sk)
		goto out;

	if (nlk->pid == p->pid || !(nlk->groups & p->group))
		goto out;

	if (p->failure) {
		netlink_overrun(sk);
		goto out;
	}

	sock_hold(sk);
	if (p->skb2 == NULL) {
		if (atomic_read(&p->skb->users) != 1) {
			p->skb2 = skb_clone(p->skb, p->allocation);
		} else {
			p->skb2 = p->skb;
			atomic_inc(&p->skb->users);
		}
	}
	if (p->skb2 == NULL) {
		netlink_overrun(sk);
		/* Clone failed. Notify ALL listeners. */
		p->failure = 1;
	} else if ((val = netlink_broadcast_deliver(sk, p->skb2)) < 0) {
		netlink_overrun(sk);
	} else {
		p->congested |= val;
		p->delivered = 1;
		p->skb2 = NULL;
	}
	sock_put(sk);

out:
	return 0;
}

代码逐行解释

函数声明

c 复制代码
static inline int do_one_broadcast(struct sock *sk,
				   struct netlink_broadcast_data *p)
  • static inline: 内联函数,减少函数调用开销
  • sk: 目标socket
  • p: 广播数据状态结构指针
  • 返回整型(总是返回0)

获取Netlink选项

c 复制代码
struct netlink_opt *nlk = nlk_sk(sk);

从socket中获取Netlink特定的选项结构,包含pid、groups等信息

排除自身检查

c 复制代码
if (p->exclude_sk == sk)
    goto out;

如果当前socket是排除的socket(即发送者自身),跳过处理

目标过滤检查

c 复制代码
if (nlk->pid == p->pid || !(nlk->groups & p->group))
    goto out;

检查是否匹配目标条件:

  • nlk->pid == p->pid: 如果指定了特定pid且匹配
  • !(nlk->groups & p->group): 如果socket不在目标组中
    满足任一条件则跳过处理

失败状态检查

c 复制代码
if (p->failure) {
    netlink_overrun(sk);
    goto out;
}

如果之前已经发生过失败(如内存不足),标记当前socket为溢出状态并跳过

增加socket引用计数

c 复制代码
sock_hold(sk);

增加socket的引用计数,防止在发送过程中socket被释放

准备发送的SKB

c 复制代码
if (p->skb2 == NULL) {
    if (atomic_read(&p->skb->users) != 1) {
        p->skb2 = skb_clone(p->skb, p->allocation);
    } else {
        p->skb2 = p->skb;
        atomic_inc(&p->skb->users);
    }
}

处理SKB的共享:

  • 如果还没有克隆的SKB:
    • 如果原始SKB的引用计数不为1(被多个使用者共享),创建克隆
    • 否则直接使用原始SKB,但增加其引用计数

克隆失败处理

c 复制代码
if (p->skb2 == NULL) {
    netlink_overrun(sk);
    /* Clone failed. Notify ALL listeners. */
    p->failure = 1;
}

如果SKB克隆失败:

  • 标记当前socket溢出
  • 设置全局失败标志,后续所有socket都会跳过

尝试投递消息

c 复制代码
else if ((val = netlink_broadcast_deliver(sk, p->skb2)) < 0) {
    netlink_overrun(sk);
}

尝试投递消息到socket:

  • 如果返回负值(投递失败),标记socket溢出

投递成功处理

c 复制代码
else {
    p->congested |= val;
    p->delivered = 1;
    p->skb2 = NULL;
}

投递成功:

  • p->congested |= val: 记录拥塞状态(val可能表示是否拥塞)
  • p->delivered = 1: 标记至少有一个消息成功投递
  • p->skb2 = NULL: 重置克隆SKB指针,下一个socket需要重新克隆

减少socket引用计数

c 复制代码
sock_put(sk);

减少socket的引用计数,与之前的sock_hold对应

退出标签

c 复制代码
out:
    return 0;

所有退出路径都返回0

函数功能总结

主要功能 :向单个Netlink socket投递广播消息,处理各种边界条件和错误情况

  1. 过滤机制:

    • 排除发送者自身
    • 基于pid和group的目标过滤
    • 提前跳过不匹配的socket
  2. 内存管理:

    • SKB引用计数管理
    • 智能克隆策略(仅在需要时克隆)
    • socket引用计数保护
  3. 错误处理:

    • 克隆失败时的全局失败标记
    • 单个socket溢出不影响其他socket
    • 拥塞状态记录
  4. 状态跟踪:

    • 记录成功投递状态
    • 跟踪拥塞情况
    • 管理SKB共享状态

SKB共享策略:

  • 如果原始SKB只有一个使用者,直接共享使用
  • 如果多个使用者需要,创建克隆
  • 避免不必要的内存拷贝
相关推荐
律宏阔2 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔2 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴2 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千3 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei373 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei373 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei373 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW3 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW3 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei373 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化