Linux 内核通知链和例程代码
概念
大多數內核子系統都是相互獨立的,因此某個子系統可能對其它子系統產生的事件感興趣。為了滿足這個需求,也即是讓某個子系統在發生某個事件時通知其它的子系統,Linux內核提供了通知鏈的機制。通知鏈表只能夠在內核的子系統之間使用,而不能夠在內核與用戶空間之間進行事件的通知。通知鏈表是一個函數鏈表,鏈表上的每一個節點都注冊了一個函數。當某個事情發生時,鏈表上所有節點對應的函數就會被執行。所以對于通知鏈表來說有一個通知方與一個接收方。在通知這個事件時所運行的函數由被通知方決定,實際上也即是被通知方注冊了某個函數,在發生某個事件時這些函數就得到執行。其實和系統調用signal的思想差不多。
代碼位置
include/linux/notifier.h kernel/notifier.c 代碼不超過 1000 行,但是也是因為代碼少,才顯現出大神的厲害之處。
數據結構
Linux 內核通知鏈的操刀大神是 Alan Cox,這個大神也是Linux 協議棧頭部的大boss,我猜測當時主要是用來做網絡方面的通知需要,比如網絡IP發生變化需要通知到其他的子系統等等。像類似的usb插拔等也是用到Linux 內核通知鏈的。
也就是,你想關心我,就提前給我注冊回調函數,我發生情況后,我就告訴你,這就好像你小時候出去玩,出去的時候跟你老媽說,媽媽媽媽,煮好飯的時候要記得叫我哈,你媽煮好飯后就在大門口叫,仔啊,快回家吃飯了。
通知鏈有四種類型:原子通知鏈( Atomic notifier chains ):通知鏈元素的回調函數(當事件發生時要執行的函數)只能在中斷上下文中運行,不允許阻塞。
對應的鏈表頭結構:
struct atomic_notifier_head { spinlock_t lock; struct notifier_block *head; };可阻塞通知鏈( Blocking notifier chains ):通知鏈元素的回調函數在進程上下文中運行,允許阻塞。
對應的鏈表頭:
struct blocking_notifier_head { struct rw_semaphore rwsem; struct notifier_block *head; };原始通知鏈( Raw notifier chains ):對通知鏈元素的回調函數沒有任何限制,所有鎖和保護機制都由調用者維護。
對應的鏈表頭:
struct raw_notifier_head { struct notifier_block *head; };SRCU 通知鏈( SRCU notifier chains ):可阻塞通知鏈的一種變體。
對應的鏈表頭:
struct srcu_notifier_head { struct mutex mutex; struct srcu_struct srcu; struct notifier_block *head; };通知鏈的核心結構:
struct notifier_block { int (*notifier_call)(struct notifier_block *, unsigned long, void *); struct notifier_block *next; int priority; };其中notifier_call是通知鏈要執行的函數指針,next用來連接其它的通知結構,priority是這個通知的優先級,同一條鏈上的notifier_block{}是按優先級排列的。內核代碼中一般把通知鏈命名為xxx_chain, xxx_nofitier_chain這種形式的變量名。
簡單的使用說明
通知鏈的事件和注冊通知鏈的回調是對應的,當對應的事件產生了,發送者就觸發注冊通知鏈的回調函數執行。
運作機制
通知鏈的運作機制包括兩個角色:
?被通知者:?對某一事件感興趣一方。定義了當事件發生時,相應的處理函數,即回調函數。但需要事先將其注冊到通知鏈中(被通知者注冊的動作就是在通知鏈中增加一項)。?通知者:?事件的通知者。當檢測到某事件,或者本身產生事件時,通知所有對該事件感興趣的一方事件發生。他定義了一個通知鏈,其中保存了每一個被通知者對事件的處理函數(回調函數)。通知這個過程實際上就是遍歷通知鏈中的每一項,然后調用相應的事件處理函數。
包括以下過程:
?通知者定義通知鏈。?被通知者向通知鏈中注冊回調函數。?當事件發生時,通知者發出通知(執行通知鏈中所有元素的回調函數)。
被通知者調用 notifier_chain_register 函數注冊回調函數,該函數按照優先級將回調函數加入到通知鏈中:
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; }注銷回調函數則使用 notifier_chain_unregister 函數,即將回調函數從通知鏈中刪除:
static int notifier_chain_unregister(struct notifier_block **nl, struct notifier_block *n) { while ((*nl) != NULL) { if ((*nl) == n) { rcu_assign_pointer(*nl, n->next); return 0; } nl = &((*nl)->next); } return -ENOENT; }通知者調用 notifier_call_chain 函數通知事件的到達,這個函數會遍歷通知鏈中所有的元素,然后依次調用每一個的回調函數(即完成通知動作):
static int __kprobes 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(*nl); while (nb && nr_to_call) { next_nb = rcu_dereference(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)++; if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK) break; nb = next_nb; nr_to_call--; } return ret; }參數nl是通知鏈的頭部,val表示事件類型,v用來指向通知鏈上的函數執行時需要用到的參數,一般不同的通知鏈,參數類型也不一樣,例如當通知一個網卡被注冊時,v就指向net_device結構,nr_to_call表示準備最多通知幾個,-1表示整條鏈都通知,nr_calls非空的話,返回通知了多少個。
每個被執行的notifier_block回調函數的返回值可能取值為以下幾個:
?NOTIFY_DONE:表示對相關的事件類型不關心。?NOTIFY_OK:順利執行。?NOTIFY_BAD:執行有錯。?NOTIFY_STOP:停止執行后面的回調函數。?NOTIFY_STOP_MASK:停止執行的掩碼。Notifier_call_chain()把最后一個被調用的回調函數的返回值作為它的返回值。
舉例應用
在這里,寫了一個簡單的通知鏈表的代碼。實際上,整個通知鏈的編寫也就兩個過程:
首先是定義自己的通知鏈的頭節點,并將要執行的函數注冊到自己的通知鏈中。其次則是由另外的子系統來通知這個鏈,讓其上面注冊的函數運行。這里將第一個過程分成了兩步來寫,第一步是定義了頭節點和一些自定義的注冊函數(針對該頭節點的),第二步則是使用自定義的注冊函數注冊了一些通知鏈節點。分別在代碼buildchain.c與regchain.c中。發送通知信息的代碼為notify.c。
代碼1 buildchain.c。它的作用是自定義一個通知鏈表test_chain,然后再自定義兩個函數分別向這個通知鏈中加入或刪除節點,最后再定義一個函數通知這個test_chain鏈:
#include <asm/uaccess.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/notifier.h> #include <linux/init.h> #include <linux/types.h> #include <linux/module.h> MODULE_LICENSE("GPL"); /* * 定義自己的通知鏈頭結點以及注冊和卸載通知鏈的外包函數 */ /* * RAW_NOTIFIER_HEAD是定義一個通知鏈的頭部結點, * 通過這個頭部結點可以找到這個鏈中的其它所有的notifier_block */ static RAW_NOTIFIER_HEAD(test_chain); /* * 自定義的注冊函數,將notifier_block節點加到剛剛定義的test_chain這個鏈表中來 * raw_notifier_chain_register會調用notifier_chain_register */ int register_test_notifier(struct notifier_block *nb) { return raw_notifier_chain_register(&test_chain, nb); } EXPORT_SYMBOL(register_test_notifier); int unregister_test_notifier(struct notifier_block *nb) { return raw_notifier_chain_unregister(&test_chain, nb); } EXPORT_SYMBOL(unregister_test_notifier); /* * 自定義的通知鏈表的函數,即通知test_chain指向的鏈表中的所有節點執行相應的函數 */ int test_notifier_call_chain(unsigned long val, void *v) { return raw_notifier_call_chain(&test_chain, val, v); } EXPORT_SYMBOL(test_notifier_call_chain); /* * init and exit */ static int __init init_notifier(void) { printk("init_notifier\n"); return 0; } static void __exit exit_notifier(void) { printk("exit_notifier\n"); } module_init(init_notifier); module_exit(exit_notifier);代碼2 regchain.c。該代碼的作用是將test_notifier1 test_notifier2 test_notifier3這三個節點加到之前定義的test_chain這個通知鏈表上,同時每個節點都注冊了一個函數:
#include <asm/uaccess.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/notifier.h> #include <linux/init.h> #include <linux/types.h> #include <linux/module.h> MODULE_LICENSE("GPL"); /* * 注冊通知鏈 */ extern int register_test_notifier(struct notifier_block*); extern int unregister_test_notifier(struct notifier_block*); static int test_event1(struct notifier_block *this, unsigned long event, void *ptr) { printk("In Event 1: Event Number is %d\n", event); return 0; } static int test_event2(struct notifier_block *this, unsigned long event, void *ptr) { printk("In Event 2: Event Number is %d\n", event); return 0; } static int test_event3(struct notifier_block *this, unsigned long event, void *ptr) { printk("In Event 3: Event Number is %d\n", event); return 0; } /* * 事件1,該節點執行的函數為test_event1 */ static struct notifier_block test_notifier1 = { .notifier_call = test_event1, }; /* * 事件2,該節點執行的函數為test_event1 */ static struct notifier_block test_notifier2 = { .notifier_call = test_event2, }; /* * 事件3,該節點執行的函數為test_event1 */ static struct notifier_block test_notifier3 = { .notifier_call = test_event3, }; /* * 對這些事件進行注冊 */ static int __init reg_notifier(void) { int err; printk("Begin to register:\n"); err = register_test_notifier(&test_notifier1); if (err) { printk("register test_notifier1 error\n"); return -1; } printk("register test_notifier1 completed\n"); err = register_test_notifier(&test_notifier2); if (err) { printk("register test_notifier2 error\n"); return -1; } printk("register test_notifier2 completed\n"); err = register_test_notifier(&test_notifier3); if (err) { printk("register test_notifier3 error\n"); return -1; } printk("register test_notifier3 completed\n"); return err; } /* * 卸載剛剛注冊了的通知鏈 */ static void __exit unreg_notifier(void) { printk("Begin to unregister\n"); unregister_test_notifier(&test_notifier1); unregister_test_notifier(&test_notifier2); unregister_test_notifier(&test_notifier3); printk("Unregister finished\n"); } module_init(reg_notifier); module_exit(unreg_notifier);代碼3 notify.c。該代碼的作用就是向test_chain通知鏈中發送消息,讓鏈中的函數運行:
#include <asm/uaccess.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/notifier.h> #include <linux/init.h> #include <linux/types.h> #include <linux/module.h> MODULE_LICENSE("GPL"); extern int test_notifier_call_chain(unsigned long val, void *v); /* * 向通知鏈發送消息以觸發注冊了的函數 */ static int __init call_notifier(void) { int err; printk("Begin to notify:\n"); /* * 調用自定義的函數,向test_chain鏈發送消息 */ printk("==============================\n"); err = test_notifier_call_chain(1, NULL); printk("==============================\n"); if (err) printk("notifier_call_chain error\n"); return err; } static void __exit uncall_notifier(void) { printk("End notify\n"); } module_init(call_notifier); module_exit(uncall_notifier);Makefile文件:
ifneq ($(KERNELRELEASE),) EXTRA_CFLAGS = -Wall -g obj-m := buildchain.o regchain.o notify.o else PWD := $(shell pwd) KVER := $(shell uname -r) KDIR := /lib/modules/$(KVER)/build all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions endif執行結果:
掃碼或長按關注
回復「?加群?」進入技術群聊
總結
以上是生活随笔為你收集整理的Linux 内核通知链和例程代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深圳数据分析认证(CPDA)含金量怎么样
- 下一篇: 波士顿房价数据集