使用netlink機制在內核與應用程序之間通信
https://blog.csdn.net/zhongbeida_xue/article/details/79026398
轉載:https://blog.csdn.net/zoe6553/article/details/8026033
前一段時間,在開發一個驅動程序的過程中,需要在驅動程序與應用程序之間進行通信。其中驅動程序在接收到一個硬件中斷之后通知應用程序進行相應的處理。為 解決此類問題,驅動程序提供了幾種機制:(1)使用copy_to_user/copy_from_user方法,缺點是通信響應時間過長(2)使用信 號,但是限於字符設備(3)使用netlink。
在linux2.4之后引入了netlink機制,它將是Linux用戶態與內核態交流的主要方法之一。netlink 的特點是對中斷過程的支持,也就是說,可以在中斷程序中直接調用netlink相關函數。它在內核空間接收用戶空間數據時不再需要用戶自行啟動一個內核線 程,而是通過另一個軟中斷調用用戶事先指定的接收函數。netlink的通信過程如下:
下面分用戶空間與內核空間2個部分講述netlink的基本使用方法
1. 用戶空間的程序
用戶的應用程序使用標准套接字socket與內核空間進行通訊,標准socket API的函數, socket()、 bind()、sendmsg()、recvmsg() 和 close()很容易地應用到 netlink socket。
程序代碼:
#define <include/linux/netlink.h>
struct u_packet_info
{
struct nlmsghdr hdr;
struct packet_info p_info;
};
struct u_packet_info info;
/*自定義消息首部,它僅包含了netlink的消息首部*/
struct msg_to_kernel
{
struct nlmsghdr hdr;
};
struct msg_to_kernel message;
static int skfd, kpeerlen, rcvlen;
struct sockaddr_nl local;
struct sockaddr_nl kpeer;
struct msg_to_kernel message;
skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
if(skfd < 0)
{
printf("can not create a netlink socket/n");
exit(0);
}
memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid(); /*設置pid為自己的pid值*/
local.nl_groups = 0;
/*綁定套接字*/
if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)
{
printf("bind() error/n");
return -1;
}
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;
memset(&message, 0, sizeof(message));
/*計算消息,因為這里只是發送一個請求消息,沒有多余的數據,所以,數據長度為0*/
message.hdr.nlmsg_len = NLMSG_LENGTH(0);
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = USER_TYPE; /*設置自定義消息類型*/
message.hdr.nlmsg_pid = local.nl_pid; /*設置發送者的PID*/
/*發送一個請求*/
sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr*)&kpeer, sizeof(kpeer));
while(1)
{
kpeerlen = sizeof(struct sockaddr_nl);
/*接收內核空間返回的數據*/
rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info), 0, (struct sockaddr*)&kpeer, &kpeerlen);
/*處理接收到的數據*/
……
}
可以看到,應用程序流程與一般socket程序類似。
1) 調用socket創建套接字。
netlink對應的協議簇是 AF_NETLINK,第二個參數必須是SOCK_RAW或SOCK_DGRAM, 第三個參數指定netlink協議類型,它可以是一個自定義的類型,也可以使用內核預定義的類型,內核預定義的類型在文件include/linux/netlink.h中。
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_W1 1 /* 1-wire subsystem */
#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
用戶自定義的協議類型可以在此文件中加入。
2) 調用bind,綁定協議地址
bind函數需要綁定協議地址,netlink的socket地址使用struct sockaddr_nl結構描述:
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,如果希望內核處理消息或多播消息,就把該字段設置為 0,否則設置為處理消息的進程 ID。成員 nl_groups 用於指定多播組,bind 函數用於把調用進程加入到該字段指定的多播組,如果設置為 0,表示調用者不加入任何多播組。
3) 調用sendto向內核發送消息
一個重要的問題就是發內核發送的消息的組成,使用我們發送一個IP網絡數據包的話,則數據包結構為“IP包頭+IP數據”,同樣地,netlink的消息結構是“netlink消息頭部+數據”。netlink消息頭部使用struct nlmsghdr結構來描述:
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 */
};
字段 nlmsg_len 指定消息的總長度,包括緊跟該結構的數據部分長度以及該結構的大小,一般地,我們使用netlink提供的宏NLMSG_LENGTH來計算這個長度,僅需向NLMSG_LENGTH宏提供要發送的數據的長度,它會自動計算對齊后的總長度:
/*計算包含報頭的數據報長度*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字節對齊*/
#define NLMSG_ALIGN(len) (((len)+NLMSG_ALIGNTO-1)&~(NLMSG_ALIGNTO-1) )
可以看到netlink提供很多宏,這些宏可以為我們編寫netlink宏提供很大的方便。
字段 nlmsg_type 用於應用內部定義消息的類型,它對netlink內核實現是透明的,因此大部分情況下設置為0,字段nlmsg_flags用於設置消息標志,對於一般的使用,用戶把它設置為0就可以,只是一些高級應用(如 netfilter和路由daemon需要它進行一些復雜的操作),字段nlmsg_seq和nlmsg_pid用於應用追蹤消息,前者表示順序號,后者為消息來源進程ID。
4) 調用recvfrom
當發送完請求后,就可以調用recv函數簇從內核接收數據了,接收到的數據包含了netlink消息首部和要傳輸的數據。程序中定義了如下的數據結構用來進行通信:
/*接收的數據包含了netlink消息首部和自定義數據結構*/
struct u_packet_info
{
struct nlmsghdr hdr;
struct packet_info p_info;
};
2. 內核空間的程序
與應用程序內核,內核空間也主要完成三件工作:
1) 創建netlink套接字
API函數netlink_kernel_create用於創建一個netlink socket,同時,注冊一個回調函數,用於接收處理用戶空間的消息。
struct sock * netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
參數unit表示netlink協議類型,如NETLINK_GENERIC,參數input則為內核模塊定義的netlink消息處理函數,當有消息到達這個 netlink socket時,該input函數指針就會被引用。函數指針input的參數sk實際上就是函數 netlink_kernel_create返回的struct sock指針,sock實際是socket的一個內核表示數據結構,用戶態應用創建的 socket在內核中也會有一個struct sock結構來表示。
2) 接收處理用戶空間發送的數據
前面通過netlink_kernel_create注冊的input函數對來自用戶空間的數據進行處理。函數input()會在發送進程執行sendmsg()時被調用,這樣處理消息比較及時,但是,如果消息特別長時,這樣處 理將增加系統調用sendmsg()的執行時間,對於這種情況,可以定義一個內核線程專門負責消息接收,而函數input的工作只是喚醒該內核線程,這樣 sendmsg將很快返回。
3) 發送數據至用戶空間
在內核中,模塊調用函數 netlink_unicast 來發送單播消息:
int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
參數sk為函數netlink_kernel_create()返回的socket,參數skb存放消息,它的data字段指向要發送的netlink消息結構,而skb的控制塊保存了消息的地址信息,參數pid為接收消息進程的pid,參數nonblock表示該函數是否為非阻塞,如果為1,該函數將在沒有接收緩存可利用時立即返回,而如果為0,該函數在沒有接收緩存可利用時睡眠。
內核模塊或子系統也可以使用函數netlink_broadcast來發送廣播消息:
void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation);
內核程序代碼:
static int __init init(void)
{
/*創建一個netlink socket,協議類型是自定義的NETLINK_GENERIC,kernel_reveive為接受處理函數*/
nlfd = netlink_kernel_create(NETLINK_GENERIC, kernel_receive);
if(!nlfd) /*創建失敗*/
{
printk("can not create a netlink socket/n");
return -1;
}
}
/*程序在退出模塊中釋放netlink sockets*/
static void __exit fini(void)
{
if(nlfd)
sock_release(nlfd->socket); /*釋放netlink socket*/
}
DECLARE_MUTEX(receive_sem); /*初始化信號量*/
static void kernel_receive(struct sock *sk, int len)
{
do
{
struct sk_buff *skb;
if(down_trylock(&receive_sem)) /*獲取信號量*/
return;
/*從接收隊列中取得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 == USER_TYPE) /*請求*/
{
write_lock_bh(&user_proc.pid);
user_proc.pid = nlh->nlmsg_pid;
write_unlock_bh(&user_proc.pid);
}
}
}
kfree_skb(skb);
}
up(&receive_sem); /*返回信號量*/
}while(nlfd && nlfd->receive_queue.qlen);
}
/*一個中斷處理程序,向應用程序發送數據*/
static irqreturn_t irq_handler(int irq, void *dev_id, struct pt_regs *regs)
{
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消息首部*/
nlh = NLMSG_PUT(skb, 0, 0, 0, size-sizeof(*nlh));
/*跳過消息首部,指向數據區*/
packet = NLMSG_DATA(nlh);
/*初始化數據區*/
memset(packet, 0, sizeof(struct packet_info));
/*填充待發送的數據*/
.......
/*計算skb兩次長度之差,即netlink的長度總和*/
nlh->nlmsg_len = skb->tail - old_tail;
/*設置控制字段*/
NETLINK_CB(skb).dst_groups = 0;
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
return IRQ_HANDLED;
}