当前位置: 首页 > article >正文

linux 内核 :Netlink 原理分析

Netlink 是一种IPC(Inter Process Commumicate)机制,它是一种用于内核与用户空间通信的机制,同时它也以用于进程间通信(Netlink 更多用于内核通信,进程之间通信更多使用Unix域套接字)。在一般情况下,用户态和内核态通信会使用传统的Ioctl、sysfs属性文件或者procfs属性文件,这3种通信方式都是同步通信方式,由用户态主动发起向内核态的通信,内核无法主动发起通信。而Netlink是一种异步全双工的通信方式,它支持由内核态主动发起通信,内核为Netlink通信提供了一组特殊的API接口,用户态则基于socket API,内核发送的数据会保存在接收进程socket 的接收缓存中,由接收进程处理。Netlink 有以下优点:

  1. 双向全双工异步传输,支持由内核主动发起传输通信,而不需要用户空间出发(例如使用ioctl这类的单工方式)。如此用户空间在等待内核某种触发条件满足时就无需不断轮询,而异步接收内核消息即可。
  2. 支持组播传输,即内核态可以将消息发送给多个接收进程,这样就不用每个进程单独来查询了。

Netlink架构框图如下:
在这里插入图片描述
目前在Linux 4.1.x 的主线内核版本中,已经有许多内核模块使用netlink 机制,其中驱动模型中使用的uevent 就是基于netlink 实现。目前 netlink 协议族支持32种协议类型,它们定义在 include/uapi/linux/netlink.h 中:

#define NETLINK_ROUTE		0	/* Routing/device hook				*/
#define NETLINK_UNUSED		1	/* Unused number				*/
#define NETLINK_USERSOCK	2	/* Reserved for user mode socket protocols 	*/
#define NETLINK_FIREWALL	3	/* Unused number, formerly ip_queue		*/
#define NETLINK_SOCK_DIAG	4	/* socket monitoring				*/
#define NETLINK_NFLOG		5	/* netfilter/iptables ULOG */
#define NETLINK_XFRM		6	/* ipsec */
#define NETLINK_SELINUX		7	/* SELinux event notifications */
#define NETLINK_ISCSI		8	/* Open-iSCSI */
#define NETLINK_AUDIT		9	/* auditing */
#define NETLINK_FIB_LOOKUP	10	
#define NETLINK_CONNECTOR	11
#define NETLINK_NETFILTER	12	/* netfilter subsystem */
#define NETLINK_IP6_FW		13
#define NETLINK_DNRTMSG		14	/* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT	15	/* Kernel messages to userspace */
#define NETLINK_GENERIC		16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT	18	/* SCSI Transports */
#define NETLINK_ECRYPTFS	19
#define NETLINK_RDMA		20
#define NETLINK_CRYPTO		21	/* Crypto layer */#define NETLINK_INET_DIAG	NETLINK_SOCK_DIAG#define MAX_LINKS 32</span>

现在4.1.x 的内核版本中已经定义了22种协议类型,其中NETLINK_ROUTE是用于设置和查询路由表等网络核心模块的,NETLINK_KOBJECT_UEVENT是用于uevent消息通信的…

对于在实际的项目中,可能会有一些定制化的需求,以上这几种专用的协议类型无法满足,这时可以在不超过最大32种类型的基础之上自行添加。但是一般情况下这样做有些不妥,于是内核开发者就设计了一种通用netlink 协议类型(Generic Netlink)NETLINK_GENERIC,它就是一个Netlink复用器,便于用户自行扩展子协议类型(后面我会使用该Generic Netlink 编写一个示例程序用于演示内核和用户空间的通信)。

下面以linux 4.1.12版本的内核源码为例来分析Netlink的具体创建和通信流程。

一、Netlink子系统初始化
内核Netlink的初始化在系统启动阶段完成,初始化代码在af_netlink.c的netlink_proto_init()函数中,整个初始化流程如下
在这里插入图片描述
netlink子系统初始化

static int __init netlink_proto_init(void)
{int i;int err = proto_register(&netlink_proto, 0);if (err != 0)goto out;BUILD_BUG_ON(sizeof(struct netlink_skb_parms) > FIELD_SIZEOF(struct sk_buff, cb));nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);if (!nl_table)goto panic;for (i = 0; i < MAX_LINKS; i++) {if (rhashtable_init(&nl_table[i].hash,&netlink_rhashtable_params) < 0) {while (--i > 0)rhashtable_destroy(&nl_table[i].hash);kfree(nl_table);goto panic;}}INIT_LIST_HEAD(&netlink_tap_all);netlink_add_usersock_entry();sock_register(&netlink_family_ops);register_pernet_subsys(&netlink_net_ops);/* The netlink device handler may be needed early. */rtnetlink_init();
out:return err;
panic:panic("netlink_init: Cannot allocate nl_table\n");
}core_initcall(netlink_proto_init);

本初始化函数首先向内核注册netlink协议;然后创建并初始化了nl_table表数组,这个表是整个netlink实现的最关键的一步,每种协议类型占数组中的一项,后续内核中创建的不同种协议类型的netlink都将保存在这个表中,由该表统一维护,来简单看一些它的定义,有一个大概的印象:

struct netlink_table {struct rhashtable	hash;struct hlist_head	mc_list;struct listeners __rcu	*listeners;unsigned int		flags;unsigned int		groups;struct mutex		*cb_mutex;struct module		*module;int			(*bind)(struct net *net, int group);void			(*unbind)(struct net *net, int group);bool			(*compare)(struct net *net, struct sock *sock);int			registered;
};

这里的hash(哈希表)用来索引同种协议类型的不同netlink套接字实例,mc_list为多播使用的sock散列表,listeners为监听者掩码,groups为协议支持的最大多播组数量,同时还定义了一些函数指针,它们会在内核首次创建netlink时被赋值,后续应用层创建和绑定socket时调用到。
回到初始化函数中,接下来初始化应用层使用的NETLINK_USERSOCK协议类型的netlink(用于应用层进程间通信);然后调用sock_register向内核注册协议处理函数,即将netlink的socket创建处理函数注册到内核中,如此以后应用层创建netlink类型的socket时将会调用该协议处理函数,其中netlink_family_ops函数的定义如下:

static const struct net_proto_family netlink_family_ops = {.family = PF_NETLINK,.create = netlink_create,.owner	= THIS_MODULE,	/* for consistency 8) */
};

这样以后应用层创建PF_NETLINK(AF_NETLINK)类型的socket()系统调用时将由netlink_create()函数负责处理。
再次回到初始化函数中,接下来调用register_pernet_subsys向内核所有的网络命名空间注册”子系统“的初始化和去初始化函数,这里的"子系统”并非指的是netlink子系统,而是一种通用的处理方式,在网络命名空间创建和注销时会调用这里注册的初始化和去初始化函数(当然对于已经存在的网络命名空间,在注册的过程中也会调用其初始化函数),后文中创建各种协议类型的netlink也是通过这种方式实现的。这里netlink_net_ops定义如下:

static struct pernet_operations __net_initdata netlink_net_ops = {.init = netlink_net_init,.exit = netlink_net_exit,
};

其中netlink_net_init()会在文件系统中位每个网络命名空间创建一个proc入口,而netlink_net_exit()就是则销毁之。下面回来看netlink_proto_init()初始化函数的最后,调用rtnetlink_init()创建NETLINK_ROUTE协议类型的netlink,该种类型的netlink才是当初内核设计netlink的初衷,它用来传递网络路由子系统、邻居子系统、接口设置、防火墙等消息。至此整个netlink子系统初始化完成,还是比较直观易懂的,接下来就需要关注如何使用它进行通信了。

二、内核Netlink套接字

内核中各种协议类型的netlink分别在不同的模块中进行创建和初始化,我以前文中的NETLINK_ROUTE为例来分析一下内核中netlink套接字的创建流程。下面首先来看一下内核netlink使用到的几个关键数据结构:

1、内核netlink配置结构:struct netlink_kernel_cfg

/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {unsigned int	groups;unsigned int	flags;void		(*input)(struct sk_buff *skb);struct mutex	*cb_mutex;int		(*bind)(struct net *net, int group);void		(*unbind)(struct net *net, int group);bool		(*compare)(struct net *net, struct sock *sk);
};

该结构包含了内核netlink的可选参数。其中groups用于指定最大的多播组;flags成员可以为NL_CFG_F_NONROOT_RECV或NL_CFG_F_NONROOT_SEND,这两个符号前者用来限定非超级用户是否可以绑定到多播组,后者用来限定非超级用户是否可以发送组播;input指针用于指定回调函数,该回调函数用于接收和处理来自用户空间的消息(若无需接收来自用户空间的消息可不指定),最后的三个函数指针实现sock的绑定和解绑定等操作,会添加到nl_table对应的项中去

2、netlink属性头:struct nlattr

struct nlattr {__u16           nla_len;__u16           nla_type;
};

netlink的消息头后面跟着的是消息的有效载荷部分,它采用的是格式为“类型——长度——值”,简写TLV。其中类型和长度使用属性头nlattr来表示。其中nla_len表示属性长度;nla_type表示属性类型,它可以取值为以下几种类型(定义在include\net\netlink.h中):

enum {NLA_UNSPEC,NLA_U8,NLA_U16,NLA_U32,NLA_U64,NLA_STRING,NLA_FLAG,NLA_MSECS,NLA_NESTED,NLA_NESTED_COMPAT,NLA_NUL_STRING,NLA_BINARY,NLA_S8,NLA_S16,NLA_S32,NLA_S64,__NLA_TYPE_MAX,
};

其中比较常用的NLA_UNSPEC表示类型和长度未知、NLA_U32表示无符号32位整形数、NLA_STRING表示变长字符串、NLA_NESTED表示嵌套属性(即包含一层新的属性)。

3、netlink有效性策略:struct nla_policy

struct nla_policy {u16		type;u16		len;
};

netlink协议可以根据消息属性定义其特定的消息有效性策略,即对于某一种属性,该属性的期望类型是什么,内核将在收到消息以后对该消息的属性进行有效性判断(如果不设定len值,就不会执行有效性检查),只有判断一直的消息属性才算是合法的,否则只会默默的丢弃。这种有效性属性使用nla_policy来描述,一般定义为一个有效性对象数组(当前这种netlink协议中的每一种attr属性(指定不是属性类型,而是用户定义的属性)有一个对应的数组项),这里type值同struct nlattr中的nla_type,len字段表示本属性的有效载荷长度。

4、netlink套接字结构:netlink_sock

struct netlink_sock {/* struct sock has to be the first member of netlink_sock */struct sock		sk;u32			portid;u32			dst_portid;u32			dst_group;u32			flags;u32			subscriptions;u32			ngroups;unsigned long		*groups;unsigned long		state;size_t			max_recvmsg_len;wait_queue_head_t	wait;bool			bound;bool			cb_running;struct netlink_callback	cb;struct mutex		*cb_mutex;struct mutex		cb_def_mutex;void			(*netlink_rcv)(struct sk_buff *skb);int			(*netlink_bind)(struct net *net, int group);void			(*netlink_unbind)(struct net *net, int group);struct module		*module;
#ifdef CONFIG_NETLINK_MMAPstruct mutex		pg_vec_lock;struct netlink_ring	rx_ring;struct netlink_ring	tx_ring;atomic_t		mapped;
#endif /* CONFIG_NETLINK_MMAP */struct rhash_head	node;struct rcu_head		rcu;
};

本结构用于描述一个netlink套接字,其中portid表示本套接字自己绑定的id号,对于内核来说它就是0,dst_portid表示目的id号,ngroups表示协议支持多播组数量,groups保存组位掩码,netlink_rcv保存接收到用户态数据后的处理函数,netlink_bind和netlink_unbind用于协议子协议自身特有的绑定和解绑定处理函数。

5、创建内核netlink套接字

现在查看rtnetlink_net_init()函数来分析NETLINK_ROUTE类型netlink套接字的创建流程:首先回到子系统初始化函数netlink_proto_init()的最后,来查看rtnetlink_init()函数的执行流程:
在这里插入图片描述
图2 内核netlink套接字创建流程

void __init rtnetlink_init(void)
{if (register_pernet_subsys(&rtnetlink_net_ops))panic("rtnetlink_init: cannot initialize rtnetlink\n");...
}

这里的手法前文中已经见过了,这里将rtnetlink的init函数和exit函数注册到内核的每个网络命名空间中,对于已经存在的网络命名空间会调用其中个的init函数,这里就是rtnetlink_net_init()函数了。

static struct pernet_operations rtnetlink_net_ops = {.init = rtnetlink_net_init,.exit = rtnetlink_net_exit,
};
static int __net_init rtnetlink_net_init(struct net *net)
{struct sock *sk;struct netlink_kernel_cfg cfg = {.groups		= RTNLGRP_MAX,.input		= rtnetlink_rcv,.cb_mutex	= &rtnl_mutex,.flags		= NL_CFG_F_NONROOT_RECV,};sk = netlink_kernel_create(net, NETLINK_ROUTE, &cfg);if (!sk)return -ENOMEM;net->rtnl = sk;return 0;
}

首先这里定义了一个netlink_kernel_cfg结构体实例,设置groups为RTNLGRP_MAX后指定消息接收处理函数为rtnetlink_rcv,并设置flag为NL_CFG_F_NONROOT_RECV,这表明非超级用户可以绑定到多播组,但是没有设置NL_CFG_F_NONROOT_SEND,这表明非超级用户将不能发送组播消息

随后init函数调用netlink_kernel_create()向当前的网络命名空间创建NETLINK_ROUTE类型的套接字,并指定定义的那个配置结构cfg。进入netlink_kernel_create()函数内部:

netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}

它其实就是__netlink_kernel_create()的一个封装而已,__netlink_kernel_create函数比较长,分段分析:

/**	We export these functions to other modules. They provide a*	complete set of kernel non-blocking support for message*	queueing.*/struct sock *
__netlink_kernel_create(struct net *net, int unit, struct module *module,struct netlink_kernel_cfg *cfg)
{struct socket *sock;struct sock *sk;struct netlink_sock *nlk;struct listeners *listeners = NULL;struct mutex *cb_mutex = cfg ? cfg->cb_mutex : NULL;unsigned int groups;BUG_ON(!nl_table);if (unit < 0 || unit >= MAX_LINKS)return NULL;if (sock_create_lite(PF_NETLINK, SOCK_DGRAM, unit, &sock))return NULL;/** We have to just have a reference on the net from sk, but don't* get_net it. Besides, we cannot get and then put the net here.* So we create one inside init_net and the move it to net.*/if (__netlink_create(&init_net, sock, cb_mutex, unit) < 0)goto out_sock_release_nosk;

这里首先进行简单的参数判断之后就调用sock_create_lite()函数创建了一个以PF_NETLINK为地址族的SOCK_DGRAM类型的socket套接字,其协议类型就是作为参数传入的NETLINK_ROUTE。然后该函数调用最核心的__netlink_create()函数向内核初始化netlink套接字(其实在下文中将会看到用户态创建netlink套接字也是间接调用到该函数):

static int __netlink_create(struct net *net, struct socket *sock,struct mutex *cb_mutex, int protocol)
{struct sock *sk;struct netlink_sock *nlk;sock->ops = &netlink_ops;sk = sk_alloc(net, PF_NETLINK, GFP_KERNEL, &netlink_proto);if (!sk)return -ENOMEM;sock_init_data(sock, sk);nlk = nlk_sk(sk);if (cb_mutex) {nlk->cb_mutex = cb_mutex;} else {nlk->cb_mutex = &nlk->cb_def_mutex;mutex_init(nlk->cb_mutex);}init_waitqueue_head(&nlk->wait);
#ifdef CONFIG_NETLINK_MMAPmutex_init(&nlk->pg_vec_lock);
#endifsk->sk_destruct = netlink_sock_destruct;sk->sk_protocol = protocol;return 0;
}

首先将sock的操作函数集指针设置为netlink_ops,这在后面消息通讯时会详细分析,然后分配sock结构并进行初始化,主要包括初始化发送接收消息队列、数据缓存、等待队列和互斥锁等等,最后设置sk_destruct回调函数和协议类型。再回到__netlink_kernel_create()函数中继续分析:

	sk = sock->sk;sk_change_net(sk, net);if (!cfg || cfg->groups < 32)groups = 32;elsegroups = cfg->groups;listeners = kzalloc(sizeof(*listeners) + NLGRPSZ(groups), GFP_KERNEL);if (!listeners)goto out_sock_release;sk->sk_data_ready = netlink_data_ready;if (cfg && cfg->input)nlk_sk(sk)->netlink_rcv = cfg->input;if (netlink_insert(sk, 0))goto out_sock_release;nlk = nlk_sk(sk);nlk->flags |= NETLINK_KERNEL_SOCKET;

这里有一点值得注意的就是前面在调用__netlink_create()时分配struct sock结构实例使用的是init_net命名空间,这里会调用sk_change_net将网络命名空间转移回到当前的net命名空间(至于为什么要这样做,注释中有说明,大概意思是当前的上下文中无法对net命名空间执行get_net操作,可能是防止内核还在初始化的过程中不支持这样的操作,具体原因还不是很理解)。
接下来校验groups,默认最小支持32个组播地址(因为后文会看到用户层在绑定地址时最多绑定32个组播地址),但内核也有可能支持大于32个组播地址的情况(Genetlink就属于这种情况),然后分配listeners内存空间,这里边保存了监听者(监听套接字)的信息;接下来继续初始化函数指针,这里将前文中定义的rtnetlink_rcv注册到了nlk_sk(sk)->netlink_rcv中,这样就设置完了内核态的消息处理函数;然后调用netlink_insert()函数将本次创建的这个套接字添加到nl_table中去(其核心是调用__netlink_insert()),注册的套接字是通过nl_table中的哈希表来管理的
然后设置标识NETLINK_KERNEL_SOCKET表明这个netlink套接字是一个内核套接字。

	netlink_table_grab();if (!nl_table[unit].registered) {nl_table[unit].groups = groups;rcu_assign_pointer(nl_table[unit].listeners, listeners);nl_table[unit].cb_mutex = cb_mutex;nl_table[unit].module = module;if (cfg) {nl_table[unit].bind = cfg->bind;nl_table[unit].unbind = cfg->unbind;nl_table[unit].flags = cfg->flags;if (cfg->compare)nl_table[unit].compare = cfg->compare;}nl_table[unit].registered = 1;} else {kfree(listeners);nl_table[unit].registered++;}netlink_table_ungrab();return sk;

接下来继续初始化nl_table表中对应传入NETLINK_ROUTE协议类型的数组项,首先会判断是否已经先有同样协议类型的已经注册过了,如果有就不再初始化该表项了,直接释放刚才申请的listeners内存空间然后递增注册个数并返回。这里假定是首次注册NETLINK_ROUTE协议类型的套接字,这里依次初始化了nl_table表项中的groups、listeners、cb_mutex、module、bind、unbind、flags和compare字段。通过前文中cfg的实例分析,这里的初始化的值分别如下:
nl_table[NETLINK_ROUTE].groups = RTNLGRP_MAX;
nl_table[NETLINK_ROUTE].cb_mutex = &rtnl_mutex;
nl_table[NETLINK_ROUTE].module = THIS_MODULE;
nl_table[NETLINK_ROUTE].bind = NULL;
nl_table[NETLINK_ROUTE].unbind = NULL;
nl_table[NETLINK_ROUTE].compare = NULL;
nl_table[NETLINK_ROUTE].flags= NL_CFG_F_NONROOT_RECV;
这些写值在后面的通信流程中就会使用到。在函数的最后返回成功创建的netlink套接字中的sock指针,它会在最先前的rtnetlink_net_init()函数中被保存到net->rtnl中去,注意只有NETLINK_ROUTE协议类型的套接字才会执行这个步骤,因为网络命名空间中专门为其预留了一个sock指针。
至此内核NETLINK_ROUTE套接字创建完成,下面来看一下应用层是如何创建netlink套接字的。

三、应用层Netlink套接字

应用层通过标准的sock API即可使用Netlink完成通信功能(如socket()、sendto()、recv()、sendmsg()和recvmsg()等)。首先来看一些基本的数据结构及创建流程:

在这里插入图片描述
图3 用户层netlink套接字创建流程

1、套接字地址数据结构sockaddr_nl

struct sockaddr_nl {__kernel_sa_family_t	nl_family;	/* AF_NETLINK	*/unsigned short	nl_pad;		/* zero		*/__u32		nl_pid;		/* port ID	*/__u32		nl_groups;	/* multicast groups mask */
};

其中(1)nl_family始终为AF_NETLINK;(2)nl_pad始终为0;(3)nl_pid为netlink套接字的单播地址,在发送消息时用于表示目的套接字的地址,在用户空间绑定时可以指定为当前进程的PID号(对于内核来说这个值为0)或者干脆不设置(在绑定bind时由内核调用netlink_autobind()设置为当前进程的PID),但需要注意的是当用户同一个进程中需要创建多个netlink套接字时则必须保证这个值是唯一的(一般在多线程中可以使用”pthread_self() << 16 | getpid()“这样的方法进行设置);(4)nl_groups表示组播组。在发送消息时用于表示目的多播组,在绑定地址时用于表示加入的多播组。这里nl_groups为一个32位无符号数,其中的每一位表示一个 多播组,一个netlink套接字可以加入多个多播组用以接收多个多播组的多播消息(最多支持32个)。

2、创建Netlink套接字

应用层通过socket()系统调用创建Netlink套接字,socket系统调用的第一个参数可以是AF_NETLINK或PF_NETLINK(在Linux系统中它俩实际为同一种宏),第二个参数可以是SOCK_RAW或SOCK_DGRAM(原始套接字或无连接的数据报套接字),最后一个参为netlink.h中定义的协议类型,用户可以按需求自行创建上述不同种类的套接字。

例如调用 socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) 即创建了一个NETLINK_ROUTE类型的Netlink套接字。下面跟进这个系统调用,查看内核是如何为用户层创建这个套接字然后又做了哪些初始化动作:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{int retval;struct socket *sock;int flags;/* Check the SOCK_* constants for consistency.  */BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);flags = type & ~SOCK_TYPE_MASK;if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))return -EINVAL;type &= SOCK_TYPE_MASK;if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;retval = sock_create(family, type, protocol, &sock);if (retval < 0)goto out;retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));if (retval < 0)goto out_release;out:/* It may be already another descriptor 8) Not kernel problem. */return retval;out_release:sock_release(sock);return retval;
}

该函数首先做了一些参数检查之后就调用sock_create()函数创建套接字,在创建完成后向内核申请描述符并返回该描述符。进入sock_create()函数内部,它是__sock_create()的一层封装(内核中往往前面带两个下划线的函数才是做事实的,嘿嘿),这里要注意的是调用时又多了两个个参数,一是当前进程绑定的网络命名空间,而是最后一个kern参数,这里传入0表明是从应用层创建的套接字。__sock_create()函数比较长,来分段分析之:

int sock_create(int family, int type, int protocol, struct socket **res)
{return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
int __sock_create(struct net *net, int family, int type, int protocol,struct socket **res, int kern)
{int err;struct socket *sock;const struct net_proto_family *pf;/**      Check protocol is in range*/if (family < 0 || family >= NPROTO)return -EAFNOSUPPORT;if (type < 0 || type >= SOCK_MAX)return -EINVAL;/* Compatibility.This uglymoron is moved from INET layer to here to avoiddeadlock in module load.*/if (family == PF_INET && type == SOCK_PACKET) {static int warned;if (!warned) {warned = 1;pr_info("%s uses obsolete (PF_INET,SOCK_PACKET)\n",current->comm);}family = PF_PACKET;}

这里依然是一些入参判断,非常直观,无需分析,继续往下:

	err = security_socket_create(family, type, protocol, kern);if (err)return err;/**	Allocate the socket and allow the family to set things up. if*	the protocol is 0, the family is instructed to select an appropriate*	default.*/sock = sock_alloc();if (!sock) {net_warn_ratelimited("socket: no more sockets\n");return -ENFILE;	/* Not exactly a match, but its theclosest posix thing */}sock->type = type;

首先对创建socket执行安全性检查,security_socket_create这个函数在内核没有启用CONFIG_SECURITY_NETWORK配置时是一个空函数直接返回0,这里先不考虑。接下来调用sock_alloc()分配socket实例,它会为其创建和初始化索引节点(inode)。然后将sock->type赋值为传入的SOCK_RAW。

#ifdef CONFIG_MODULES/* Attempt to load a protocol module if the find failed.** 12/09/1996 Marcin: But! this makes REALLY only sense, if the user* requested real, full-featured networking support upon configuration.* Otherwise module support will break!*/if (rcu_access_pointer(net_families[family]) == NULL)request_module("net-pf-%d", family);
#endifrcu_read_lock();pf = rcu_dereference(net_families[family]);err = -EAFNOSUPPORT;if (!pf)goto out_release;

在启用内核模块的情况下,这里会到内核net_families数组中查找该family(AF_NETLINK)是否已经注册,如果没有注册就会尝试加载网络子系统模块。其实在内核的netlink初始化函数中已经调用sock_register()完成注册了(见前文)。接下来从net_families数组中获取已经注册的struct net_proto_family结构实例,这里就是第一节中描述过的netlink_family_ops了。继续往下分析:

	/** We will call the ->create function, that possibly is in a loadable* module, so we have to bump that loadable module refcnt first.*/if (!try_module_get(pf->owner))goto out_release;/* Now protected by module ref count */rcu_read_unlock();err = pf->create(net, sock, protocol, kern);if (err < 0)goto out_module_put;/** Now to bump the refcnt of the [loadable] module that owns this* socket at sock_release time we decrement its refcnt.*/if (!try_module_get(sock->ops->owner))goto out_module_busy;/** Now that we're done with the ->create function, the [loadable]* module can have its refcnt decremented*/module_put(pf->owner);err = security_socket_post_create(sock, family, type, protocol, kern);if (err)goto out_sock_release;*res = sock;return 0;

这里先获取当前模块的引用计数并上锁,然后调用netlink协议的creat()钩子函数执行进一步的创建和初始化操作(这里就是netlink_family_ops中定义的netlink_create()了),完成之后就释放锁同时释放当前模块的引用计数并返回创建成功的socket。下面进入netlink_create()内部继续分析:

static int netlink_create(struct net *net, struct socket *sock, int protocol,int kern)
{struct module *module = NULL;struct mutex *cb_mutex;struct netlink_sock *nlk;int (*bind)(struct net *net, int group);void (*unbind)(struct net *net, int group);int err = 0;sock->state = SS_UNCONNECTED;if (sock->type != SOCK_RAW && sock->type != SOCK_DGRAM)return -ESOCKTNOSUPPORT;if (protocol < 0 || protocol >= MAX_LINKS)return -EPROTONOSUPPORT;netlink_lock_table();
#ifdef CONFIG_MODULESif (!nl_table[protocol].registered) {netlink_unlock_table();request_module("net-pf-%d-proto-%d", PF_NETLINK, protocol);netlink_lock_table();}
#endifif (nl_table[protocol].registered &&try_module_get(nl_table[protocol].module))module = nl_table[protocol].module;elseerr = -EPROTONOSUPPORT;cb_mutex = nl_table[protocol].cb_mutex;bind = nl_table[protocol].bind;unbind = nl_table[protocol].unbind;netlink_unlock_table();if (err < 0)goto out;err = __netlink_create(net, sock, cb_mutex, protocol);if (err < 0)goto out_module;local_bh_disable();sock_prot_inuse_add(net, &netlink_proto, 1);local_bh_enable();nlk = nlk_sk(sock->sk);nlk->module = module;nlk->netlink_bind = bind;nlk->netlink_unbind = unbind;
out:return err;out_module:module_put(module);goto out;
}

首先将socket的状态标记为未连接,判断套接字的类型是否是SOCK_RAW或SOCK_DGRAM类型的,若不是就不能继续创建;接着判断该协议类型的netlink是否已经注册了,由于前文中内核在初始化netlink子系统时已经初始化了NETLINK_ROUTE内核套接字并向nl_table注册,所以这里的几个赋值结果如下:
cb_mutex = nl_table[NETLINK_ROUTE].cb_mutex = &rtnl_mutex;
module = nl_table[NETLINK_ROUTE].module = THIS_MODULE;
bind = nl_table[NETLINK_ROUTE].bind = NULL;
unbind = nl_table[NETLINK_ROUTE].unbind = NULL;

接下来将调用__netlink_create()完成核心的创建初始化,这个函数在前面已经分析过了,就不进入继续分析了。再往下调用sock_prot_inuse_add添加协议的引用计数,最后完成赋值:

nlk->module = module = THIS_MODULE ;
nlk->netlink_bind = bind = NULL;
nlk->netlink_unbind = unbind = NULL;
至此用户态NETLINK_ROUTE类型的套接字就创建完成了。

3、绑定套接字

在创建完成套接字后需要调用bind()函数进行绑定,将该套接字绑定到一个特定的地址或者加入一个多播组中,以后内核或其他应用层套接字向该地址单播或向该多播组发送组播消息时即可通过recv()或recvmsg()函数接收消息了。绑定地址时需要使用到sockaddr_nl地址结构,如果使用使用单播则需要将地址本地地址信息填入nl_pid变量并设置nl_groups为0,如果使用多播则将nl_pid设置为0并填充nl_groups为多播地址,如下可将当前进程的PID号作为单播地址进行绑定:

struct sockaddr_nl local;fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid();bind(fd, (struct sockaddr *) &local, sizeof(local));

其中bind()的第一个参数为刚创建的Netlink套接字描述符,第二个参数就是需要绑定的套接字地址,最后一个参数是地址的长度。这个绑定操作同创建TCP套接字类似,需要制定绑定的端口(或者由内核给指定一个亦可)。下面进入bind()系统调用分析整个绑定的过程:

在这里插入图片描述
图3 用户层netlink套接字绑定流程

/**	Bind a name to a socket. Nothing much to do here since it's*	the protocol's responsibility to handle the local address.**	We move the socket address to kernel space before we call*	the protocol layer (having also checked the address is ok).*/SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{struct socket *sock;struct sockaddr_storage address;int err, fput_needed;sock = sockfd_lookup_light(fd, &err, &fput_needed);if (sock) {err = move_addr_to_kernel(umyaddr, addrlen, &address);if (err >= 0) {err = security_socket_bind(sock,(struct sockaddr *)&address,addrlen);if (!err)err = sock->ops->bind(sock,(struct sockaddr *)&address, addrlen);}fput_light(sock->file, fput_needed);}return err;
}

首先根据用户传入的fd文件描述符向内核查找对应的socket结构,然后将用户空间传入的地址struct sockaddr拷贝到内核中(会使用到copy_from_user()),接下来继续跳过安全检查函数security_socket_bind(),剩下的主要工作就交给了sock->ops->bind()注册函数了。在创建套接字时调用的__netlink_create()函数中已经将sock->ops赋值为netlink_ops了,来看一下这个结构实例:

static const struct proto_ops netlink_ops = {.family =	PF_NETLINK,.owner =	THIS_MODULE,.release =	netlink_release,.bind =		netlink_bind,.connect =	netlink_connect,.socketpair =	sock_no_socketpair,.accept =	sock_no_accept,.getname =	netlink_getname,.poll =		netlink_poll,.ioctl =	sock_no_ioctl,.listen =	sock_no_listen,.shutdown =	sock_no_shutdown,.setsockopt =	netlink_setsockopt,.getsockopt =	netlink_getsockopt,.sendmsg =	netlink_sendmsg,.recvmsg =	netlink_recvmsg,.mmap =		netlink_mmap,.sendpage =	sock_no_sendpage,
};

这个结构中的各个函数指针都会由系统调用根据套接字的协议类型间接调用到,此时就会调用到这里的netlink_bind()函数,这个函数较长,分段分析:

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;int err;long unsigned int groups = nladdr->nl_groups;bool bound;if (addr_len < sizeof(struct sockaddr_nl))return -EINVAL;if (nladdr->nl_family != AF_NETLINK)return -EINVAL;/* Only superuser is allowed to listen multicasts */if (groups) {if (!netlink_allowed(sock, NL_CFG_F_NONROOT_RECV))return -EPERM;err = netlink_realloc_groups(sk);if (err)return err;}

可以看到,这里又将用户传入的地址类型强制转换成了sockaddr_nl类型的地址结构,然后做了一些参数的判断,接着如果用户设定了需要绑定的多播地址,这里会去检擦nl_table中注册的套接字是否已经设置了NL_CFG_F_NONROOT_RECV标识,如果没有设置将拒绝用户绑定到组播组,显然在前文中已经看到了NETLINK_ROUTE类型的套接字是设置了这个标识的,所以这里会调用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;}if (nlk->ngroups >= groups)goto out_unlock;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));nlk->groups = new_groups;nlk->ngroups = groups;out_unlock:netlink_table_ungrab();return err;
}

这里会比较验证一下当前套接字中指定的组播地址上限是否大于NETLINK_ROUTE套接字支持的最大地址(这里为RTNLGRP_MAX),由于这个套接字是前面刚刚创建的,所以nlk->ngroups = 0。

然后为其分配内存空间,分配的空间大小为NLGRPSZ(groups)(这是一个取整对齐的宏),分配完成后将新分配的空间清零,首地址保存在nlk->groups中,最后更新nlk->ngroups变量。回到netlink_bind()函数中继续往下分析:

	bound = nlk->bound;if (bound) {/* Ensure nlk->portid is up-to-date. */smp_rmb();if (nladdr->nl_pid != nlk->portid)return -EINVAL;}if (nlk->netlink_bind && groups) {int group;for (group = 0; group < nlk->ngroups; group++) {if (!test_bit(group, &groups))continue;err = nlk->netlink_bind(net, group + 1);if (!err)continue;netlink_undo_bind(group, groups, sk);return err;}}

接下来如果已经绑定过了,会检查新需要绑定的id号是否等于已经绑定的id号,若不相等则返回失败。接着如果netlink套接字子协议存在特有的bind函数且用户指定了需要绑定的组播地址,则调用之为其绑定到特定的组播组中去。现由于NETLINK_ROUTE套接字并不存在nlk->netlink_bind()函数实现,所以这里并不会调用。

	/* No need for barriers here as we return to user-space without* using any of the bound attributes.*/if (!bound) {err = nladdr->nl_pid ?netlink_insert(sk, nladdr->nl_pid) :netlink_autobind(sock);if (err) {netlink_undo_bind(nlk->ngroups, groups, sk);return err;}}

如果本套接字并没有被绑定过(目前就是这种情况),这里会根据用户是否指定了单播的绑定地址来调用不同的函数。首先假定用户空间指定了单播的绑定地址,这里会调用netlink_insert()函数将这个套接字插入到nl_table[NETLINK_ROUTE]数组项的哈希表中去,同时设置nlk_sk(sk)->bound = nlk_sk(sk)->portid = nladdr->nl_pid。我们再假定用户空间没有设置单播的绑定地址,这里会调用netlink_autobind()动态的绑定一个地址,进入该函数简单的看一下:

static int netlink_autobind(struct socket *sock)
{struct sock *sk = sock->sk;struct net *net = sock_net(sk);struct netlink_table *table = &nl_table[sk->sk_protocol];s32 portid = task_tgid_vnr(current);int err;static s32 rover = -4097;retry:cond_resched();rcu_read_lock();if (__netlink_lookup(table, portid, net)) {/* Bind collision, search negative portid values. */portid = rover--;if (rover > -4097)rover = -4097;rcu_read_unlock();goto retry;}rcu_read_unlock();err = netlink_insert(sk, portid);if (err == -EADDRINUSE)goto retry;/* If 2 threads race to autobind, that is fine.  */if (err == -EBUSY)err = 0;return err;
}

这里会首先尝试选用当前的进程ID作为端口地址,如果当前进程ID已经绑定过其他的相同protocol套接字则会选用一个负数作为ID号(查找直到存在可用的),最后同样调用netlink_insert()函数。回到netlink_bind()函数中:

	if (!groups && (nlk->groups == NULL || !(u32)nlk->groups[0]))return 0;netlink_table_grab();netlink_update_subscriptions(sk, nlk->subscriptions +hweight32(groups) -hweight32(nlk->groups[0]));nlk->groups[0] = (nlk->groups[0] & ~0xffffffffUL) | groups;netlink_update_listeners(sk);netlink_table_ungrab();return 0;

如果没有指定组播地址且没有分配组播的内存,绑定工作到这里就已经结束了,可以直接返回了。现假定用户指定了需要绑定的组播地址,这里首先调用netlink_update_subscriptions绑定sk->sk_bind_node到nl_table[sk->sk_protocol].mc_list中,同时将加入的组播组数目记录到nlk->subscriptions中,并将组播地址保存到nlk->groups[0]中,最后更新netlink监听位掩码。至此绑定操作结束。

分析完成netlink子系统的创建、内核netlink套接字的创建、应用层netlink套接字的创建和绑定后,下一篇来分析一下内核和应用层之间是如何发送消息的。

http://www.lryc.cn/news/2420354.html

相关文章:

  • [Java] currentTimeMillis方法与Date类
  • 总结指针数组与数组指针的区别
  • Redis持久化的两种方式(RDB持久化和AOF持久化)
  • 网络体系结构基本概念及OSI七层模型
  • 机器学习之回归中的相关度和决定系数
  • mul matlab,汇编语言MUL指令:无符号数乘法
  • VM虚拟机(操作)
  • Logcat命令详解 和 adb 常用命令
  • Android 服务(Service)详解
  • 【2024最新版】超详细NMAP安装保姆级教程,Nmap的介绍、功能并进行网络扫描,收藏这一篇就够了
  • setAttribute的作用
  • Oracle 分区表快速使用
  • adb工具包的安装和使用(Windows)
  • 什么是硬件加速,什么时候应该打开它?这里提供详细解释
  • 计算机网络---数据链路层HDLC协议
  • JAVA SCRIPT全述
  • SyntaxError: invalid syntax报错(python)
  • OSGI初识
  • 秩和检验
  • Linux——文件管理(文件系统、目录管理、文件操作)
  • C程序设计语言——指针
  • 【iReport+JasperReport】1.iReport与JasperReport基础
  • sql中的int、bigint、smallint和tinyint四种数据类型
  • 【大数据】Hadoop—— 三大核心组件理论入门 | 完全分布式集群搭建 | 入门项目实战
  • ansible脚本-Playbook(一)
  • mdf文件和ldf文件ndf是什么,怎么用?如何给SQL server添加数据文件?分离和附加数据库的操作
  • Ctex+texmaker
  • caffe入门教程
  • oninput、onblur和onchange的区别
  • MySQL常用命令大全(完整)