当前位置: 首页 > 科技观察

netlink源码分析及内核通信案例分析

时间:2023-03-14 14:01:08 科技观察

本文转载自微信公众号《黑光科技》,作者helight。转载本文请联系黑光科技公众号。前言这几天在看ipvs相关代码的时候又遇到了netlink,于是这两天又花了点时间重新整理了一下netlink。什么是netlinklinux内核一直存在的一个严重问题是内核态和用户态的交互。针对这个问题,内核高手们一直在研究各种方法,让内核和用户态的交互变得安全高效。比如系统调用、proc、sysfs等内存文件系统,但是这些方法一般比较简单,只能在用户空间轮询访问内核变化,不能主动推出内核变化。netlink的出现较好的解决了这个问题,而且netlink还有以下优点:可以直接使用socketAPI在内核态和用户态之间进行通信,开发和使用都比较简单。使用内核协议栈有缓冲队列是一种异步通信机制。可以是内核态和用户态的双向通信,内核可以主动向用户态进程发送消息。这是以前的通讯方式所没有的。对于同一协议类型的所有用户进程,内核可以向所有进程广播消息,也可以指定进程pid发送消息。目前netlink这种机制被广泛应用在各种场景中。Linux内核中有很多应用程序使用netlink来实现应用程序与内核之间的通信;包括:路由守护进程(NETLINK_ROUTE)、用户态套接字协议(NETLINK_USERSOCK)、防火墙(NETLINK_FIREWALL)、netfilter子系统(NETLINK_NETFILTER)、内核事件通知用户态(NETLINK_KOBJECT_UEVENT)等。具体支持的类型请查看这个文件include/uapi/linux/netlink.h。Netlink内核代码走读Netlink内核相关文档介绍netlink内核代码在内核代码的net/netlink/目录下。我目前正在查看内核版本5.7.10。netlink内核相关文件不多,比较清楚:helightxu@~/open_code/linux-5.7.10lsnet/netlinkconfigMakefileaf_netlink.caf_netlink.hdiag.cgenetlink.chelightxu@~/open_code/linux-5.7.10文件说明af_netlink.c和af_netlink.h:是netlink的核心文件,下面也有详细的阅读内容。diag.c监控netlinksock,可以插入内核或从内核中卸载。genetlink.c可以看成是netlink的升级版,或者说是高级封装。注:genetlink.c附加说明:netlink默认支持30多种场景,其他场景没有具体定义。这时候,这种通用的封装就有了很大的好处,可以在不改变内核的情况下实现。应用场景扩展,这部分内容可以看这个wiki:https://wiki.linuxfoundation.org/networking/generic_netlink_howto在include目录下还有一个头文件,如下图,这个头文件是一些辅助函数、宏定义和相关的数据结构,正在学习的同学一定要看这个文件。里面的注释非常详细。这些注释对于理解netlink的消息结构非常有用,建议大家详细看看。helightxu@~/open_code/linux-5.7.10lsinclude/net/netlink.haf_netlink.c在文件af_netlink.c的底部有一行代码:core_initcall(netlink_proto_init);这段代码是什么意思?可以看到这段代码的最终实现,就是告诉编译器将netlink_proto_init函数放到最终编译好的二进制文件的.init段中。内核启动时,会从这一端开始,一个一个地执行函数。这意味着netlink默认直接被内核支持,是nativekernel的一部分(其实我是想和内核的动态插件模块区别开来)。netlink_proto_init函数中最关键的一行代码是下面的最后一行,它将netlink的协议族注册到网络协议栈中。staticconststructnet_proto_familynetlink_family_ops={.family=PF_NETLINK,.create=netlink_create,.owner=THIS_MODULE,/*forconsistency8)*/};...sock_register(&netlink_family_ops);PF_NETLINK是代表netlink的协议族,我们在客户端创建netlink以后Socket就要用到这个东东了。如下代码所示,代码来自于我的测试代码https://github.com/helight/kernel_modules/tree/master/netlink_test中的客户端代码。可以看出:PF_NETLINK表示我们使用的是netlink协议,SOCK_RAW表示我们使用的是原始协议包,NETLINK_USER,是我们自己定义的协议字段。Netlink前面我们说过有30多个应用场景,已经固定在内核代码中,所以客户端在使用的时候,会指定这个字段来表示在内核中与那个应用场景的功能模块进行交互。//intsocket(intdomain,inttype,intprotocol);sock_fd=socket(PF_NETLINK,SOCK_RAW,NETLINK_USER);sock_register这个函数主要是将PF_NETLINK协议类型注册到内核中,让内核识别这个协议,并在内核中建立起来网络协议套接字知道使用哪种协议为其提供操作支持。注册后内核支持netlink协议,接下来就是在内核中创建监听socket,在用户态创建链接socket。netlink用户态与内核交互过程这里我简单画一张图来说明socket通信主要有两个操作对象:server端和client端。netlink的运行原理如下:对象的位置——在服务器的内核中——客户端的用户态进程——netlink的关键数据结构和函数sockaddr_nl协议socketnetlink的地址用sockaddr_nl表示structsockaddr_nl{__kernel_sa_family_tnl_family;/*AF_NETLINK*/unsignedshortnl_pad;/*zero*/__u32nl_pid;/*portID一般为进程id*/__u32nl_groups;/*multicastgroupsmask*/};nl_family发展了一个协议族,netlink有自己独立的值:AF_NETLINK,nl_pid一般取为进程pid。nl_groups用于多播,当不需要多播时,该字段为0。nlmsghdr消息体netlink消息作为socketbuffersk_buff的数据部分传递,它本身分为头和数据。标头是:structnlmsghdr{__u32nlmsg_len;/*Lengthofmessageincludingheader*/__u16nlmsg_type;/*Messagecontent*/__u16nlmsg_flags;/*Additionalflags*/__u32nlmsg_seq;/*Sequencenumber*/__u32nlmsg_pid;标题在里面。nlmsg_pid是发送进程的端口号,用户可以自定义,一般使用进程pid。msghdr用户态发送消息体使用sendmsg和recvmsg函数发送和接收消息,使用的消息体如下所示。structiovec{/*Scatter/gatherarrayitems*/void*iov_base;/*Startingaddress*/size_tiov_len;/*Numberofbytestotransfer*/};/*iov_base:iov_base指向数据包缓冲区,是参数buff,iov_len是长度爱好者。msghdr允许一次传递多个buff,以数组的形式组织在msg_iov中,msg_iovlen记录数组的长度(即有多少个buff)*/structmsghdr{void*msg_name;/*Socketname*/intmsg_namelen;/*Lengthofname*/structiovec*msg_iov;/*Datablocks*/__kernel_size_tmsg_iovlen;/*Numberofblocks*/void*msg_control;/*Perprotocolmagic(egBSDfiledescriptorpassing)*/__kernel_size_tmsg_controllen;/*Lengthofcmsglist*/*unsignedintmsglist}/**/unsignedintmsg_flags;}/*msPacket指向sockaddr_in,netlink指向sockaddr_nl;msg_namelen:msg_name表示地址长度msg_iov:指向缓冲区数组msg_iovlen:缓冲区数组长度msg_control:辅助数据,控制信息(发送任何控制信息)msg_controllen:辅助信息长度msg_flags:消息标志*/逻辑结构如下:socket也是一个特殊的文件,也可以通过VFS接口进行使用和管理。socket本身需要实现文件系统的相应接口,有自己的一套操作方法。netlink通用宏#defineNLMSG_ALIGNTO4U/*宏NLMSG_ALIGN(len)用于取最小值不小于len且字节对齐*/#defineNLMSG_ALIGN(len)(((len)+NLMSG_ALIGNTO-1)&~(NLMSG_ALIGNTO-1))/*Netlinkheaderlength*/#defineNLMSG_HDRLEN((int)NLMSG_ALIGN(sizeof(structnlmsghdr)))/*计算消息数据len的真实消息长度(消息体+ 消息头)*/#defineNLMSG_LENGTH(len)((len)+NLMSG_HDRLEN)/*宏NLMSG_SPACE(len)返回最小值不小于NLMSG_LENGTH(len)且字节对齐*/#defineNLMSG_SPACE(len)NLMSG_ALIGN(NLMSG_LENGTH(len))/*宏NLMSG_DATA(nlh)To获取报文数据部分的首地址,设置和读取报文数据部分时需要用到该宏*/#defineNLMSG_DATA(nlh)((void*)(((char*)nlh)+NLMSG_LENGTH(0)))/*宏NLMSG_NEXT(nlh,len)用于获取下一条消息的首地址,len成为剩余消息的长度*/#defineNLMSG_NEXT(nlh,len)((len)-=NLMSG_ALIGN((nlh)->nlmsg_len),\(structnlmsghdr*)(((char*)(nlh))+NLMSG_ALIGN((nlh)->nlmsg_len)))/*判断消息是否>len*/#defineNLMSG_OK(nlh,len)((len)>=(int)sizeof(structnlmsghdr)&&\(nlh)->nlmsg_len>=sizeof(structnlmsghdr)&&\(nlh)->nlmsg_len<=(len))/*NLMSG_PAYLOAD(nlh,len)是用于返回有效载荷的长度*/#defineNLMSG_PAYLOAD(nlh,len)((nlh)->nlmsg_len-NLMSG_SPACE((len)))netlink内核常用函数netlink_kernel_create该内核函数用于创建内核套接字,提供与用户态的通信staticinlinestructsock*netlink_kernel_create(structnet*net,intunit,structnetlink_kernel_cfg*cfg)/*net:指向所在的网络命名空间,默认输入&init_net(无需定义);在net_namespace.c(externstructnetinit_net)中定义;unit:netlinkprotocoltypecfg:cfg存放netlink内核配置参数(如下)*//*optionalNetlinkkernelconfigurationparameters*/structnetlink_kernel_cfg{unsignedintgroups;unsignedintflags;void(*input)(structsk_buff*skb);/*输入回调函数*/structmutex*cb_mutex;void(*bind)(intgroup);bool(*compare)(structnet*net,structsock*sk);};单播函数netlink_unicast()和多播函数netlink_broadcast()/*发送单播消息*/externintnetlink_unicast(structsock*ssk,structsk_buff*skb,__u32portid,intnonblock);/*ssk:netlinksocketskb:skbbuffpointerportid:通信端口号nonblock:表示函数是否是非阻塞的,如果为1,没有时函数会立即返回接收缓冲区可用,如果为0,则当没有接收缓冲区时函数会立即返回可以使用定时休眠*//*发送组播消息*/externintnetlink_broadcast(structsock*ssk,structsk_buff*skb,__u32portid,__u32group,gfp_tallocation);/*ssk:同上(对应netlink_kernel_create返回值),skb:kernelskbbuffportid:portidgroup:allocationofthealltargetmulticastgroups对应掩码的“或”操作:指定内核内存分配方式,通常GFP_ATOMIC用于中断上下文,GFP_KERNEL用于其他场合。存在此参数是因为API可能需要分配一个或多个缓冲区来克隆多播消息。*/测试示例代码netlink内核搭建socket进程内核代码很简单,这里给出核心代码,仅此而已,在接收函数中直接打印接收到的消息。#include#include#include#include#include#include#include#defineNETLINK_XUX31/*testing*/staticstructsock*xux_sock=NULL;//接收消息的回调函数,接收参数为sk_buffstaticvoidrecv_netlink(structsk_buff*skb){structnlmsghdr*nlh;nlh=nlmsg_hdr(skb);//获取消息体printk("receivedatafromuserprocess:%s",(char*)NLMSG_DATA(nlh));//打印接收到的数据内容...}int__initinit_link(void){structnetlink_kernel_cfgcfg={.input=recv_netlink,};xux_sock=netlink_kernel_create(&init_net,NETLINK_XUX,&cfg);//创建内核socketif(!xux_sock){printk("cannotinitializenetlinksocket");return-1;}printk("InitOK!\n");返回0;}netlink用户态建立链接和收发信息...//以上保存,char*argv[]){sock_fd=socket(PF_NETLINK,SOCK_RAW,NETLINK_USER);//创建socketif(sock_fd<0)return-1;memset(&src_addr,0,sizeof(src_addr));src_addr.nl_family=AF_NETLINK;src_addr.nl_pid=getpid();/*当前进程的pid*/if(bind(sock_fd,(structsockaddr*)&src_addr,sizeof(src_addr))){//Socket与指定协议绑定perror("bind()error\n");close(skfd);return-1;}memset(&dest_addr,0,sizeof(dest_addr));dest_addr.nl_family=AF_NETLINK;dest_addr.nl_pid=0;/*ForLinuxKernel*/dest_addr.nl_groups=0;/*单播*/nlh=(structnlmsghdr*)malloc(NLMSG_SPACE(MAX_PAYLOAD));memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));nlh->nlmsg_len=NLMSG_SPACE(MAX_PAYLOAD);nlh->nlmsg_pid=getpid();//selfpidnlh->nlmsg_flags=0;//复制信息到发送缓冲区strcpy(NLMSG_DATA(nlh),"Hellothisisamsgfromuserspace");//构造发送消息体iov.iov_base=(void*)nlh;//iov->nlhiov.iov_len=nlh->nlmsg_len;msg.msg_name=(void*)&dest_addr;msg.msg_namelen=sizeof(dest_addr);msg.msg_iov=&iov;//iov存储netlink消息头和消息数据msg.msg_iovlen=1;printf("Sendingmessagetokernel\n");intret=sendmsg(sock_fd,&msg,0);//向内核发送消息printf("sendret:%d\n",ret);printf("Waitingformessagefromkernel\n");/*从内核接收消息*/recvmsg(sock_fd,&msg,0);printf("Receivedmessagepayload:%s\n",NLMSG_DATA(nlh));//打印接收到的消息close(sock_fd);返回0;}以上代码在我个人仓库。有兴趣的可以自己克隆测试:https://github.com/helight/kernel_modules/tree/master/netlink_test。总结Netlink目前感觉是一种比较好用的内核和用户空间的交互方式,但是也有它的使用场景,适合用户空间和内核空间的主动交互。但在单机场景下,大部分主动权在用户进程,用户进程向内核写入数据,用户进程主动读取内核数据。这两个场景涵盖了内核的大部分场景。在需要激活内核的场景下,netlink比较合适。我能想到的是内核数据审计,安全触发等,在这种场景下,内核可以实时告知进程内核的用户。在看ipvs的代码的时候,看到里面有netlink的使用。我发现早期的iptables是使用netlink来下发配置指令的。内核中的netfilter和iptables中也有这部分代码。今天顺便下载下来,粗略看了下,可以搜索关键词NETLINK看。不过iptables后来的代码中并没有使用这种方式,而是采用了一个叫iptc的库。核心思想是使用setsockops,最后copy_from_user。不过这种方式对于iptables的配置下发场景还是很实用的。最后,如果你对这方面感兴趣,可以来找我一起学习。我也想继续深入看看,但是目前没有什么好的场景。明天开始阅读kubernetes的代码。如果你有兴趣,也可以和我交流。参考https://www.cnblogs.com/x_wukong/p/5920437.html