Netlink组播机制

数据结构

下面仅罗列了和组播机制相关的数据结构及其字段。

协议对象: netlink_table

struct netlink_table {
...
    struct hlist_head mc_list; // 组织所有监听该协议的多播数据的传输控制块对象
    unsigned long *listeners; // 标记该协议哪些多播组被监听,被监听多播组对应bit为1
    unsigned int groups; // 协议支持的多播组个数,最小32个组
...
};

传输控制块: netlink_sock

struct netlink_sock {
...
	u32	pid; // 该套接字的标识,相当于端口号,见netlink_bind()
	u32	dst_pid; // connect()调用可以设置套接字的目的PID和目的组
	u32	dst_group;

	u32	subscriptions;
	u32	ngroups;
	unsigned long *groups;
...
};
  • subscriptions

记录该套接字接收多少个多播组的数据,见下方的绑定过程。

  • ngroups/groups

当套接字需要接收某些多播组的数据时,在绑定时,会设置ngroups为netlink_table.groups,并且保证groups数组的大小足够表示这些多播组(按位使用,类似netlink_table.listeners的使用)。

组播发送控制信息: netlink_broadcast_data

struct netlink_broadcast_data {
    // 发送时排除哪个套接字,通常设置为发送方自己,从而避免发送方收到自己发送的组播数据
	struct sock *exclude_sk;
	struct net *net;
	u32 pid; // 组播目的地址
	u32 group;
	int failure; // 标记组播发送过程中是否有错误
	int congested; // 标记组播发送过程中是否有拥塞
	int delivered;
	gfp_t allocation;
	// skb是要发送的数据,发送过程如果需要拷贝skb,则拷贝后的数据保存在skb2中
	struct sk_buff *skb, *skb2;
};

协议注册时

在具体协议向Netlink框架注册自己时,会初始化对应的协议对象netlink_table,其中和组播相关的字段设置如下。

@unit: 协议类型,对应socket(2)中的protocol参数
@groups: 协议支持的多播组个数
struct sock *netlink_kernel_create(struct net *net, int unit, unsigned int groups,
    void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module)
{
    struct socket *sock;
    struct sock *sk;
    struct netlink_sock *nlk;
    unsigned long *listeners = NULL;

...
    // 根据多播组个数分配listeners数组
    if (groups < 32) // 最少32个组播
        groups = 32;
    listeners = kzalloc(NLGRPSZ(groups), GFP_KERNEL);
    if (!listeners)
        goto out_sock_release;
...
    // 设置协议对象中的相关字段
    netlink_table_grab();
    if (!nl_table[unit].registered) { // 首次调用就是注册
        nl_table[unit].groups = groups;
        nl_table[unit].listeners = listeners;
        nl_table[unit].cb_mutex = cb_mutex;
        nl_table[unit].module = module;
        nl_table[unit].registered = 1; // 标记协议已注册
    } else { // 重复注册
    	kfree(listeners);
    	nl_table[unit].registered++;
    }
    netlink_table_ungrab();
    return sk;
...
}

listeners数组是按照bit使用的,每个多播组对应一个位,索引就是组号。宏NLGRPSZ()可以保证分配足够多的bit。

// sizeof(unsigned long) * 8表示一个unsigned long类型的整数的bit个数,参数x为多播组个数,
// 向上对齐后除以8,得到的结果是按比特表示x个多播组需要多少个字节
#define NLGRPSZ(x)	(ALIGN(x, sizeof(unsigned long) * 8) / 8)

套接字绑定时

static int netlink_bind(struct socket *sock, struct sockaddr *addr, int addr_len)
{
	struct sock *sk = sock->sk;
	struct net *net = sock_net(sk);
	struct netlink_sock *nlk = nlk_sk(sk);
	struct sockaddr_nl *nladdr = (struct sockaddr_nl *)addr;

...
	/* Only superuser is allowed to listen multicasts */
	if (nladdr->nl_groups) { // 需要绑定到多播组上
	    // 调用进程有组播接收权限
		if (!netlink_capable(sock, NL_NONROOT_RECV))
			return -EPERM;
		// 尝试重新分配传输控制块上的groups空间
		err = netlink_realloc_groups(sk);
		if (err)
			return err;
	}
...
    // 没有绑定组播地址,并且groups信息也为空,那么不需要更新它们,结束绑定过程
	if (!nladdr->nl_groups && (nlk->groups == NULL || !(u32)nlk->groups[0]))
		return 0;

    // 更新组播监听者
	netlink_table_grab();
	// hweight32()计算的是参数指定的整数的二进制位中1的个数,这里为何第二个参数不能直接
	// 写成hweight32(nladdr->nl_groups)
	netlink_update_subscriptions(sk, nlk->subscriptions +
					 hweight32(nladdr->nl_groups) - hweight32(nlk->groups[0]));
    // 为何不直接设置为nladdr->nl_groups
	nlk->groups[0] = (nlk->groups[0] & ~0xffffffffUL) | nladdr->nl_groups;
	netlink_update_listeners(sk);
	netlink_table_ungrab();

	return 0;
}

netlink_realloc_groups()

static int netlink_realloc_groups(struct sock *sk)
{
	struct netlink_sock *nlk = nlk_sk(sk);
	unsigned int groups;
	unsigned long *new_groups;
	int err = 0;

	netlink_table_grab();

    // 拿到该协议支持的多播组个数
	groups = nl_table[sk->sk_protocol].groups;
	if (!nl_table[sk->sk_protocol].registered) {
		err = -ENOENT;
		goto out_unlock;
	}
    // 如果该套接字的groups内存已经足够,则不用重新分配
	if (nlk->ngroups >= groups)
		goto out_unlock;

    // 重新分配一块更大的空间,可见groups的大小和协议对象中listener的大小是一致的
	new_groups = krealloc(nlk->groups, NLGRPSZ(groups), GFP_ATOMIC);
	if (new_groups == NULL) {
		err = -ENOMEM;
		goto out_unlock;
	}
	// 将新分配的部分清零
	memset((char *)new_groups + NLGRPSZ(nlk->ngroups), 0,
	       NLGRPSZ(groups) - NLGRPSZ(nlk->ngroups));
    // 记录新的groups信息
	nlk->groups = new_groups;
	nlk->ngroups = groups;

out_unlock:
	netlink_table_ungrab();
	return err;
}

更新签约信息: netlink_update_subscriptions()

static void
netlink_update_subscriptions(struct sock *sk, unsigned int subscriptions)
{
	struct netlink_sock *nlk = nlk_sk(sk);

	if (nlk->subscriptions && !subscriptions)
	    // 新的传输控制块已经不需要接收任何多播组数据了,将其从协议对象的mc_list链表中删除
		__sk_del_bind_node(sk);
	else if (!nlk->subscriptions && subscriptions)
	    // 相反,将传输控制块对象加入mc_list中
		sk_add_bind_node(sk, &nl_table[sk->sk_protocol].mc_list);
	// 记录新的监听多播组个数
	nlk->subscriptions = subscriptions;
}

更新监听信息: netlink_update_listeners()

// 得到的结果是按bit表示x个多播组需要的unsigned long整数的个数
#define NLGRPLONGS(x)	(NLGRPSZ(x)/sizeof(unsigned long))

static void
netlink_update_listeners(struct sock *sk)
{
	struct netlink_table *tbl = &nl_table[sk->sk_protocol]; // 找到协议对象
	struct hlist_node *node;
	unsigned long mask;
	unsigned int i;

    // 遍历整个mc_list链表,重新标记listeners,只要对应bit的多播组有套接字监听,
    // listeners的对应bit就标记为1
	for (i = 0; i < NLGRPLONGS(tbl->groups); i++) {
		mask = 0;
		sk_for_each_bound(sk, node, &tbl->mc_list) {
			if (i < NLGRPLONGS(nlk_sk(sk)->ngroups))
				mask |= nlk_sk(sk)->groups[i];
		}
		tbl->listeners[i] = mask;
	}
	/* this function is only called with the netlink table "grabbed", which
	 * makes sure updates are visible before bind or setsockopt return. */
}

套接字连接时

static int netlink_connect(struct socket *sock, struct sockaddr *addr,
			   int alen, int flags)
{
	int err = 0;
	struct sock *sk = sock->sk;
	struct netlink_sock *nlk = nlk_sk(sk);
	struct sockaddr_nl *nladdr = (struct sockaddr_nl *)addr;

...
	/* Only superuser is allowed to send multicasts */
	// 连接到某个多播组,检查调用者是否有组播发送权限
	if (nladdr->nl_groups && !netlink_capable(sock, NL_NONROOT_SEND))
		return -EPERM;
...
    // 保存目的地址信息
	if (err == 0) {
		sk->sk_state = NETLINK_CONNECTED;
		nlk->dst_pid = nladdr->nl_pid;
		nlk->dst_group = ffs(nladdr->nl_groups);
	}
	return err;
}

ffs(x)是个体系结构实现相关的宏,它是Find Fist Set的缩写,它按照从低位到高位的顺序查找x中第一个为1的比特的位置,最低位为1,依次类推,所以上述nlk->dst_group最终保存的是多播组的组号(组号是从1开始的)。

套接字发送时

static int netlink_sendmsg(struct kiocb *kiocb, struct socket *sock, struct msghdr *msg, size_t len)
{
	u32 dst_pid;
	u32 dst_group;
...

	if (msg->msg_namelen) { // 指定了目的地,对组播发送进行权限检查
		dst_pid = addr->nl_pid;
		// 如果是组播消息,那么校验其发送权限
		dst_group = ffs(addr->nl_groups);
		if (dst_group && !netlink_capable(sock, NL_NONROOT_SEND))
			return -EPERM;
	} else {
	    // 本次发送没有指定目的地址,使用传输控制块中的目的地址,只有连接过的socket这两个字段才有值,
	    // 见连接调用实现。特别的,如果不指定也未连接过,那么这两个默认都为0,此时会发送消息给内核
		dst_pid = nlk->dst_pid;
		dst_group = nlk->dst_group;
	}
...
    // 先进行组播发送,然后进行单播发送
	if (dst_group) {
		atomic_inc(&skb->users);
		netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);
	}
	err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

out:
	return err;
}

@pid/group: 代表了组播目的地址
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
		      u32 group, gfp_t allocation)
{
	struct net *net = sock_net(ssk);
	struct netlink_broadcast_data info;
	struct hlist_node *node;
	struct sock *sk;

	skb = netlink_trim(skb, allocation);

    // 填充组播发送控制信息
	info.exclude_sk = ssk; // 避免发送者收到自己发送的数据
	info.net = net;
	info.pid = pid; // 组播数据的目的地址
	info.group = group;
	info.failure = 0;
	info.congested = 0;
	info.delivered = 0;
	info.allocation = allocation;
	info.skb = skb;
	info.skb2 = NULL;

	/* While we sleep in clone, do not allow to change socket list */
	netlink_lock_table();

    // 遍历该协议上每个监听组播的传输控制块,调用do_one_broadcast()尝试将skb发送给它
	sk_for_each_bound(sk, node, &nl_table[ssk->sk_protocol].mc_list)
		do_one_broadcast(sk, &info);

	kfree_skb(skb);
	netlink_unlock_table();

    // 发送过程中可能需要拷贝skb,拷贝后的skb放在了skb2中,发送完毕后将其释放
	if (info.skb2)
		kfree_skb(info.skb2);

    // 处理可能的拥塞和发送失败
	if (info.delivered) {
		if (info.congested && (allocation & __GFP_WAIT))
			yield();
		return 0;
	}
	if (info.failure)
		return -ENOBUFS;
	return -ESRCH;
}

do_one_broadcast()

如上,该函数尝试将skb发送给参数sk代表的传输控制块。

static inline int do_one_broadcast(struct sock *sk, struct netlink_broadcast_data *p)
{
	struct netlink_sock *nlk = nlk_sk(sk);
	int val;

	if (p->exclude_sk == sk) // 跳过要排除的socket
		goto out;

    // 目的地址匹配,因为组号是从1开始的,所以这里要减1处理
	if (nlk->pid == p->pid || p->group - 1 >= nlk->ngroups ||
	    !test_bit(p->group - 1, nlk->groups))
		goto out;

	if (!net_eq(sock_net(sk), p->net))
		goto out;

	if (p->failure) { // 遍历过程中发生失败,后续socket需要处理这种失败
		netlink_overrun(sk);
		goto out;
	}

	sock_hold(sk);
	// 将skb拷贝一份
	if (p->skb2 == NULL) {
		if (skb_shared(p->skb)) {
			p->skb2 = skb_clone(p->skb, p->allocation);
		} else {
			p->skb2 = skb_get(p->skb);
			/*
			 * skb ownership may have been set when
			 * delivered to a previous socket.
			 */
			skb_orphan(p->skb2);
		}
	}
	if (p->skb2 == NULL) { // skb拷贝失败
		netlink_overrun(sk);
		/* Clone failed. Notify ALL listeners. */
		p->failure = 1;
	} else if (sk_filter(sk, p->skb2)) { // 通过目标socket上挂的过滤器 
		kfree_skb(p->skb2);
		p->skb2 = NULL;
	} else if ((val = netlink_broadcast_deliver(sk, p->skb2)) < 0) { // 将skb发送给目标socket
		netlink_overrun(sk);
	} else {
		p->congested |= val; // 发送成功
		p->delivered = 1;
		p->skb2 = NULL;
	}
	sock_put(sk);

out:
	return 0;
}

static inline int netlink_broadcast_deliver(struct sock *sk,
					    struct sk_buff *skb)
{
	struct netlink_sock *nlk = nlk_sk(sk);

    // 接收内存可用的情况下,将skb放入其接收队列,然后唤醒可能的读进程,
    // 之后的接收流程,组播数据和普通单播数据并无不同
	if (atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf &&
	    !test_bit(0, &nlk->state)) {
		skb_set_owner_r(skb, sk);
		skb_queue_tail(&sk->sk_receive_queue, skb);
		sk->sk_data_ready(sk, skb->len);
		return atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf;
	}
	// 返回-1表示发生了拥塞
	return -1;
}

特别注意:上述组播发送过程中,在目的地址匹配时,要注意只要目的pid和目的组二者有一个匹配即可。

小结

以上就是Netlink组播机制的核心内容。Netlink组播常用于内核态向用户态发送某种通知,可实际上也是支持用户态到用户态的Netlink组播(用户态到内核态组播没有意义,单播就可以搞定),只要发送方和接收方约定好使用哪个协议的哪个多播组即可。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值