linux netlink機制詳解

netlink是一種基於網絡的機制,允許在內核內部以及內核與用戶層之間進行通信。最早在內核2.2引入,旨在替代笨拙的IOCTL,IOCTL不能從內核向用戶空間發送異步消息,而且必須定義IOCTL號。

Netlink協議定義在RFC3549中。以前是可以編譯成模塊,現在直接集成到內核了。與profs和sysfs相比,有一些優勢如下:

不需要輪詢;系統調用和ioctl也能從用戶層想內核傳遞信息,但是難以實現,另外netlink不會和模塊衝突;內核可以直接向用戶層發送信息;使用標準的套接字即可。

/proc/net/netlink文件中包含了當前活動的netlink連接信息。

            代碼位於net/netlink中。

af_netlink.c  af_netlink.h  diag.c  genetlink.c

            其中genetlink提供通用的Netlink API,af_netlink提供了套接字API,diag是監視接口提供用於讀寫有關Netlink套接字的信息。

1 Netlink子系統初始化

通過函數netlink_proto_init(net/netlink/af_netlink.c)向內核註冊協議,註冊的


static struct proto netlink_proto = {
	.name	  = "NETLINK",
	.owner	  = THIS_MODULE,
	.obj_size = sizeof(struct netlink_sock),
};
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,
};

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

static int __init netlink_proto_init(void)
{
	int i;
	unsigned long limit;
	unsigned int order;
    //這邊註冊的netlink_proto 是和tcp 以及udp等傳輸層協議同一個層次的結構,只不過這邊
    //netlink 只是申明瞭這個結構,並沒有具體實現
	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;

	if (totalram_pages >= (128 * 1024))
		limit = totalram_pages >> (21 - PAGE_SHIFT);
	else
		limit = totalram_pages >> (23 - PAGE_SHIFT);

	order = get_bitmask_order(limit) - 1 + PAGE_SHIFT;
	limit = (1UL << order) / sizeof(struct hlist_head);
	order = get_bitmask_order(min(limit, (unsigned long)UINT_MAX)) - 1;

	for (i = 0; i < MAX_LINKS; i++) {
		struct nl_portid_hash *hash = &nl_table[i].hash;

		hash->table = nl_portid_hash_zalloc(1 * sizeof(*hash->table));
		if (!hash->table) {
			while (i-- > 0)
				nl_portid_hash_free(nl_table[i].hash.table,
						 1 * sizeof(*hash->table));
			kfree(nl_table);
			goto panic;
		}
		hash->max_shift = order;
		hash->shift = 0;
		hash->mask = 0;
		hash->rehash_time = jiffies;
	}

	netlink_add_usersock_entry();
    //netlink_family_ops 結構是和ipv4的網絡層結構inet以及ipv6 的inet6 平行的一個結構
	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");
}

從上面可以看出,netlink的實現平行於 ipv4或者ipv6網絡層協議族,但是沒有相對應的傳輸層實現。所以對應的收發函數都在netlink_ops 中,不會再分層傳遞。sock_register(源碼:sock_register(&netlink_family_ops);)向內核註冊協議處理函數,即將netlink的socket創建處理函數註冊到內核中,以後應用層創建netlink類型的socket時將會調用該協議處理函數,每次創建PF_NETLINK(AF_NETLINK)類型的socket()系統調用時,將由netlink_create()函數負責處理。

版權聲明:本文爲CSDN博主「badman250」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/notbaron/article/details/79851241

創建並初始化了nl_table表數組,這個表是整個netlink實現的關鍵一步,每種協議類型佔數組中的一項,後續內核中創建的不同種協議類型的netlink都將保存在這個表中,由該表統一維護,該表結構如下

struct netlink_table {
	struct nl_portid_hash	hash;
	struct hlist_head	mc_list;
	struct listeners __rcu	*listeners;
	unsigned int		flags;
	unsigned int		groups;
	struct mutex		*cb_mutex;
	struct module		*module;
	void			(*bind)(int group);
	int			registered;
};

2 使用

Netlink套接字可以是SOCK_RAW套接字,也可以是SOCK_DGRAM套接字。內核和用戶空間都可以使用Netlink套接字,只是調用的方法不同,用戶空間使用傳統的socket系統調用,內核態使用netlink_kernel_create函數。最終都會調用__netlink_create方法。

 

然後創建一個sockaddr_nl結構來表示用戶空間或內核Netlink套接字的地址。

開發使用Netlink套接字來收發數據的用戶空間應用程序時,推薦使用libnl API。Libnl包還包含支持通用Netlink簇、路由選擇簇和Netfilter簇的庫。

            Netlink套接字不僅用於網絡子系統,還用於其他子系統如:SELinux、audit、uevent等。

            Netlink採用地址編碼,struct sockaddr_nl,每個通過netlink發出的消息都必須附帶一個netlink自己的消息頭(struct nlmsghdr)。

            下面來看下相關的數據結構


3 數據結構

所有socket之間的通信,必須有個地址結構,netlink的地址結構如下:

sockaddr_nl定義在include/uapi/linux/netlink.h文件中。

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 */
};

在內核網絡棧中,可創建多種Netlink套接字,每種內核套接字可處理不同的類型消息。例如,NETLINK_ROUTE消息,通過和NETLINK_ROUTE協議通信,可以獲得內核的路由信息。現在支持到了23個種類,用戶也可以添加其他的種類來實現自己特定的netlink 機制。

Netlink_kernel_cfg結構體包含用於創建Netlink套接字的可選參數, 是內核netlink配置結構。其中input函數就是內核用來處理對應的netlink消息的接收函數

struct netlink_kernel_cfg {
	unsigned int	groups;
	unsigned int	flags;
	void		(*input)(struct sk_buff *skb);
	struct mutex	*cb_mutex;
	void		(*bind)(int group);
};

4 消息格式

在用戶空間和內核空間進行交換時候,必須採用特定的格式。消息的開頭是長度固定的netlink報頭。

報頭的結構體爲nlmsghdr結構體,共16個字節。

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 port ID */
};

整個消息長度,包括首部和任何所需的填充字節,在nlmsg_len。nlmsg_pid是發送消息的用戶程序進程PID。nlmsg_seq序列號,用於排列消息,並不是必須的。nlmsg_flags例如是NLM_F_REQUEST。nlmsg_type表示消息類型,如NLMMSG_ERROR發生了錯誤。

netlink的消息頭後面跟着的是消息的有效載荷部分,它採用的是格式爲“類型——長度——值”,簡寫TLV。其中類型和長度使用屬性頭nlattr來表示。其中nla_len表示屬性長度;nla_type表示屬性類型,取值定義在include\net\netlink.h中。

 netlink屬性頭是struct nlattr,定義在include/uapi/linux/netlink.h中,

struct nlattr {
        __u16           nla_len;
        __u16           nla_type;
};

幾個重要的數據數據結構關係如下:

 5 使用

使用Netlink的方法如下,先運行netlink內核模塊;然後運行用戶態程序,向內核發送連接消息,通知內核用戶的進程id;內核接收用戶消息,記錄其進程id;內核向用戶進程id發送netlink消息;用戶接收內核發送的netlink消息。Ok,整體流程這樣。

此外,爲了獲取netlink報文中數據的方便,netlink提供了下面幾個宏進行數據的獲取和解包操作,定義在include/uapi/linux/netlink.h

#define NLMSG_ALIGNTO   4U
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
#define NLMSG_NEXT(nlh,len)      ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
                                  (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
                           (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
                           (nlh)->nlmsg_len <= (len))
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

從用戶空間接收的數據將由netlink_kernel_cfg結構體中的input指定函數來處理。

下面舉一個例子來說明內核和用戶層之間大概是如何通信的:

5.1 內核代碼

內核代碼中初始化一個自定義協議NETLINK_USER並初始化,然後指定數據回調函數hello_nl_recv_msg,該函數只是簡單發送一個字符串“hello,from kernel”,如下:

#include <linux/module.h>

#include <net/sock.h>

#include <linux/netlink.h>

#include <linux/skbuff.h>


#define NETLINK_USER 31     //the user defined channel, the key factor

struct sock *nl_sk = NULL;

static void hello_nl_recv_msg(struct sk_buff *skb)

{

    struct nlmsghdr *nlh;

    int pid;

    struct sk_buff *skb_out;

    int msg_size;

    char *msg="hello,from kernel";

    int res;

    printk(KERN_INFO "Entering: %s\n", __FUNCTION__);

    msg_size=strlen(msg);

    //for receiving...

    nlh=(struct nlmsghdr*)skb->data;    //nlh message comes from skb's data... (sk_buff: unsigned char *data)

    /*  static inline void *nlmsg_data(const struct nlmsghdr *nlh)

        {

                return (unsigned char *) nlh + NLMSG_HDRLEN;

        }

    nlmsg_data - head of message payload */

    printk(KERN_INFO "Netlink received msg payload: %s\n",(char*)nlmsg_data(nlh));

    //for sending...

    pid = nlh->nlmsg_pid; // Sending process port ID, will send new message back to the 'user space sender'


    skb_out = nlmsg_new(msg_size,0);    //nlmsg_new - Allocate a new netlink message: skb_out


    if(!skb_out)

    {

        printk(KERN_ERR "Failed to allocate new skb\n");

        return;

    }

    nlh=nlmsg_put(skb_out,0,0,NLMSG_DONE,msg_size,0);  

* nlmsg_put - Add a new netlink message to an skb

 * @skb: socket buffer to store message in

 * @portid: netlink PORTID of requesting application

 * @seq: sequence number of message

 * @type: message type

 * @payload: length of message payload

 * @flags: message flags

    //#define NETLINK_CB(skb)           (*(struct netlink_skb_parms*)&((skb)->cb))

    //cb: This is the control buffer. It is free to use for every layer. Please put your private variables there

    /* struct netlink_skb_parms {

        struct ucred            creds;          // Skb credentials

        __u32                   pid;

        __u32                   dst_group;

    }; */

    //map skb->cb (char cb[48] __aligned(8); control buffer) to "struct netlink_skb_parms", so it has field pid/dst_group

    //so there should be convention: cb[48] is divided into creds/pid/dst_group...to convey those info

    NETLINK_CB(skb_out).dst_group = 0;                  /* not in mcast group */

    strncpy(nlmsg_data(nlh),msg,msg_size); //char *strncpy(char *dest, const char *src, size_t count)

//msg "Hello from kernel" => nlh -> skb_out

    res=nlmsg_unicast(nl_sk,skb_out,pid); //nlmsg_unicast - unicast a netlink message

                                         //@pid: netlink pid of the destination socket

    if(res<0)

        printk(KERN_INFO "Error while sending bak to user\n");

}

 

static int __init hello_init(void)

{

    //struct net init_net; defined in net_namespace.c

    //unit=NETLINK_USER: refer to some kernel examples

    //groups = 0, unicast

    //nl_sk: global sock, will be sent to hello_nl_recv_msg as argument (nl_sk ->...-> skb) and return from below func, by Tom Xue, not totally verified

   struct netlink_kernel_cfg cfg = {

     .input = hello_nl_recv_msg,//該函數原型可參考內核代碼,其他參數默認即可

           };

     nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);

    printk("Entering: %s\n",__FUNCTION__);

    if(!nl_sk)

    {

        printk(KERN_ALERT "Error creating socket.\n");

        return -10;

    }


    return 0;

}

static void __exit hello_exit(void)

{

    printk(KERN_INFO "exiting hello module\n");

    netlink_kernel_release(nl_sk);

}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

5.2 用戶態代碼

#include <stdlib.h>

#include <sys/socket.h>

#include <sys/types.h>

#include <string.h>

#include <asm/types.h>

#include <linux/netlink.h>

#include <linux/socket.h>

#include <errno.h>

#define NETLINK_USER 31  //self defined

#define MAX_PAYLOAD 1024 /* maximum payload size*/

struct sockaddr_nl src_addr, dest_addr;

struct nlmsghdr *nlh = NULL;

struct iovec iov;

int sock_fd;

struct msghdr msg;  //msghdr includes: struct iovec *   msg_iov;

void main()
{

    //int socket(int domain, int type, int protocol);

    sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);

    if(sock_fd<0)

        return -1;

    memset(&src_addr, 0, sizeof(src_addr));

    src_addr.nl_family = AF_NETLINK;

    src_addr.nl_pid = getpid(); /* self pid */

    //int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));

    memset(&dest_addr, 0, sizeof(dest_addr));

    dest_addr.nl_family = AF_NETLINK;

    dest_addr.nl_pid = 0;       /* For Linux Kernel */

    dest_addr.nl_groups = 0;    /* unicast */

    //nlh: contains "Hello"

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));

    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));

    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);

    nlh->nlmsg_pid = getpid();  //self pid

    nlh->nlmsg_flags = 0;

    strcpy(NLMSG_DATA(nlh), "Hello");   //put "Hello" into nlh

    iov.iov_base = (void *)nlh;         //iov -> nlh

    iov.iov_len = nlh->nlmsg_len;

    msg.msg_name = (void *)&dest_addr;  //msg_name is Socket name: dest

    msg.msg_namelen = sizeof(dest_addr);

    msg.msg_iov = &iov;                 //msg -> iov

    msg.msg_iovlen = 1;

    printf("Sending message to kernel\n");

    sendmsg(sock_fd,&msg,0);    //msg -> find the (destination) socket name: dest

                                //msg -> iov -> nlh -> "Hello"

    printf("Waiting for message from kernel\n");

    /* Read message from kernel */

    recvmsg(sock_fd, &msg, 0);  //msg is also receiver for read

    printf("Received message payload: %s\n", NLMSG_DATA(nlh));  //msg -> iov -> nlh

    close(sock_fd);

}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章