歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> Linux文化 >> Linux用戶態與內核態的交互

Linux用戶態與內核態的交互

日期:2017/2/27 11:55:30   编辑:Linux文化

參考文檔

《Linux 系統內核空間與用戶空間通信的實現與分析》 陳鑫

《在 Linux 下用戶空間與內核空間數據交換的方式》 楊燚

理論篇

在 Linux 2.4 版以後版本的內核中,幾乎全部的中斷過程與用戶態進程的通信都是使用 netlink 套接字實現的,例如iprote2網絡管理工具,它與內核的交互就全部使用了netlink,著名的內核包過濾框架Netfilter在與用戶空間的通讀,也在最新版本中改變為netlink,無疑,它將是Linux用戶態與內核態交流的主要方法之一。它的通信依據是一個對應於進程的標識,一般定為該進程的 ID。當通信的一端處於中斷過程時,該標識為 0。當使用 netlink 套接字進行通信,通信的雙方都是用戶態進程,則使用方法類似於消息隊列。但通信雙方有一端是中斷過程,使用方法則不同。netlink 套接字的最大特點是對中斷過程的支持,它在內核空間接收用戶空間數據時不再需要用戶自行啟動一個內核程,而是通過另一個軟中斷調用用戶事先指定的接收函數。工作原理如圖:

如圖所示,這裡使用了軟中斷而不是內核線程來接收數據,這樣就可以保證數據接收的實時性。 當 netlink 套接字用於內核空間與用戶空間的通信時,在用戶空間的創建方法和一般套接字使用類似,但內核空間的創建方法則不同,下圖是 netlink 套接字實現此類通信時創建的過程:

用戶空間

用戶態應用使用標准的socket與內核通訊,標准的socket API 的函數, socket(), bind(), sendmsg(), recvmsg() 和 close()很容易地應用到 netlink socket。

為了創建一個 netlink socket,用戶需要使用如下參數調用 socket():

socket(AF_NETLINK, SOCK_RAW, netlink_type)

netlink對應的協議簇是 AF_NETLINK,第二個參數必須是SOCK_RAW或SOCK_DGRAM, 第三個參數指定netlink協議類型,它可以是一個自定義的類型,也可以使用內核預定義的類型:

#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

同樣地,socket函數返回的套接字,可以交給bing等函數調用:

static int skfd;

skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);

if(skfd < 0)

{

printf("can not create a netlink socket\n");

exit(0);

}

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,表示調用者不加入任何多播組:

struct sockaddr_nl local;

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;

}

用戶空間可以調用send函數簇向內核發送消息,如sendto、sendmsg等,同樣地,也可以使用struct sockaddr_nl來描述一個對端地址,以待send函數來調用,與本地地址稍不同的是,因為對端為內核,所以nl_pid成員需要設置為0:

struct sockaddr_nl kpeer; memset(&kpeer, 0, sizeof(kpeer)); kpeer.nl_family = AF_NETLINK;

kpeer.nl_pid = 0;

kpeer.nl_groups = 0;

另一個問題就是發內核發送的消息的組成,使用我們發送一個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。

struct msg_to_kernel /*自定義消息首部,它僅包含了netlink的消息首部*/

{

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; /*設置發送者的PID*/

這樣,有了本地地址、對端地址和發送的數據,就可以調用發送函數將消息發送給內核了:

/*發送一個請求*/

sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr*)&kpeer, sizeof(kpeer));

當發送完請求後,就可以調用recv函數簇從內核接收數據了,

接收到的數據包含了netlink消息首部和要傳輸的數據:

/*接收的數據包含了netlink消息首部和自定義數據結構*/

struct u_packet_info

{

struct nlmsghdr hdr;

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);

/*處理接收到的數據*/

……

}

同樣地,函數close用於關閉打開的netlink socket。程序中,因為程序一直循環接收處理內核的消息,需要收到用戶的關閉信號才會退出,所以關閉套接字的工作放在了自定義的信號函數sig_int中處理:

/*這個信號函數,處理一些程序退出時的動作*/

static void sig_int(int signo)

{

struct sockaddr_nl kpeer;

struct msg_to_kernel message;

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);

}

這個結束函數中,向內核發送一個“我已經退出了”的消息,然後調用close函數關閉netlink套接字,退出程序。

內核空間

與應用程序內核,內核空間也主要完成三件工作:

n 創建netlink套接字

n 接收處理用戶空間發送的數據

n 發送數據至用戶空間

API函數netlink_kernel_create用於創建一個netlink socket,同時,注冊一個回調函數,用於接收處理用戶空間的消息:

struct sock *

netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len)); 參數unit表示netlink協議類型,如NL_IMP2,參數input則為內核模塊定義的netlink消息處理函數,當有消息到達這個netlink socket時,該input函數指針就會被引用。函數指針input的參數sk實際上就是函數netlink_kernel_create返回的struct sock指針,sock實際是socket的一個內核表示數據結構,用戶態應用創建的socket在內核中也會有一個struct sock結構來表示。 static int __init init(void)

{

rwlock_init(&user_proc.lock); /*初始化讀寫鎖*/

/*創建一個netlink socket,協議類型是自定義的ML_IMP2,kernel_reveive為接受處理函數*/

nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);

if(!nlfd) /*創建失敗*/

{

printk("can not create a netlink socket\n");

return -1;

}

/*注冊一個Netfilter 鉤子*/

return nf_register_hook(&imp2_ops);

} module_init(init); 用戶空間向內核發送了兩種自定義消息類型: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; /*從接收隊列中取得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);

} 因為內核模塊可能同時被多個進程同時調用,所以函數中使用了信號量和鎖來進行互斥。skb =skb_dequeue(&sk->receive_queue)用於取得socket sk的接收隊列上的消息,返回為一個struct sk_buff的結構,skb->data指向實際的netlink消息。

程序中注冊了一個Netfilter鉤子,鉤子函數是get_icmp,它截獲ICMP數據包,然後調用send_to_user函數將數據發送給應用空間進程。發送的數據是info結構變量,它是struct packet_info結構,這個結構包含了來源/目的地址兩個成員。Netfilter Hook不是本文描述的重點,略過。

send_to_user 用於將數據發送給用戶空間進程,發送調用的是API函數netlink_unicast 完成的:

int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);

參數sk為函數netlink_kernel_create()返回的套接字,參數skb存放待發送的消息,它的data字段指向要發送的netlink消息結構,而skb的控制塊保存了消息的地址信息, 參數pid為接收消息進程pid,參數nonblock表示該函數是否為非阻塞,如果為1,該函數將在沒有接收緩存可利用時立即返回,而如果為0,該函數在沒有接收緩存可利用時睡眠。

向用戶空間進程發送的消息包含三個部份:netlink 消息頭部、數據部份和控制字段,控制字段包含了內核發送netlink消息時,需要設置的目標地址與源地址,內核中消息是通過sk_buff來管理的, linux/netlink.h中定義了NETLINK_CB宏來方便消息的地址設置:

#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))

例如: NETLINK_CB(skb).pid = 0;

NETLINK_CB(skb).dst_pid = 0;

NETLINK_CB(skb).dst_group = 1;

字段pid表示消息發送者進程ID,也即源地址,對於內核,它為 0, dst_pid 表示消息接收者進程 ID,也即目標地址,如果目標為組或內核,它設置為 0,否則 dst_group 表示目標組地址,如果它目標為某一進程或內核,dst_group 應當設置為 0。

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消息首部*/

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;

/*發送數據*/

read_lock_bh(&user_proc.lock);

ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);

read_unlock_bh(&user_proc.lock);

}

函數初始化netlink 消息首部,填充數據區,然後設置控制字段,這三部份都包含在skb_buff中,最後調用netlink_unicast函數把數據發送出去。

函數中調用了netlink的一個重要的宏NLMSG_PUT,它用於初始化netlink 消息首部:

#define NLMSG_PUT(skb, pid, seq, type, len) \

({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; \

__nlmsg_put(skb, pid, seq, type, len); })

static __inline__ struct nlmsghdr *

__nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)

{

struct nlmsghdr *nlh;

int size = NLMSG_LENGTH(len);

nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));

nlh->nlmsg_type = type;

nlh->nlmsg_len = size;

nlh->nlmsg_flags = 0;

nlh->nlmsg_pid = pid;

nlh->nlmsg_seq = seq;

return nlh;

}

這個宏一個需要注意的地方是調用了nlmsg_failure標簽,所以在程序中應該定義這個標簽。

在內核中使用函數sock_release來釋放函數netlink_kernel_create()創建的netlink socket: void sock_release(struct socket * sock);程序在退出模塊中釋放netlink sockets和netfilter hook:

static void __exit fini(void)

{

if(nlfd)

{

sock_release(nlfd->socket); /*釋放netlink socket*/

}

nf_unregister_hook(&imp2_ops); /*撤鎖netfilter 鉤子*/

}

Copyright © Linux教程網 All Rights Reserved