一溪王、前言
在 嵌入式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)核建立起連接后即可進行雙工數(shù)據(jù)發(fā)送
2.2 netlink 優(yōu)點
- 支持全雙工捞魁、異步通信
- 用戶空間使用標準的socket接口即可進行通信
- 支持多播
- 在內(nèi)核端可用于進程上下文與中斷上下文
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ù)奶锹瘛D敲此托枰?消息格式 和 協(xié)議流程宣吱。說得簡單一些,就是 netlink 是一種通信方式瞳别,是內(nèi)核提供的一種功能征候。按照內(nèi)核的只提供機制不提供策略的理念,netlink 本身是不具備 協(xié)議流程 這個概念的祟敛。但我們要使用好 netlink疤坝,往往需要我們?nèi)ブ付〝?shù)據(jù)的傳輸流程,所以就需要有一定的 數(shù)據(jù)格式 和 傳輸流程馆铁。其中 數(shù)據(jù)格式 可以看成就是 消息跑揉,是內(nèi)核已經(jīng)做好的固定格式,而 傳輸流程 則是 協(xié)議流程埠巨。當然了历谍,另一方面則是因為 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)核定義了多個標準的消息類型铜邮。
內(nèi)核已經(jīng)使用 netlink 實現(xiàn)了多種協(xié)議,每個 協(xié)議都有特定的功能寨蹋,所以每個 協(xié)議 都可能定義了額外的消息類型松蒜。長度為 16bit - 消息標志:可以用來更改 消息類型 的行為,某些 協(xié)議 有可能會用到該字段已旧。長度為 16bit
- 序列號:該項是 可選項秸苗,類似 TCP協(xié)議 中的報文號。長度為 32bit
- 端口號:表示 消息 發(fā)往的進程运褪。如果 消息 沒有指定端口號惊楼,那么會被發(fā)送給 同一個協(xié)議中第一個匹配的內(nèi)核端套接字〗斩铮可以理解為網(wǎng)絡(luò)編程中用于 尋址 的數(shù)據(jù)檀咙,端口號 一般是當前進程的 進程號。如果 端口號 為 0 璃诀,則是表明消息需要發(fā)往 內(nèi)核
關(guān)于 消息類型 和 消息標志 的信息可以參考附錄中的《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 的基本要點中弛作,我們知道 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)機制蜘拉。所以一個基本的自定義 netlink協(xié)議 需要分別在 應(yīng)用層 和 內(nèi)核 中有代碼實現(xiàn)
2.5.2 應(yīng)用層接口
前面說了 netlink 可以直接使用 socket套接字 的標準接口直接進行代碼編寫,也就是說 socket 有鹿、 bind 等接口都可以直接使用旭旭。
而按照筆者理解,netlink 的編程與 UDP 編程類似葱跋,但在網(wǎng)絡(luò)編程中我們是使用 IP地址 + 端口號 進行尋址的持寄,而 netlink 則是通過 協(xié)議類型+進程ID 進行尋址的腋颠,其中 協(xié)議類型 會在調(diào)用 socket接口 的時候指定坚弱。
netlink 基本的編程步驟如下:
- 使用 socket 聲明套接字
- 使用 bind 綁定 本地地址 到套接字
- 構(gòu)造 消息
- 發(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)核通信的進程的進程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和recvfrom 在 netlink中 的基本接收單元也就是 消息迈喉。
消息 = 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; //消息標志
__u32 nlmsg_seq; //消息報文的序列號
__u32 nlmsg_pid; //發(fā)送進程的進程ID
};
構(gòu)造消息 免不了需要進行一些 長度 和 地址 的計算孩革,內(nèi)核已經(jīng)提供了一些宏幫助我們進行此類操作。如下所示:
#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é)果是 有效載荷的首地址
sento 和 recvfrom 的原型如下:
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)用層靈活一些备绽。其步驟大致如下:
- 創(chuàng)建socket套接字
- 構(gòu)造消息
- 發(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)體指針雨让,其常用成員是 input 和 groups。 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)建的套接字
- skb:struct sk_buff 結(jié)構(gòu)體,會將需要傳輸?shù)?消息 放在其中薛匪。
- portid:指定發(fā)送目的進程的PID
- nonblock:指定阻塞標志捐川,默認情況下為阻塞,可以使用 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ù)襟诸,當計數(shù)為 0 時則釋放緩存塊瓦堵。
2.5.3.3 消息模塊接口
在實現(xiàn) netlink 是需要對消息進行構(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)的基本步驟:
- 實現(xiàn)數(shù)據(jù)的接收回調(diào)
- 注冊接收回調(diào)并創(chuàng)建socket
- 實現(xiàn)發(fā)送函數(shù)
- 根據(jù)協(xié)議需要編寫流程代碼
2.6 代碼例程
下面我們看看示例代碼滤港,示例代碼中實現(xiàn)了一個簡單的協(xié)議,協(xié)議流程如下:
- 應(yīng)用程序請求驅(qū)動趴拧,請求消息不需要設(shè)置有效載荷
- 驅(qū)動對請求進行應(yīng)答溅漾,成功返回 0,不成功返回 1
- 成功后應(yīng)用程序可以一直接收來自驅(qū)動的外部數(shù)據(jù)
- 成功請求的程序退出后需要向驅(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默認阻塞 */
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ù)消息類型進行相應(yīng)處理 */
switch(nl_msghdr->nlmsg_type)
{
/* 2.1 請求占用 */
case GPIO_NLMSG_REQUEST:
/* 獲取鎖以改變pid占用標志 */
down(&g_gpio_device->gpio_nl_info.flag_lock);
/* 如果當前netlink的pid已經(jīng)被占用 */
if(1 == g_gpio_device->gpio_nl_info.request_flag)
{
/* 如果當前進程再一次請求則提示不需要再次請求 */
if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
printk(KERN_INFO"can not get request again\n");
else/* 如果其他進程請求則提示當前有其他用戶在占用 */
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
{
/* 如果當前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);
/* 只有占用標志為 1 的情況下才會執(zhí)行操作 */
if(1 == g_gpio_device->gpio_nl_info.request_flag)
{
/* 只有占用的進程才能釋放請求 */
if(nl_msghdr->nlmsg_pid == g_gpio_device->gpio_nl_info.user_pid)
{
/* 設(shè)置占用標志和pid以釋放請求 */
g_gpio_device->gpio_nl_info.request_flag = 0;
g_gpio_device->gpio_nl_info.user_pid = 0;
}
else/* 其他進程無權(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 默認情況則打印數(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è)備是否在進行 熱插播妄田,而內(nèi)核或者某些驅(qū)動實現(xiàn)了 熱插拔 上報 uevent事件信息 的機制。這樣我們就可以在應(yīng)用層監(jiān)聽設(shè)備的 熱插拔狀態(tài) 驮捍,并根據(jù)狀態(tài)執(zhí)行不同操作
- 當我們使用 insmod 命令加載內(nèi)核時疟呐,常常需要手動使用 mknod 命令手動添加驅(qū)動節(jié)點。內(nèi)核其實是提供了一種機制东且,該機制可以在加載內(nèi)核時启具,通過上報 uevent事件信息,將 主次設(shè)備號 上報給應(yīng)用空間珊泳,拿到設(shè)備號號后我們就可以自動設(shè)備節(jié)點鲁冯,而這也就是 udev 實現(xiàn)的基本原理拷沸。
2.7.2 如何捕獲uevent事件
實現(xiàn)捕獲 uevent事件信息 比較簡單,代碼編寫同前面所講類似薯演,其應(yīng)用層步驟如下:
- 創(chuàng)建 socket撞芍,其協(xié)議類型為 NETLINK_KOBJECT_UEVENT。
- 使用 bind 綁定 socket跨扮。其中地址結(jié)構(gòu)體 struct sockaddr_nl 的 group成員 要指定為 NETLINK_KOBJECT_UEVENT
- 直接接收來自內(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ù)雜在筆者學習的過程中深有體會,還有許多知識筆者還沒接觸到祭椰。希望通過本文可以讓讀者們對 netlink 的使用有一些基本的了解臭家,能夠幫助各位讀者在工作生活中解決一些東西。
四方淤、附錄參考鏈接
libnl庫應(yīng)用詳解(一):http://www.reibang.com/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)核與用戶層通信之netlink:https://blog.csdn.net/stone8761/article/details/72780863
netlink 學習筆記 3.8.13內(nèi)核:https://www.cnblogs.com/D3Hunter/archive/2013/07/22/3207670.html
linux內(nèi)核編程之netlink:https://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