熟悉Linuxwifi的同学都知道,wpasupplicant程序是基于netlink与wifi驱动进行通信的。(wpasupplicant是wifistation用户空间守护进程) 本文学习下Linux的netlink,给出用户空间与内核空间基于netlink通信的示例。示例包括netlink和genericnetlink。用户空间程序包括基于原生LinuxAPI和基于libnlAPI。netlink基础 netlink协议是一个基于socket的,用于内核与用户空间进程通信的一个协议。 用户态创建netlinksocket的代码如下:intfdsocket(AFNETLINK,SOCKRAW,MYNETLINK)socket接口的原型为:intsocket(intdomain,inttype,intprotocol);这里用到了第三个入参protocol,即创建Netlinksocket时要指定协议号 内核态创建netlinksocket的接口原型为:structsocknetlinkkernelcreate(structnetnet,intunit,structnetlinkkernelcfgcfg)这里的第二个参数unit为协议号,与用户空间的protocol相同 Netlink消息有固定的格式,structnlmsghdrstructnlmsghdr{u32nlmsglen;Lengthofmessageincludingheaderu16nlmsgtype;Messagecontentu16nlmsgflags;Additionalflagsu32nlmsgseq;Sequencenumberu32nlmsgpid;SendingprocessportID}; 完整示例见github:https:github。comjiansoftnetlinkexamples 下文为关键代码注释。原生LinuxAPI示例内核侧示例定义自己的netlink协议号defineMYNETLINK31接收回调,即内核侧收到用户发来的netlink消息回调staticvoidnetlinkrecvmsg(structskbuffskb){。。。nlh(structnlmsghdr)skbdata;取netlink消息头pidnlhnlmsgpid;pidofsendingprocessmsg(char)nlmsgdata(nlh);取netlink消息data部分msgsizestrlen(msg);printk(KERNINFOnetlinkkernel:Receivedfrompidd:s,pid,msg);。。。}定义netlinkkernelcfg,即声明接收回调structnetlinkkernelcfgcfg{。inputnetlinkrecvmsg,};创建内核测netlinksocketgnlsocknetlinkkernelcreate(initnet,MYNETLINK,cfg);if(!gnlsock){printk(KERNALERTnetlinkkernel:Errorcreatingsocket。);return10;}用户侧示例intmain(intargc,charargv〔〕){创建socketsockfdsocket(PFNETLINK,SOCKRAW,MYNETLINK);memset(srcaddr,0,sizeof(srcaddr));srcaddr。nlfamilyAFNETLINK;srcaddr。nlpidgetpid();selfpid绑定端口bind(sockfd,(structsockaddr)srcaddr,sizeof(srcaddr));设置目标地址为内核netlinkmemset(destaddr,0,sizeof(destaddr));destaddr。nlfamilyAFNETLINK;destaddr。nlpid0;ForLinuxKerneldestaddr。nlgroups0;unicast可以通过sendmsg或sendto两种接口向内核发送消息相应的有recvmsg和recvfrom两种接口接收来自内核的消息详见github,此处略}libnlAPI示例 libnl对用户空间Linu原生的netlinkAPI进行了封装,使得用户空间程序更容易编写,尤其是对于genericnetlinkAPI。关于genericnetlinkAPI我们下一章节详细介绍。 另外wpasupplicant与内核wifi驱动的通信就是用的libnlgenericnetlinkAPI。 先介绍下libnl的主要接口,定义在头文件创建netlinksocket,libnl中用structnlsock表示一个socketincludenetlinksocket。hstructnlsocknlsocketalloc(void)voidnlsocketfree(structnlsocksk)回调配置structnlcbnlsocketgetcb(conststructnlsocksk);voidnlsocketsetcb(structnlsocksk,structnlcbcb);intnlsocketmodifycb(structnlsock,enumnlcbtype,enumnlcbkind,nlrecvmsgmsgcbt,void);发送intnlsendauto(structnlsocksk,structnlmsgmsg)intnlsend(structnlsocksk,structnlmsgmsg)intnlsendiovec(structnlsocksk,structnlmsgmsg,structioveciov,unsignediovlen)intnlsendmsg(structnlsocksk,structnlmsgmsg,structmsghdrhdr)nlsendmsg里调用Linux原生sendmsg接口intnlsendto(structnlsocksk,voidbuf,sizetsize)nlsendto调用Linux原生sendto接口intnlsendsimple(structnlsocksk,inttype,intflags,voidbuf,sizetsize)接收intnlrecvmsgsdefault(structnlsocksk)intnlrecvmsgs(structnlsocksk,structnlcbcb)如果socket是阻塞的,就阻塞式接收。recv到数据之后,通过cb进行处理libnl用户侧示例 基于上一节的例子,内核测代码不变,用户侧使用libnl重写。includenetlinknetlink。hincludenetlinkmsg。hdefineMYNETLINK31defineMYNETLINKTYPESET0接收回调staticintmyinput(structnlmsgmsg,voidarg){structnlmsghdrnlhnlmsghdr(msg);chardatanlmsgdata(nlh);intdatalennlmsgdatalen(nlh);printf(inputcb:datalen:d,data:d,datalen,data);return0;}intmain(intargc,charargv〔〕){structnlsocksk;intret;创建并绑定socketsknlsocketalloc();retnlconnect(sk,MYNETLINK);修改接收回调函数,收到任何消息都会回调myinputnlsocketmodifycb(sk,NLCBMSGIN,NLCBCUSTOM,myinput,NULL);charmsg〔〕Hellolibnl!retnlsendsimple(sk,MYNETLINKTYPESET,0,msg,sizeof(msg));阻塞式等待接收。接收到内核发来的消息后,会进入接收回调myinputnlrecvmsgsdefault(sk);nlsocketfree(sk);}genericnetlink示例,基于libnl netlink通信协议在不修改内核源码的情况下,最大只支持定义32种协议。随着netlink的使用越来越多,32个协议号已不够用,所以引入了genericnetlink。genericnetlink其实是对netlink报文进行了又一次封装,genericnetlink使用的netlink协议号是NETLINKGENERIC16。 genl的消息格式如下:012301234567890123456789012345678901Netlinkmessageheader(nlmsghdr)GenericNetlinkmessageheader(genlmsghdr)OptionaluserspecificmessageheaderOptionalGenericNetlinkmessagepayloadstructgenlmsghdr{u8cmd;u8version;u16reserved;};genl的messagepayload基于netlink的属性机制,即payload是由一个个nlattr组成NLAHDRLENNLAALIGN(payload)HeaderPadPayloadPad(structnlattr)ingingnlattrnlalenstructnlattr{u16nlalen;u16nlatype;};genl内核侧示例 参考:https:wiki。linuxfoundation。orgnetworkinggenericnetlinkhowto 注意:genlregisterops接口只在3。12及之前版本有;3。134。9版本用genlregisterfamilywithops;4。10版本及以后没有注册ops的接口,只有注册family的接口,ops要直接定义在family内。 本文示例基于4。15内核。 注册genericnetlinkfamily需要3步:定义操作定义family注册familyStep1:定义操作attributesenum{EXMPLAUNSPEC,EXMPLAMSG,EXMPLAMAX,};defineEXMPLAMAX(EXMPLAMAX1)attributepolicystaticstructnlapolicyexmplgenlpolicy〔EXMPLAMAX1〕{〔EXMPLAMSG〕{。typeNLANULSTRING},};handlerstaticintexmplecho(structskbuffskb,structgenlinfoinfo);commandsenum{EXMPLCUNSPEC,EXMPLCECHO,EXMPLCMAX,};defineEXMPLCMAX(EXMPLCMAX1)operationdefinitionstructgenlopsexmplgenlops〔EXMPLCMAX〕{{。cmdEXMPLCECHO,。doitexmplecho,。policyexmplgenlpolicy,}};defineFAMILYNAMEmygenlStep2:定义familyfamilydefinitionstaticstructgenlfamilymygenlfamily{。id0,。hdrsize0,表示没有用户自定义的额外header。nameFAMILYNAME,。version1,。opsexmplgenlops,。nopsARRAYSIZE(exmplgenlops),。maxattrEXMPLAMAX1,};handler的具体定义staticintexmplecho(structskbuffskb,structgenlinfoinfo){structnlattrna;structskbuffreplyskb;voidmsghead;intret;printk(sin。,func);内核已经解析好了每个attrnainfoattrs〔EXMPLAMSG〕;if(!na){printk(Error:attrEXMPLAMSGisnull);returnEINVAL;}printk(Recvmessage:s,nladata(na));将收到的消息发回去replyskbgenlmsgnew(NLMSGGOODSIZE,GFPKERNEL);填写genl消息头msgheadgenlmsgput(replyskb,infosndportid,infosndseq,mygenlfamily,0,EXMPLCECHO);向skb尾部填写attrnlaputstring(replyskb,EXMPLAMSG,nladata(na));Finalizethemessage:更新nlmsghdr中的nlmsglen字段genlmsgend(replyskb,msghead);Sendthemessagebackretgenlmsgreply(replyskb,info);if(ret!0){printk(genlmsgreplyreturnfail:d,ret);returnret;}return0;}Step3:注册famliyintret;retgenlregisterfamily(mygenlfamily);if(err!0){printk(genlregisterfamilyfail,ret:d,ret);returnret;}genl用户侧示例(基于libnl)defineMYFAMILYNAMEmygenl用户侧需要定义和内核侧相同的属性以及命令,所以通常把这一部分摘成一个独立的。h,内核和app共用这里没有摘成一个独立的。h,用户侧也重复定义一份attributesenum{EXMPLAUNSPEC,EXMPLAMSG,EXMPLAMAX,};defineEXMPLAMAX(EXMPLAMAX1)defineattributepolicystaticstructnlapolicyexmplgenlpolicy〔EXMPLAMAX1〕{〔EXMPLAMSG〕{。typeNLASTRING},};commandsenum{EXMPLCUNSPEC,EXMPLCECHO,EXMPLCMAX,};defineEXMPLCMAX(EXMPLCMAX1)接收回调定义intrecvcallback(structnlmsgrecvmsg,voidarg){structnlmsghdrnlhnlmsghdr(recvmsg);structnlattrtbmsg〔EXMPLAMAX1〕;if(nlhnlmsgtypeNLMSGERROR){printf(ReceivedNLMSGERRORmessage!);returnNLSTOP;}structgenlmsghdrgnlh(structgenlmsghdr)nlmsgdata(nlh);按照每attr解析内核发来的genl消息nlaparse(tbmsg,EXMPLAMAX,genlmsgattrdata(gnlh,0),genlmsgattrlen(gnlh,0),exmplgenlpolicy);判断是否包含属性EXMPLAMSGif(tbmsg〔EXMPLAMSG〕){parseitasstringcharpayloadmsgnlagetstring(tbmsg〔EXMPLAMSG〕);printf(Kernelreplied:s,payloadmsg);}else{printf(AttributeEXMPLAMSGismissing);}returnNLOK;}intmain(intargc,charargv〔〕){创建并连接genlsocketstructnlsocksknlsocketalloc();genlconnect(sk);根据FAMILYNAME获得对应的famlilyidintfamilyid;familyidgenlctrlresolve(sk,FAMILYNAME);if(familyid0){printf(genericnetlinkfamilyFAMILYNAMENOTREGISTERED);nlsocketfree(sk);exit(1);}else{printf(FamilyIDofgenericnetlinkfamilyFAMILYNAMEis:d,familyid);}设置接收回调nlsocketmodifycb(sk,NLCBMSGIN,NLCBCUSTOM,recvcallback,NULL);发送消息structnlmsgmsgnlmsgalloc();genlmsgput(msg,NLAUTOPORT,NLAUTOSEQ,familyid,0,NLMFREQUEST,EXMPLCECHO,1);NLAPUTSTRING(msg,EXMPLAMSG,genlmessagefromusertokernel);intresnlsendauto(sk,msg);nlmsgfree(msg);if(res0){printf(nlsendautofail,ret:d,res);}else{printf(nlsendautoOK,ret:d,res);}接收消息。接收到内核发来的消息后,触发回调recvcallbacknlrecvmsgsdefault(sk);nlaputfailure:referencedbyNLAPUTSTRINGnlsocketfree(sk);return0;} 这里的示例是内核收到用户空间发来的genl消息后,根据发送端的structgenlinfoinfo,调用genlmsgreply(replyskb,info),将内核的genl消息单播给用户空间app。 如果内核不知道用户空间的socket信息,内核如何将消息发送到用户空间呢?这时一般用组播netlink消息,即内核将消息组播出去。用户空间谁订阅了这个组播,谁就能收到内核发来的消息。关于组播netlink示例,后续有空再补一下 完整示例见github:https:github。comjiansoftnetlinkexamples