|
需要在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的地址结构如下:
- structsockaddr_nl
- {
- sa_family_tnl_family;
- unsignedshortnl_pad;
- __u32nl_pid;
- __u32nl_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的通信协议号。每个协议号代表一种“应用”,上层可以用内核已经定义的协议和内核进行通信,获得内核已经提供的信息。具体支持的协议列表如下:
- #defineNETLINK_ROUTE0/*Routing/devicehook*/
- #defineNETLINK_UNUSED1/*Unusednumber*/
- #defineNETLINK_USERSOCK2/*Reservedforusermodesocketprotocols*/
- #defineNETLINK_FIREWALL3/*Firewallinghook*/
- #defineNETLINK_INET_DIAG4/*INETsocketmonitoring*/
- #defineNETLINK_NFLOG5/*netfilter/iptablesULOG*/
- #defineNETLINK_XFRM6/*ipsec*/
- #defineNETLINK_SELINUX7/*SELinuxeventnotifications*/
- #defineNETLINK_ISCSI8/*Open-iSCSI*/
- #defineNETLINK_AUDIT9/*auditing*/
- #defineNETLINK_FIB_LOOKUP10
- #defineNETLINK_CONNECTOR11
- #defineNETLINK_NETFILTER12/*netfiltersubsystem*/
- #defineNETLINK_IP6_FW13
- #defineNETLINK_DNRTMSG14/*DECnetroutingmessages*/
- #defineNETLINK_KOBJECT_UEVENT15/*Kernelmessagestouserspace*/
- #defineNETLINK_GENERIC16
- #defineNETLINK_SCSITRANSPORT18/*SCSITransports*/
- #defineNETLINK_ECRYPTFS19
协议的用途很好理解,比如我们单纯创建一个上层应用,通过和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的代码样例:
- {
- structsockaddr_nladdr;
- intflags;
- s_nlm_socket=socket(PF_NETLINK,SOCK_DGRAM,NETLINK_XXX);
- if(s_nlm_socket<0)
- {
- USE_DBG_OUT("createnetlinksocketerror.\r\n");
- gotoErr_Exit;
- }
- addr.nl_family=PF_NETLINK;
- addr.nl_pad=0;
- addr.nl_pid=getpid();
- addr.nl_groups=0;
- if(bind(s_nlm_socket,(structsockaddr*)&addr,sizeof(addr))<0)
- {
- USE_DBG_OUT("bindsocketerror.\r\n");
- gotoErr_Exit;
- }
- flags=fcntl(s_nlm_socket,F_GETFL,0);
- fcntl(s_nlm_socket,F_SETFL,flags|O_NONBLOCK);
- return0;
- Err_Exit:
- return-1;
- }
用户态接收、发送消息的API:
用户态用sendto向内核发送netlink消息,用recvfrom接收消息。只是注意,发送、接收的时候在自己附带的消息前面要加上一个netlink的消息头。例如,定义一个如下的消息通信结构:
- structtag_rcv_buf
- {
- structnlmsghdrhdr;
- netlink_notify_smy_msg;
- }st_snd_buf;
发送代码的例子:
- My_send_msg
- {
- structtag_rcv_buf
- {
- structnlmsghdrhdr;
- netlink_notify_smy_msg;
- }st_snd_buf;
- fd_setst_write_set;
- structtimevalwrite_time_out={10,0};
- intret;
- FD_ZERO(&st_write_set);
- FD_SET(s_nlm_socket,&st_write_set);
- st_snd_buf.hdr.nlmsg_len=sizeof(st_snd_buf);
- st_snd_buf.hdr.nlmsg_flags=0;
- st_snd_buf.hdr.nlmsg_type=0;
- st_snd_buf.hdr.nlmsg_pid=getpid();
- 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)
- {
- USE_DBG_OUT("sendhassomeerror%d.\n",errno);
- gotoout;
- }
- elseif(ret==0)
- {
- TMP_DBG_OUT("sendtimeout.\n");
- gotoout;
- }
- else
- {
- ret=sendto(s_nlm_socket,&st_snd_buf,sizeof(st_snd_buf),0,
- (structsockaddr*)&s_peer_addr,sizeof(s_peer_addr));
- if(ret<0)
- {
- USE_DBG_OUT("sendtokernalbynlerror%d\r\n",errno);
- }
- else
- {
- TMP_DBG_OUT("sendtokernaloks_idis%d,e_idis%d.\r\n",s_id,e_id);
- }
- }
- out:
- return;
- }
接收数据的代码例子:
- {
- structtag_rcv_buf
- {
- structnlmsghdrhdr;
- netlink_notify_smy_msg;
- }st_rcv_buf;
- intret,addr_len,io_ret;
- structsockaddr_nlst_peer_addr;
- fd_setst_read_set;
- structtimevalread_time_out={10,0};
- intrcv_buf;
- st_peer_addr.nl_family=AF_NETLINK;
- st_peer_addr.nl_pad=0;
- st_peer_addr.nl_pid=0;
- st_peer_addr.nl_groups=0;
- addr_len=sizeof(st_peer_addr);
- FD_ZERO(&st_read_set);
- FD_SET(s_nlm_socket,&st_read_set);
- ret=select(s_nlm_socket+1,&st_read_set,NULL,NULL,&read_time_out);
- if(ret==-1)
- {
- USE_DBG_OUT("selectrcvsomeerror%d",errno);
- gotoerr;
- }
- elseif(ret==0)
- {
- TMP_DBG_OUT("rcvtimeout.\n");
- *p_size=0;
- gotoout;
- }
- else
- {
- ret=recvfrom(s_nlm_socket,&st_rcv_buf,sizeof(st_rcv_buf),0,
- (structsockaddr*)&st_peer_addr,&addr_len);
- }
- if(ret==sizeof(st_rcv_buf))
- {
- else
- {
- USE_DBG_OUT("rcvmsghavesomeerr.retis%d,errnois%d\r\n",ret,errno);
- gotoerr;
- }
- out:
- return0;
- err:
- *p_size=0;
- return-1;
- }
|