您的位置:首页 > 运维架构 > Linux

netlink---Linux下基于socket的内核和上层通信机制 (转)

2016-03-29 15:56 549 查看
需要在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的地址结构如下:

[cpp]
view plaincopy

struct sockaddr_nl

{

sa_family_t nl_family; //必须为AF_NETLINK或者PF_NETLINK

unsigned short nl_pad; //必须为0

__u32 nl_pid; //通信端口

__u32 nl_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的通信协议号。每个协议号代表一种“应用”,上层可以用内核已经定义的协议和内核进行通信,获得内核已经提供的信息。具体支持的协议列表如
下:

[cpp]
view plaincopy

#define NETLINK_ROUTE 0 /* Routing/device hook */

#define NETLINK_UNUSED 1 /* Unused number */

#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */

#define NETLINK_FIREWALL 3 /* Firewalling hook */

#define NETLINK_INET_DIAG 4 /* INET socket monitoring */

#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */

#define NETLINK_XFRM 6 /* ipsec */

#define NETLINK_SELINUX 7 /* SELinux event notifications */

#define NETLINK_ISCSI 8 /* Open-iSCSI */

#define NETLINK_AUDIT 9 /* auditing */

#define NETLINK_FIB_LOOKUP 10

#define NETLINK_CONNECTOR 11

#define NETLINK_NETFILTER 12 /* netfilter subsystem */

#define NETLINK_IP6_FW 13

#define NETLINK_DNRTMSG 14 /* DECnet routing messages */

#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */

#define NETLINK_GENERIC 16

/* leave room for NETLINK_DM (DM Events) */

#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */

#define NETLINK_ECRYPTFS 19

协议的用途很好理解,比如我们单纯创建一个上层应用,通过和
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的代码样例:

[cpp]
view plaincopy

{

struct sockaddr_nl addr;

int flags;

//建立netlink socket

s_nlm_socket = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX);

if(s_nlm_socket < 0)

{

USE_DBG_OUT("create netlink socket error.\r\n");

goto Err_Exit;

}

//bind

addr.nl_family = PF_NETLINK;

addr.nl_pad = 0;

addr.nl_pid = getpid();

addr.nl_groups = 0;

if(bind(s_nlm_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0)

{

USE_DBG_OUT("bind socket error.\r\n");

goto Err_Exit;

}

//设置socket为非阻塞模式

flags = fcntl(s_nlm_socket, F_GETFL, 0);

fcntl(s_nlm_socket, F_SETFL, flags|O_NONBLOCK);

return 0;

Err_Exit:

return -1;

}

用户态接收、发送消息的API:

用户态用sendto向内核发送netlink消息,用recvfrom接收消息。只是注意,发送、接收的时候在自己附带的消息前面要加上一个netlink的消息头。例如,定义一个如下的消息通信结构:

[cpp]
view plaincopy

struct tag_rcv_buf

{

struct nlmsghdr hdr; //netlink的消息头

netlink_notify_s my_msg; //通信实体消息

}st_snd_buf;

发送代码的例子:

[cpp]
view plaincopy

My_send_msg

{

struct tag_rcv_buf

{

struct nlmsghdr hdr; //netlink的消息头

netlink_notify_s my_msg; //通信实体消息

}st_snd_buf;

fd_set st_write_set; //select fd,避免线程吊死

struct timeval write_time_out = {10, 0}; //10秒超时

int ret;

//设置select

FD_ZERO(&st_write_set);

FD_SET(s_nlm_socket, &st_write_set);

/*

设置发送数据

*/

st_snd_buf.hdr.nlmsg_len = sizeof(st_snd_buf); //NLMSG_LENGTH(sizeof(netlink_notify_s))--这个宏包含有头

st_snd_buf.hdr.nlmsg_flags = 0; /*消息的附加选项,没啥用*/

st_snd_buf.hdr.nlmsg_type = 0; /*设置自定义消息类型*/

st_snd_buf.hdr.nlmsg_pid = getpid(); /*设置发送者的PID*/

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)

{

//have some error.

USE_DBG_OUT("send has some error %d.\n", errno);

goto out;

}

else if(ret == 0)

{

//超时退出

TMP_DBG_OUT("send timeout.\n");

goto out;

}

else

{

//接收消息

ret = sendto(s_nlm_socket, &st_snd_buf, sizeof(st_snd_buf), 0,

(struct sockaddr*)&s_peer_addr, sizeof(s_peer_addr));

if(ret < 0)

{

USE_DBG_OUT("send to kernal by nl error %d\r\n", errno);

}

else

{

TMP_DBG_OUT("send to kernal ok s_id is %d, e_id is %d.\r\n", s_id, e_id);

}

}

out:

return;

}

接收数据的代码例子:

[cpp]
view plaincopy

{

struct tag_rcv_buf

{

struct nlmsghdr hdr; //netlink的消息头

netlink_notify_s my_msg; //通信实体消息

}st_rcv_buf;

int ret, addr_len, io_ret;

struct sockaddr_nl st_peer_addr;

fd_set st_read_set; //select fd,避免线程吊死

struct timeval read_time_out = {10, 0}; //10秒超时

int rcv_buf;

//设置内核的通信地址

st_peer_addr.nl_family = AF_NETLINK;

st_peer_addr.nl_pad = 0; /*always set to zero*/

st_peer_addr.nl_pid = 0; /*kernel's pid is zero*/

st_peer_addr.nl_groups = 0; /*multicast groups mask, if unicast set to zero*/

addr_len = sizeof(st_peer_addr);

//设置select

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)

{

//have some error.

USE_DBG_OUT("select rcv some error %d", errno);

goto err;

}

else if(ret == 0)

{

//超时退出

TMP_DBG_OUT("rcv timeout.\n");

*p_size = 0;

goto out;

}

else

{

//接收消息

ret = recvfrom(s_nlm_socket, &st_rcv_buf, sizeof(st_rcv_buf), 0,

(struct sockaddr *)&st_peer_addr, &addr_len);

}

if(ret == sizeof(st_rcv_buf) )

{

//收到消息了...

else

{

USE_DBG_OUT("rcv msg have some err. ret is %d, errno is %d\r\n", ret, errno);

goto err;

}

out:

return 0;

err:

*p_size = 0;

return -1;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: