linux学习札记:netlink实践演练

linux学习笔记:netlink实践演练

内核和用户空间之间存在如下交互手段:

1.内核启动参数 2.模块参数与 3.sysfs、4.sysctl、5.系统调用、6.netlink、7.procfs、8.seq_file、9.debugfs 10.relayfs

另外 call_usermodehelper 可以从内核发起用户态的应用程序运行

  其中netlink作为一种进程之间的通讯手段 ,和其他内核与用户空间的通讯手段比较,有很大的优势:

 1.  新增通讯通道的灵活性:

     netlink提供了一套完善的机制,用户既可以静态的增加通讯通道,也可以动态的创建通讯通道,这样用户可以很灵活的根据自己的需要来定制和开发。

  2. 丰富的特性支持:

     netlink可以支持异步双工, 其他机制只支持用户到内核单向的通道,或者只支持内核到用户的单向通道,netlink支持对称的双向通道。

      同时neilink支持单点传输和多点传输,这些优势都是其他通讯机制所不具备的。

 3. 传输效率比较高:  

     和其他用户向内核通讯手段一样,netlink也是借助某些系统调用接口实现的,并且netlink的目标数据接收操作是直接在软中断里面执行的,比有些在内核开辟线程接收数据的方式要快。

 4. 易于扩展:

     内核已为netlink提供的动态机制扩展,新增一个应用通道非常方便,只需要修改少量代码。

      netlink充分体现了linux开发的宗旨:“提供机制而不是策略”,“do one thing and do it well”, 从内核版本的演进历程看来,同一类型的机制,linux提供的功能越来越强大,给用户的选择空间也是越来越丰富。

从学习的角度出发,这里使用静态方式新增了一个netlink通道,并实现了一个用户态和内核态通讯的双向通讯的样例,设计如下:

       user                                                    kernel

          |                                                                |

        send    -> "hello from usr"->  receive and print 

           |                                                               |

 receive and print  < -"hello from usr " <-send

           |                                                               |

         exit

 如上图所述,由用户首先发起一个问候消息给内核,内核收到这个消息以后返回一个问候消息给用户, 以下通过代码来分析netlink的实现:

一、内核部分代码:

1. 头文件和静态申明

这里包含了必要的头文件, 新增了一个 netlink协议号,这个协议号和内核自定义的NETLINK_GENERIC是同一类型,应该定义在<linux/netlink.h>中,为了方便显示放到了这里。

新增内核源码文件 eknetlink.c :

/*kernel example code of netlink*/
#include <linux/netlink.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/spinlock.h>
#include <linux/netlink.h>
#include <net/sock.h>
#include <net/flow.h>

#define NETLINK_EXAMPLE		31  /*新增netlink协议号*/

/*本netlink过滤类型*/
enum nf_eknetlink_hooks {
    NF_EKNETLINK_IN,                 
    NF_EKNETLINK_OUT,
    NF_EKNETLINK_NUMHOOKS
};

#define TFR     printk      /*trace function routine*/

static struct sock *eknl = NULL;    /*模块内部全局的netlink套接字*/

static DEFINE_MUTEX(eknl_mutex);  /*多线程互斥, 必需,有可能多个用户进程(如多个shell窗口)同时向本模块发消息*/

static void eknl_lock(void)
{
    mutex_lock(&eknl_mutex);
}
static void eknl_unlock(void)
{
    mutex_unlock(&eknl_mutex);
}

 

2. 内核netlink初始化

   1) .netlink_kernel_create 为 NETLINK_EXAMPLE协议创建了一个套接字。

   2) &init_net 使用当前的网络命名空间。

   3) eknetlink_rcv 使用这个处理用户发送过来的soket数据。

   4) nf_register_hook 注册一个数据包过滤函数。 

static int __init eknetlink_init(void)
{
    int rv = 0;

    printk("example kernel netlink_init.\n");

    eknl = netlink_kernel_create(&init_net, NETLINK_EXAMPLE, 0,
                      eknetlink_rcv, NULL, THIS_MODULE);
    if (!eknl) {
        printk(KERN_ERR "cannot initialize nfnetlink!\n");
        return -1;
    }

    rv = nf_register_hook(&eknl_ops);
    if (rv) {
        netlink_kernel_release(eknl);
    }

    return rv;
}

static void __exit eknetlink_exit(void)
{
    printk("Removing example kernel NETLINK layer.\n");
    netlink_kernel_release(eknl);
    return;
}

module_init(eknetlink_init);
module_exit(eknetlink_exit);
MODULE_AUTHOR("monan");
MODULE_LICENSE("GPL");


3. 内核netlink消息接收函数

/*
netlink是基于socket套接字实现的通讯,所以netlink接收到的原始数据是socket数据类型,由struct sk_buff定义,函数中的netlink_rcv_sk()会通过nlh = nlmsg_hdr(skb);解析sk_buff,获取socket数据中的netlink的数据(由struct nlmsghdr定义),并交给eknetlink_rcv_msg,让我们自行处理netlink消息
*/
static void eknetlink_rcv(struct sk_buff *skb)
{
    TFR("eknetlink_rcv enter.skb(0x%x)\n", (unsigned int) skb);

    eknl_lock();
    netlink_rcv_skb(skb, &eknetlink_rcv_msg);
    eknl_unlock();

    TFR("eknetlink_rcv exit.\n");
}

/*这里开始解析netlink数据:struct nlmsghdr *nlh */

static int eknetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    char* str_buf;
    int ret;
    
    TFR("eknetlink_rcv_msg enter.\n");

    printk("nlmsg_len(%d) nlmsg_type(0x%x) nlmsg_flags(0x%x) nlmsg_seq(%d) nlmsg_pid(0x%x)\n",
    nlh->nlmsg_len, nlh->nlmsg_type, nlh->nlmsg_flags, nlh->nlmsg_seq, nlh->nlmsg_pid);

   /*对接收数据进行过滤处理*/
   ret = NF_HOOK(AF_NETLINK, NF_EKNETLINK_IN, skb, NULL, NULL, eknetlink_okfn);
   if (NF_ACCEPT !=  ret)
   {
        printk("out NF_HOOK not accept (%d)!\n", ret);
        return -1;
   }

    /*
       netlink数据中的用户数据(这里即用户控件发来的"hello kernel"字符串),是netlink消息数据中消息头(struct nlmsghdr)后面紧接着的部分,
        长度由struct nlmsghdr.nlmsg_len描述,如果只有消息头没有用户数据,则struct nlmsghdr.nlmsg_len 为0。
        */
    if (nlh->nlmsg_len > 0) {
        str_buf = kmalloc(nlh->nlmsg_len, GFP_KERNEL); 
        memcpy(str_buf, NLMSG_DATA(nlh), nlh->nlmsg_len);
        str_buf[nlh->nlmsg_len - 1] = '\0';
        printk("Message received(%d):%s\n", nlh->nlmsg_pid, str_buf) ;
        kfree(str_buf);

        /*nlh->nlmsg_pid是发送者的用户进程ID,传递,用于描述内核返回消息时的发送对象*/
        if(!eknetlink_sendmsg("From kernel: hello user!", nlh->nlmsg_pid)){
            printk("eknetlink_rcv_msg send fail. \n");
        }
    }

    TFR("eknetlink_rcv_msg exit.\n");

    return 0;
}

4. 内核netlink消息发送函数:

/*
  message: 要发送的消息字符串
  pid :发送的目标,用户进程ID
*/
int eknetlink_sendmsg(char *message, int pid)
{
    struct sk_buff *skb;
    struct nlmsghdr *nlh;
    int msize = 0;
    int ret;
    
    if(!message || !eknl)
    {
        return -1;
    }

    msize = strlen(message) + 1; 
    
  /* nlmsg_new 会新申请一个socket buffer ,其大小为
        socket消息头大小+  netlink 消息头大小+ 用户消息大小*/ 
    skb = nlmsg_new(msize, GFP_KERNEL);
    if(!skb)
    {
        printk(KERN_ERR "eknetlink_sendnlmsg:alloc_skb_1 error\n");
        return -1;
    }
    
    nlh = nlmsg_put(skb,0,0,0,msize,0);   /*填充部分netlink消息头*/

    NETLINK_CB(skb).pid = 0;        /*描述发送者ID,这里发送者是内核,填0*/
    NETLINK_CB(skb).dst_group = 0;

    memcpy(NLMSG_DATA(nlh), message, msize);/*填充用户区数据*/
    printk("Message send  '%s'.\n",(char *)NLMSG_DATA(nlh));

    ret =  NF_HOOK(AF_NETLINK, NF_EKNETLINK_OUT, skb, NULL, NULL, eknetlink_okfn);
    if (NF_ACCEPT !=  ret)
    {
         printk("out NF_HOOK not accept (%d)!\n", ret);
         return -1;
    }
    /*单播消息,目标用户态pid*/
    /*需要特别注意的是: skb申请的空间会在这里面释放,
     所以不能重复调用此接口发送同一个skb,会造成严重后果*/
   return netlink_unicast(eknl, skb, pid, MSG_DONTWAIT); }

 

5. 内核netlink消息过滤:

netfilter 提供了一种过滤机制,上文在eknetlink_init  中 调用nf_register_hook注册了一个过滤钩子,即可以对收发消息进行截取或者过滤。

这个机制不是netlink独有的,是socket网络代码的一部分,但是也可以修改后被netlink借过来使用。

需要在本模块的消息接收和发送接口中主动调用NF_HOOK,这个钩子才能被执行。

查看宏 NF_HOOK 的定义可知:只有当eknl_hook()和eknetlink_okfn()都返回NF_ACCEPT,此消息包才会判定为允许接收。

static unsigned int eknl_hook(unsigned int hook,
            struct sk_buff *skb,
            const struct net_device *in,
            const struct net_device *out,
            int (*okfn)(struct sk_buff *))
{
    
    TFR("eknl_hook .\n");
    return NF_ACCEPT;    /*钩子回调,返回NF_ACCEPT表示数据允许接收*/
}
int eknetlink_okfn (struct sk_buff * skb)
{
    TFR("eknetlink_okfn .skb(0x%x)\n", (unsigned int)skb);
    return NF_ACCEPT; /*钩子回调的回调,返回NF_ACCEPT表示数据允许接收,*/
}
static struct nf_hook_ops eknl_ops __read_mostly = {
    .hook       = eknl_hook,
    .pf         = AF_NETLINK,       /*设定这个值的同时需要修改NFPROTO_NUMPROTO, 否则nf_register_hook访问nf_hooks会溢出,系统一直起不来。*/
    .hooknum    = NF_EKNETLINK_IN,
    .priority   = 0,
    .owner      = THIS_MODULE,
};

修改NFPROTO_NUMPROTO,在文件netfilter.h中:

enum {
	NFPROTO_UNSPEC =  0,
	NFPROTO_IPV4   =  2,
	NFPROTO_ARP    =  3,
	NFPROTO_BRIDGE =  7,
	NFPROTO_IPV6   = 10,
	NFPROTO_DECNET = 12,
        NFPROTO_NETLINK  = 16 , /*add for netlink study*/
	NFPROTO_NUMPROTO,
};

 6.  新增编译脚本并编译内核:

Kconfig

config EKNETLINK
     tristate "Netlink study example."
     default n
     help
     Netlink example for study, this is kernel  part , cooperation with net link usr example part. 

Makefile

obj-$(CONFIG_EKNETLINK) += eknetlink.o

make menuconfig 打开eknetlink模块编译

make -j4 编译内核

至此内核的netlink新增通道接收和发送流程描述完成,接下来记录用户态的netlink新增通道实现。

 

二、用户部分代码:

在用户态新增源码文件

external/netlink/eunetlink.c

1) 头文件和静态申明

/*user example code for netlink study*/ #include <asm/types.h> #include <sys/socket.h> #include <unistd.h> #include <err.h> #include <stdio.h> #include <netinet/in.h> #include <linux/netlink.h> #include <linux/rtnetlink.h>

enum {     EUNETLINK_MSG_OPEN =  NLMSG_MIN_TYPE, /*比NLMSG_MIN_TYPE 小的类型是系统保留的,不能使用,                                       否则消息在内核回调中会被netlink_rcv_skb过滤,eknetlink_rcv_msg得不到调用*/         EUNETLINK_MSG_CLOSE }EUNETLINK_MSG;

#define NETLINK_EXAMPLE  31  /*新增netlink协议类型,和内核态的定义保持一致*/

2) 用户态主函数

int main(int argc, char *argv[])
{     
    int nlsk = eunetlink_open(); /*打开一个netlink 套接字*/  
    int ret;
    if (nlsk<0) {  
        err(1,"netlink");  
        return -1;
    }     
    printf("eunetlink open socket = 0x%x\n", nlsk);     

    ret = eunetlink_send(nlsk);  /*向内核发送"hello"消息*/ 
    printf("eunetlink send ret = 0x%x\n", ret);     

    eunetlink_recv(nlsk);    /*等待接收内核返回的消息*/ 
    
    close(nlsk); /*关闭netlink 套接字*/
    
    return 0;
}

 

3)用户态消息发送接口

int eunetlink_send(int nlsk)
{
    char buffer[] = "From user : hello kernel!";
    struct nlmsghdr* nlhdr;
    struct iovec iov;  /* 用于把多个消息通过一次系统调用来发送*/
    struct msghdr msg; 
    struct sockaddr_nl nladdr;

    /*必须,消息头数据清零,否则会包含错误的数据,发送失败*/
    memset(&msg, 0 ,sizeof(struct msghdr));
    memset(&nladdr, 0 ,sizeof(struct sockaddr_nl));
    
    nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(strlen(buffer) + 1)); 
    strcpy(NLMSG_DATA(nlhdr),buffer); 

    /*填充待发送的消息体*/
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer) + 1); 
    nlhdr->nlmsg_pid = getpid();   /*发送者进程ID,内核代码会根据这个ID返回消息*/
    nlhdr->nlmsg_flags = NLM_F_REQUEST;        /*必须,不设置的话,内核中netlink_rcv_skb() 会过滤此消息,不会调用户回调*/
    nlhdr->nlmsg_type  = EUNETLINK_MSG_OPEN;   /*必须,不设置的话,内核中netlink_rcv_skb() 会过滤此消息,不会调用户回调*/

    /*填充目标地址结构体*/
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_pid = 0;      /*目标地址是内核,所以这里需要填0*/
    nladdr.nl_groups = 0;

    printf("Message Send :%s\n",buffer);    

#if 0
    /*使用struct iovec iov[]数组 和 sendmsg可以实现一次调用发送多个消息请求*/
    iov.iov_base = (void *)nlhdr; 
    iov.iov_len = nlhdr->nlmsg_len;
    
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_name = (void *)&(nladdr);
    msg.msg_namelen = sizeof(nladdr);
        
    return  sendmsg(nlsk, &msg, 0);  
#else
     /*发送单个个请求*/
    return sendto(nlsk, nlhdr, nlhdr->nlmsg_len, 0,
             (struct sockaddr*)&nladdr, sizeof(nladdr));
#endif     
}

4)用户态消息接收

int eunetlink_recv(int sock)
{ 
  struct sockaddr_nl nladdr;  
    struct msghdr msg;  
    struct iovec iov[1];  
    struct nlmsghdr * nlh;    
    char buffer[65536]; /*临时buffer,用于接收内核发过来的数据*/  
    int len;       

    /*填充待接收消息结构体*/ 
    msg.msg_name = (void *)&(nladdr);  /*设定接收的发送源地址*/   
    msg.msg_namelen = sizeof(nladdr);  
 
    /*设定临时缓冲*/
    iov[0].iov_base = (void *)buffer; 
    iov[0].iov_len = sizeof(buffer);
    msg.msg_iov = iov;                    
    msg.msg_iovlen = sizeof(iov)/sizeof(iov[0]);  /*允许多个临时缓冲*/
    
    len = recvmsg(sock, &msg, 0);  /*阻塞等待接收数(除非特别设定,socket都是阻塞式接收)*/
    if (len < 0) { 
        printf("recvmsg error: %d", len);     
        return len; 
    }       

    /*遍历接收到的消息数据,进行处理
        NLMSG_OK会判断数据是不是已经结束
        NLMSG_NEXT 会修改nlh和len的值,使之指向buffer中的下一个netlink消息
       */     
    for (nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {  
        if (nlh->nlmsg_type == NLMSG_ERROR) { 
            // Do some error handling.       
            puts("eunetlink_recv: NLMSG_ERROR");  
        return 0;   
        } 

        /*处理有效的用户数据*/
        if (nlh->nlmsg_len > 0) {
                char *str_buf = malloc(nlh->nlmsg_len); 
                memcpy(str_buf, NLMSG_DATA(nlh), nlh->nlmsg_len);
                
                str_buf[nlh->nlmsg_len - 1] = '\0';
                printf("Message received:%s\n",str_buf) ;
        
                free(str_buf);
            }
    }

    return 0;
}

5)新增编译脚本并编译用户应用程序
新增external/netlink/Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := eunetlink
LOCAL_SRC_FILES := $(call all-subdir-c-files)
include $(BUILD_EXECUTABLE)

 编译用户程序:

mmm external/netlink/


打包system.img:

make snod

 

三、运行和调试

   1)。运行android的goldfish模拟器,使用新编译出来的内核:

  emulator -kernel (YOUR_KERNEL_PATH)/arch/arm/boot/zImage -shell

 运行用户测试程序:

eunetlink

正确执行会显示如下信息:

linux学习札记:netlink实践演练

本篇代码的内核态打印:

linux学习札记:netlink实践演练

2)调试用户态

 运行: strace eunetlink

   会显示所有用户态的系统调用路径和返回值,并且会把入参展开显示,这个对调试很有用:

   这里截取其中本文代码相关的部分:

linux学习札记:netlink实践演练

 

四、总结

       netlink提供了一种很好很强大的的用户与内核之间的通讯机制,本文通过静态的新增一个netlink协议类型,并使用这个新的netlink类型实现用户态和内核态的双向通讯,对linux的netlink通讯方式有了一个初步的认识。

       动态申请 netlink通道机制在genetlink.c中实现,是对NETLINK_GENERIC协议的上层封装, 可以参考内核代码net\tipc\netlink.c中的对他的应用,这个文件实现比较简单,可以作为很好的样例。

说明:本文所含代码的运行环境为 android提供的 goldfish 模拟器平台, Linux 内核版本为 2.6.29

参考文档:

android-goldfish-2.6.29 code

《Linux内核设计与实现》(Linux Kernel Development)第3版

Linux 用户态与内核态的交互——netlink 篇:http://bbs.chinaunix.net/thread-2162796-1-1.html

《Linux 系统内核空间与用户空间通信的实现与分析》   陈鑫  http://www-128.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux
《在 Linux 下用户空间与内核空间数据交换的方式》      杨燚  http://www-128.ibm.com/developerworks/cn/linux/l-kerns-usrs/