设备接口层之net_device注册与去注册

网络设备对应的结构体是struct net_device,而且驱动程序只有将该结构注册给内核,内核才认为这是一个合法的网络设备,这篇笔记就记录了驱动程序如何将网络设备注册给内核,以及对应反操作,如何去注册。

1. net_device的创建

首先,驱动程序需要创建一个struct net_device结构,并且填充其中必要的成员,然后才能注册给内核。

网络设备种类有很多,但是从软件层面来看,又有很多的共性,内核提供的struct net_device就是对这些共性的抽象。虽然该结构体定义的成员已经非常多了,但是不同的设备很有可能还有很多额外的特性,所以网卡驱动程序在实际定义中完全可以在该结构的基础上重新定义,通常格式是这样的:

struct new_net_device
{
	//struct net_device必须放在第一个位置
	struct net_device dev;
	...
}

这里,我们不讨论这种不同网卡所具备的新特性,这些特性太多了,我们不关注。struct net_device是内核抽象出来的结构,对于该结构的分配,内核提供了标准的接口可用,这才是我们应该关注的。

1.1 内存分配

驱动程序可以调用alloc_netdev()分配一个struct net_device结构。

#define alloc_netdev(sizeof_priv, name, setup) \
	alloc_netdev_mq(sizeof_priv, name, setup, 1)

/**
 *	alloc_netdev_mq - allocate network device
 *	@sizeof_priv: 私有数据空间的内存占用字节数
 *	@name: 为接口指定一个名字validate_addr
 *	@setup: 调用者可以提供一个初始化函数用于初始化net_device,该回调会在分配内存后被调用
 *	@queue_count:子队列数目,关于子队列不在这里讨论,这里默认为1
 *
 *	Allocates a struct net_device with private data area for driver use
 *	and performs basic initialization.  Also allocates subquue structs
 *	for each queue on the device at the end of the netdevice.
 */
struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,
		void (*setup)(struct net_device *), unsigned int queue_count)
{
	void *p;
	struct net_device *dev;
	int alloc_size;
	//接口名字不能超过15个字符,dev->name[]数组的长度就是15
	BUG_ON(strlen(name) >= sizeof(dev->name));

	/* ensure 32-byte alignment of both the device and private area */
	//保证32字节对齐的情况下,计算总共需要分配的内存大小
	alloc_size = (sizeof(*dev) + NETDEV_ALIGN_CONST +
		     (sizeof(struct net_device_subqueue) * (queue_count - 1))) &
		     ~NETDEV_ALIGN_CONST;
	alloc_size += sizeof_priv + NETDEV_ALIGN_CONST;
	//内存分配
	p = kzalloc(alloc_size, GFP_KERNEL);
	if (!p) {
		printk(KERN_ERR "alloc_netdev: Unable to allocate device.\n");
		return NULL;
	}
	//dev指向32字节地址对齐处,这会导致实际分配的内存可能在开始会有一段padding
	dev = (struct net_device *)
		(((long)p + NETDEV_ALIGN_CONST) & ~NETDEV_ALIGN_CONST);
	//计算首部padding大小保存到padded中,内存释放的释放会用到
	dev->padded = (char *)dev - (char *)p;
	dev->nd_net = &init_net;
	//dev->priv指向私有数据结构的起始处
	if (sizeof_priv) {
		dev->priv = ((char *)dev +
			     ((sizeof(struct net_device) +
			       (sizeof(struct net_device_subqueue) *
				(queue_count - 1)) + NETDEV_ALIGN_CONST)
			      & ~NETDEV_ALIGN_CONST));
	}

	dev->egress_subqueue_count = queue_count;
	//默认的获取网络设备统计信息的接口是internal_stats()
	dev->get_stats = internal_stats;
	//netpoll机制初始化,这里不讨论
	netpoll_netdev_init(dev);
	//回调驱动程序提供的初始化函数进一步初始化,可以看出该函数是必须要提供的
	setup(dev);
	//保存网络接口名字到dev->name
	strcpy(dev->name, name);
	return dev;
}
EXPORT_SYMBOL(alloc_netdev_mq);

从上面可以看到,在分配过程中,会对struct net_device中的一些字段进行初始化;更重要的是,这期间会调用驱动程序提供的setup()回调,这给了驱动程序对分配的net_device进行初始化的机会。当然了,因为alloc_netdev()执行后,分配好的net_device指针还是会返回给调用者(一般就是驱动程序了),所以调用者也可以在函数返回后执行本来应该在setup()中执行的逻辑。

2. net_device的注册

分配好net_device并进行初始化后,驱动程序就可以通过register_netdev()向内核注册该网络设备了,代码如下:

/**
 *	register_netdev	- register a network device
 *	@dev: device to register
 *
 *	Take a completed network device structure and add it to the kernel
 *	interfaces. A %NETDEV_REGISTER message is sent to the netdev notifier
 *	chain. 0 is returned on success. A negative errno code is returned
 *	on a failure to set up the device, or if the name is a duplicate.
 *
 *	This is a wrapper around register_netdevice that takes the rtnl semaphore
 *	and expands the device name if you passed a format string to
 *	alloc_netdev.
 */
int register_netdev(struct net_device *dev)
{
	int err;

	//持有RTNETLINK互斥锁
	rtnl_lock();

	/*
	 * If the name is a format string the caller wants us to do a
	 * name allocation.
	 */
	//驱动程序分配网络设备时,如果名字中有%d,则这里为该网络设备分配一个唯一的ID,
	//以此组成最终的网络设备名。关于网络设备的名字确定,这里不再深入分析了
	if (strchr(dev->name, '%')) {
		err = dev_alloc_name(dev, dev->name);
		if (err < 0)
			goto out;
	}
	//完成剩余的注册工作
	err = register_netdevice(dev);
out:
	//释放锁
	rtnl_unlock();
	return err;
}

互斥锁RTNETLINK是将系统中所有的对net_device内容的读写操作串行化,代码如下:

static DEFINE_MUTEX(rtnl_mutex);

void rtnl_lock(void)
{
	mutex_lock(&rtnl_mutex);
}

void __rtnl_unlock(void)
{
	mutex_unlock(&rtnl_mutex);
}

2.1 register_netdevice()

实际的注册工作由该函数完成,代码逻辑如下:

/**
 *	register_netdevice	- register a network device
 *	@dev: device to register
 *
 *	Take a completed network device structure and add it to the kernel
 *	interfaces. A %NETDEV_REGISTER message is sent to the netdev notifier
 *	chain. 0 is returned on success. A negative errno code is returned
 *	on a failure to set up the device, or if the name is a duplicate.
 *
 *	Callers must hold the rtnl semaphore. You may want
 *	register_netdev() instead of this.
 *
 *	BUGS:
 *	The locking appears insufficient to guarantee two parallel registers
 *	will not get the same name.
 */
int register_netdevice(struct net_device *dev)
{
	struct hlist_head *head;
	struct hlist_node *p;
	int ret;
	struct net *net;

	//设备接口层必须已经初始化完成,即net_dev_init()已经执行完毕
	BUG_ON(dev_boot_phase);
	ASSERT_RTNL();

	might_sleep();

	/* When net_device's are persistent, this will be fatal. */
	//网络设备的注册状态必须是未初始化状态,刚分配的struct net_device就是这个状态
	BUG_ON(dev->reg_state != NETREG_UNINITIALIZED);
	BUG_ON(!dev->nd_net);
	net = dev->nd_net;

	//对net_device结构中的一些锁进行初始化,这些锁的作用在数据传输部分再介绍
	spin_lock_init(&dev->queue_lock);
	spin_lock_init(&dev->_xmit_lock);
	netdev_set_lockdep_class(&dev->_xmit_lock, dev->type);
	dev->xmit_lock_owner = -1;
	spin_lock_init(&dev->ingress_lock);

	dev->iflink = -1;

	/* Init, if this function is available */
	//如果驱动程序提供了init()接口,则回调该函数。如果该函数返回0,继续后面的注册,否则注册失败
	if (dev->init) {
		ret = dev->init(dev);
		if (ret) {
			if (ret > 0)
				ret = -EIO;
			goto out;
		}
	}
	//检查接口名字中有没有特殊字符,长度有没有越界
	if (!dev_valid_name(dev->name)) {
		ret = -EINVAL;
		goto err_uninit;
	}
	//为该网络设备分配一个全局唯一的索引
	dev->ifindex = dev_new_index(net);
	if (dev->iflink == -1)
		dev->iflink = dev->ifindex;

	/* Check for existence of name */
	//检查系统中是否已经存在相同名字的网络接口,如果存在则注册失败,返回-EEXIST错误码
	//从这里可以看出,同一网络命名空间中的网卡设备是不可以有重名的
	head = dev_name_hash(net, dev->name);
	hlist_for_each(p, head) {
		struct net_device *d
			= hlist_entry(p, struct net_device, name_hlist);
		if (!strncmp(d->name, dev->name, IFNAMSIZ)) {
			ret = -EEXIST;
			goto err_uninit;
		}
	}

	/* Fix illegal checksum combinations */
	//下面的这几组代码是校正驱动程序指定的Feature字段。Featur代表的是设备的能力,这里先忽略
	if ((dev->features & NETIF_F_HW_CSUM) &&
	    (dev->features & (NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM))) {
		printk(KERN_NOTICE "%s: mixed HW and IP checksum settings.\n",
		       dev->name);
		dev->features &= ~(NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM);
	}

	if ((dev->features & NETIF_F_NO_CSUM) &&
	    (dev->features & (NETIF_F_HW_CSUM|NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM))) {
		printk(KERN_NOTICE "%s: mixed no checksumming and other settings.\n",
		       dev->name);
		dev->features &= ~(NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM|NETIF_F_HW_CSUM);
	}

	/* Fix illegal SG+CSUM combinations. */
	if ((dev->features & NETIF_F_SG) &&
	    !(dev->features & NETIF_F_ALL_CSUM)) {
		printk(KERN_NOTICE "%s: Dropping NETIF_F_SG since no checksum feature.\n",
		       dev->name);
		dev->features &= ~NETIF_F_SG;
	}

	/* TSO requires that SG is present as well. */
	if ((dev->features & NETIF_F_TSO) &&
	    !(dev->features & NETIF_F_SG)) {
		printk(KERN_NOTICE "%s: Dropping NETIF_F_TSO since no SG feature.\n",
		       dev->name);
		dev->features &= ~NETIF_F_TSO;
	}
	if (dev->features & NETIF_F_UFO) {
		if (!(dev->features & NETIF_F_HW_CSUM)) {
			printk(KERN_ERR "%s: Dropping NETIF_F_UFO since no "
					"NETIF_F_HW_CSUM feature.\n",
							dev->name);
			dev->features &= ~NETIF_F_UFO;
		}
		if (!(dev->features & NETIF_F_SG)) {
			printk(KERN_ERR "%s: Dropping NETIF_F_UFO since no "
					"NETIF_F_SG feature.\n",
					dev->name);
			dev->features &= ~NETIF_F_UFO;
		}
	}
	//将网络设备注册到设备模型中
	ret = netdev_register_kobject(dev);
	if (ret)
		goto err_uninit;
	//设置网络设备的注册状态为已注册状态
	dev->reg_state = NETREG_REGISTERED;

	/*
	 *	Default initial state at registry is that the
	 *	device is present.
	 */
    //设置设备状态为“存在”
	set_bit(__LINK_STATE_PRESENT, &dev->state);
	//初始化发送排队规则,见《流量控制》
	dev_init_scheduler(dev);
	//初始化基本完成,网络设备的引用计数+1
	dev_hold(dev);
	//将网络设备同时挂接到系统维护三个表中:名字表、索引表、设备表
	list_netdevice(dev);

	/* Notify protocols, that a new device appeared. */
	//向其他模块发送"NETDEV_REGISTER"事件
	ret = call_netdevice_notifiers(NETDEV_REGISTER, dev);
	ret = notifier_to_errno(ret);
	//如果通知失败,这里回滚前面所有的额注册步骤,并设置注册状态为UNREGISTERED
	if (ret) {
		rollback_registered(dev);
		dev->reg_state = NETREG_UNREGISTERED;
	}

out:
	return ret;

err_uninit:
	if (dev->uninit)
		dev->uninit(dev);
	goto out;
}

3. 设备的去注册

设备从系统中拔出或者由于其它某些原因,需要从内核中将网络设备删除时,驱动会调用unregister_netdev()来完成,代码如下:

/**
 *	unregister_netdev - remove device from the kernel
 *	@dev: device
 *
 *	This function shuts down a device interface and removes it
 *	from the kernel tables.
 *
 *	This is just a wrapper for unregister_netdevice that takes
 *	the rtnl semaphore.  In general you want to use this and not
 *	unregister_netdevice.
 */
void unregister_netdev(struct net_device *dev)
{
	//首先持有信号量,之前的笔记有介绍过该信号量用来保护全局的net_device
	//组织结构,去注册当然需要修改该组织结构,所以需要持有信号量
	rtnl_lock();
	unregister_netdevice(dev);
	rtnl_unlock();
}

持锁然后调用unregister_netdevices()。

扫描二维码关注公众号,回复: 4106600 查看本文章
/**
 *	unregister_netdevice - remove device from the kernel
 *	@dev: device
 *
 *	This function shuts down a device interface and removes it
 *	from the kernel tables.
 *
 *	Callers must hold the rtnl semaphore.  You may want
 *	unregister_netdev() instead of this.
 */
void unregister_netdevice(struct net_device *dev)
{
	ASSERT_RTNL();
	//撤销注册时执行的操作
	rollback_registered(dev);
	/* Finish processing unregister after unlock */
	//将设备加入系统的todo_list中,在rtnl_unlock()时执行一些清理工作
	net_set_todo(dev);
}

/* Delayed registration/unregisteration */
//全局的net_todo_list专门用来延迟执行去注册操作。从前面的注册过程过程来看,
//并没有使用net_todo_list
static LIST_HEAD(net_todo_list);
static void net_set_todo(struct net_device *dev)
{
	list_add_tail(&dev->todo_list, &net_todo_list);
}

3.1 rollback_registered()

从上面的逻辑看来,实际的去注册工作都是由rollback_registered()完成的.

static void rollback_registered(struct net_device *dev)
{
	//预置条件判断:1)设备接口层已经初始化完毕;2)已经持有RTNETLINK信号量
	BUG_ON(dev_boot_phase);
	ASSERT_RTNL();

	//未初始化的设备不能执行去注册
	if (dev->reg_state == NETREG_UNINITIALIZED) {
		printk(KERN_DEBUG "unregister_netdevice: device %s/%p never "
				  "was registered\n", dev->name, dev);

		WARN_ON(1);
		return;
	}
	//设备当前注册状态应该是REGISTERED,即已注册状态
	BUG_ON(dev->reg_state != NETREG_REGISTERED);

	//设备可能还处于UP状态,首先关闭设备
	/* If device is running, close it first. */
	dev_close(dev);

	//将设备从名字表、索引表、设备表中移除
	unlist_netdevice(dev);

	//设置设备注册状态为UNREGISTERING,即正在去注册
	dev->reg_state = NETREG_UNREGISTERING;
	//同步其它CPU上面该设备的状态
	synchronize_net();

	/* Shutdown queueing discipline. */
	//关闭设备的发送队列
	dev_shutdown(dev);

	/* Notify protocols, that we are about to destroy
	   this device. They should clean all the things.
	*/
	//发送UNREGISTER通知给其它对这一事件感兴趣的模块
	call_netdevice_notifiers(NETDEV_UNREGISTER, dev);

	/*
	 *	Flush the unicast and multicast chains
	 */
	//清除设备的所有地址
	dev_addr_discard(dev);

	//回调驱动程序uninit()接口
	if (dev->uninit)
		dev->uninit(dev);

	/* Notifier chain MUST detach us from master device. */
	BUG_TRAP(!dev->master);

	//从统一设备模型中移除
	netdev_unregister_kobject(dev);

	synchronize_net();
	//递减设备引用计数,注意仅仅是递减,引用计数为0并不会释放内存
	dev_put(dev);
}

3.2 netdev_run_todo()

从上面的代码中可以看到,在unregister_netdevice()末尾调用net_set_todo()将待去注册设备添加到了net_todo_list中,对该链表的处理是在rtnl_unlock()中调用netdev_run_todo()。

/* The sequence is:
 *
 *	rtnl_lock();
 *	...
 *	register_netdevice(x1);
 *	register_netdevice(x2);
 *	...
 *	unregister_netdevice(y1);
 *	unregister_netdevice(y2);
 *      ...
 *	rtnl_unlock();
 *	free_netdev(y1);
 *	free_netdev(y2);
 *
 * We are invoked by rtnl_unlock().
 * This allows us to deal with problems:
 * 1) We can delete sysfs objects which invoke hotplug
 *    without deadlocking with linkwatch via keventd.
 * 2) Since we run with the RTNL semaphore not held, we can sleep
 *    safely in order to wait for the netdev refcnt to drop to zero.
 *
 * We must not return until all unregister events added during
 * the interval the lock was held have been completed.
 */
void netdev_run_todo(void)
{
	struct list_head list;

	//尽可能的缩短持有RTNETLINK信号量的时间,将net_todo_list链表用list做个快照,然后释放信号量
	/* Snapshot list, allow later requests */
	list_replace_init(&net_todo_list, &list);
	__rtnl_unlock();

	//遍历备份的net_todo_list,将其中的每一个设备从系统中移除
	while (!list_empty(&list)) {
		struct net_device *dev
			= list_entry(list.next, struct net_device, todo_list);
		list_del(&dev->todo_list);
		//检查设备的注册状态,如果已经是去注册状态了,那么是异常
		if (unlikely(dev->reg_state != NETREG_UNREGISTERING)) {
			printk(KERN_ERR "network todo '%s' but state %d\n",
			       dev->name, dev->reg_state);
			dump_stack();
			continue;
		}
		//设置注册状态为去注册状态
		dev->reg_state = NETREG_UNREGISTERED;

		on_each_cpu(flush_backlog, dev, 1);
		//等待对该设备的所有引用计数都释放
		netdev_wait_allrefs(dev);

		/* paranoia */
		BUG_ON(atomic_read(&dev->refcnt));
		WARN_ON(dev->ip_ptr);
		WARN_ON(dev->ip6_ptr);
		WARN_ON(dev->dn_ptr);
		//调用驱动的释放接口
		if (dev->destructor)
			dev->destructor(dev);
		//释放设备模型中的相关结构
		/* Free network device */
		kobject_put(&dev->dev.kobj);
	}
}

3.2.1 netdev_wait_allrefs()

在上面的去注册过程中,我们并没有看到释放struct net_device的动作。实际上,是否释放net_device是由其成员refcnt,即引用计数决定的,该引用计数的增减可以通过接口dev_hold()和dev_put()操作,在注册过程中,会将该引用计数初始化为1,在去注册过程中会将该引用计数减1,所以如果不执行去注册过程,那么引用计数是不可能为0的。

要释放net_device,必须等引用计数变为0为止,但是在去注册时,我们无法保证其它模块已经释放了对该设备的引用,所以必须有一种机制能够可靠的释放该设备:

  1. 首先,在去注册时会向外发送NETDEV_UNREGISTER事件,持有设备引用计数的模块应该关注该事件;
  2. 即使有通知,还是应该等待,因为无法保证其它子系统能够及时的处理该事件。

综上,内核使用netdev_wait_allrefs()来实现这一设计。

/*
 * netdev_wait_allrefs - wait until all references are gone.
 *
 * This is called when unregistering network devices.
 *
 * Any protocol or device that holds a reference should register
 * for netdevice notification, and cleanup and put back the
 * reference if they receive an UNREGISTER event.
 * We can get stuck here if buggy protocols don't correctly
 * call dev_put.
 */
static void netdev_wait_allrefs(struct net_device *dev)
{
	unsigned long rebroadcast_time, warning_time;

	rebroadcast_time = warning_time = jiffies;
	//循环等待,直到引用计数变为0
	while (atomic_read(&dev->refcnt) != 0) {
		//每隔1s向外发送一次NETDEV_UNREGISTER事件通知
		if (time_after(jiffies, rebroadcast_time + 1 * HZ)) {
			rtnl_lock();
			/* Rebroadcast unregister notification */
			call_netdevice_notifiers(NETDEV_UNREGISTER, dev);
			//对链路状态的处理我们在单独的笔记中介绍
			if (test_bit(__LINK_STATE_LINKWATCH_PENDING &dev->state)) {
				/* We must not have linkwatch events
				 * pending on unregister. If this
				 * happens, we simply run the queue
				 * unscheduled, resulting in a noop
				 * for this device.
				 */
				linkwatch_run_queue();
			}
			__rtnl_unlock();
			rebroadcast_time = jiffies;
		}
		//休眠250ms
		msleep(250);
		//等待每超过10s,打印一条告警信息
		if (time_after(jiffies, warning_time + 10 * HZ)) {
			printk(KERN_EMERG "unregister_netdevice: "
			       "waiting for %s to become free. Usage "
			       "count = %d\n",
			       dev->name, atomic_read(&dev->refcnt));
			warning_time = jiffies;
		}
	}
}

由于netdev_wait_allrefs()会休眠等待,所以这里需要注意的是,调用去注册过程可能会阻塞一段时间,所以禁止在原子上下文执行该过程。

4. netdevice的销毁

驱动程序在将设备从内核中去注册后,可以调用接口free_netdev()释放设备。

/**
 *	free_netdev - free network device
 *	@dev: device
 *
 *	This function does the last stage of destroying an allocated device
 * 	interface. The reference to the device object is released.
 *	If this is the last reference then it will be freed.
 */
void free_netdev(struct net_device *dev)
{
	struct napi_struct *p, *n;
	//释放对net的引用计数
	release_net(dev_net(dev));
	//释放发送队列
	kfree(dev->_tx);
	//释放分配的内存
	/* Flush device addresses */
	dev_addr_flush(dev);

	list_for_each_entry_safe(p, n, &dev->napi_list, dev_list)
		netif_napi_del(p);

	//如果分配设备后立即调用该接口,就是在未初始化状态,这里直接释放返回
	//但是正常来讲,在去注册过程中,我们是将注册状态修改为了去注册状态
	/*  Compatibility with error handling in drivers */
	if (dev->reg_state == NETREG_UNINITIALIZED) {
		kfree((char *)dev - dev->padded);
		return;
	}
	//检查注册状态必须为去注册状态
	BUG_ON(dev->reg_state != NETREG_UNREGISTERED);
	dev->reg_state = NETREG_RELEASED;
	//减少设备模型中的device引用计数
	/* will free via device release */
	put_device(&dev->dev);
}
EXPORT_SYMBOL(free_netdev);

void put_device(struct device *dev)
{
	/* might_sleep(); */
	if (dev)
		kobject_put(&dev->kobj);
}

仔细检查上面的代码,发现并无释放net_device的动作,实际上,这个工作实在设备模型中device的引用计数变为0时执行的,代码如下:

/*
 *	netdev_release -- destroy and free a dead device.
 *	Called when last reference to device kobject is gone.
 */
static void netdev_release(struct device *d)
{
	struct net_device *dev = to_net_dev(d);
	//必须是RELEASED状态
	BUG_ON(dev->reg_state != NETREG_RELEASED);

	kfree(dev->ifalias);
	//释放net_device占用内存
	kfree((char *)dev - dev->padded);
}

static struct class net_class = {
	.name = "net",
	//设备模型的析构函数
	.dev_release = netdev_release,
...
};

//如上,该函数在设备注册过程中被调用
/* Create sysfs entries for network device. */
int netdev_register_kobject(struct net_device *net)
{
	struct device *dev = &(net->dev);
...
	//设备类为net_class
	dev->class = &net_class;
...
}

5. 设备注册状态管理

从上面的代码中可以看出,贯穿整个分配、注册、去注册、释放过程,dev->reg_state的取值非常关键,它控制了整个流程,内核中定义的注册状态有如下几种:

/* register/unregister state machine */
enum { NETREG_UNINITIALIZED=0,
	   NETREG_REGISTERED,	/* completed register_netdevice */
	   NETREG_UNREGISTERING,	/* called unregister_netdevice */
	   NETREG_UNREGISTERED,	/* completed unregister todo */
	   NETREG_RELEASED,		/* called free_netdev */
	   NETREG_DUMMY,		/* dummy device for NAPI poll */
} reg_state;

目前只有NETREG_DUMMY状态没有见过,该状态在NAPI pool机制中会用到,目前忽略它即可。在整个流程中,注册状态的变迁关系见下图:
在这里插入图片描述

此外,从上图中还可以看到一个非常重要的信息是在整个流程中,框架都会回调那些net_device结构中的回调,驱动程序可以通过实现这些回调在特定的结点执行一些自己的特有逻辑。

小结

设备的注册与去注册代码流程中还涉及到了很多其它方面的内容,比如设备的打开关闭、收发队列的管理等内容,不可能在一篇笔记中就将这些所有内容都说清楚,这里我们重点把握主体流程,其它方面在对应的笔记中再check。

猜你喜欢

转载自blog.csdn.net/fanxiaoyu321/article/details/84001548