內核

淺談 Linux 內核無線子系統

本文章節

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

1 全局概覽

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

arch
圖一 Linux 無線網路結構示意圖

圖示中的虛線內展示的是內核空間的情況。用戶空間的程序運行在最上層,而硬體相關的設備則在最下面。圖示中左邊為乙太網設備,右邊為 WiFi 設備。
正如圖中看到的一樣,存在著兩種 WiFi 設備,具體是哪一類要看 IEEE802.11 標準的 MLME 如何實現。
如果直接通過硬體實現,那麼設備就是硬 MAC (full MAC)設備;如果通過軟體的方式實現,那麼設備就是軟 MAC (soft MAC)設備。現階段大部分無線設備都是軟體實現的軟 MAC 設備。

通常我們把 Linux 內核無線子系統看成兩大塊: cfg80211mac80211 ,它們連通內核其他模塊和用戶空間的應用程序。
特別指出, 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

本文鏈接:https://linuxstory.org/linux-wireless

轉載請註明出處,否則必究相關責任!

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:內核

    內核

    rootfs initramfs kexec 與 Linux 啟動過程

    作為 Debian 用戶,在使用 apt 更新系統時偶爾會發現某次安裝更新的時間特別長,這往往出現在較大版本的更新中,仔細觀察後就會發現,這個耗時極長的操作並不是安裝某個軟體,而是對一個名為 init […]
    內核

    龍芯開始發布針對3A6000系列CPU的Linux補丁

    儘管龍芯3A6000處理器尚未正式推出,但自去年以來的傳言將其定於在今年上半年推出,並有人聲稱這種性能提升可以與AMD Zen 3或英特爾Tiger Lake的性能水平相媲美。在3A6000系列推出之 […]
    內核

    Linux 共享庫的 soname 命名機制

    Linux 有一套規則來命名系統中的每一個共享庫,它規定共享庫的文件命名規則如下:libname.so.x.y.z,即前綴"lib"+庫名稱+後綴".so"+三個數字組成的版本號,其中,x 表示主版本號,y 表示次版本號,z 表示發布版本號。SO-NAME 命名機制,就是把共享庫的文件名去掉次版本號和發布版本號,只保留主版本號。在 Linux 系統中,系統會為每個共享庫在它所在的目錄創建一個跟它的 」SO-NAME」 一樣的軟鏈接指向它。