天天看點

使用 netlink 進行使用者空間和核心空間資料互動--使用者空間使用netlink 的使用者空間使用關鍵點:

​​​​​​​目錄

netlink 的使用者空間使用

1. netlink 套接字使用者空間接口

2. netlink 的使用者空間套接字建立

3. netlink 的套接字綁定

4. netlink 的消息發送

5. netlink 的消息接收

關鍵點:

 Linux 支援很多的進階網絡特性,例如防火牆、隊列品質 QoS 、類别和過濾、通路狀

态、 netlink 套接字等。 netlink 用于在使用者空間和核心空間傳遞資料,它提供了核心/使用者空間的雙向通信方法。此部分介紹 netlink 套接字的使用者空間建立和使用。

netlink 的使用者空間使用

netlink 包含使用者空間的标準套接字接口和用于建構核心子產品的核心 API 。 防火牆使用 netlink 進行使用者空間和核心空間的通信。

1. netlink 套接字使用者空間接口

使用者層的 netlink 程式設計與通用的套接字程式設計一緻,其順序如下所述。

socket():建立 netlink 套接字。

bind():将 netlink 套接字與 netlink 位址類型進行綁定。

sendmsg()/sendto():向核心或者其他程序發送消息。

recvmsg()/recvfrom():從核心或者其他程序接收消息。

close():關閉 netlink 套接字。

其中 socket() 函數用于建立 netlink 類型的套接字。 bind() 函數将 socket() 函數生成的套接字檔案描述符與一個 netlink 類型的位址結構綁定到一起。sendmsg()/sendto()函數由使用者空間向核心空間發送資料,recvmsg()/recvfrom()函數用于使用者空間接收來自核心空間的資料。最後close()關閉 netlink 網絡套接字。

2. netlink 的使用者空間套接字建立

建立一個使用者空間的 netlink 套接字使用套接字 socket() 函數,三個參數的原型是一緻的,但是建立 netlink 套接字需要輸入不同的參數,可以使用如下方式建立一個 netlink 套接字:

#define NETLINK_TEST 27 //這個是自定義的類型,在核心與之通信的子產品中需要用此相同的類型

int sk_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
           

其中 AF_NETLINK 為協定族,套接字的類型為 SOCK_RAW ,協定類型為系統定義的某種類型或者使用者自定義的類型(這裡的 NETLINK_TEST 為自定義類型)

netlink 建立套接字的時候,套接字類型是一種資料包套接字類型的服務,可以為 SOCK _ RAW(IP) 或者 SOCK _ DGRAM(UDP)。

網絡協定的協定可以為如下幾個類型:                                                         

NETLINK_ROUTE :接收路由更新并且可以用于修改IPv4路由表、網絡路由、IP位址、連接配接參數、鄰居設定、隊列丢棄、負載類型和資料包分類等。

NETLNK_FIREWALL :接收Pv4防火牆發送的資料包。

NETLNK_ARPD :用于使用者空間管理 arp 表。

NETLINK_ROUTE6:接收和發送IPv6路由表更新。

NETLINK_TAPBASE .. NETLINK_TAPBASE +15:為一種 ethertap 裝置執行個體, ethertap 是一種僞網絡通道裝置,它允許使用者空間仿真網絡裝置。

NETLINK_SKIP :保留用于 ENskip 。

3. netlink 的套接字綁定

對于 netlink 的每一個協定類型,最多支援32個多點傳播群組,每一個多點傳播的群組占用一個32位數的一位,例如某一個 netlink 協定對應的多點傳播群為 i ,另一個為 j ,則它們組成的數值1<< i | 1 <<j 為多點傳播的組。這種多點傳播的方法在多個相同功能的程序和核心通信的時候十分有用,使用者和核心之間的通信可以在群組内進行,可以減少核心和程序之間的系統調用。

bind() 函數将一個套接字檔案描述符和位址結構綁定在一起。對于 netlink 類型的綁定,其位址結構為如下形式的原型結構:

struct sockaddr_nl
{
    sa_family_t nl_family; // AF_NETLINK 協定族
    unsigned short nl_pad; //空
    __u32 nl_pid;          //程序的 ID 号/
    __u32 nl_groups;       //多點傳播組的掩碼/
}nladdr;
           

在使用bind()函數的時候,結構 sockaddr_nl 的成員 nl_pid 是一個唯一辨別,這裡的pid和程序pid沒有什麼關系,相當于tcp協定的端口号的作用,但這個也不是端口。便于從核心中發資料給指定的使用者态中的NL socket。

是以廣義的講義nl_pid 不是程序ID,是一個可以唯一辨別本機上使用者态建立的socket。

        1、每個程序隻建立一個NL socket,則nl_pid = getpid ()的确可以唯一辨別。

        2、當一個程序中有多個線程使用多個不同的NL socket,這個時候就需要用這個方法填充 nl_pid : nl_pid = pthread_self ()<<16 | getpid(),可以唯一辨別。

        2、當一個線程中有多個NL socket,這時就需要另外的方式辨別唯一性。

當然也可以給每個NL socket給固定的pid值,隻要不重複就行。

如果應用程式想接收某個群組中的多點傳播資料消息,那麼應用程式可以在成員變量 nl_groups 中将感興趣的多點傳播組的掩碼使用 OR 運算加入。如果 nl_groups 的值為0,應用程式僅僅接收來自核心程式的單點傳播消息。

當對 nl_addr 填充完畢後,需要用bind()函數按照如下方式進行綁定,這裡nl_pid填的固定值。

#define USER_PORT_ID 100 //這裡使用固定的ID,核心态可以往這個ID發消息

struct sockaddr_nl _userAddr;
memset(&_userAddr, 0, sizeof(_userAddr));
_userAddr.nl_family = AF_NETLINK; //AF_NETLINK
_userAddr.nl_pid = USER_PORT_ID; 
_userAddr.nl_groups = 0;
bind(sk_fd, (struct sockaddr *)_userAddr, sizeof(_userAddr));
           

4. netlink 的消息發送

可以使用sendmsg()/sendto()函數向核心或者其他程序發送消息。向其他程序發送消息需要填充目的程序的 sockaddr_nl 結構,這種情況下與 UDP 協定的sendmsg()函數使用情況相同。如果向核心發送消息,則指定核心位址的結構 sockaddr_nl 中的成員 nl_pid 和 nl_gourps 均需要設定為0。

struct sockaddr_nl {
    sa_family_t nl_family; /*AF_NETLINK*/
    unsigned short nl_pad; /* 0 */
    pid_t nl_pid; /* 程序pid */
    u_32 nl_groups; /* 多點傳播組掩碼*/
}nl;
           

在 netlink 套接字中有自己的消息頭,在實際的實作過程中是将 netlink 自己的消息頭部包含在通用消息中,在通用消息的基礎上構造 netlink 的自由消息。 netlink 規定每個消息中必須含有 netlink 私有消息,其私有消息的結構原型如下:

struct nlmsghdr
{
    __u32 nlmsg_len;      /*消息長度, 它包括消息頭部*/
    __ul6 nlmsg_type;     /*消息類型, 僅用于應用程式,對于 netlink 核心來說是透明的*/
    __ul6 nlmsg_flags;    /*附加資訊, 給出額外的控制資訊*/
    __u32 nlmsg_seq;      /*序列号*/
    __u32 nlmsg_pid;      /*發送方的程序 ID */
) nlmh;
           

nlmsg _ len 表示消息的長度,它包括消息頭部。 nlmsg _ type 僅用于應用程式,對于 netlink 核心來說是透明的。 nlmsg flags 給出額外的控制資訊。 nlmsg seq 和 nlmsg _ pid 是應用程式用于跟蹤消息使用,對于 netlink 核心是透明的。

netlink 的消息包含 nlmsghdr 和消息的負載。使用者态發送的資料需要在資料前面封裝nlmsghdr頭部。這一步需要使用者自己封裝。

這裡我采用的是sendto()函數發送:

ssize_t sendto(int, const void *, size_t, int, const sockaddr *, socklen_t);

//發送
char *send_paylod = "Hello, I`m player!";

//總長度,頭 + payload
uint32_t send_all_len = NLMSG_SPACE(strlen(send_paylod));

//申請記憶體 頭 + payload
struct nlmsghdr *SendNLHMsg =(struct nlmsghdr *)malloc(send_all_len); //頭 + payload

memset(SendNLHMsg, 0, NLMSG_SPACE(strlen(send_paylod)));

//頭指派
SendNLHMsg->nlmsg_flags = 0;
SendNLHMsg->nlmsg_seq = 0;
SendNLHMsg->nlmsg_pid = USER_PORT_ID; // 發送端的pid, 這個主要用于對端收到資料後,可以向此socket發送資料
SendNLHMsg->nlmsg_len = send_all_len; //總長度
SendNLHMsg->nlmsg_type = 1; //這個用來辨別消息類型

//payload 
char *pMsgPayload = (char *)(NLMSG_DATA(SendNLHMsg));
memcpy(pMsgPayload, send_paylod, strlen(send_paylod));

struct sockaddr_nl _sendAddr; //接收的Addr
memset(&_sendAddr, 0, sizeof(_sendAddr));
_sendAddr.nl_family = AF_NETLINK;
_sendAddr.nl_pid = 0;    // to kernel 
_sendAddr.nl_groups = 0;

int SendRet = sendto(sk_fd, SendNLHMsg, SendNLHMsg->nlmsg_len, 0, 
        (struct sockaddr *)&_sendAddr, sizeof(struct sockaddr_nl));

free(SendNLHMsg);
           

5. netlink 的消息接收

recvmsg()/recvfrom()函數用于接收核心和其他應用程式發送的資料。接收消息的緩沖區要足夠包含 netlink 消息的頭部和消息的負載。接收消息的代碼如下:

這裡使用recvfrom()函數示例:

ssize_t recvfrom(int, void *, size_t, int, sockaddr *, socklen_t *)

//接收payload最大長度
#define RECV_USER_MSG_LEN 1024 

//接收的Msg
struct nlmsghdr *RecvNLHMsg =(struct nlmsghdr *)malloc(NLMSG_SPACE(RECV_USER_MSG_LEN)); //頭 + payload
memset(RecvNLHMsg, 0, NLMSG_SPACE(RECV_USER_MSG_LEN));

struct sockaddr_nl _recvAddr; //用來儲存接收的Addr
socklen_t len = sizeof(struct sockaddr_nl);
recvRet = recvfrom(sk_fd, (void*)RecvNLHMsg, NLMSG_SPACE(RECV_USER_MSG_LEN), MSG_DONTWAIT, 
            (struct sockaddr *)&_recvAddr, &len);
free(RecvNLHMsg);
           

當正确地接收到消息之後,指針 RecvNLHMsg 指向剛剛接收 netlink 消息的頭部。 _recvAddr 中儲存的為接收到消息的目的位址,包含着程序的 pid 和多點傳播群組。

宏 NLMSG_SPACE ( RECV_USER_MSG_LEN )為包含頭的最大長度。

 宏 NLMSG_DATA(RecvNLHMsg) 傳回指向負載部分的指針,在頭檔案<netlink.h>中定義。

關鍵點:

1、為了辨別唯一性,在使用者态調用bind()函數時,sockaddr_nl _userAddr.nl_pid =USER_PORT_ID 要給一個能夠辨別唯一的值。

                2、在發送消息時為了讓核心接收後能夠知道是哪一個使用者态socket發送的,需要在SendNLHMsg->nlmsg_pid = USER_PORT_ID;這個友善核心傳回消息給特性的使用者socket。

                3、NL 消息發送消息需要封裝struct nlmsghdr 頭部,接收到的消息也有頭部。

相較于UDP程式設計,除了多了一個Netlink消息頭的設定。但是我們發現程式中調用了bind函數,這個函數再UDP程式設計中的用戶端不是必須的,因為我們不需要把UDP socket與某個位址關聯,同時再發送UDP資料包時核心會為我們配置設定一個随即的端口。但是對于Netlink必須要有這一步bind(),因為Netlink核心可不會為我們配置設定一個pid。再強調一遍消息頭(nlmsghdr)中的pid是告訴核心接收端要回複的位址,但是這個位址存不存在核心并不關心,這個位址隻有使用者端調用了bind()後才存在。

netlink在核心空間使用:使用 netlink 進行使用者空間和核心空間資料互動--核心空間使用_xuwaiwai的部落格-CSDN部落格

凡是過往,即為序章  

繼續閱讀