netlink套接字

  • Post author:
  • Post category:其他


首先介绍netlink套接字以及它支持的协议族的不同类型。然后详细介绍netlink套接字在启动时如何注册。另外,还将分别阐述如何创建内核和用户netlink套接字、netlink数据结构的细节以及netlink报文的格式。最后,本博客还会详细说明netlink用户如何与内核套接字进行交互。

netlink套接字介绍

netlink是一种在内核模块与用户空间进程之间传输数据的双向通信方法。其功能由为用户空间进程提供的标准套接字API和为内核模块提供的内部内核API组成。

Linux支持的netlink族如下:

  • NETLINK_ROUTE:它被用于排队规则,以更新IP v4路由表。
  • NETLINK_SKIP:为ENskip保留。
  • NETLINK_USERSOCK:为用户模式套接字协议保留。
  • NETLINK_FIREWALL:接收通过IP v4防火墙编码发送的报文。
  • NETLINK_ARPD:用于根棍arp表。
  • NETLINK_ROUTE6:用于更新ipv6路由表。

为什么选择netlink套接字?

  • netlink套接字支持多播,且一个进程可以将消息多播到netlink地址组。
  • netlink提供BSD套接字样式的API。
  • netlink套接字是异步的,并提供套接字的消息排队规则。
  • 要支持任何新的特征,只需要实现对应的协议类型。

启动时netlink套接字的注册和初始化

启动时,当加载netlink模块(net/netlink/af_netlink.c)时,module_init会调用netlink_proto_init初始化例程:

static int __init netlink_proto_init(void)
{
	struct sk_buff *dummy_skb;

	if (sizeof(struct netlink_skb_parms) > sizeof(dummy_skb->cb)) {
		printk(KERN_CRIT "netlink_init: panic\n");
		return -1;
	}
	sock_register(&netlink_family_ops);
#ifdef CONFIG_PROC_FS
	create_proc_read_entry("net/netlink", 0, 0, netlink_read_proc, NULL);
#endif
	return 0;
}

在netlink_proto_init例程中,以参数netlink_family_ops调用sock_register。

netlink_family_ops是一种net_proto_family结构,如果是netlink协议,则其定义如下所示,其中PF_NETLINK是协议类型族。netlink_create是PF_NETLINK套接字的创建函数。

struct net_proto_family netlink_family_ops = {
	PF_NETLINK,
	netlink_create
};

函数sock_register的主要目的是通知协议处理程序的地址族,并购将其链接到套接字模块。

/*
 *	This function is called by a protocol handler that wants to
 *	advertise its address family, and have it linked into the
 *	SOCKET module.
 */

int sock_register(struct net_proto_family *ops)
{
	int err;

	if (ops->family >= NPROTO) {
		printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family, NPROTO);
		return -ENOBUFS;
	}
	net_family_write_lock();
	err = -EEXIST;
	if (net_families[ops->family] == NULL) {
		net_families[ops->family]=ops;
		err = 0;
	}
	net_family_write_unlock();
	return err;
}

sock_register在1614行检查net_families表中的套接字系统的协议族表项,然后在1615行将协议族表项插入到net_families表中(在这种情况下,它是netlink协议)。

表net_families是net_proto_family指针的数组,其中所有的协议族都已经注册。表net_families的定义如下所示,其中NPROTO是允许注册的最大协议数目。在内核中其值被设置为32。

static struct net_proto_family *net_families[NPROTO];

内核netlink套接字的创建

在Linux启动时,CPU子系统启动并运行,内存和进程管理开始工作,函数do_basic_setup通过在第713行调用函数sock_init进行网络初始化,如下源码所示:

/*
 * Ok, the machine is now initialized. None of the devices
 * have been touched yet, but the CPU subsystem is up and
 * running, and memory and process management works.
 *
 * Now we can finally start doing some real work..
 */
static void __init do_basic_setup(void)
{
......

	/* Networking initialization needs a process context */ 
	sock_init();
......
}

函数sock_init在第1657行和1658行初始化所有的地址(协议)族。这里,我们对协议模块的初始化感兴趣,尤其时netlink协议。为了初始化netlink协议,在第1697行调用了rtnetlink_init函数来初始化和创建内核netlink套接字。

void __init sock_init(void)
{
......
	 
	for (i = 0; i < NPROTO; i++) 
		net_families[i] = NULL;
......
#ifdef  CONFIG_RTNETLINK
	rtnetlink_init();
#endif
#ifdef CONFIG_NETLINK_DEV
	init_netlink();
#endif
#ifdef CONFIG_NETFILTER
	netfilter_init();
#endif
}

rtnetlink_init在内核中创建一个netlink套接字,以处理用户请求。在第529行以NETLINK_ROUTE和rtnetlink_rcv函数指针为参数调用例程netlink_kernel_create。

void __init rtnetlink_init(void)
{
#ifdef RTNL_DEBUG
	printk("Initializing RT netlink socket\n");
#endif
	rtnl = netlink_kernel_create(NETLINK_ROUTE, rtnetlink_rcv);
......
}

函数netlink_kernel_create首先在第699行调用sock_alloc来分配一个套接字。然后在第702行将套接字类型初始化为SOCK_RAW。

/*
 *	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(int unit, void (*input)(struct sock *sk, int len))
{
	struct socket *sock;
	struct sock *sk;

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

	if (!(sock = sock_alloc())) 
		return NULL;

	sock->type = SOCK_RAW;

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

	netlink_insert(sk, 0);
	return sk;
}

在第704行调用函数netlink_create创建内核netlink套接字,然后在第724行初始化sock结构指针sk为指向套接字结构的套接字对象,这个对象在netlink_create函数中被动态分配的。还初始化sock结构的data_ready函数指针为指向netlink_data_ready函数,然后检查是否向本函数输入了第二个参数;如果是,则在第711行初始化af_netlink->data_ready函数指针为第二个输入参数,该参数是用于netlink协议的rtnetlink_rcv。最后,在第713行调用例程netlink_insert在nl_table中添加该套接字的表项。

用户netlink套接字的创建

用户空间netlink套接字是通过socket系统调用创建的,例如:

fd = socket(AF_NETLINK, SOCK_RAW, protocol);

其中AF_NETLINK是地址族,SOCK_RAW是套接字类型。

netlink套接字支持如下的协议族。

  • NETLINK_ROUTE
  • NETLINK_FIREWALL
  • NETLINK_ARPD
  • NETLINK_ROUTE6
  • NETLINK_IP6_FW
  • NETLINK_TAPBASE

这里讨论NETLINK_ROUTE协议,NETLINK_ROUTE协议用于更新路由表:连接欸设置网络接口的参数;解决网络接口设置IP地址、排队规则、流量分类、流量分类的过滤器、邻居设置以及路由规则设置。它控制了Linux的网络路由系统。

例如,对于NETLINK_ROUTE协议,使用netlink套接字更新路由表的用户命令是ip,排队规则和流量分配的用户命令是tc。

LINK参数消息 link消息允许NETLINK_ROUTE协议用户设置和检索系统的网络接口信息。它由如下的消息类型组成:

  • RTM_NEWLINK
  • RTM_DELLINK
  • RTM_GETLINK

addr消息  addr消息允许NETLINK_ROUTE协议用户设置、取消系统的网络接口上的IP地址。它由如下的消息类型组成:

  • RTM_NEWADDR
  • RTM_DELADDR
  • RTM_GETADDR

ROUTE消息  ROUTE消息允许NETLINK_ROUTE协议用户更新路由表。它由如下消息类型组成:

  • RTM_NEWROUTE
  • RTM_DELROUTE
  • RTM_GETROUTE

QDISC消息  QDISC消息允许NETLINK_ROUTE协议用户在系统的排队规则中添加、删除qdisc。它由如下的消息类型组成:

  • RTM_NEWQDISC
  • RTM_DELQDISC
  • RTM_GETQDISC

CLASS消息  CLASS消息允许NETLINK_ROUTE协议用户从系统的排队规则的qdisc中添加和删除一个类别。它由如下的消息类型组成:

  • RTM_NEWTCLASS
  • RTM_DELTCLASS
  • RTM_GETTCLASS

FILTER消息  FILTER消息允许NETLINK_ROUTE协议用户从系统的排队规则的qdisc的类中添加和删除一个过滤器。它由如下的消息类型组成:

  • RTM_NEWTFILTER
  • RTM_DELTFILTER
  • RTM_GETTFILTER

socket系统调用,它随后由内核执行。socket调用sys_socketcall,该函数然后调用sys_socket,sys_socket又调用sock_create,在本示例中其协议族为netlink,因此sock_create再调用netlink_create。该函数创建套机子并初始化对于套接字的协议操作。初始化dock->ops为netlink_ops,其中netlink_ops是一列将再netlink套接字上进行的各种操作的函数指针。

struct proto_ops netlink_ops = {
	family:		PF_NETLINK,

	release:	netlink_release,
	bind:		netlink_bind,
	connect:	netlink_connect,
	socketpair:	sock_no_socketpair,
	accept:		sock_no_accept,
	getname:	netlink_getname,
	poll:		datagram_poll,
	ioctl:		sock_no_ioctl,
	listen:		sock_no_listen,
	shutdown:	sock_no_shutdown,
	setsockopt:	sock_no_setsockopt,
	getsockopt:	sock_no_getsockopt,
	sendmsg:	netlink_sendmsg,
	recvmsg:	netlink_recvmsg,
	mmap:		sock_no_mmap,
};

netlink数据结构

内核数据结构:

  • nl_table
  • rtnetlink_link

nl_table

nl_table是指向sock结构的指针数组(链接的套接字列表)。其大小设置为MAX_LINKS(32)。再内核中的定义如下。

static struct sock *nl_table[MAX_LINKS];

nl_table数组中每个元素表示一个NETLINK协议族–例如,NETLINK_ROUTE、NETLINK_FIREWALL等。如下图所示,每个netlink协议族包含一个指向套接字(struct sock)链接列表的指针。当用户空间和内核空间netlink套接字要通信时,需要根据协议查找nl_table;并且根据协议,可以在套接字链接列表中搜索与当前进程有相同进程ID的sock。一旦在nl_table的该协议对应的sock列表中找到了某个sock,则将skbuff(包含netlink报文)加入sock接收队列中。

rtnetlink_link

rtnetlink_links定义为指向rtnetlink_link数据结构的指针数组。

struct rtnetlink_link * rtnetlink_links[NPROTO];

每个rtnetlink_link数据结构对应一个rtnetlink命令–例如,RTM_NEWQDISC,该命令用于添加一个新的qdisc。这里rtnetlink_link如下:


struct rtnetlink_link
{
	int (*doit)(struct sk_buff *, struct nlmsghdr*, void *attr);
	int (*dumpit)(struct sk_buff *, struct netlink_callback *cb);
};
  • doit:指向一个函数,该函数根据控制消息中的命令调用。
  • dumpit:指向一个函数,该函数在完成命令或者遇到错误时清除数据。

rtnetlink_links表中的每个表项都对应一个具体的族,例如AF_NETLINK。

在排队规则的情况下,rtnetlink_links在pktsched_init中进行初始化,代码如下:


int __init pktsched_init(void)
{
#ifdef CONFIG_RTNETLINK
	struct rtnetlink_link *link_p;
#endif

#if PSCHED_CLOCK_SOURCE == PSCHED_CPU
	if (psched_calibrate_clock() < 0)
		return -1;
#elif PSCHED_CLOCK_SOURCE == PSCHED_JIFFIES
	psched_tick_per_us = HZ<<PSCHED_JSCALE;
	psched_us_per_tick = 1000000;
#ifdef PSCHED_WATCHER
	psched_tick(0);
#endif
#endif

#ifdef CONFIG_RTNETLINK
	link_p = rtnetlink_links[PF_UNSPEC];

	/* Setup rtnetlink links. It is made here to avoid
	   exporting large number of public symbols.
	 */

	if (link_p) {
		link_p[RTM_NEWQDISC-RTM_BASE].doit = tc_modify_qdisc;
		link_p[RTM_DELQDISC-RTM_BASE].doit = tc_get_qdisc;
		link_p[RTM_GETQDISC-RTM_BASE].doit = tc_get_qdisc;
		link_p[RTM_GETQDISC-RTM_BASE].dumpit = tc_dump_qdisc;
		link_p[RTM_NEWTCLASS-RTM_BASE].doit = tc_ctl_tclass;
		link_p[RTM_DELTCLASS-RTM_BASE].doit = tc_ctl_tclass;
		link_p[RTM_GETTCLASS-RTM_BASE].doit = tc_ctl_tclass;
		link_p[RTM_GETTCLASS-RTM_BASE].dumpit = tc_dump_tclass;
	}
......

rtnetlink_links是一个二维数组,这里对PF_UNSPEC下标的元素(一维数组)进行设置对应的doit和dumpit的函数地址。

类似地,在函数tc_filter_init中初始化用于在类中添加过滤器的排队规则过滤函数指针。代码如下:

int __init tc_filter_init(void)
{
#ifdef CONFIG_RTNETLINK
	struct rtnetlink_link *link_p = rtnetlink_links[PF_UNSPEC];

	/* Setup rtnetlink links. It is made here to avoid
	   exporting large number of public symbols.
	 */

	if (link_p) {
		link_p[RTM_NEWTFILTER-RTM_BASE].doit = tc_ctl_tfilter;
		link_p[RTM_DELTFILTER-RTM_BASE].doit = tc_ctl_tfilter;
		link_p[RTM_GETTFILTER-RTM_BASE].doit = tc_ctl_tfilter;
		link_p[RTM_GETTFILTER-RTM_BASE].dumpit = tc_dump_tfilter;
	}
#endif
......

对于路由,定义该表为inet_rtnetlink_table,且在函数devinet_init中初始化。inet_rtnetlink_table的定义如下:

static struct rtnetlink_link inet_rtnetlink_table[RTM_MAX-RTM_BASE+1] =
{
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},

	{ inet_rtm_newaddr,	NULL,			},
	{ inet_rtm_deladdr,	NULL,			},
	{ NULL,			inet_dump_ifaddr,	},
	{ NULL,			NULL,			},

	{ inet_rtm_newroute,	NULL,			},
	{ inet_rtm_delroute,	NULL,			},
	{ inet_rtm_getroute,	inet_dump_fib,		},
	{ NULL,			NULL,			},

	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},

#ifdef CONFIG_IP_MULTIPLE_TABLES
	{ inet_rtm_newrule,	NULL,			},
	{ inet_rtm_delrule,	NULL,			},
	{ NULL,			inet_dump_rules,	},
	{ NULL,			NULL,			},
#else
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
	{ NULL,			NULL,			},
#endif
};

其他重要的数据结构

nlmsghdr结构

nlmsghdr是一个标准的消息头,netlink协议使用该消息头发送或接收的每个消息。

struct nlmsghdr
{
	__u32		nlmsg_len;	/* Length of message including header */
	__u16		nlmsg_type;	/* Message content */
	__u16		nlmsg_flags;	/* Additional flags */
	__u32		nlmsg_seq;	/* Sequence number */
	__u32		nlmsg_pid;	/* Sending process PID */
};

nlmsg_len 是包括消息头本身在内的消息中数据的总长度。

nlmsg_type定义数据的格式,与netlink消息头一致。

nlmsg_flags定义不同种类的控制标志。

nlmsg_seq由创建netlink请求消息并将这些请求与对应的响应关联的进程使用。

nlmsg_pid是发送进程的进程ID。

msghdr结构

msghdr数据结构包含将传输到内核的netlink消息中。

struct msghdr {
	void	*	msg_name;	/* Socket name			*/
	int		msg_namelen;	/* Length of name		*/
	struct iovec *	msg_iov;	/* Data blocks			*/
	__kernel_size_t	msg_iovlen;	/* Number of blocks		*/
	void 	*	msg_control;	/* Per protocol magic (eg BSD file descriptor passing) */
	__kernel_size_t	msg_controllen;	/* Length of cmsg list */
	unsigned	msg_flags;
};

msg_iov是一个iovec类型的指针,定义如下:

struct iovec
{
	void *iov_base;		/* BSD uses caddr_t (1003.1g requires void *) */
	__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};

iovec结构由两个元素组成:数据指针和数据长度。

iov_base指向netlink保温(netlink消息头加上数据)。

iov_len包含将要传输到内核的报文长度。

netlink报文格式

在排队规则情况下的netlink套接字格式如下所示。在将netlink套接字传递到内核之前,其参数必须按照上述格式进行填充。根据填充的参数,特定的内核模块才能采取恰当的行动。

对于路由表的情况,只将结构tcmsg替换成rtmsg。因此,netlink关于排队规则的报文由如下部分组成。

  • 结构nlnsghdr:netlink消息头。
  • 结构tcmsg:用于设置类、qdisc类型和过滤器。
  • 结构rtattr和属性(将传递到缓冲区的参数)。

netlink套接字例子–添加qdisc的tc命令

这里将介绍netlink套接字是如何用于tc命令实现的,如tc qdisc add dev eth0 root handle 1:0 cbq bandwidth 10mbit

用户空间中田间qdisc的tc命令流

下图展示了tc命令用户空间流程图并列出详细介绍tc命令用户空间流程代码。图中可以清楚的看出request和msghdr结构是如何分配的。分配这些结构后,根据request和msghdr结构的内容调用sendmsg 系统调用进入内核模式。

对应的代码链接:


tc源码

tc用户态源码分析

main

int main(int argc, char **argv)
{
......
	ret = do_cmd(argc-1, argv+1);
Exit:
	rtnl_close(&rth);

	if (use_names)
		cls_names_uninit();

	return ret;
}

main=>do_cmd

static int do_cmd(int argc, char **argv)
{
	if (matches(*argv, "qdisc") == 0)
		return do_qdisc(argc-1, argv+1);
	if (matches(*argv, "class") == 0)
		return do_class(argc-1, argv+1);
	if (matches(*argv, "filter") == 0)
		return do_filter(argc-1, argv+1);
	if (matches(*argv, "chain") == 0)
		return do_chain(argc-1, argv+1);
	if (matches(*argv, "actions") == 0)
		return do_action(argc-1, argv+1);
	if (matches(*argv, "monitor") == 0)
		return do_tcmonitor(argc-1, argv+1);
	if (matches(*argv, "exec") == 0)
		return do_exec(argc-1, argv+1);
	if (matches(*argv, "help") == 0) {
		usage();
		return 0;
	}

	fprintf(stderr, "Object \"%s\" is unknown, try \"tc help\".\n",
		*argv);
	return -1;
}

main=>do_cmd=>do_qdisc

int do_qdisc(int argc, char **argv)
{
	if (argc < 1)
		return tc_qdisc_list(0, NULL);
	if (matches(*argv, "add") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, NLM_F_EXCL|NLM_F_CREATE, argc-1, argv+1);
	if (matches(*argv, "change") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, 0, argc-1, argv+1);
	if (matches(*argv, "replace") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, NLM_F_CREATE|NLM_F_REPLACE, argc-1, argv+1);
	if (matches(*argv, "link") == 0)
		return tc_qdisc_modify(RTM_NEWQDISC, NLM_F_REPLACE, argc-1, argv+1);
	if (matches(*argv, "delete") == 0)
		return tc_qdisc_modify(RTM_DELQDISC, 0,  argc-1, argv+1);
	if (matches(*argv, "list") == 0 || matches(*argv, "show") == 0
	    || matches(*argv, "lst") == 0)
		return tc_qdisc_list(argc-1, argv+1);
	if (matches(*argv, "help") == 0) {
		usage();
		return 0;
	}
	fprintf(stderr, "Command \"%s\" is unknown, try \"tc qdisc help\".\n", *argv);
	return -1;
}

main=>do_cmd=>do_qdisc=>tc_qdisc_modify

static int tc_qdisc_modify(int cmd, unsigned int flags, int argc, char **argv)
{

	struct qdisc_util *q = NULL;
	struct tc_estimator est = {};
	struct {
		struct tc_sizespec	szopts;
		__u16			*data;
	} stab = {};
	char  d[IFNAMSIZ] = {};
	char  k[FILTER_NAMESZ] = {};
	struct {
		struct nlmsghdr	n;
		struct tcmsg		t;
		char			buf[TCA_BUF_MAX];
	} req = {
		.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)),
		.n.nlmsg_flags = NLM_F_REQUEST | flags,
		.n.nlmsg_type = cmd,
		.t.tcm_family = AF_UNSPEC,
	};
......

	if (rtnl_talk(&rth, &req.n, NULL) < 0)
		return 2;

	return 0;
}

main=>do_cmd=>do_qdisc=>tc_qdisc_modify=>rtnl_talk

int rtnl_talk(struct rtnl_handle *rtnl, struct nlmsghdr *n,
	      struct nlmsghdr **answer)
{
	return __rtnl_talk(rtnl, n, answer, true, NULL);
}

main=>do_cmd=>do_qdisc=>tc_qdisc_modify=>rtnl_talk=>__rtnl_talk

static int __rtnl_talk(struct rtnl_handle *rtnl, struct nlmsghdr *n,
		       struct nlmsghdr **answer,
		       bool show_rtnl_err, nl_ext_ack_fn_t errfn)
{
	struct iovec iov = {
		.iov_base = n,
		.iov_len = n->nlmsg_len
	};

	return __rtnl_talk_iov(rtnl, &iov, 1, answer, show_rtnl_err, errfn);
}

main=>do_cmd=>do_qdisc=>tc_qdisc_modify=>rtnl_talk=>__rtnl_talk=>__rtnl_talk_iov

static int __rtnl_talk_iov(struct rtnl_handle *rtnl, struct iovec *iov,
			   size_t iovlen, struct nlmsghdr **answer,
			   bool show_rtnl_err, nl_ext_ack_fn_t errfn)
{
	struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
	struct iovec riov;
	struct msghdr msg = {
		.msg_name = &nladdr,
		.msg_namelen = sizeof(nladdr),
		.msg_iov = iov,
		.msg_iovlen = iovlen,
	};
	unsigned int seq = 0;
	struct nlmsghdr *h;
	int i, status;
	char *buf;

	for (i = 0; i < iovlen; i++) {
		h = iov[i].iov_base;
		h->nlmsg_seq = seq = ++rtnl->seq;
		if (answer == NULL)
			h->nlmsg_flags |= NLM_F_ACK;
	}

	status = sendmsg(rtnl->fd, &msg, 0);
......
}

内核空间的tc命令

sys_sendmsg   系统调用sendmsg在内核空间中调用本函数。sys_sendmsg的主要参数是struct msghdr *msg。msg结构包含指向netlink报文的指针(struct req)。sys_sendmsg创建一个与用户空间中struct msghdr *msg结构体相同的新数据结构,新数据结构在第1334行定义为msg_sys。

然后在第1338行使用copy_from_user,将用户空间msg结构中的每个元素都复制到内核空间新的数据结构msg_sys中。msg_sys的iovec元素包含一个指向netlink套接字的指针,在第1376行调用函数verify_iovec来对其进行验证和赋值。最后,以msg_sys为参数调用sock_sendmsg。

sys_sendmsg

asmlinkage long sys_sendmsg(int fd, struct msghdr *msg, unsigned flags)
{
	struct socket *sock;
	char address[MAX_SOCK_ADDR];
......
	if (copy_from_user(&msg_sys,msg,sizeof(struct msghdr)))
		goto out; 

	sock = sockfd_lookup(fd, &err);
	if (!sock) 
		goto out;

......
	/* This will also move the address data into kernel space */
	err = verify_iovec(&msg_sys, iov, address, VERIFY_READ);
......
	err = sock_sendmsg(sock, &msg_sys, total_len);

......
}

sock_sendmsg    sock_sendmsg创建了一个scm_cookie类型的数据结构对象。其主要用途是保存套接字控制消息的信息(进程的uid、gid和pid等)。在第506行调用函数scm_send来初始化scm_cookie数据结构。最后,在第508行调用函数指针sendmsg。这里操作指针指向netlink_ops数据结构,而netlink_ops中的sendmsg指向netlink_sendmsg,从而调用netlink_sendmsg。

sys_sendmsg=>sock_sendmsg

int sock_sendmsg(struct socket *sock, struct msghdr *msg, int size)
{
	int err;
	struct scm_cookie scm;

	err = scm_send(sock, msg, &scm);
	if (err >= 0) {
		err = sock->ops->sendmsg(sock, msg, size, &scm);
		scm_destroy(&scm);
	}
	return err;
}

sys_sendmsg=>sock_sendmsg=>netlink_sendmsg


static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, int len,
			   struct scm_cookie *scm)
{
......
	skb = alloc_skb(len, GFP_KERNEL);
	if (skb==NULL)
		goto out;
......
	if (memcpy_fromiovec(skb_put(skb,len), msg->msg_iov, len)) {
		kfree_skb(skb);
		goto out;
	}
......
	if (dst_groups) {
		atomic_inc(&skb->users);
		netlink_broadcast(sk, skb, dst_pid, dst_groups, GFP_KERNEL);
	}
	err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

out:
	return err;
}

netlink_unicast在第396行从sock结构中获取套接字的协议(作为参数ssk->protocol传递)。然后调用函数netlink_lookup来从全局netlink表(如nl_table)中查找相应的链表。随后根据套接字创建时定义的模式,调用add_wait_queue将当前进程键入到套接字的等待队列中,并将该进程的状态设置为TASK_INTERRUPTIBLE。之后会重复检查是否可以运行当前进程;如果没有负载,则在第434行将当前进程的状态改变为TASK_RUNNING。最后,在第447行将sk_buff加入到套接字的接收队列中,并在第448行调用函数sk->data_ready(sk, len)。该函数指针为netlink_data_ready函数。

sys_sendmsg=>sock_sendmsg=>netlink_sendmsg=>netlink_unicast

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
{
......
	int protocol = ssk->protocol;
......
	sk = netlink_lookup(protocol, pid);
......
		__set_current_state(TASK_INTERRUPTIBLE);
		add_wait_queue(&sk->protinfo.af_netlink->wait, &wait);
......
		remove_wait_queue(&sk->protinfo.af_netlink->wait, &wait);
......
	skb_queue_tail(&sk->receive_queue, skb);
	sk->data_ready(sk, len);
	sock_put(sk);
	return len;

no_dst:
	kfree_skb(skb);
	return -ECONNREFUSED;
}

sys_sendmsg=>sock_sendmsg=>netlink_sendmsg=>netlink_unicast=>netlink_data_ready

void netlink_data_ready(struct sock *sk, int len)
{
	if (sk->protinfo.af_netlink->data_ready)
		sk->protinfo.af_netlink->data_ready(sk, len);

......
}

sys_sendmsg=>sock_sendmsg=>netlink_sendmsg=>netlink_unicast=>netlink_data_ready=>rtnetlink_rcv

static void rtnetlink_rcv(struct sock *sk, int len)
{
	do {
		struct sk_buff *skb;

		if (rtnl_shlock_nowait())
			return;

		while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {
			if (rtnetlink_rcv_skb(skb)) {
				if (skb->len)
					skb_queue_head(&sk->receive_queue, skb);
				else
					kfree_skb(skb);
				break;
			}
			kfree_skb(skb);
		}

		up(&rtnl_sem);
	} while (rtnl && rtnl->receive_queue.qlen);
}

rtnetlink_rcv_skb   rtnetlink_rcv_skb在第411行将skb->data指针强制转换成结构nlmsghdr类型,nlmsghdr结构是netlink报文头结构,这个skb->data是netlink报文的起始地址。然后rtnetlink_rcv_skb在第417行以netlink报文头结构作为参数之一调用函数rtnetlink_rcv_msg。

sys_sendmsg=>sock_sendmsg=>netlink_sendmsg=>netlink_unicast=>netlink_data_ready=>rtnetlink_rcv=>rtnetlink_rcv_skb

extern __inline__ int rtnetlink_rcv_skb(struct sk_buff *skb)
{
	int err;
	struct nlmsghdr * nlh;

	while (skb->len >= NLMSG_SPACE(0)) {
		u32 rlen;

		nlh = (struct nlmsghdr *)skb->data;
		if (nlh->nlmsg_len < sizeof(*nlh) || skb->len < nlh->nlmsg_len)
			return 0;
		rlen = NLMSG_ALIGN(nlh->nlmsg_len);
		if (rlen > skb->len)
			rlen = skb->len;
		if (rtnetlink_rcv_msg(skb, nlh, &err)) {
			/* Not error, but we must interrupt processing here:
			 *   Note, that in this case we do not pull message
			 *   from skb, it will be processed later.
			 */
			if (err == 0)
				return -1;
			netlink_ack(skb, nlh, err);
		} else if (nlh->nlmsg_flags&NLM_F_ACK)
			netlink_ack(skb, nlh, 0);
		skb_pull(skb, rlen);
	}

	return 0;
}

rtnetlink_rcv_msg  rtnetlink_rcv_msg首先在第295行和第305行从作为输入参数传递给本地函数的netlink报文(nlh)中获取netlink套接字的类型和族。doit和dunmpit函数指针存储在rtnetlink_links表中的rtnetlink_link中。设置族和类型在tc中(tc的用户空间代码)。最后,根据族的行和类型的列,在第384行调用doit函数。这里是以添加一个qdisc为例,因此调用的函数时tc_qdisc_modify。

sys_sendmsg=>sock_sendmsg=>netlink_sendmsg=>netlink_unicast=>netlink_data_ready=>rtnetlink_rcv=>rtnetlink_rcv_skb=>rtnetlink_rcv_msg


/* Process one rtnetlink message. */

extern __inline__ int
rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh, int *errp)
{
......

	type = nlh->nlmsg_type;
......
	type -= RTM_BASE;
......

	family = ((struct rtgenmsg*)NLMSG_DATA(nlh))->rtgen_family;
......
	link_tab = rtnetlink_links[family];
	if (link_tab == NULL)
		link_tab = rtnetlink_links[PF_UNSPEC];
	link = &link_tab[type];
......
	if (link->doit == NULL)
		link = &(rtnetlink_links[PF_UNSPEC][type]);
	if (link->doit == NULL)
		goto err_inval;
	err = link->doit(skb, nlh, (void *)&rta);

......
}

内核空间中的tc命令流程图

内核空间的tc命令流程如下图。



版权声明:本文为guoguangwu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。