需要在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的地址結構如下:
- struct sockaddr_nl
- {
- sa_family_t nl_family; //必須為AF_NETLINK或者PF_NETLINK
- unsigned short nl_pad; //必須為0
- __u32 nl_pid; //通信端口
- __u32 nl_groups; //組播掩碼
- };
上面幾個數據,最關鍵的是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的通信協議號。每個協議號代表一種“應用”,上層可以用內核已經定義的協議和內核進行通信,獲得內核已經提供的信息。具體支持的協議列表如 下:
- #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #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
- /* leave room for NETLINK_DM (DM Events) */
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
- #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的代碼樣例:
- {
- struct sockaddr_nl addr;
- int flags;
- //建立netlink socket
- s_nlm_socket = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_XXX);
- if(s_nlm_socket < 0)
- {
- USE_DBG_OUT("create netlink socket error.\r\n");
- goto Err_Exit;
- }
- //bind
- addr.nl_family = PF_NETLINK;
- addr.nl_pad = 0;
- addr.nl_pid = getpid();
- addr.nl_groups = 0;
- if(bind(s_nlm_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0)
- {
- USE_DBG_OUT("bind socket error.\r\n");
- goto Err_Exit;
- }
- //設置socket為非阻塞模式
- flags = fcntl(s_nlm_socket, F_GETFL, 0);
- fcntl(s_nlm_socket, F_SETFL, flags|O_NONBLOCK);
- return 0;
- Err_Exit:
- return -1;
- }
用戶態接收、發送消息的API:
用戶態用sendto向內核發送netlink消息,用recvfrom接收消息。只是注意,發送、接收的時候在自己附帶的消息前面要加上一個netlink的消息頭。例如,定義一個如下的消息通信結構:
- struct tag_rcv_buf
- {
- struct nlmsghdr hdr; //netlink的消息頭
- netlink_notify_s my_msg; //通信實體消息
- }st_snd_buf;
發送代碼的例子:
- My_send_msg
- {
- struct tag_rcv_buf
- {
- struct nlmsghdr hdr; //netlink的消息頭
- netlink_notify_s my_msg; //通信實體消息
- }st_snd_buf;
- fd_set st_write_set; //select fd,避免線程吊死
- struct timeval write_time_out = {10, 0}; //10秒超時
- int ret;
- //設置select
- FD_ZERO(&st_write_set);
- FD_SET(s_nlm_socket, &st_write_set);
- /*
- 設置發送數據
- */
- st_snd_buf.hdr.nlmsg_len = sizeof(st_snd_buf); //NLMSG_LENGTH(sizeof(netlink_notify_s))--這個宏包含有頭
- st_snd_buf.hdr.nlmsg_flags = 0; /*消息的附加選項,沒啥用*/
- st_snd_buf.hdr.nlmsg_type = 0; /*設置自定義消息類型*/
- st_snd_buf.hdr.nlmsg_pid = getpid(); /*設置發送者的PID*/
- st_snd_buf.my_msg.start_pack_id = s_id;
- st_snd_buf.my_msg.end_pack_id = e_id;
- ret = select(s_nlm_socket+1, NULL, &st_write_set, NULL, &write_time_out);
- if(ret == -1)
- {
- //have some error.
- USE_DBG_OUT("send has some error %d.\n", errno);
- goto out;
- }
- else if(ret == 0)
- {
- //超時退出
- TMP_DBG_OUT("send timeout.\n");
- goto out;
- }
- else
- {
- //接收消息
- ret = sendto(s_nlm_socket, &st_snd_buf, sizeof(st_snd_buf), 0,
- (struct sockaddr*)&s_peer_addr, sizeof(s_peer_addr));
- if(ret < 0)
- {
- USE_DBG_OUT("send to kernal by nl error %d\r\n", errno);
- }
- else
- {
- TMP_DBG_OUT("send to kernal ok s_id is %d, e_id is %d.\r\n", s_id, e_id);
- }
- }
- out:
- return;
- }
接收數據的代碼例子:
- {
- struct tag_rcv_buf
- {
- struct nlmsghdr hdr; //netlink的消息頭
- netlink_notify_s my_msg; //通信實體消息
- }st_rcv_buf;
- int ret, addr_len, io_ret;
- struct sockaddr_nl st_peer_addr;
- fd_set st_read_set; //select fd,避免線程吊死
- struct timeval read_time_out = {10, 0}; //10秒超時
- int rcv_buf;
- //設置內核的通信地址
- st_peer_addr.nl_family = AF_NETLINK;
- st_peer_addr.nl_pad = 0; /*always set to zero*/
- st_peer_addr.nl_pid = 0; /*kernel's pid is zero*/
- st_peer_addr.nl_groups = 0; /*multicast groups mask, if unicast set to zero*/
- addr_len = sizeof(st_peer_addr);
- //設置select
- FD_ZERO(&st_read_set);
- FD_SET(s_nlm_socket, &st_read_set);
- ret = select(s_nlm_socket+1, &st_read_set, NULL, NULL, &read_time_out);
- if(ret == -1)
- {
- //have some error.
- USE_DBG_OUT("select rcv some error %d", errno);
- goto err;
- }
- else if(ret == 0)
- {
- //超時退出
- TMP_DBG_OUT("rcv timeout.\n");
- *p_size = 0;
- goto out;
- }
- else
- {
- //接收消息
- ret = recvfrom(s_nlm_socket, &st_rcv_buf, sizeof(st_rcv_buf), 0,
- (struct sockaddr *)&st_peer_addr, &addr_len);
- }
- if(ret == sizeof(st_rcv_buf) )
- {
- //收到消息了...
- else
- {
- USE_DBG_OUT("rcv msg have some err. ret is %d, errno is %d\r\n", ret, errno);
- goto err;
- }
- out:
- return 0;
- err:
- *p_size = 0;
- return -1;
- }