第3、4章 网络设备在内核中的抽象和识别

网络设备在Linux内核代码中的实体是 struct net_device数据结构的实例,网络设备初始化流程:

  1. 分配网络设备数据结构实例的内存空间
  2. 初始化数据结构,初始化设备的发送队列,建立函数指针
  3. 注册设备,之后设备就被挂接到了TCP/IP协议栈上
  4. 初始化其他特殊操作

1.命令行参数

命令行参数:通过bootloader给内核传送各种配置参数。
配置方法:

  • bootloader中命令行提供参数: 参数名 = 值

    eg : 进入到Linux进入到启动界面,输入“a” 可以在bootloader环境中加入新的命令行参数:netdev=5,0x260,eth0 netdev=15,0x300,eth1 【向内核指定eth0和eth1的中断号和I/O端口的起始地址】

  • U-boot通过设置环境变量指定传给内核的命令行参数

    eg:#define CONFIG_EXFIG_ENV_SETTINGS
    " netdev = eth0 5 0x26\0"

用户可以向内核传递什么命令行参数,需要在内核代码中事先定义。

  1. 如何定义命令行参数
    使用__setup宏指定可接收的命令行参数,并注册到系统中

     /*
    * srting --命令行参数的字符串
    * function --解析和处理该命令行参数的函数
    * 定义在include/linux/init.h中
    */
    __setup(string, function)
    

    在内核中实现命令行参数的例子:
    (1)编写命令行参数的处理函数,处理函数前以关键字__init说明

    static int __init my_function(char *str)
    {
         ...
    }
    __setup("netdev=",my_function);
    

    (2) 紧跟在处理函数后面,宏__setup注册命令行参数和处理命令行参数的函数。

  2. 命令行参数的解析:在start_kernel中通过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函数时解析。

  3. 命令行参数的存放和管理
    命令行参数与其处理函数由obs_kernel_param 数据结构描述,

    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.initsection中,而这个section在内核完成初始化之后,会被释放掉。
    __attribute_used__ 表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。

    所有obs_kernel_param实例(命令行参数)都在以指针__setup_start和__setup_end指定的内存区域中(也即.init.setup段),该区域内核初始化接收后将释放。
    调用parse_args函数做第二遍解析的命令行参数会复制到__start_param和__stop_param(start_kernel函数中定义)之间的区域中,这部分区域在内核初始化结束后不会释放,通过/sys文件系统向用户空间输出,供应用程序使用。

网络子系统命令行参数解析例子
net/core/dev.c -> netdev_boot_set实现命令行参数解析

struct ifmap{
    unsigned long mem_start;
    unsigned long men_end;
    unsigned short base_addr;
    unsigned char irq;
    unsigned char dma;
    unsigned char port;
}
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);

内核启动时,在boot loader中用以下格式通过命令行参数向网络子系统传递网络设备的硬件配置信息:

netdev = 5,0x260,eth0 netdev=15,0x300,eth1

netdev_boot_setup函数查看输入的字符串,将字符串后跟着的值解析到ifmap结构的数据域中,然后调用netdev_boot_setup_add将解析好的所有命令行参数以设备名为索引保存在struct netdev_boot_setup数据结构类型的数组dev_boot_setup[NETDEV_BOOT_SETUP_MAX]中。

struct netdev_boot_setup{
    char name[IFNAMSIZ];
    struct ifmap map;
}
static sturct dev_boot_setup[NETDEV_BOOT_SETUP_MAX];
#define NETDEV_BOOT_SETUP_MAX 8

在内核启动后期,网络子系统通过netdev_boot_setup_check查看数组dev_boot_setup[NETDEV_BOOT_SETUP_MAX]是否有硬件配置参数,如果不为空,则将数据填入到网络设备struct net_device dev实例的数据结构中。

2.内核启动过程

  1. 执行bootloader中的指令,完成系统基本的硬件初始化,将命令行参数传给内核
  2. secondloader由汇编语言实现,完成CPU硬件的初始化,包括MMU(Memory Management Unit,内存管理单元),建立内存页面管理结构(页表);物理地址到虚拟地址的转换;
  3. start_kernel,开始Linux内核的启动过程。

start_kernel->rest_init() ->kernel_init -> do_basic_setup ->do_initcalls
在这里插入图片描述
do_inicall初始化逐一执行_initcall_start与_initcall_end之间的函数,然后kernel_init ->init_post -> free_initmem舍弃内存的__init_begin至__init_end(包括.init.setup、.initcall.init)之间的数据。所有使用__init标记过的函数和使用__initdata
标记过的数据,在free_initmem函数执行后都不能使用,它们曾经获得内存现在可以重新用于其他目的。
在这里插入图片描述
使用下面的宏注册不同优先级子系统初始化程序,内核将各优先级的初始化函数放入相应的.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)

3.网络子系统初始化

网络子系统由net_dev_init初始化,用subsys_initcall宏将网络子系统初始化函数注册放入.initcall内存段中,在系统启动时由do_initcal调用执行。

subsys_initcall(net_dev_init);

所有协议的packet_type存放在两条协议链中:ptype_base和ptype_all,ptype_base为哈希链表,ptype_all为双向链表,系统使用dev_add_pack函数将指定协议类型的packet_type添加到这两个表中。ETH_P_ALL类型的数据报文将在ptype_all表中找到自己对应的接收函数,而ETH_P,ETH_P_ARP等协议的数据报文将在ptype_base找到接收函数
net_dev_init完成了以下任务:

  1. 创建/proc文件系统下的入口
    /proc/net/dev 存放设备名称相关信息
    /proc/net/softnet_stat
    /proc/net/ptype 注册的协议接收函数

  2. 流量管理初始化
    为每个CPU建立网络数据包的接收/发送队列
    向系统注册接收网络设备数据包的上层协议栈的接收处理函数。
    注册软中断处理函数
    NET_TX_SOFTIRQ - 》 net_tx_action
    NET_RX_SOFTIRQ - 》net_rx_action

  3. 设备与事件初始化

  4. 初始化路由与其他初始化过程

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;
}

4.网络设备初始化

内核代码可以静态地连接(在编译时就连接)到整个内核的目标代码中,也可以作为模块在系统运行时动态地加载到内核,但并不是所有的内核组件都适于编译成模块,一般设备驱动程序或内核功能扩展的代码编译成模块。
每个模块提供两个特殊的函数:

  • 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时被调用来卸载网络设备驱动程序模块。

5.struct net_device数据结构实例的初始化

在网络设备硬件可以被内核识别使用之前,描述网络设备的struct net_device数据结构实例必须被创建、初始化,加入到内核的网络设备数据库中,然后配置设备参数,激活设备,允许设备开始收发网络数据。

memset的作用是把指定的一段内存按字节设置为第二个参数指定的值。 memset(内存地址,1,字节数)这句的意思就是要把指定的内存空间的值设置成 0x1,eg:

/*设置数据链路层广播地址FF:FF:FF:FF:FF:FF*/
memset(dev->broadcast, 0xFF, ETH_ALEN);

5.1 为设备创建net_device数据结构实例

/*申请内存,将变量驻留在申请到内存空间上*/
struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,
						void (*setup)(struct net_device*), unsigned int queue_count )

说明:

  • sizeof_priv : 私有数据需占用的内存空间。
  • name : 设备名
  • *setup : 初始化例程
  • queue_count : 网络设备的发送队列数
  • 返回值
    成功则返回分配好的net_device结构实例的指针,失败则返回NULL指针

网络设备实例需要的 内存空间由三部分组成:net_device数据结构所需的内存空间 + 私有数据结构占用的内存空间 + 设备发送队列所需内存空间

网络设备名以字符串开头后跟一个数字,前缀字符串描述了网络设备的类型,数字按同类网络设备注册到系统中的顺序产生的序列号。

网络设备的发送队列数为设备分配发送队列数据结构netdev_queue所需的内存空间,初始化struct net_device 数据结构的发送队列、发送队列数量、实际可用发送队列数等数据域。

5.2 内核代码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);
}

5.3 驱动程序初始化struct net_device数据结构的其他数据域

由内核代码xxx_setup初始化struct net_device数据结构的通用数据结构后,接着由网络设备驱动程序初始化struct net_device数据结构的其他部分,最后调用register_netdev注册设备。
此处的初始化是对该网络设备而言特定的数据域的初始化,与具体的网络设备硬件特性相关,例如,xxx_probe函数初始化网络设备向系统申请的中断号dev->irq数据域,I/O端口地址dev->base_addr、网络设备DMA通道号dev->dma等,建立驱动程序的函数指针,如open、stop、hard_start_xmit等虚函数的实例化。

网络设备从创建设备实例到设备注册到内核过程如下图所示,开始由函数alloc_etherdev_mq来分配struct net_device数据结构所需的内存,实际的分配在kzalloc函数中完成,netdev_boot_setup_check查看系统启动时用户是否将配置参数传递给内核,最后调用register_netdev注册设备。
在这里插入图片描述
设备注销:
在这里插入图片描述

6.网络设备的注册和注销

在这里插入图片描述

  • 网络设备的注册和注销分别由函数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数据结构实例的注册(或注销)。

6.1 网络设备的注册

在这里插入图片描述

  • 初始化struct net_device数据结构实例的某些数据域
  • dev->netdev_ops->ndo_init(dev)初始化设备驱动程序的私有数据结构
  • dev_new_index分配唯一标识符
  • 设置feature(特征)标志
  • dev_init_scheduler初始化设备的队列策略
  • list_netdevice将网络设备的struct net_device数据结构实例加入到全局链表dev_base_head和两个哈希表中。
  • netdev_chain通知NETDEV_REGISTER事件
  • 更新dev_reg_state状态,将设备注册到/sysfs文件系统中。

6.2 网络设备的注销

在这里插入图片描述

  • 取消网络设备注册时所有工作
  • 调用dev_close禁止网络设备操作,释放为网络设备分配的资源
  • 从全局链表dev_base_head和两个哈希链表中移走网络设备的struct net_device数据结构实例。
  • dev_shutdown取消设备的策略实例
  • netdev_chain通过NETDEV_UNREGISTER通知网络设备卸载
  • free_netdev释放struct net_device

6.3 网络设备的引用计数(refrence count)

对网络设备struct net_device数据结构实例没有完全释放前,网络设备不能释放。引用计数存放在dev->refcnt数据域中,当网络设备向内核注册时,初始化为1,第一个引用计数由内核代码保存,负责维护网络设备数据库,注销后,内核组件通过网络子系统通知链netdev_chain传送NETDEV_UNREGISTER事件获取消息,释放对网络设备的引用。

6.4 激活网络设备

激活网络设备由dev_open函数完成,激活一个设备,要完成的任务包括:

  • 调用dev->open
  • 将dev->state的标志设为_LINK_STATE_START,标志设备开始运行
  • 设置dev->flags标志IFF_UP,表明设备已经可以工作
  • 调用dev_activate初始化网络设备的数据包传送队列策略,该队列策略由流量控制系统系统使用,启动watchdog时钟。如果用户没有配置流量控制,队列策略默认为先进先出(FIFO)
  • 通过网络子系统的事件通知链netdev_chain发送NETDEV_UP事件通知,通知内核其他组件,此设备可用。
/*激活网络设备*/
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;
}

6.5 禁止网络设备

禁止网络设备的使用是通过dev_close完成:

  • 通过网络子系统的事件通知链发送NET_GOING_DOWN事件,通知内核组件,网络设备将停止工作
  • 调用dev_deactivate停止网络设备的传送队列,停止watchdog时钟
  • 清除dev->state状态标志_LINK_STSTE_START,表明网络设备已下线
  • 如果轮询被调度来读网络设备的输入队列,等到轮询活动结束
  • 调用dev->stop
  • 清除dev->flags的IFF_UP标志
  • 通过网络子系统的事件通知链netdev_chain发送NET_DOWN事件,通知内核其他组件网络设备已停止工作。
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;
}

7.网络设备的管理

网络设备struct net_device数据结构实例在创建后会插入到一个全局链表dev_base_head和两个哈希链表中,使内核查找设备更容易。
操作函数:

/*将网络设备的net_device数据结构实例插入或移出全局链表和两个哈希表*/
static int list_netdevice(struct net_device *dev);
static int unlist_netdevice(struct net_device *dev);

7.1 网络设备全局链表

dev_base_head全局链表包含所有网络设备的struct net_device数据结构实例,因为每个网络设备驱动程序都可能有自己的私有数据结构,所以链表中网络设备的net_device数据结构实例大小可能不一样。
在这里插入图片描述
全局链表dev_base_head由dev_base_lock sprin_lock和rtnl semaphore保护,在对链表进行写操作时,必须首先获取rtnl semaphore,开始在全局链表中查找要操作的网络设备;找到匹配的网络设备后,进行实际的写操作前,必须获取dev_base_lock写操作锁。这样做的目的是对全局链表的只读操作在写操作查找网络设备期仍能访问全局链表。

基于网络设备全局链表的搜索函数:

  1. 按网络设备硬件地址搜索
 struct net_device *dev_getbyhwaddr(struct net *net, unsigned short type, char *ha);
  1. 按网络设备类型搜索
struct net_device *__dev_getfirstbyhwtype(struct net *net, unsigned short type);
struct net_device *dev_getfirstbyhwtype(struct net *net, unsigned short type);
  1. 按网络设备标志搜索
struct net_device *dev_get_by_flags(struct net *net, unsigned short if_flags, unsigned short mask);

7.2 哈希链表

网络子系统以网络设备名网络设备索引号为关键字建立两个哈希链表

  1. dev_name_head 以设备名(dev->name)为关键字的哈希链表,场景:ioctl改变网络设备配置
  2. dev_index_head以设备索引号为关键字的哈希链表,场景:网络配置工具IP使用Netlink socket与内核通信

网络设备哈希链表建立函数:

static inline struct hlist_head *dev_name_hash(struct net *net,const char *name)
{
    unsigned hash = full_name_hash(name, strlen(name, IFNAMSIZ));
    return &net->dev_name_head[hash & ((1<<NETDEV_HASHBITS)-1)];
}
static inline struct hlist_head *dev_index_hash(struct net *net, int ifindex)
{
    return &net->dev_index_head[ifindex& ((1<<NETDEV_HASHBITS)-1)];
}

基于哈希链表搜索函数

struct net_device *dev_get_by_name(struct net *net, const char *name);
struct net_device *dev_get_by_index(struct net *net, const int ifindex);

8.通知链

在网络子系统协议栈中,为了实现网络传输的高性能,简化协议处理,协议栈实例往往将它们使用的设备引用固定存放在自己的数据结构中,当网络设备的状态发送变化时(如设备注销),在协议栈实例中存储的信息就无效了,这时协议栈实例不应再通过无效的网络设备收发数据包。因此协议栈实例需要获取网络设备状态发送变化的消息,以便按照网络设备状态的变化进行处理。

事件通知链是一个事件处理函数的列表,每个通知链都与某个或某些事件相关,当特定的事件发生时,列在通知链中的函数就依次被执行,通知事件处理函数所属的子系统某个事件发生了,子系统接到通知后做相应的处理。

通知链使用发布-订阅(publish-and-subcribe)模型:

  • 被通知者 ——接收某事件的子系统,提供回调函数予以调用
  • 通知者 ——感受到一个事件并调用回呼函数的子系统 通知

通知链列表元素的类型是notifier_block,定义:


struct nofitier_block{
	int (*notifier_call) (struct notifier_block *self, unsigned long , void *);
	struct notifier_block *next;
	int priority;
}

函数说明:

  1. notifier_call 指向事件处理函数,三个入参分别是 (1)struct notifier_block *self 指明事件通知来自系统的哪个通知链中 (2)unsigned long 当前发生的是什么事件 (3)void * 传给事件处理回调函数的参数。
  2. struct notifier_block *next 将事件通知链中成员连接成列表的指针;
  3. priority 事件处理函数优先级。

例如netdev_chain通知链如下:
在这里插入图片描述
内核中某个组件需要对一个已存在的时间通知链上所产生的事件做处理时,该组件需要向这个事件通知链注册自己的事件处理回调函数。这个过程可以使用通用注册函数notifier_chain_register完成。

/* 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;
}

在网络子系统中一共创建了3个事件通知链:

网络子系统通知链作用注册除名
inetaddr_chain发送有关本地接口上的IPv4地址的插入、删除以及变更的通知信息register_inetaddr_notifierunregister_inetaddr_notifier
inet6addr_chain发送有关本地接口上的IPv6地址的插入、删除以及变更的通知信息register_inet6addr_notifierunregister_inet6addr_notifier
netdev_chain发送有关网络设备注册状态的通知信息register_netdevice_notifierunregister_netdevice_notifier

向事件通知链注册步骤:

  1. 声明struct notifier bolck 数据结构实例
  2. 编写事件处理回调函数
  3. 将事件回调处理函数地址赋值给struct notifier_block 数据结构实例的* notifier_call成员
  4. 将struct notifier_block注册到通知链中

拥有时间通知链的事件通知方在时间发生时,调用notifier_call_chain通知其他子系统有事件发生。

/*
 * n 通知链
 * val事件类型
 * v 输入参数
 * 返回值: NOTIFY_OK-通知信息被正确处理
           NOTIFY_DONE - 对通知信息不敢兴趣
           NOTIFY_BAD - 有些事情出错,停止调用此事件的回调函数
           NOTIFY_STOP - 函数被正确调用,此事件不需要进一步调用其他回调函数
           NOTIFY_STOP_MASK - 由notifier_call_chain检查,以了解是否停止调用回调函数,或者继续调用下去
 */
int notifier_call_chain(struct notifier_block **n, unsigned long val, void *v)
{
    int ret = NOTIFY_DONE;
    struct notifier_block *nb = *n;
    while(nb)
    {
        ret = nb->notifier_call(list,val,v);
        if(ret&NOTIFY_STOP_MASK) 
        {
            return ret;
        }
        nb = nb->next;
    }
    return ret;
}

网络子系统事件描述:

事件符号名事件描述
NETDEV_UP激活一个网络设备
NETDEV_DOWN停止一个网络设备
NETDEV_REBOOT检查到网络设备硬件崩溃,硬件重启
NETDEV_CHANGE网络设备数据包队列状态发生了变化
NETDEV_REGISTER网络设备注册到系统中,但未激活
NETDEV_UNREGISTER网络设备驱动程序已卸载
NETDEV_CHANGEMTU网络设备的最大传送单元发生变化
NETDEV_CHANGEADDR网络设备硬件地址发生变化
NETDEV_CHANGENAME网络设备名发生变化
NETDEV_GOING_DOWN网络设备即将注销
NETDEV_FEAT_CHANGE网络设备硬件功能特性改变
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值