Linux Netlink機制:現代網絡通信的核心
在Linux的廣袤世界里,進程間通信(IPC)是一個核心話題,它就像是系統的神經系統,讓不同的進程能夠交流協作,共同維持系統的正常運轉。而在眾多的 IPC 機制中,Netlink 猶如一把秘密武器,發揮著獨特而關鍵的作用。傳統的進程間通信方式,如管道、消息隊列、共享內存等,各自有著特定的應用場景和局限性。管道雖然簡單易用,但它是半雙工的,數據只能單向流動,并且通常只適用于具有親緣關系的進程之間;消息隊列可以實現消息的異步傳遞,但它的效率相對較低,不太適合大量數據的傳輸;共享內存雖然速度快,但需要復雜的同步機制來保證數據的一致性。
Netlink 的出現,為 Linux 系統中的進程間通信帶來了新的活力。它主要用于內核空間與用戶空間的進程之間的通信,為兩者搭建了一座高效溝通的橋梁。與傳統的系統調用相比,Netlink 提供了更為靈活且高效的通訊方式,特別適用于在多線程環境中進行大量數據傳輸。在網絡配置、路由管理、設備驅動等眾多場景中,Netlink 都有著廣泛的應用。
比如,我們日常使用的網絡配置工具,就是通過 Netlink 與內核進行交互,實現對網絡接口的配置和管理;路由守護進程(routed)也依賴 Netlink 來獲取和更新路由信息,確保網絡數據包能夠正確地轉發。Netlink 之所以能夠在這些復雜的場景中發揮重要作用,得益于它的一些獨特優勢。接下來,就讓我們深入探索 Netlink 的奧秘,揭開它神秘的面紗。
一、什么是Netlink?
Netlink 套接字是實現用戶進程與內核進程通信的一種特殊的進程間通信(IPC)方式,也是網絡應用程序與內核通信的最常用接口 。它是 Linux 所特有的一種特殊 socket,類似于 BSD 中的 AF_ROUTE,但功能遠比其強大。在最新的 Linux 內核(2.6.14 及以后版本)中,有眾多應用借助 netlink 實現應用與內核的通信。
從本質上講,Netlink 是一種在內核與用戶應用間進行雙向數據傳輸的機制。用戶態應用可以使用標準的 socket API 來利用 netlink 提供的強大功能,而內核態則需要使用專門的內核 API 來操作 netlink。這就好比是兩個不同世界的居民,使用著不同的語言(API),卻通過 Netlink 這座橋梁能夠順暢地交流。
Netlink 的通信基于 BSD socket 和 AF_NETLINK 地址簇,采用 32 位的端口號尋址(以前也稱作 PID) 。每個 Netlink 協議(也可稱作總線,在 man 手冊中被稱為 netlink family),通常都與一個或一組內核服務 / 組件緊密相關聯。例如,NETLINK_ROUTE 專門用于獲取和設置路由與鏈路信息,當我們需要配置網絡路由時,相關的用戶空間程序就會通過 NETLINK_ROUTE 協議與內核進行交互;NETLINK_KOBJECT_UEVENT 則用于內核向用戶空間的 udev 進程發送通知,在設備熱插拔等事件發生時,內核就會通過這個協議將事件信息傳遞給用戶空間,以便 udev 進程做出相應的處理。
一般來說用戶空間和內核空間的通信方式有三種:/proc、ioctl、Netlink。而前兩種都是單向的,而Netlink可以實現雙工通信。
Netlink 相對于系統調用,ioctl 以及 /proc 文件系統而言具有以下優點:
- 為了使用 netlink,用戶僅需要在 include/linux/netlink.h 中增加一個新類型的 netlink 協議定義即可, 如 #define NETLINK_MYTEST 17 然后,內核和用戶態應用就可以立即通過 socket API 使用該 netlink 協議類型進行數據交換。但系統調用需要增加新的系統調用,ioctl 則需要增加設備或文件, 那需要不少代碼,proc 文件系統則需要在 /proc 下添加新的文件或目錄,那將使本來就混亂的 /proc 更加混亂。
- netlink是一種異步通信機制,在內核與用戶態應用之間傳遞的消息保存在socket緩存隊列中,發送消息只是把消息保存在接收者的socket的接 收隊列,而不需要等待接收者收到消息,但系統調用與 ioctl 則是同步通信機制,如果傳遞的數據太長,將影響調度粒度。
- 使用 netlink 的內核部分可以采用模塊的方式實現,使用 netlink 的應用部分和內核部分沒有編譯時依賴,但系統調用就有依賴,而且新的系統調用的實現必須靜態地連接到內核中,它無法在模塊中實現,使用新系統調用的應用在編譯時需要依賴內核。
- netlink 支持多播,內核模塊或應用可以把消息多播給一個netlink組,屬于該neilink 組的任何內核模塊或應用都能接收到該消息,內核事件向用戶態的通知機制就使用了這一特性,任何對內核事件感興趣的應用都能收到該子系統發送的內核事件,在 后面的文章中將介紹這一機制的使用。
- 內核可以使用 netlink 首先發起會話,但系統調用和 ioctl 只能由用戶應用發起調用。
- netlink 使用標準的 socket API,因此很容易使用,但系統調用和 ioctl則需要專門的培訓才能使用。
Netlink協議基于BSD socket和AF_NETLINK地址簇,使用32位的端口號尋址,每個Netlink協議通常與一個或一組內核服務/組件相關聯,如NETLINK_ROUTE用于獲取和設置路由與鏈路信息、NETLINK_KOBJECT_UEVENT用于內核向用戶空間的udev進程發送通知等。
二、用戶態數據結構
用戶態應用使用標準的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 就能很容易地使用 netlink socket,查詢手冊頁可以了解這些函數的使用細節,本文只是講解使用 netlink 的用戶應該如何使用這些函數。注意,使用 netlink 的應用必須包含頭文件 linux/netlink.h。當然 socket 需要的頭文件也必不可少,sys/socket.h。Netlink通信跟常用UDP Socket通信類似,struct sockaddr_nl是netlink通信地址,跟普通socket struct sockaddr_in類似。
(1)struct sockaddr_nl結構:
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK (跟AF_INET對應)*/
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID (通信端口號)*/
__u32 nl_groups; /* multicast groups mask */
};
(2)struct nlmsghd 結構:
/* struct nlmsghd 是netlink消息頭*/
struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process port ID */
};
nlmsg_type:消息狀態,內核在include/uapi/linux/netlink.h中定義了以下4種通用的消息類型,它們分別是:
#define NLMSG_NOOP 0x1 /* Nothing. */
#define NLMSG_ERROR 0x2 /* Error */
#define NLMSG_DONE 0x3 /* End of a dump */
#define NLMSG_OVERRUN 0x4 /* Data lost */
#define NLMSG_MIN_TYPE 0x10 /* < 0x10: reserved control messages */
nlmsg_flags:消息標記,它們用以表示消息的類型,如下:
/* Flags values */
#define NLM_F_REQUEST 1 /* It is request message. */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 8 /* Echo this request */
#define NLM_F_DUMP_INTR 16 /* Dump was inconsistent due to sequence change */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
(3)struct msghdr 結構體
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
/* iov_base: iov_base指向數據包緩沖區,即參數buff,iov_len是buff的長度。msghdr中允許一次傳遞多個buff,以數組的形式組織在 msg_iov中,msg_iovlen就記錄數組的長度 (即有多少個buff) */
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
為了創建一個 netlink socket,用戶需要使用如下參數調用 socket():
socket(AF_NETLINK, SOCK_RAW, netlink_type)
第一個參數必須是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它們倆實際為一個東西,它表示要使用netlink,第二個參數必須是SOCK_RAW或SOCK_DGRAM, 第三個參數指定netlink協議類型,如前面講的用戶自定義協議類型NETLINK_MYTEST, NETLINK_GENERIC是一個通用的協議類型,它是專門為用戶使用的,因此,用戶可以直接使用它,而不必再添加新的協議類型。內核預定義的協議類 型有:
#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
對于每一個netlink協議類型,可以有多達 32多播組,每一個多播組用一個位表示,netlink 的多播特性使得發送消息給同一個組僅需要一次系統調用,因而對于需要多撥消息的應用而言,大大地降低了系統調用的次數。
函數 bind() 用于把一個打開的 netlink socket 與 netlink 源 socket 地址綁定在一起。netlink socket 的地址結構如下:
struct sockaddr_nl
{
sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
字段 nl_family 必須設置為 AF_NETLINK 或著 PF_NETLINK,字段 nl_pad 當前沒有使用,因此要總是設置為 0,字段 nl_pid 為接收或發送消息的進程的 ID,如果希望內核處理消息或多播消息,就把該字段設置為 0,否則設置為處理消息的進程 ID。字段 nl_groups 用于指定多播組,bind 函數用于把調用進程加入到該字段指定的多播組,如果設置為 0,表示調用者不加入任何多播組。
傳遞給 bind 函數的地址的 nl_pid 字段應當設置為本進程的進程 ID,這相當于 netlink socket 的本地地址。但是,對于一個進程的多個線程使用 netlink socket 的情況,字段 nl_pid 則可以設置為其它的值,如:
pthread_self() << 16 | getpid();
因此字段 nl_pid 實際上未必是進程 ID,它只是用于區分不同的接收者或發送者的一個標識,用戶可以根據自己需要設置該字段。函數 bind 的調用方式如下:
bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));
fd為前面的 socket 調用返回的文件描述符,參數 nladdr 為 struct sockaddr_nl 類型的地址。為了發送一個 netlink 消息給內核或其他用戶態應用,需要填充目標 netlink socket 地址,此時,字段 nl_pid 和 nl_groups 分別表示接收消息者的進程 ID 與多播組。如果字段 nl_pid 設置為 0,表示消息接收者為內核或多播組,如果 nl_groups為 0,表示該消息為單播消息,否則表示多播消息。使用函數 sendmsg 發送 netlink 消息時還需要引用結構 struct msghdr、struct nlmsghdr 和 struct iovec,結構 struct msghdr 需如下設置:
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
其中 nladdr 為消息接收者的 netlink 地址,struct nlmsghdr 為 netlink socket 自己的消息頭,這用于多路復用和多路分解 netlink 定義的所有協議類型以及其它一些控制,netlink 的內核實現將利用這個消息頭來多路復用和多路分解已經其它的一些控制,因此它也被稱為netlink 控制塊。因此,應用在發送 netlink 消息時必須提供該消息頭。
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 指定消息的總長度,包括緊跟該結構的數據部分長度以及該結構的大小,字段 nlmsg_type 用于應用內部定義消息的類型,它對 netlink 內核實現是透明的,因此大部分情況下設置為 0,字段 nlmsg_flags 用于設置消息標志,可用的標志包括:
/* Flags values */
#define NLM_F_REQUEST 1 /* It is request message. */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 8 /* Echo this request */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
- 標志NLM_F_REQUEST用于表示消息是一個請求,所有應用首先發起的消息都應設置該標志。
- 標志NLM_F_MULTI 用于指示該消息是一個多部分消息的一部分,后續的消息可以通過宏NLMSG_NEXT來獲得。
- 宏NLM_F_ACK表示該消息是前一個請求消息的響應,順序號與進程ID可以把請求與響應關聯起來。
- 標志NLM_F_ECHO表示該消息是相關的一個包的回傳。
- 標志NLM_F_ROOT 被許多 netlink 協議的各種數據獲取操作使用,該標志指示被請求的數據表應當整體返回用戶應用,而不是一個條目一個條目地返回。有該標志的請求通常導致響應消息設置 NLM_F_MULTI標志。注意,當設置了該標志時,請求是協議特定的,因此,需要在字段 nlmsg_type 中指定協議類型。
- 標志 NLM_F_MATCH 表示該協議特定的請求只需要一個數據子集,數據子集由指定的協議特定的過濾器來匹配。
- 標志 NLM_F_ATOMIC 指示請求返回的數據應當原子地收集,這預防數據在獲取期間被修改。
- 標志 NLM_F_DUMP 未實現。
- 標志 NLM_F_REPLACE 用于取代在數據表中的現有條目。
- 標志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果條目已經存在,將失敗。
- 標志 NLM_F_CREATE 指示應當在指定的表中創建一個條目。
- 標志 NLM_F_APPEND 指示在表末尾添加新的條目。
內核需要讀取和修改這些標志,對于一般的使用,用戶把它設置為 0 就可以,只是一些高級應用(如 netfilter 和路由 daemon 需要它進行一些復雜的操作),字段 nlmsg_seq 和 nlmsg_pid 用于應用追蹤消息,前者表示順序號,后者為消息來源進程 ID。下面是一個示例:
#define MAX_MSGSIZE 1024
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
結構 struct iovec 用于把多個消息通過一次系統調用來發送,下面是該結構使用示例:
struct iovec iov;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
在完成以上步驟后,消息就可以通過下面語句直接發送:
sendmsg(fd, &msg, 0);
應用接收消息時需要首先分配一個足夠大的緩存來保存消息頭以及消息的數據部分,然后填充消息頭,添完后就可以直接調用函數 recvmsg() 來接收。
#define MAX_NL_MSG_LEN 1024
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
struct nlmsghdr * nlhdr;
nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
iov.iov_base = (void *)nlhdr;
iov.iov_len = MAX_NL_MSG_LEN;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
recvmsg(fd, &msg, 0);
注意:fd為socket調用打開的netlink socket描述符,在消息接收后,nlhdr指向接收到的消息的消息頭,nladdr保存了接收到的消息的目標地址,宏NLMSG_DATA(nlhdr)返回指向消息的數據部分的指針。
在linux/netlink.h中定義了一些方便對消息進行處理的宏,這些宏包括:
#define NLMSG_ALIGNTO 4
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
宏NLMSG_ALIGN(len)用于得到不小于len且字節對齊的最小數值。
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
宏NLMSG_LENGTH(len)用于計算數據部分長度為len時實際的消息長度。它一般用于分配消息緩存。
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字節對齊的最小數值,它也用于分配消息緩存。
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
宏NLMSG_DATA(nlh)用于取得消息的數據部分的首地址,設置和讀取消息數據部分時需要使用該宏。
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
宏NLMSG_NEXT(nlh,len)用于得到下一個消息的首地址,同時len也減少為剩余消息的總長度,該宏一般在一個消息被分成幾個部分發送或接收時使用。
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
宏NLMSG_OK(nlh,len)用于判斷消息是否有len這么長。
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
宏NLMSG_PAYLOAD(nlh,len)用于返回payload的長度,函數close用于關閉打開的netlink socket。
三、netlink內核數據結構
(1)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 /* Unused number, formerly ip_queue */
#define NETLINK_SOCK_DIAG 4 /* 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
#define NETLINK_RDMA 20
#define NETLINK_CRYPTO 21 /* Crypto layer */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG
#define MAX_LINKS 32
(2)netlink常用宏:
#define NLMSG_ALIGNTO 4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字節對齊的最小數值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* Netlink 頭部長度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 計算消息數據len的真實消息長度(消息體 + 消息頭)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字節對齊的最小數值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 宏NLMSG_DATA(nlh)用于取得消息的數據部分的首地址,設置和讀取消息數據部分時需要使用該宏 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* 宏NLMSG_NEXT(nlh,len)用于得到下一個消息的首地址, 同時len 變為剩余消息的長度 */
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/* 判斷消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的長度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
(3)netlink 內核常用函數
netlink_kernel_create內核函數用于創建內核socket與用戶態通信
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
/* net: net指向所在的網絡命名空間, 一般默認傳入的是&init_net(不需要定義); 定義在net_namespace.c(extern struct net init_net);
unit:netlink協議類型
cfg:cfg存放的是netlink內核配置參數(如下)
*/
/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb); /* input 回調函數 */
struct mutex *cb_mutex;
void (*bind)(int group);
bool (*compare)(struct net *net, struct sock *sk);
};
(4)單播netlink_unicast() 和 多播netlink_broadcast()
/* 發送單播消息 */
extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
/*
ssk: netlink socket
skb: skb buff 指針
portid:通信的端口號
nonblock:表示該函數是否為非阻塞,如果為1,該函數將在沒有接收緩存可利用時立即返回,而如果為0,該函數在沒有接收緩存可利用定時睡眠
*/
/* 發送多播消息 */
extern int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
__u32 group, gfp_t allocation);
/*
ssk: 同上(對應netlink_kernel_create 返回值)、
skb: 內核skb buff
portid:端口id
group: 是所有目標多播組對應掩碼的"OR"操作的合值。
allocation: 指定內核內存分配方式,通常GFP_ATOMIC用于中斷上下文,而GFP_KERNEL用于其他場合。這個參數的存在是因為該API可能需要分配一個或多個緩沖區來對多播消息進行clone
*/
四、netlink內核API
netlink的內核實現在.c文件net/core/af_netlink.c中,內核模塊要想使用netlink,也必須包含頭文件linux /netlink.h。內核使用netlink需要專門的API,這完全不同于用戶態應用對netlink的使用。如果用戶需要增加新的netlink協 議類型,必須通過修改linux/netlink.h來實現,當然,目前的netlink實現已經包含了一個通用的協議類型 NETLINK_GENERIC以方便用戶使用,用戶可以直接使用它而不必增加新的協議類型。前面講到,為了增加新的netlink協議類型,用戶僅需增 加如下定義到linux/netlink.h就可以:
#define NETLINK_MYTEST 17
只要增加這個定義之后,用戶就可以在內核的任何地方引用該協議,在內核中,為了創建一個netlink socket用戶需要調用如下函數:
struct sock *netlink_kernel_create(
int unit, void (*input)(struct sock *sk, int len));
參數unit表示netlink協議類型,如NETLINK_MYTEST,參數input則為內核模塊定義的netlink消息處理函數,當有消 息到達這個netlink socket時,該input函數指針就會被引用。函數指針input的參數sk實際上就是函數netlink_kernel_create返回的 struct sock指針,sock實際是socket的一個內核表示數據結構,用戶態應用創建的socket在內核中也會有一個struct sock結構來表示。下面是一個input函數的示例:
void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *data = NULL;
while ((skb = skb_dequeue(&sk->receive_queue))
!= NULL) {
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
data = NLMSG_DATA(nlh);
/* process netlink message with header pointed by
* nlh and data pointed by data
*/
}
}
函數input()會在發送進程執行sendmsg()時被調用,這樣處理消息比較及時,但是,如果消息特別長時,這樣處理將增加系統調用 sendmsg()的執行時間,對于這種情況,可以定義一個內核線程專門負責消息接收,而函數input的工作只是喚醒該內核線程,這樣sendmsg將 很快返回。
函數skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收隊列上的消息,返回為一個struct sk_buff的結構,skb->data指向實際的netlink消息。
函數skb_recv_datagram(nl_sk)也用于在netlink socket nl_sk上接收消息,與skb_dequeue的不同指出是,如果socket的接收隊列上沒有消息,它將導致調用進程睡眠在等待隊列 nl_sk->sk_sleep,因此它必須在進程上下文使用,剛才講的內核線程就可以采用這種方式來接收消息。
下面的函數input就是這種使用的示例:
void input (struct sock *sk, int len)
{
wake_up_interruptible(sk->sk_sleep);
}
當內核中發送netlink消息時,也需要設置目標地址與源地址,而且內核中消息是通過struct sk_buff來管理的, linux/netlink.h中定義了一個宏:
#define NETLINK_CB(skb) (*(struc
t 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。
在內核中,模塊調用函數 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的控制塊保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便設置該控制塊, 參數pid為接收消息進程的pid,參數nonblock表示該函數是否為非阻塞,如果為1,該函數將在沒有接收緩存可利用時立即返回,而如果為0,該函 數在沒有接收緩存可利用時睡眠。
內核模塊或子系統也可以使用函數netlink_broadcast來發送廣播消息:
void netlink_broadcast(
struct sock *sk, struct sk_bu
ff *skb, u32 pid, u32 group, int allocation
);
前面的三個參數與netlink_unicast相同,參數group為接收消息的多播組,該參數的每一個代表一個多播組,因此如果發送給多個多播 組,就把該參數設置為多個多播組組ID的位或。參數allocation為內核內存分配類型,一般地為GFP_ATOMIC或 GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。
在內核中使用函數sock_release來釋放函數netlink_kernel_create()創建的netlink socket:
void sock_release(struct socket * sock);
注意函數netlink_kernel_create()返回的類型為struct sock,因此函數sock_release應該這種調用:
sock_release(sk->sk_socket);
sk為函數netlink_kernel_create()的返回值。在源代碼包中 給出了一個使用 netlink 的示例,它包括一個內核模塊 netlink-exam-kern.c 和兩個應用程序 netlink-exam-user-recv.c, netlink-exam-user-send.c。內核模塊必須先插入到內核,然后在一個終端上運行用戶態接收程序,在另一個終端上運行用戶態發送程 序,發送程序讀取參數指定的文本文件并把它作為 netlink 消息的內容發送給內核模塊,內核模塊接受該消息保存到內核緩存中,它也通過proc接口出口到 procfs,因此用戶也能夠通過 /proc/netlink_exam_buffer 看到全部的內容,同時內核也把該消息發送給用戶態接收程序,用戶態接收程序將把接收到的內容輸出到屏幕上。
五、Netlink性能優勢
(1)異步通信,效率飛升
Netlink 是一種異步通信機制 ,這是它的一大亮點。在異步通信中,當發送方發送消息時,消息會被暫存在 socket 接收緩存中,發送方無需等待接收者立即處理消息,就可以繼續執行其他任務。就好比我們寄快遞,把包裹交給快遞員后,我們不用一直等著包裹被收件人簽收,就可以去做別的事情了。
與之相對的同步通信,就像打電話,在對方接聽并回應之前,我們只能干等著,啥也做不了。在系統調用和 ioctl 這類同步通信機制中,如果傳遞的數據量較大或者接收方處理速度較慢,發送方就會被阻塞,這無疑會影響整個系統的調度粒度和效率。而 Netlink 的異步通信機制避免了這種等待,大大提升了系統的效率,使得系統能夠更高效地處理多個任務。
(2)全雙工通信,雙向奔赴
全雙工通信是指在通信的任意時刻,線路上可以同時存在 A 到 B 和 B 到 A 的雙向信號傳輸。Netlink 就支持全雙工通信,這意味著在內核與用戶空間之間,數據能夠同時在兩個方向上傳輸 。例如,當用戶空間的網絡配置工具向內核發送配置請求時,內核可以同時將網絡狀態信息反饋給用戶空間,雙方的交流就像兩個人面對面交談一樣順暢,無需等待一方說完另一方才能開口。
這種雙向數據傳輸的能力,使得內核和用戶空間能夠及時地交換信息,極大地增強了系統的交互性和響應速度。在網絡監控場景中,用戶空間的監控程序可以實時向內核詢問網絡流量等信息,內核也能隨時將新的網絡事件通知給監控程序,確保監控的實時性和準確性。
(3)多播功能,一對多的狂歡
Netlink 支持多播功能,這為它在一些特定場景下的應用提供了強大的支持。通過多播,內核模塊或應用可以把消息發送給一個 Netlink 組,屬于該組的任何內核模塊或應用都能接收到該消息 。每個 Netlink 協議類型最多可以有 32 個多播組,每個多播組用一個位表示,發送消息給同一個組僅需要一次系統調用,大大降低了系統調用的次數。
在網絡管理中,當內核檢測到網絡拓撲發生變化時,它可以通過多播將這一消息同時發送給多個關注網絡狀態的用戶空間進程,如網絡監控程序、路由守護進程等,這些進程就能及時做出相應的調整,而不需要內核分別向每個進程單獨發送消息,大大提高了信息傳遞的效率。
(4)簡單易用,上手輕松
對于開發者來說,Netlink 的易用性也是它的一大優勢。用戶態應用可以使用標準的 socket API,如 socket ()、bind ()、sendmsg ()、recvmsg () 和 close () 等函數來使用 Netlink socket 。這對于熟悉 socket 編程的開發者來說,幾乎沒有學習成本,能夠快速上手進行 Netlink 相關的開發。不需要像使用系統調用和 ioctl 那樣,需要專門的培訓才能使用,降低了開發的門檻,使得開發者能夠更專注于業務邏輯的實現。
(5)無編譯依賴,靈活部署
使用 Netlink 的內核部分可以采用模塊的方式實現,并且使用 Netlink 的應用部分和內核部分沒有編譯時依賴 。這意味著在開發過程中,我們可以獨立地對內核模塊和用戶空間應用進行開發、調試和更新,而不需要因為一方的改動而重新編譯另一方。當我們需要更新內核模塊的功能時,只需要重新編譯內核模塊并加載,而用戶空間應用無需重新編譯就可以繼續使用;反之,當用戶空間應用進行功能升級時,也不會影響到內核模塊。這種靈活性大大提高了開發和部署的效率,使得系統的維護和升級更加便捷。
六、Netlink常用場景
6.1網絡配置與管理
在網絡配置與管理領域,Netlink 堪稱一把 “瑞士軍刀”,發揮著舉足輕重的作用。
在網絡接口管理方面,Netlink 為用戶空間程序與內核之間搭建了一座溝通的橋梁,使得對網絡接口的各種操作變得輕而易舉。通過 Netlink,我們可以創建、刪除虛擬接口,就像在搭建一個虛擬網絡世界時,能夠自由地添加或移除各種 “虛擬橋梁” 。比如,在容器網絡中,經常會用到 veth 對(虛擬以太網設備對)來實現容器與外部網絡的通信,而創建 veth 對就是通過 Netlink 與內核交互完成的。我們還能靈活地配置接口屬性,像設置 MTU(最大傳輸單元),就如同為網絡傳輸的 “通道” 設定合適的寬度,確保數據能夠高效傳輸;修改 MAC 地址,就像是給網絡設備換了一個獨特的 “身份標識”;以及啟用或禁用接口,控制網絡設備的 “開關” 。在企業網絡中,當需要調整網絡架構時,管理員可以利用 Netlink 相關工具,快速地對網絡接口進行配置,保障網絡的穩定運行。
路由表更新也是 Netlink 的重要應用場景之一。它能夠幫助我們添加、刪除路由條目,如同在網絡的 “地圖” 上標記或抹去特定的路徑 。當我們要設置靜態路由,讓數據包按照指定的路線傳輸時,就可以借助 Netlink 向內核發送相應的消息。例如,在一個擁有多個子網的企業網絡中,為了實現不同子網之間的通信,管理員可以通過 Netlink 添加靜態路由條目。查詢當前路由表狀態也不在話下,通過 Netlink,我們能隨時獲取路由表的信息,了解網絡的 “交通路線” 狀況 。這對于網絡故障排查非常重要,當網絡出現連接問題時,管理員可以通過查詢路由表,判斷數據包的傳輸路徑是否正確,從而快速定位問題所在。
在實際應用中,有許多強大的工具都依賴 Netlink 來實現網絡配置與管理功能。其中,iproute2 工具集就是一個典型代表 。它包含了眾多實用的命令,如 ip addr 用于管理網絡接口地址,ip route 用于操作路由表等。這些命令底層都是通過 Netlink 與內核進行交互,替代了傳統的 ifconfig 和 route 命令。當我們執行 “ip route add 192.168.2.0/24 via 10.0.0.1” 這條命令時,實際上是通過 NETLINK_ROUTE 協議向內核發送 RTM_NEWROUTE 消息,告知內核添加一條到 192.168.2.0/24 網絡的路由,下一跳為 10.0.0.1 。還有一些網絡自動化配置工具,在大規模網絡部署中,通過調用 Netlink 接口,能夠快速、批量地對網絡設備進行配置,大大提高了網絡部署的效率。
6.2系統監控與安全
Netlink 在系統監控與安全領域同樣有著不可忽視的作用。
在系統資源監控方面,Netlink 為用戶空間的監控工具打開了一扇通往內核信息寶庫的大門。通過 Netlink,監控工具可以實時獲取系統資源的使用情況,如 CPU 使用率、內存占用、磁盤 I/O 等 。這就好比我們在駕駛汽車時,儀表盤上的各種指針和數據能夠實時反饋汽車的運行狀態,讓我們隨時了解車輛的情況。以 top 命令為例,它能夠動態顯示系統中各個進程的資源占用情況,而這背后就離不開 Netlink 的支持。top 命令通過 Netlink 與內核通信,獲取進程的相關信息,然后進行整理和展示,讓用戶對系統的運行狀態一目了然。在服務器運維中,管理員可以利用基于 Netlink 的監控工具,實時監控服務器的資源使用情況,當發現資源使用率過高時,及時采取措施進行優化,保障服務器的穩定運行。
在安全策略管理方面,Netlink 扮演著重要的角色。它為用戶空間的安全工具與內核之間的通信提供了通道,使得安全策略的管理和配置變得更加高效 。防火墻是保障網絡安全的重要防線,用戶空間的防火墻配置工具(如 iptables、nftables)可以通過 Netlink 與內核中的 netfilter 模塊進行通信,實現對網絡數據包的過濾和安全策略的設置 。我們可以通過這些工具,根據實際需求制定規則,允許或禁止特定的網絡連接,就像在城堡的大門設置守衛,對進出的人員進行嚴格的檢查和篩選。入侵檢測系統(IDS)和入侵防御系統(IPS)也可以利用 Netlink 獲取網絡數據包和系統狀態信息,及時發現并阻止潛在的安全威脅 。當 IDS 檢測到異常的網絡流量時,它可以通過 Netlink 向內核發送相關信息,觸發相應的防御機制,保障系統的安全。
6.3內核與用戶空間交互
Netlink 作為內核與用戶空間通信的橋梁,其重要性不言而喻,在眾多場景中都發揮著關鍵作用。
設備驅動與用戶空間程序的交互是一個常見的場景。設備驅動在內核空間負責與硬件設備進行通信,而用戶空間程序則需要與設備驅動交互,以實現對硬件設備的控制和數據傳輸 。以網絡設備驅動為例,當用戶空間的網絡應用程序需要發送或接收網絡數據包時,它會通過 Netlink 與網絡設備驅動進行通信。用戶空間程序將數據包發送給設備驅動,設備驅動再將數據包發送到物理網絡上;反之,當設備驅動接收到來自網絡的數據包時,也會通過 Netlink 將數據包傳遞給用戶空間程序 。這就像在一個工廠中,生產線上的工人(設備驅動)與管理人員(用戶空間程序)需要密切配合,通過特定的溝通渠道(Netlink)來協調工作,確保生產的順利進行。
系統服務與內核之間的通信也經常依賴 Netlink。許多系統服務,如網絡服務、存儲服務等,需要與內核進行交互,獲取系統資源或執行特定的操作 。在網絡服務中,DHCP(動態主機配置協議)服務器需要與內核通信,獲取網絡接口信息,為客戶端分配 IP 地址 。這一過程中,DHCP 服務器就會通過 Netlink 向內核發送請求,內核響應請求并返回相關信息,實現了系統服務與內核之間的信息交互,保障了網絡服務的正常運行。
七、Netlink 工作原理
Netlink 的架構就像是一個精心構建的通信網絡,各個部分協同工作,實現了內核與用戶空間之間高效的通信。我們來看下面這張:
圖片
從圖中可以看出,Netlink 主要由以下幾個部分組成:
- 用戶空間應用:這是我們日常使用的各種應用程序,它們通過標準的 socket API 與 Netlink 套接字進行交互。比如我們前面提到的網絡配置工具、系統監控程序等,它們通過 Netlink 向內核發送請求,獲取系統信息或者執行特定的操作。
- Netlink 套接字:作為用戶空間與內核空間通信的橋梁,Netlink 套接字負責在兩者之間傳遞數據。它基于 BSD socket 和 AF_NETLINK 地址簇,采用 32 位的端口號尋址 。每個 Netlink 套接字都有一個對應的協議類型,用于標識通信的內容和目的。
- 內核空間:內核是 Linux 系統的核心,它包含了各種設備驅動、網絡協議棧等重要組件。內核通過 Netlink 與用戶空間進行通信,接收用戶空間的請求并返回相應的結果,同時也可以主動向用戶空間發送通知和事件信息。
- Netlink 協議族:Netlink 支持多種協議類型,每種協議類型都與特定的內核服務或組件相關聯。例如,NETLINK_ROUTE 用于網絡路由相關的操作,NETLINK_KOBJECT_UEVENT 用于內核向用戶空間發送設備事件通知等 。不同的協議類型使得 Netlink 能夠滿足各種不同的通信需求。
7.1Netlink 協議類型全解析
Netlink 協議族豐富多樣,目前支持 32 種協議類型 ,它們在不同的場景中發揮著關鍵作用。以下是一些常見的 Netlink 協議類型及其用途:
NETLINK_ROUTE:這是最為常用的協議類型之一,主要用于網絡路由和設備相關的操作 。通過它,我們可以獲取和設置路由信息,就像為網絡數據包規劃行進的路線;管理網絡接口,如創建、刪除接口,配置接口屬性等,相當于對網絡的 “出入口” 進行管控;還能監控網絡狀態,實時掌握網絡的運行情況 。在企業網絡中,網絡管理員經常會使用基于 NETLINK_ROUTE 的工具來配置和管理網絡路由,確保網絡的高效運行。
NETLINK_SOCK_DIAG:主要用于監控系統中的套接字信息 。它可以獲取套接字的狀態,比如是處于監聽狀態、連接狀態還是關閉狀態;查看套接字選項,了解套接字的各種配置參數 。在網絡故障排查中,NETLINK_SOCK_DIAG 非常有用,管理員可以通過它來檢查網絡連接是否正常,是否存在套接字資源泄漏等問題。例如,當我們發現某個網絡應用無法正常連接時,可以使用基于 NETLINK_SOCK_DIAG 的工具來查看相關套接字的狀態,找出問題所在。
NETLINK_NFLOG:是 netfilter/iptables ULOG 的通信接口,在防火墻和網絡安全領域有著重要應用 。它允許用戶空間的防火墻工具(如 iptables)與內核中的 netfilter 模塊進行通信,實現對網絡數據包的過濾和日志記錄 。當有網絡數據包進入系統時,netfilter 模塊會根據預設的規則對數據包進行檢查,然后通過 NETLINK_NFLOG 將相關信息(如數據包是否被允許通過、被丟棄的原因等)傳遞給用戶空間的防火墻工具,以便進行進一步的處理和分析。在企業網絡安全防護中,管理員可以利用 NETLINK_NFLOG 來監控網絡流量,及時發現和阻止潛在的安全威脅。
NETLINK_KOBJECT_UEVENT:用于內核向用戶空間發送設備事件通知,是內核熱插拔機制的基礎 。當有設備插入或拔出系統時,內核會通過 NETLINK_KOBJECT_UEVENT 協議向用戶空間發送相應的事件消息 。用戶空間的 udev 進程接收到這些消息后,會根據設備的屬性和規則,自動完成設備的識別、驅動加載等操作。在我們日常使用的電腦中,當插入 U 盤時,系統能夠自動識別并掛載 U 盤,這背后就離不開 NETLINK_KOBJECT_UEVENT 的支持。
NETLINK_GENERIC:作為一種通用的 Netlink 協議類型,它就像是一個靈活的 “瑞士軍刀”,為用戶提供了自定義協議的能力 。當現有的協議類型無法滿足特定需求時,用戶可以基于 NETLINK_GENERIC 定義自己的子協議類型 。在一些特定的行業應用中,可能需要與內核進行特定的數據交互,此時就可以利用 NETLINK_GENERIC 來實現自定義的通信協議,滿足業務的特殊需求。
7.2數據結構與函數
在 Netlink 的世界里,有一些關鍵的數據結構和函數,它們是實現高效通信的基礎。
⑴數據結構
struct sockaddr_nl:這是 Netlink 通信地址的數據結構,類似于普通 socket 編程中的 struct sockaddr_in 。它包含以下幾個重要成員:
- nl_family:固定為 AF_NETLINK,用于標識地址族,就像給通信地址貼上一個 “Netlink 專屬” 的標簽 。
- nl_pad:目前未使用,通常填充為 0,就像一個暫時閑置的小隔間。
- nl_pid:表示端口 ID,通常設置為當前進程的進程號,用于唯一標識一個基于 netlink 的 socket 通道 。當用戶空間的進程與內核進行通信時,內核可以通過這個 nl_pid 來識別是哪個進程在發送請求。
- nl_groups:用于指定多播組,是多播組掩碼 。每個 Netlink 協議最多支持 32 個多播組,每個多播組用一個 bit 表示。如果進程希望加入某個多播組,就需要設置相應的 bit 位 。在網絡監控場景中,多個監控程序可以加入同一個多播組,接收內核發送的網絡狀態信息。
struct nlmsghdr:這是 Netlink 消息頭的數據結構,每個 Netlink 消息都包含這樣一個消息頭 。它的成員如下:
- nlmsg_len:表示整個消息的長度,包括消息頭和消息體就像一個包裹的總重量包含了包裝盒和里面的物品 。
- nlmsg_type:用于標識消息的類型,比如是數據消息還是控制消息 。內核定義了一些標準的消息類型,如 NLMSG_NOOP(空消息,什么也不做)、NLMSG_ERROR(表示消息中包含錯誤)、NLMSG_DONE(用于標記消息隊列的結束)等 。
- nlmsg_flags:是消息的附加標志位,用于對消息進行額外的控制 。例如,NLM_F_REQUEST 表示這是一個請求消息,NLM_F_MULTI 表示消息由多個部分組成,最后一個部分會標注 NLMSG_DONE 。
- nlmsg_seq:是序列號,用于追蹤消息的順序,類似于快遞單號,方便接收方對消息進行排序和處理 。
- nlmsg_pid:表示發送進程的端口 ID,用于接收方識別消息的來源 。
⑵函數
- socket():在用戶空間創建 Netlink 套接字時使用,它的參數包括地址族(AF_NETLINK)、套接字類型(SOCK_RAW 或 SOCK_DGRAM)和協議類型 。就像是在通信網絡中搭建一個新的 “通信站點”,指定它的類型和所屬的 “通信頻道”。例如,int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); 這行代碼創建了一個基于 NETLINK_ROUTE 協議的 RAW 類型的 Netlink 套接字。
- bind():用于將 Netlink 套接字與本地地址綁定,也就是給這個 “通信站點” 確定一個具體的位置 。它的參數包括套接字描述符、本地地址和地址長度 。通過綁定,內核和其他進程就知道該去哪里與這個套接字進行通信。例如,bind(sock_fd, (struct sockaddr*)&nl_addr, sizeof(nl_addr)); 這行代碼將之前創建的套接字 sock_fd 與本地地址 nl_addr 進行綁定。
- sendmsg():用于發送 Netlink 消息,它需要一個 struct msghdr 結構體來指定消息的各種參數,包括目標地址、消息內容等 。就像是把裝滿信息的 “包裹” 發送出去,告訴快遞員(內核或其他進程)要送到哪里。在發送消息前,需要填充好 struct msghdr 結構體的各個成員,然后調用 sendmsg 函數將消息發送出去。
- recvmsg():用于接收 Netlink 消息,接收方通過這個函數從套接字接收內核或其他進程發送過來的消息 。它就像是在 “通信站點” 等待接收 “包裹”,當有消息到達時,將其接收并進行處理。例如,recvmsg(sock_fd, &msg, 0); 這行代碼從套接字 sock_fd 接收消息,并將消息存儲在 msg 結構體中。
7.3通信流程大起底
Netlink 的通信流程就像是一場有序的接力賽,每個環節都緊密相連,確保數據能夠準確、高效地傳輸。下面我們來詳細了解一下 Netlink 的通信流程:
①消息發送:
- 用戶空間應用首先使用 socket () 函數創建一個 Netlink 套接字,并指定協議類型 。比如要進行網絡路由相關的操作,就創建一個基于 NETLINK_ROUTE 協議的套接字。
- 然后,應用使用 bind () 函數將套接字與本地地址綁定,確定通信的端點 。
- 接下來,應用構造一個 Netlink 消息,填充 struct nlmsghdr 消息頭和消息體 。消息頭中設置消息類型、標志位、序列號等信息,消息體中包含具體的數據內容。
- 最后,應用使用 sendmsg () 函數將消息發送出去 。在發送時,需要指定目標地址(如果是發送給內核,nl_pid 和 nl_groups 通常設置為 0;如果是發送給其他進程,則設置為目標進程的 pid 和相應的多播組掩碼)。
②消息接收:
- 內核或其他接收方進程在接收到 Netlink 消息后,首先會根據消息頭中的信息進行初步處理 。
- 內核會檢查消息的合法性,包括消息類型是否正確、消息長度是否符合要求等 。如果消息不合法,會返回錯誤信息。
- 對于合法的消息,內核會根據消息類型和協議類型進行進一步的處理 。例如,如果是 NETLINK_ROUTE 協議的消息,內核會將其轉發到網絡路由模塊進行處理。
③消息處理:
- 內核或接收方進程根據消息的內容執行相應的操作 。如果是網絡配置請求,內核會更新網絡配置信息;如果是設備事件通知,內核會通知相關的設備驅動進行處理。
- 處理完成后,內核或接收方進程可能會返回一個響應消息給發送方 。響應消息同樣包含消息頭和消息體,消息頭中會設置相應的標志位和消息類型(如 NLMSG_ACK 表示確認消息,NLMSG_ERROR 表示錯誤消息)。
- 發送方接收到響應消息后,根據消息內容進行相應的處理 。如果是確認消息,發送方知道自己的請求已經被成功處理;如果是錯誤消息,發送方會根據錯誤信息進行調試和修正。
八、Netlink 應用開發實戰
8.1準備工作
在開始 Netlink 應用開發之前,我們需要搭建好開發環境,準備好必要的工具。
首先,確保你的開發機器上安裝了 Linux 系統,推薦使用較新的版本,如 Ubuntu 20.04 或 CentOS 8,因為它們對 Netlink 的支持更加完善 。如果你還沒有安裝 Linux 系統,可以通過虛擬機軟件(如 VirtualBox 或 VMware)來安裝,這就好比在你的電腦里搭建了一個虛擬的 Linux 世界,讓你可以在不影響原有系統的情況下進行開發。
接下來,安裝開發工具。GCC(GNU Compiler Collection)是 Linux 下常用的編譯器,我們需要它來編譯我們的代碼 。在 Ubuntu 系統中,可以通過以下命令安裝:
sudo apt-get update
sudo apt-get install build-essential
在 CentOS 系統中,安裝命令如下:
sudo yum groupinstall "Development Tools"
除了 GCC,還需要安裝一些開發庫,如 libnl。libnl 是一個用于簡化 Netlink 編程的庫,它提供了一些封裝好的函數,讓我們可以更方便地使用 Netlink 。在 Ubuntu 系統中,可以通過以下命令安裝:
sudo apt-get install libnl-3-dev libnl-genl-3-dev
在 CentOS 系統中,安裝命令如下:
sudo yum install libnl3-devel libnl3-genl-devel
8.2用戶態編程示例
下面我們來看一個用戶態的 Netlink 編程示例,通過這個示例,你將學會如何創建 Netlink 套接字、綁定地址、發送和接收消息。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define NETLINK_USER 31 // 自定義Netlink協議號
#define MSG_LEN 1024
int main() {
// 創建Netlink套接字
int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER);
if (sock_fd < 0) {
perror("socket");
return -1;
}
// 本地地址配置
struct sockaddr_nl src_addr;
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); // 綁定到當前進程
src_addr.nl_groups = 0; // 不訂閱多播
// 綁定套接字
if (bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)) < 0) {
perror("bind");
close(sock_fd);
return -1;
}
// 目標地址配置(內核)
struct sockaddr_nl dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; // 發送到內核
dest_addr.nl_groups = 0; // 不訂閱多播
// 構造發送消息
struct nlmsghdr *nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MSG_LEN));
memset(nlh, 0, NLMSG_SPACE(MSG_LEN));
nlh->nlmsg_len = NLMSG_SPACE(MSG_LEN); // 消息長度
nlh->nlmsg_pid = getpid(); // 發送者PID
nlh->nlmsg_flags = 0; // 無特殊標志位
strcpy((char*)NLMSG_DATA(nlh), "Hello from user space!"); // 消息內容
// 發送消息到內核
if (sendto(sock_fd, nlh, nlh->nlmsg_len, 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr)) < 0) {
perror("sendto");
free(nlh);
close(sock_fd);
return -1;
}
printf("Message sent to kernel: %s\n", (char*)NLMSG_DATA(nlh));
// 接收內核響應
memset(nlh, 0, NLMSG_SPACE(MSG_LEN));
if (recv(sock_fd, nlh, NLMSG_SPACE(MSG_LEN), 0) < 0) {
perror("recv");
free(nlh);
close(sock_fd);
return -1;
}
printf("Message received from kernel: %s\n", (char*)NLMSG_DATA(nlh));
// 清理資源
free(nlh);
close(sock_fd);
return 0;
}
- 創建 Netlink 套接字:socket(AF_NETLINK, SOCK_RAW, NETLINK_USER) 函數用于創建一個 Netlink 套接字,AF_NETLINK 表示地址族,SOCK_RAW 表示套接字類型為原始套接字,NETLINK_USER 是我們自定義的 Netlink 協議號 。這一步就像是在網絡世界中搭建了一個專門用于 Netlink 通信的 “站點”。
- 綁定地址:通過 bind 函數將套接字與本地地址綁定,src_addr.nl_pid = getpid() 將端口 ID 設置為當前進程的進程號,這樣內核就知道該套接字屬于哪個進程 。就好比給這個 “站點” 貼上了一個獨一無二的 “標簽”,方便識別。
- 構造發送消息:創建一個 nlmsghdr 結構體來表示消息頭,設置消息長度、發送者 PID、標志位等信息,并將消息內容復制到消息體中 。這就像是把要發送的信息裝進一個 “包裹”,并填寫好收件人和寄件人的信息。
- 發送消息:使用 sendto 函數將消息發送到內核,指定目標地址為內核地址(dest_addr.nl_pid = 0) 。就像是把 “包裹” 寄給內核這個 “收件人”。
- 接收消息:通過 recv 函數接收內核返回的響應消息,并打印出來 。就像是在 “站點” 等待接收內核寄回的 “包裹”,并查看里面的內容。
8.3內核態編程示例
接下來是內核態的 Netlink 編程示例,展示如何在內核模塊中創建 Netlink socket、注冊回調函數、發送和接收消息。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>
#define NETLINK_USER 31
struct sock *nl_sk = NULL;
static void netlink_recv_msg(struct sk_buff *skb) {
struct nlmsghdr *nlh;
int pid;
struct sk_buff *skb_out;
char *msg = "Hello from kernel!";
int msg_size = strlen(msg);
int res;
// 獲取Netlink消息頭部
nlh = (struct nlmsghdr*)skb->data;
printk(KERN_INFO "Kernel received message: %s\n", (char*)NLMSG_DATA(nlh));
pid = nlh->nlmsg_pid; // 獲取用戶進程PID
// 構造響應消息
skb_out = nlmsg_new(msg_size, 0);
if (!skb_out) {
printk(KERN_ERR "Failed to allocate new skb\n");
return;
}
nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
strncpy(NLMSG_DATA(nlh), msg, msg_size);
res = nlmsg_unicast(nl_sk, skb_out, pid); // 發送響應
if (res < 0) {
printk(KERN_INFO "Error sending message to user\n");
}
}
static int __init netlink_init(void) {
struct netlink_kernel_cfg cfg = {
.input = netlink_recv_msg // 注冊消息接收回調
};
nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
if (!nl_sk) {
printk(KERN_ALERT "Error creating Netlink socket\n");
return -10;
}
printk(KERN_INFO "Netlink module loaded\n");
return 0;
}
static void __exit netlink_exit(void) {
netlink_kernel_release(nl_sk);
printk(KERN_INFO "Netlink module unloaded\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_LICENSE("GPL");
- 創建 Netlink socket 并注冊回調函數:netlink_kernel_create 函數用于在內核中創建一個 Netlink socket,并注冊一個回調函數 netlink_recv_msg 。當有 Netlink 消息到達時,內核會調用這個回調函數進行處理 。這就像是在內核中設置了一個 “消息接收站”,并指定了處理消息的 “工作人員”。
- 接收消息:在 netlink_recv_msg 函數中,首先獲取消息頭部,打印接收到的消息內容,并獲取發送方的 PID 。這一步就像是 “工作人員” 接收消息,并查看寄件人的信息。
- 構造響應消息:使用 nlmsg_new 函數創建一個新的套接字緩沖區來存放響應消息,然后使用 nlmsg_put 函數填充消息頭和消息體 。這就像是 “工作人員” 準備好要回復的 “包裹”,并填寫好相關信息。
- 發送響應消息:通過 nlmsg_unicast 函數將響應消息發送回給用戶空間的進程,指定目標 PID 為發送方的 PID 。這就像是 “工作人員” 把回復的 “包裹” 寄給寄件人。
8.4測試與調試
在完成代碼編寫后,我們需要對程序進行測試和調試,以確保其正常運行。
測試方法:
①編譯內核模塊:將上述內核態代碼保存為 netlink_kernel.c 文件,然后使用以下命令編譯:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
編譯完成后,會生成 netlink_kernel.ko 文件。
②加載內核模塊:使用以下命令加載內核模塊:
sudo insmod netlink_kernel.ko
③編譯用戶態程序:將上述用戶態代碼保存為 netlink_user.c 文件,然后使用以下命令編譯:
gcc -o netlink_user netlink_user.c
④運行用戶態程序:執行編譯好的用戶態程序:
./netlink_user
如果一切正常,你應該能看到用戶態程序發送消息到內核,并接收到內核返回的響應消息。
調試技巧:
①打印調試信息:在內核模塊中,可以使用 printk 函數打印調試信息 。通過查看內核日志(使用 dmesg 命令),可以了解內核模塊的運行情況。比如,在 netlink_recv_msg 函數中,我們使用 printk 打印了接收到的消息內容,這樣在調試時就可以清楚地知道內核接收到了什么消息。
②使用 GDB 調試用戶態程序:對于用戶態程序,可以使用 GDB(GNU Debugger)進行調試 。在編譯用戶態程序時,加上 -g 選項,生成包含調試信息的可執行文件:
gcc -g -o netlink_user netlink_user.c
然后使用 GDB 啟動調試:
gdb netlink_user
在 GDB 中,可以設置斷點、單步執行等,幫助我們找出程序中的問題。比如,我們可以在 sendto 函數調用處設置斷點,查看發送消息時的參數是否正確。
③檢查 Netlink 套接字狀態:使用 netstat -anp | grep netlink 命令可以查看 Netlink 套接字的狀態,包括是否綁定成功、是否正在監聽等 。這對于排查網絡連接問題非常有幫助。如果發現套接字沒有正確綁定,就需要檢查綁定代碼和地址配置是否正確。
九、Netlink 使用中的注意事項
9.1錯誤處理
在 Netlink 通信中,錯誤處理至關重要。它就像是通信過程中的 “安全衛士”,能夠確保通信的穩定性和可靠性。如果忽視錯誤處理,一旦出現問題,程序可能會出現異常行為,甚至導致系統崩潰。常見的錯誤類型包括:
套接字創建失敗:在使用 socket () 函數創建 Netlink 套接字時,可能會因為系統資源不足、地址族或協議類型錯誤等原因導致創建失敗 。比如,當系統中同時創建的套接字數量過多,超過了系統的限制時,就會出現套接字創建失敗的情況。此時,我們需要檢查返回值,如果返回值小于 0,說明創建失敗,應使用perror函數打印錯誤信息,以便定位問題所在。
綁定地址失敗:bind () 函數用于將套接字與本地地址綁定,如果綁定失敗,可能是因為地址已被占用、權限不足等原因 。在一個多進程的系統中,如果多個進程嘗試綁定同一個地址,就會出現地址沖突,導致綁定失敗。當綁定失敗時,同樣需要打印錯誤信息,根據錯誤提示進行處理,比如更換綁定地址或者提升權限。
消息發送失敗:sendmsg () 函數在發送 Netlink 消息時,可能會因為網絡故障、目標地址錯誤、消息長度超過限制等原因導致發送失敗 。當網絡出現中斷時,消息就無法正常發送。如果消息發送失敗,我們需要根據具體的錯誤情況進行處理,比如重新發送消息、檢查目標地址等。
消息接收失敗:recvmsg () 函數接收消息時,也可能會遇到各種問題,如套接字未正確連接、緩沖區溢出等 。當接收緩沖區的大小小于接收到的消息長度時,就會發生緩沖區溢出,導致消息接收失敗。在這種情況下,我們可以通過調整緩沖區大小、檢查套接字連接狀態等方式來解決問題。
9.2性能優化
為了提高 Netlink 通信的性能,我們可以從以下幾個方面入手:
合理設置緩沖區大小:緩沖區的大小直接影響著通信的效率。如果緩沖區過小,可能會導致消息丟失或需要頻繁地進行數據傳輸;如果緩沖區過大,又會浪費系統資源 。在實際應用中,我們需要根據消息的大小和通信的頻率來合理設置緩沖區大小。對于一些實時性要求較高、消息量較小的通信場景,可以適當減小緩沖區大小,以提高消息的處理速度;而對于一些大數據量的傳輸場景,則需要增大緩沖區大小,減少數據傳輸的次數。
優化消息處理邏輯:在處理 Netlink 消息時,應盡量減少不必要的計算和操作,提高消息處理的速度 。可以采用多線程或異步處理的方式,將消息處理任務分配到不同的線程中,避免主線程被阻塞。在處理網絡配置消息時,如果需要進行復雜的網絡計算,可以將這些計算任務放到單獨的線程中執行,主線程繼續接收和處理其他消息,從而提高系統的整體性能。
減少系統調用次數:系統調用是一種比較耗時的操作,應盡量減少不必要的系統調用 。在 Netlink 通信中,可以通過批量處理消息的方式,減少 sendmsg () 和 recvmsg () 等系統調用的次數。當需要發送多個小消息時,可以將這些消息合并成一個大消息進行發送,從而減少系統調用的開銷。
使用高效的數據結構和算法:選擇合適的數據結構和算法可以顯著提高程序的性能 。在存儲和處理 Netlink 消息時,可以使用鏈表、哈希表等數據結構來提高數據的查找和訪問效率。對于一些需要頻繁查找消息的場景,使用哈希表可以大大提高查找速度,減少處理時間。
9.3安全問題
在 Netlink 通信中,安全問題不容忽視。如果通信過程中出現安全漏洞,可能會導致系統信息泄露、被惡意攻擊等嚴重后果。以下是一些需要注意的安全問題:
防止非法訪問:Netlink 通信涉及到內核與用戶空間的交互,需要確保只有合法的進程能夠訪問 Netlink 套接字 。可以通過權限控制、身份驗證等方式來防止非法訪問。在創建 Netlink 套接字時,可以設置適當的權限,只有具有相應權限的用戶或進程才能訪問;也可以采用身份驗證機制,如使用數字證書、密鑰等方式,驗證通信雙方的身份,確保通信的安全性。
防止數據篡改:在 Netlink 消息傳輸過程中,需要防止消息被篡改 。可以使用消息摘要、加密等技術來保證數據的完整性和保密性。消息摘要算法(如 MD5、SHA-1 等)可以生成消息的唯一摘要,接收方可以通過驗證摘要來判斷消息是否被篡改;加密技術(如 AES、RSA 等)可以對消息進行加密,只有擁有正確密鑰的接收方才能解密消息,從而保證消息的保密性。
避免緩沖區溢出:緩沖區溢出是一種常見的安全漏洞,可能會被攻擊者利用來執行惡意代碼 。在處理 Netlink 消息時,要確保接收和發送緩沖區的大小足夠,并且對輸入的數據進行嚴格的邊界檢查,防止緩沖區溢出的發生。在接收消息時,要檢查消息的長度是否超過了緩沖區的大小,如果超過,應采取相應的措施,如拒絕接收或調整緩沖區大小。