初试Netlink之unicast

        在之前已经了解了好几种内核态与用户态之间数据的交换方法,但那些方法都是单向的(即单工),其关系是一对一的,而Netlink Socket则是一种用于内核与用户空间之间传递信息的特殊IPC,在用户进程中,其以标准的Socket API为内核和用户之间提供了全双工的通信通道,在内核模块中则提供了一类特殊的API。相比TCP/IP socket所使用的AF_INET地址族,Netlink socket则使用AF_NETLINK地址族,每一个Netlink Socket都在Kernel的include/uapi/linux/netlink.h(linux-3.11.0-rc4,关于Netlink部分相关文档,如无注明均以该版本为基准)中定义了相应的protocol type。
        在Linux  Kernel中,以模块形式去创建Netlink Socket时需要涉及到如下相关的API及数据结构:
        1.创建Socket
           在include/linux/netlink.h文件中,有如下的定义:
           /* 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;
                   void            (*bind)(int group);
                   bool            (*compare)(struct net *net, struct sock *sk);
           };
           extern struct sock *__netlink_kernel_create(struct net *net, int unit,
                                                       struct module *module,
                                                       struct netlink_kernel_cfg *cfg);
           static inline struct sock *
           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的第一个参数net一般使用&init_net这个全局量;第二个参数unit可使用include/uapi/linux/netlink.h文件中以NETLINK_开头的宏定义,其对应Netlink支持的协议类型,也可以是自定义的类型(可自行添加相应的宏定义,等会的例子里会使用),而cfg对应上面的可选配置参数结构体(等会例子会使用到input,用于接收到数据时的回调处理,其对应的skb为接收到的数据对应的结构体,其他量在后期使用时再说明)。
        2.数据处理
           当上面的socket创建好后,其就会进行监听对应协议的数据,当收到数据时会调用上面的input回调函数,而其参数skb为接收到的struct sk_buff结构体,使用include/linux/netlink.h的如下函数获取struct nlmsghdr结构体指针值:
           static inline struct nlmsghdr *nlmsg_hdr(const struct sk_buff *skb)
           {
                   return (struct nlmsghdr *)skb->data;
           }
           而struct nlmsghdr结构体在include/uapi/linux/netlink.h文件中有如下声明:
           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 */
           };     
           通过同一文件的如下宏:
           #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)))
           我们可以按需获得相应的参数数据,通过NLMSG_DATA宏即可获得接收到的数据内容。上述内容我们可以从获取到的sk_buff提取nlmsghdr后利用NLMSG_DATA宏来获取接收的数据内容,接下来我们看下如何构建要发送的数据和操作:
           通过include/net/netlink.h文件中的如下函数:
           static inline struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)
           {
                   return alloc_skb(nlmsg_total_size(payload), flags);
           }
           static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
                                                    int type, int payload, int flags)
           {
                   if (unlikely(skb_tailroom(skb) < nlmsg_total_size(payload)))
                           return NULL;
                  return __nlmsg_put(skb, portid, seq, type, payload, flags);
           }
           来构建要发送的sk_buff和填充相应的nlmsghdr数据,最终使用include/linux/netlink.h中的如下函数:
           extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
           或include/net/netlink.h中的如下函数:
           static inline int nlmsg_unicast(struct sock *sk, struct sk_buff *skb, u32 portid)
           {
                   int err;            
                  err = netlink_unicast(sk, skb, portid, MSG_DONTWAIT);
                  if (err > 0)
                          err = 0;
                  return err;
           }
           进行单播发送数据,在接下来的例子我们调用nlmsg_put时会设置portid为0,该处表示发送进程的端口号(因为是内核,默认设置为0,会与应用层处配置对应),而上面的netlink_unicast的portid为接收进程的端口号(下面的实例会使用传进来的pid),注意这两者的区别。
        上面了解了Kernel部分的操作后,下面了解下用户态应用程序的处理所需:
        1.创建及绑定socket
            使用int socket(int domain, int type, int protocol)函数来创建一个socket,其中domain为AF_NETLINK,type只能为SOCK_RAW或SOCK_DGRAM(因为Netlink是基于数据报的传输,类似UDP),protocol为使用到的Netlink Socket协议(可以是自定义的,在下面例子会使用自定义的)。
           在创建好socket后需要将其绑定bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr),其中fd为上面socket的返回值,而nladdr为struct 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 */
           };
           最终会将其转换为sockaddr类型的指针,相应的在include/linux/socket.h文件中有如下声明:
           struct sockaddr {
                   sa_family_t     sa_family;      /* address family, AF_xxx       */
                   char            sa_data[14];    /* 14 bytes of protocol address */
           };
        2.数据处理
           在上面创建好socket和绑定好本地信息后,接下来要构建发送给接收端的struct msghdr结构体数据,该结构体在include/linux/socket.h有如下声明:
           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 int    msg_flags;
           };
           对于该结构体,我们需要填充msg_name和msg_iov,具体在下面实例中可以看到填充什么数据,其中iovec结构体在include/uapi/linux/uio.h文件中有声明:
           struct iovec
           {
                   void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
                   __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
           };
           在构建好数据后,我们使用如下函数进行数据收发:
           ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
           ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
        在上述分析后,接下来我们实例感受下通过Netlink Socket进行内核态与用户态之间进行通信的过程:
        对于Kernel部分netlink_unicast.c的源码如下:
        #include <linux/init.h>
        #include <linux/module.h>
        #include <net/sock.h>
        #include <net/netlink.h>
        #define NETLINK_TEST 25
        #define MAX_PAYLOAD_SIZE 1024
        struct sock *nl_sk = NULL;
        void sendnlmsg(int pid)
        {
            struct sk_buff *skb;
            struct nlmsghdr *nlh;
            char msg[30] = "Say hello from kernel!";
            if(!nl_sk){
                return;
            }
            skb = nlmsg_new(MAX_PAYLOAD_SIZE, GFP_KERNEL);
            if(!skb){
                printk(KERN_ERR "nlmsg_new error!\n");
            }
            nlh = nlmsg_put(skb, 0, 0, 0, MAX_PAYLOAD_SIZE, 0);
            memcpy(NLMSG_DATA(nlh), msg, sizeof(msg));
            printk("Send message '%s'.\n",(char *)NLMSG_DATA(nlh));
            netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
        }
        void nl_data_ready(struct sk_buff *__skb)
        {
            struct sk_buff *skb;
            struct nlmsghdr *nlh;
            char str[100];
            skb = skb_get (__skb);
            if(skb->len >= NLMSG_SPACE(0)){
                nlh = nlmsg_hdr(skb);
                memcpy(str, NLMSG_DATA(nlh), sizeof(str));
                printk("Message received:%s\n",str) ;
                sendnlmsg(nlh->nlmsg_pid);
                kfree_skb(skb);
            }
        }
        static int netlink_unicast_init(void)
        {
            struct netlink_kernel_cfg netlink_kerncfg = {
                .input = nl_data_ready,
            };
            nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &netlink_kerncfg);
            if(!nl_sk){
                printk(KERN_ERR "netlink_unicast_init: Create netlink socket error.\n");
                return -1;
            }
            printk("netlink_unicast_init: Create netlink socket ok.\n");
            return 0;
        }
        static void netlink_unicast_exit(void)
        {
            if(nl_sk != NULL){
                sock_release(nl_sk->sk_socket);
            }
            printk("netlink_unicast_exit!\n");
        }
        module_init(netlink_unicast_init);
        module_exit(netlink_unicast_exit);
        MODULE_AUTHOR("X-SLAM XINU");
        MODULE_LICENSE("GPL");
        相应的Makefile内容如下:
        obj-m += netlink_unicast.o
        CUR_PATH:=$(shell pwd)
        LINUX_KERNEL_PATH:=/home/guochongxin/xinu/linux-stable/
        all:
               make -C $(LINUX_KERNEL_PATH) M=$(CUR_PATH) modules
        clean:
               make -C $(LINUX_KERNEL_PATH) M=$(CUR_PATH) clean
        而对于用户层应用程序netlink_unicast_app.c文件的源码如下:
        #include <stdio.h>
        #include <stdlib.h>
        #include <errno.h>
        #include <string.h>
        #include <sys/socket.h>
        #include <sys/types.h>
        #include <linux/netlink.h>
        #define NETLINK_TEST 25
        #define MAX_PAYLOAD_SIZE 1024 // maximum payload size
        int main(int argc, char* argv[])
        {
            int state;
           struct sockaddr_nl src_addr, dest_addr;
           struct nlmsghdr *nlh = NULL;
           struct iovec iov;
           struct msghdr msg;
           int sock_fd, retval;
           int state_smg = 0;
           // Create a socket
            sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
            if(sock_fd == -1){
                printf("error getting socket: %s", strerror(errno));
                return -1;
            }
            // To prepare binding
           memset(&msg,0,sizeof(msg));
           memset(&src_addr, 0, sizeof(src_addr));
            src_addr.nl_family = AF_NETLINK;
            src_addr.nl_pid = getpid();
            src_addr.nl_groups = 0;
            retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
            if(retval < 0){
                printf("bind failed: %s", strerror(errno));
                close(sock_fd);
                return -1;
            }
            // To prepare recvmsg
            nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD_SIZE));
            if(!nlh){
                printf("malloc nlmsghdr error!\n");
                close(sock_fd);
                return -1;
            }
            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->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD_SIZE);
            nlh->nlmsg_pid = getpid();
            nlh->nlmsg_flags = 0;
           strcpy(NLMSG_DATA(nlh), "Say hello from user application!");
           iov.iov_base = (void *)nlh;
           iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD_SIZE);
            memset(&msg, 0, sizeof(msg));
            msg.msg_name = (void *)&dest_addr;
            msg.msg_namelen = sizeof(dest_addr);
            msg.msg_iov = &iov;
           msg.msg_iovlen = 1;
            state_smg = sendmsg(sock_fd, &msg, 0);
           if(state_smg == -1){
                printf("get error sendmsg = %s\n",strerror(errno));
            }
            memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD_SIZE));
            printf("waiting received!\n");
            // Read message from kernel
           state = recvmsg(sock_fd, &msg, 0);
            if(state < 0){
                printf("recvmsg state < 1");
           }
            printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
            close(sock_fd);
           return 0;
        }
        相应的Makefile文件内容如下:
        all:
                gcc -o netlink_unicast_app netlink_unicast_app.c
        clean:
                rm -rf *.o netlink_unicast_app
        最终的源码文件目录树如下:
        /home/guochongxin/xinu/xinu/linux_kernel_driver_l1/netlink_unicast/
        ├── kernel_module
        │   ├── Makefile
        │   └── netlink_unicast.c
        └── user_application
           ├── Makefile
            └── netlink_unicast_app.c
        当我们先编译加载内核模块sudo insmod netlink_unicast.ko时,dmesg命令有如下输出:
        netlink_unicast_init: Create netlink socket ok.
        当我们运行应用程序./netlink_unicast_app时,Terminal有如下输出:
        waiting received!
        Received message: Say hello from kernel!
        而当我们再使用dmesg命令查看时会有如下输出:
        Message received:Say hello from user application!
        Send message 'Say hello from kernel!'.
        至此,使用Netlink Socket的Unicast方式通信能正常使用了,以后内核态与用户态之间自由交换意见就更容易了,接下来我们会讲到Broadcast等方式的通信,静候更新。
        参考网址:http://www.cnblogs.com/D3Hunter/p/3207670.html
                        http://blog.csdn.net/wangpengqi/article/details/9969599
                        http://www.cnblogs.com/hoys/archive/2011/04/09/2010788.html
                        http://binwaheed.blogspot.com/2010/08/after-reading-kernel-source-i-finally.html
                        http://ycool.com/post/by6vz7j
                        http://blog.csdn.net/vichie2008/article/details/37568065
                        http://www.linuxjournal.com/article/7356?page=0,3

评论

此博客中的热门博文

I/O映射之I/O端口

通过Netlink检测网线插拔

使用seq_file