Netlink是一种在内核态和用户态可以进行双向通信的机制,也就是说,用户进程既可以作为服务器端又可以作为客户端,内核也是如此。用户进程和内核谁是服务器端谁是客户端,这个问题与谁先主动发起数据交互会话有关。
用户进程主动向内核发起会话在Linux内核中很常见,比如系统调用、对/proc的操作等。本文通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。在该程序中,用户进程向内核发送一段字符串,内核接收到后再将该字符串后再重新发给用户进程。
用户态程序
netlink是一种特殊的套接字,在用户态除了一些参数的传递对其使用的方法与一般套接字无较大差异,。
1.宏与数据结构的定义
在使用netlink进行用户进程和内核的数据交互时,最重要的是定义好通信协议。协议一词直白的说就是用户进程和内核应该以什么样的形式发送数据,以什么样的形式接收数据。而这个“形式”通常对应程序中的一个特定数据结构。
本文所演示的程序并没有使用netlink已有的通信协议,因此我们自定义一种协议类型NETLINK_TEST。
#define NETLINK_TEST 18 #define MAX_PAYLOAD 1024 struct req { struct nlmsghdr nlh; char buf[MAX_PAYLOAD]; };
除此之外,我们应该再自定义一个数据报类型req,该结构包含了netlink数据包头结构的变量nlh和一个MAX_PAYLOAD大小的缓冲区。这里我们为了演示简单,并没有像上文中描述的那样将一个特定数据结构与nlmsghdr封装起来。
2.创建netlink套接字
要使用netlink,必须先创建一个netlink套接字。创建方法同样采用socket(),只是这里需要注意传递的参数:
int sock_fd; sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST); if (sock_fd < 0) { eprint(errno, "socket", __LINE__); return errno; }
第一个参数必须指定为PF_NETLINK或AF_NETLINK。第二个参数必须指定为SOCK_RAW或SOCK_DGRAM,因为netlink提供的是一种无连接的数据报服务。第三个参数则指定具体的协议类型,我们这里使用自定义的协议类型NETLINK_TEST。
另外,eprint()是一个自定义的出错处理函数,实现如下:
void eprint(int err_no, char *str, int line) { printf("Error %d in line %d:%s() with %s\n", err_no, line, str, strerror(errno)); }
3.将本地套接字与源地址绑定
将本地的套接字与源地址进行绑定通过bind()完成。在绑定之前,需要将源地址进行初始化,nl_pid字段指明发送消息一方的pid,nl_groups表示多播组的掩码,这里我们并没有涉及多播,因此默认为0。
struct sockaddr_nl src_addr; memset(&src_addr, 0, sizeof(src_addr)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); src_addr.nl_groups = 0; if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0) { eprint(errno, "bind", __LINE__); return errno; }
4.初始化msghdr结构
用户进程最终发送的是msghdr结构的消息,因此必须对这个结构进行初始化。而此结构又与sockaddr_nl,iovec和nlmsghdr三个结构相关,因此必须依次对这些数据结构进行初始化。
首先初始化目的套接字的地址结构,该结构与源套接字地址结构初始化的方法稍有不同,即nl_pid必须为0,表示接收方为内核。
struct sockaddr_nl dest_addr; memset(&dest_addr, 0, sizeof(dest_addr)); dest_addr.nl_family = AF_NETLINK; dest_addr.nl_pid = 0; dest_addr.nl_groups = 0;
接下来对req类型的数据报进行初始化,即依次对其封装的两个数据结构初始化:
struct req r; r.nlh.nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); r.nlh.nlmsg_pid = getpid(); r.nlh.nlmsg_flags = 0; memset(r.buf, 0, MAX_PAYLOAD); strcpy(NLMSG_DATA(&(r.nlh)), "hello, I am edsionte!");
这里的nlmsg_len为为sizeof(struct nlmsghdr)+MAX_PAYLOAD的总和。宏NLMSG_SPACE会自动将两者的长度相加。接下来对缓冲区向量iov进行初始化,让iov_base字段指向数据报结构,而iov_len为数据报长度。
struct iovec iov; iov.iov_base = (void *)&r; iov.iov_len = sizeof(r);
一切就绪后,将目的套接字地址与当前要发送的消息msg绑定,即将目的套接字地址复制给msg_name。再将要发送的数据iov与msg_iov绑定,如果一次性要发送多个数据包,则创建一个iovec类型的数组。
struct msghdr msg; msg.msg_name = (void *)&dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = &iov; msg.msg_iovlen = 1;
5.向内核发送消息
发送消息则很简单,通过sendmsg函数即可完成,前提是正确的创建netlink套接字和要发送的消息。
if (sendmsg(sock_fd, &msg, 0) < 0) { eprint(errno, "sendmsg", __LINE__); return errno; }
6.接受内核发来的消息
如果用户进程需要接收内核发送的消息,则需要通过recvmsg完成,只不过在接收之前需要将数据报r重新初始化,因为发送和接收时传递的数据结构可能是不同的。
为了简单演示netlink的用法,本文所述的用户进程发送的是一段字符串,这一点从数据报结构req的定义可以看出。而内核向用户进程发送的也是一段字符串,具体情况下面将会具体说明。
memset(&r.nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); if (recvmsg(sock_fd, &msg, 0) < 0) { eprint(errno, "recvmsg", __LINE__); return errno; } printf("Received message payload:%s\n", (char *)NLMSG_DATA(&r.nlh)); close(sock_fd);
接收完毕后,通过专门的宏NLMSG_DATA对数据报进行操作。 netlink对数据报的的访问和操作都是通过一系列标准的宏NLMSG_XXX来完成的,具体的说明可以通过man netlink查看。这里的NLMSG_DATA传递进去的是nlh,但它获取的是紧邻nlh的真正数据。本程序中传递的是字符串,所以取数据时候用char *强制类型转换,如果传递的是其他数据结构,则相应转换数据类型即可。
内核模块
netlink既然是一种用户态和内核态之间的双向通信机制,那么除了编写用户程序还要编写内核模块,也就是说用户进程和内核模块之间对数据的处理要彼此对应起来。
1.内核模块加载和卸载函数
内核模块加载函数主要通过netlink_kernel_create函数申请服务器端的套接字nl_sk,内核中对套接字表示为sock结构。另外,在创建套接字时还需要传递和用户进程相同的netlink协议类型NETLINK_TEST。创建套接字函数的第一个参数默认为init_net,第三个参数为多播时使用,我们这里不使用多播因此默认值为0。nl_data_handler是一个钩子函数,每当内核接收到一个消息时,这个钩子函数就被回调对用户数据进行处理。
#define NETLINK_TEST 17 struct sock *nl_sk = NULL; static int __init hello_init(void) { printk("hello_init is starting..\n"); nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE); if (nl_sk == 0) { printk("can not create netlink socket.\n"); return -1; } return 0; }
内核模块卸载函数所做的工作与加载函数相反,通过sock_release函数释放一开始申请的套接字。
static void __exit hello_exit(void) { sock_release(nl_sk->sk_socket); printk("hello_exit is leaving..\n"); }
2.钩子函数的实现
在内核创建netlink套接字时,必须绑定一个钩子函数,该钩子函数原型为:
void (*input)(struct sk_buff *skb);
钩子函数的实现主要是先接收用户进程发送的消息,接收以后内核再发送一条消息到用户进程。
在钩子函数中,先通过skb_get函数对套接字缓冲区增加一次引用值,再通过nlmsg_hdr函数获取netlink消息头指针nlh。接着使用NLMSG_DATA宏获取用户进程发送过来的数据str。除此之外,再打印发送者的pid。
void nl_data_handler(struct sk_buff *__skb) { struct sk_buff *skb; struct nlmsghdr *nlh; u32 pid; int rc; char str[100]; int len = NLMSG_SPACE(MAX_PAYLOAD); printk("read data..\n"); skb = skb_get(__skb); if (skb->len >= NLMSG_SPACE(0)) { nlh = nlmsg_hdr(skb); printk("Recv: %s\n", (char *)NLMSG_DATA(nlh)); memcpy(str, NLMSG_DATA(nlh), sizeof(str)); pid = nlh->nlmsg_pid; printk("pid is %d\n", pid); kfree_skb(skb);
接下来重新申请一个套接字缓冲区,为内核发送消息到用户进程做准备,nlmsg_put函数将填充netlink数据报头。接下来将用户进程发送的字符串复制到nlh紧邻的数据缓冲区中,等待内核发送。netlink_unicast函数将以非阻塞的方式发送数据包到用户进程,pid具体指明了接收消息的进程。
skb = alloc_skb(len, GFP_ATOMIC); if (!skb){ printk(KERN_ERR "net_link: allocate failed.\n"); return; } nlh = nlmsg_put(skb, 0, 0, 0, MAX_PAYLOAD, 0); NETLINK_CB(skb).pid = 0; memcpy(NLMSG_DATA(nlh), str, sizeof(str)); printk("net_link: going to send.\n"); rc = netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT); if (rc < 0) { printk(KERN_ERR "net_link: can not unicast skb (%d)\n", rc); } printk("net_link: send is ok.\n"); } }
这样就完成了内核模块的编写,它与用户进程通信共同完成数据交互。