三、無線資訊傳遞——Generic Netlink(一、初始化)
系列說明
上一篇說明了無線資訊的user space至kernel space的大致傳遞流程,這一主要針對以下3點進行一個順序的描述:
- 1、無線驅動資訊傳遞框架:說明無線資訊傳遞的步驟流程以及各程式塊之間的聯絡;
- 2、generic Netlink訊號傳遞機制:hostapd與無線驅動之間的資訊傳遞機制;
- 3、以ssid為例說明使用者將user space中的ssid配置內容傳遞至kernel space的流程:以此係統地瞭解整個無線資訊傳遞流程。
上篇中,多次提到了genetlink,他的全名為generic netlink。而generic netlink在user space和kernel space之間起到了關鍵性的作用。由於內容較多,這一篇就對generic netlink進行第一階段的分析。下一篇再通過一個具體的程式碼例子系統地說明generic netlink是怎麼讓user space和kernel space雙向通訊的。
一、generic netlink
為什麼會出現generic netlink?
因為netlink最多隻支援32個協議簇,所以generic netlink的出現是為擴充套件netlink協議簇而設計,即是netlink協議簇的子集。
博文中是這樣說明的:Generic Netlink 是核心專門為了擴充套件netlink協議簇而設計的“通用netlink協議簇”。由於netlink協議最多支援32個協議簇,目前Linux4.1的核心中已經使用其中21個,對於使用者需要定製特殊的協議型別略顯不夠,而且使用者還需自行在include/linux/netlink.h中新增簇定義(略顯不妥),為此Linux設計了這種通用Netlink協議簇,使用者可在此之上定義更多型別的子協議。
generic netlink在程式中的程式碼實現框架:
從上圖中可以看出Generic Netlink也還是需要藉助Netlink來完成使用者環境和核心環境的資訊傳遞。使用者層發信息先至Netlink,然後再轉給Generic netlink分支,再至核心;同樣的核心發信息先發給Generic netlink分支,然後再通過netlink傳給使用者層。
框圖中Ctrl控制器是一種特殊型別的Genetlink協議族,它用於使用者空間通過Genetlink簇名查詢對應的ID號。
以下為netlink型別的訊息結構:
具象化地來說,上面的三幅圖內容如下圖說明所示:
二、generic netlink主要結構體
/*
* 為上圖中的family header
*/
struct genlmsghdr {
__u8 cmd; //表示訊息命令,對於使用者自己定義的每個子協議型別都需要定義特定的訊息命令集,這裡該欄位表示當前訊息的訊息命令
__u8 version; //version欄位表示版本控制(可以在在不破壞向後相容性的情況下修改訊息的格式),可以不使用該欄位
__u16 reserved; //保留欄位
};
/*
* Generic Netlink按照family進行管理,使用者需註冊自己定義的genl_family結構,同時核心使用一個雜湊表family_ht對已經註冊的genl family進行管理
*/
struct genl_family {
struct list_head list; //連結串列結構,用於將當前當前簇鏈入全域性family_ht散列表中
unsigned int id; //genl family的ID號,一般由核心進行分配,取值範圍為GENL_MIN_ID~GENL_MAX_ID(16~1023),其中GENL_ID_CTRL為控制器的family ID,不可另行分配,該familyID全域性唯一併且在family_ht中的位置也由該值確定
unsigned int hdrsize; //使用者私有報頭的長度,即上圖中可選的user msg header長度,若沒有則為0
unsigned int version; //版本號
unsigned int maxattr; //訊息屬性attr最大的型別數(即該genl family所支援的最大attr屬性型別的種類個數)
const char *name; //genl family的名稱,必須是獨一無二且使用者層已知的(使用者通過它來向控制查詢family id)
bool netnsok; //指示當前簇是否能夠處理網路名稱空間
struct nlattr **attrbuf; //儲存拷貝的attr屬性快取
int (*pre_doit)(struct genl_ops *ops, struct sk_buff *skb, struct genl_info *info); //呼叫genl_ops結構中處理訊息函式doit()前呼叫的鉤子函式,一般用於執行一些前置的當前簇通用化處理,例如對臨界區加鎖等
void (*post_doit)(struct genl_ops *ops, struct sk_buff *skb, struct genl_info *info); //呼叫genl_ops結構中處理訊息函式doit()後呼叫的鉤子函式,一般執行pre_doit函式相反的操作
void (*mcast_unbind)(struct net *net, int group); //在解繫結socket到一個特定的genl netlink組播組中呼叫(目前核心中沒有相關使用)
int (*mcast_bind)(struct net *net, int group); //在繫結socket到一個特定的genl netlink組播組中呼叫(目前核心中沒有相關使用)
const struct genl_multicast_group *mcgrps; //儲存當前簇使用的組播組及組播地址的個數
unsigned int n_mcgrps;
const struct genl_ops * ops; //儲存genl family命令處理結構即命令的個數,後面詳細描述
unsigned int n_ops;
};
/*
* 該結構用於註冊genl family的使用者命令cmd處理函式(對於只嚮應用層傳送訊息的簇可以不用實現和註冊該結構)
*/
struct genl_ops {
u8 cmd; //簇命令型別,由使用者自行根據需要定義
u8 internal_flags; //簇私有標識,用於進行一些分支處理,可以不使用
unsigned int flags; //操作標識,有下面四中型別
const struct nla_policy *policy; //屬性attr有效性策略,若該欄位不為空,在genl執行訊息處理函式前會對訊息中的attr屬性進行校驗,否則則不做校驗
int (*doit)(struct sk_buff *skb, struct genl_info *info); //標準命令回撥函式,在當前族中收到資料時觸發呼叫,函式的第一個入參skb中儲存了使用者下發的訊息內容
int (*dumpit)(struct sk_buff *skb, struct netlink_callback *cb); //轉儲回撥函式,當genl_ops的flag標誌被添加了NLM_F_DUMP以後會呼叫該回調函式,這裡的第一個入參skb中不再有使用者下發訊息內容,而是要求函式能夠在傳入的skb中填入訊息載荷並返回填入資料長度
int (*done)(struct netlink_callback *cb); //轉儲結束後執行的回撥函式
};
#define GENL_ADMIN_PERM 0x01 /* 當設定該標識時表示本命令操作需要具有CAP_NET_ADMIN許可權 */
#define GENL_CMD_CAP_DO 0x02 /* 當genl_ops結構中實現了doit()回撥函式則設定該標識 */
#define GENL_CMD_CAP_DUMP 0x04 /* 當genl_ops結構中實現了dumpit()回撥函式則設定該標識 */
#define GENL_CMD_CAP_HASPOL 0x08 /* 當genl_ops結構中定義了屬性有效性策略(nla_policy)則設定該標識 */
/*
* 核心在接收到使用者的genetlink訊息後,會對訊息解析並封裝成genl_info結構,便於命令回撥函式進行處理
*/
struct genl_info {
struct sock* dst_sk; //目的socket
u32 snd_seq; //訊息的傳送序號(不強制使用)
u32 snd_pid; //訊息傳送端socket所繫結的ID
struct genlmsghdr *genlhdr; //generic netlink訊息頭
struct nlattr **attrs; //netlink屬性,包含了訊息的實際載荷
void *user_ptr[2]; //使用者私有報頭
struct nlmsghdr* nlhdr; //netlink訊息頭
};
那麼這幾個結構體有什麼聯絡呢?
回到原來netlink解析的圖。
從接收到user space的訊息順序來看:
genlmsghdr(第一個結構體)在第二層中的family header中使用,用於獲取接收到的訊息的cmd命令;
然後是genl_info(第四個結構體),該結構體中的attrs包含了訊息的實際載荷,以此可以獲取到使用者傳下來的id,程式中從全域性family_ht[](這裡的family_ht連結串列在程式執行時通過呼叫genl_register_family介面向該連結串列新增family協議簇)表示的雜湊表中找到相同對應id的genl_family(第二個結構體)。
genl_family一般是控制器協議簇,結構體中有兩個重要內容。一個為name,user space以此向核心獲取family id來建立與核心通訊的連線,所以user space想要與核心通訊時,這裡的name與user space的name必須一致,同時name也需要確保他的一致性。另外一個為ops列表,其中包含genl_ops(第三個結構體)。
genl_ops結構體用於呼叫上層使用者發下來的命令的對應的回撥處理函式。
三、Generic netlink 初始化
在瞭解了generic netlink的主要幾個結構體之後,接下來繼續對genetlink初始化的瞭解會方便很多。下面我們以一個程式流程圖和generic netlink的基本幾段程式來進行初始化說明。
static int __init genl_init(void)
{
int i, err;
for (i = 0; i < GENL_FAM_TAB_SIZE; i++)
INIT_LIST_HEAD(&family_ht[i]); //初始化用於儲存和維護Generic netlink family的散列表family_ht陣列,其中family_ht為一個全域性變數
err = genl_register_family_with_ops_groups(&genl_ctrl, genl_ctrl_ops, genl_ctrl_groups); //向核心Generic netlink子系統註冊控制器簇型別的Genetlink Family(genl_ctrl_groups, genl_ctrl和genl_ctrl_ops, genl_register_family_with_ops_groups如下面程式碼所示)
if (err < 0)
goto problem;
err = register_pernet_subsys(&genl_pernet_ops); //為當前系統中的網路名稱空間建立Generic Netlink套接字(在下面程式碼中說明)
if (err)
goto problem;
return 0;
problem:
panic("GENL: Cannot register controller: %d\n", err);
}
static struct genl_family genl_ctrl = {
.id = GENL_ID_CTRL, //GENL_ID_CTRL(16),分配區間的最小值
.name = "nlctrl",
.version = 0x2,
.maxattr = CTRL_ATTR_MAX, //maxattr定義為支援的attr屬性最大個數CTRL_ATTR_MAX(表示該genl family所支援的最大attr屬性型別的種類個數),如下列舉所示
.netnsok = true, //為true表示支援net名稱空間
};
enum {
CTRL_ATTR_UNSPEC,
CTRL_ATTR_FAMILY_ID,
CTRL_ATTR_FAMILY_NAME,
CTRL_ATTR_VERSION,
CTRL_ATTR_HDRSIZE,
CTRL_ATTR_MAXATTR,
CTRL_ATTR_OPS,
CTRL_ATTR_MCAST_GROUPS,
__CTRL_ATTR_MAX,
};
#define CTRL_ATTR_MAX(__CTRL_ATTR_MAX - 1)
static struct genl_ops genl_ctrl_ops[] = {
{
.cmd = CTRL_CMD_GETFAMILY, //用於應用空間從核心中獲取指定family名稱的ID號的命令(因為該ID號在核心註冊family時由核心進行分配,應用空間一般只知道需要通訊的family name,但是要發起通訊就必須知道該ID號,所以核心設計了控制器型別的family並定義了CTRL_CMD_GETFAMILY命令的處理介面用於應用程式查詢ID號)
.doit = ctrl_getfamily, //回撥執行函式
.dumpit = ctrl_dumpfamily,
.policy = ctrl_policy, //有效性策略為ctrl_policy(如下程式碼所示)
},
};
//attr有效性策略為ctrl_policy,這裡獲取應用的值筆者理解為attr[CTRL_ATTR_FAMILY_ID]獲取到cmd的id(u16型別),attr[CTRL_ATTR_FAMILY_NAME]獲取到的為cmd的msg(string型別)
static const struct nla_policy ctrl_policy[CTRL_ATTR_MAX+1] = {
[CTRL_ATTR_FAMILY_ID] = { .type = NLA_U16 }, //屬性限定型別為16位無符號數
[CTRL_ATTR_FAMILY_NAME] = { .type = NLA_NUL_STRING, //屬性限定為空結尾的字串型別並限定了長度
.len = GENL_NAMSIZ - 1 },
};
//註冊的組播組
static struct genl_multicast_group genl_ctrl_groups[] = {
{
.name = "notify", //添加了name為”notify“的組播組
},
};
#define genl_register_family_with_ops_groups(family, ops, grps) \
_genl_register_family_with_ops_grps((family), \
(ops), ARRAY_SIZE(ops), \
(grps), ARRAY_SIZE(grps))
static inline int
_genl_register_family_with_ops_grps(struct genl_family *family,
const struct genl_ops *ops, size_t n_ops,
const struct genl_multicast_group *mcgrps,
size_t n_mcgrps)
{
family->module = THIS_MODULE;
family->ops = ops; //genl_family中賦值genl_ops型別的genl_ctrl_ops指標
family->n_ops = n_ops;
family->mcgrps = mcgrps; //genl_family賦值genl_multicast_group型別的genl_ctrl_groups指標
family->n_mcgrps = n_mcgrps;
return __genl_register_family(family); //繼續註冊
}
int __genl_register_family(struct genl_family *family)
{
int err = -EINVAL, i;
if (family->id && family->id < GENL_MIN_ID) //對入參的ID號進行判斷,一般來說,為了保證ID號的全域性唯一性,程式中一般都設定為GENL_ID_GENERATE,由核心統一分配(當然這裡註冊控制器family除外了)
goto errout;
if (family->id > GENL_MAX_ID)
goto errout;
err = genl_validate_ops(family); //對ops函式集做校驗
//校驗內容為:
// 1、每一個註冊的genl_ops結構,其中doit和dumpit回撥函式必須至少實現一個
// 2、針對的cmd命令不可以出現重複
if (err)
return err;
genl_lock_all(); //上鎖開始啟動連結串列操作
if (genl_family_find_byname(family->name)) { //前面已經說明family中的name需要確保其唯一性,所以這裡進行判斷是否有name重複,有則不符合,不進行註冊
err = -EEXIST;
goto errout_locked;
}
if (family->id == GENL_ID_GENERATE) { //判斷傳入的ID號是否為GENL_ID_GENERATE,若是則由核心分配一個空閒的ID號
u16 newid = genl_generate_id();
if (!newid) {
err = -ENOMEM;
goto errout_locked;
}
family->id = newid;
} else if (genl_family_find_byid(family->id)) { //不是核心分配時,判斷是否已經存在該id號,有則不註冊
err = -EEXIST;
goto errout_locked;
}
if (family->maxattr && !family->parallel_ops) { //根據註冊的最大attr引數maxattr(注意僅僅是儲存屬性的地址而非內容)
family->attrbuf = kmalloc((family->maxattr+1) * sizeof(struct nlattr *), GFP_KERNEL); //分配核心空間(GFP_KERNEL)
if (family->attrbuf == NULL) {
err = -ENOMEM;
goto errout_locked;
}
} else
family->attrbuf = NULL;
err = genl_validate_assign_mc_groups(family); //呼叫genl_validate_assign_mc_groups()函式判斷新增組播地址空間
/*
* 這裡的genl_validate_assign_mc_groups介面一共做3件事:
* 1、判斷註冊family的group組播名的有效性
* 2、為該family分配組播地址位元位並將bit偏移儲存到family->mcgrp_offset變數中(由於generic netlink中不同型別的family簇共用NETLINK_GENERIC協議型別的group組播地址空間,因此核心特別維護了幾個全域性變數mc_groups_longs、mc_groups和mc_group_start用以維護組播地址的位元位,另外對於幾種特殊的family是已經分配了的。無需再行分配,例如這裡的crtl控制器)
* 3、更新全域性nl_table對應的NETLINK_GENERIC協議型別netlink的groups標識
*/
if (err)
goto errout_locked;
list_add_tail(&family->family_list, genl_family_chain(family->id)); //將family註冊到連結串列中
genl_unlock_all();
/* send all events */
genl_ctrl_event(CTRL_CMD_NEWFAMILY, family, NULL, 0); //呼叫genl_ctrl_event()函式向核心的控制器family傳送CTRL_CMD_NEWFAMILY和CTRL_CMD_NEWMCAST_GRP命令訊息
for (i = 0; i < family->n_mcgrps; i++)
genl_ctrl_event(CTRL_CMD_NEWMCAST_GRP, family, &family->mcgrps[i], family->mcgrp_offset + i);
return 0;
errout_locked:
genl_unlock_all();
errout:
return err;
}
EXPORT_SYMBOL(__genl_register_family);
static int __net_init genl_pernet_init(struct net *net)
{
//定義了genetlink核心套接字的配置
struct netlink_kernel_cfg cfg = {
.input = genl_rcv, //訊息處理函式
.flags = NL_CFG_F_NONROOT_RECV,
.bind = genl_bind, //套接字繫結函式
.unbind = genl_unbind, //套接字解繫結函式
};
/* we'll bump the group number right afterwards */
net->genl_sock = netlink_kernel_create(net, NETLINK_GENERIC, &cfg); //核心套接字的註冊,並將生成的套接字賦值到網路名稱空間net的genl_sock中,以後就可以通過net->genl_sock來找到genetlink核心套接字了(這裡netlink的說明可以參考文章末尾連結)
if (!net->genl_sock && net_eq(net, &init_net))
panic("GENL: Cannot initialize generic netlink\n");
if (!net->genl_sock)
return -ENOMEM;
return 0;
}