• linux内核空间与用户空间的通信:基于netlink


    参考自:

    http://blog.chinaunix.net/uid-28541347-id-5578403.html

    https://blog.csdn.net/jasenwan88/article/details/7365060

    https://www.cnblogs.com/oracleloyal/p/5991819.html

    https://blog.csdn.net/kanda2012/article/details/7580623

    https://blog.csdn.net/u012819339/article/details/51334600

    一:Netlink简介

    (一)什么是Netlink?

    Netlink是linux提供的用于内核和用户态进程之间的通信方式。但是注意虽然Netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。只是进程间通信有其他很多方式,一般不用Netlink,除非需要用到Netlink的广播特性时。

    (二)那么Netlink有什么优势呢?

    一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,但是Netlink可以实现双工通信。

    (三)Netlink特点

    Netlink协议基于BSD socket和AF_NETLINK地址簇(address family),使用32位的端口号寻址(以前称作PID),每个Netlink协议(或称作总线,man手册中则称之为netlink family),通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。

    netlink具有以下特点:

     ① 支持全双工、异步通信(当然同步也支持)
     ② 用户空间可使用标准的BSD socket接口(但netlink并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
     ③ 在内核空间使用专用的内核API接口
     ④ 支持多播(因此支持“总线”式通信,可实现消息订阅)
     ⑤ 在内核端可用于进程上下文与中断上下文

    (四)NETLINK对比UDP

    AF_NETLINK和AF_INET对应,是一个协议族,而NETLINK_ROUTE、NETLINK_GENERIC这些是协议,对应于UDP。
    那么我们主要关注Netlink和UDP socket之间的不同点,其中最重要的一点就是:

    使用UDP socket发送数据包时,用户无需构造UDP数据包的包头,内核协议栈会根据原、目的地址(sockaddr_in)填充头部信息。但是Netlink需要我们自己构造一个包头(这个包头有什么用,我们后面再说)。

    一般我们使用Netlink都要指定一个协议,我们可以使用内核为我们预留的NETLINK_GENERIC(定义在linux/netlink.h中),也可以使用我们自定义的协议,其实就是定义一个内核还没有占用的数字。下面我们用NETLINK_TEST做为我们定义的协议写一个例子(注意:自定义协议不一定非要添加到linux/netlink.h中,只要用户态和内核态代码都能找到该定义就行)。我们知道使用UDP发送报文有两种方式:sendto和sendmsg,同样Netlink也支持这两种方式。下面先看使用sendmsg的方式。

    二:用户态数据结构

    首先看一下几个重要的数据结构的关系:

     

    (一)struct msghdr

    msghdr这个结构在socket变成中就会用到,并不算Netlink专有的。

    我们知道socket消息的发送和接收函数一般有这几对:recv/send、readv/writev、recvfrom/sendto。当然还有recvmsg/sendmsg,前面三对函数各有各的特点功能,而recvmsg/sendmsg就是要囊括前面三对的所有功能,当然还有自己特殊的用途。msghdr的前两个成员就是为了满足recvfrom/sendto的功能,中间两个成员msg_iov和msg_iovlen则是为了满足readv/writev的功能,而最后的msg_flags则是为了满足recv/send中flag的功能,剩下的msg_control和msg_controllen则是满足recvmsg/sendmsg特有的功能。

    (二)struct sockaddr_nl

    struct sockaddr_nl为Netlink的地址,和我们通常socket编程中的sockaddr_in作用一样,他们的结构对比如下:

    struct sockaddr_nl{}的详细定义和描述如下:

        struct sockaddr_nl
        {
            sa_family_t nl_family; /*该字段总是为AF_NETLINK */
            unsigned short nl_pad; /* 目前未用到,填充为0*/
            __u32 nl_pid; /* process pid */
            __u32 nl_groups; /* multicast groups mask */
        };

      (1) nl_pid:在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况下nl_pid都设置为当前进程的进程号。前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般指内核。
      (2) nl_groups:如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。

    (三)struct nlmsghdr

    Netlink的报文由消息头和消息体构成,struct nlmsghdr即为消息头。消息头定义在文件里,由结构体nlmsghdr表示:

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

    消息头中各成员属性的解释及说明:
     (1) nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。
     (2) nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下:
       a) NLMSG_NOOP-空消息,什么也不做;
       b) NLMSG_ERROR-指明该消息中包含一个错误;
       c) NLMSG_DONE-如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
       d) NLMSG_OVERRUN-暂时没用到。
     (3) nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。
    那消息体怎么设置呢?可以使用NLMSG_DATA,具体见后面例子。

    三:内核态实现

    (一)内核netlink api

        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);

       参数说明:

      (1) net:是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量。
      (2) unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。
      (3) groups:多播地址。
      (4) input:为内核模块定义的netlink消息处理函数,当有消 息到达这个netlink socket时,该input函数指针就会被引用,且只有此函数返回时,调用者的sendmsg才能返回。
      (5) cb_mutex:为访问数据时的互斥信号量。
      (6) module: 一般为THIS_MODULE。

    2.发送单播消息netlink_unicast

        int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)

       参数说明:

      (1) ssk:为函数 netlink_kernel_create()返回的socket。
      (2) skb:存放消息,它的data字段指向要发送的netlink消息结构,而 skb的控制块保存了消息的地址信息,宏NETLINK_CB(skb)就用于方便设置该控制块。
      (3) pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为 0。
      (4) nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。

    3.发送广播消息netlink_broadcast

        int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)

      前面的三个参数与 netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。

    void netlink_kernel_release(struct sock *sk)

    (二)代码实现

    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/types.h>
    #include <linux/sched.h>
    #include <linux/netlink.h>
    #include <net/sock.h>
    
    #define NETLINK_USER 22
    #define USER_MSG (NETLINK_USER+1)
    #define USER_PORT 50
    
    static int stringlen(char* buf);
    
    static int send_msg(char* pbuf,int pid);
    static void recv_cb(struct sk_buff* skb);
    
    static struct sock* netlink_fd = NULL;
    
    struct netlink_kernel_cfg cfg = {
        .input = recv_cb,
    };
    
    static int __init test_netlink_init(void)
    {
        printk("init netlink!
    ");
    
        netlink_fd = netlink_kernel_create(&init_net,USER_MSG,&cfg);
        if(!netlink_fd)
        {
            printk(KERN_ERR "can not create a netlink socket!
    ");
            return -1;
        }
    
        printk("netlink init successful!
    ");
        return 0;
    }
    
    static void __exit test_netlink_exit(void)
    {
        printk("exit netlink!
    ");
        sock_release(netlink_fd->sk_socket);
        printk(KERN_DEBUG "netlink exit!
    ");
    }
    
    module_init(test_netlink_init);
    module_exit(test_netlink_exit);
    
    MODULE_AUTHOR("SSYFJ");
    MODULE_LICENSE("GPL");
    
    static void recv_cb(struct sk_buff* __skb)
    {
        struct nlmsghdr* nlh = NULL;
        void* data = NULL;
        int pid;
    
        struct sk_buff* skb = skb_get(__skb);    //通过为原始数据__skb添加引用来获取数据
    
        printk("skb->len:%u
    ",skb->len);
        if(skb->len>=NLMSG_SPACE(0))             //NLMSG_SPACE(0)表示  只有头部的大小
        {
            nlh = nlmsg_hdr(skb);
            printk("nlmsg->len:%u %u
    ",nlh->nlmsg_len,nlmsg_len(nlh));
            data = NLMSG_DATA(nlh);
            if(data)
            {
                printk("kernel receive data:%s
    ",(int8_t*)data);
                pid = nlh->nlmsg_pid;
                send_msg("hello userspace",pid);
            }
         }
    
         kfree_skb(skb);                            //前面引用+1,这里一定要释放,不然会warn
    }
    
    static int stringlen(char* buf)
    {
        int len = 0;
        for(;*buf;buf++)
        {
            len++;
        }
        return len;
    }
    
    
    static int send_msg(char* pbuf,int pid)
    {
        int len = stringlen(pbuf),ret;
        
        struct sk_buff* nl_skb;
        struct nlmsghdr* nlh;
    
        nl_skb = nlmsg_new(len,GFP_ATOMIC);
        if(!nl_skb)
        {
            printk("netlink_alloc_skb error!
    ");
            return -1;
        }
    
        nlh = nlmsg_put(nl_skb,0,0,pid,len,0);
        if(!nlh)
        {
            printk("nlmsg_put() error!
    ");
            nlmsg_free(nl_skb);
            return -1;
        }
    
        memcpy(nlmsg_data(nlh),pbuf,len);
    
        ret = netlink_unicast(netlink_fd,nl_skb,pid,MSG_DONTWAIT);
    
        return ret;
    }

    (三)Makefile文件

    obj-m := kern.o
    KDIR := /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
    default:
        $(MAKE) -C $(KDIR) M=$(PWD) modules

    注意:kern.o与我们编写的代码文件一致

    (四)内核操作

    sudo insmod kern.ko
    sudo rmmod kern

    其中kern是我们编译出来的内核文件,对于ko文件的插入移除,可以通过dmesg查看printk的打印结果

    四:用户态实现

    (一)sendmsg与recvmsg方法的用户态实现

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/netlink.h>
    
    #define NETLINK_USER 22                //从22开始到32可以自定义Netlink通信宏
    #define USER_MSG (NETLINK_USER+1)      //自定义用于和内核空间通信
    
    #define MAX_PLOAD 1024                 //nlmsg最大负载(包含头部)
    
    
    int main(int argc,char** argv)
    {
        char* data = "hello kernel space by user 1";
        
        int sockfd;
        struct sockaddr_nl local,remote;
    
        struct nlmsghdr* nlh = NULL;
        struct iovec iov;
        struct msghdr msg;
    
        int ret;
    
        /*
        第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,
        第二个参数必须是SOCK_RAW或SOCK_DGRAM,因为netlink是一种面向数据包的服务.
        第三个参数指定netlink协议类型,可以是自己在netlink.h中定义的,也可以是内核中已经定义好的。
        */
        sockfd = socket(AF_NETLINK,SOCK_RAW,USER_MSG);
        if(sockfd == -1)
        {
            printf("create socket failure! %s
    ",strerror(errno));
            return -1;
        }
    
        memset(&local,0,sizeof(local));
        local.nl_family = AF_NETLINK;
        local.nl_pid = 50;            //nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,可以看做通信双方端口。
        local.nl_groups = 0;
    
        ret = bind(sockfd,(struct sockaddr*)&local,sizeof(local));
        if(ret < 0)
        {
            printf("bind() error!
    ");
            close(sockfd);
            return -1;
        }
    
        memset(&remote,0,sizeof(remote));
        remote.nl_family = AF_NETLINK;
        remote.nl_pid = 0;            //为0表示与内核通信
        remote.nl_groups = 0;
    
        //1.先设置nlh格式和数据
        nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PLOAD));
        if(!nlh)
        {
            printf("malloc nlmsghdr error!
    ");
            close(sockfd);
            return -1;
        }
    
        memset(nlh,0,sizeof(struct nlmsghdr));
        nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
        nlh->nlmsg_flags = 0;
        nlh->nlmsg_type = 0;
        nlh->nlmsg_seq = 0;
        nlh->nlmsg_pid = local.nl_pid;
    
        memcpy(NLMSG_DATA(nlh),data,strlen(data));
    
        //2.设置iovec数据
        iov.iov_base = (void*)nlh;
        iov.iov_len = NLMSG_SPACE(MAX_PLOAD);
    
        //3.设置msg数据
        memset(&msg,0,sizeof(msg));
        msg.msg_name = (void*)&remote;
        msg.msg_namelen = sizeof(remote);
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
    
    
        //开始发送数据
        printf("sendmsg start....
    ");
    
        ret = sendmsg(sockfd,&msg,0);
        if(ret < 0)
        {
            perror("send to kernel failure!
    ");
            close(sockfd);
            free(nlh);
            exit(-1);
        }
    
        //接受数据
        printf("recvmsg start....
    ");
        ret = recvmsg(sockfd,&msg,0);
        if(ret < 0)
        {
            perror("recv from kernel failure!
    ");
            close(sockfd);
            free(nlh);
            exit(-1);        
        }
        printf("recv data:%s
    ", (char*)NLMSG_DATA(nlh));
    
        close(sockfd);
        free((void*)nlh);
    
        return 0;
    }

    (二)sendto与recvfrom方法的用户态实现

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/netlink.h>
    
    #define NETLINK_USER 22
    #define USER_MSG (NETLINK_USER+1)
    
    #define MAX_PLOAD 1024
    
    int main(int argc,char** argv)
    {
        char* data = "hello kernel space by user 2";
        
        int sockfd,ret;
        struct sockaddr_nl local,remote;
    
        struct nlmsghdr* nlh = NULL;
    
        sockfd = socket(AF_NETLINK,SOCK_RAW,USER_MSG);
        if(sockfd == -1)
        {
            printf("create socket failure! %s
    ",strerror(errno));
            return -1;
        }
    
        memset(&local,0,sizeof(local));
        local.nl_family = AF_NETLINK;
        local.nl_pid = 50;
        local.nl_groups = 0;
        if(bind(sockfd,(struct sockaddr*)&local,sizeof(local)) != 0)
        {
            printf("bind() error!
    ");
            close(sockfd);
            return -1;
        }
    
        memset(&remote,0,sizeof(remote));
        remote.nl_family = AF_NETLINK;
        remote.nl_pid = 0;
        remote.nl_groups = 0;
    
        nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_PLOAD));
        memset(nlh,0,sizeof(struct nlmsghdr));
        nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
        nlh->nlmsg_flags = 0;
        nlh->nlmsg_type = 0;
        nlh->nlmsg_seq = 0;
        nlh->nlmsg_pid = local.nl_pid;
    
        printf("sendmsg start....
    ");
    
        memcpy(NLMSG_DATA(nlh),data,strlen(data));
        ret = sendto(sockfd,nlh,nlh->nlmsg_len,0,(struct sockaddr*)&remote,sizeof(struct sockaddr_nl));
    
        if(ret<0)
        {
            perror("send to kernel failure!
    ");
            close(sockfd);
            exit(-1);
        }
    
        //接受数据
        printf("recvmsg start....
    ");
    
        memset(nlh,0,NLMSG_SPACE(MAX_PLOAD));
        ret = recvfrom(sockfd,nlh,NLMSG_LENGTH(MAX_PLOAD),0,NULL,NULL);
        if(ret<0)
        {
            printf("recv from kernel failure!
    ");
            close(sockfd);
            exit(-1);
        }
    
        printf("recv data:%s
    ",(char*)NLMSG_DATA(nlh));
    
        close(sockfd);
        free((void*)nlh);
    
        return 0;
    }

    (三)两种情况测试

    使用gcc编译:

    gcc ./user.c -o user
    gcc ./user2.c -o user2

     查看dmesg结果:

    注意:在内核种并不需要实现循环即可一直运行,我们多次调用用户态程序,依旧可以与内核通信!!!

    (四)分析与UDP发送数据包的不同点:

    1. socket地址结构不同,UDP为sockaddr_in,Netlink为struct sockaddr_nl;

    2. 与UDP发送数据相比,Netlink多了一个消息头结构struct nlmsghdr需要我们构造。

    3.注意sockaddr_nl中的nl_pid,网上很多文章把这个字段说成是进程的pid,其实这完全是望文生义。这里的pid和进程pid没有什么关系,仅仅相当于UDP的port,用做双方通信。

  • 相关阅读:
    shutil文件去重模块
    Nexus构建npm、yum、maven私有仓库
    centos7添加自定义服务到systemctl
    Sonatype nuxus私有仓库介绍
    rancher单节点备份和恢复
    rancher证书过期X509:certificate has expired or is not ye valid
    清理docker日志
    mysql 9 hash索引和B+tree索引的区别
    mysql 8 索引
    mysql 7 慢查询+慢查询工具
  • 原文地址:https://www.cnblogs.com/ssyfj/p/15390323.html
Copyright © 2020-2023  润新知