嵌入式Linux网络体系结构设计与TCP/IP协议栈(二)
第3章 网络设备在内核中的抽象——struct net_device 数据结构
- 网络设备是最终将数据发送到网络上的终点设备,也是从网络上接收数据包的源头
- Linux将网络设备抽象为struct net_device数据结构,根据不同的网络设备初始化为不同的属性
- dev->open,dev->hard_start_xmit可以依照网络设备的不同,初始化为不同的设备驱动函数
3.1 协议栈与网络设备
协议栈软件与网络设备硬件之间的接口要满足如下要求:
- 抽象出网络适配器的硬件特性
- 为协议栈提供统一的调用接口
struct net_device
- 是网络设备在内核中的表示,包含硬件属性、网络中与设备有关的上层协议栈的配置信息
- 是上层的网络协议和硬件之间的通用接口,将网络协议层的实现从具体的网络硬件中抽象出来,独立于硬件设备。
3.2 struct net_device数据结构
- name[IFNAME] 网络设备名
- name_hlist 以网络设备名为关键字建立的哈希链表
- *ifalias 网络设备别名
- mem_end/mem_start 设备和内核之间通信的共享存储区
- base_addr 设备上存储区映射到内存地址范围的I/O起始地址
- irq 中断号
- if_port 设置设备的接口类型
- dma Direct Memory Access直接存储器访问通道号
- state 网络设备状态
- dev_list 网络设备实例链表
- napi_list 支持NAPI功能的网络设备组成的链表
- features 硬件特性
- ifindex 唯一标识网络设备的索引号;iflink 标识在虚拟设备中实际完成数据传送的网络设备
- stats 网络设备工作状态统计(rx_packets/tx_packets/rx_bytes/tx_bytes)
- *wireless_handlers/*wireless_data 无线网络设备
- *netdev_ops 网络设备驱动程序实现的功能函数
- *ethtool_ops 控制网络接口的工具函数
- *get_settings 获取网络设备设置
- *set_setings 设置网络设备
- header_ops 函数指针,指向操作数据链路层协议头的功能函数组
- flags 网络设备的工作模式
- gflags 无用
- priv_flags 对用户空间不可见的标志位
- paddded 用alloc_netdev函数为网络设备分配内存空间时,应该补多少个零
- operstate 网络操作状态
- link_mode RFC2863兼容状态的策略
- mtu 网络接口最大传送单元,代表网络设备一次可以处理的最大字节数
- type 网络设备类型
- hard_header_len 某种类型网络设备可传送的数据帧头信息长度
- needed_headroom/needed_tailroom Socket Buffer中预留的额外headroom和tailroom的空间
- *master 网络设备组
- per_addr[MAX_ADDR_LEN]网络设备的硬件地址
- dev_id 共享网卡设备号
- addr_list_lock 访问地址列表时防止并发访问的锁
- *uc_list uc_count 网络设备的主机网络地址列表
- last_rx 最近一次接收到数据包的时间
- dev_addr[MAX_ADDR_LEN]数据链路层地址
- rx_queue 网络设备接收数据包的队列和发送数据包队列
- num_tx_queues 发送队列个数
- real_num_tx_queues 当前可用发送队列数
- tx_queue_len 发送队列的长度
- watchdog_timeo 传送超时设置
- watchdog_timer 传送超时时钟
- refcnt 引用计数
- struct device dev 网络设备在/sys文件系统下 class/net/name 目录下的入口
3.3 函数指针
int (*init)(struct net_device *dev)
网络设备初始化:
- 创建struct net_device 数据结构实例
- 初始化net_device相关数据域
- 向内核注册设备
void (*uninit)(struct net_device *dev)//注销设备
void (*open)(struct net_device *dev)//初始化
void (*stop)(struct net_device *dev)//停止设备
int (*hard_start_xmit)(struct sk_buff *skb, struct net_device *dev);//数据包放入发送缓冲区
void (*tx_timeout)(struct net_device *dev);//错误处理函数
int (*poll)(struct net_device *dev, int *quota)//轮询处理收包
void (*poll_controller)(struct net_device *dev)
u16 (*select_queue)(struct net_device *dev, struct sk_buff *skb);//选择发送队列
...
...
第4章 网络设备在Linux内核中识别
网络设备初始化流程:
- 分配网络设备数据结构实例的内存空间
- 初始化数据结构,初始化设备的发送队列,建立函数指针
- 注册设备
- 初始化其他特殊操作
内核启动时的重要任务之一是首先为内核的各子系统建立运行的共享资源:内存管理、中断、时钟子系统。
在内核启动的早期阶段,初始化程序可分为两大部分:
- 各种典型的和必须的子系统初始化。
- 其他组件初始化
4.1 命令行参数
内核启动允许用户通过bootloader给内核传送各种配置参数,这称为命令行参数。
- bootloader中命令行提供参数: 参数名 = 值
- U-boot通过设置环境变量指定传给内核的命令行参数
- 如何定义命令行参数
使用__setup
宏指定可接收的命令行参数,并注册到系统中
__setup(string, function)
/*
* srting --命令行参数的字符串
* function --解析和处理该命令行参数的函数
*/
eg:
static int __init my_function(char *str)
{
...
}
__setup("netdev=",my_function);
- 命令行参数的解析:通过parse_args完成解析
parse_args函数的功能是:以重启动程序(boot loader)处接收到的命令行参数名作为关键字,用此关键字在内核中注册的命令行参数中查找,一旦找到匹配的命令行参数,就执行该命令行参数注册的处理函数。赋给命令行参数的“值”是命令行参数处理函数的输入参数。如果没找到匹配的命令行参数,在内核启动结束时,最终将该关键字传给第一个用户进程init来处理。
start_kernel分两次调用函数parse_args,一次是间接调用,一次是直接调用,这是因为内核启动时配置选项分为以下两大类:
(1) 早期选项
内核启动期间,有些选项必须优先于其他选项,用early_param宏来定义这类选项,这些选项由parse_early_param函数调用函数parse_args来解析,用宏early_param注册的命令行参数与用宏__setup注册的命令行参数,唯一不同的是前者有一个特殊的标志“early”
(2) 默认选项
大部分命令行参数都是默认选项,由__setup定义并在第二次调用parse_args函数时解析。
- 命令行参数的存放和管理
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
/*
* Only for really core code. See moduleparam.h for the normal way.
*
* Force the alignment so the compiler doesn't space elements of the
* obs_kernel_param "array" too far apart in .init.setup.
*/
#define __setup_param(str, unique_id, fn, early) \
static char __setup_str_##unique_id[] __initdata = str; \
static struct obs_kernel_param __setup_##unique_id \
__attribute_used__ \
__attribute__((__section__(".init.setup"))) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
/* NOTE: fn is as per module_param, not __setup! Emits warning if fn
* returns non-zero. */
#define early_param(str, fn) \
__setup_param(str, fn, fn, 1)
’ # ’ 的功能是将其后面的宏参数进行字符串化操作
’ ## '是连接符,前加##或后加##,将标记作为一个合法的标识符的一部分,不是字符串.多用于多行的宏定义中。
定义:#define __initdata attribute ((section (".data.init")))
注释:这个标志符和变量声明放在一起,表示gcc编译器在编译的时候需要把这个变量放在.data.init section中,而这个section在内核完成初始化之后,会被释放掉。
attribute_used
This attribute, attached to a function, means that code must be emitted for the function even if it appears that the function is not referenced. This is useful, for example, when the function is referenced only in inline assembly. 表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
__setup_param宏将所有obs_kernel_param实例放入由.init.setup标志的段内,这样做:
- 便于遍历以关键字“str”为索引的所有命令行参数。
- 内核在初始化后可以快速释放这个区域中数据结构所占用的内存空间。
调用parse_args函数做第二遍解析的命令行参数会复制到__start_param和__stop_param之间的区域中,这部分区域在内核初始化结束后不会释放,通过/sys文件系统向用户空间输出,供应用程序使用。
网络子系统命令行参数解析例子:
net/core/dev.c -> netdev_boot_set实现命令行参数解析
int __init netdev_boot_setup(char *str)
{
int ints[5];
struct ifmap map;
str = get_options(str, ARRAY_SIZE(ints), ints);
if(!str || !*str)
return 0;
memset(&map , 0 , sizeof(map));
if(ints[0] > 0)
map.irp = ints[1];
if(ints[0] >1)
map.base_addr = ints[2];
if(ints[0] > 2)
map.mem_start =ints[3];
if(ints[0] > 3)
map.mem_end = ints[4];
return netdev_boot_setup_add(str, &map);//以设备名为索引保存在struct netdev_boot_setup数据结构型的数组dev_boot_setup[NETDEV_BOOT_SETUP_MAX]中,最大设备数8个,
}
__setup("netdev=", netdev_boot_setup);
在内核启动后期,网络子系统通过netdev_boot_setup_check查看数组dev_boot_setup[NETDEV_BOOT_SETUP_MAX]是否有硬件配置参数,如果不为空,则将数据填入到网络设备struct net_device dev实例的数据结构中。
4.2 内核启动过程
- 执行bootloader中的指令,完成系统基本的硬件初始化,将命令行参数传给内核
- secondloader由汇编语言实现,完成CPU硬件的初始化,包括MMU(Memory Management Unit,内存管理单元),建立内存页面管理结构(页表);物理地址到虚拟地址的转换;
- start_kernel,开始Linux内核的启动过程。
start_kernel->rest_init() ->kernel_init -> do_basic_setup ->do_initcalls
4.2.1 do_inicall初始化
- 使用下面的宏注册不同优先级子系统初始化程序,内核将各优先级的初始化函数放入相应的.initcallN.init段
/* 目录: include/linux/init.h */
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
*/
#define pure_initcall(fn) __define_initcall("0",fn,0)
#define core_initcall(fn) __define_initcall("1",fn,1)
#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
#define postcore_initcall(fn) __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)
#define arch_initcall(fn) __define_initcall("3",fn,3)
#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)
#define fs_initcall(fn) __define_initcall("5",fn,5)
#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) __define_initcall("6",fn,6)
#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)
#define late_initcall(fn) __define_initcall("7",fn,7)
#define late_initcall_sync(fn) __define_initcall("7s",fn,7s)
- 内存空间分布
/* arch/avr32/kernel/vmlinux.lds */
__initcall_start = .;
INITCALLS
__initcall_end = .;
-
do_initcalls逐一执行_initcall_start与_initcall_end之间的函数
-
kernel_init ->init_post -> free_initmem
内核初始化已经接近尾声,所有的初始化函数都已经被调用,free_initmem可以舍弃内存的__init_begin
至__init_end(包括.init.setup、.initcall.init)之间的数据。所有使用__init标记过的函数和使用__initdata
标记过的数据,在free_initmem函数执行后都不能使用,它们曾经获得内存现在可以重新用于其他目的
4.2.2 网络子系统初始化
//用subsys_initcall宏将网络子系统初始化函数注册放入.initcall内存段中
subsys_initcall(net_dev_init);
net_dev_init完成了以下任务:
- 创建/proc文件系统下的入口
- 流量管理初始化
- 设备与事件初始化
- 初始化路由与其他初始化过程
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
/*不是内核启动阶段不调用该函数*/
/*dev_boot_phase标记net_dev_init是否已经被执行过*/
BUG_ON(!dev_boot_phase);
/*初始化网络设备在/proc文件系统下的入口*/
if (dev_proc_init())
goto out;
if (netdev_sysfs_init())
goto out;
/*注册需处理来自网络设备数据的上层协议实例的处理函数*/
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < 16; i++)
INIT_LIST_HEAD(&ptype_base[i]);
for (i = 0; i < ARRAY_SIZE(dev_name_head); i++)
INIT_HLIST_HEAD(&dev_name_head[i]);
for (i = 0; i < ARRAY_SIZE(dev_index_head); i++)
INIT_HLIST_HEAD(&dev_index_head[i]);
/*
* Initialise the packet receive queues.
*/
/*初始化每个CPU的数据包输入/输出队列*/
for_each_possible_cpu(i) {
struct softnet_data *queue;
queue = &per_cpu(softnet_data, i);
skb_queue_head_init(&queue->input_pkt_queue);//初始化输入数据包队列
queue->completion_queue = NULL; //完成队列为空
INIT_LIST_HEAD(&queue->poll_list); //建立轮询设备队列
set_bit(__LINK_STATE_START, &queue->backlog_dev.state);
queue->backlog_dev.weight = weight_p;
queue->backlog_dev.poll = process_backlog;
atomic_set(&queue->backlog_dev.refcnt, 1);
}
netdev_dma_register();
dev_boot_phase = 0;
//注册网络子系统的接收、发送网络数据包软件中断处理函数
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
//注册接收CPU时间通知的处理函数到CPU事件通知链中
hotcpu_notifier(dev_cpu_callback, 0);
//初始化路由表
dst_init();
//初始化组传送设备
dev_mcast_init();
rc = 0;
out:
return rc;
}
内核代码可以静态地连接(在编译时就连接)到整个内核的目标代码中,也可以作为模块在系统运行时动态地加载到内核,但并不是所有的内核组件都适于编译成模块,一般设备驱动程序或内核功能扩展的代码编译成模块。
每个模块提供两个特殊的函数:
- init_module 装载模块时的初始化函数
- cleanup_module 卸载模块时的清除函数,释放资源
内核提供两个宏module_init和module_exit允许模块命名自己的模块初始化函数和模块清除函数,通过它们标记了的函数就称为init_module和cleanup_module的别名。
一个网络设备的驱动程序可以直接编译成内核的一个组件,也可以编译成模块,在系统运行期间插入内核,两种模式都是通过宏module_init和module_exit定义的初始化函数及清除函数来完成网络设备的初始化(如果静态编译到内核中就在系统启动时执行,如果编译成模块就在运行时装载模块)和卸载。
- 静态编译到内核
/* include/linux/init.h */
#ifndef MODULE
#define device_initcall(fn) __define_initcall("6",fn,6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
#endif
静态编译到内核时(即不为模块),module_init描述的函数就分类为启动时的初始化函数
网络子系统初始化函数由宏subsys_initcall声明,网络设备初始化函数由device_initcall声明,保证了网络子系统初始化完成前不会有任何网络设备注册。
- 编译为模块
编译成模块时,module_init宏声明的函数会在系统运行期间用户发出insmod或modprobe命令时被调用执行,以初始化网络设备。module_exit宏声明的函数会在用户发出rmmod/modprobe -r时被调用来卸载网络设备驱动程序模块。
4.3 网络设备注册和struct net_device数据结构实例的初始化
在网络设备硬件可以被内核识别使用之前,描述网络设备的struct net_device数据结构实例必须被创建、初始化,加入到内核的网络设备数据库中,然后配置设备参数,**设备,允许设备开始收发网络数据。
memset的作用是把指定的一段内存按字节设置为第二个参数指定的值。 memset(内存地址,1,字节数)这句的意思就是要把指定的内存空间的值设置成 0x1
/*设置数据链路层广播地址FF:FF:FF:FF:FF:FF*/
memset(dev->broadcast, 0xFF, ETH_ALEN);
- 为设备创建net_device数据结构实例
/*申请内存,将变量驻留在申请到内存空间上*/
struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,
void (*setup)(struct net_device*), unsigned int queue_count )
(1) 输入参数
- sizeof_priv : 私有数据需占用的内存空间。struct net_device数据结构可以由设备驱动程序通过私有数据结构块来扩展,其中存放设备驱动程序的参数决定了私有数据结构的大小。
- name : 设备名
- *setup : 初始化例程
- queue_count : 网络设备的发送队列数
(2)返回值
成功则返回分配好的net_device结构实例的指针,失败则返回NULL指针
(3) 网络设备实例需要的内存空间
三部分组成:net_device数据结构所需的内存空间 + 私有数据结构占用的内存空间 + 设备发送队列所需内存空间
(4) 为网络设备分配设备名
网络设备名以字符串开头后跟一个数字,前缀字符串描述了网络设备的类型,数字按同类网络设备注册到系统中的顺序产生的***。
(5) 网络设备发送队列的建立
分配发送队列数据结构netdev_queue所需的内存空间,初始化struct net_device 数据结构的发送队列、发送队列数量、实际可用发送队列数等数据域。
- 内核代码xxx_setup对网络设备实例的初始化
网络设备都有一个统一的xxx_setup例程来初始化struct net_device数据结构实例的某些数据域(参数和函数指针),这些数据域对同类的网络设备而言为通用数据域,物理网络设备的生存厂商如何,值都相同。
/*初始化所有以太网卡共享的数据域和函数指针,最大传送单元:1500,数据链路层广播地址:FF:FF:FF:FF:FF:FF,发送队列长度: 1000个数据包*/
void ether_setup(struct net_device *dev)
{
dev->change_mtu = eth_change_mtu;
dev->hard_header = eth_header;
dev->rebuild_header = eth_rebuild_header;
dev->set_mac_address = eth_mac_addr;
dev->hard_header_cache = eth_header_cache;
dev->header_cache_update= eth_header_cache_update;
dev->hard_header_parse = eth_header_parse;
dev->type = ARPHRD_ETHER;
dev->hard_header_len = ETH_HLEN;
dev->mtu = ETH_DATA_LEN;
dev->addr_len = ETH_ALEN;
dev->tx_queue_len = 1000; /* Ethernet wants good queues */
dev->flags = IFF_BROADCAST|IFF_MULTICAST;
memset(dev->broadcast, 0xFF, ETH_ALEN);
}
- 驱动程序初始化struct net_device数据结构的其他数据域
由内核代码xxx_setup初始化struct net_device数据结构的通用数据结构后,接着由网络设备驱动程序初始化struct net_device数据结构的其他部分,最后调用register_netdev注册设备。
4.3.1 网络设备的注册和注销
- 网络设备的注册和注销分别由函数register_netdev和unregister_netdev完成,它们获取防止并发访问的锁,然后调用register_netdevice和unregister_netdevice。
- 网络设备实例需经过注册才能被内核识别,内核需要了解网络设备实例注册是否成功。如果注册成功,设备就加入到了内核管理网络设备的数据库中,协议栈即可使用网络设备提供的服务;如果注册不成功,内核需要清除网络设备实例分配时占用的所有系统资源。
- 一旦register_netdevice完成处理,就把网络设备对应的struct net_device数据结构实例用net_set_todo加到net_todo_list(由dev->todo_list指针指向)列表中,这个列表包含了所有需要完成注册(或注销)的设备。net_todo_list列表不由独立的内核线程或周期性函数来处理,它会在register_netdev函数释放锁时(rtnl_unlock)间接被调用。
- net_dev_run_todo遍历net_todo_list列表,完成所有网络设备net_device数据结构实例的注册(或注销)。
网络设备的注册
module_init ——> register_device——>net_set_todo ——> netdev_run_todo完成设备注册
- 初始化struct net_device数据结构实例的某些数据域
- dev->netdev_ops->ndo_init(dev)初始化设备驱动程序的私有数据结构
- dev_new_index分配唯一标识符
- dev_init_scheduler初始化设备的队列策略
- list_netdevice将网络设备的struct net_device数据结构实例加入到全局链表dev_base_head和两个哈希表中。
- 更新dev_reg_state状态,将设备注册到/sysfs文件系统中。
网络设备的注销
- 取消网络设备注册时所有工作
- 调用dev_close禁止网络设备操作
- 释放为网络设备分配的资源
- 从全局链表dev_base_head和两个哈希链表中移走网络设备的struct net_device数据结构实例。
- free_netdev释放struct net_device
网络设备的引用计数(refrence count)
引用计数存放在dev->refcnt数据域中,注册时,初始化为1,注销后,内核组件通过网络子系统通知链netdev_chain传送NETDEV_UNREGISTER事件获取消息,释放对网络设备的引用。
**网络设备
/***网络设备*/
int dev_open(struct net_device *dev)
{
int ret = 0;
/*
* Is it already up?
*/
if (dev->flags & IFF_UP)
return 0;
/*
* Is it even present?
*/
if (!netif_device_present(dev))
return -ENODEV;
/*
* Call device private open method
*/
/*设备设备开始运行标志:__LINK_STATE_START*/
set_bit(__LINK_STATE_START, &dev->state);
/*初始化open*/
if (dev->open) {
ret = dev->open(dev);
if (ret)
clear_bit(__LINK_STATE_START, &dev->state);
}
/*
* If it went open OK then:
*/
if (!ret) {
/*
* Set the flags.
*/
/*设置flags为IFF_UP,表明设备已经可以工作*/
dev->flags |= IFF_UP;
/*
* Initialize multicasting status
*/
dev_set_rx_mode(dev);
/*
* Wakeup transmit queue engine
*/
/*初始化网络设备的数据包传送队列策略(默认FIFO),启动watchdog时钟*/
dev_activate(dev);
/*
* ... and announce new interface.
*/
/*通知其他组件,此设备可用*/
raw_notifier_call_chain(&netdev_chain, NETDEV_UP, dev);
}
return ret;
}
禁止网络设备
int dev_close(struct net_device *dev)
{
if (!(dev->flags & IFF_UP))
return 0;
/*
* Tell people we are going down, so that they can
* prepare to death, when device is still operating.
*/
/*通知其他组件,网络设备将停止工作*/
raw_notifier_call_chain(&netdev_chain, NETDEV_GOING_DOWN, dev);
/*停止网络设备的传送队列,停止watchdog时钟*/
dev_deactivate(dev);
/*清除标记,表明设备下线*/
clear_bit(__LINK_STATE_START, &dev->state);
/* Synchronize to scheduled poll. We cannot touch poll list,
* it can be even on different cpu. So just clear netif_running(),
* and wait when poll really will happen. Actually, the best place
* for this is inside dev->stop() after device stopped its irq
* engine, but this requires more changes in devices. */
smp_mb__after_clear_bit(); /* Commit netif_running(). */
while (test_bit(__LINK_STATE_RX_SCHED, &dev->state)) {
/* No hurry. */
msleep(1);
}
/*
* Call the device specific close. This cannot fail.
* Only if device is UP
*
* We allow it to be called even after a DETACH hot-plug
* event.
*/
if (dev->stop)
dev->stop(dev);
/*
* Device is now down.
*/
dev->flags &= ~IFF_UP;
/*
* Tell people we are down
*/
/*通知其他组件网络设备已停止工作*/
raw_notifier_call_chain(&netdev_chain, NETDEV_DOWN, dev);
return 0;
}
4.4 网络设备的管理
网络设备全局链表(dev_base_head)
包含所有网络设备的struct net_device数据结构实例,使内核管理网络设备更容易。
哈希链表
网络子系统还建立了两个哈希链表,这两个哈希链表分别以网络设备名和网络设备索引号为关键字建立
- dev_name_head 以设备名(dev->name)为关键字,场景:ioctl改变网络设备配置
- dev_index_head以设备索引号为关键字,场景:Netlink socket与内核通信
4.5 事件通知链
事件通知链是一个事件处理函数的列表,每个通知链都与某个或某些事件相关,当特定的事件发生时,列在通知链中的函数就依次被执行,通知事件处理函数所属的子系统某个事件发生了,子系统接到通知后做相应的处理。
事件通知链中的成员:
struct nofitier_block{
int (*notifier_call) (struct notifier_block *self, unsigned long , void *);
struct notifier_block *next;
int priority;
}
说明:
- notifier_call 指向事件处理函数,三个入参分别是 (1)struct notifier_block *self 指明事件通知来自系统的哪个通知链中 (2)unsigned long 当前发生的是什么事件 (3)void * 传给事件处理回调函数的参数。
- struct notifier_block *next 将事件通知链中成员连接成列表的指针;
- priority 事件处理函数优先级。
向通知链注册事件处理函数:
/* struct notifier_block **nl 注册到哪个事件通知链上
struct notifier_block *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;
}
向事件通知链注册步骤:
- 声明struct notifier bolck 数据结构实例
- 编写事件处理回调函数
- 将事件回调处理函数地址赋值给struct notifier_block 数据结构实例的* notifier_call成员
- 将struct notifier_block注册到通知链中
在网络子系统中一共创建了3个事件通知链:
- inetaddr_chain : 本地网络接口的IPv4地址发生变化或网卡的IP地址移走、改变时,在该事件通知链上发出事件通知。
- inet6addr_chain : IPv6地址变化
- netdev_chain : 网络设备注册、状态变化
/*struct notifier_block **nl 表明哪个通知链发出的事件通知在调用事件处理回调函数
unsigned log val : 发生的事件类型
void *v : 传给事件处理回调函数的参数
int nr_to_call : 要调用的事件处理回调函数的个数,不关心,传值-1
int *nr_calls : 已调用了的时间处理回调函数的个数*/
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;
//n和next_nb分别指向当前正在调用的事件处理函数与要调用的事件处理函数
struct notifier_block *nb, *next_nb;
//获取事件通知链的首地址
nb = rcu_dereference(*nl);
while (nb && nr_to_call) {
next_nb = rcu_dereference(nb->next);
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;
}