首頁 > 軟體

淺談 Linux 核心無線子系統

2020-06-16 17:32:36

淺談 Linux 核心無線子系統

本文目錄

  • 1. 全域性概覽
  • 2. 模組間介面
  • 3. 資料路徑與管理路徑
  • 4. 封包是如何被傳送?
  • 5. 談談管理路徑
  • 6. 封包又是如何被接收?
  • 7. 總結一下

Linux 核心是如何實現無線網路介面呢?封包是通過怎樣的方式被傳送和接收呢?
剛開始工作接觸 Linux 無線網路時,我曾迷失在浩瀚的基礎程式碼中,尋找具有介紹性的材料來回答如上面提到的那些高層次的問題。
跟蹤探索了一段時間的原始碼後,我寫下了這篇總結,希望在 Linux 無線網路的工作原理上,讀者能從這篇文章獲得一個具有幫助性的概覽。

1 全域性概覽

在開始探索 Linux 無線具體細節之前,讓我們先來把握一下 Linux 無線子系統整體結構。如圖1,展示了 Linux 無線子系統各個模組之間的抽象關係。

圖一 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

本文永久更新連結地址http://www.linuxidc.com/Linux/2016-09/135382.htm


IT145.com E-mail:sddin#qq.com