首页 > 系统相关 >Linux内核学习-通知链

Linux内核学习-通知链

时间:2023-06-14 15:02:23浏览次数:40  
标签:nl 通知 nb STOP call NOTIFY Linux notifier 内核

前言

内核中有许多子系统,他们相互独立,但又具有很强的依赖性。因此其中一个子系统侦测到的或者产生的事件其他子系统可能都有兴趣,那么为了实现这样的交互需求,Linux使用了所谓的通知链(notification chain)。


本博客包含的主要内容

1.通知链如何声明以及内核代码定义了那些链(chain)。

2.内核子系统如何向通知链注册。

内核子系统如何在链上产生通知消息。


正文


为什么需要通知链?

考虑下面一个场景或者例子。假设你的所在的网络D有一台路由器RT可以直接访问网络B和网络C,又可以通过网络B或者网络C访问到网络A。这样的一个拓扑结构。最初通过网络C这条路径访问网络A是因为这条路径相对来说成本比较少,但是现在网络C这条路径坏了无法通过这条路径访问A了因此路由表应该更新网络A路由改走网络B,这种决策基础包括一些本地主机事件,例如设备的删除和注册,以及路由配置中的复杂因素和所用路由协议,在任何情况下路由表的路由子系统必须从其他子系统那里收到相关信息的通知,因而产生了通知链的需求。


通知链

通知链本质上是一份简单的函数列表,简单理解就是一系列执行不同事件的函数,每个函数都让另外一个子系统知道,调用这个函数的这个系统发生了什么事件。也就意味着通知链有被动端(被通知者)和主动端(通知者),也就是所谓的发布-订阅模型。


被通知者:就是要接受某个事件通知的子系统,他来提供call函数。

通知者:就是将事件传给被通知者,他调用call函数来通知。


这两种角色是相对的,其实每个子系统既是通知者也是被通知者。子系统自己应该明确对其他子系统那些事件感兴趣,并且自己知道的事件有哪几种,其他子系统可能感兴趣的又有哪些。

在内核中的设计其实包括三个部分,第一个是定义了一个链,只不过这个链条存的是一个一个的call函数。第二是想着个链条添加函数。第三就是执行这个链条中的每个call函数。


定义链

先给出内核代码:include/linux/notifier.h

struct notifier_block;

typedef	int (*notifier_fn_t)(struct notifier_block *nb,
			unsigned long action, void *data);

struct notifier_block {
	notifier_fn_t notifier_call;
	struct notifier_block __rcu *next;
	int priority;
};

链列表元素的类型为 notifier_block 其中:

notifier_call是要执行的函数

next用于指向链表中的下一个元素

priority代表的是该函数的优先级,较高的优先级会被优先执行。但是在实际中,注册时几乎都不管这个priority,执行的次序仅仅依赖于注册的次序。

定义这个链的目的可以理解成定义了一连串的连续执行的函数执行完一个执行下一个。


链注册

还是先给出代码:kernel/notifier.c

//注册
static int notifier_chain_register(struct notifier_block **nl,
				   struct notifier_block *n,
				   bool unique_priority)
{
	while ((*nl) != NULL) {
		if (unlikely((*nl) == n)) {
			WARN(1, "notifier callback %ps already registered",
			     n->notifier_call);
			return -EEXIST;
		}
		if (n->priority > (*nl)->priority)
			break;
		if (n->priority == (*nl)->priority && unique_priority)
			return -EBUSY;
		nl = &((*nl)->next);
	}
	n->next = *nl;
	rcu_assign_pointer(*nl, n);
	trace_notifier_register((void *)n->notifier_call);
	return 0;
}
//注销
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);
			trace_notifier_unregister((void *)n->notifier_call);
			return 0;
		}
		nl = &((*nl)->next);
	}
	return -ENOENT;
}

注册的目的是:当一个内核组建对给定通知链条的事件感兴趣时,可以用这个

int notifier_chain_register()

予以注册,当然这个函数很少被直接调用,都是调用在内核中提供的一些内含notifier_chain_register的包裹函数,例如跟网络有关的

可用于向inetaddr_chain、inet6addr_chain、netdev_chain注册的

register_inetaddr_notifier、register_inet6addr_notifier、register_netdevice_notifier。

给一个例子:

//如下函数可用于向netdev_chain注册
int register_netdevice_notifier(struct notifier_Block *nb){
					return notifier_chain_register(&netdev_chain,nb);
}

根据代码不难看出,对于每条链条,notifier_block实体被插入到一个按优先级排序的列表,相同优先级元素按照时间顺序插入,新的元素在最底下。

//如果n的优先级比*nl的优先级高那么循环结束
if (n->priority > (*nl)->priority)
			break;
//否则*nl继续向后移 继续进入循环比较
nl = &((*nl)->next);

//如果n的优先级比*nl大了就讲n插入到*nl的前面
n->next = *nl;
rcu_assign_pointer(*nl, n);//根据别的博客看到的讲解这个函数等价于*nl=n

注销的话就相对简单

//循环判断找到了要注销的然后执行注销
//从链表中移除。
if ((*nl) == n) {
			rcu_assign_pointer(*nl, n->next);
			trace_notifier_unregister((void *)n->notifier_call);
			return 0;
		}
		nl = &((*nl)->next);

链上的通知事件

还是先给出内核的代码:kernel/notifier.c


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
		trace_notifier_run((void *)nb->notifier_call);
		ret = nb->notifier_call(nb, val, v);

		if (nr_calls)
			(*nr_calls)++;

		if (ret & NOTIFY_STOP_MASK)
			break;
		nb = next_nb;
		nr_to_call--;
	}
	return ret;
}

这个函数的主要功能是按照优先级次序调用对此链注册的所有回调函数。值得注意的是,回调函数是在调用notifier_call_chain的进程上下文(context)中执行的。

代码中的核心是:

	int ret = NOTIFY_DONE;//返回值指调用回调函数之后的结果。
	struct notifier_block *nb, *next_nb; //执行的链条

	while (nb && nr_to_call) {
    	ret = nb->notifier_call(nb, val, v);//执行过后返回一个执行的结果
    
		if (nr_calls)
			(*nr_calls)++;

		if (ret & NOTIFY_STOP_MASK)
            //这里判定结果如果成功了这个ret & NOTIFY_STOP_MASK值为假不会跳出循环
            //否则跳出循环
			break;
		nb = next_nb;//继续执行下一个
    }

参数的含义:

nl

指的通知链

val

事件类型。链本身标识的一组事件,val明确标识一种事件类型

v

此参数可由各种各样的客户所注册的处理函数使用,在不同情况下可以有不同用途。例如当一个新的网络设备在内核注册时,相关通知信息会使用v以标识net_device数据结构。

nr_to_call 

要调用的通知程序函数数量。如果不需要将此参数的值为 -1。

nr_calls 

记录发送的通知数。不需要的话将此字段的值为 NULL。

ret

代表的返回值也在include/linux/notifier.h给出了定义

#define NOTIFY_DONE		0x0000		/* Don't care */
#define NOTIFY_OK		0x0001		/* Suits me */
#define NOTIFY_STOP_MASK	0x8000		/* Don't call further */
#define NOTIFY_BAD		(NOTIFY_STOP_MASK|0x0002)
						/* Bad/Veto action */
/*
 * Clean way to return from the notifier and stop further calls.
 */
#define NOTIFY_STOP		(NOTIFY_OK|NOTIFY_STOP_MASK)

每调用一个函数都要有个返回值来表示执行结果,内核代码中定义了

NOTIFY_DONE:对通知信息不敢兴趣

NOTIFY_OK:通知信息被正确处理

NOTIFY_STOP_MASK:此标识由notifier_call_chain检查,以了解是否停止调用回调函数,或者继续调用。NOTIFY_BAD、NOTIFY_STOP都在其定义中包括了此标识。

NOTIFY_BAD: 有些事情出错。停止调用此事件的回调函数

NOTIFY_STOP:函数被正确调用,然而此事件不需要进一步调用其他回调函数。

    int ret0=NOTIFY_DONE	;
    int ret1=NOTIFY_OK;
    int ret2=NOTIFY_STOP_MASK;
    int ret3=NOTIFY_BAD;
    int ret4=NOTIFY_STOP;

    printf("%x\n",ret0 &NOTIFY_STOP_MASK);//0
    printf("%x\n",ret1 &NOTIFY_STOP_MASK);//0
    printf("%x\n",ret2 &NOTIFY_STOP_MASK);//8000
    printf("%x\n",ret3 &NOTIFY_STOP_MASK);//8000
    printf("%x\n",ret4 &NOTIFY_STOP_MASK);//8000

我手动模拟将

ret & NOTIFY_STOP_MASK 

的值表示出来,如果成功执行这个表达式为假,没有成功执行表达式为真,就会跳出循环。不再执行此链条的回调函数。


那么以上就是对通知链的学习和分享。有问题欢迎大家评论区一起讨论。


标签:nl,通知,nb,STOP,call,NOTIFY,Linux,notifier,内核
From: https://blog.51cto.com/u_16160587/6477257

相关文章

  • 2023年天博&天柏端午节放假通知
    亲爱的伙伴们2023年端午节将至,根据国务院办公厅放假规定,并结合公司实际情况,现将端午节放假安排通知如下放假时间:2023年6月22日至2023年6月24日共3天25号上班。端午节,是集拜神祭祖、祈福辟邪、欢庆娱乐和饮食为一体的民俗大节。各地习俗各有不同。主要有划龙舟、祭龙、采草药、挂艾......
  • RockyLinux9设置静态IP地址和主机名
    Rocky9.2使用体验2022年1月31日,CentOSLinux8支持服务已经正式停止。CentOSLinux7(简称CentOS7)也将于2024年6月30日停止维护。Rocky和Almalinux都可以作为CentOS的替代者,都是完全兼容RHEL的Linux发行版。本文使用vmwareworkstation15安装测试Rocky9.21、官网下载RockyLin......
  • Linux - MySQL修改临时密码并设置访问权限【Linux】
    1.查阅临时密码cat/var/log/mysqld.log|greppassword2.登录MySQL①.登录mysql(复制日志中的临时密码登录)mysql-uroot-p输入临时密码②.修改密码setglobalvalidate_password_length=4;设置密码长度最低位数setglobalvalidate......
  • java开发系统内核:使用一个中断实现多个API调用
    在上一节,我们实现了通过中断访问内核API的功能,本节,我们进一步改进中断调用内核API的机制。当前,我们使用一个中断来对应一个API,问题是内核导出的API不可能只有一个,如果始终保持一个中断对应一个API的话,那么CPU只支持两百多个中断,也就是说,按照上一节的办法,我们内核最多只能导出两百......
  • linux GDB高级调试
    gdb-v查看版本 CppCon2015:GregLaw'Giveme15minutes&I'llchangeyourviewofGDB'       https://undo.io/resources/cppcon-2015-greg-law-give-me-15-minutes-ill-change/视频不行可以看下面说明gcc-ghello.c-ogdba.out ctrl+x+actrl......
  • java开发系统内核:应用程序与系统内核的内存隔离
    当前,我们可以开发运行在系统上的应用程序了,接下来的问题是如何保护系统内核免受恶意应用程序的危害。恶意程序要想侵犯系统,主要路径有两条,一是让内核执行它的代码,而是修改内核数据,通过修改数据改变内核的行为。我们看看,如何预防恶意程序侵入到系统内核的数据区域中。无论是内核还是......
  • Linux简单命令
    Linux统分为两种:RedHat系列,包含Redhat、Centos、Fedora等,RedHat系列的包管理工具是yum,因而,我们可以使用如下命令安装:sudoyuminstallxxxDebian系列,包含Debian、Ubuntu等,Debian系列的包管理工具是apt-get,因而,我们可以使用如下命令安装:sudoapt-getinstallxxx......
  • java开发操作系统内核:由实模式进入保护模式之32位寻址
    从时模式到保护模式,是计算法技术跨时代的发展。大家想想笨拙的Dos界面,黑底白字的那种冷漠界面到win95各种色彩斑斓的窗口,两者之间的区别其实就是实模式和保护模式的天壤之别。保护模式中,最重要的一个概念莫过于”保护”二字,有了“保护”功能后,CPU为软件提供了很多的功能,当然也有了......
  • java开发操作系统内核:让内核突破512字节的限制
    我们当前的系统内核,必须包含在虚拟软盘的第1扇区,由于一个扇区只有512字节,因此,系统内核的大小不可能超过512字节。但是,一个拥有完善功能的内核不可能只有512字节,因此要想越过512字节的限制,具体的做法就是做一个内核加载器,放入到第一扇区,加载器加载如内存后,再将内核从软盘加载到系统......
  • 用java做操作系统内核:软盘读写
    在前两节,我们将一段代码通过软盘加载到了系统内存中,并指示cpu执行加入到内存的代码,事实上,操作系统内核加载也是这么做的。只不过我们加载的代码,最大只能512byte,一个操作系统内核,少说也要几百兆,由此,系统内核不可能直接从软盘读入系统内存。通常的做法是,被加载进内存的512Byte程......