Linux 用戶態與內核態的交互

轉自:http://bbs.chinaunix.net/thread-2162796-1-1.html

Linux 用戶態與內核態的交互

——netlink 篇

作者:Kendo
2006-9-3

這是一篇學習筆記,主要是對《Linux 系統內核空間與用戶空間通信的實現與分析》中的源碼imp2的分析。其中的源碼,可以到以下URL下載:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz

參考文檔
《Linux 系統內核空間與用戶空間通信的實現與分析》                陳鑫
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux
《在 Linux 下用戶空間與內核空間數據交換的方式》                楊燚
http://www-128.ibm.com/developerworks/cn/linux/l-kerns-usrs/

理論篇
        在 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():

  1. socket(AF_NETLINK, SOCK_RAW, netlink_type)
複製代碼


netlink對應的協議簇是 AF_NETLINK,第二個參數必須是SOCK_RAW或SOCK_DGRAM, 第三個參數指定netlink協議類型,它可以是一個自定義的類型,也可以使用內核預定義的類型:
  1. #define NETLINK_ROUTE          0       /* Routing/device hook                          */
  2. #define NETLINK_W1             1       /* 1-wire subsystem                             */
  3. #define NETLINK_USERSOCK       2       /* Reserved for user mode socket protocols      */
  4. #define NETLINK_FIREWALL       3       /* Firewalling hook                             */
  5. #define NETLINK_INET_DIAG      4       /* INET socket monitoring                       */
  6. #define NETLINK_NFLOG          5       /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM           6       /* ipsec */
  8. #define NETLINK_SELINUX        7       /* SELinux event notifications */
  9. #define NETLINK_ISCSI          8       /* Open-iSCSI */
  10. #define NETLINK_AUDIT          9       /* auditing */
  11. #define NETLINK_FIB_LOOKUP     10
  12. #define NETLINK_CONNECTOR      11
  13. #define NETLINK_NETFILTER      12      /* netfilter subsystem */
  14. #define NETLINK_IP6_FW         13
  15. #define NETLINK_DNRTMSG        14      /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT 15      /* Kernel messages to userspace */
複製代碼
#define NETLINK_GENERIC        16

同樣地,socket函數返回的套接字,可以交給bing等函數調用:
  1. static int skfd;
  2. skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
  3. if(skfd < 0)
  4. {
  5.       printf("can not create a netlink socket\n");
  6.       exit(0);
  7. }
複製代碼


bind函數需要綁定協議地址,netlink的socket地址使用struct sockaddr_nl結構描述:
  1. struct sockaddr_nl
  2. {
  3.   sa_family_t    nl_family;
  4.   unsigned short nl_pad;
  5.   __u32          nl_pid;
  6.   __u32          nl_groups;
  7. };
複製代碼


成員 nl_family爲協議簇 AF_NETLINK,成員 nl_pad 當前沒有使用,因此要總是設置爲 0,成員 nl_pid 爲接收或發送消息的進程的 ID,如果希望內核處理消息或多播消息,就把該字段設置爲 0,否則設置爲處理消息的進程 ID。成員 nl_groups 用於指定多播組,bind 函數用於把調用進程加入到該字段指定的多播組,如果設置爲 0,表示調用者不加入任何多播組:
  1. struct sockaddr_nl local;

  2. memset(&local, 0, sizeof(local));
  3. local.nl_family = AF_NETLINK;
  4. local.nl_pid = getpid();                /*設置pid爲自己的pid值*/
  5. local.nl_groups = 0;
  6. /*綁定套接字*/
  7. if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0)
  8. {
  9. printf("bind() error\n");
  10.      return -1;
  11. }
複製代碼


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

  1. struct sockaddr_nl kpeer;
  2. memset(&kpeer, 0, sizeof(kpeer));
  3. kpeer.nl_family = AF_NETLINK;
  4. kpeer.nl_pid = 0;
  5. kpeer.nl_groups = 0;
複製代碼


另一個問題就是發內核發送的消息的組成,使用我們發送一個IP網絡數據包的話,則數據包結構爲“IP包頭+IP數據”,同樣地,netlink的消息結構是“netlink消息頭部+數據”。Netlink消息頭部使用struct nlmsghdr結構來描述:
  1. struct nlmsghdr
  2. {
  3.   __u32 nlmsg_len;   /* Length of message */
  4.   __u16 nlmsg_type;  /* Message type*/
  5.   __u16 nlmsg_flags; /* Additional flags */
  6.   __u32 nlmsg_seq;   /* Sequence number */
  7.   __u32 nlmsg_pid;   /* Sending process PID */
  8. };
複製代碼


字段 nlmsg_len 指定消息的總長度,包括緊跟該結構的數據部分長度以及該結構的大小,一般地,我們使用netlink提供的宏NLMSG_LENGTH來計算這個長度,僅需向NLMSG_LENGTH宏提供要發送的數據的長度,它會自動計算對齊後的總長度:
  1. /*計算包含報頭的數據報長度*/
  2. #define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
  3. /*字節對齊*/
  4. #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。

  1. struct msg_to_kernel                /*自定義消息首部,它僅包含了netlink的消息首部*/
  2. {
  3.   struct nlmsghdr hdr;
  4. };

  5. struct msg_to_kernel message;
  6. memset(&message, 0, sizeof(message));
  7. message.hdr.nlmsg_len = NLMSG_LENGTH(0);                /*計算消息,因爲這裏只是發送一個請求消息,沒有多餘的數據,所以,數據長度爲0*/
  8. message.hdr.nlmsg_flags = 0;
  9. message.hdr.nlmsg_type = IMP2_U_PID;                        /*設置自定義消息類型*/
  10. message.hdr.nlmsg_pid = local.nl_pid;                /*設置發送者的PID*/

  11. 這樣,有了本地地址、對端地址和發送的數據,就可以調用發送函數將消息發送給內核了:
  12.   /*發送一個請求*/
  13.   sendto(skfd, &message, message.hdr.nlmsg_len, 0,
  14.          (struct sockaddr*)&kpeer, sizeof(kpeer));
複製代碼


當發送完請求後,就可以調用recv函數簇從內核接收數據了,接收到的數據包含了netlink消息首部和要傳輸的數據:
  1. /*接收的數據包含了netlink消息首部和自定義數據結構*/
  2. struct u_packet_info
  3. {
  4.   struct nlmsghdr hdr;
  5.   struct packet_info icmp_info;
  6. };
  7. struct u_packet_info info;
  8. while(1)
  9. {
  10.     kpeerlen = sizeof(struct sockaddr_nl);
  11.       /*接收內核空間返回的數據*/
  12.       rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
  13.                         0, (struct sockaddr*)&kpeer, &kpeerlen);
  14.                   
  15.        /*處理接收到的數據*/
  16. ……
  17. }
複製代碼


同樣地,函數close用於關閉打開的netlink socket。程序中,因爲程序一直循環接收處理內核的消息,需要收到用戶的關閉信號纔會退出,所以關閉套接字的工作放在了自定義的信號函數sig_int中處理:
  1. /*這個信號函數,處理一些程序退出時的動作*/
  2. static void sig_int(int signo)
  3. {
  4.   struct sockaddr_nl kpeer;
  5.   struct msg_to_kernel message;

  6.   memset(&kpeer, 0, sizeof(kpeer));
  7.   kpeer.nl_family = AF_NETLINK;
  8.   kpeer.nl_pid    = 0;
  9.   kpeer.nl_groups = 0;

  10.   memset(&message, 0, sizeof(message));
  11.   message.hdr.nlmsg_len = NLMSG_LENGTH(0);
  12.   message.hdr.nlmsg_flags = 0;
  13.   message.hdr.nlmsg_type = IMP2_CLOSE;
  14.   message.hdr.nlmsg_pid = getpid();

  15.   /*向內核發送一個消息,由nlmsg_type表明,應用程序將關閉*/
  16.   sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr *)(&kpeer),         sizeof(kpeer));

  17.   close(skfd);
  18.   exit(0);
  19. }
複製代碼


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

內核空間

與應用程序內核,內核空間也主要完成三件工作:
n        創建netlink套接字
n        接收處理用戶空間發送的數據
n        發送數據至用戶空間

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

  1. struct sock *
  2. 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結構來表示。
  1. static int __init init(void)
  2. {
  3.   rwlock_init(&user_proc.lock);                /*初始化讀寫鎖*/

  4.   /*創建一個netlink socket,協議類型是自定義的ML_IMP2,kernel_reveive爲接受處理函數*/
  5.   nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
  6.   if(!nlfd)                /*創建失敗*/
  7.   {
  8.       printk("can not create a netlink socket\n");
  9.       return -1;
  10.   }

  11.   /*註冊一個Netfilter 鉤子*/
  12.   return nf_register_hook(&imp2_ops);
  13. }


  14. module_init(init);
複製代碼


用戶空間向內核發送了兩種自定義消息類型:IMP2_U_PID和IMP2_CLOSE,分別是請求和關閉。kernel_receive 函數分別處理這兩種消息:
  1. DECLARE_MUTEX(receive_sem);                                                        /*初始化信號量*/
  2. static void kernel_receive(struct sock *sk, int len)
  3. {
  4.         do
  5.     {
  6.                 struct sk_buff *skb;
  7.                 if(down_trylock(&receive_sem))                                /*獲取信號量*/
  8.                         return;
  9.                 /*從接收隊列中取得skb,然後進行一些基本的長度的合法性校驗*/
  10.                 while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
  11.         {
  12.                         {
  13.                                 struct nlmsghdr *nlh = NULL;
  14.                                 
  15.                                 if(skb->len >= sizeof(struct nlmsghdr))
  16.                                 {
  17.                                         /*獲取數據中的nlmsghdr 結構的報頭*/
  18.                                         nlh = (struct nlmsghdr *)skb->data;
  19.                                         if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))
  20.                                                 && (skb->len >= nlh->nlmsg_len))
  21.                                         {
  22.                                                 /*長度的全法性校驗完成後,處理應用程序自定義消息類型,主要是對用戶PID的保存,即爲內核保存“把消息發送給誰”*/
  23.                                                 if(nlh->nlmsg_type == IMP2_U_PID)                /*請求*/
  24.                                                 {
  25.                                                         write_lock_bh(&user_proc.pid);
  26.                                                         user_proc.pid = nlh->nlmsg_pid;
  27.                                                         write_unlock_bh(&user_proc.pid);
  28.                                                 }
  29.                                                 else if(nlh->nlmsg_type == IMP2_CLOSE)        /*應用程序關閉*/
  30.                                                 {
  31.                                                         write_lock_bh(&user_proc.pid);
  32.                                                         if(nlh->nlmsg_pid == user_proc.pid)
  33.                                                                 user_proc.pid = 0;
  34.                                                         write_unlock_bh(&user_proc.pid);
  35.                                                 }
  36.                                         }
  37.                                 }
  38.                         }
  39.                         kfree_skb(skb);
  40.         }
  41.                 up(&receive_sem);                                /*返回信號量*/
  42.     }while(nlfd && nlfd->receive_queue.qlen);
  43. }
複製代碼


因爲內核模塊可能同時被多個進程同時調用,所以函數中使用了信號量和鎖來進行互斥。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 完成的:
  1. 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宏來方便消息的地址設置:

  1. #define NETLINK_CB(skb)         (*(struct netlink_skb_parms*)&((skb)->cb))
複製代碼


例如:

  1. NETLINK_CB(skb).pid = 0;
  2. NETLINK_CB(skb).dst_pid = 0;
  3. NETLINK_CB(skb).dst_group = 1;
複製代碼


字段pid表示消息發送者進程ID,也即源地址,對於內核,它爲 0, dst_pid 表示消息接收者進程 ID,也即目標地址,如果目標爲組或內核,它設置爲 0,否則 dst_group 表示目標組地址,如果它目標爲某一進程或內核,dst_group 應當設置爲 0。
  1. static int send_to_user(struct packet_info *info)
  2. {
  3. int ret;
  4. int size;
  5. unsigned char *old_tail;
  6. struct sk_buff *skb;
  7. struct nlmsghdr *nlh;
  8. struct packet_info *packet;

  9. /*計算消息總長:消息首部加上數據加度*/
  10. size = NLMSG_SPACE(sizeof(*info));

  11. /*分配一個新的套接字緩存*/
  12. skb = alloc_skb(size, GFP_ATOMIC);
  13. old_tail = skb->tail;

  14. /*初始化一個netlink消息首部*/
  15. nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
  16. /*跳過消息首部,指向數據區*/
  17. packet = NLMSG_DATA(nlh);
  18. /*初始化數據區*/
  19. memset(packet, 0, sizeof(struct packet_info));
  20. /*填充待發送的數據*/
  21. packet->src = info->src;
  22. packet->dest = info->dest;

  23. /*計算skb兩次長度之差,即netlink的長度總和*/
  24. nlh->nlmsg_len = skb->tail - old_tail;
  25. /*設置控制字段*/
  26. NETLINK_CB(skb).dst_groups = 0;

  27. /*發送數據*/
  28. read_lock_bh(&user_proc.lock);
  29. ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
  30. read_unlock_bh(&user_proc.lock);


  31. }
複製代碼


函數初始化netlink 消息首部,填充數據區,然後設置控制字段,這三部份都包含在skb_buff中,最後調用netlink_unicast函數把數據發送出去。
函數中調用了netlink的一個重要的宏NLMSG_PUT,它用於初始化netlink 消息首部:
  1. #define NLMSG_PUT(skb, pid, seq, type, len) \
  2. ({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; \
  3.    __nlmsg_put(skb, pid, seq, type, len); })
  4. static __inline__ struct nlmsghdr *
  5. __nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
  6. {
  7.         struct nlmsghdr *nlh;
  8.         int size = NLMSG_LENGTH(len);

  9.         nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
  10.         nlh->nlmsg_type = type;
  11.         nlh->nlmsg_len = size;
  12.         nlh->nlmsg_flags = 0;
  13.         nlh->nlmsg_pid = pid;
  14.         nlh->nlmsg_seq = seq;
  15.         return nlh;
  16. }
複製代碼


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

在內核中使用函數sock_release來釋放函數netlink_kernel_create()創建的netlink socket:
  1. void sock_release(struct socket * sock);
複製代碼


程序在退出模塊中釋放netlink sockets和netfilter hook:
  1. static void __exit fini(void)
  2. {
  3.   if(nlfd)
  4.     {
  5.       sock_release(nlfd->socket);                /*釋放netlink socket*/
  6.     }
  7.   nf_unregister_hook(&imp2_ops);                /*撤鎖netfilter 鉤子*/
  8. }
複製代碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章