Generic Netlink核心實現分析(二):通訊詳解
轉載自:https://blog.csdn.net/luckyapple1028/article/details/51232582#t6
程式碼路徑:https://github.com/luckyapple1028/demo-genetlink
前一篇博文中分析了Generic Netlink的訊息結構及核心初始化流程,本文中通過一個示例程式來了解Generic Netlink在核心和應用層之間的單播通訊流程。
程式主要功能:應用層程式接收使用者的輸入“字串”和“資料”向核心傳送,核心接收後回發應用層,應用層通過終端列印輸出。一:建立核心Demo Genetlink
(一)定義Demo Genetlink
定義兩種型別的Genetlink cmd指令:
enum { DEMO_CMD_UNSPEC = 0, /* Reserved */ DEMO_CMD_ECHO, /* user->kernel request/get-response */ DEMO_CMD_REPLY, /* kernel->user event */ __DEMO_CMD_MAX, }; #define DEMO_CMD_MAX (__DEMO_CMD_MAX - 1)
其中DEMO_CMD_ECHO用於應用層下發資料,DEMO_CMD_REPLY用於核心嚮應用層回發資料;
同時定義兩種型別的attr屬性引數:
enum { DEMO_CMD_ATTR_UNSPEC = 0, DEMO_CMD_ATTR_MESG, /* demo message */ DEMO_CMD_ATTR_DATA, /* demo data */ __DEMO_CMD_ATTR_MAX, }; #define DEMO_CMD_ATTR_MAX (__DEMO_CMD_ATTR_MAX - 1)
其中DEMO_CMD_ATTR_MESG表示字串,DEMO_CMD_ATTR_DATA表示資料。(分別傳送兩種型別,這裡沒有一塊傳送)
定義demo_family:
staticstruct genl_family demo_family = { .id = GENL_ID_GENERATE, .name = DEMO_GENL_NAME, .version = DEMO_GENL_VERSION, .maxattr = DEMO_CMD_ATTR_MAX, };
其中ID號為GENL_ID_GENERATE,表示由核心統一分配;maxattr為DEMO_CMD_ATTR_MAX,是前文中定義的最大attr屬性數,核心將為其分配快取空間。
定義操作函式集operations:demo_ops
static const struct genl_ops demo_ops[] = { { .cmd = DEMO_CMD_ECHO, .doit = demo_echo_cmd, .policy = demo_cmd_policy, .flags = GENL_ADMIN_PERM, }, };
這裡只為DEMO_CMD_ECHO型別的cmd建立訊息處理函式介面(因為DEMO_CMD_REPLY型別的cmd用於核心訊息,應用層不使用),指定doit訊息處理回撥函式為demo_echo_cmd,同時指定有效組策略為demo_cmd_policy。
static const struct nla_policy demo_cmd_policy[DEMO_CMD_ATTR_MAX+1] = { [DEMO_CMD_ATTR_MESG] = { .type = NLA_STRING }, [DEMO_CMD_ATTR_DATA] = { .type = NLA_S32 }, };
這裡限定DEMO_CMD_ATTR_MESG的屬性型別為NLA_STRING(字串型別),限定DEMO_CMD_ATTR_DATA的屬性型別為NLA_S32(有符號32位數)。
(二)核心註冊Demo Genetlink(核心態實現)
module_init(demo_genetlink_init); module_exit(demo_genetlink_exit); MODULE_LICENSE("GPL");
模組初始化demo_genetlink_init函式中實現核心註冊:
static int __init demo_genetlink_init(void) { int ret; pr_info("demo generic netlink module %d init...\n", DEMO_GENL_VERSION); ret = genl_register_family_with_ops(&demo_family, demo_ops); //前一篇部落格詳解1中詳細介紹了!! if (ret != 0) { pr_info("failed to init demo generic netlink example module\n"); return ret; } pr_info("demo generic netlink module init success\n"); return 0; }
在模組的初始化函式中,呼叫genl_register_family_with_ops()同時註冊demo_family及demo_ops,該函式同前面建立CTRL型別的family簇類似,最終都是呼叫_genl_register_family_with_ops_grps函式完成建立。這個函式已經大致分析過了,此處的註冊流程基本一致, 主要區別在於最後的send all events,它向所有的應用層加入CTRL控制器簇組播組的Generic Netlink套接字多播發送CTRL_CMD_NEWFAMILY訊息,通知應用層有新的family註冊了,這樣應用層就可以捕獲這一訊息。
詳細分析一下:
static int genl_ctrl_event(int event, struct genl_family *family, const struct genl_multicast_group *grp, int grp_id) { struct sk_buff *msg; /* genl is still initialising */ if (!init_net.genl_sock) //已經初始化過了,所以不會進入return return 0; switch (event) { case CTRL_CMD_NEWFAMILY: case CTRL_CMD_DELFAMILY: WARN_ON(grp); msg = ctrl_build_family_msg(family, 0, 0, event); break; case CTRL_CMD_NEWMCAST_GRP: case CTRL_CMD_DELMCAST_GRP: BUG_ON(!grp); msg = ctrl_build_mcgrp_msg(family, grp, grp_id, 0, 0, event); break; default: return -EINVAL; }
函式首先判斷是否已經註冊了控制器CTRL簇,這裡顯然已經註冊過了,然後主要的工作在ctrl_build_family_msg()中:
static struct sk_buff *ctrl_build_family_msg(struct genl_family *family, u32 portid, int seq, u8 cmd) { struct sk_buff *skb; int err; skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL); if (skb == NULL) return ERR_PTR(-ENOBUFS); err = ctrl_fill_info(family, portid, seq, 0, skb, cmd); if (err < 0) { nlmsg_free(skb); return ERR_PTR(err); } return skb; }
首先呼叫nlmsg_new()函式建立netlink型別的skb,第一個入參是訊息的長度,第二個引數為記憶體空間分配型別,這裡分配的資料空間(包括netlink訊息頭)一共為一個page。進入nlmsg_new內部:
static inline struct sk_buff *nlmsg_new(size_t payload, gfp_t flags) { return alloc_skb(nlmsg_total_size(payload), flags); } static inline int nlmsg_total_size(int payload) { return NLMSG_ALIGN(nlmsg_msg_size(payload)); } static inline int nlmsg_msg_size(int payload) { return NLMSG_HDRLEN + payload; }
可以看到總共預留的空間為NLMSG_ALIGN(NLMSG_HDRLEN+NLMSG_DEFAULT_SIZE),這裡實際可能用不了這麼多的空間,接下來呼叫ctrl_fill_info()填充訊息內容:
static int ctrl_fill_info(struct genl_family *family, u32 portid, u32 seq, u32 flags, struct sk_buff *skb, u8 cmd) { void *hdr; hdr = genlmsg_put(skb, portid, seq, &genl_ctrl, flags, cmd); if (hdr == NULL) return -1;
這個函式比較長,這裡使用插圖的形式來觀察訊息的封裝流程(圖中未顯示空白Pad區):
首先確定genlmsg_put函式中各個入參的內容:family為新註冊的demo_family;portid和seq為0,表示訊息的傳送端為核心,傳送訊息序號為0;最後的cmd為CTRL_CMD_NEWFAMILY。
函式首先呼叫genlmsg_put()函式初始化netlink訊息頭和genetlink訊息頭;
void *genlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, struct genl_family *family, int flags, u8 cmd) { struct nlmsghdr *nlh; struct genlmsghdr *hdr; nlh = nlmsg_put(skb, portid, seq, family->id, GENL_HDRLEN + family->hdrsize, flags); if (nlh == NULL) return NULL;其中nlmsg_put()函式向skb緩衝區中獲取訊息頭空間並且初始化netlink訊息頭,入參中的第5個引數為genetlink訊息頭和使用者私有訊息頭(這裡並未使用)的總空間,實際呼叫的函式為__nlmsg_put():
struct nlmsghdr * __nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int len, int flags) { struct nlmsghdr *nlh; int size = nlmsg_msg_size(len); nlh = (struct nlmsghdr *)skb_put(skb, NLMSG_ALIGN(size)); nlh->nlmsg_type = type; nlh->nlmsg_len = size; nlh->nlmsg_flags = flags; nlh->nlmsg_pid = portid; nlh->nlmsg_seq = seq; if (!__builtin_constant_p(size) || NLMSG_ALIGN(size) - size != 0) memset(nlmsg_data(nlh) + len, 0, NLMSG_ALIGN(size) - size); return nlh; }
首先這裡的分配的空間大小為size = 傳入的len長度 + netlink訊息頭的長度,然後初始化netlink訊息頭的各個欄位:
nlh->nlmsg_type :核心genl_ctrl family簇的ID 號GENL_ID_CTRL;
nlh->nlmsg_len :訊息長度,即genetlink頭+使用者私有頭+netlink頭的長度總和;
nlh->nlmsg_flags:0;
nlh->nlmsg_pid:傳送端的ID號為0,表示又核心傳送;
nlh->nlmsg_seq:0;
初始化完成後將記憶體對齊用的空白區刷為0;然後回到genlmsg_put()函式中繼續分析:
hdr = nlmsg_data(nlh); hdr->cmd = cmd; hdr->version = family->version; hdr->reserved = 0; return (char *) hdr + GENL_HDRLEN; }
這裡通過巨集nlmsg_data獲取genetlink訊息頭的地址,然後開始填充該訊息頭的各個欄位:
hdr->cmd:訊息的cmd命令,CTRL_CMD_NEWFAMILY;
hdr->version:genl_ctrl family簇的version;
hdr->reserved:0;
填充完畢後返回訊息使用者私有頭(若有)或實際載荷的首地址,此時的訊息skb中的訊息填充如圖1-a所示。
然後再回到ctrl_fill_info()函式中,接下來就要開始填充實際的資料了(如下程式碼,會新增多個attr,在nla_put中每次新增一個):
if (nla_put_string(skb, CTRL_ATTR_FAMILY_NAME, family->name) || nla_put_u16(skb, CTRL_ATTR_FAMILY_ID, family->id) || nla_put_u32(skb, CTRL_ATTR_VERSION, family->version) || nla_put_u32(skb, CTRL_ATTR_HDRSIZE, family->hdrsize) || nla_put_u32(skb, CTRL_ATTR_MAXATTR, family->maxattr)) goto nla_put_failure;
這裡將新註冊的family結構中的幾個欄位都填充到了訊息中,包括name、id號、版本號、私有頭長度以及maxattr(注意屬性需要一一對應),呼叫的函式nla_put_string、nla_put_u16和nla_put_u32都是nla_put()的封裝,而nla_put實際呼叫的是__nla_put():
int nla_put(struct sk_buff *skb, int attrtype, int attrlen, const void *data) { if (unlikely(skb_tailroom(skb) < nla_total_size(attrlen))) return -EMSGSIZE; __nla_put(skb, attrtype, attrlen, data); return 0; } void __nla_put(struct sk_buff *skb, int attrtype, int attrlen, const void *data) { struct nlattr *nla; nla = __nla_reserve(skb, attrtype, attrlen); memcpy(nla_data(nla), data, attrlen); }
__nla_put()的作用是向skb中新增一個netlink attr屬性,入參分別為skb地址、要新增的attr屬性型別、屬性長度和屬性實際資料。
首先呼叫了__nla_reserve在skb中預留出attr屬性的記憶體空間:
struct nlattr *__nla_reserve(struct sk_buff *skb, int attrtype, int attrlen) { struct nlattr *nla; nla = (struct nlattr *) skb_put(skb, nla_total_size(attrlen)); nla->nla_type = attrtype; nla->nla_len = nla_attr_size(attrlen); memset((unsigned char *) nla + nla->nla_len, 0, nla_padlen(attrlen)); return nla; }
這裡首先預留空間長度為nla_total_size(attrlen),即attrlen+NLA_HDRLEN(屬性頭長度)+對齊用記憶體空白;
然後初始化屬性頭的兩個欄位:
nla->nla_type:attr屬性,即前文中的CTRL_ATTR_FAMILY_NAME等;
nla->nla_len:attr屬性長度(attrlen+NLA_HDRLEN);
然後再將attr屬性中的實際資料拷貝到預留測空間中,如此一個attr屬性就新增完成了,此時的訊息skb中的訊息填充如圖1-b所示。
再回到ctrl_fill_info()函式中:
if (family->n_ops) { struct nlattr *nla_ops; int i; nla_ops = nla_nest_start(skb, CTRL_ATTR_OPS); if (nla_ops == NULL) goto nla_put_failure; for (i = 0; i < family->n_ops; i++) { struct nlattr *nest; const struct genl_ops *ops = &family->ops[i]; u32 op_flags = ops->flags; if (ops->dumpit) op_flags |= GENL_CMD_CAP_DUMP; if (ops->doit) op_flags |= GENL_CMD_CAP_DO; if (ops->policy) op_flags |= GENL_CMD_CAP_HASPOL; nest = nla_nest_start(skb, i + 1); if (nest == NULL) goto nla_put_failure; if (nla_put_u32(skb, CTRL_ATTR_OP_ID, ops->cmd) || nla_put_u32(skb, CTRL_ATTR_OP_FLAGS, op_flags)) goto nla_put_failure; nla_nest_end(skb, nest); } nla_nest_end(skb, nla_ops); }
然後如果新註冊的family簇也同時註冊了操作介面operations,這裡會追加上對應的attr屬性引數;
但同前面不同的是,這裡追加的attr引數是“打包”在一起的,使用的屬性為CTRL_ATTR_OPS。
由於netlink的attr屬性是支援多級巢狀的,所以這裡的“打包”指的就是新建一級巢狀,首先使用nla_nest_start()函式來建立新的一級巢狀:
static inline struct nlattr *nla_nest_start(struct sk_buff *skb, int attrtype) { struct nlattr *start = (struct nlattr *)skb_tail_pointer(skb); if (nla_put(skb, attrtype, 0, NULL) < 0) return NULL; return start; }
可以看到這裡呼叫的依然是nla_put()函式,不過這裡的入參中指定的attr長度為0,然後資料為NULL,那這裡其實就是向skb中添加了一段attr屬性頭,然後指定它的屬性nla_type為CTRL_ATTR_OPS,屬性nla_len為0,注意函式返回的是新增巢狀attr頭之前的訊息有效資料末尾地址。
然後回到ctrl_fill_info()函式中繼續往下是一個for迴圈,在每個迴圈中向剛才新建立的一級巢狀attr屬性中新增屬性。它首先會根據operations中實現的回撥函式封裝flag,然後依舊是呼叫nla_nest_start()函式再次建立新的一級巢狀,不過這次的attrtype為函式的序列,隨後在訊息中追加上回調函式處理的cmd以及flag,最後呼叫nla_nest_end()函式結束這一層attr巢狀:
static inline int nla_nest_end(struct sk_buff *skb, struct nlattr *start) { start->nla_len = skb_tail_pointer(skb) - (unsigned char *)start; return skb->len; }
這個函式其實只做了一件事,那就是更新這個巢狀的attr屬性頭的nla_len欄位為本巢狀屬性的實際長度,實現的方式為當前的訊息末尾地址減去建立該級巢狀之前的訊息末尾地址(這就是nla_nest_start()函式要返回start地址的原因了)。回到ctrl_fill_info()函式中,在for迴圈結束以後,依舊呼叫nla_nest_end來結束CTRL_ATTR_OPS的那一層attr巢狀,此時的訊息skb中的訊息填充如圖1-c所示。
ctrl_fill_info()函式接下來會再判斷family->n_mcgrps欄位,若存在組播組,會同operations一樣增加一級和operations平級的attr巢狀然後新增CTRL_ATTR_MCAST_GROUPS屬性,這裡就不詳細分析了。
在函式的最後呼叫nla_nest_end()完成本次訊息封裝:
static inline void genlmsg_end(struct sk_buff *skb, void *hdr) { nlmsg_end(skb, hdr - GENL_HDRLEN - NLMSG_HDRLEN); }
該函式間接呼叫nlmsg_end()函式,注意第二個入參為訊息attr載荷的首地址減去2個頭的長度,即netlink訊息頭的首地址。
static inline void nlmsg_end(struct sk_buff *skb, struct nlmsghdr *nlh) { nlh->nlmsg_len = skb_tail_pointer(skb) - (unsigned char *)nlh; }
這裡填充nlh->nlmsg_len為整個訊息的長度(包括attr載荷部分和所有的訊息頭部分)。到這裡,向CTRL控制器簇傳送的訊息就已經封裝完成了,再回到最上層的genl_ctrl_event()中:
if (IS_ERR(msg)) return PTR_ERR(msg); if (!family->netnsok) { genlmsg_multicast_netns(&genl_ctrl, &init_net, msg, 0, 0, GFP_KERNEL); } else { rcu_read_lock(); genlmsg_multicast_allns(&genl_ctrl, msg, 0, 0, GFP_ATOMIC); rcu_read_unlock(); }
這裡根據是否支援net名稱空間來選擇傳送的流程,genlmsg_multicast_allns函式從命名中就可以看出會像所有名稱空間的控制器簇傳送訊息,而genlmsg_multicast_netns則指定了向init_net傳送,不論哪一種情況,最後都是呼叫nlmsg_multicast()函式。不過這裡有一點需要注意的就是這裡的第三個入參portid為0,這是為了防止向傳送端傳送報文,這也就表明核心控制器簇套接字是不會接受該廣播報文的(核心也不應該接收,否則會panic,可參見netlink_data_ready()函式的實現)。
至此Demo Genetlink的核心建立流程就全部結束了,此時應用層可以通過Ctrl簇獲取它的ID號並向他傳送訊息了。下面來分析應用層是如何初始化genetlink套接字的。
二:應用層初始化Genetlink套接字
int main(int argc, char* argv[]) { ...... /* 初始化socket */ nl_fd = demo_create_nl_socket(NETLINK_GENERIC); if (nl_fd < 0) { fprintf(stderr, "failed to create netlink socket\n"); return 0; } ...... }
static int demo_create_nl_socket(int protocol) { int fd; struct sockaddr_nl local; /* 建立socket */ fd = socket(AF_NETLINK, SOCK_RAW, protocol); if (fd < 0) return -1; memset(&local, 0, sizeof(local)); local.nl_family = AF_NETLINK; local.nl_pid = getpid(); /* 使用本程序的pid進行繫結 */ if (bind(fd, (struct sockaddr *) &local, sizeof(local)) < 0) goto error; return fd; error: close(fd); return -1; }
應用層依然通過socket系統呼叫建立AF_NETLINK地址簇的SOCK_RAW套接字,指定協議型別為NETLINK_GENERIC,建立的流程同前博文 《Netlink 核心實現分析(一):建立》中分析的NETLINK_ROUTE類似,這裡不再贅述。
接下來初始化sockaddr_nl地址結構並進行繫結操作,為了簡單起見,這裡使用程序ID進行繫結(該值全域性唯一),在實際的程式中可自行安排。繫結的流程前博文也已經分析過了,會呼叫到netlink回撥函式netlink_bind(),該函式會將繫結的ID號新增到全域性nl_table中。
這裡有一點需要說明的就是在前一篇博文中已經看到核心genetlink套接字已經指定了bind回撥函式為genl_bind,這裡如果指定了多播組地址nl_groups,會呼叫到該回調函式進行多播組的繫結操作。
簡單看一下:
if (nlk->netlink_bind && groups) { int group; for (group = 0; group < nlk->ngroups; group++) { if (!test_bit(group, &groups)) continue; err = nlk->netlink_bind(net, group + 1); if (!err) continue; netlink_undo_bind(group, groups, sk); return err; } }
這裡在genetlink支援的最大組播數中進行輪詢,檢測使用者需要繫結的多播組並將其轉換為位序號,然後呼叫netlink_bind回撥函式,這裡該函式就是genl_bind():
static int genl_bind(struct net *net, int group) { int i, err = -ENOENT; down_read(&cb_lock); for (i = 0; i < GENL_FAM_TAB_SIZE; i++) { struct genl_family *f; list_for_each_entry(f, genl_family_chain(i), family_list) { if (group >= f->mcgrp_offset && group < f->mcgrp_offset + f->n_mcgrps) { int fam_grp = group - f->mcgrp_offset; if (!f->netnsok && net != &init_net) err = -ENOENT; else if (f->mcast_bind) err = f->mcast_bind(net, fam_grp); else err = 0; break; } } } up_read(&cb_lock); return err; }
由於不同family型別的genetlink都共用同一個組播地址空間,所以這裡根據使用者輸入的組播號來查詢對應的family,然後會呼叫該family對應的mcast_bind()回撥函式,它需要根據family的需求自行實現,可用於做進一步的特殊需求處理,不實現亦可(目前核心中註冊的genl_family均未使用到該介面)。
至此,應用層genetlink套接字初始化完成,下面來分析它是如何傳送訊息到前文中註冊的核心demo genelink套接字的。
三:使用者空間與核心空間通訊
使用者空間想要傳送訊息到核心的demo genelink套接字,它首先得知道核心分配的demo family的family id號,因為genelink子系統是根據該id號來區分不同family簇的genelink套接字和分發訊息的。此時前文中的ctrl就用於該目的,它可以將family name轉換為對應的family id,使用者空間也通過family name向ctrl簇查詢對應的family id。在應用層序獲取了family id後它就可以像核心傳送訊息,該訊息分別包含了字串和資料,同時核心也在接受後進行回發操作。
另外,在一般的程式中,如果應用層無需向核心傳送訊息,僅僅需要接收核心傳送的訊息時,它並不需要通過Ctrl簇獲取family id了,僅需要接收核心的genetlink訊息並做好cmd和attr型別判斷並做出相應的處理即可。
(一)使用者查詢Demo Family ID
int main(int argc, char* argv[]) { ...... /* 獲取family id */ nl_family_id = demo_get_family_id(nl_fd); if (!nl_family_id) { fprintf(stderr, "Error getting family id, errno %d\n", errno); goto out; } PRINTF("family id %d\n", nl_family_id); ...... }
static int demo_get_family_id(int sd) { struct msgtemplate ans; char name[100]; int id = 0, ret; struct nlattr *na; int rep_len; /* 根據gen family name查詢family id */ strcpy(name, DEMO_GENL_NAME); ret = demo_send_cmd(sd, GENL_ID_CTRL, getpid(), CTRL_CMD_GETFAMILY, CTRL_ATTR_FAMILY_NAME, (void *)name, strlen(DEMO_GENL_NAME)+1); if (ret < 0) return 0; /* 接收核心訊息 */ rep_len = recv(sd, &ans, sizeof(ans), 0); if (ans.n.nlmsg_type == NLMSG_ERROR || (rep_len < 0) || !NLMSG_OK((&ans.n), rep_len)) return 0; /* 解析family id */ na = (struct nlattr *) GENLMSG_DATA(&ans); na = (struct nlattr *) ((char *) na + NLA_ALIGN(na->nla_len)); if (na->nla_type == CTRL_ATTR_FAMILY_ID) { id = *(__u16 *) NLA_DATA(na); } return id; }
該demo_get_family_id()函式比較簡單,僅僅是封裝查詢訊息並向核心的ctrl簇傳送,然後接收核心的回髮結果然後解析出其中的family id,具體的訊息傳送函式由demo_send_cmd()封裝函式來完成,其中入參分別是socket fd、ctrl family id、訊息傳送端netlink繫結ID號、訊息cmd型別、訊息attr屬性、訊息正文內容、訊息正文長度。
static int demo_send_cmd(int sd, __u16 nlmsg_type, __u32 nlmsg_pid, __u8 genl_cmd, __u16 nla_type, void *nla_data, int nla_len) { struct nlattr *na; struct sockaddr_nl nladdr; int r, buflen; char *buf; struct msgtemplate msg; /* 填充msg (本函式傳送的msg只填充一個attr) */ msg.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN); msg.n.nlmsg_type = nlmsg_type; msg.n.nlmsg_flags = NLM_F_REQUEST; msg.n.nlmsg_seq = 0; msg.n.nlmsg_pid = nlmsg_pid; msg.g.cmd = genl_cmd; msg.g.version = DEMO_GENL_VERSION; na = (struct nlattr *) GENLMSG_DATA(&msg); na->nla_type = nla_type; na->nla_len = nla_len + 1 + NLA_HDRLEN; memcpy(NLA_DATA(na), nla_data, nla_len); msg.n.nlmsg_len += NLMSG_ALIGN(na->nla_len); buf = (char *) &msg; buflen = msg.n.nlmsg_len; memset(&nladdr, 0, sizeof(nladdr)); nladdr.nl_family = AF_NETLINK; /* 迴圈傳送直到傳送完成 */ while ((r = sendto(sd, buf, buflen, 0, (struct sockaddr *) &nladdr, sizeof(nladdr))) < buflen) { if (r > 0) { buf += r; buflen -= r; } else if (errno != EAGAIN) return -1; } return 0; }
訊息的封裝過程同核心態訊息封裝過程類似,需嚴格按照genelink訊息格式進行封裝。
首先填充netlink訊息頭,其中nlmsg_type欄位不使用netlink定義的標準type,填充為目標family的ID號,其他欄位同其他型別的netlink類似;然後填充genetlink訊息頭,這裡設定訊息cmd欄位為CTRL_CMD_GETFAMILY,version欄位為DEMO_GENL_VERSION(同核心保持一致);最後填充一個attr屬性,其中屬性頭的nla_type設定為函式傳入的屬性type,現該值為CTRL_ATTR_FAMILY_NAME,然後將傳入的family name拷貝到屬性attr的payload載荷中,最後更新各個訊息頭中的長度欄位。
訊息分裝完成後呼叫sendto系統呼叫啟動傳送流程,指定目的地址的地址簇為AF_NETLINK,ID號為0(表示核心)。
sendto函式同前博文 《Netlink 核心實現分析(二):通訊》中分析的sendmsg()系統呼叫類似(sendto的msg訊息封裝過程由核心完成),最後都是呼叫到sock_sendmsg()函式,具體的中間傳送流程前博文中已詳細描述,這裡不再贅述,直接進入到傳送的最後階段,來看Ctrl簇是如何處理接收到的查詢訊息的。在netlink函式呼叫流程的最後會呼叫具體協議型別的netlink_rcv()回撥函式,其中genetlink的回撥函式在前文中已經看到為genl_rcv():
static void genl_rcv(struct sk_buff *skb) { down_read(&cb_lock); netlink_rcv_skb(skb, &genl_rcv_msg); up_read(&cb_lock); }
這裡netlink_rcv_skb函式的兩個入參其中第一個為訊息skb,第二個為genl_rcv_msg回撥函式;netlink_rcv_skb()函式會對訊息進行一些通用性的處理,將使用者訊息封裝成genl_info結構,最後會把訊息控制權交給genl_rcv_msg()回撥函式:
int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *, struct nlmsghdr *)) { struct nlmsghdr *nlh; int err; while (skb->len >= nlmsg_total_size(0)) { int msglen; nlh = nlmsg_hdr(skb); err = 0; if (nlh->nlmsg_len < NLMSG_HDRLEN || skb->len < nlh->nlmsg_len) return 0; /* Only requests are handled by the kernel */ if (!(nlh->nlmsg_flags & NLM_F_REQUEST)) goto ack; /* Skip control messages */ if (nlh->nlmsg_type < NLMSG_MIN_TYPE) goto ack; err = cb(skb, nlh); if (err == -EINTR) goto skip; ack: if (nlh->nlmsg_flags & NLM_F_ACK || err) netlink_ack(skb, nlh, err); skip: msglen = NLMSG_ALIGN(nlh->nlmsg_len); if (msglen > skb->len) msglen = skb->len; skb_pull(skb, msglen); } return 0; }
首先判斷訊息的長度是否不小於netlink訊息頭的長度(現在的上下文中顯然成立),然後進入while迴圈開始處理存放在skb中的netlink訊息(可能有多個)。迴圈處理中會首先進行一些基本的資料長度判斷,然後根據nlmsg_flags和nlmsg_type欄位判斷是否跳過訊息處理流程、以及是否回發ACK相應。目前由於設定的nlmsg_flags為NLM_F_REQUEST、nlmsg_type為GENL_ID_CTRL(即NLMSG_MIN_TYPE),因此呼叫genl_rcv_msg()回撥函式開始訊息處理流程:
static int genl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh) { struct genl_family *family; int err; family = genl_family_find_byid(nlh->nlmsg_type); if (family == NULL) return -ENOENT; if (!family->parallel_ops) genl_lock(); err = genl_family_rcv_msg(family, skb, nlh); if (!family->parallel_ops) genl_unlock(); return err; }
該函式首先通過nlmsg_type欄位(即family id號)在散列表中查詢到對應的註冊family,然後如果訊息處理不可重入,則這裡會上鎖,接下來呼叫genl_family_rcv_msg()函式:
static int genl_family_rcv_msg(struct genl_family *family, struct sk_buff *skb, struct nlmsghdr *nlh) { const struct genl_ops *ops; struct net *net = sock_net(skb->sk); struct genl_info info; struct genlmsghdr *hdr = nlmsg_data(nlh); struct nlattr **attrbuf; int hdrlen, err; /* this family doesn't exist in this netns */ if (!family->netnsok && !net_eq(net, &init_net)) return -ENOENT; hdrlen = GENL_HDRLEN + family->hdrsize; if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen)) return -EINVAL;
函式首先判斷網路名稱空間,若不支援則當前訊息的網路空間必須為init_net,然後判斷訊息的長度。
ops = genl_get_cmd(hdr->cmd, family); if (ops == NULL) return -EOPNOTSUPP;
static const struct genl_ops *genl_get_cmd(u8 cmd, struct genl_family *family) { int i; for (i = 0; i < family->n_ops; i++) if (family->ops[i].cmd == cmd) return &family->ops[i]; return NULL; }
這裡找到訊息cmd命令對應的處理函式並儲存早ops變數中,查詢的方式是通過cmd欄位的匹配型別來找的,這裡找到的就是前文中註冊的demo_ops結構了。
if ((ops->flags & GENL_ADMIN_PERM) && !netlink_capable(skb, CAP_NET_ADMIN)) return -EPERM;
接下來判斷許可權,這裡由於已經在demo_ops中設定了GENL_ADMIN_PERM標識,因此本命令操作需要具有CAP_NET_ADMIN許可權。
if ((nlh->nlmsg_flags & NLM_F_DUMP) == NLM_F_DUMP) { int rc; if (ops->dumpit == NULL) return -EOPNOTSUPP; if (!family->parallel_ops) { struct netlink_dump_control c = { .module = family->module, /* we have const, but the netlink API doesn't */ .data = (void *)ops, .dump = genl_lock_dumpit, .done = genl_lock_done, }; genl_unlock(); rc = __netlink_dump_start(net->genl_sock, skb, nlh, &c); genl_lock(); } else { struct netlink_dump_control c = { .module = family->module, .dump = ops->dumpit, .done = ops->done, }; rc = __netlink_dump_start(net->genl_sock, skb, nlh, &c); } return rc; }
如果使用者設定了NLM_F_DUMP標識,這裡就會呼叫啟動dump流程,回填skb訊息(這裡的skb將不再是使用者下發的訊息了)。這裡不進行詳細的分析,繼續往下看:
if (family->maxattr && family->parallel_ops) { attrbuf = kmalloc((family->maxattr+1) * sizeof(struct nlattr *), GFP_KERNEL); if (attrbuf == NULL) return -ENOMEM; } else attrbuf = family->attrbuf;
這裡為attr屬性指定接收快取,在支援重入的情況下這裡會另行動態分配記憶體,否則使用在註冊family的__genl_register_family函式中分配的記憶體空間。 需要注意的是這裡的記憶體其實只是一個指標陣列,用來存放attr屬性的地址,並不會存放實際的屬性資料。
if (attrbuf) { err = nlmsg_parse(nlh, hdrlen, attrbuf, family->maxattr, ops->policy); if (err < 0) goto out; }
這裡將訊息的資料拷貝到快取空間中去,nlmsg_parse()的幾個入參分別為netlink訊息頭,genelink訊息頭長度(其實也包括了使用者私有頭,只不過這裡為0罷了),資料屬性快取地址,快取空間大小和屬性有效性策略結構。
static inline int nlmsg_parse(const struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[], int maxtype, const struct nla_policy *policy) { if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen)) return -EINVAL; return nla_parse(tb, maxtype, nlmsg_attrdata(nlh, hdrlen), nlmsg_attrlen(nlh, hdrlen), policy); }
該函式間接呼叫netlink通用的屬性拷貝函式,其中將第三個引數為attr引數的首地址:nlmsg_attrdata(nlh, hdrlen):
static inline struct nlattr *nlmsg_attrdata(const struct nlmsghdr *nlh, int hdrlen) { unsigned char *data = nlmsg_data(nlh); return (struct nlattr *) (data + NLMSG_ALIGN(hdrlen)); }這裡將指標跳過netlink的頭以及genelink頭,指向attr的首地址。 第四個引數為attr屬性的長度:nlmsg_attrlen(nlh, hdrlen):
static inline int nlmsg_attrlen(const struct nlmsghdr *nlh, int hdrlen) { return nlmsg_len(nlh) - NLMSG_ALIGN(hdrlen); }
計算方式為訊息除去netlink訊息頭的剩餘長度減去genetlink訊息頭長度後的長度。
int nla_parse(struct nlattr **tb, int maxtype, const struct nlattr *head, int len, const struct nla_policy *policy) { const struct nlattr *nla; int rem, err; memset(tb, 0, sizeof(struct nlattr *) * (maxtype + 1)); nla_for_each_attr(nla, head, len, rem) { u16 type = nla_type(nla); if (type > 0 && type <= maxtype) { if (policy) { err = validate_nla(nla, maxtype, policy); if (err < 0) goto errout; } tb[type] = (struct nlattr *)nla; } } if (unlikely(rem > 0)) pr_warn_ratelimited("netlink: %d bytes leftover after parsing attributes in process `%s'.\n", rem, current->comm); err = 0; errout: return err; }
可以看到該函式會逐一的將屬性的地址複製到tb指標陣列中去,但是如果傳入了有效性策略,那他就會呼叫validate_nla函式執行有效性判斷。對於這裡傳入的CTRL_ATTR_FAMILY_NAME屬性來說,在ctrl_policy中已經定義了有效性限制為NLA_NUL_STRING,最大長度為GENL_NAMSIZ-1:
static const struct nla_policy ctrl_policy[CTRL_ATTR_MAX+1] = { [CTRL_ATTR_FAMILY_ID] = { .type = NLA_U16 }, [CTRL_ATTR_FAMILY_NAME] = { .type = NLA_NUL_STRING, .len = GENL_NAMSIZ - 1 }, };
回到genl_family_rcv_msg()函式中繼續往下分析:
info.snd_seq = nlh->nlmsg_seq; info.snd_portid = NETLINK_CB(skb).portid; info.nlhdr = nlh; info.genlhdr = nlmsg_data(nlh); info.userhdr = nlmsg_data(nlh) + GENL_HDRLEN; info.attrs = attrbuf; info.dst_sk = skb->sk; genl_info_net_set(&info, net); memset(&info.user_ptr, 0, sizeof(info.user_ptr));
這裡開始封裝genl_info訊息結構,填充對應的欄位,比較好理解,其中snd_portid填充為傳送端的套接字ID號,attrs為前文中分配的attr快取空間首地址,接下來啟動最終的呼叫處理流程:
if (family->pre_doit) { err = family->pre_doit(ops, skb, &info); if (err) goto out; } err = ops->doit(skb, &info); if (family->post_doit) family->post_doit(ops, skb, &info);
如果在註冊family時指定了pre_doit和post_doit回撥函式,將在分別呼叫ops->doit()函式的前後呼叫他們,對於Ctrl簇而言並沒有定義,這裡會直接呼叫ops->doit()回撥函式,對於CTRL_CMD_GETFAMILY來說就是ctrl_getfamily()了:
static int ctrl_getfamily(struct sk_buff *skb, struct genl_info *info) { struct sk_buff *msg; struct genl_family *res = NULL; int err = -EINVAL; if (info->attrs[CTRL_ATTR_FAMILY_ID]) { u16 id = nla_get_u16(info->attrs[CTRL_ATTR_FAMILY_ID]); res = genl_family_find_byid(id); err = -ENOENT; }
首先函式匹配CTRL_ATTR_FAMILY_ID,由於並未傳入該屬性資料,因此這裡該屬性的地址為NULL,然後接著判斷另一個屬性型別:
if (info->attrs[CTRL_ATTR_FAMILY_NAME]) { char *name; name = nla_data(info->attrs[CTRL_ATTR_FAMILY_NAME]); res = genl_family_find_byname(name); err = -ENOENT; }
這裡就開始處理CTRL_CMD_GETFAMILY屬性了,只做了一件事,就是通過使用者傳入的family name獲取到對應的family結構。
if (res == NULL) return err; if (!res->netnsok && !net_eq(genl_info_net(info), &init_net)) { /* family doesn't exist here */ return -ENOENT; } msg = ctrl_build_family_msg(res, info->snd_portid, info->snd_seq, CTRL_CMD_NEWFAMILY); if (IS_ERR(msg)) return PTR_ERR(msg); return genlmsg_reply(msg, info);
這裡依然使用ctrl_build_family_msg()函式封裝回發訊息(該函式的分析見前文), 注意回發訊息的cmd為CTRL_CMD_NEWFAMILY(它會將查詢結果family的全部內容回傳),指定的port_id號為訊息查詢端的id(並不是核心的id號0),訊息的sequence也同查詢訊息一致。
函式最後呼叫genlmsg_reply()嚮應用層回發訊息:
static inline int genlmsg_reply(struct sk_buff *skb, struct genl_info *info) { return genlmsg_unicast(genl_info_net(info), skb, info->snd_portid); }
可以看到它就是nlmsg_unicast的一個封裝而已(nlmsg_unicast的實現分析見 《Netlink 核心實現分析(二):通訊》)。至此查詢訊息的傳送和核心的處理流程分析完畢,下面回到示例程式demo_get_family_id()中:
/* 接收核心訊息 */ rep_len = recv(sd, &ans, sizeof(ans), 0); if (ans.n.nlmsg_type == NLMSG_ERROR || (rep_len < 0) || !NLMSG_OK((&ans.n), rep_len)) return 0; /* 解析family id */ na = (struct nlattr *) GENLMSG_DATA(&ans); na = (struct nlattr *) ((char *) na + NLA_ALIGN(na->nla_len)); if (na->nla_type == CTRL_ATTR_FAMILY_ID) { id = *(__u16 *) NLA_DATA(na); }
這裡找到回發訊息中的第二個attr(訊息結構參見圖1-c),然後獲取出其中的family id號。至此使用者程式成功獲取的了demo family的id號,接下來就可以向他傳送訊息了。
(二)核心Demo Family傳送訊息
/* 傳送字串訊息 */ my_pid = getpid(); string = argv[1]; data = atoi(argv[2]); ret = demo_send_cmd(nl_fd, nl_family_id, my_pid, DEMO_CMD_ECHO, DEMO_CMD_ATTR_MESG, string, strlen(string) + 1); if (ret < 0) { fprintf(stderr, "failed to send echo cmd\n"); goto out; } /* 傳送資料訊息 */ ret = demo_send_cmd(nl_fd, nl_family_id, my_pid, DEMO_CMD_ECHO, DEMO_CMD_ATTR_DATA, &data, sizeof(data)); if (ret < 0) { fprintf(stderr, "failed to send echo cmd\n"); goto out; }
本示例程式比較簡單,直接使用程式的入參作為傳送的資料。傳送依然是呼叫demo_send_cmd函式實現,但是入參同獲取family id時的有所不同,首先發送字串訊息時第二個入參設定為剛剛獲取的demo family id,然後傳送端套接字ID為當前程序的pid號,然後傳送cmd為DEMO_CMD_ECHO,傳送的屬性依次為DEMO_CMD_ATTR_DATA(其實cmd和attr屬性並沒有明確的一一對應關係,使用者可根據需求自行組合,同時一個cmd訊息也可以帶很多的attr屬性,這點從核心ctrl回發的訊息就可以看出),最後傳送的訊息內容分別為使用者輸入的字串和資料。
(三)核心Demo Family 回發訊息
應用層序向demo family傳送DEMO_CMD_ECHO訊息後,核心會呼叫到前文中註冊時指定的doit回撥函式demo_echo_cmd(具體的資料傳送流程同前文中分析的Ctrl查詢訊息,不再詳細分析),來看一下demo_echo_cmd()函式所做的處理。
static int demo_echo_cmd(struct sk_buff *skb, struct genl_info *info) { if (info->attrs[DEMO_CMD_ATTR_MESG]) return cmd_attr_echo_message(info); else if (info->attrs[DEMO_CMD_ATTR_DATA]) return cmd_attr_echo_data(info); else return -EINVAL; }
該函式會判斷接收的屬性型別,並做出相應的處理(注意:為了簡單起見,該doit回撥函式最多一次只能處理一種型別的attr屬性),先來看cmd_attr_echo_message()函式
static int cmd_attr_echo_message(struct genl_info *info) { struct nlattr *na; char *msg; struct sk_buff *rep_skb; size_t size; int ret; /* 讀取使用者下發的訊息 */ na = info->attrs[DEMO_CMD_ATTR_MESG]; if (!na) return -EINVAL; msg = (char *)nla_data(na); pr_info("demo generic netlink receive echo mesg %s\n", msg); /* 回發訊息 */ size = nla_total_size(strlen(msg)+1); /* 準備構建訊息 */ ret = demo_prepare_reply(info, DEMO_CMD_REPLY, &rep_skb, size); if (ret < 0) return ret; /* 填充訊息 */ ret = demo_mk_reply(rep_skb, DEMO_CMD_ATTR_MESG, msg, size); if (ret < 0) goto err; /* 完成構建併發送 */ return demo_send_reply(rep_skb, info); err: nlmsg_free(rep_skb); return ret; }
這裡從DEMO_CMD_ATTR_MESG屬性地址處取出使用者下發的訊息內容,然後呼叫demo_prepare_reply構建回發訊息頭。其中入參依次為接收genl_info訊息,回發cmd型別,skb指標地址,回發資料長度。
static int demo_prepare_reply(struct genl_info *info, u8 cmd, struct sk_buff **skbp, size_t size) { struct sk_buff *skb; void *reply; /* * If new attributes are added, please revisit this allocation */ skb = genlmsg_new(size, GFP_KERNEL); if (!skb) return -ENOMEM; if (!info) return -EINVAL; /* 構建回發訊息頭 */ reply = genlmsg_put_reply(skb, info, &demo_family, 0, cmd); if (reply == NULL) { nlmsg_free(skb); return -EINVAL; } *skbp = skb; return 0; }這裡依然呼叫genlmsg_new()函式申請skb套接字快取空間,然後直接呼叫genlmsg_put_reply()函式構建回發訊息的netlink訊息頭和genetlink訊息頭:
static inline void *genlmsg_put_reply(struct sk_buff *skb, struct genl_info *info, struct genl_family *family, int flags, u8 cmd) { return genlmsg_put(skb, info->snd_portid, info->snd_seq, family, flags, cmd); }
該函式僅僅是genlmsg_put的一個封裝而已,注意入參info->snd_portid為使用者層的netlink套接字的id號。回到cmd_attr_echo_message()函式中,接下來填充訊息屬性:
/* 填充訊息 */ ret = demo_mk_reply(rep_skb, DEMO_CMD_ATTR_MESG, msg, size); if (ret < 0) goto err;
static int demo_mk_reply(struct sk_buff *skb, int aggr, void *data, int len) { /* add a netlink attribute to a socket buffer */ return nla_put(skb, aggr, len, data); }
這裡呼叫nla_put()函式將字串訊息填充到第一個attr屬性中,同時指定attr的屬性型別為DEMO_CMD_ATTR_MESG,最後呼叫demo_send_reply()將訊息往應用層傳送:
/* 完成構建併發送 */ return demo_send_reply(rep_skb, info);
static int demo_send_reply(struct sk_buff *skb, struct genl_info *info) { struct genlmsghdr *genlhdr = nlmsg_data(nlmsg_hdr(skb)); void *reply = genlmsg_data(genlhdr); genlmsg_end(skb, reply); return genlmsg_reply(skb, info); }
首先呼叫genlmsg_end()更新訊息頭重的長度欄位,然後呼叫genlmsg_reply啟動回發流程:
static inline int genlmsg_reply(struct sk_buff *skb, struct genl_info *info) { return genlmsg_unicast(genl_info_net(info), skb, info->snd_portid); }
該函式為genlmsg_unicast()的一個封裝。這樣demo family的回發字串息就傳送出去了。下面再來簡單的看一下cmd_attr_echo_data()函式,它同cmd_attr_echo_message()函式基本類似,唯一的區別就是呼叫了核心提供的nla_get_s32()和nla_put_s32()這兩個封裝函式來獲取和設定s32型別的attr屬性,不做過多的論描。
static int cmd_attr_echo_data(struct genl_info *info) { struct nlattr *na; s32 data; struct sk_buff *rep_skb; size_t size; int ret; /* 讀取使用者下發的資料 */ na = info->attrs[DEMO_CMD_ATTR_DATA]; if (!na) return -EINVAL; data = nla_get_s32(info->attrs[DEMO_CMD_ATTR_DATA]); pr_info("demo generic netlink receive echo data %d\n", data); /* 回發資料 */ size = nla_total_size(sizeof(s32)); ret = demo_prepare_reply(info, DEMO_CMD_REPLY, &rep_skb, size); if (ret < 0) return ret; /* 為了簡單這裡直接呼叫netlink庫函式(對於需求的豐富可以自行封裝) */ ret = nla_put_s32(rep_skb, DEMO_CMD_ATTR_DATA, data); if (ret < 0) goto err; return demo_send_reply(rep_skb, info); err: nlmsg_free(rep_skb); return ret; }
核心訊息全部單播發送出去以後,下面來看應用層的接收流程。
(四)應用層接收核心Demo Family回發訊息
int main(int argc, char* argv[]) { ...... /* 接收使用者訊息並解析(本示例程式中僅解析2個) */ demo_msg_recv_analysis(nl_fd, argc-1); ...... }
void demo_msg_recv_analysis(int sd, int num) { int rep_len; int len; struct nlattr *na; struct msgtemplate msg; unsigned int data; char *string; while (num--) { /* 接收核心訊息回顯 */ rep_len = recv(sd, &msg, sizeof(msg), 0); if (rep_len < 0 || demo_msg_check(msg, rep_len) < 0) { fprintf(stderr, "nonfatal reply error: errno %d\n", errno); continue; } PRINTF("received %d bytes\n", rep_len); PRINTF("nlmsghdr size=%zu, nlmsg_len=%d, rep_len=%d\n", sizeof(struct nlmsghdr), msg.n.nlmsg_len, rep_len); rep_len = GENLMSG_PAYLOAD(&msg.n); na = (struct nlattr *) GENLMSG_DATA(&msg); len = 0; /* 一個msg裡可能有多個attr,所以這裡迴圈讀取 */ while (len < rep_len) { len += NLA_ALIGN(na->nla_len); switch (na->nla_type) { case DEMO_CMD_ATTR_MESG: /* 接收到核心字串回顯 */ string = (char *) NLA_DATA(na); printf("echo reply:%s\n", string); break; case DEMO_CMD_ATTR_DATA: /* 接收到核心資料回顯 */ data = *(int *) NLA_DATA(na); printf("echo reply:%u\n", data); break; default: fprintf(stderr, "Unknown nla_type %d\n", na->nla_type); } na = (struct nlattr *) (GENLMSG_DATA(&msg) + len); } } }
應用程式呼叫demo_msg_recv_analysis()函式接收核心訊息,其中num表示接收訊息的個數。該函式中迴圈呼叫recv函式阻塞式的接收核心netlink訊息。當有訊息接收到以後呼叫demo_msg_check()函式判斷訊息的有效性:
int demo_msg_check(struct msgtemplate msg, int rep_len) { if (msg.n.nlmsg_type == NLMSG_ERROR || !NLMSG_OK((&msg.n), rep_len)) { struct nlmsgerr *err = NLMSG_DATA(&msg); fprintf(stderr, "fatal reply error, errno %d\n", err->error); return -1; } return 0; }
這裡首先判斷訊息頭中的nlmsg_type欄位,如果該欄位為NLMSG_ERROR表示接收到了錯誤的訊息,應該立即丟棄。如果接收到的訊息型別無誤,則接下來判斷訊息的長度是否足夠,使用的是NLMSG_OK巨集(見netlink.h)。然後接收函式迴圈讀取attr屬性並根據屬性的attr型別單獨進行處理,本示例中僅僅在終端中列印。需要補充的是,本程式中並沒有對接收到的訊息cmd型別進行判斷,其實為了程式的可靠性考慮,最好增加這一方面的判斷(雖然netlink的id號保證了不會收到其他id的genetlink訊息,但是當某family的cmd型別較多時容易引起混亂)。至此demo family的genetlink單播通訊過程就大致分析完畢.