一、通知鏈簡介
舉個形象的例子:將通知鏈比喻成”訂閱者-發布者“,訂閱者將感興趣的公眾號關注并設置提醒,發布者一旦發布某個文章,訂閱者即可收到通知看到發布的內容。
在linux內核中為了及時響應某些到來的事件,采取了通知鏈機制。該機制的兩個角色的任務:
1、通知者定義通知鏈
2、被通知者向通知鏈中注冊回調函數
3、當事件發生時,通知者發送通知 (執行通知鏈上每個調用塊上的回調函數)所以通知鏈是一個單鏈表,單鏈表上的節點是調用塊,每個調用塊上有事件相關的回調函數和調用塊的優先級。當事件觸發時會按優先級順序執行該鏈表上的回調函數。通知鏈只用于各個子系統之間,不能用于內核和用戶空間進行事件的通知。
二、相關細節
1、通知鏈的類型
原子通知鏈( Atomic notifier chains ):
通知鏈元素的回調函數(當事件發生時要執行的函數)只能在中斷上下文中運行,不允許阻塞。
可阻塞通知鏈( Blocking notifier chains ):
通知鏈元素的回調函數在進程上下文中運行,允許阻塞。
原始通知鏈( Raw notifier chains ):
對通知鏈元素的回調函數沒有任何限制,所有鎖和保護機制都由調用者維護。
SRCU 通知鏈( SRCU notifier chains ):可阻塞通知鏈的一種變體
本文將以原子通知鏈進行分析
2、原子通知鏈與通知塊
struct raw_notifier_head {
struct notifier_block __rcu *head;
};
初始化一個原子通知鏈使用以下宏定義
#define RAW_NOTIFIER_HEAD(name)
struct raw_notifier_head name =
RAW_NOTIFIER_INIT(name)
#define RAW_NOTIFIER_INIT(name) {
.head = NULL }
例如創建一個設備通知鏈隊列頭:
RAW_NOTIFIER_HEAD.NETdev_chain)
struct raw_notifier_head就相當于存放這條通知鏈單鏈表頭,每一個通知鏈上的元素也就是通知塊如下定義:
struct notifier_block {
notifier_fn_t notifier_call; //通知調用的函數
struct notifier_block __rcu *next;//指向下一個通知節點,從而形成鏈隊
int priority;//優先級,會根據優先級在單鏈表中排序
};
回調函數接口:
typedef int (*notifier_fn_t)(struct notifier_block *nb,
unsigned long action, void *data);
整個通知鏈的組織如下圖所示:
3、向通知鏈中插入通知塊
int raw_notifier_chain_register(struct raw_notifier_head *nh,
struct notifier_block *n)
{
return notifier_chain_register(&nh->head, n);
}
static int notifier_chain_register(struct notifier_block **nl,
struct notifier_block *n)
{
//循環遍歷通知鏈
while ((*nl) != NULL) {
if (n->priority > (*nl)->priority)//按照優先級插入通知鏈表
break;
nl = &((*nl)->next);
}
n->next = *nl;
rcu_assign_pointer(*nl, n);
return 0;
}
4、調用通知鏈
int raw_notifier_call_chain(struct raw_notifier_head *nh,
unsigned long val, void *v)
{
return __raw_notifier_call_chain(nh, val, v, -1, NULL);
}
int __raw_notifier_call_chain(struct raw_notifier_head *nh,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
return notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls);
}
static int notifier_call_chain(struct notifier_block **nl,
unsigned long val, void *v,
int nr_to_call, int *nr_calls)
{
int ret = NOTIFY_DONE;
struct notifier_block *nb, *next_nb;
nb = rcu_dereference_raw(*nl);
//循環遍歷調用鏈上的調用塊
while (nb && nr_to_call) {
next_nb = rcu_dereference_raw(nb->next);
#ifdef CONFIG_DEBUG_NOTIFIERS
if (unlikely(!func_ptr_is_kernel_text(nb->notifier_call))) {
WARN(1, "Invalid notifier called!");
nb = next_nb;
continue;
}
#endif
//執行該調用塊的回調函數
ret = nb->notifier_call(nb, val, v);
if (nr_calls)
(*nr_calls)++;
//如果該調用塊的回調函數返回值為NOTIFY_STOP_MASK則跳出調用鏈的遍歷,也就不執行后面的調用塊的回調函數了
if (ret & NOTIFY_STOP_MASK)
break;
nb = next_nb;
nr_to_call--;
}
return ret;
}
三、編寫內核模塊進行實驗
1、案例1
編寫內核模塊作為被通知者,向內核netdev_chain通知鏈中插入自定義通知塊(在通知塊中自定義事件觸發的回調函數),源碼如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/netdevice.h>
#include <linux/inetdevice.h>
//處理網絡設備的啟動與禁用等事件
int test_netdev_event(struct notifier_block *this, unsigned long event, void *ptr)
{
struct net_device *dev = (struct net_device *)ptr;
switch(event)
{
case NETDEV_UP:
if(dev && dev->name)
printk("dev[%s] is upn",dev->name);
break;
case NETDEV_DOWN:
if(dev && dev->name)
printk("dev[%s] is downn",dev->name);
break;
default:
break;
}
return NOTIFY_DONE;
}
struct notifier_block devhandle={
.notifier_call = test_netdev_event
};
static int __init test_init(void)
{
/*
在netdev_chain通知鏈上注冊消息塊
netdev_chain通知鏈是內核中用于傳遞有關網絡設備注冊狀態的通知信息
*/
register_netdevice_notifier(&devhandle);
return 0;
}
static void __exit test_exit(void)
{
unregister_netdevice_notifier(&devhandle);
return;
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
Makefile:
obj-m:=Demo.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
將模塊插入內核后,將網卡關閉再重啟一次,查看日志信息:
dx@ubuntu:~/Linux_Sys_code/Notice/Module3$sudo insmod Demo.ko
dx@ubuntu:~/Linux_Sys_code/Notice/Module3$ dmesg
[24309.137937] inet[00000000baf272e6] is down
[24313.046209] inet[00000000baf272e6] is up
2、案例2
通過寫兩個內核模塊,其中一個作為通知者一個作為被通知者
module_1.c:
- 初始化一個通知鏈
- 定義事件的回調函數并向通知鏈中插入三個通知塊(與之前定義的回調函數相對應)
- 測試通知鏈:循環遍歷通知鏈的通知塊,并同時調用對應的回調函數
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/notifier.h>
/*
模塊功能:1、初始化一個通知鏈
2、定義事件的回調函數并向通知鏈中插入三個通知塊(與之前定義的回調函數相對應)
3、測試通知鏈:循環遍歷通知鏈的通知塊,并同時調用對應的回調函數
*/
static RAW_NOTIFIER_HEAD(test_chain_head);
EXPORT_SYMBOL_GPL(test_chain_head);
//通知塊1的執行函數
static int A_call(struct notifier_block *nb, unsigned long event, void *v)
{
printk("AAAAAAAAAA---------event_A occur!---------AAAAAAAAAAn");
printk("my priority:%dn",nb->priority);
return NOTIFY_DONE;
}
//通知塊1:testA
static struct notifier_block testA = {
.notifier_call = A_call,
.priority = 7,
};
//通知塊2的執行函數
static int B_call(struct notifier_block *nb, unsigned long event, void *v)
{
printk("BBBBBBBBBB---------event_B occur!---------BBBBBBBBBn");
printk("my priority:%dn",nb->priority);
return NOTIFY_STOP_MASK;
}
//通知塊2:testB
static struct notifier_block testB = {
.notifier_call = B_call,
.priority = 9,
};
//通知塊1的執行函數
static int C_call(struct notifier_block *nb, unsigned long event, void *v)
{
printk("CCCCCCCCCC---------event_c occur!---------CCCCCCCCCCn");
printk("my priority:%dn",nb->priority);
return NOTIFY_DONE;
}
static struct notifier_block testC = {
.notifier_call = C_call,
.priority = 6,
};
static int __init my_register(void)
{
printk("----------register notice chain---------n");
raw_notifier_chain_register(&test_chain_head,&testA);
raw_notifier_chain_register(&test_chain_head,&testB);
raw_notifier_chain_register(&test_chain_head,&testC);
printk("----------register notice chain done---------n");
//遍歷已經注冊的調用鏈
struct notifier_block *nb, *next_nb;
struct raw_notifier_head *tmp = &test_chain_head;
struct notifier_block *head = tmp->head;
nb = rcu_dereference_raw(head);
printk("----Test registed notice call----n");
//循環遍歷調用鏈,測試一下所插入的通知塊
while (nb) {
int ret = NOTIFY_DONE;
int index=0;
next_nb = rcu_dereference_raw(nb->next);
printk("notice%d fun:%p,priority:%d",++index,nb->notifier_call,nb->priority);
ret = nb->notifier_call(nb, 1, NULL); //調用注冊的回調函數
nb = next_nb;
}
printk("--------------Module_1 test end-------------n");
return 0;
}
static void __exit my_unregister(void)
{
raw_notifier_chain_unregister(&test_chain_head,&testA);
raw_notifier_chain_unregister(&test_chain_head,&testB);
raw_notifier_chain_unregister(&test_chain_head,&testC);
}
module_init(my_register);
module_exit(my_unregister);
MODULE_AUTHOR("Dong Xu");
MODULE_LICENSE("GPL");
module_2.c:模擬某事件發生,并調用通知鏈
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/notifier.h>
/*
模塊功能:模擬某事件發生,并調用通知鏈.
*/
extern struct raw_notifier_head test_chain_head;
//某事件
static int event(unsigned long val)
{
int ret = raw_notifier_call_chain(&test_chain_head,val,NULL);
return notifier_to_errno(ret);
}
static int __init my_entry(void)
{
event(666);//模擬某事件發生
return 0;
}
static void __exit my_exit(void)
{
printk("test endn");
}
module_init(my_entry);
module_exit(my_exit);
MODULE_AUTHOR("Dong Xu");
MODULE_LICENSE("GPL");
(module_1與module_2的Makefile可參考上面的Demo1)
運行時先插入module_1再插入module_2結果如下,紅框內是module_1中的測試輸出日志,綠框內為世界調用通知鏈時的執行結果日志。
從上面可以看到通知鏈的執行順序是按照優先級進行的,那么當調用通知鏈時是否每個通知塊上的回調函數都會執行呢?
答案:不是,每個被執行的notifier_block回調函數的返回值可能取值以下幾個:
- NOTIFY_DONE:表示對相關的事件類型不關心。
- NOTIFY_OK:順利執行。
- NOTIFY_BAD:執行有錯。
- NOTIFY_STOP:停止執行后面的回調函數。
- NOTIFY_STOP_MASK:停止執行的掩碼
如當返回值NOTIFY_STOP_MASK會停止執行后面優先級低的調用塊的函數。
例如把module_1中通知塊的回調函數B_call的返回值修改為NOTIFY_STOP_MASK后,重新編譯,運行結果如下,只執行了調用鏈中調用塊2的回調函數。