——linux版本:3.14.38
在實際項目中,如果有定製化的需求時,最好不要去佔用剩下的暫未定義的協議類型ID號,而是使用預定義的通用netlink協議類型NETLINK_GENERIC來進行擴展。
LINUX中跟netlink相關的核心代碼位於net/netlink目錄中,其中核心頭文件主要有3個(這些都是所有協議類型的netlink共享的):
[1]. net/netlink/af_netlink.h - 這個頭文件主要包含了實現netlink的核心數據結構
[2]. include/linux/netlink.h - 這個頭文件主要包含了netlink套接字相關的內容
[3]. include/net/netlink.h - 這個頭文件主要包含了netlink消息相關的通用格式
各種協議類型的netlink代碼位於其他相關目錄(比如最常見的NETLINK_ROUTE相關代碼在net/core/rtnetlink.c中)
注意:以下所有的變量和函數註解基本都沒有區分不同的net命名空間,所以在考慮net命名空間時以下表述存在不準確的地方!
1. netlink模塊涉及的主要結構和變量
/* 以下是一張全局的netlink接口總表,是netlink模塊的核心變量.
* 表中包含了MAX_LINKS個元素,每個元素對應一種netlink協議,如NETLINK_ROUTE
*/
struct netlink_table *nl_table;
/* 以下是nl_table每個表項的數據結構,是netlink模塊的核心數據結構
*/
struct netlink_table {
struct nl_portid_hash hash; // hash表控制塊,內部的hash表記錄了已經創建的同種協議類型的所有netlink套接字
struct hlist_head mc_list; // 這個hash表頭節點用於記錄同種協議類型下所有閱訂了組播功能的套接字
struct listeners __rcu *listeners; // 記錄了同種協議類型下所有被閱訂了的組播消息集合
unsigned int flags; // 這裏的標誌位來自配置netlink_kernel_cfg,目前主要記錄了該協議類型允許的用戶態操作權限
unsigned int groups; // 記錄了該協議類型支持的最大組播數量(通常就是32個)
struct mutex *cb_mutex; // 記錄了該協議類型創建的鎖
struct module *module;
void (*bind)(int group); // 該協議類型私有的bind函數
bool (*compare)(struct net *net, struct sock *sock); // 該協議私有的net命名空間比較函數
int registered; // 標記該協議類型是否已經註冊,0表示未註冊,>=1表示已經有註冊過
};
/* 以下是netlink套接字的數據結構,所有的協議類型通用
*/
struct netlink_sock {
struct sock sk; // 該netlink套接字的sock結構
u32 portid; // 記錄了該netlink套接字綁定的單播地址,對內核來說就是0
u32 dst_portid; // 記錄了該netlink套接字的默認目的單播地址(缺省爲0,當用戶進程調用connect時可以指定)
u32 dst_group; // 記錄了該netlink套接字的默認目的組播地址(缺省爲0,當用戶進程調用connect時可以指定)
u32 flags; // 用來標識該netlink套接字的屬性,比如NETLINK_KERNEL_SOCKE
u32 subscriptions; // 記錄該netlink套接字當前閱訂的組播數量
u32 ngroups; // 記錄該netlink套接字支持的最大組播數量
unsigned long *groups;// 指向該netlink套接字的組播空間
unsigned long state; // 3.14.38版本中只用來設置擁擠標誌
wait_queue_head_t wait; // 該netlink套接字的等待隊列,當接收隊列擁擠時,那些繼續發送netlink單播消息到該套接字的用戶發送進程將會加入等待隊列
// (組播消息和來自內核的單播消息都是非阻塞的,所以不會加入等待隊列)
bool cb_running; // 用來標誌該netlink套接字是否處於dump操作中
struct netlink_callback cb; // 用來記錄該netlink套接字當前有效的操作集合
struct mutex *cb_mutex; // 這把鎖在內核netlink套接字創建時傳入,相同協議類型的netlink套接字共用一把鎖
struct mutex cb_def_mutex;
void (*netlink_rcv)(struct sk_buff *skb); // 指向具體協議類型特有的input回調函數(只對內核netlink套接字有意義)
void (*netlink_bind)(int group); // 指向具體協議類型特有的bind操作(只對用戶進程netlink套接字有意義,從所屬netlink_table中的bind成員繼承)
struct module *module;
};
/* 以下是內核創建具體協議類型的netlink套接字時傳入的參數配置數據結構
*/
struct netlink_kernel_cfg {
unsigned int groups; // 該協議類型支持的最大多播組數量
unsigned int flags; // 用來設置NL_CFG_F_NONROOT_SEND/NL_CFG_F_NONROOT_RECV這兩個標誌
void (*input)(struct sk_buff *skb); // 用來配置消息接收函數,用戶空間發送該協議類型的netlink消息給內核後,就會調用本函數
struct mutex *cb_mutex; // 用來配置協議類型私有的互斥鎖
void (*bind)(int group); // 用來配置協議類型私有的bind回調函數
bool (*compare)(struct net *net, struct sock *sk); // 用來配置協議類型私有的compare回調函數
};
2. 內核netlink模塊初始化
/* 以下是整個netlink模塊的初始化入口
*
* 備註:內核在這裏默認創建了兩種協議類型的netlink:NETLINK_USERSOCK和NETLINK_ROUTE
*/
static int __init netlink_proto_init(void)
{
// 向內核的proto_list鏈表註冊所有netlink協議共有的netlink_proto,至此所有從socket層下來的netlink消息都可以通過該接口到達相應的傳輸層
proto_register(&netlink_proto, 0);
// 創建了一張MAX_LINKS長度的nl_table表,這張表是整個netlink功能模塊的核心,每種協議類型佔一個表項
nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);
if (totalram_pages >= (128 * 1024))
limit = totalram_pages >> (21 - PAGE_SHIFT);
else
limit = totalram_pages >> (23 - PAGE_SHIFT);
order = get_bitmask_order(limit) - 1 + PAGE_SHIFT;
limit = (1UL << order) / sizeof(struct hlist_head);
order = get_bitmask_order(min(limit, (unsigned long)UINT_MAX)) - 1;
// 給nl_table表每個表項申請一張hash表並且註冊缺省的net命名空間比較函數
for (i = 0; i < MAX_LINKS; i++)
{
struct nl_portid_hash *hash = &nl_table[i].hash;
// 每張hash表默認都會創建1個初始bucket,實際也就是存放一個鏈表頭指針
hash->table = nl_portid_hash_zalloc(1 * sizeof(*hash->table));
hash->max_shift = order;
hash->shift = 0;
hash->mask = 0;
hash->rehash_time = jiffies;
nl_table[i].compare = netlink_compare;
}
// 初始化netlink_tap_all鏈表頭
INIT_LIST_HEAD(&netlink_tap_all);
// 創建並註冊NETLINK_USERSOCK協議到nl_table中
netlink_add_usersock_entry();
/* 註冊netlink協議族操作集合到內核中(主要包含netlink接口創建函數)
* 後續,應用層創建netlink類型的socket時就會調用這裏註冊的create函數
*/
sock_register(&netlink_family_ops);
/* 將netlink模塊註冊到每一個網絡命名空間,並且執行了netlink_net_init
* 需要注意的是,由於netlink_net_ops.size = 0,意味着netlink模塊沒有私有空間
*/
register_pernet_subsys(&netlink_net_ops);
// 創建並註冊NETLINK_ROUTE協議到nl_table中
rtnetlink_init();
}
/* 以下就是具體的netlink功能初始化(實際就是創建proc文件系統下的netlink接口)
*/
static int __net_init netlink_net_init(struct net *net)
{
#ifdef CONFIG_PROC_FS
// proc文件系統下創建/proc/net/netlink文件(文件屬性:普通文件 + 只讀)
proc_create("netlink", 0, net->proc_net, &netlink_seq_fops)
#endif
}
小結:內核模塊的上電初始化是分優先級的,netlink模塊作爲其他模塊的依賴模塊,需要確保優先完成初始化(通過core_initcall宏)
netlink模塊本身只提供了一個協議無關的通用平臺,實際應用時需要結合具體的協議類型才能完成通信。
以下的分析都是基於NETLINK_ROUTE協議來完成的。
3. 內核創建基於具體協議類型的netlink套接字
針對每種netlink協議類型,內核通常(但不絕對,比如NETLINK_USERSOCK)會創建2類netlink套接字:
一類是內核主動創建的完全屬於內核的netlink套接字,入口函數就是netlink_kernel_create;
另一類是由用戶進程調用socket()系統調用,從而在內核中創建的屬於用戶進程的netlink套接字,入口函數就是netlink_create(這個入口其實不完整,只包含了其中的sock結構的創建).
基於netlink的用戶空間和內核空間交互,實質就是用戶進程netlink套接字和對應內核netlink套接字的交互!
/* 以下就是創建屬於內核的具體協議的netlink套接字(可以看出只是個封裝函數)
*
* 備註:只要是跟用戶態交互的netlink協議,就需要在初始化時調用本函數,以創建一個屬於內核的netlink套接字
* 之後,只要用戶態發送了一個該協議類型的netlink消息到內核,就會執行本函數傳入的input回調函數
*/
static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}
/* 以下就是創建屬於內核的具體協議的netlink套接字(這個纔是真正的執行函數)
*/
struct sock *__netlink_kernel_create(struct net *net, int unit, struct module *module,struct netlink_kernel_cfg *cfg)
{
// 首先是創建並初始化一個協議類型相關的socket結構
sock_create_lite(PF_NETLINK, SOCK_DGRAM, unit, &sock);
/* 然後是創建並初始化一個協議類型相關的sock結構
* 備註: 這裏要注意的是,首先使用了init_net缺省網絡命名空間來創建sock結構,然後再轉回到當前的網絡命名空間
* 這麼做的原因大概是無法對當前的網絡命名空間執行get_net()操作(__netlink_create->sk_alloc中有調用到)
*/
__netlink_create(&init_net, sock, cb_mutex, unit);
sk = sock->sk;
sk_change_net(sk, net);
// 這裏可以看出,內核默認支持最少32個組播地址
if (!cfg || cfg->groups < 32)
groups = 32;
else
groups = cfg->groups;
// 分配linsters的空間
listeners = kzalloc(sizeof(*listeners) + NLGRPSZ(groups), GFP_KERNEL);
/* 從2.6.24版本開始,netlink_data_ready這個函數被作廢了
* 意味着內核netlink套接字屏蔽了sk_data_ready回調,
* 通觀整個netlink模塊可知,netlink組播消息傳遞過程中會調用到該回調,
* 這也就是說,內核netlink套接字不可能會收到組播消息
*/
sk->sk_data_ready = netlink_data_ready;
// 如果某個netlink協議配置了私有的消息處理函數,就將其註冊到netlink套接字的對應位置
if (cfg && cfg->input)
nlk_sk(sk)->netlink_rcv = cfg->input;
/* 將創建的netlink套接字添加到nl_table對應表項中的hash表中
* 由於本函數創建的都是內核nelink套接字,所以portid 固定爲 0
*/
netlink_insert(sk, net, 0);
// 標記這是個內核主動創建的netlink套接字
nlk = nlk_sk(sk);
nlk->flags |= NETLINK_KERNEL_SOCKET;
/* 判斷該協議類型是否已經註冊過了,如果沒有則在這裏初始化nl_table對應的表項,如果有就不再初始化該表項了
*
* 個人的初步猜測:基於不同的net命名空間,相同協議類型的內核netlink套接字可以對應創建多個
*/
if (!nl_table[unit].registered)
{
nl_table[unit].groups = groups;
rcu_assign_pointer(nl_table[unit].listeners, listeners);
nl_table[unit].cb_mutex = cb_mutex;
nl_table[unit].module = module;
if (cfg)
{
nl_table[unit].bind = cfg->bind;
nl_table[unit].flags = cfg->flags;
if (cfg->compare)
nl_table[unit].compare = cfg->compare;
}
nl_table[unit].registered = 1;
}
else
{
// 多個相同協議類型的內核netlink套接字爲何在這裏取消listeners空間?
kfree(listeners);
nl_table[unit].registered++;
}
/* 至此,屬於內核的跟協議類型相關的netlink套接字創建完成
*/
}
/* 以下就是用戶進程調用socket()創建PF_NETLINK類型的套接字時,由內核創建屬於用戶進程的netlink套接字的過程
*/
static int netlink_create(struct net *net, struct socket *sock, int protocol,int kern)
{
// 首先將套接字狀態標記爲未連接
sock->state = SS_UNCONNECTED;
// netlink接口只支持SOCK_RAW和SOCK_DGRAM這兩種套接字類型
if (sock->type != SOCK_RAW && sock->type != SOCK_DGRAM)
return -ESOCKTNOSUPPORT;
// 檢查協議類型是否有效
if (protocol < 0 || protocol >= MAX_LINKS)
return -EPROTONOSUPPORT;
// 檢查該協議類型的netlink是否已經註冊了,只有nl_table中已經註冊的協議類型才能繼續創建下去
if (nl_table[protocol].registered && try_module_get(nl_table[protocol].module))
module = nl_table[protocol].module;
else
err = -EPROTONOSUPPORT;
// 取出內核netlink創建時註冊的私有鎖和bind函數,接下來就要用到了
cb_mutex = nl_table[protocol].cb_mutex;
bind = nl_table[protocol].bind;
/* 創建並初始化一個協議類型相關的sock結構
*
* 備註:跟創建屬於內核的netlink套接字時不同的是,這裏似乎不再關心傳入的net命名空間
*/
__netlink_create(net, sock, cb_mutex, protocol);
sock_prot_inuse_add(net, &netlink_proto, 1);
// 最後對netlink套接字相關參數進行賦值
nlk = nlk_sk(sock->sk);
nlk->module = module;
nlk->netlink_bind = bind;
/* 至此,屬於用戶進程的跟協議類型相關的netlink套接字創建完成
*/
}
小結: 以上就是內核創建2類netlink套接字的方法,需要注意的一點就是,用戶進程netlink套接字的創建需要依賴對應的內核netlink套接字;
另外,以上2類netlink套接字的創建過程中都調用了__netlink_create這個函數來執行核心的創建過程
/* 以下就是創建並初始化一個netlink_sock結構(其中包含了sock結構)
*/
static int __netlink_create(struct net *net, struct socket *sock,struct mutex *cb_mutex, int protocol)
{
// netlink套接字和socket層netlink協議族的通用操作集合關聯
sock->ops = &netlink_ops;
// 分配基於netlink協議的sock結構
sk = sk_alloc(net, PF_NETLINK, GFP_KERNEL, &netlink_proto);
// 初始化sock的發送接收隊列、數據緩存、等待隊列和互斥鎖等
sock_init_data(sock, sk);
nlk = nlk_sk(sk);
if (cb_mutex)
nlk->cb_mutex = cb_mutex;
else
{
nlk->cb_mutex = &nlk->cb_def_mutex;
mutex_init(nlk->cb_mutex);
}
init_waitqueue_head(&nlk->wait);
sk->sk_destruct = netlink_sock_destruct; // 設置netlink sock結構的析構函數
sk->sk_protocol = protocol;
}
4. 綁定netlink套接字
由於內核netlink套接字在創建時就固定綁在了portid=0的位置,並且內核套接字目前沒有反向監聽組播的功能,所以以下的綁定操作就是針對用戶進程netlink套接字。
/* 用戶進程對netlink套接字調用bing()系統調用後,內核執行netlink操作的總入口函數
* @sock - 要綁定的socket結構,也可以認爲是netlink套接字
* @addr - 要綁定的套接字地址
* @addr_len- 套接字地址長度
*
* 備註: netlink套接字在創建的過程中(具體是在__netlink_create函數開頭),已經和netlink_ops(socket層netlink協議族的通用操作集合)關聯,
* 其中註冊的bind回調就是指向本函數
*/
static int netlink_bind(struct socket *sock, struct sockaddr *addr,int addr_len)
{
/* 如果傳入的套接字地址中指定了要監聽的組播,需要判斷該套接字是否具有監聽組播的權限
*/
if (nladdr->nl_groups)
{
if (!netlink_allowed(sock, NL_CFG_F_NONROOT_RECV))
return -EPERM;
// 爲該套接字分配組播空間
netlink_realloc_groups(sk);
}
/* 需要注意的一點就是,如果netlink套接字已經綁定在一個地址上,就不能再綁到一個新的地址.
* 對於尚未綁定的netlink套接字,這時候如果傳入的套接字地址中指定了要綁定的單播地址就用該地址綁定;
* 如果沒有指定就自動綁定一個
*/
if (nlk->portid)
{
if (nladdr->nl_pid != nlk->portid)
return -EINVAL;
}
else
{
// 綁定netlink套接字的過程,其實就是將其加入nl_table的過程
err = nladdr->nl_pid ? netlink_insert(sk, net, nladdr->nl_pid) :netlink_autobind(sock);
}
// 如果用戶態沒有指定組播地址,並且沒有分配組播的內存,那麼bind工作就到此結束了
if (!nladdr->nl_groups && (nlk->groups == NULL || !(u32)nlk->groups[0]))
return 0;
// 程序運行到這裏意味着用戶態指定了需要綁定的組播地址
// 將指定套接字加入所屬netlink協議類型的多播hash鏈表
netlink_update_subscriptions(sk, nlk->subscriptions +hweight32(nladdr->nl_groups) -hweight32(nlk->groups[0]));
// 保存組播地址mask值到開頭申請的組播空間
nlk->groups[0] = (nlk->groups[0] & ~0xffffffffUL) | nladdr->nl_groups;
// 更新套接字所屬協議類型的監聽集合
netlink_update_listeners(sk);
// 如果具體的netlink協議類型註冊了私有的bind函數,並且用戶態指定了要綁定的組播地址,則在這裏調用該私有的bind函數
if (nlk->netlink_bind && nlk->groups[0])
{
for (i=0; i<nlk->ngroups; i++)
if (test_bit(i, nlk->groups))
nlk->netlink_bind(i);
}
}