前言

這幾天在看 ipvs 相關代碼的時候又遇到了 netlink 的事情,所以這兩天花了點時間重新把 netlink 的事情梳理了一下。

什麼是 netlink

linux 內核一直存在的一個嚴重問題就是內核態和用戶態的交互的問題,對於這個問題內核大佬們一直在研究各種方法,想讓內核和用戶態交互能夠安全高效的進行。如系統調用,proc,sysfs等內存文件系統,但是這些方式一般都比較簡單,只能在用戶空間輪詢訪問內核的變化,內核的變化無法主動的推送出來。

而 netlink 的出現比較好的解決了這個問題,而且 netlink 還有以下一些優勢:

  1. 可以直接使用 socket 套接字的 API 進行內核和用戶態的通信,開發使用上相對簡單了很多。

  2. 利用內核協議棧有了緩衝隊列,是一種異步通信機制。

  3. 可以是內核和用戶態的雙向通信,內核可以主動向用戶態進程發送消息。這個是以往通信方式不具備的。

  4. 針對同一個協議類型的所有用戶進程,內核可以廣播消息給所有的進程,也可以指定進程 pid 進行消息發送。

目前 netlink 的這種機制被廣泛使用在各種場景中,在 Linux 內核中使用 netlink 進行應用與內核通信的應用很多; 包括:路由 daemon(NETLINK_ROUTE),用戶態 socket 協議(NETLINK_USERSOCK),防火牆(NETLINK_FIREWALL),netfilter 子系統(NETLINK_NETFILTER),內核事件向用戶態通知(NETLINK_KOBJECT_UEVENT)等。具體支持的類型可以查看這個文件 include/uapi/linux/netlink.h

netlink 內核代碼走讀

netlink 內核相關文件介紹

netlink 的內核代碼在內核代碼的 net/netlink/ 目錄下,我目前看的是  5.7.10 的內核版本,netlink 內核相關的文件不多,還是比較清晰的:

helightxu@  ~/open_code/linux-5.7.10  ls net/netlink
Kconfig      Makefile     af_netlink.c af_netlink.h diag.c       genetlink.c
 helightxu@  ~/open_code/linux-5.7.10 
文件 描述
af_netlink.c 和 af_netlink.h: 是 netlink 的核心文件,這個也是下面詳細走讀的內容。
diag.c 對 netlink sock 進行監控,可以插入到內核或者從內核中卸載
genetlink.c 這個可以看作是 netlink 的升級版本,或者說是一種高層封裝。

注:

genetlink.c 額外說明:netlink 默認支持了 30 多種的場景,但是對於其它場景並沒有具體定義,這個時候這種通用封裝就有了很大的好處,可以在不改動內核的前提下進行應用場景擴展,這部分內容可以看這個 wiki:https://wiki.linuxfoundation.org/networking/generic_netlink_howto

還有一個頭文件是在 include 目錄,如下所示,這個頭文件是一些輔助函數、宏定義和相關數據結構,大家學習的同學一定要看這個文件,它裏面的註釋非常詳細。這些註釋對理解 netlink 的消息結構非常有用,建議可以詳細看看。

helightxu@  ~/open_code/linux-5.7.10  ls include/net/netlink.h

af_netlink.c 代碼走讀

在 af_netlink.c 這個文件的最下面有一行代碼:

core_initcall(netlink_proto_init);

這段代碼的意思是什麼呢?通過看這個代碼最終的實現可以看出,就是告訴編譯器把 netlink_proto_init 這個函數放入到最終編譯出來二進制文件的  .init 段中,內核在啓動的時候會從這個端裏面的函數挨個的執行。這裏也就是說 netlink 是內核默認就直接支持的,是原生內核的一部分(這裏其實想和內核的動態插拔模塊區別)。

netlink_proto_init 函數中最關鍵的一行代碼就是下面最後一行,把 netlink 的協議族註冊到網絡協議棧中去。

static const struct net_proto_family netlink_family_ops = {
    .family = PF_NETLINK,
    .create = netlink_create,
    .owner  = THIS_MODULE,  /* for consistency 8) */
};
...
sock_register(&netlink_family_ops);

PF_NETLINK 是表示 netlink 的協議族,在後面我們在客戶端創建 netlink socket 的時候就要用到這個東東。如下面的代碼,代碼來自我的測試代碼 https://github.com/helight/kernel_modules/tree/master/netlink_test 中的客戶端代碼,可以看出:PF_NETLINK 表示我們所用的就是 netlink 的協議,SOCK_RAW 表示我們使用的是原始協議包,NETLINK_USER 這個我們自己定義的一個協議字段。netlink 我們前面說了有 30 多種應用場景,這些都已經在內核代碼中固定了,所以在客戶端使用的時候會指定這個字段來表示和內核中的那個應用場景的函數模塊進行交互。

//int socket(int domain, int type, int protocol);
    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);

sock_register 這個函數的作用主要就是註冊  PF_NETLINK 這個協議類型到內核中,讓內核認識這個協議,在內核網絡協議中建立 socket 的時候知道該使用那個協議爲它提供操作支持。

註冊了之後內核就支持了 netlink 協議了,接下來就是內核中創建監聽 socket,用戶態創建鏈接 socket 了。

netlink 用戶態和內核交互過程

這裏我簡單畫一個圖來表示一下,socket 通信主要有 2 個操作對象:server 端和 client 端。netlink 的操作原理是這樣的:

對象 所處位置 -
server 端 內核中 -
client 端 用戶態進程 -

netlink 關鍵數據結構和函數

sockaddr_nl 協議套接字

netlink 的地址表示由 sockaddr_nl 負責

struct sockaddr_nl {
    __kernel_sa_family_t    nl_family;    /* AF_NETLINK    */
    unsigned short          nl_pad;        /* zero        */
    __u32                   nl_pid;        /* port ID 這個一般是進程id */
    __u32                   nl_groups;    /* multicast groups mask */
};

nl_family 制定了協議族,netlink 有自己獨立的值:AF_NETLINK,nl_pid 一般取爲進程 pid。nl_groups 用以多播,當不需要多播時,該字段爲 0。

nlmsghdr 消息體

netlink 消息是作爲套接字緩衝區 sk_buff 的數據部分傳遞的,其消息本身又分爲頭部和數據。頭部爲:

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_len 爲消息的長度,包含該頭部在內。nlmsg_pid 爲發送進程的端口 ID,這個用戶可以自定義,一般也是使用進程 pid。

msghdr 用戶態系發送消息體

使用 sendmsg 和 recvmsg 函數進行發送和接收消息,使用的消息體是這個樣子的。

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;    /* Socket name            */
    int          msg_namelen;    /* Length of name        */
    struct iovec *    msg_iov;    /* Data blocks            */
    __kernel_size_t   msg_iovlen;    /* Number of blocks        */
    void     *         msg_control;    /* Per protocol magic (eg BSD file descriptor passing) */
    __kernel_size_t    msg_controllen;    /* Length of cmsg list */
    unsigned int      msg_flags;
};
/*
   msg_name:數據的目的地址,網絡包指向 sockaddr_in, netlink 則指向 sockaddr_nl;
   msg_namelen: msg_name 所代表的地址長度
   msg_iov: 指向的是緩衝區數組
   msg_iovlen: 緩衝區數組長度
   msg_control: 輔助數據,控制信息(發送任何的控制信息)
   msg_controllen: 輔助信息長度
   msg_flags: 消息標識
*/

邏輯結構如下:

socket 也是一種特殊的文件,通過 VFS 的接口同樣可以對其進行使用管理。socket 本身就需要實現文件系統的相應接口,有自己的操作方法集。

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)))

netlink 內核常用函數

netlink_kernel_create

這個內核函數用於創建內核 socket,以提供和用戶態通信

static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
/*
    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);
};

單播函數 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 內核建立 socket 過程

內核的代碼非常簡單,這裏給出了核心代碼,就這麼多,接收函數中直接打印了接收到的消息。

#include <linux/module.h>#include <linux/kernel.h>#include <linux/init.h>#include <net/sock.h>#include <asm/types.h>#include <linux/netlink.h>#include <linux/skbuff.h>
#define NETLINK_XUX           31       /* testing */  
static struct sock *xux_sock = NULL;

// 接收消息的回調函數,接收參數是 sk_buff
static void recv_netlink(struct sk_buff *skb)
{
    struct nlmsghdr *nlh;
    nlh = nlmsg_hdr(skb); // 取得消息體
    printk("receive data from user process: %s", (char *)NLMSG_DATA(nlh)); // 打印接收的數據內容

    ...
}

int __init init_link(void)
{
    struct netlink_kernel_cfg cfg = {
		.input = recv_netlink,
	};
    xux_sock = netlink_kernel_create(&init_net, NETLINK_XUX, &cfg); // 創建內核 socket
    if (!xux_sock){
        printk("cannot initialize netlink socket");
        return -1;
    }
    
    printk("Init OK!\n");
    return 0;
}

netlink 用戶態建立鏈接和收發信息

... // 上面的就省了
#define NETLINK_USER 31  //self defined
#define MAX_PAYLOAD 1024 /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct msghdr msg;
struct iovec iov;
int sock_fd;

int main(int args, char *argv[])
{
    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER); // 建立 socket

    if(sock_fd < 0)
        return -1;

    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); /* 當前進程的 pid */

    if(bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr))){ // 和指定協議進行 socket 綁定
        perror("bind() error\n");
        close(skfd);
        return -1;
    }

    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;       /* For Linux Kernel */
    dest_addr.nl_groups = 0;    /* unicast */

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();  //self pid
    nlh->nlmsg_flags = 0;
    // 拷貝信息到發送緩衝中
    strcpy(NLMSG_DATA(nlh), "Hello this is a msg from userspace");
    // 構造發送消息體
    iov.iov_base = (void *)nlh;         //iov -> nlh
    iov.iov_len = nlh->nlmsg_len;
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;  // iov 中存放 netlink 消息頭和消息數據
    msg.msg_iovlen = 1;

    printf("Sending message to kernel\n");

    int ret = sendmsg(sock_fd, &msg, 0);  // 發送消息到內核
    printf("send ret: %d\n", ret);

    printf("Waiting for message from kernel\n");

    /* 從內核接收消息 */
    recvmsg(sock_fd, &msg, 0);
    printf("Received message payload: %s\n", NLMSG_DATA(nlh));  // 打印接收到的消息

    close(sock_fd);
    return 0;
}

以上代碼在我的個人倉庫中都有,如果有興趣可以 clone 下來自己測試玩耍一遍。代碼倉庫:https://github.com/helight/kernel_modules/tree/master/netlink_test 。

總結

netlink 目前感覺還是一個比較好用的內核和用戶空間的交互方式,但是也是有他的使用場景,適合用戶空間和內核空間主動交互的場景。

但是在單機場景下,大多數的主動權在用戶進程,用戶進程寫數據到內核,用戶進程主動讀取內核數據。這兩種場景覆蓋了內核的絕大多數場景。

在內核要主動的場景下,netlink 就比較適合。我能想到的就是內核數據審計,安全觸發等,這類場景下內核可以實時的告知用戶進程內核發生的情況。

我是在看 ipvs 的代碼時候看到了裏面有 netlink 的使用,發現早期 iptables 就是使用 netlink 來下發配置指令的,內核中 netfilter 和 iptables 中還有這部分的代碼,今天也順便下載大致走讀了一遍,大家可以搜索 NETLINK 這個關鍵字來看。但是 iptables 後來的代碼中沒有使用這樣的方式,而是採用了一個叫做  iptc 的庫,其核心思路還是使用 setsockops 的方式,最終還是  copy_from_user 。不過這種方式對於 iptables 這種配置下發的場景來說還是非常實用的。

最後大家如果對這方面有興趣的可以找我一起再研究研究,我也想繼續在深入看看,但是目前沒有太好的場景。

明天開始走讀 kubernetes 的代碼,大家有興趣的也可以一起來交流。

參考

  1. https://www.cnblogs.com/x_wukong/p/5920437.html

相關文章