linux應(yīng)用程序——netlink的部分使用方法

一、前言

在 嵌入式linux 中,應(yīng)用程序常常需要和內(nèi)核做通信,其中我們熟悉的方法有系統(tǒng)調(diào)用,異步IO等,但這些只能用于單工通信,即應(yīng)用程序主動跟內(nèi)核通信或者內(nèi)核發(fā)送信號給應(yīng)用程序,在某些場合中并不使用。而本文將介紹一種雙工通信方法——netlink,它即可以在內(nèi)核中主動傳輸數(shù)據(jù),有可以在應(yīng)用程序中發(fā)送數(shù)據(jù),如同我們在做網(wǎng)編編程一樣。本文就講述 netlink 在嵌入式中常用的 2 種使用方法:數(shù)據(jù)傳輸、獲取uevent事件信息

二、netlink

2.1 通信方式總結(jié)

應(yīng)用程序和內(nèi)核通信的常用方式如下:

  • 系統(tǒng)調(diào)用:常見的有 write、read、ioctl 等等,它需要應(yīng)用程序主動向內(nèi)核寫入或讀取數(shù)據(jù),是一種同步的單工數(shù)據(jù)傳輸方式
  • /proc文件系統(tǒng):同 系統(tǒng)調(diào)用 類似
  • 異步IO:可以通過編寫驅(qū)動代碼,使得內(nèi)核在某些時刻主動向應(yīng)用程序發(fā)送信號,但無法傳輸大量數(shù)據(jù)。
  • netlink:是一種同步或者異步的數(shù)據(jù)傳輸方式,使用 netlink 在應(yīng)用程序和內(nèi)核建立起連接后即可進(jìn)行雙工數(shù)據(jù)發(fā)送

2.2 netlink 優(yōu)點(diǎn)

  • 支持全雙工、異步通信
  • 用戶空間使用標(biāo)準(zhǔn)的socket接口即可進(jìn)行通信
  • 支持多播
  • 在內(nèi)核端可用于進(jìn)程上下文與中斷上下文

2.3 netlink 常見應(yīng)用

目前 linux內(nèi)核 已經(jīng)使用 netlink 實現(xiàn)了多種功能應(yīng)用,關(guān)于功能使用后面會有部分講解。下面羅列出幾個常用的功能,如下:

  • 獲取或修改路由信息
  • 監(jiān)聽TCP協(xié)議數(shù)據(jù)報文
  • 防火墻
  • netfilter子系統(tǒng)
  • 內(nèi)核事件向用戶態(tài)通知

2.4 netlink 協(xié)議

為什么把 netlink 稱之為協(xié)議呢?
按照筆者的理解,netlink 很想網(wǎng)絡(luò)編程,因為它有一定的格式要求,是通過發(fā)送 消息 來完成數(shù)據(jù)傳輸?shù)?。那么它就需?消息格式協(xié)議流程。說得簡單一些,就是 netlink 是一種通信方式,是內(nèi)核提供的一種功能。按照內(nèi)核的只提供機(jī)制不提供策略的理念,netlink 本身是不具備 協(xié)議流程 這個概念的。但我們要使用好 netlink,往往需要我們?nèi)ブ付〝?shù)據(jù)的傳輸流程,所以就需要有一定的 數(shù)據(jù)格式傳輸流程。其中 數(shù)據(jù)格式 可以看成就是 消息,是內(nèi)核已經(jīng)做好的固定格式,而 傳輸流程 則是 協(xié)議流程。當(dāng)然了,另一方面則是因為 netlink 用就是 socket套接字 那一套編程接口,所以十分像是網(wǎng)絡(luò)協(xié)議。

2.4.1 消息

消息netlink 的主要發(fā)送單元,也就是 netlink 發(fā)送的數(shù)據(jù)只能是以 消息 為單位。 消息 的組成是 netlink消息頭有效載荷(數(shù)據(jù))。從內(nèi)存的角度來講,就是 有效載荷 是直接放在 netlink消息頭 后面的,也就是前面 16個字節(jié)netlink消息頭,后面的全部都是 有效載荷

netlink消息頭 組成如下:

消息組成

  • 總長度:包括 netlink消息頭 在內(nèi)的總字節(jié)數(shù),長度為 32bit
  • 消息類型:表示 消息 的類型,往往用于協(xié)議流程。內(nèi)核定義了多個標(biāo)準(zhǔn)的消息類型。
    內(nèi)核已經(jīng)使用 netlink 實現(xiàn)了多種協(xié)議,每個 協(xié)議都有特定的功能,所以每個 協(xié)議 都可能定義了額外的消息類型。長度為 16bit
  • 消息標(biāo)志:可以用來更改 消息類型 的行為,某些 協(xié)議 有可能會用到該字段。長度為 16bit
  • 序列號:該項是 可選項,類似 TCP協(xié)議 中的報文號。長度為 32bit
  • 端口號:表示 消息 發(fā)往的進(jìn)程。如果 消息 沒有指定端口號,那么會被發(fā)送給 同一個協(xié)議中第一個匹配的內(nèi)核端套接字??梢岳斫鉃榫W(wǎng)絡(luò)編程中用于 尋址 的數(shù)據(jù),端口號 一般是當(dāng)前進(jìn)程的 進(jìn)程號。如果 端口號 為 0 ,則是表明消息需要發(fā)往 內(nèi)核

關(guān)于 消息類型消息標(biāo)志 的信息可以參考附錄中的《libnl庫應(yīng)用詳解(一)》

2.4.2 協(xié)議

這里簡單的說一下內(nèi)核支持的 netlink 協(xié)議,在 include/uapi/linux/netlink.h 中已經(jīng)定義了一些 協(xié)議類型 的宏,每個宏都代表一種協(xié)議,內(nèi)核最多支持 32 種協(xié)議。如下所示:

#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_SMC     22  /* SMC monitoring */

#define NETLINK_INET_DIAG   NETLINK_SOCK_DIAG

#define MAX_LINKS 32    

NETLINK_ROUTE 協(xié)議為例子,它可以獲取和修改設(shè)備的路由信息。下面是它支持的一些 消息類型:

RTM_NEWLINK        創(chuàng)建獲取網(wǎng)絡(luò)設(shè)備的信息
RTM_DELLINK        刪除網(wǎng)絡(luò)設(shè)備的信息
RTM_GETLINK        獲取網(wǎng)絡(luò)設(shè)備的信息
RTM_NEWADDR        創(chuàng)建網(wǎng)絡(luò)設(shè)備的IP信息 
RTM_DELADDR        刪除網(wǎng)絡(luò)設(shè)備的IP信息 
RTM_GETADDR        獲取網(wǎng)絡(luò)設(shè)備的IP信息 
RTM_NEWROUTE       創(chuàng)建網(wǎng)絡(luò)設(shè)備的路由信息
RTM_DELROUTE       刪除網(wǎng)絡(luò)設(shè)備的路由信息
RTM_GETROUTE       獲取網(wǎng)絡(luò)設(shè)備的路由信息 
RTM_NEWNEIGH       創(chuàng)建網(wǎng)絡(luò)設(shè)備的相鄰信息
RTM_DELNEIGH       刪除網(wǎng)絡(luò)設(shè)備的相鄰信息
RTM_GETNEIGH       獲取網(wǎng)絡(luò)設(shè)備的相鄰信息 
RTM_NEWRULE        創(chuàng)建路由規(guī)則信息
RTM_DELRULE        刪除路由規(guī)則信息
RTM_GETRULE        獲取路由規(guī)則信息 
RTM_NEWQDISC       創(chuàng)建隊列的原則
RTM_DELQDISC       刪除隊列的原則
RTM_GETQDISC       獲取隊列的原則 
RTM_NEWTCLASS      創(chuàng)建流量的類別
RTM_DELTCLASS      刪除流量的類別
RTM_GETTCLASS      獲取流量的類別 
RTM_NEWTFILTER     創(chuàng)建流量的過慮
RTM_DELTFILTER     刪除流量的過慮
RTM_GETTFILTER     獲取流量的過慮

除了 消息類型 之外,有效載荷 也是使用一些指定的數(shù)據(jù)結(jié)構(gòu),比如 ifinfomsg 、rtattr。因為筆者對此并不熟悉,所以不做過多描述。只是為了舉例讓讀者門理解 netlink協(xié)議 的概念,有興趣的讀者可以從其他途徑查找資料。

2.5 接口及數(shù)據(jù)結(jié)構(gòu)

在前面講了關(guān)于 netlink 的基本要點(diǎn)中,我們知道 netlink消息頭 是數(shù)據(jù)傳輸?shù)倪^程中需要的基本格式,內(nèi)核也已經(jīng)幫我們實現(xiàn)了一些數(shù)據(jù)結(jié)構(gòu)及接口來幫助我們實現(xiàn)相關(guān)功能。
netlink 的接口分為 應(yīng)用層接口內(nèi)核接口,我們分別需要在 應(yīng)用層實現(xiàn)策略,然后在 內(nèi)核實現(xiàn)機(jī)制。所以一個基本的自定義 netlink協(xié)議 需要分別在 應(yīng)用層內(nèi)核 中有代碼實現(xiàn)

2.5.2 應(yīng)用層接口

前面說了 netlink 可以直接使用 socket套接字 的標(biāo)準(zhǔn)接口直接進(jìn)行代碼編寫,也就是說 socket 、 bind 等接口都可以直接使用。
而按照筆者理解,netlink 的編程與 UDP 編程類似,但在網(wǎng)絡(luò)編程中我們是使用 IP地址 + 端口號 進(jìn)行尋址的,而 netlink 則是通過 協(xié)議類型+進(jìn)程ID 進(jìn)行尋址的,其中 協(xié)議類型 會在調(diào)用 socket接口 的時候指定。
netlink 基本的編程步驟如下:

  1. 使用 socket 聲明套接字
  2. 使用 bind 綁定 本地地址 到套接字
  3. 構(gòu)造 消息
  4. 發(fā)送或接收 消息,而在 netlink 中有 2 套發(fā)送及接收數(shù)據(jù)的接口,分別是 sendto和recvfrom、sendmsg和recvmsg

2.5.2.1 socket

socket 在網(wǎng)絡(luò)編程中也需要用到,用于聲明一個套接字、但在 netlink 中,它的參數(shù)有所不同。socket接口 的原型如下:

int socket(int domain, int type, int protocol);

netlink 中的參數(shù)如下:

  • domain:用于聲明協(xié)議簇,在 netlink 中一般為 AF_NETLINK。
  • type:用于聲明套接字類型,在 netlink 中一般為 SOCK_RAW
  • protocol:用于聲明 協(xié)議類型,在 netlink 中可以是內(nèi)核支持的 協(xié)議,也可以是 自定義協(xié)議

例子如下所示:

    #define NETLINK_TEST 23//自定義協(xié)議
    ......
    int skfd = 0;
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);

2.5.2.2 bind

bind 接口用于綁定套接字和地址,原型如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

netlink 使用的 地址數(shù)據(jù)結(jié)構(gòu) 與網(wǎng)絡(luò)編程不同,如下所示:

struct sockaddr_nl {
     __kernel_sa_family_t    nl_family;  //一般為AF_NETLINK
     unsigned short          nl_pad;     //無需填充
     __u32                   nl_pid;     //與內(nèi)核通信的進(jìn)程的進(jìn)程ID,0 則代表地址為內(nèi)核
     __u32                   nl_groups;  //多播組號,netlink支持多播
};

例子如下:

    struct sockaddr_nl nlsrc_addr = {0};
    /* 設(shè)置本地socket地址 */
    nlsrc_addr.nl_family = AF_NETLINK;
    nlsrc_addr.nl_pid = getpid();
    nlsrc_addr.nl_groups = 0;

    /*綁定套接字*/
    if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
    {
        printf("bind addr error\n");   
        return -1;
    }

2.5.2.1 sendto及recvfrom

上面分別完成了 聲明套接字本地地址綁定套接字 的 2 個前置步驟,完成之后我們就可以 構(gòu)造和發(fā)送消息。netlink 有 2 套接收和發(fā)送消息的接口,本文主要講述 sendto和recvfrom,對于 sendmsg和recvmsg 有興趣的朋友可以參考附錄中的《內(nèi)核與用戶層通信之netlink》

netlink 的基本數(shù)據(jù)單元是 消息,那么 sendto和recvfromnetlink中 的基本接收單元也就是 消息。
消息 = netlink消息頭 + 有效載荷,netlink消息頭 的數(shù)據(jù)結(jié)構(gòu)如下,與 2.4.1節(jié) 中的圖片一一對應(yīng):

struct nlmsghdr {   
    __u32       nlmsg_len;  //包括netlink消息頭在內(nèi),整個消息的程度
    __u16       nlmsg_type; //消息類型
    __u16       nlmsg_flags; //消息標(biāo)志
    __u32       nlmsg_seq;  //消息報文的序列號
    __u32       nlmsg_pid;  //發(fā)送進(jìn)程的進(jìn)程ID
};

構(gòu)造消息 免不了需要進(jìn)行一些 長度地址 的計算,內(nèi)核已經(jīng)提供了一些宏幫助我們進(jìn)行此類操作。如下所示:

#define NLMSG_ALIGNTO   4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字節(jié)對齊的最小數(shù)值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

/* Netlink 頭部長度 */
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))

/* 計算消息數(shù)據(jù)len的真實消息長度(消息體 + netlink消息頭)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)

/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字節(jié)對齊的最小數(shù)值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

/* 宏NLMSG_DATA(nlh)用于取得消息的數(shù)據(jù)部分的首地址,設(shè)置和讀取消息數(shù)據(jù)部分時需要使用該宏 */
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

/* 宏NLMSG_NEXT(nlh,len)用于得到下一個消息的首地址, 同時len 變?yōu)槭S嘞⒌拈L度,一般用于分片消息中 */
#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)))

而我們常用的一般有下面幾個:

  • NLMSG_SPACE:參數(shù)是 有效載荷的長度,其計算結(jié)果是包含 netlink消息頭 在內(nèi)的 消息長度
  • NLMSG_DATA:參數(shù)是 netlink消息頭地址,計算結(jié)果是 有效載荷的首地址

sentorecvfrom 的原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

從原型中我們可有看到,都分別需要傳入 地址 來指定 發(fā)送目的地接收源地址。

實例如下:


#define PAYLOAD_LEN 128
/* 自定義消息類型 */
typedef enum{
    GPIO_NLMSG_REQUEST = 1,
    GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;

/* 使用結(jié)構(gòu)體將netlink消息頭和有效載荷捆綁為一個結(jié)構(gòu)體,編程時直接對結(jié)構(gòu)體操作即可 */
typedef struct _user_msg_t
{
    struct nlmsghdr hdr;//netlink消息頭
    char   paylaod[PAYLOAD_LEN];//有效載荷
}user_msg_t;

int main()
{
    struct sockaddr_nl nlsrc_addr = {0};
    struct sockaddr_nl nldst_addr = {0};
    user_msg_t user_msg_send = {0};
    user_msg_t user_msg_recv = {0};
    ......

    /* 設(shè)置目的socket地址 */
    nldst_addr.nl_family = AF_NETLINK;
    nldst_addr.nl_pid = 0;//0表示內(nèi)核netlink地址
    nldst_addr.nl_groups = 0;

    /* 構(gòu)造發(fā)送消息 */
    user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE會自動加上消息頭部去計算, 請求消息不需要有數(shù)據(jù)
    user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
    user_msg_send.hdr.nlmsg_type  = GPIO_NLMSG_REQUEST;
    user_msg_send.hdr.nlmsg_flags = 0;
    user_msg_send.hdr.nlmsg_seq   = 0;

    /* 發(fā)送消息 */
    send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);  

    /* 等待并接收消息 */
    recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
}

2.5.3 內(nèi)核接口

netlink的內(nèi)核實現(xiàn)步驟與應(yīng)用層相似,但相對來說比應(yīng)用層靈活一些。其步驟大致如下:

  1. 創(chuàng)建socket套接字
  2. 構(gòu)造消息
  3. 發(fā)送消息

netlink 內(nèi)核接口成分比較復(fù)雜,涉及到個不同的頭文件,下面簡單的說明一下各個內(nèi)核接口所在的頭文件。

2.5.3.1 netlink模塊接口

該模塊的接口一般是實現(xiàn) netlink 的功能,比如創(chuàng)建socket套接字釋放socket套接字、單播多播 等,其頭文件是 include/linux/netlink.h。下面羅列幾個常用的接口及數(shù)據(jù)結(jié)構(gòu)

struct netlink_kernel_cfg {
    unsigned int    groups;
    unsigned int    flags;
    void            (*input)(struct sk_buff *skb);
    struct mutex    *cb_mutex;
    int             (*bind)(struct net *net, int group);
    void            (*unbind)(struct net *net, int group);
    bool            (*compare)(struct net *net, struct sock *sk);
};

static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
void netlink_kernel_release(struct sock *sk);
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid, __u32 group, gfp_t allocation);
  • netlink_kernel_create 用于創(chuàng)建套接字,其參數(shù)如下:

    • net: 一般為 &init_net
    • unit:指定協(xié)議類型,與用戶空間的 socket接口 對應(yīng)
    • cfg :該參數(shù)是 struct netlink_kernel_cfg結(jié)構(gòu)體指針,其常用成員是 inputgroups。 input 是接收到消息時的 回調(diào)函數(shù),我們一般在這個 回調(diào)函數(shù) 內(nèi)完成數(shù)據(jù)接收。groups 用于指定 套接字的多播組
  • netlink_kernel_release 用于釋放套接字,參數(shù)如下:

    • sk:是由 netlink_kernel_create 創(chuàng)建的套接字。
  • netlink_unicast 用于單播傳輸數(shù)據(jù),參數(shù)如下:

    • ssk:由 netlink_kernel_create 創(chuàng)建的套接字
    • skbstruct sk_buff 結(jié)構(gòu)體,會將需要傳輸?shù)?消息 放在其中。
    • portid:指定發(fā)送目的進(jìn)程的PID
    • nonblock:指定阻塞標(biāo)志,默認(rèn)情況下為阻塞,可以使用 MSG_DONTWAIT 指定為非阻塞

2.5.3.2 skb模塊接口

struct sk_buff 結(jié)構(gòu)體是內(nèi)核實現(xiàn)的應(yīng)用于網(wǎng)絡(luò)系統(tǒng)的數(shù)據(jù)結(jié)構(gòu)體,而 netlink 的部分接口也需要使用到。該結(jié)構(gòu)體的實現(xiàn)及其接口比較復(fù)雜,其頭文件在 include/linux/skbuff.h。筆者對這一部分不甚了解,所以這里簡單的羅列幾個常用到的接口并簡單講述一下其使用場景,原型如下:

static inline struct sk_buff *skb_get(struct sk_buff *skb)
void kfree_skb(struct sk_buff *skb);
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
  • alloc_skb:用于開辟緩存塊,一般是在 發(fā)送 時使用

    • size:指定緩存塊大小,在 netlink 中可以使用宏 NLMSG_SPACE 來計算其大小
    • priority:一般指定為 GFP_KERNEL
  • skb_get:用于增加變量 skb 的計數(shù),一般是在 接收回調(diào) 中對回調(diào)的參數(shù)使用。

  • kfree_skb:用于減少變量 skb 的計數(shù),當(dāng)計數(shù)為 0 時則釋放緩存塊。

2.5.3.3 消息模塊接口

在實現(xiàn) netlink 是需要對消息進(jìn)行構(gòu)造或者計算等操作,內(nèi)核為我們提供一系列的方法,其頭文件為inlcude/net/netlink.h
nlmsg_put 可以用于直接構(gòu)造 消息,一般是在 發(fā)送 消息時使用,其原型如下:

static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int payload, int flags)
  • skb:指定作為 有效載荷 的 **struct sk_buff ** 結(jié)構(gòu)體,一般是使用 alloc_skb 開辟
  • portid:與 netlink消息頭 中的 nlmsg_pid 對應(yīng)。
  • seq:與 netlink消息頭 中的 nlmsg_seq 對應(yīng)。
  • type:與 netlink消息頭 中的 nlmsg_type 對應(yīng)。
  • payload:與 netlink消息頭 中的 nlmsg_len 對應(yīng)。
  • flags:與 netlink消息頭 中的 nlmsg_flags 對應(yīng)。

2.5.3.4 內(nèi)核netlink步驟

看完以上的接口,我們就可以大致總結(jié)一下內(nèi)核 netlink 實現(xiàn)的基本步驟:

  1. 實現(xiàn)數(shù)據(jù)的接收回調(diào)
  2. 注冊接收回調(diào)并創(chuàng)建socket
  3. 實現(xiàn)發(fā)送函數(shù)
  4. 根據(jù)協(xié)議需要編寫流程代碼

2.6 代碼例程

下面我們看看示例代碼,示例代碼中實現(xiàn)了一個簡單的協(xié)議,協(xié)議流程如下:

  1. 應(yīng)用程序請求驅(qū)動,請求消息不需要設(shè)置有效載荷
  2. 驅(qū)動對請求進(jìn)行應(yīng)答,成功返回 0,不成功返回 1
  3. 成功后應(yīng)用程序可以一直接收來自驅(qū)動的外部數(shù)據(jù)
  4. 成功請求的程序退出后需要向驅(qū)動釋放請求,以便其他程序可以繼續(xù)獲取驅(qū)動的外部數(shù)據(jù)

可以看出,其功能就是:外部程序通過對驅(qū)動寫入數(shù)據(jù),這些數(shù)據(jù)會通過驅(qū)動發(fā)送請求成功的應(yīng)用程序。

PS:驅(qū)動層代碼因為涉及到一些其他代碼,所以筆者對netlink意外的代碼做了刪減,但整體上不影響閱讀

應(yīng)用層代碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <sys/socket.h>  

#define NETLINK_TEST 23//自定義協(xié)議
#define PAYLOAD_LEN 128

/* 自定義消息類型 */
typedef enum{
    GPIO_NLMSG_REQUEST = 1,
    GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;

/* 使用結(jié)構(gòu)體將netlink消息頭和有效載荷捆綁為一個結(jié)構(gòu)體,編程時直接對結(jié)構(gòu)體操作即可 */
typedef struct _user_msg_t
{
    struct nlmsghdr hdr;
    char   paylaod[PAYLOAD_LEN];
}user_msg_t;

int main()
{
    struct nlmsghdr nlmsg_hdr = {0};
    struct sockaddr_nl nlsrc_addr = {0};
    struct sockaddr_nl nldst_addr = {0};
    user_msg_t user_msg_send = {0};
    user_msg_t user_msg_recv = {0};
    int skfd = 0;
    int addr_len = 0;
    int recv_len = 0;
    int send_len = 0;
    char ack = 0;

    addr_len = sizeof(struct sockaddr_nl);
    /* 創(chuàng)建套接字 */
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(skfd < 0)
    {
        printf("can not create a netlink socket\n");
        return 0;
    }

    /* 設(shè)置本地socket地址 */
    nlsrc_addr.nl_family = AF_NETLINK;
    nlsrc_addr.nl_pid = getpid();
    nlsrc_addr.nl_groups = 0;

    /*綁定套接字*/
    if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
    {
        printf("bind addr error\n");   
        return -1;
    }

    /* 設(shè)置目的socket地址 */
    nldst_addr.nl_family = AF_NETLINK;
    nldst_addr.nl_pid = 0;//0表示內(nèi)核netlink地址
    nldst_addr.nl_groups = 0;

    /* 設(shè)置請求消息體, 向驅(qū)動發(fā)送請求,請求消息不需要設(shè)置有效載荷 */
    user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE會自動加上消息頭部去計算, 請求消息不需要有數(shù)據(jù)
    user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
    user_msg_send.hdr.nlmsg_type  = GPIO_NLMSG_REQUEST;
    user_msg_send.hdr.nlmsg_flags = 0;
    user_msg_send.hdr.nlmsg_seq   = 0;

    /* 發(fā)送請求 */
    send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);  

    /* 接收來自驅(qū)動的應(yīng)答 */
    recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);

    /* 獲取應(yīng)答數(shù)據(jù)并根據(jù)應(yīng)答來判斷 */
    ack = user_msg_recv.paylaod[0];
    if(-1 == ack)
    {
        printf("can't get request\n");
        return -1;
    }

    while(1)
    {
        /* 接收內(nèi)核空間返回的數(shù)據(jù), recvfrom默認(rèn)阻塞 */
        recv_len = recvfrom(skfd, &user_msg_recv, sizeof(user_msg_t), 0, (struct sockaddr*)&nldst_addr, &addr_len);
        printf("recv data = %s\n", user_msg_recv.paylaod);
        /* 如果外部數(shù)據(jù)外exit字符串,則退出程序 */
        if(!strncmp(user_msg_recv.paylaod, "exit", 4))
            break;
    }

    /* 設(shè)置反請求消息體, 讓驅(qū)動釋放清奇 */
    user_msg_send.hdr.nlmsg_len = NLMSG_SPACE(0);//NLMSG_SPACE會自動加上消息頭部去計算, 請求消息不需要有數(shù)據(jù)
    user_msg_send.hdr.nlmsg_pid = nlsrc_addr.nl_pid;
    user_msg_send.hdr.nlmsg_type  = GPIO_NLMSG_UNREQUEST;
    user_msg_send.hdr.nlmsg_flags = 0;
    user_msg_send.hdr.nlmsg_seq   = 0;    
    /* 發(fā)送反請求消息體 */
    send_len = sendto(skfd, &user_msg_send, user_msg_send.hdr.nlmsg_len, 0, (struct sockaddr*)&nldst_addr, addr_len);

    return 0;  
}

驅(qū)動代碼 如下:

#include <linux/module.h>   
#include <linux/init.h>  
#include <linux/fs.h>   
#include <linux/device.h>   
#include <linux/slab.h>  
#include <linux/cdev.h>  
#include <linux/err.h>  
#include <asm/uaccess.h>  
#include <linux/io.h>  
#include <linux/of.h>  
#include <linux/of_gpio.h>  
#include <linux/gpio.h>  
#include <linux/platform_device.h>
#include <linux/kern_levels.h>
#include <linux/ioport.h>      
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <net/sock.h>
#include <linux/netlink.h>
#include <linux/semaphore.h>

#define NETLINK_TEST 23
#define PAYLOAD_LEN 128
typedef enum{
    GPIO_NLMSG_REQUEST = 1,
    GPIO_NLMSG_UNREQUEST,
}GPIO_NLMSG_TYPE;

typedef struct _gpio_nl_info_t
{
    pid_t user_pid;
    int request_flag;
    struct semaphore flag_lock;
}gpio_nl_info_t;

struct gpio_device
{
    ......
    struct sock* gpio_sock;
    gpio_nl_info_t gpio_nl_info;
};
struct gpio_device* g_gpio_device  = NULL;
int gpio_netlink_send(struct sock* nl_sock, int dst_pid, char* data, int len)
{
    if(NULL == data)
    {
        printk(KERN_INFO"data is NULL\n");
        return -1;
    }
    if(len > PAYLOAD_LEN)
    {
        printk(KERN_INFO"length(%d) is out of range\n", len);
        return -1;
    }
    struct nlmsghdr *nl_msghdr = NULL;
    struct sk_buff  *skb_send  = NULL;
    int data_len = 0;

    /* 使用宏NLMSG_SPACE計算包括消息頭在內(nèi)的消息長度 */
    data_len = NLMSG_SPACE(PAYLOAD_LEN);

    /* 開辟skb緩存塊 */
    skb_send = alloc_skb(data_len, GFP_KERNEL);
    if(NULL == skb_send)
    {
        printk(KERN_INFO"alloc skb error\n");
        return -1;
    }
    
    /* 使用nlmsg_put構(gòu)造消息結(jié)構(gòu)體 */
    nl_msghdr = nlmsg_put(skb_send, 0, 0, 0, data_len - NLMSG_SPACE(0), 0);

    /* 將要發(fā)送的數(shù)據(jù)賦值到消息上 */
    memcpy(NLMSG_DATA(nl_msghdr), data, len);

    /* 發(fā)送消息 */
    netlink_unicast(nl_sock, skb_send, dst_pid, 0);

    return 0;
}


void gpio_netlink_input(struct sk_buff *__skb)
{
    if(__skb == NULL)
    {
        printk(KERN_INFO"sk_buff is NULL\n");
        return;
    }

    struct sk_buff *skb = NULL;
    struct nlmsghdr *nl_msghdr = NULL;
    char* data = NULL;
    char ack = 0;

    /* 獲取__skb的使用權(quán) */
    skb = skb_get(__skb);

    /* 1. 獲取消息頭 */
    nl_msghdr = nlmsg_hdr(skb);
    if(skb->len < NLMSG_SPACE(0))
    {
        printk(KERN_INFO"message length error, len = %d\n", skb->len);
        goto INPUT_EXIT;
    }

    /* 2. 根據(jù)消息類型進(jìn)行相應(yīng)處理 */
    switch(nl_msghdr->nlmsg_type)
    {
        /* 2.1 請求占用 */
        case GPIO_NLMSG_REQUEST:
            /* 獲取鎖以改變pid占用標(biāo)志 */
            down(&g_gpio_device->gpio_nl_info.flag_lock);
            /* 如果當(dāng)前netlink的pid已經(jīng)被占用 */
            if(1 == g_gpio_device->gpio_nl_info.request_flag)
            {
                /* 如果當(dāng)前進(jìn)程再一次請求則提示不需要再次請求 */
                if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
                    printk(KERN_INFO"can not get request again\n");
                else/* 如果其他進(jìn)程請求則提示當(dāng)前有其他用戶在占用 */
                    printk(KERN_INFO"other user get request\n");
                /* 請求失敗返回 -1 給用戶空間以做其他處理 */
                ack = -1;
                gpio_netlink_send(g_gpio_device->gpio_sock, nl_msghdr->nlmsg_pid, &ack, 1); 
            }
            else
            {
                /* 如果當(dāng)前netlink的pid沒被占用, 則設(shè)置相關(guān)數(shù)據(jù) */
                g_gpio_device->gpio_nl_info.request_flag = 1;
                g_gpio_device->gpio_nl_info.user_pid = nl_msghdr->nlmsg_pid;
                /* 返回 0 給用戶空間表示成功 */
                ack = 0;
                gpio_netlink_send(g_gpio_device->gpio_sock, nl_msghdr->nlmsg_pid, &ack, 1);
            }
            /* 釋放鎖 */
            up(&g_gpio_device->gpio_nl_info.flag_lock);
            break;
        /* 2.1 釋放占用 */
        case GPIO_NLMSG_UNREQUEST:
            down(&g_gpio_device->gpio_nl_info.flag_lock);
            /* 只有占用標(biāo)志為 1 的情況下才會執(zhí)行操作 */
            if(1 == g_gpio_device->gpio_nl_info.request_flag)
            {
                /* 只有占用的進(jìn)程才能釋放請求 */
                if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
                {
                    /* 設(shè)置占用標(biāo)志和pid以釋放請求 */
                    g_gpio_device->gpio_nl_info.request_flag = 0;
                    g_gpio_device->gpio_nl_info.user_pid = 0;  
                }
                else/* 其他進(jìn)程無權(quán)釋放請求 */
                    printk(KERN_INFO"you have no right to release the request\n");
            }
            up(&g_gpio_device->gpio_nl_info.flag_lock);                   
            break;
        /* 2.1 默認(rèn)情況則打印數(shù)據(jù)用于調(diào)試 */
        default:
            data = NLMSG_DATA(nl_msghdr);
            printk(KERN_INFO"netlink get data_len = %d, pid = %d, data = %s\n", (nl_msghdr->nlmsg_len - NLMSG_SPACE(0)), nl_msghdr->nlmsg_pid, data);
            break;
    }
INPUT_EXIT:
    /* 釋放__skb的使用權(quán), 如果此時skb的計數(shù)為 0 則會釋放出內(nèi)存 */
    kfree_skb(skb);
    return;
}

static int gpio_open(struct inode* inode, struct file* filp)
{   
    printk(KERN_INFO"device open, filp = %#x, f_owner.pid = %#p\n", filp, filp->f_owner.pid);
    filp->private_data = (void*)container_of(inode->i_cdev, struct gpio_device, cdev);
    return 0;
}

static ssize_t gpio_write(struct file* filp, const char __user * buf, size_t size, loff_t* ppos)
{
    struct gpio_device* gpio_device = (struct gpio_device*)filp->private_data;
    char gpio_value = 0;
    char* gpio_buf = kzalloc(size, GFP_KERNEL);
    copy_from_user(gpio_buf, buf, size);    

    /* 將數(shù)據(jù)發(fā)往應(yīng)用層 */
    gpio_netlink_send(gpio_device->gpio_sock, gpio_device->gpio_nl_info.user_pid, gpio_buf, size);
    kfree(gpio_buf);

    return size;
}
static struct file_operations gpio_fops = 
{
    .open = gpio_open,
    .write = gpio_write,
};
static int gpio_probe(struct platform_device *pdev)
{
    int ret = 0;

    /* 為設(shè)備數(shù)據(jù)分配空間 */
    struct gpio_device* gpio_device = devm_kzalloc(&pdev->dev, sizeof(struct gpio_device), GFP_KERNEL);

    struct device_node *np = pdev->dev.of_node;
    if(!gpio_device)
    {
        printk(KERN_INFO"can't create gpio_device\n", gpio_device->gpio_num);
        goto ALLOC_FAIL;
    }
    ......

    /* 創(chuàng)建netlink的socket */    
    struct netlink_kernel_cfg nl_cfg = {
        .input = gpio_netlink_input,
    };

    gpio_device->gpio_sock = netlink_kernel_create(&init_net, NETLINK_TEST, &nl_cfg);
    if(NULL == gpio_device->gpio_sock)
    {
        printk(KERN_INFO"create socket error\n");
        goto DEVICE_FAILE;
    }
    /* 初始化信號量 */
    sema_init(&gpio_device->gpio_nl_info.flag_lock, 1);
    g_gpio_device = gpio_device;
    return 0;

    ......
    
}  
static int gpio_remove(struct platform_device *pdev)
{    
    struct gpio_device* gpio_device = (struct gpio_device*)platform_get_drvdata(pdev);
    int n_num = 1;
    
    netlink_kernel_release(gpio_device->gpio_sock);
    ...
    return 0;
}  

static const struct of_device_id of_gpio_match[] = {
    { .compatible = "gpio_node", .data = NULL},
    {},
};
static struct platform_driver gpio_driver = {
    .probe  = gpio_probe,
    .remove = gpio_remove,
    .driver = {
        .name   = "gpio_driver",
        .of_match_table = of_gpio_match,
    },
};

module_platform_driver(gpio_driver);
MODULE_LICENSE("GPL");  

2.7 獲取uevent事件信息

我們前面已經(jīng)講了 netlink 用于通信的使用方法,那么本節(jié)說說如何使用 netlink 捕捉 uevent事件。

2.7.1 捕獲 uevent事件 的作用

  • 在 嵌入式Linux 中,我們有時需要檢測設(shè)備是否在進(jìn)行 熱插播,而內(nèi)核或者某些驅(qū)動實現(xiàn)了 熱插拔 上報 uevent事件信息 的機(jī)制。這樣我們就可以在應(yīng)用層監(jiān)聽設(shè)備的 熱插拔狀態(tài) ,并根據(jù)狀態(tài)執(zhí)行不同操作
  • 當(dāng)我們使用 insmod 命令加載內(nèi)核時,常常需要手動使用 mknod 命令手動添加驅(qū)動節(jié)點(diǎn)。內(nèi)核其實是提供了一種機(jī)制,該機(jī)制可以在加載內(nèi)核時,通過上報 uevent事件信息,將 主次設(shè)備號 上報給應(yīng)用空間,拿到設(shè)備號號后我們就可以自動設(shè)備節(jié)點(diǎn),而這也就是 udev 實現(xiàn)的基本原理。

2.7.2 如何捕獲uevent事件

實現(xiàn)捕獲 uevent事件信息 比較簡單,代碼編寫同前面所講類似,其應(yīng)用層步驟如下:

  1. 創(chuàng)建 socket,其協(xié)議類型為 NETLINK_KOBJECT_UEVENT。
  2. 使用 bind 綁定 socket。其中地址結(jié)構(gòu)體 struct sockaddr_nlgroup成員 要指定為 NETLINK_KOBJECT_UEVENT
  3. 直接接收來自內(nèi)核的 uevent事件信息

除了 應(yīng)用層 之外,內(nèi)核 也需要做一定的工作,就是在設(shè)備加載或者初始化的時候使用接口 device_create,其原型如下:

device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)

第一個參數(shù)指定所要創(chuàng)建的設(shè)備所從屬的類,第二個參數(shù)是這個設(shè)備的父設(shè)備,如果沒有就指定為NULL,第三個參數(shù)是設(shè)備號,第四個參數(shù)是設(shè)備名稱,第五個參數(shù)是從設(shè)備號。
device_create 的參數(shù)如下:

  • class:指定所要創(chuàng)建的設(shè)備所從屬的類
  • parent:指定設(shè)備的父設(shè)備,如果沒有就指定為NULL
  • devt:指定設(shè)備號
  • drvdata:設(shè)備的私有數(shù)據(jù)
  • fmt:指定 設(shè)備名稱 (按照筆者的理解)

2.7.3 代碼示例

看了步驟之后,其實會發(fā)現(xiàn)非常簡單,應(yīng)用層代碼 如下:(PS:別忘了在驅(qū)動中使用device_create)

#define PAYLOAD_LEN 4096
typedef struct _uevent_msg_t
{
    struct nlmsghdr hdr;
    char   paylaod[PAYLOAD_LEN];
}uevent_msg_t;

int main(int argc,char **argv)
{
    uevent_msg_t uevent_msg = {0};
    struct sockaddr_nl nl_sockaddr;
    int sockfd = 0;
    int recv_len = 0;
    int i = 0;
    int addr_len = sizeof(struct sockaddr_nl);

    memset(&nl_sockaddr, 0, sizeof(nl_sockaddr));
    nl_sockaddr.nl_family = AF_NETLINK;
    nl_sockaddr.nl_groups = NETLINK_KOBJECT_UEVENT;//捕獲uevnet時間只能將組設(shè)為NETLINK_KOBJECT_UEVENT
    nl_sockaddr.nl_pid = getpid(); 

    sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);

    if(sockfd == -1)
        printf("socket creating failed:%s\n", strerror(errno));

    if(bind(sockfd, (struct sockaddr *)&nl_sockaddr, sizeof(nl_sockaddr)) == -1)
        printf("bind error:%s\n", strerror(errno));

    while(1)
    {
        memset(&uevent_msg, 0, sizeof(uevent_msg));
        recv_len = recvfrom(sockfd, &uevent_msg, sizeof(uevent_msg_t), 0, (struct sockaddr*)&nl_sockaddr, &addr_len);
        
        if(recv_len < 0)
            printf("receive error\n");
        else if(recv_len < 32 || recv_len > sizeof(uevent_msg.paylaod))
            printf("invalid message");

        for(i = 0; i < recv_len; i++)
        {
            if(uevent_msg.paylaod[i] == '\0')
                uevent_msg.paylaod[i] = '\n';
        }

        printf("received %d bytes\n%s\n", addr_len, uevent_msg.paylaod);
    }
    return 0;
}

驅(qū)動層 代碼做了一些刪減,主要展示初始化函數(shù)中的代碼實現(xiàn),如下:

static int gpio_probe(struct platform_device *pdev)
{
    ......

    /* 創(chuàng)建設(shè)備,發(fā)出uevent事件 ,在/sys/class/目錄下創(chuàng)建設(shè)備類別目錄gpio_class */
    gpio_device->gpio_class = class_create(THIS_MODULE, "gpio_class");
    if(IS_ERR(gpio_device->gpio_class)) 
    {
        printk(KERN_INFO"create a class error\n");
        goto CDEV_FAILE;
    }       

    /*在/dev/目錄和/sys/class/gpio_class目錄下分別創(chuàng)建設(shè)備文件gpio_dev*/
    gpio_device->gpio_dev = device_create(gpio_device->gpio_class, NULL, gpio_device->n_dev, NULL, "gpio_dev");
    if(IS_ERR(gpio_device->gpio_dev)) 
    {
        printk(KERN_INFO"create a device error\n");
        goto CLASS_FAILE;
    }

    ......

}  

那么,以上就是 捕獲uevent事件 的基本原理和簡單實現(xiàn)。

三、結(jié)語

本文主要講述了 netlink 的 2 種使用方法:應(yīng)用與內(nèi)核的雙工通信、捕獲uevent事件。但其實這只是 netlink 的冰山一角,netlink 牽涉到的模塊代碼之多,之復(fù)雜在筆者學(xué)習(xí)的過程中深有體會,還有許多知識筆者還沒接觸到。希望通過本文可以讓讀者們對 netlink 的使用有一些基本的了解,能夠幫助各位讀者在工作生活中解決一些東西。

四、附錄參考鏈接

libnl庫應(yīng)用詳解(一)http://www.itdecent.cn/p/ab2cd37a9b76
netlink編程介紹https://blog.csdn.net/aabb3575007/article/details/17959199
Netlink編程-數(shù)據(jù)結(jié)構(gòu)http://edsionte.com/techblog/archives/4131
內(nèi)核與用戶層通信之netlinkhttps://blog.csdn.net/stone8761/article/details/72780863
netlink 學(xué)習(xí)筆記 3.8.13內(nèi)核https://www.cnblogs.com/D3Hunter/archive/2013/07/22/3207670.html
linux內(nèi)核編程之netlinkhttps://blog.csdn.net/shallnet/article/details/17734643
linux協(xié)議棧skb操作函數(shù)https://www.cnblogs.com/x_wukong/p/5924484.html
linux內(nèi)核skb操作https://blog.csdn.net/weixin_30315435/article/details/98735670
sk_buff 結(jié)構(gòu)體以及完全解釋https://blog.csdn.net/shanshanpt/article/details/21024465
Netlink實現(xiàn)熱拔插監(jiān)控http://blog.chinaunix.net/uid-24943863-id-3223000.html
Netlink的簡介及使用方法https://blog.csdn.net/ganshuyu/article/details/30241313/
內(nèi)核kobject上報uevent過濾規(guī)則https://blog.csdn.net/tankai19880619/article/details/11776589

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容