Linux 用户态与内核态的交互 ——netlink 篇

2023-10-29 03:40

本文主要是介绍Linux 用户态与内核态的交互 ——netlink 篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者:Kendo
2006-9-3

这是一篇学习笔记,主要是对《Linux 系统内核空间与用户空间通信的实现与分析》中的源码imp2的分析。其中的源码,可以到以下URL下载:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz

参考文档
《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/

理论篇
        在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,例如iprote2网络管理工具,它与内核的交互就全部使用了netlink,著名的内核包过滤框架Netfilter在与用户空间的通 读,也在最新版本中改变为netlink,无疑,它将是Linux用户态与内核态交流的主要方法之一。它的通信依据是一个对应于进程的标识,一般定为该进 程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函 数。工作原理如图:



如图所示,这里使用了软中断而不是内核线程来接收数据,这样就可以保证数据接收的实时性。
当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同,下图是 netlink 套接字实现此类通信时创建的过程:



用户空间

用户态应用使用标准的socket与内核通讯,标准的socket API 的函数, socket(), bind(), sendmsg(), recvmsg() 和 close()很容易地应用到 netlink socket。
为了创建一个 netlink socket,用户需要使用如下参数调用 socket():

  1. socket(AF_NETLINK, SOCK_RAW, netlink_type)
复制代码


netlink对应的协议簇是 AF_NETLINK,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型:
  1. #define NETLINK_ROUTE          0       /* Routing/device hook                          */
  2. #define NETLINK_W1             1       /* 1-wire subsystem                             */
  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 */
复制代码
#define NETLINK_GENERIC        16

同样地,socket函数返回的套接字,可以交给bing等函数调用:
  1. static int skfd;
  2. skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
  3. if(skfd < 0)
  4. {
  5.       printf("can not create a netlink socket\n");
  6.       exit(0);
  7. }
复制代码


bind函数需要绑定协议地址,netlink的socket地址使用struct sockaddr_nl结构描述:
  1. struct sockaddr_nl
  2. {
  3.   sa_family_t    nl_family;
  4.   unsigned short nl_pad;
  5.   __u32          nl_pid;
  6.   __u32          nl_groups;
  7. };
复制代码


成员 nl_family为协议簇 AF_NETLINK,成员 nl_pad 当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。成员 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组:
  1. struct sockaddr_nl local;

  2. memset(&local, 0, sizeof(local));
  3. local.nl_family = AF_NETLINK;
  4. local.nl_pid = getpid();                /*设置pid为自己的pid值*/
  5. local.nl_groups = 0;
  6. /*绑定套接字*/
  7. if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)
  8. {
  9. printf("bind() error\n");
  10.      return -1;
  11. }
复制代码


用户空间可以调用send函数簇向内核发送消息,如sendto、sendmsg等,同样地,也可以使用struct sockaddr_nl来描述一个对端地址,以待send函数来调用,与本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0:

  1. struct sockaddr_nl kpeer;
  2. memset(&kpeer, 0, sizeof(kpeer));
  3. kpeer.nl_family = AF_NETLINK;
  4. kpeer.nl_pid = 0;
  5. kpeer.nl_groups = 0;
复制代码


另一个问题就是发内核发送的消息的组成,使用我们发送一个IP网络数据包的话,则数据包结构为“IP包头+IP数据”,同样地,netlink的消息结构是“netlink消息头部+数据”。Netlink消息头部使用struct nlmsghdr结构来描述:
  1. struct nlmsghdr
  2. {
  3.   __u32 nlmsg_len;   /* Length of message */
  4.   __u16 nlmsg_type;  /* Message type*/
  5.   __u16 nlmsg_flags; /* Additional flags */
  6.   __u32 nlmsg_seq;   /* Sequence number */
  7.   __u32 nlmsg_pid;   /* Sending process PID */
  8. };
复制代码


字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用netlink提供的宏NLMSG_LENGTH来计算这个长度,仅需向NLMSG_LENGTH宏提供要发送的数据的长度,它会自动计算对齐后的总长度:
  1. /*计算包含报头的数据报长度*/
  2. #define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
  3. /*字节对齐*/
  4. #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
复制代码


后面还可以看到很多netlink提供的宏,这些宏可以为我们编写netlink宏提供很大的方便。

字段 nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。

  1. struct msg_to_kernel                /*自定义消息首部,它仅包含了netlink的消息首部*/
  2. {
  3.   struct nlmsghdr hdr;
  4. };

  5. struct msg_to_kernel message;
  6. memset(&message, 0, sizeof(message));
  7. message.hdr.nlmsg_len = NLMSG_LENGTH(0);                /*计算消息,因为这里只是发送一个请求消息,没有多余的数据,所以,数据长度为0*/
  8. message.hdr.nlmsg_flags = 0;
  9. message.hdr.nlmsg_type = IMP2_U_PID;                        /*设置自定义消息类型*/
  10. message.hdr.nlmsg_pid = local.nl_pid;                /*设置发送者的PID*/

  11. 这样,有了本地地址、对端地址和发送的数据,就可以调用发送函数将消息发送给内核了:
  12.   /*发送一个请求*/
  13.   sendto(skfd, &message, message.hdr.nlmsg_len, 0,
  14.          (struct sockaddr*)&kpeer, sizeof(kpeer));
复制代码


当发送完请求后,就可以调用recv函数簇从内核接收数据了,接收到的数据包含了netlink消息首部和要传输的数据:
  1. /*接收的数据包含了netlink消息首部和自定义数据结构*/
  2. struct u_packet_info
  3. {
  4.   struct nlmsghdr hdr;
  5.   struct packet_info icmp_info;
  6. };
  7. struct u_packet_info info;
  8. while(1)
  9. {
  10.     kpeerlen = sizeof(struct sockaddr_nl);
  11.       /*接收内核空间返回的数据*/
  12.       rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
  13.                         0, (struct sockaddr*)&kpeer, &kpeerlen);
  14.                   
  15.        /*处理接收到的数据*/
  16. ……
  17. }
复制代码


同样地,函数close用于关闭打开的netlink socket。程序中,因为程序一直循环接收处理内核的消息,需要收到用户的关闭信号才会退出,所以关闭套接字的工作放在了自定义的信号函数sig_int中处理:
  1. /*这个信号函数,处理一些程序退出时的动作*/
  2. static void sig_int(int signo)
  3. {
  4.   struct sockaddr_nl kpeer;
  5.   struct msg_to_kernel message;

  6.   memset(&kpeer, 0, sizeof(kpeer));
  7.   kpeer.nl_family = AF_NETLINK;
  8.   kpeer.nl_pid    = 0;
  9.   kpeer.nl_groups = 0;

  10.   memset(&message, 0, sizeof(message));
  11.   message.hdr.nlmsg_len = NLMSG_LENGTH(0);
  12.   message.hdr.nlmsg_flags = 0;
  13.   message.hdr.nlmsg_type = IMP2_CLOSE;
  14.   message.hdr.nlmsg_pid = getpid();

  15.   /*向内核发送一个消息,由nlmsg_type表明,应用程序将关闭*/
  16.   sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr *)(&kpeer),         sizeof(kpeer));

  17.   close(skfd);
  18.   exit(0);
  19. }
复制代码


这个结束函数中,向内核发送一个“我已经退出了”的消息,然后调用close函数关闭netlink套接字,退出程序。

内核空间

与应用程序内核,内核空间也主要完成三件工作:
n        创建netlink套接字
n        接收处理用户空间发送的数据
n        发送数据至用户空间

API函数netlink_kernel_create用于创建一个netlink socket,同时,注册一个回调函数,用于接收处理用户空间的消息:

  1. struct sock *
  2. netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
复制代码


参数unit表示netlink协议类型,如NL_IMP2,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个 netlink socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的 struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。
  1. static int __init init(void)
  2. {
  3.   rwlock_init(&user_proc.lock);                /*初始化读写锁*/

  4.   /*创建一个netlink socket,协议类型是自定义的ML_IMP2,kernel_reveive为接受处理函数*/
  5.   nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
  6.   if(!nlfd)                /*创建失败*/
  7.   {
  8.       printk("can not create a netlink socket\n");
  9.       return -1;
  10.   }

  11.   /*注册一个Netfilter 钩子*/
  12.   return nf_register_hook(&imp2_ops);
  13. }


  14. module_init(init);
复制代码


用户空间向内核发送了两种自定义消息类型:IMP2_U_PID和IMP2_CLOSE,分别是请求和关闭。kernel_receive 函数分别处理这两种消息:
  1. DECLARE_MUTEX(receive_sem);                                                        /*初始化信号量*/
  2. static void kernel_receive(struct sock *sk, int len)
  3. {
  4.         do
  5.     {
  6.                 struct sk_buff *skb;
  7.                 if(down_trylock(&receive_sem))                                /*获取信号量*/
  8.                         return;
  9.                 /*从接收队列中取得skb,然后进行一些基本的长度的合法性校验*/
  10.                 while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
  11.         {
  12.                         {
  13.                                 struct nlmsghdr *nlh = NULL;
  14.                                 
  15.                                 if(skb->len >= sizeof(struct nlmsghdr))
  16.                                 {
  17.                                         /*获取数据中的nlmsghdr 结构的报头*/
  18.                                         nlh = (struct nlmsghdr *)skb->data;
  19.                                         if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))
  20.                                                 && (skb->len >= nlh->nlmsg_len))
  21.                                         {
  22.                                                 /*长度的全法性校验完成后,处理应用程序自定义消息类型,主要是对用户PID的保存,即为内核保存“把消息发送给谁”*/
  23.                                                 if(nlh->nlmsg_type == IMP2_U_PID)                /*请求*/
  24.                                                 {
  25.                                                         write_lock_bh(&user_proc.pid);
  26.                                                         user_proc.pid = nlh->nlmsg_pid;
  27.                                                         write_unlock_bh(&user_proc.pid);
  28.                                                 }
  29.                                                 else if(nlh->nlmsg_type == IMP2_CLOSE)        /*应用程序关闭*/
  30.                                                 {
  31.                                                         write_lock_bh(&user_proc.pid);
  32.                                                         if(nlh->nlmsg_pid == user_proc.pid)
  33.                                                                 user_proc.pid = 0;
  34.                                                         write_unlock_bh(&user_proc.pid);
  35.                                                 }
  36.                                         }
  37.                                 }
  38.                         }
  39.                         kfree_skb(skb);
  40.         }
  41.                 up(&receive_sem);                                /*返回信号量*/
  42.     }while(nlfd && nlfd->receive_queue.qlen);
  43. }
复制代码


因为内核模块可能同时被多个进程同时调用,所以函数中使用了信号量和锁来进行互斥。skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。

程序中注册了一个Netfilter钩子,钩子函数是get_icmp,它截获ICMP数据包,然后调用send_to_user函数将数据发送给应用空 间进程。发送的数据是info结构变量,它是struct packet_info结构,这个结构包含了来源/目的地址两个成员。Netfilter Hook不是本文描述的重点,略过。
send_to_user 用于将数据发送给用户空间进程,发送调用的是API函数netlink_unicast 完成的:
  1. int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
复制代码


参数sk为函数netlink_kernel_create()返回的套接字,参数skb存放待发送的消息,它的data字段指向要发送的netlink 消息结构,而skb的控制块保存了消息的地址信息, 参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函 数在没有接收缓存可利用时睡眠。
向用户空间进程发送的消息包含三个部份:netlink 消息头部、数据部份和控制字段,控制字段包含了内核发送netlink消息时,需要设置的目标地址与源地址,内核中消息是通过sk_buff来管理的, linux/netlink.h中定义了NETLINK_CB宏来方便消息的地址设置:

  1. #define NETLINK_CB(skb)         (*(struct netlink_skb_parms*)&((skb)->cb))
复制代码


例如:

  1. NETLINK_CB(skb).pid = 0;
  2. NETLINK_CB(skb).dst_pid = 0;
  3. NETLINK_CB(skb).dst_group = 1;
复制代码


字段pid表示消息发送者进程ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。
  1. static int send_to_user(struct packet_info *info)
  2. {
  3. int ret;
  4. int size;
  5. unsigned char *old_tail;
  6. struct sk_buff *skb;
  7. struct nlmsghdr *nlh;
  8. struct packet_info *packet;

  9. /*计算消息总长:消息首部加上数据加度*/
  10. size = NLMSG_SPACE(sizeof(*info));

  11. /*分配一个新的套接字缓存*/
  12. skb = alloc_skb(size, GFP_ATOMIC);
  13. old_tail = skb->tail;

  14. /*初始化一个netlink消息首部*/
  15. nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
  16. /*跳过消息首部,指向数据区*/
  17. packet = NLMSG_DATA(nlh);
  18. /*初始化数据区*/
  19. memset(packet, 0, sizeof(struct packet_info));
  20. /*填充待发送的数据*/
  21. packet->src = info->src;
  22. packet->dest = info->dest;

  23. /*计算skb两次长度之差,即netlink的长度总和*/
  24. nlh->nlmsg_len = skb->tail - old_tail;
  25. /*设置控制字段*/
  26. NETLINK_CB(skb).dst_groups = 0;

  27. /*发送数据*/
  28. read_lock_bh(&user_proc.lock);
  29. ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
  30. read_unlock_bh(&user_proc.lock);


  31. }
复制代码


函数初始化netlink 消息首部,填充数据区,然后设置控制字段,这三部份都包含在skb_buff中,最后调用netlink_unicast函数把数据发送出去。
函数中调用了netlink的一个重要的宏NLMSG_PUT,它用于初始化netlink 消息首部:
  1. #define NLMSG_PUT(skb, pid, seq, type, len) \
  2. ({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; \
  3.    __nlmsg_put(skb, pid, seq, type, len); })
  4. static __inline__ struct nlmsghdr *
  5. __nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
  6. {
  7.         struct nlmsghdr *nlh;
  8.         int size = NLMSG_LENGTH(len);

  9.         nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
  10.         nlh->nlmsg_type = type;
  11.         nlh->nlmsg_len = size;
  12.         nlh->nlmsg_flags = 0;
  13.         nlh->nlmsg_pid = pid;
  14.         nlh->nlmsg_seq = seq;
  15.         return nlh;
  16. }
复制代码


这个宏一个需要注意的地方是调用了nlmsg_failure标签,所以在程序中应该定义这个标签。

在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socket:
  1. void sock_release(struct socket * sock);
复制代码


程序在退出模块中释放netlink sockets和netfilter hook:
  1. static void __exit fini(void)
  2. {
  3.   if(nlfd)
  4.     {
  5.       sock_release(nlfd->socket);                /*释放netlink socket*/
  6.     }
  7.   nf_unregister_hook(&imp2_ops);                /*撤锁netfilter 钩子*/
  8. }

这篇关于Linux 用户态与内核态的交互 ——netlink 篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/298027

相关文章

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Linux下MySQL数据库定时备份脚本与Crontab配置教学

《Linux下MySQL数据库定时备份脚本与Crontab配置教学》在生产环境中,数据库是核心资产之一,定期备份数据库可以有效防止意外数据丢失,本文将分享一份MySQL定时备份脚本,并讲解如何通过cr... 目录备份脚本详解脚本功能说明授权与可执行权限使用 Crontab 定时执行编辑 Crontab添加定

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

linux系统上安装JDK8全过程

《linux系统上安装JDK8全过程》文章介绍安装JDK的必要性及Linux下JDK8的安装步骤,包括卸载旧版本、下载解压、配置环境变量等,强调开发需JDK,运行可选JRE,现JDK已集成JRE... 目录为什么要安装jdk?1.查看linux系统是否有自带的jdk:2.下载jdk压缩包2.解压3.配置环境

Linux搭建ftp服务器的步骤

《Linux搭建ftp服务器的步骤》本文给大家分享Linux搭建ftp服务器的步骤,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录ftp搭建1:下载vsftpd工具2:下载客户端工具3:进入配置文件目录vsftpd.conf配置文件4:

Linux实现查看某一端口是否开放

《Linux实现查看某一端口是否开放》文章介绍了三种检查端口6379是否开放的方法:通过lsof查看进程占用,用netstat区分TCP/UDP监听状态,以及用telnet测试远程连接可达性... 目录1、使用lsof 命令来查看端口是否开放2、使用netstat 命令来查看端口是否开放3、使用telnet

Linux系统管理与进程任务管理方式

《Linux系统管理与进程任务管理方式》本文系统讲解Linux管理核心技能,涵盖引导流程、服务控制(Systemd与GRUB2)、进程管理(前台/后台运行、工具使用)、计划任务(at/cron)及常用... 目录引言一、linux系统引导过程与服务控制1.1 系统引导的五个关键阶段1.2 GRUB2的进化优

Linux查询服务器 IP 地址的命令详解

《Linux查询服务器IP地址的命令详解》在服务器管理和网络运维中,快速准确地获取服务器的IP地址是一项基本但至关重要的技能,下面我们来看看Linux中查询服务器IP的相关命令使用吧... 目录一、hostname 命令:简单高效的 IP 查询工具命令详解实际应用技巧注意事项二、ip 命令:新一代网络配置全

linux安装、更新、卸载anaconda实践

《linux安装、更新、卸载anaconda实践》Anaconda是基于conda的科学计算环境,集成1400+包及依赖,安装需下载脚本、接受协议、设置路径、配置环境变量,更新与卸载通过conda命令... 目录随意找一个目录下载安装脚本检查许可证协议,ENTER就可以安装完毕之后激活anaconda安装更

Linux查询服务器系统版本号的多种方法

《Linux查询服务器系统版本号的多种方法》在Linux系统管理和维护工作中,了解当前操作系统的版本信息是最基础也是最重要的操作之一,系统版本不仅关系到软件兼容性、安全更新策略,还直接影响到故障排查和... 目录一、引言:系统版本查询的重要性二、基础命令解析:cat /etc/Centos-release详