netlink---Linux下基於socket的內核和上層通信機制 (轉)


需要在linux網卡 驅動中加入一個自己的驅動,實現在內核態完成一些報文處理(這個過程可以實現一種零COPY的網絡報文截獲),對於復雜報文COPY下必要的數據交給用戶 態來完成(因為過於復雜的報文消耗CPU太大,會導致中斷占用時間太長)。因此需要一種內核和用戶態配合的通信機制,嘗試了很多方式都不太理想,最后采用 netlink+內存映射的模式很好的解決了這個問題。Netlink是一種采用socket通信的機制,用於linux內核和上層用戶空間進行通信的一 種機制,通過實踐我認為netlink最大的優點是可以實現“雙向通信”,是內核向用戶態發起通知的一種最好選擇。


內核和用戶空間進行通信,大概有如下幾種方式可以考慮:
采用內存映射的方式,將內核地址映射到用戶態。這種方式最直接,可以適用大量的數據傳輸機制。這種方式的缺點是很難進行“業務控制”,沒有一種可靠的機制 保障內核和用戶態的調動同步,比如信號量等都不能跨內核、用戶層使用。因此內存映射機制一般需要配合一種“消息機制”來控制數據的讀取,比如采用“消息” 類型的短數據通道來完成一個可靠的數據讀取功能。
ioctl機制,ioctl機制可以在驅動中擴展特定的ioctl消息,用於將一些狀態從內核反應到用戶態。Ioctl有很好的數據同步保護機制,不要擔 心內核和用戶層的數據訪問沖突,但是ioctl不適合傳輸大量的數據,通過和內存映射結合可以很好的完成大量數據交換過程。但是,ioctl的發起方一定 是在用戶態,因此如果需要內核態主動發起一個通知消息給用戶層,則非常的麻煩。可能需要用戶態程序采用輪詢機制不停的ioctl。
其他一些方式比如系統調用必須通過用戶態發起,proc方式不太可靠和實時,用於調試信息的輸出還是非常合適的。
通過前面的項目背景,我需要一種可以在內核態主動發起消息的通知方式,而用戶態的程序最好可以采用一種“阻塞調用”的方式等待消息。這樣的模型可以最大限度的節省CPU的調度,同時可以滿足及時處理的要求,最終選擇了netlink完成通信的過程。
Netlink的通信模型和socket通信非常相似,主要要點如下:

  • netlink采用自己獨立的地址編碼,struct sockaddr_nl;
  • 每個通過netlink發出的消息都必須附帶一個netlink自己的消息頭,struct nlmsghdr;
  • 內核態的netlink的操作API和用戶態完全不一樣,后面再介紹;
  • 用戶態的netlink操作完成采用socket函數,非常方便和簡單,有TCP/UDP socket編程基礎的非常容易上手。



Netlink的通信地址和協議

所有socket之間的通信,必須有個地址結構,Netlink也不例外。我們最熟悉的就是IPV4的地址了,netlink的地址結構如下:

  1. struct sockaddr_nl  
  2. {  
  3.     sa_family_t nl_family;          //必須為AF_NETLINK或者PF_NETLINK  
  4.     unsigned short  nl_pad;             //必須為0  
  5.     __u32       nl_pid;             //通信端口  
  6. __u32       nl_groups;              //組播掩碼  
  7. };  


上面幾個數據,最關鍵的是nl_family(就對應IP通信中的AF_INET)和nl_pid。

nl_pid就是一個約定的通信端口,用戶態使用的時候需要用一個非0的數字,一般來 說可以直接采用上層應用的進程ID(不用進程ID號碼也沒事,只要系統中不沖突的一個數字即可使用)。對於內核的地址,該值必須用0,也就是說,如果上層 通過sendto向內核發送netlink消息,peer addr中nl_pid必須填寫0。


nl_groups用於一個消息同時分發給不同的接收者,是一種組播應用,本文不講組播應用。


本質上,nl_pid就是netlink的通信地址。除了通信地址,netlink還提供“協議”來標示通信實體,在創建socket的時候,需要指定 netlink的通信協議號。每個協議號代表一種“應用”,上層可以用內核已經定義的協議和內核進行通信,獲得內核已經提供的信息。具體支持的協議列表如 下:

  1. #define NETLINK_ROUTE       0   /* Routing/device hook              */  
  2. #define NETLINK_UNUSED      1   /* Unused number                */  
  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 */  
  17. #define NETLINK_GENERIC     16  
  18. /* leave room for NETLINK_DM (DM Events) */  
  19. #define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */  
  20. #define NETLINK_ECRYPTFS    19  


協議的用途很好理解,比如我們單純創建一個上層應用,通過和 NETLINK_ROUTE協議通信,可以獲得內核的路由信息。我需要利用netlink創建一個我自己的通信協議,因此我定義了一種新的協議。新協議的 定義不能和內核已經定義的沖突,同時不能超過MAX_LINKS這個宏的限定,MAX_LINKS = 32。所以我定義的協議號為30。


小結:netlink采用協議號+通信端口的方式構建自己的地址體系。


用戶態操作netlink socket

用戶態創建netlink socket的基本過程和操作其他socket的API一模一樣,區別就2點:
1、 netlink有自己的地址;
2、 netlink接收到的消息帶一個netlink自己的消息頭;


用戶態創建、銷毀socket的過程:
1、 用socket函數創建,socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX);第一個參數必須是PF_NETLINK或者AF_NETLINK,第二個參數用SOCK_DGRAM和SOCK_RAW都沒問 題,第三個參數就是netlink的協議號。
2、 用bind函數綁定自己的地址。
3、 用close關閉套接字。

創建socket的代碼樣例:

  1. {  
  2.     struct sockaddr_nl addr;  
  3.     int flags;  
  4.      
  5.     //建立netlink socket  
  6.     s_nlm_socket = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX);  
  7.     if(s_nlm_socket < 0)  
  8.     {  
  9.         USE_DBG_OUT("create netlink socket error.\r\n");  
  10.         goto Err_Exit;  
  11.     }  
  12.       
  13.     //bind  
  14.     addr.nl_family = PF_NETLINK;  
  15.     addr.nl_pad    = 0;  
  16.     addr.nl_pid    = getpid();  
  17.     addr.nl_groups = 0;  
  18.       
  19.     if(bind(s_nlm_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0)  
  20.     {  
  21.         USE_DBG_OUT("bind socket error.\r\n");  
  22.         goto Err_Exit;  
  23.     }  
  24.   
  25.   
  26.     //設置socket為非阻塞模式  
  27.     flags = fcntl(s_nlm_socket, F_GETFL, 0);  
  28.     fcntl(s_nlm_socket, F_SETFL, flags|O_NONBLOCK);  
  29.       
  30.   
  31.   
  32.     return 0;  
  33. Err_Exit:  
  34.     return -1;  
  35. }  



用戶態接收、發送消息的API:
用戶態用sendto向內核發送netlink消息,用recvfrom接收消息。只是注意,發送、接收的時候在自己附帶的消息前面要加上一個netlink的消息頭。例如,定義一個如下的消息通信結構:

  1. struct tag_rcv_buf  
  2. {  
  3.         struct nlmsghdr hdr;            //netlink的消息頭  
  4.         netlink_notify_s my_msg;        //通信實體消息  
  5. }st_snd_buf;  


發送代碼的例子:

  1. My_send_msg  
  2. {  
  3.     struct tag_rcv_buf  
  4.     {  
  5.         struct nlmsghdr hdr;            //netlink的消息頭  
  6.         netlink_notify_s my_msg;        //通信實體消息  
  7.     }st_snd_buf;  
  8.     fd_set st_write_set;                         //select fd,避免線程吊死  
  9.     struct timeval write_time_out = {10, 0};     //10秒超時  
  10.     int ret;  
  11.       
  12.     //設置select  
  13.     FD_ZERO(&st_write_set);  
  14.     FD_SET(s_nlm_socket, &st_write_set);  
  15.       
  16.     /* 
  17.         設置發送數據 
  18.     */  
  19.     st_snd_buf.hdr.nlmsg_len   = sizeof(st_snd_buf);        //NLMSG_LENGTH(sizeof(netlink_notify_s))--這個宏包含有頭  
  20.     st_snd_buf.hdr.nlmsg_flags = 0;                         /*消息的附加選項,沒啥用*/  
  21.     st_snd_buf.hdr.nlmsg_type  = 0;                         /*設置自定義消息類型*/  
  22.     st_snd_buf.hdr.nlmsg_pid   = getpid();                  /*設置發送者的PID*/  
  23.   
  24.   
  25.     st_snd_buf.my_msg.start_pack_id = s_id;  
  26.     st_snd_buf.my_msg.end_pack_id   = e_id;  
  27.       
  28.     ret = select(s_nlm_socket+1, NULL, &st_write_set, NULL, &write_time_out);  
  29.     if(ret == -1)  
  30.     {  
  31.         //have some error.  
  32.         USE_DBG_OUT("send has some error %d.\n", errno);  
  33.         goto out;  
  34.     }  
  35.     else if(ret == 0)  
  36.     {  
  37.         //超時退出  
  38.         TMP_DBG_OUT("send timeout.\n");  
  39.         goto out;  
  40.     }  
  41.     else  
  42.     {  
  43.         //接收消息  
  44.         ret = sendto(s_nlm_socket, &st_snd_buf, sizeof(st_snd_buf), 0,   
  45.                         (struct sockaddr*)&s_peer_addr, sizeof(s_peer_addr));  
  46.   
  47.   
  48.         if(ret < 0)  
  49.         {  
  50.             USE_DBG_OUT("send to kernal by nl error %d\r\n", errno);  
  51.         }  
  52.         else  
  53.         {  
  54.             TMP_DBG_OUT("send to kernal ok s_id is %d, e_id is %d.\r\n", s_id, e_id);  
  55.         }  
  56.     }  
  57.       
  58. out:      
  59.     return;  
  60. }  


接收數據的代碼例子:

  1. {  
  2.     struct tag_rcv_buf  
  3.     {  
  4.         struct nlmsghdr hdr;            //netlink的消息頭  
  5.         netlink_notify_s my_msg;        //通信實體消息  
  6.     }st_rcv_buf;  
  7.     int ret, addr_len, io_ret;  
  8.     struct sockaddr_nl st_peer_addr;  
  9.     fd_set st_read_set;                         //select fd,避免線程吊死  
  10.     struct timeval read_time_out = {10, 0};     //10秒超時  
  11.     int rcv_buf;  
  12.       
  13.     //設置內核的通信地址  
  14.     st_peer_addr.nl_family = AF_NETLINK;  
  15.     st_peer_addr.nl_pad = 0;                                   /*always set to zero*/  
  16.     st_peer_addr.nl_pid = 0;                                   /*kernel's pid is zero*/  
  17.     st_peer_addr.nl_groups = 0;                                /*multicast groups mask, if unicast set to zero*/  
  18.     addr_len = sizeof(st_peer_addr);  
  19.   
  20.     //設置select  
  21.     FD_ZERO(&st_read_set);  
  22.     FD_SET(s_nlm_socket, &st_read_set);  
  23.   
  24.     ret = select(s_nlm_socket+1, &st_read_set, NULL, NULL, &read_time_out);  
  25.     if(ret == -1)  
  26.     {  
  27.         //have some error.  
  28.         USE_DBG_OUT("select rcv some error %d", errno);  
  29.         goto err;  
  30.     }  
  31.     else if(ret == 0)  
  32.     {  
  33.         //超時退出  
  34.         TMP_DBG_OUT("rcv timeout.\n");  
  35.         *p_size = 0;  
  36.         goto out;  
  37.     }  
  38.     else  
  39.     {  
  40.         //接收消息  
  41.         ret = recvfrom(s_nlm_socket, &st_rcv_buf, sizeof(st_rcv_buf), 0,   
  42.             (struct sockaddr *)&st_peer_addr, &addr_len);  
  43.     }  
  44.      
  45.     if(ret == sizeof(st_rcv_buf) )  
  46.     {  
  47.         //收到消息了...  
  48.   
  49.     else  
  50.     {  
  51.         USE_DBG_OUT("rcv msg have some err. ret is %d, errno is %d\r\n", ret, errno);  
  52.         goto err;  
  53.     }  
  54.   
  55. out:      
  56.     return 0;  
  57. err:  
  58.     *p_size = 0;  
  59.     return -1;  
  60. }  



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM