netlink---Linux下基于socket的内核和上层通信机制(上)

本文介绍Linux内核与用户空间通信机制Netlink的应用场景和技术细节,包括其地址结构、协议定义及用户态操作方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我最近有一个项目需求,需要在Linux网卡驱动中加入一个自己的驱动,实现在内核态完成一些报文处理(这个过程可以实现一种零COPY的网络报文截获),对于复杂报文COPY下必要的数据交给用户态来完成(因为过于复杂的报文消耗CPU太大,会导致中断占用时间太长)。因此需要一种内核和用户态配合的通信机制,尝试了很多方式都不太理想,最后采用netlink+内存映射的模式很好的解决了这个问题。Netlink是一种采用socket通信的机制,用于linux内核和上层用户空间进行通信的一种机制,通过实践我认为netlink最大的优点是可以实现“双向通信”,是内核向用户态发起通知的一种最好选择。


内核和用户空间进行通信,大概有如下几种方式可以考虑:
采用内存映射的方式,将内核地址映射到用户态。这种方式最直接,可以适用大量的数据传输机制。这种方式的缺点是很难进行“业务控制”,没有一种可靠的机制保障内核和用户态的调动同步,比如信号量等都不能跨内核、用户层使用。因此内存映射机制一般需要配合一种“消息机制”来控制数据的读取,比如采用“消息”类型的短数据通道来完成一个可靠的数据读取功能。
ioctl机制,ioctl机制可以在驱动中扩展特定的ioctl消息,用于将一些状态从内核反应到用户态。Ioctl有很好的数据同步保护机制,不要担心内核和用户层的数据访问冲突,但是ioctl不适合传输大量的数据,通过和内存映射结合可以很好的完成大量数据交换过程。但是,ioctl的发起方一定是在用户态,因此如果需要内核态主动发起一个通知消息给用户层,则非常的麻烦。可能需要用户态程序采用轮询机制不停的ioctl。
其他一些方式比如系统调用必须通过用户态发起,proc方式不太可靠和实时,用于调试信息的输出还是非常合适的。
通过前面的项目背景,我需要一种可以在内核态主动发起消息的通知方式,而用户态的程序最好可以采用一种“阻塞调用”的方式等待消息。这样的模型可以最大限度的节省CPU的调度,同时可以满足及时处理的要求,最终选择了netlink完成通信的过程。
Netlink的通信模型和socket通信非常相似,主要要点如下:
  • netlink采用自己独立的地址编码,struct sockaddr_nl;
  • 每个通过netlink发出的消息都必须附带一个netlink自己的消息头,struct nlmsghdr;
  • 内核态的netlink的操作API和用户态完全不一样,后面再介绍;
  • 用户态的netlink操作完成采用socket函数,非常方便和简单,有TCP/UDP socket编程基础的非常容易上手。


Netlink的通信地址和协议

所有socket之间的通信,必须有个地址结构,Netlink也不例外。我们最熟悉的就是IPV4的地址了,netlink的地址结构如下:

  1. struct sockaddr_nl 
  2.     sa_family_t nl_family;          //必须为AF_NETLINK或者PF_NETLINK 
  3.     unsigned short  nl_pad;             //必须为0 
  4.     __u32       nl_pid;             //通信端口 
  5. __u32       nl_groups;              //组播掩码 
  6. }; 
struct sockaddr_nl
{
	sa_family_t	nl_family;			//必须为AF_NETLINK或者PF_NETLINK
	unsigned short	nl_pad;				//必须为0
	__u32		nl_pid;				//通信端口
__u32		nl_groups;				//组播掩码
};

上面几个数据,最关键的是nl_family(就对应IP通信中的AF_INET)和nl_pid。

nl_pid就是一个约定的通信端口,用户态使用的时候需要用一个非0的数字,一般来说可以直接采用上层应用的进程ID(不用进程ID号码也没事,只要系统中不冲突的一个数字即可使用)。对于内核的地址,该值必须用0,也就是说,如果上层通过sendto向内核发送netlink消息,peer addr中nl_pid必须填写0。


nl_groups用于一个消息同时分发给不同的接收者,是一种组播应用,本文不讲组播应用。


本质上,nl_pid就是netlink的通信地址。除了通信地址,netlink还提供“协议”来标示通信实体,在创建socket的时候,需要指定netlink的通信协议号。每个协议号代表一种“应用”,上层可以用内核已经定义的协议和内核进行通信,获得内核已经提供的信息。具体支持的协议列表如下:

  1. #define NETLINK_ROUTE       0   /* Routing/device hook              */ 
  2. #define NETLINK_UNUSED      1   /* Unused number                */ 
  3. #define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */ 
  4. #define NETLINK_FIREWALL    3   /* Firewalling hook             */ 
  5. #define NETLINK_INET_DIAG   4   /* INET socket monitoring           */ 
  6. #define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */ 
  7. #define NETLINK_XFRM        6   /* ipsec */ 
  8. #define NETLINK_SELINUX     7   /* SELinux event notifications */ 
  9. #define NETLINK_ISCSI       8   /* Open-iSCSI */ 
  10. #define NETLINK_AUDIT       9   /* auditing */ 
  11. #define NETLINK_FIB_LOOKUP  10   
  12. #define NETLINK_CONNECTOR   11 
  13. #define NETLINK_NETFILTER   12  /* netfilter subsystem */ 
  14. #define NETLINK_IP6_FW      13 
  15. #define NETLINK_DNRTMSG     14  /* DECnet routing messages */ 
  16. #define NETLINK_KOBJECT_UEVENT  15  /* Kernel messages to userspace */ 
  17. #define NETLINK_GENERIC     16 
  18. /* leave room for NETLINK_DM (DM Events) */ 
  19. #define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */ 
  20. #define NETLINK_ECRYPTFS    19 
#define NETLINK_ROUTE		0	/* Routing/device hook				*/
#define NETLINK_UNUSED		1	/* Unused number				*/
#define NETLINK_USERSOCK	2	/* Reserved for user mode socket protocols 	*/
#define NETLINK_FIREWALL	3	/* Firewalling hook				*/
#define NETLINK_INET_DIAG	4	/* INET socket monitoring			*/
#define NETLINK_NFLOG		5	/* netfilter/iptables ULOG */
#define NETLINK_XFRM		6	/* ipsec */
#define NETLINK_SELINUX		7	/* SELinux event notifications */
#define NETLINK_ISCSI		8	/* Open-iSCSI */
#define NETLINK_AUDIT		9	/* auditing */
#define NETLINK_FIB_LOOKUP	10	
#define NETLINK_CONNECTOR	11
#define NETLINK_NETFILTER	12	/* netfilter subsystem */
#define NETLINK_IP6_FW		13
#define NETLINK_DNRTMSG		14	/* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT	15	/* Kernel messages to userspace */
#define NETLINK_GENERIC		16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT	18	/* SCSI Transports */
#define NETLINK_ECRYPTFS	19

协议的用途很好理解,比如我们单纯创建一个上层应用,通过和NETLINK_ROUTE协议通信,可以获得内核的路由信息。我需要利用netlink创建一个我自己的通信协议,因此我定义了一种新的协议。新协议的定义不能和内核已经定义的冲突,同时不能超过MAX_LINKS这个宏的限定,MAX_LINKS = 32。所以我定义的协议号为30。


小结:netlink采用协议号+通信端口的方式构建自己的地址体系。


用户态操作netlink socket

用户态创建netlink socket的基本过程和操作其他socket的API一模一样,区别就2点:
1、 netlink有自己的地址;
2、 netlink接收到的消息带一个netlink自己的消息头;


用户态创建、销毁socket的过程:
1、 用socket函数创建,socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX);第一个参数必须是PF_NETLINK或者AF_NETLINK,第二个参数用SOCK_DGRAM和SOCK_RAW都没问题,第三个参数就是netlink的协议号。
2、 用bind函数绑定自己的地址。
3、 用close关闭套接字。

创建socket的代码样例:

  1.     struct sockaddr_nl addr; 
  2.     int flags; 
  3.     
  4.     //建立netlink socket 
  5.     s_nlm_socket = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX); 
  6.     if(s_nlm_socket < 0) 
  7.     { 
  8.         USE_DBG_OUT("create netlink socket error.\r\n"); 
  9.         goto Err_Exit; 
  10.     } 
  11.      
  12.     //bind 
  13.     addr.nl_family = PF_NETLINK; 
  14.     addr.nl_pad    = 0; 
  15.     addr.nl_pid    = getpid(); 
  16.     addr.nl_groups = 0; 
  17.      
  18.     if(bind(s_nlm_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0) 
  19.     { 
  20.         USE_DBG_OUT("bind socket error.\r\n"); 
  21.         goto Err_Exit; 
  22.     } 
  23.  
  24.  
  25.     //设置socket为非阻塞模式 
  26.     flags = fcntl(s_nlm_socket, F_GETFL, 0); 
  27.     fcntl(s_nlm_socket, F_SETFL, flags|O_NONBLOCK); 
  28.      
  29.  
  30.  
  31.     return 0; 
  32. Err_Exit: 
  33.     return -1; 
{
    struct sockaddr_nl addr;
    int flags;
   
    //建立netlink socket
    s_nlm_socket = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX);
    if(s_nlm_socket < 0)
    {
        USE_DBG_OUT("create netlink socket error.\r\n");
        goto Err_Exit;
    }
    
    //bind
    addr.nl_family = PF_NETLINK;
    addr.nl_pad    = 0;
    addr.nl_pid    = getpid();
    addr.nl_groups = 0;
    
    if(bind(s_nlm_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    {
        USE_DBG_OUT("bind socket error.\r\n");
        goto Err_Exit;
    }


    //设置socket为非阻塞模式
    flags = fcntl(s_nlm_socket, F_GETFL, 0);
    fcntl(s_nlm_socket, F_SETFL, flags|O_NONBLOCK);
    


    return 0;
Err_Exit:
    return -1;
}


用户态接收、发送消息的API:
用户态用sendto向内核发送netlink消息,用recvfrom接收消息。只是注意,发送、接收的时候在自己附带的消息前面要加上一个netlink的消息头。例如,定义一个如下的消息通信结构:

  1. struct tag_rcv_buf 
  2.         struct nlmsghdr hdr;            //netlink的消息头 
  3.         netlink_notify_s my_msg;        //通信实体消息 
  4. }st_snd_buf; 
struct tag_rcv_buf
{
        struct nlmsghdr hdr;            //netlink的消息头
        netlink_notify_s my_msg;        //通信实体消息
}st_snd_buf;

发送代码的例子:

  1. My_send_msg 
  2.     struct tag_rcv_buf 
  3.     { 
  4.         struct nlmsghdr hdr;            //netlink的消息头 
  5.         netlink_notify_s my_msg;        //通信实体消息 
  6.     }st_snd_buf; 
  7.     fd_set st_write_set;                         //select fd,避免线程吊死 
  8.     struct timeval write_time_out = {10, 0};     //10秒超时 
  9.     int ret; 
  10.      
  11.     //设置select 
  12.     FD_ZERO(&st_write_set); 
  13.     FD_SET(s_nlm_socket, &st_write_set); 
  14.      
  15.     /*
  16.         设置发送数据
  17.     */ 
  18.     st_snd_buf.hdr.nlmsg_len   = sizeof(st_snd_buf);        //NLMSG_LENGTH(sizeof(netlink_notify_s))--这个宏包含有头 
  19.     st_snd_buf.hdr.nlmsg_flags = 0;                         /*消息的附加选项,没啥用*/ 
  20.     st_snd_buf.hdr.nlmsg_type  = 0;                         /*设置自定义消息类型*/ 
  21.     st_snd_buf.hdr.nlmsg_pid   = getpid();                  /*设置发送者的PID*/ 
  22.  
  23.  
  24.     st_snd_buf.my_msg.start_pack_id = s_id; 
  25.     st_snd_buf.my_msg.end_pack_id   = e_id; 
  26.      
  27.     ret = select(s_nlm_socket+1, NULL, &st_write_set, NULL, &write_time_out); 
  28.     if(ret == -1) 
  29.     { 
  30.         //have some error. 
  31.         USE_DBG_OUT("send has some error %d.\n", errno); 
  32.         goto out; 
  33.     } 
  34.     else if(ret == 0) 
  35.     { 
  36.         //超时退出 
  37.         TMP_DBG_OUT("send timeout.\n"); 
  38.         goto out; 
  39.     } 
  40.     else 
  41.     { 
  42.         //接收消息 
  43.         ret = sendto(s_nlm_socket, &st_snd_buf, sizeof(st_snd_buf), 0,  
  44.                         (struct sockaddr*)&s_peer_addr, sizeof(s_peer_addr)); 
  45.  
  46.  
  47.         if(ret < 0) 
  48.         { 
  49.             USE_DBG_OUT("send to kernal by nl error %d\r\n", errno); 
  50.         } 
  51.         else 
  52.         { 
  53.             TMP_DBG_OUT("send to kernal ok s_id is %d, e_id is %d.\r\n", s_id, e_id); 
  54.         } 
  55.     } 
  56.      
  57. out:     
  58.     return
My_send_msg
{
    struct tag_rcv_buf
    {
        struct nlmsghdr hdr;            //netlink的消息头
        netlink_notify_s my_msg;        //通信实体消息
    }st_snd_buf;
    fd_set st_write_set;                         //select fd,避免线程吊死
    struct timeval write_time_out = {10, 0};     //10秒超时
    int ret;
    
    //设置select
    FD_ZERO(&st_write_set);
    FD_SET(s_nlm_socket, &st_write_set);
    
    /*
        设置发送数据
    */
    st_snd_buf.hdr.nlmsg_len   = sizeof(st_snd_buf);        //NLMSG_LENGTH(sizeof(netlink_notify_s))--这个宏包含有头
    st_snd_buf.hdr.nlmsg_flags = 0;                         /*消息的附加选项,没啥用*/
    st_snd_buf.hdr.nlmsg_type  = 0;                         /*设置自定义消息类型*/
    st_snd_buf.hdr.nlmsg_pid   = getpid();                  /*设置发送者的PID*/


    st_snd_buf.my_msg.start_pack_id = s_id;
    st_snd_buf.my_msg.end_pack_id   = e_id;
    
    ret = select(s_nlm_socket+1, NULL, &st_write_set, NULL, &write_time_out);
    if(ret == -1)
    {
        //have some error.
        USE_DBG_OUT("send has some error %d.\n", errno);
        goto out;
    }
    else if(ret == 0)
    {
        //超时退出
        TMP_DBG_OUT("send timeout.\n");
        goto out;
    }
    else
    {
        //接收消息
        ret = sendto(s_nlm_socket, &st_snd_buf, sizeof(st_snd_buf), 0, 
                        (struct sockaddr*)&s_peer_addr, sizeof(s_peer_addr));


        if(ret < 0)
        {
            USE_DBG_OUT("send to kernal by nl error %d\r\n", errno);
        }
        else
        {
            TMP_DBG_OUT("send to kernal ok s_id is %d, e_id is %d.\r\n", s_id, e_id);
        }
    }
    
out:    
    return;
}

接收数据的代码例子:

  1.     struct tag_rcv_buf 
  2.     { 
  3.         struct nlmsghdr hdr;            //netlink的消息头 
  4.         netlink_notify_s my_msg;        //通信实体消息 
  5.     }st_rcv_buf; 
  6.     int ret, addr_len, io_ret; 
  7.     struct sockaddr_nl st_peer_addr; 
  8.     fd_set st_read_set;                         //select fd,避免线程吊死 
  9.     struct timeval read_time_out = {10, 0};     //10秒超时 
  10.     int rcv_buf; 
  11.      
  12.     //设置内核的通信地址 
  13.     st_peer_addr.nl_family = AF_NETLINK; 
  14.     st_peer_addr.nl_pad = 0;                                   /*always set to zero*/ 
  15.     st_peer_addr.nl_pid = 0;                                   /*kernel's pid is zero*/ 
  16.     st_peer_addr.nl_groups = 0;                                /*multicast groups mask, if unicast set to zero*/ 
  17.     addr_len = sizeof(st_peer_addr); 
  18.  
  19.     //设置select 
  20.     FD_ZERO(&st_read_set); 
  21.     FD_SET(s_nlm_socket, &st_read_set); 
  22.  
  23.     ret = select(s_nlm_socket+1, &st_read_set, NULL, NULL, &read_time_out); 
  24.     if(ret == -1) 
  25.     { 
  26.         //have some error. 
  27.         USE_DBG_OUT("select rcv some error %d", errno); 
  28.         goto err; 
  29.     } 
  30.     else if(ret == 0) 
  31.     { 
  32.         //超时退出 
  33.         TMP_DBG_OUT("rcv timeout.\n"); 
  34.         *p_size = 0; 
  35.         goto out; 
  36.     } 
  37.     else 
  38.     { 
  39.         //接收消息 
  40.         ret = recvfrom(s_nlm_socket, &st_rcv_buf, sizeof(st_rcv_buf), 0,  
  41.             (struct sockaddr *)&st_peer_addr, &addr_len); 
  42.     } 
  43.     
  44.     if(ret == sizeof(st_rcv_buf) ) 
  45.     { 
  46.         //收到消息了... 
  47.  
  48.     else 
  49.     { 
  50.         USE_DBG_OUT("rcv msg have some err. ret is %d, errno is %d\r\n", ret, errno); 
  51.         goto err; 
  52.     } 
  53.  
  54. out:     
  55.     return 0; 
  56. err: 
  57.     *p_size = 0; 
  58.     return -1; 
  59. }

转自:http://blog.youkuaiyun.com/squiffy/article/details/7046963

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值