介绍
netlink socekt是一种用于在内核态和用户态进程之间进行数据传输的特殊的IPC。它通过为内核模块提
供一组特殊的API,并为用户程序提供了一组标准的socket 接口的方式,实现了一种全双工的通讯连接。类似于TCP/IP中使用AF_INET地址族一样,netlink socket使用地址族AF_NETLINK。每一个netlink
socket在内核头文件
include/linux/netlink.h
中定义自己的协议类型。
下面是netlink socket 目前的特性集合以及它支持的协议类型:
NETLINK_ROUTE 用户空间的路由守护程序之间的通讯通道,比如BGP,OSPF,RIP以及内核数据转发模块。用户态的路由守护程序通过此类型的协议来更新内核中的路由表。
NETLINK_FIREWALL:接收IPV4防火墙代码发送的数据包。
NETLINK_NFLOG:用户态的iptables管理工具和内核中的netfilter模块之间通讯的通道。
NETLINK_ARPD:用来从用户空间管理内核中的ARP表。
为什么以上的功能在实现用户程序和内核程序通讯时,都使用netlink方法而不是系统调用,ioctls
或者proc文件系统呢?原因在于:为新的特性添加一个新的系统调用,ioctls或者一个proc文件的做法并不是很容易的一件事情,因为我们要冒着污染内核代码并且可能破坏系统稳定性的风险去完成这件事情。
然而,netlink socket却是如此的简单,你只需要在文件netlink.h中添加一个常量来标识你的协议类型,然后,内核模块和用户程序就可以立刻使用socket风格的API进行通讯了!
Netlink提供了一种异步通讯方式,与其他socket API一样,它提供了一个socket队列来缓冲或者平滑
瞬时的消息高峰。发送netlink消息的系统调用在把消息加入到接收者的消息对列后,会触发接收者的接收处理函数。接收者在接收处理函数上下文中,可以决定立即处理消息还是把消息放在队列中,在以后其它上下文去处理它(因为我们希望接收处理函数执行的尽可能快)。系统调用与netlink不同,它需要一个同步的处理,因此,当我们使用一个系统调用来从用户态传递消息到内核时,如果处理这个消息的时间很长的话,内核调度的粒度就会受到影响。
内核中实现系统调用的代码都是在编译时静态链接到内核的,因此,在动态加载模块中去包含一个系统调用的做法是不合适的,那是大多数设备驱动的做法。使用netlink socket时,动态加载模块中的netlink程序不会和linux内核中的netlink部分产生任何编译时依赖关系。
Netlink优于系统调用,ioctls和proc文件系统的另外一个特点就是它支持多点传送。一个进程可以把消息传输给一个netlink组地址,然后任意多个进程都可以监听那个组地址(并且接收消息)。这种机制为内核到用户态的事件分发提供了一种近乎完美的解决方案。
系统调用和ioctl都属于单工方式的IPC,也就是说,这种IPC会话的发起者只能是用户态程序。但是,如果内核有一个紧急的消息想要通知给用户态程序时,该怎么办呢?如果直接使用这些IPC的话,是没办法做到这点的。通常情况下,应用程序会周期性的轮询内核以获取状态的改变,然而,高频度的轮询势必会增加系统的负载。Netlink 通过允许内核初始化会话的方式完美的解决了此问题,我们称之为netlink socket的双工特性。
最后,netlink socket提供了一组开发者熟悉的BSD风格的API函数,因此,相对于使用神秘的系统调用API或者ioctl而言,netlink开发培训的费用会更低些。
与BSD的Routing socket的关系
在BSD TCP/IP的协议栈实现中,有一种特殊的socket叫做Routing socket.它的地址族为AF_ROUTE, 协议族为PF_ROUTE, socket类型为SOCK_RAW. 这种Routing socket是用户态进程用来向内核中的路由表增加或者删除路由信息用的。在Linux系统中,netlink socket通过协议类型NETLINK_ROUTE实现了与Routing socket相同的功能,可以说,netlink socket提供了BSD Routing socket功能的超集。
实例
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_RECV_SIZE 1024
static int Init_USB_Monitor( void )
{
// 本函数返回值
int iResult = 0;
// 接收内核发来的消息缓冲区大小
const unsigned int uiRecvBuffSize = MAX_RECV_SIZE;
// 套接字地址
struct sockaddr_nl snl;
// 套接字文件描述符
int sfd = -1;
// 1.添写套接字地址
snl.nl_family = AF_NETLINK;
snl.nl_pad = 0;
// 如果希望内核处理消息或多播消息,就把该字段设置为 0,
// 否则设置为处理消息的进程ID。
snl.nl_pid = getpid();
snl.nl_groups = 1;
// 2.创建套接字
// NETLINK_KOBJECT_UEVENT - 内核消息到用户空间,出现在 Linux 2.6.10
sfd = socket( PF_NETLINK, // 使用 netlink
SOCK_DGRAM, // 使用不连续不可信赖的数据包连接
NETLINK_KOBJECT_UEVENT );
// 如果 创建套接字失败 的话,则
if ( -1 == sfd )
{
return -1;
}
// 3.设置套接字接收缓冲区大小
setsockopt( sfd,
SOL_SOCKET, // 存取 socket 层
SO_RCVBUF, // 设置接收缓冲区大小
&uiRecvBuffSize,
sizeof( uiRecvBuffSize ) );
// 4.将套接字加入指定的多播组
iResult = bind( sfd,
(struct sockaddr*)&snl,
sizeof( snl ) );
// 如果 将套接字加入指定的多播组失败 的话,则
if ( -1 == iResult )
{
return -2;
}
return sfd;
}
int USB_Monitoring(int fd)
{
// 接收内核发来的消息字符串
char caKernelMsgBuff[MAX_RECV_SIZE];
if(fd <= 0)
{
return -1;
}
// 接收内核消息
recv(fd, &caKernelMsgBuff, sizeof( caKernelMsgBuff ), 0);
printf( "Kernel Message:\n%s\n", caKernelMsgBuff );
#if 0
// USB 设备的插入时,会出现以 add@/devices/ 开头,含 usb 的字符串
if( 0 == memcmp( caKernelMsgBuff, "add@",4 )&&NULL != strstr( caKernelMsgBuff,"usb" ) )
{
printf( "Add USB Device\n" );
//break;
}
// USB 设备的拔除时,会出现以 remove@/devices/ 开头,含 usb 的字符串
if(0 == memcmp( caKernelMsgBuff,"remove@",7 )&&NULL != strstr( caKernelMsgBuff,"usb" ) )
{
printf( "Remove USB Device\n" );
//break;
}
#endif
return 0;
}
int main(int argc, char* argv[])
{
int fd = Init_USB_Monitor();
while(1)
{
USB_Monitoring(fd);
}
if (fd > 0)
{
close(fd);
fd = -1;
}
return 0;
}