NetLink通信机制学习
Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的。当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同,下图是 netlink 套接字实现此类通信时创建的过程:
用户空间:
1. 创建套接字
skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
skfd为static int类型,AF_NETLINK是netlink对应的协议簇,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型
2. 绑定套接字
bind(skfd, (struct sockaddr*)&local, sizeof(local));
♦ local为netlink的socket地址,其结构描述为:
struct sockaddr_nl { sa_family_t nl_family; unsigned short nl_pad; __u32 nl_pid; __u32 nl_groups; };
/*成员 nl_family为协议簇 AF_NETLINK,成员 nl_pad 当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID*/
♦ 定义socket地址
struct sockaddr_nl local; memset(&local, 0, sizeof(local)); local.nl_family = AF_NETLINK; local.nl_pid = getpid(); /*设置pid为自己的pid值*/ local.nl_groups = 0;
3. 用户空间调用send函数(如sendto、sendmsg等)向内核发送数据,使用同样的socket地址来描述内核,不过需要注意,由于对端是内核,nl_pid必须设置为0。
♦ 内核的socket地址
struct sockaddr_nl kpeer; memset(&kpeer, 0, sizeof(kpeer)); kpeer.nl_family = AF_NETLINK; kpeer.nl_pid = 0; kpeer.nl_groups = 0;
♦ 用户进程想内核发送的数据包格式为:“netlink消息头 + 数据”,消息头描述为:
struct nlmsghdr { __u32 nlmsg_len; /* Length of message */ __u16 nlmsg_type; /* Message type*/ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq; /* Sequence number */ __u32 nlmsg_pid; /* Sending process PID */ };
♦ 自定义消息首部,它仅包含了netlink的消息首部
struct msg_to_kernel { struct nlmsghdr hdr; };
♦ 填充首部信息
struct msg_to_kernel message; memset(&message, 0, sizeof(message)); message.hdr.nlmsg_len = NLMSG_LENGTH(0); /*没有数据,所以长度为0.*/ message.hdr.nlmsg_flags = 0; message.hdr.nlmsg_type = IMP2_U_PID; message.hdr.nlmsg_pid = local.nl_pid;
♦ 向内核发送消息
sendto(skfd, &message, message.hdr.nlmsg_len, 0,(struct sockaddr*)&kpeer, sizeof(kpeer));
4. 当发送完请求后,就可以调用recv函数簇从内核接收数据了,接收的数据包含了netlink消息首部和自定义数据结构。
♦ 自定义的数据结构
struct u_packet_info { struct nlmsghdr hdr; /*netlink消息头*/ struct packet_info icmp_info; }; struct u_packet_info info;
♦ 接受和处理从内核接受到的信息
while(1) { kpeerlen = sizeof(struct sockaddr_nl); /*接收内核空间返回的数据*/ rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),0, (struct sockaddr*)&kpeer, &kpeerlen); /*处理接收到的数据*/ …… }
5. 函数close用于关闭打开的netlink socket。程序中,因为程序一直循环接收处理内核的消息,需要收到用户的关闭信号才会退出
♦ 关闭套接字的工作放在了自定义的信号函数sig_int中处理
static void sig_int(int signo) { struct sockaddr_nl kpeer; /*内核的socket地址*/ struct msg_to_kernel message; /*自定义netlink消息首部*/ memset(&kpeer, 0, sizeof(kpeer)); kpeer.nl_family = AF_NETLINK; kpeer.nl_pid = 0; kpeer.nl_groups = 0; memset(&message, 0, sizeof(message)); message.hdr.nlmsg_len = NLMSG_LENGTH(0); message.hdr.nlmsg_flags = 0; message.hdr.nlmsg_type = IMP2_CLOSE; message.hdr.nlmsg_pid = getpid(); /*向内核发送一个消息,由nlmsg_type表明,应用程序将关闭*/ sendto(skfd, &message, message.hdr.nlmsg_len, 0,(struct sockaddr *)(&kpeer),sizeof(kpeer)); close(skfd); exit(0); }
内核空间:
1. 创建netlink套接字(netlink_kernel_create函数)
♦ 通过netlink_kernel_create创建一个netlink套接字,同时,注册一个回调函数,用于接收处理用户空间的消息。
struct sock *netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
2. 接收处理用户空间发送的数据(kernel_receive 函数)
♦ 用户空间向内核发送了两种自定义消息类型:IMP2_U_PID和IMP2_CLOSE,分别是请求和关闭。kernel_receive 函数分别处理这两种消息:
DECLARE_MUTEX(receive_sem); /*初始化信号量*/ static void kernel_receive(struct sock *sk, int len) { do { struct sk_buff *skb; if(down_trylock(&receive_sem)) /*获取信号量*/ return; /*从sk接收队列中取得skb,然后进行一些基本的长度的合法性校验*/ while((skb = skb_dequeue(&sk->receive_queue)) != NULL) { { struct nlmsghdr *nlh = NULL; if(skb->len >= sizeof(struct nlmsghdr)) { /*获取数据中的nlmsghdr 结构的报头*/ nlh = (struct nlmsghdr *)skb->data; if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))&& (skb->len >= nlh->nlmsg_len)) { /*长度的全法性校验完成后,处理应用程序自定义消息类型,主要是对用户PID的保存,即为内核保存“把消息发送给谁”*/ if(nlh->nlmsg_type == IMP2_U_PID)/*请求*/ { write_lock_bh(&user_proc.pid); user_proc.pid = nlh->nlmsg_pid; write_unlock_bh(&user_proc.pid); } else if(nlh->nlmsg_type == IMP2_CLOSE)/*应用程序关闭*/ { write_lock_bh(&user_proc.pid); if(nlh->nlmsg_pid == user_proc.pid) user_proc.pid = 0; write_unlock_bh(&user_proc.pid); } } } } kfree_skb(skb); } up(&receive_sem); /*返回信号量*/ }while(nlfd && nlfd->receive_queue.qlen); }
3. 发送数据至用户空间(send_to_user函数)
♦ send_to_user 用于将数据发送给用户空间进程,发送调用的是API函数netlink_unicast完成的:
int netlink_unicast(struct sock *sk , struct sk_buff *skb , u32 pid , int nonblock);
♦ 向用户空间进程发送的消息包含三个部份:netlink 消息头部、数据部份和控制字段。函数初始化netlink 消息首部,填充数据区,然后设置控制字段,这三部份都包含在skb_buff中,最后调用netlink_unicast函数把数据发送出去。
static int send_to_user(struct packet_info *info) { int ret; int size; unsigned char *old_tail; struct sk_buff *skb; struct nlmsghdr *nlh; struct packet_info *packet; /*计算消息总长:消息首部加上数据加度*/ size = NLMSG_SPACE(sizeof(*info)); /*分配一个新的套接字缓存*/ skb = alloc_skb(size, GFP_ATOMIC); old_tail = skb->tail; /*初始化一个netlink消息首部,NLMSG_PUT(skb, pid, seq, type, len)*/ nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh)); /*跳过消息首部,指向数据区*/ packet = NLMSG_DATA(nlh); /*初始化数据区*/ memset(packet, 0, sizeof(struct packet_info)); /*填充待发送的数据*/ packet->src = info->src; packet->dest = info->dest; /*计算skb两次长度之差,即netlink的长度总和*/ nlh->nlmsg_len = skb->tail - old_tail; /*设置控制字段*/ NETLINK_CB(skb).dst_groups = 0; /*如果它目标为某一进程或内核,dst_group 应当设置为 0。*/ /*发送数据*/ read_lock_bh(&user_proc.lock); ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT); read_unlock_bh(&user_proc.lock); }