前言
Linux 内核是如何实现无线网络接口呢?数据包是通过怎样的方式被发送和接收呢?
刚开始工作接触 Linux 无线网络时,我曾迷失在浩瀚的基础代码中,寻找具有介绍性的材料来回答如上面提到的那些高层次的问题。
跟踪探索了一段时间的源代码后,我写下了这篇总结,希望在 Linux 无线网络的工作原理上,读者能从这篇文章获得一个具有帮助性的概览。
1.全局概览
在开始探索 Linux 无线具体细节之前,让我们先来把握一下 Linux 无线子系统整体结构。如图1,展示了 Linux 无线子系统各个模块之间的抽象关系。
图示中的虚线内展示的是内核空间的情况。用户空间的程序运行在最上层,而硬件相关的设备则在最下面。
图示中左边为以太网设备,右边为 WiFi 设备。
正如图中看到的一样,存在着两种 WiFi 设备,具体是哪一类要看 IEEE802.11 标准的 MLME 如何实现。
如果直接通过硬件实现,那么设备就是硬 MAC (full MAC)设备;如果通过软件的方式实现,那么设备就是软 MAC (soft MAC)设备。现阶段大部分无线设备都是软件实现的软 MAC 设备。
通常我们把 Linux 内核无线子系统看成两大块: cfg80211 和 mac80211 ,它们连通内核其他模块和用户空间的应用程序。
特别指出, cfg80211 在内核空间提供配置管理服务,内核与应用层通过 nl80211 实现配置管理接口。需要记住的是,
硬 MAC 设备和软 MAC 设备都需要 cfg80211 才能工作。而 mac80211 只是一个驱动 API ,它只支持软件实现的软 MAC 设备。
接下来,我们主要关注软 MAC 设备。
Linux 内核无线子系统统一各种 WiFi 设备,并处理 OSI 模型中最底层的 MAC 、 PHY 两层。
若进一步划分, MAC 层可以分为 MAC 高层和 MAC 底层。前者负责管理 MAC 层无线网络的探测发现、身份认证、关联等;
后者实现 MAC 层如 ACK 等紧急操作。大部分情况下,硬件(如无线适配器)处理大部分的 PHY 层以及 MAC 底层操作。Linux 子系统实现大部分的 MAC 高层回调函数。
2 模块间接口
从图一中我们可以看出,各个模块之间分界线很清晰,并且模块间相互透明不可见。模块之间一般不会相互影响。
举个例子,我们在 WiFi 设备驱动做修改(如,打补丁、添加新的 WiFi 驱动等),这些变更并不会影响到 mac80211 模块,
所以我们根本不用改动 mac80211 的代码。再如,添加一个新的网络协议理论上是不用修改套接字层以及设备无关层代码。一般情况下,内核通过一系列的函数指针实现各层之间相互透明。
如下代码展示 rtl73usb 无线网卡驱动与 mac80211 的联系。
static const struct ieee80211_ops** rt73usb_mac80211_ops = {
.tx = rt2x00mac_tx,
.start = rt2x00mac_start,
.stop = rt2x00mac_stop,
.add_interface = rt2x00mac_add_interface,
.remove_interface = rt2x00mac_remove_interface,
.config = rt2x00mac_config,
.configure_filter = rt2x00mac_configure_filter,
.set_tim = rt2x00mac_set_tim,
.set_key = rt2x00mac_set_key,
.sw_scan_start = rt2x00mac_sw_scan_start,
.sw_scan_complete = rt2x00mac_sw_scan_complete,
.get_stats = rt2x00mac_get_stats,
.bss_info_changed = rt2x00mac_bss_info_changed,
.conf_tx = rt73usb_conf_tx,
.get_tsf = rt73usb_get_tsf,
.rfkill_poll = rt2x00mac_rfkill_poll,
.flush = rt2x00mac_flush,
.set_antenna = rt2x00mac_set_antenna,
.get_antenna = rt2x00mac_get_antenna,
.get_ringparam = rt2x00mac_get_ringparam,
.tx_frames_pending = rt2x00mac_tx_frames_pending,
};
左侧是 mac80211 为 WiFi 驱动模块实现的 ieee80211_ops 结构体形式的回调接口,回调函数的具体内容由驱动层实现。
显然,不同设备相应驱动的实现不同。结构体 ieee80211_ops 负责将不同设备驱动实现的回调函数与 mac80211 提供的 API 映射绑定起来。
当驱动模块插入注册时,这些回调函数就被注册到 mac80211 里面(通过 ieee80211_alloc_hw 实现),接着 mac80211 就绑定了相应的回调函数,根本不用知道具体的名字,以及实现细节等。
完整定义的 ieee80211_ops 结构包含很多成员,但不是所有都必须要驱动层实现。一般而言,实现的前七个成员函数就足够了。但是,要想正确实现其他功能,某些相关的成员函数就需要被实现,就像上面的例子一样。
3 数据路径与管理路径
图一所示中,存在两条主要路径:数据路径和管理路径。数据路径对应 IEEE802.11 数据帧,而管理路径对应着控制帧。
在 IEEE802.11 的控制帧中,大部分用于如 ACK 这类时间紧急的操作,并且一般直接由硬件实现。一个例外可能就是 PS-Poll 帧(用于 Power Save 控制),它也可以由 mac80211 实现。
数据和管理路径在 mac80211 里面是分开实现的。
4数据包是如何被发送?
接下来,我们集中探讨下数据的发送过程。
首先,数据包起源于用户空间的应用程序,应用程序首先创建一个套接字,然后绑定一个接口(如,以太网接口、 WiFi 接口)。
接下来将数据写入到套接字缓冲区,最后再将缓冲区的数据发送出去。在套接字创建时,我们需要指明将要使用的协议族,这将在内核中起作用。
刚才这些发生在图一中的 Data Application 模块中,最终应用程序陷入系统调用,随后在内核空间进行接下来的工作。
数据的传输首先经过套接字层,这个过程中一个最重要的数据结构就是 sk_buff ,一般称为 skb 。一个 skb 结构中的成员包含着缓冲区的地址以及数据长度。
它还为内核中不同层对数据的操纵提供了良好的支持;实现了众多的接口,如,不同网络层首部的插入与去除等。整个数据的发送/接收过程均会用到这个结构。
我们跳过网络协议模块,对于网络协议我没有太多想说的,因为一旦涉及网络协议,简直说不尽道不完。在这里协议并不是我们主要关心的。
不过我们需要知道的是,数据传输使用的协议在套接字创建的时候就与指定的协议绑定了,然后相关的协议便会负责相关层的数据传输。
接下来,数据由网络层落到了设备无关层。这一层透明的连接着各种各样的硬件设备(如以太网设备、 WiFi 设备等)。
设备无关层一个重要的结构是: net_device 。我们回去看图一,再看接下来的代码就能解释内核是如何与以太网设备驱动通信的。
具体接口通过 net_device_ops 结构实现,该结构对应了 net_device 的很多操作。
如下是 net_device_ops 结构的部分成员:
struct net_device_ops {
int (*ndo_init)(struct net_device *dev);
void (*ndo_uninit)(struct net_device *dev);
int (*ndo_open)(struct net_device *dev);
int (*ndo_stop)(struct net_device *dev);
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
struct net_device *dev);
netdev_features_t (*ndo_features_check)(struct sk_buff *skb,
struct net_device *dev,
netdev_features_t features);
u16 (*ndo_select_queue)(struct net_device *dev,
struct sk_buff *skb,
void *accel_priv,
select_queue_fallback_t fallback);
void (*ndo_change_rx_flags)(struct net_device *dev,
int flags);
void (*ndo_set_rx_mode)(struct net_device *dev);
int (*ndo_set_mac_address)(struct net_device *dev,
void *addr);
int (*ndo_validate_addr)(struct net_device *dev);
int (*ndo_do_ioctl)(struct net_device *dev,
struct ifreq *ifr, int cmd);
int (*ndo_set_config)(struct net_device *dev,
struct ifmap *map);
int (*ndo_change_mtu)(struct net_device *dev,
int new_mtu);
int (*ndo_neigh_setup)(struct net_device *dev,
struct neigh_parms *);
void (*ndo_tx_timeout) (struct net_device *dev);
struct rtnl_link_stats64* (*ndo_get_stats64)(struct net_device *dev,
struct rtnl_link_stats64 *storage);
struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);
int (*ndo_vlan_rx_add_vid)(struct net_device *dev,
__be16 proto, u16 vid);
int (*ndo_vlan_rx_kill_vid)(struct net_device *dev,
...
...
};
发包的时候, skb 在调用 dev_queue_xmit 时被传入。在跟踪具体调用关系后,最终是这样调用的: ops->ndo_start_xmit(skb, dev) 。
注意,刚才的这个函数需要注册才能生效。
对于 WiFi 设备而言,通常我们使用 mac80211 (代替了相应的设备驱动),那是因为 mac80211 已经帮我们注册了。
从 net/mac80211/iface.c 可以看到:
static const struct net_device_ops ieee80211_dataif_ops = {
.ndo_open = ieee80211_open,
.ndo_stop = ieee80211_stop,
.ndo_uninit = ieee80211_uninit,
.ndo_start_xmit = ieee80211_subif_start_xmit,
.ndo_set_rx_mode = ieee80211_set_multicast_list,
.ndo_change_mtu = ieee80211_change_mtu,
.ndo_set_mac_address = ieee80211_change_mac,
.ndo_select_queue = ieee80211_netdev_select_queue,
.ndo_get_stats64 = ieee80211_get_stats64,
};
因此 mac80211 也就可以看作是一个 net_device ,当一个数据包通过 WiFi 传输时,相关的传输函数 ieee80211_subif_start_xmit 将被调用。
我们进入 mac80211 内部 ieee80211_subif_start_xmit 实现可以看到这样一个调用子序列:ieee80211_xmit => ieee80211_tx => ieee80211_tx_frags => drv_tx
目前我们处在 mac80211 和 WiFi 驱动的边界, drv_tx 仅仅调用了一个在 WiFi 驱动层实现的并已注册的回调函数。
static inline void drv_tx(struct ieee80211_local *local, struct ieee80211_tx_control *control, struct sk_buff *skb)
{
local->ops->tx(&local->hw, control, skb);
}
到这里, mac80211 就结束了,并且设备驱动相关也暂时告一段落了。
正如之前提到的一样,通过 mac80211 中的 local->ops->tx ,注册到设备驱动中的回调函数将会被调用。尽管每个驱动对相应回调函数的实现不尽相同。
下面利用之前模块间接口的例子。结构体成员 tx 对应的函数 rt2x00max_tx 首先需要填充准备发送描述符(一般包含帧长度、ACK 策略、 RTS/CTS、重传时限、分片标志以及 MCS 等)
部分信息由 mac80211 传下来(结构体 ieee80211_tx_info 中就有一些信息将会被使用到),然后驱动程序还要将数据转换成底层硬件可识别的形式。
一旦发送描述符就位,驱动程序还会调整帧数据(如,调整字节对齐等),然后将数据帧放入发送队列,最后将要发送的帧的描述符发给硬件。
由于我们以一个基于 rt73usb 的 USB WiFi 适配器为例,所以数据帧最后是通过 USB 接口发送给无线设备。
然后数据将被插入 PHY 首部以及其他信息,最后数据包被发送到了空中。驱动同时也需要反馈发送状态给 mac80211 , 通常状态信息存放在 struct ieee80211_tx_info 中。
经过 ieee80211_tx_status 一系列的调用,或者某些变种函数反馈给上层。
说到这里,关于数据包的发送也暂时告一段落了。
5谈谈管理路径
理论上,我们可以像数据路径一样在用户空间下通过套接字发送控制帧。但是目前有很多开发得十分完善的用户层管理工具能完成这样的工作。
特别是 wpa_supplicant 和 host_apd 。wpa_supplicant 控制客户端 STA 模式下无线网络的连接,如扫描发现网络、身份认证、关联等。
而 host_apd 可以做 AP 。说白了前者就是用来连接热点,后者用来发射热点。这些用户层工具通过 netlink 套接字与内核通信。
内核中相关的回调接口是 cfg80211 中的 nl80211 。用户层的工具通过 netlink 提供的库(如, NL80211_CMD_TRIGGER_SCAN )将命令发送到内核。
在内核中,由 nl80211 接收应用层发出的命令。如下代码展示了对应绑定情况。
static const struct genl_ops nl80211_ops[] = {
{
.cmd = NL80211_CMD_GET_WIPHY,
.doit = nl80211_get_wiphy,
.dumpit = nl80211_dump_wiphy,
.done = nl80211_dump_wiphy_done,
.policy = nl80211_policy,
/* can be retrieved by unprivileged users */
.internal_flags = NL80211_FLAG_NEED_WIPHY |
NL80211_FLAG_NEED_RTNL,
},
...
...
{
.cmd = NL80211_CMD_TRIGGER_SCAN,
.doit = nl80211_trigger_scan,
.policy = nl80211_policy,
.flags = GENL_ADMIN_PERM,
.internal_flags = NL80211_FLAG_NEED_WDEV_UP | NL80211_FLAG_NEED_RTNL,
},
...
...
};
以 triggering scan 为例,扫描请求从 cfg80211 到 mac80211 是通过 mac80211 在 cfg80211 中注册的回调函数来实现的。
const struct cfg80211_ops mac80211_config_ops = {
...
.scan = ieee80211_scan,
...
};
在 mac80211 中, ieee80211_scan 将会具体去实现扫描发现网络的具体细节。
=>ieee80211_scan_state_send_probe
=>ieee80211_send_probe_req
=>ieee80211_tx_skb_tid_band
=>ieee80211_xmit
=>ieee80211_tx
=>ieee80211_tx_frags
=>drv_txj
6 数据包又是如何被接收?
我们接下来反过来看看数据接收的过程,现在我们不再比较数据路径与管理路径的不同了。相信读者同样能明白。
当一个数据包在空中被无线设备捕捉到后,硬件将会向内核发出一个中断(大部分 PCI 接口的设备这样做),或则通过轮询机制判断是否有数据到来(如,使用了 USB 接口)。
前者,中断将会引发中断处理程序的执行,后者促使特定的接收函数将被调用。
一般设备驱动层的回调函数不会做太多关于接收数据包的操作,仅仅做数据校验,为 mac80211 填充接收描述符,然后把数据包推给 mac80211 , 由 mac80211 来做之后的工作(直接或间接将数据包放入接收队列)。
数据进入 mac80211 后,将会调用 ieee80211_rx 或者其他变种接收函数。在这里数据路径和管理路径也将分开进行。
如果收到的帧是数据,它将被转换成 802.3 数据帧(通过 __ieee80211_data_to8023 实现),然后该数据帧将通过 netif_receive_skb 交付到网络协议栈。在协议栈中,各层网络协议将会对数据进行解析,识别协议首部。
如果接收到的是控制帧,数据将会由 ieee80211_sta_rx_queued_mgmt 处理。部分控制帧在 mac80211 层就终止,另外一些将会通过 cfg80211 发送到用户空间下的管理程序。
例如,身份认证控制帧被 cfg80211_rx_mlme_mgmt 处理,然后通过 nl80211_send_rx_auth 发送到用户空间下的 wpa_supplicant ; 相应的关联响应控制帧被 cfg80211_rx_assoc_resp 处理,并由 nl80211_send_rx_assoc 发送到用户空间。
7 总结一下
一般 WiFi 驱动包含如下三个部分:配置、发送回调、接收回调。再以 USB WiFi 适配器为例,当内核探测到设备被插入时,会调用 probe 函数。这可能发生在注册配置好的 ieee80211_ops 时。
首先, ieee80211_alloc_hw 分配一个 ieee80211_hw 结构体,代表着相应 WiFi 设备。另外,如下的数据结构也会被分配:
- wiphy 结构:主要用来描述 WiFi 硬件参数(如, MAC 地址、接口模式与组合、支持的波特率以及其他一些硬件功能)。
- ieee80211_local 结构:这是一个设备驱动层可见的结构,并且被 mac80211 大量使用。ieee80211_ops 的映射绑定将链接到 ieee80211_local 中。 前者作为后者的一个成员。在 ieee80211_hw 中可以通过 container_of 或者 hw_to_local 这个专用 API 得到 ieee80211_local 。
- 设备驱动使用到的在 ieee80211_hw 中的私有结构 void *priv 。
注意:硬件设备的注册由 ieee80211_register_hw 完成,前提是事先已经插入注册了 mac80211 模块,好比在 STA 模式中,要先用 wpa_supplicant 控制设备连接上了某个热点才能进行通信一样。
最后希望这篇总结能让相关人员在探索源代码时具有整体把握。
参考原文:https://www.linux.com/blog/linux-wireless-networking-short-walk
参考资料:https://wireless.wiki.kernel.org/en/developers/documentation/glossary
转载原文:浅谈 Linux 内核无线子系统