Linux kernel中网络设备的管理

时间:2021-10-07 15:36:03

kernel中使用net_device结构来描述网络设备,这个结构是网络驱动及接口层中最重要的结构。该结构不仅描述了接口方面的信息,还包括硬件信息,致使该结构很大很复杂。通过这个结构,内核在底层的网络驱动和网络层之间构建了一个网络接口核心层(这个叫法引自《TCP迁移报告》),这个中间层类似于文件子系统的VFS。这样底层的驱动程序就不需要过多地关注上层的网络协议,只需要通过内核提供的网络接口核心层就可以很方便将和网络层进行数据的交互。而网络层在向下发送数据时,只需要通过内核提供的这个中间层进行交互即可,不需要关心底层究竟是什么类型的网卡。

1、注册网络设备
  网络设备通常在内核启动时或者插拔网络设备时注册,由网络设备驱动负责。网络设备驱动会首先根据自己的网卡类型调用相应的分配net_device结构的函数(例如以太网网卡可以调用alloc_etherdev(),当然也可以直接调用alloc_netdev()),然后初始化网卡相关的成员,最后调用register_netdev()来完成网络设备的注册。每个网络设备在系统中都要有一个唯一的名称,通常以网卡类型的一个缩写为前缀,后面跟着数字,例如,eth1就是一个以太网卡。register_netdev()是对register_netdevice()的包装函数。在调用register_netdev()注册设备时,如果指定的名称中包含%d格式串(只支持%d),内核会选择一个适当的数字来替换格式化串,真正的注册工作由register_netdevice()来完成。
在调用register_netdevice()之前,必须要先调用rtnl_lock()获取rtnl_mutex互斥锁,任何网络配置信息改变时都要首先获取rtnl_mutex互斥锁。注册过程如下所示:

Linux kernel中网络设备的管理

  net_device结构的netdev_ops由驱动程序初始化,存储的是设备相关的操作。如果设置了初始化函数,则通过ndo_init成员来进行设备相关的初始化操作
dev_valid_name()用来检查设备名是否为空或者包含不合法的字符(例如‘/’、空白字符),并不会检查名称是否冲突。
dev_new_index()为设备分配一个可用的索引号,用来标识设备,索引号由一个32位计数器(dev_new_index()中定义的静态变量)产生,每当一个新的设备添加到系统中时,计数器都会加1,然后检查该索引号是否已经使用,如果可用,则返回,否则继续加1.
在检查完合法性后,会调用dev_name_hash()找到在dev_name_head散列表(网络命名空间net结构中的成员)中的槽位,即冲突链表的头,然后在这个冲突链表中查找是否已存在相同名称的设备。如果找到相同的,则返回错误,终止注册过程。
接下来是是对设备的特性进行检查,看是否冲突,并进行调整。
netdev_register_kobject()用来在sysfs中创建跟设备关联的项,网络设备的索引号以及状态信息可以通过/sys/class/net/eth0(eth0为设备名,不同设备名称不同)目录下的项来查看。
在完成上述的操作后,内核会调用list_netdevice()将设备添加到网络名称空间中的dev_base_head链表、dev_name_head和dev_index_head散列表中,最后发送NETDEV_REGISTER消息到netdev_chain通知链上,通知对设备注册感兴趣的内核组件。
注册到系统中的所有网络设备都会添加到dev_base_head链表、dev_name_head和dev_index_head散列表中,其中dev_list按照FIFO顺序添加,加入name_hlist散列表是根据名称计算出的哈希值添加,加入index_hlist散列表是根据设备索引号计算的哈希值添加。它们的关系如下所示:

Linux kernel中网络设备的管理

有了dev_name_head和dev_index_head散列表,可以分别通过dev_get_by_name()和dev_get_by_index()来根据设备名称或索引来获取网络设备。

2、启用网络设备

  设备注册后即可使用,但必须在用户或用户空间应用程序开启后才能收发数据。因为注册到系统中的网络设备,其初始状态是关闭的,此时不能传送数据。用户可以通过"ifconfig 设备名 up"命令来启用,该命令(ioctl()的SIOCSIFFLAGS命令)通过dev_change_flags()调用dev_open()来激活网络设备。
启用网络设备后,会设置IFF_UP标志,如果该标志已经设置,则不用再继续操作,直接返回。如果设备被挂起(休眠状态,电源管理相关)或已经移除,则不能启用,返回ENODEV错误。
设备的启用主要是调用驱动提供的ndo_open接口(存储在net_device结构的netdev_ops成员上)来完成的,除此之外,在开启之前会给netdev_chain通知链上发送NETDEV_PRE_UP消息。开启成功后会给netdev_chain通知链上发送NETDEV_UP消息。
启用流程如下所示:

Linux kernel中网络设备的管理

3、禁用网络设备

  网络设备能够被用户命令或其他事件隐含地禁止。用户可以通过"ifconfig 设备名 down"来禁止,最终也是通过ioctl的SIOCSIFFLAGS命令来关闭设备。注意,禁用只是说不能再用这个设备来收发数据了,网络设备依然还是注册的,还可以启用。
禁用设备时,会通过调用dev_chang_flags()调用dev_close()来进行。
  dev_close会首先检查设备是否已禁用,即检查是否设置IFF_UP标志,如果已禁用,则直接返回。
  在启用设备时,会设置__LINK_STATE_START标志,表示设备可以传递数据,所以在禁用时要清除该标志位。在清除标志位后,会发送NETDEV_GOING_DOWN消息给netdev_chain通知链,通知对禁用设备感兴趣的内核组件。
  接着会调用dev_deactivate()禁止出口队列规则,确保该设备不再用于传输,并停止不再需要的监控定时器。
  同样,设备的禁用还是要靠驱动提供的接口来完成,这里调用的ndo_stop接口。只要设备是启用的,调用该接口就不能失败,而且内核在这里也没有检查其返回值。所以驱动在提供这个接口时一定要注意这一点。这个接口在DETACH hot-plug事件(应该是热插拔网络设备)后也允许被调用。
调用设备相关的关闭操作后,会清除IFF_UP标志位,然后发送NETDEV_DOWN消息到netdev_chain通知链上。
  禁用流程如下所示:

Linux kernel中网络设备的管理

4、注销网络设备

  网络设备的注销比较复杂,注销网络设备通过调用unregister_netdev()完成。注销过程主要分为两个阶段,分别由unregister_netdevice()和netdev_run_todo()完成。
  unregister_netdevice()负责关闭设备并将其从内核的表中移除,调用设备相关的注销操作,完成后会将待注销的设备放置到net_todo_list队列中。
  unregister_netdevice()中通过调用rollback_registered()和net_set_todo()来完成主要的注销操作。
  net_set_todo()比较简单,只是将待注销的设备放到net_todo_list队列上。
  这里主要看rollback_registered()的操作。在注销之前,要先调用dev_close()来禁用设备,这样之后就不会再使用该设备来收发数据。在注销时会调用list_netdevice()将设备添加到内核的管理结构中,注销时要调用unlist_netdevice()将设备从这些管理结构中移除。在注销的时候可能设备正在接收数据,所以要调用synchronize_net()来等待设备接收完正在接收的数据包。接下来会调用dev_shutdown()来释放所有与设备相关的队列规则实例。在完成这些操作后,会发送NETDEV_UNREGISTER消息给netdev_chain通知链,通知其他使用设备的内核组件进行一些清理,以完成后续的释放操作。在完成内核结构中的清理后,真正的注销操作还是要有网络设备驱动来做,通过调用ndo_uninit接口来进行驱动程序相关的注销操作。最后会释放sysfs中相应的项并释放对网络设备的引用。
  这里有一点要注意,注册网络设备时,引用计数被初始化为1,但网络设备不像其他内核对象在引用数为0时由xxx_put()释放,而是直到从内核注销时,即调用unregister_netdevice()时引用计数才减为0,然后释放设备。
  netdev_run_todo()会将net_todo_list队列上的设备取下,等待设备的引用计数为0,然后释放网络设备对应的kobject对象(过程中会释放net_device结构占用的内存)。     netdev_run_todo()是通过rtnl_unlock()调用的,也就是说在每次释放rtnl_mutex互斥锁的时候都会继续处理待注销的设备。netdev_run_todo()会调用netdev_wait_allrefs()等待设备的引用计数为0,如果在其他组件仍然使用时直接销毁设备的管理结构会出现问题。在等待的过程中,netdev_wait_allrefs()会每秒发送一个NETDEV_UNREGISTER通知,每10s打印一次警告信息(通过系统日志可以看到)。在引用计数为0后,就可以真正销毁设备的管理结构了。设备相关的销毁操作通过destructor接口(net_device结构的成员)来完成,通过这个接口可以调用到free_netdev()函数。free_netdev()中会完成最后阶段的销毁过程,释放设备的接收队列及对应的device结构实例,并将设备状态设置为NETREG_RELEASED状态,表示即将释放网络设备net_device结构实例。net_device结构实例在netdev_run_todo()调用的kobject_put()中释放,调用的函数是netdev_release(),这个函数才会真正释放net_device结构的实例(如果是未注册的设备,在free_netdev()中释放),至此终于将设备从系统中彻底清除。
注销流程如下所示:

Linux kernel中网络设备的管理

5、网络设备状态迁移通知

  在网络设备注册、启用、禁用、注销过程中,总是伴随着状态的改变,每次在发生改变之前或之后,内核总是会发送相应类型的消息到netdev_chain通知链上。内核中的模块如果注册到netdev_chain通知链上,网络设备相关的事件都会通知该模块。如果用户层应用程序要获取这些事件消息,要注册到netlink的RTMGRP_LINK组播组,这样应用程序也可以接收到网络设备相关的事件通知。
  在内核中注册netdev_chain通知链时,你的handler中一定不要调用或者隐含调用rtnl_lock()来获取rtnl_mutex互斥锁。因为在事件改变的时候相应的处理函数中都会在获取rtnl_mutex互斥锁的情况下进行,所以在发送消息的时候已经持有了rtnl_mutex互斥锁。如果你在自己的handler中也尝试去获取rtn_mutex互斥锁,则会造成死锁 。