Linux下wifi子系统的数据流

对比下蓝牙的数据流:

Linux下蓝牙框架的数据流-CSDN博客

这里梳理下wifi的
注意,wifi子系统分为控制路径和数据路径,不要搞混了

关于控制路径,参考这篇:

现代Linux下的wifi框架-CSDN博客

下行数据流

WiFi 子系统架构回顾

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│ 用户空间应用程序 (如 iw, wpa_supplicant)                            │
│                         ↓                                           │
│ 内核: nl80211/cfg80211 (配置与管理层)                               │
│                         ↓                                           │
│ 内核: mac80211 (软件MAC层)                                          │
│                         ↓                                           │
│ 内核: 设备驱动 (如 ath9k, iwlwifi)                                  │
│                         ↓                                           │
│ 硬件设备                                                             │
└─────────────────────────────────────────────────────────────────────┘

关键理解 :这个架构图主要展示的是控制路径 (配置、连接管理)。对于数据路径(实际收发网络数据),还需要叠加完整的网络协议栈。

而数据路径,就是常规的socket编程

📡 完整的数据下行路径

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        用户空间                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  网络应用 (浏览器 / curl / ping)                            │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓ socket()                             │
├─────────────────────────────────────────────────────────────────────┤
│                     内核网络协议栈                                   │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  传输层 (TCP/UDP) - 分段、拥塞控制                          │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  网络层 (IP) - 路由、分片、Netfilter                        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  邻居层 (ARP) - MAC 地址解析                                │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  网络设备层 - qdisc 排队、dev_queue_xmit()                  │   │
│  └─────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│                     WiFi 子系统 (你的架构图)                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  cfg80211 (配置层) - 本例中不直接参与数据路径               │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  mac80211 (软件MAC层) - 核心处理                            │   │
│  │  - 802.3 → 802.11 帧转换                                    │   │
│  │  - 速率控制、分片、加密、聚合                               │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  设备驱动 (ath9k/iwlwifi) - 硬件操作                        │   │
│  │  - 填充发送描述符                                           │   │
│  │  - DMA 映射                                                 │   │
│  │  - 写 Tx Ring,通知硬件                                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓ USB/PCIe/SDIO                        │
├─────────────────────────────────────────────────────────────────────┤
│                        硬件层                                        │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  WiFi 芯片 - 空中发送                                       │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

📦 逐层详解

第一层:用户空间应用程序

复制代码
// 应用程序发送数据
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, ...);
send(sock, "Hello WiFi", 10, 0);

这一层产生的是纯应用数据,完全不知道下面会经过 WiFi。


第二层:内核网络协议栈(TCP/IP)

这是 WiFi 与蓝牙的关键区别------WiFi 数据经过完整的 TCP/IP 栈:

复制代码
// net/ipv4/tcp.c - TCP 层
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    // 1. 拷贝用户数据到内核
    // 2. 根据 MSS 分段
    // 3. 添加 TCP 头
    // 4. 加入发送队列
    tcp_push(sk, flags, mss_now, nonagle);
}

// net/ipv4/ip_output.c - IP 层
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
    // 1. 添加 IP 头
    // 2. 路由查找 (决定从 wlan0 出去)
    // 3. 分片处理
    // 4. 调用邻居层
    ip_local_out(net, sk, skb);
}

第三层:网络设备层

复制代码
// net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb)
{
    struct net_device *dev = skb->dev;  // wlan0
    
    // 1. 选择发送队列
    txq = netdev_pick_tx(dev, skb, NULL);
    
    // 2. 应用 qdisc 排队规则 (如 fq_codel)
    qdisc = rcu_dereference_bh(txq->qdisc);
    
    // 3. 入队并调度
    return qdisc->enqueue(skb, qdisc, &to_free);
}

第四层:cfg80211(配置层)

重要 :在数据路径中,cfg80211 不直接参与 。它主要负责:

  • 设备注册 (wiphy_register)

  • 配置管理 (扫描、连接、密钥设置)

  • 监管合规

数据路径绕过 cfg80211,直接从 net_device 进入 mac80211。

复制代码
// net/mac80211/iface.c
static const struct net_device_ops ieee80211_dataif_ops = {
    .ndo_start_xmit     = ieee80211_subif_start_xmit,  // 数据入口!
    .ndo_open           = ieee80211_open,
    .ndo_stop           = ieee80211_stop,
};

第五层:mac80211(核心处理)

这是 WiFi 数据路径的最关键层

5.1 入口函数

复制代码
// net/mac80211/tx.c
static int ieee80211_subif_start_xmit(struct sk_buff *skb,
                                       struct net_device *dev)
{
    struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
    struct ethhdr *ehdr = (struct ethhdr *)skb->data;
    
    // 1. 检查接口状态
    // 2. 处理 VLAN 和桥接
    // 3. 调用核心发送函数
    return ieee80211_xmit(sdata, skb, IEEE80211_TX_CTL_SEND_AFTER_DTIM);
}

5.2 802.3 → 802.11 帧转换

复制代码
// 帧转换的关键逻辑
static void ieee80211_8023_to_80211(struct sk_buff *skb,
                                     struct ieee80211_sub_if_data *sdata)
{
    struct ethhdr *ehdr = (struct ethhdr *)skb->data;
    u8 *da = ehdr->h_dest;   // 目的 MAC
    u8 *sa = ehdr->h_source;  // 源 MAC
    
    // 根据接口类型决定 802.11 地址字段填充方式
    
    // STA 模式 (连接到 AP):
    //   Addr1 = AP 的 MAC (接收端)
    //   Addr2 = 自己的 MAC (发送端)
    //   Addr3 = 目的 MAC (最终目标)
    
    // AP 模式:
    //   Addr1 = 目的 MAC (接收端)
    //   Addr2 = 自己的 MAC (发送端)
    //   Addr3 = 源 MAC (原始发送者)
    
    // IBSS (Ad-hoc) 模式:
    //   Addr1 = 目的 MAC
    //   Addr2 = 自己的 MAC
    //   Addr3 = BSSID
}

5.3 发送处理链

复制代码
// net/mac80211/tx.c
static void invoke_tx_handlers(struct ieee80211_tx_data *tx)
{
    // 依次调用各处理器
    ieee80211_tx_h_rate_ctrl(tx);      // 1. 速率选择 (minstrel)
    ieee80211_tx_h_fragment(tx);       // 2. 分片 (如果超过阈值)
    ieee80211_tx_h_encrypt(tx);        // 3. 加密 (WEP/TKIP/CCMP)
    ieee80211_tx_h_amsdu(tx);          // 4. A-MSDU 聚合
    ieee80211_tx_h_sequence(tx);       // 5. 分配序列号
    ieee80211_tx_h_ampdu(tx);          // 6. A-MPDU 聚合
    ieee80211_tx_h_check_control(tx);  // 7. 控制字段检查
    ieee80211_tx_h_driver(tx);         // 8. 调用驱动
}

5.4 速率控制

复制代码
// net/mac80211/rc80211_minstrel.c
static void minstrel_tx_status(struct ieee80211_hw *hw,
                                struct ieee80211_tx_status *st)
{
    // 根据发送成功/失败,调整速率
    // 动态选择最优速率 (MCS 索引)
}

第六层:设备驱动

复制代码
// drivers/net/wireless/ath/ath9k/xmit.c
static void ath9k_tx(struct ieee80211_hw *hw,
                     struct ieee80211_tx_control *control,
                     struct sk_buff *skb)
{
    struct ath_softc *sc = hw->priv;
    struct ath_common *common = ath9k_hw_common(sc->sc_ah);
    
    // 1. 填充硬件发送描述符
    // 2. DMA 映射 (将 skb 数据映射到硬件可访问的内存)
    // 3. 将描述符加入发送环 (Tx Ring)
    // 4. 更新发送环写指针,通知硬件有数据待发
    
    ath_tx_start(sc, skb);
}

📊 数据包格式变化追踪

层级 数据包格式 示例数据
应用层 应用数据 "Hello WiFi"
TCP 层 TCP 段 TCP Header + "Hello WiFi"
IP 层 IP 包 IP Header + TCP Header + "Hello WiFi"
以太网层 802.3 帧 ETH Header + IP Header + TCP Header + "Hello WiFi"
mac80211 802.11 帧 802.11 Header + LLC/SNAP + IP Header + TCP Header + "Hello WiFi"
硬件 无线电帧 经过调制的无线电信号

🔄 与蓝牙下行数据流的对比

复制代码
WiFi 下行路径:
应用 → TCP/UDP → IP → 邻居 → qdisc → netdev → mac80211 → 驱动 → 硬件
         ↑                    ↑
    完整的网络协议栈      WiFi 特有

蓝牙下行路径:
应用 → RFCOMM/ATT → L2CAP → HCI → 驱动 → 硬件
         ↑
    独立蓝牙协议栈
维度 WiFi 蓝牙
网络协议栈 ✅ 完整 TCP/IP ❌ 独立蓝牙协议
传输层 TCP/UDP (内核) 应用层处理
网络层 IP (内核)
数据接口 netdev (wlan0) HCI Socket / RFCOMM TTY
核心处理层 mac80211 L2CAP / HCI

💡 关键总结

  • 控制路径 vs 数据路径

    • nl80211/cfg80211:只负责控制(扫描、连接、配置)

    • mac80211:负责数据路径的核心处理

  • WiFi 是完整的网络设备

    • 数据经过 TCP/IP 协议栈

    • 通过 net_device 接口与网络栈交互

  • mac80211 的核心职责

    • 802.3 → 802.11 帧转换

    • 速率控制、分片、加密、聚合

    • 为驱动提供统一接口 (ieee80211_ops)

  • 驱动层的职责

    • 填充硬件描述符

    • DMA 映射

    • 通知硬件发送

netdev (wlan0)是啥

netdevnetwork device 的缩写,wlan0 是 Linux 系统中无线网络设备的名称 。它是 WiFi 子系统与 Linux 网络协议栈之间的关键接口


🎯 核心概念

什么是 netdev?

netdev 是 Linux 内核中所有网络设备的抽象层。无论是有线网卡(eth0)、无线网卡(wlan0)、虚拟网桥(br0)、环回接口(lo),在内核中都被抽象为 struct net_device

复制代码
// include/linux/netdevice.h
struct net_device {
    char name[IFNAMSIZ];              // 设备名称: "wlan0"
    unsigned long state;              // 设备状态
    struct net_device_ops *netdev_ops; // 设备操作函数集
    struct net_device_stats stats;    // 统计信息
    
    // WiFi 特有
    const struct ieee80211_ops *ieee80211_ops;  // mac80211 操作
    
    // ... 更多字段
};

wlan0 是什么?

wlan0 是无线网络设备的典型命名

  • wlan = Wireless LAN

  • 0 = 第一个无线网卡(第二个是 wlan1,以此类推)


📡 netdev 在 WiFi 架构中的位置

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                     用户空间                                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  ping 8.8.8.8  │  curl  │  iperf  │  tcpdump -i wlan0     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│                      socket API (AF_INET)                          │
├─────────────────────────────────────────────────────────────────────┤
│                  内核网络协议栈 (TCP/IP)                            │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  TCP / UDP  →  IP  →  路由  →  ARP                         │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│                    ┌─────────────────┐                             │
│                    │   net_device    │  ← 抽象层                    │
│                    │   (struct net_device)                         │
│                    └────────┬────────┘                             │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    wlan0 (无线网卡)                          │   │
│  │  - netdev_ops: ndo_start_xmit = ieee80211_subif_start_xmit │   │
│  │  - 私有数据: 指向 mac80211 的 sdata                         │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│                    mac80211 (软件MAC层)                            │
│                              ↓                                      │
│                    WiFi 驱动 (ath9k/iwlwifi)                       │
│                              ↓                                      │
│                    硬件 (WiFi 芯片)                                 │
└─────────────────────────────────────────────────────────────────────┘

🔌 netdev 的关键作用

1. 向上:与网络协议栈对接

wlan0 向网络协议栈呈现一个标准的以太网接口

复制代码
// 用户空间看到的 wlan0
$ ip addr show wlan0
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP
    inet 192.168.1.100/24 brd 192.168.1.255 scope global wlan0
       valid_lft forever preferred_lft forever

// 应用程序使用标准 socket API
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, ...);
send(sock, data, len);  // 数据通过 wlan0 发出

2. 向下:连接 mac80211

wlan0netdev_ops 将网络栈的发送请求转发给 mac80211

复制代码
// net/mac80211/iface.c
static const struct net_device_ops ieee80211_dataif_ops = {
    .ndo_start_xmit     = ieee80211_subif_start_xmit,  // 发送入口
    .ndo_open           = ieee80211_open,              // 打开设备
    .ndo_stop           = ieee80211_stop,              // 关闭设备
    .ndo_set_mac_address = ieee80211_change_mac,       // 设置 MAC 地址
    .ndo_get_stats64    = ieee80211_get_stats64,       // 获取统计信息
};

// 当网络栈有数据要发送时,调用 ndo_start_xmit
static int ieee80211_subif_start_xmit(struct sk_buff *skb,
                                       struct net_device *dev)
{
    // 这里 skb 包含完整的 802.3 以太网帧
    // 需要转换为 802.11 帧并发送
    struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
    return ieee80211_xmit(sdata, skb, ...);
}

📋 netdev 的关键字段

复制代码
struct net_device {
    // 基本信息
    char name[IFNAMSIZ];           // "wlan0"
    unsigned int flags;            // IFF_UP, IFF_BROADCAST, IFF_RUNNING
    
    // 操作函数集(关键!)
    const struct net_device_ops *netdev_ops;
    
    // 网络协议栈相关
    unsigned int mtu;              // 最大传输单元 (通常 1500)
    unsigned short type;           // ARPHRD_ETHER (以太网类型)
    
    // MAC 地址
    unsigned char perm_addr[MAX_ADDR_LEN];
    unsigned char dev_addr[MAX_ADDR_LEN];
    
    // 队列管理
    struct netdev_queue *_tx;
    unsigned int num_tx_queues;
    
    // 统计信息
    struct net_device_stats stats;
    
    // WiFi 特有:指向 mac80211 的私有数据
    void *ieee80211_ptr;           // struct wireless_dev *
    
    // 驱动私有数据
    void *ml_priv;                 // 驱动自定义数据
};

🏗️ wlan0 的创建过程

复制代码
// mac80211 创建 net_device 的过程
int ieee80211_if_add(struct ieee80211_local *local, const char *name,
                     unsigned char name_assign_type,
                     struct wireless_dev **new_wdev, enum nl80211_iftype type,
                     struct vif_params *params)
{
    struct net_device *ndev;
    struct ieee80211_sub_if_data *sdata;
    
    // 1. 分配 net_device 结构体
    ndev = alloc_netdev_mqs(sizeof(*sdata), name, name_assign_type,
                            ieee80211_if_setup, txqs, rxqs);
    
    // 2. 设置 netdev_ops
    ndev->netdev_ops = &ieee80211_dataif_ops;
    
    // 3. 设置设备类型为以太网
    ndev->type = ARPHRD_ETHER;
    
    // 4. 设置 MTU
    ndev->mtu = IEEE80211_MAX_DATA_LEN;
    
    // 5. 关联 mac80211 私有数据
    sdata = netdev_priv(ndev);
    sdata->dev = ndev;
    ndev->ieee80211_ptr = &sdata->wdev;
    
    // 6. 注册到网络系统
    register_netdevice(ndev);
    
    // 用户空间就能看到 wlan0 了
}

🔍 如何查看和使用 netdev

查看 wlan0 信息

复制代码
# 查看所有网络设备
ip link show
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP

# 查看 wlan0 详细信息
ip addr show wlan0
ethtool -i wlan0      # 查看驱动信息
cat /sys/class/net/wlan0/statistics/tx_bytes

# 查看 qdisc(排队规则)
tc qdisc show dev wlan0

配置 wlan0

复制代码
# 配置 IP 地址
ip addr add 192.168.1.100/24 dev wlan0

# 启用设备
ip link set wlan0 up

# 添加路由
ip route add default via 192.168.1.1 dev wlan0

监控 wlan0

复制代码
# 抓包
tcpdump -i wlan0 -e -v

# 查看统计
watch -n 1 'cat /sys/class/net/wlan0/statistics/tx_bytes'

# 查看连接信息
iw dev wlan0 link

💡 netdev 与蓝牙的对比

维度 WiFi (netdev) 蓝牙
设备名称 wlan0, wlp2s0 hci0 (HCI设备)
设备类型 网络设备 (ARPHRD_ETHER) 字符设备 / HCI 设备
数据接口 send()/write() 通过 socket RFCOMM TTY (/dev/rfcomm0) 或 L2CAP socket
IP 协议栈 ✅ 完整支持 ❌ 需要 BNEP 桥接
应用程序 curl, ping, 浏览器 bluetoothctl, 串口程序

🎯 总结

问题 答案
netdev 是什么? Linux 内核中所有网络设备的抽象
wlan0 是什么? 无线网卡的具体实例,是 netdev 的一种
wlan0 的作用? 连接网络协议栈和 WiFi 子系统的桥梁
数据如何经过 wlan0? 网络栈 → ndo_start_xmitieee80211_subif_start_xmit → mac80211
用户如何与 wlan0 交互? ip, iw, ifconfig, tcpdump, 以及所有 socket 应用

一句话总结wlan0 是一个 net_device,它让 WiFi 硬件看起来就像一个普通的以太网卡,使得所有网络应用(curl、ping、浏览器)无需修改就能通过 WiFi 上网。这是 Linux 网络设计"一切皆文件"和"统一抽象"哲学的体现。

用户空间与内核的通信方式取决于具体场景:配置/控制用 Netlink,数据传输用 Socket。两者并不矛盾。


🎯 核心答案:两种不同的通信路径

WiFi 子系统有两条完全不同的用户-内核通信路径

路径 用途 接口 数据内容
控制路径 配置、管理、扫描、连接 nl80211 (Netlink) 命令和事件
数据路径 实际网络数据传输 socket (AF_INET) TCP/IP 数据包

📡 一、控制路径:Netlink (nl80211)

用于配置和管理 WiFi 设备:

复制代码
// 用户空间工具 iw 通过 Netlink 发送扫描命令
$ iw dev wlan0 scan

// 内部实现
struct nl_sock *sk = nl_socket_alloc();
genl_connect(sk);  // Netlink 连接

// 构造 nl80211 命令
nl80211_send_scan_request(sk, ...);

// 内核 nl80211 接收
// net/wireless/nl80211.c
static int nl80211_trigger_scan(struct sk_buff *skb, struct genl_info *info)
{
    // 执行扫描...
}

控制路径的数据流向

复制代码
iw (用户空间) → Netlink (nl80211) → cfg80211 (内核) → mac80211 → 驱动

📦 二、数据路径:Socket (AF_INET)

用于实际数据传输(上网):

复制代码
// 应用程序发送数据(如 curl、浏览器)
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, ...);
send(sock, "GET /", 5);  // 这里不是 Netlink,是普通的网络 socket!

数据路径的数据流向

复制代码
curl (用户空间) → socket (AF_INET) → TCP/IP 协议栈 → netdev (wlan0) → mac80211 → 驱动

🏗️ 完整的 WiFi 用户-内核通信架构

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                         用户空间                                     │
│                                                                      │
│  ┌─────────────────────────┐    ┌─────────────────────────────┐    │
│  │  网络应用               │    │  管理工具                   │    │
│  │  (curl / ping / 浏览器) │    │  (iw / wpa_supplicant)     │    │
│  └───────────┬─────────────┘    └──────────────┬──────────────┘    │
│              │                                  │                    │
│              │ 数据路径                         │ 控制路径           │
│              ↓                                  ↓                    │
│  ┌─────────────────────────┐    ┌─────────────────────────────┐    │
│  │  socket(AF_INET)        │    │  Netlink (nl80211)         │    │
│  │  (TCP/UDP 协议)         │    │  (通用 Netlink 协议)       │    │
│  └───────────┬─────────────┘    └──────────────┬──────────────┘    │
├──────────────┼────────────────────────────────┼─────────────────────┤
│              ↓                                  ↓                    │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                      内核空间                               │   │
│  │                                                             │   │
│  │  ┌─────────────────────┐    ┌─────────────────────────┐    │   │
│  │  │  TCP/IP 协议栈      │    │  nl80211/cfg80211       │    │   │
│  │  │  - TCP              │    │  - 扫描                 │    │   │
│  │  │  - IP               │    │  - 连接                 │    │   │
│  │  │  - 路由             │    │  - 密钥管理             │    │   │
│  │  └──────────┬──────────┘    └──────────┬──────────────┘    │   │
│  │             ↓                          ↓                    │   │
│  │  ┌─────────────────────────────────────────────────────┐   │   │
│  │  │              net_device (wlan0)                     │   │   │
│  │  │         ┌─────────────────────────────┐             │   │   │
│  │  │         │        mac80211             │             │   │   │
│  │  │         │  - 802.3 → 802.11 转换      │             │   │   │
│  │  │         │  - 速率控制、加密、聚合     │             │   │   │
│  │  │         └─────────────────────────────┘             │   │   │
│  │  └─────────────────────────────────────────────────────┘   │   │
│  │                              ↓                             │   │
│  │                     WiFi 驱动 (ath9k/iwlwifi)              │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

🔍 为什么需要两条路径?

1. 数据路径为什么用 Socket?

因为 WiFi 传输的是普通网络数据(TCP/IP),必须走标准的网络协议栈:

复制代码
// 数据路径:应用程序的标准网络通信
curl http://example.com
  ↓
socket(AF_INET)  ← 标准 socket,不是 Netlink
  ↓
TCP/IP 协议栈
  ↓
wlan0 (net_device)

2. 控制路径为什么用 Netlink?

因为控制命令需要灵活、异步、多播等特性:

需求 Netlink 优势
异步通知 内核可以主动推送事件(如扫描结果)
多播 多个程序可以同时监听事件
扩展性 可以灵活添加新命令
安全性 支持权限检查
复制代码
// 控制路径:内核主动推送事件
内核发现新 AP → Netlink 多播 → wpa_supplicant 收到 → 自动连接

📊 两种通信方式对比

维度 数据路径 (socket) 控制路径 (Netlink)
协议族 AF_INET / AF_INET6 AF_NETLINK
用途 传输用户数据 (上网) 配置和管理
数据内容 TCP/IP 包 nl80211 命令/事件
方向 主要是用户→内核 双向 (内核可主动推送)
接口 socket(), send(), recv() libnl, genl
应用 curl, ping, 浏览器 iw, wpa_supplicant

💡 类比理解

把 WiFi 子系统想象成一个出租车公司

角色 类比 说明
数据路径 乘客坐车 应用程序通过 socket 发送数据
控制路径 调度中心 iw/wpa_supplicant 通过 Netlink 配置网络
wlan0 出租车 net_device 负责数据传输
nl80211 对讲机 控制命令通道

乘客上车后,司机通过对讲机 与调度中心通信(控制路径),同时开车送乘客(数据路径)。两者是独立的。


🛠️ 验证两种路径

查看控制路径(Netlink)

复制代码
# 监控 Netlink 消息
$ strace -e sendto iw dev wlan0 scan 2>&1 | grep netlink
sendto(3, {{len=40, type=GENL_ID_CTRL...}, ...}, ...)

# 查看 Netlink 连接
$ ss -A netlink -p
nl     UNCONN 0 0      genl:0   *   users:(("iw",pid=1234,fd=3))

查看数据路径(Socket)

复制代码
# 查看网络连接
$ ss -tunp
tcp   ESTAB  0  0   192.168.1.100:45678  8.8.8.8:443   users:(("curl",pid=5678,fd=3))
#                                          ↑ 这是 AF_INET socket,不是 Netlink

🎯 总结

问题 答案
用户空间和内核用什么通信? 取决于场景:控制用 Netlink,数据用 Socket
为什么不用统一的方式? 两者设计目标不同:Netlink 适合控制,Socket 适合数据传输
wlan0 是什么? net_device,数据路径的入口
nl80211 是什么? Netlink 协议,控制路径的入口
两者会混淆吗? 不会,它们服务于完全不同的目的

一句话总结 :WiFi 子系统有两条独立的用户-内核通信通道 ------控制通道用 Netlink (nl80211) 来配置和管理设备,数据通道用标准 Socket (AF_INET) 来传输实际的网络数据。这是"控制与数据分离"设计思想的典型体现。

上行数据流

上行数据流指的是从 WiFi 硬件到应用程序的数据路径。与下行对称但方向相反,经过完整的网络协议栈处理。


🎯 上行数据流全景图

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        硬件层                                        │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  WiFi 芯片接收到空中数据包,产生中断                         │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓ DMA / 中断                          │
├─────────────────────────────────────────────────────────────────────┤
│                     驱动层 (ath9k/iwlwifi)                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  中断处理 → 从 Rx Ring 取数据 → 填充 skb → ieee80211_rx()  │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│                    mac80211 (软件MAC层)                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  - 802.11 → 802.3 帧转换                                    │   │
│  │  - 解密、去重、重组                                         │   │
│  │  - 监控接口拷贝 (monitor)                                   │   │
│  │  - 提交给网络协议栈                                         │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│                    net_device (wlan0)                               │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  网络协议栈 (TCP/IP)                                        │   │
│  │  - 邻居层 (ARP)                                             │   │
│  │  - IP 层 (重组、Netfilter)                                  │   │
│  │  - TCP/UDP 层 (重组、流量控制)                              │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓ socket                              │
├─────────────────────────────────────────────────────────────────────┤
│                        用户空间                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  应用程序 (浏览器 / curl / ping) 通过 recv() 读取数据        │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

📥 第一步:硬件 → 驱动层

硬件中断触发

当 WiFi 芯片收到空中数据包时,通过 PCIe/USB/SDIO 触发中断:

复制代码
// drivers/net/wireless/ath/ath9k/pci.c
static irqreturn_t ath9k_pci_irq_handler(int irq, void *dev)
{
    struct ath_softc *sc = dev;
    
    // 1. 读取硬件中断状态
    status = ath9k_hw_get_isr(sc->sc_ah, &isr);
    
    // 2. 如果是接收中断
    if (isr & ATH9K_INT_RX) {
        // 调度接收任务
        tasklet_schedule(&sc->rx_tasklet);
    }
    
    return IRQ_HANDLED;
}

驱动接收处理

复制代码
// drivers/net/wireless/ath/ath9k/recv.c
static void ath9k_rx_tasklet(struct tasklet_struct *t)
{
    struct ath_softc *sc = from_tasklet(sc, t, rx_tasklet);
    struct sk_buff *skb;
    
    // 1. 从硬件 Rx Ring 读取数据
    while (ath_rx_buf_link(sc, &skb)) {
        struct ieee80211_rx_status *rx_status = IEEE80211_SKB_RXCB(skb);
        
        // 2. 填充接收状态(信号强度、速率、频率等)
        rx_status->signal = rs->rs_rssi;
        rx_status->rate_idx = rs->rs_rate;
        rx_status->freq = curchan->center_freq;
        
        // 3. 提交给 mac80211
        ieee80211_rx(sc->hw, skb);
    }
}

⚙️ 第二步:mac80211 接收处理

接收入口

复制代码
// net/mac80211/rx.c
void ieee80211_rx(struct ieee80211_hw *hw, struct sk_buff *skb)
{
    struct ieee80211_local *local = hw_to_local(hw);
    
    // 1. 加入接收队列
    skb_queue_tail(&local->rx_skb_queue, skb);
    
    // 2. 调度接收工作
    queue_work(local->workqueue, &local->rx_work);
}

static void ieee80211_rx_work(struct work_struct *work)
{
    struct ieee80211_local *local = container_of(work, ...);
    
    while ((skb = skb_dequeue(&local->rx_skb_queue))) {
        // 调用接收处理链
        ieee80211_rx_handlers(local, skb);
    }
}

接收处理链

复制代码
// net/mac80211/rx.c
static void ieee80211_rx_handlers(struct ieee80211_local *local,
                                   struct sk_buff *skb)
{
    // 依次调用各处理器
    ieee80211_rx_h_monitor(skb);        // 1. 拷贝给监控接口 (tcpdump)
    ieee80211_rx_h_decrypt(skb);        // 2. 解密 (WEP/TKIP/CCMP)
    ieee80211_rx_h_defragment(skb);     // 3. 重组分片
    ieee80211_rx_h_check(skb);          // 4. 检查重复帧
    ieee80211_rx_h_amsdu(skb);          // 5. A-MSDU 解聚合
    ieee80211_rx_h_data(skb);           // 6. 数据处理 → 802.3 转换
    ieee80211_rx_h_mgmt(skb);           // 7. 管理帧处理
}

802.11 → 802.3 帧转换

复制代码
// net/mac80211/rx.c
static int ieee80211_rx_h_data(struct ieee80211_rx_data *rx)
{
    struct ieee80211_hdr *hdr = (void *)rx->skb->data;
    struct ethhdr *ehdr;
    
    // 1. 从 802.11 帧头提取源 MAC 和目的 MAC
    //    - From DS / To DS 位决定地址字段含义
    
    // STA 模式收到 AP 发来的帧:
    //   Addr1 = 自己的 MAC (接收端)
    //   Addr2 = AP 的 MAC (发送端)
    //   Addr3 = 源 MAC (原始发送者)
    
    // 2. 构造 802.3 以太网帧头
    ehdr = (struct ethhdr *)skb_push(rx->skb, ETH_HLEN);
    memcpy(ehdr->h_dest, dest, ETH_ALEN);  // 目的 MAC
    memcpy(ehdr->h_source, src, ETH_ALEN); // 源 MAC
    ehdr->h_proto = htons(ETH_P_IP);       // 协议类型
    
    // 3. 移除 802.11 帧头,保留 802.3 帧头 + 数据
    
    // 4. 提交给网络协议栈
    netif_receive_skb(rx->skb);
}

🌐 第三步:网络协议栈接收

netif_receive_skb 入口

复制代码
// net/core/dev.c
int netif_receive_skb(struct sk_buff *skb)
{
    // 1. 经过 ptype 链表,根据协议类型分发
    return __netif_receive_skb(skb);
}

static int __netif_receive_skb(struct sk_buff *skb)
{
    // 2. 处理网桥 (如果存在)
    skb = skb_vlan_untag(skb);
    
    // 3. 调用协议处理器 (如 IP)
    return __netif_receive_skb_core(skb, false);
}

IP 层接收

复制代码
// net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev,
           struct packet_type *pt, struct net_device *orig_dev)
{
    // 1. 检查 IP 头
    // 2. 重组分片
    // 3. 处理 IP 选项
    // 4. Netfilter 钩子 (NF_INET_PRE_ROUTING)
    // 5. 路由查找,决定本地接收还是转发
    
    if (ip_local_deliver) {
        // 本地接收
        return ip_local_deliver(skb);
    }
}

int ip_local_deliver(struct sk_buff *skb)
{
    // 1. 重组分片
    // 2. Netfilter 钩子 (NF_INET_LOCAL_IN)
    // 3. 根据协议分发到 TCP/UDP
    return ip_local_deliver_finish(skb);
}

TCP/UDP 层接收

复制代码
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    // 1. 查找 socket
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
    
    // 2. 加入 socket 接收队列
    if (!tcp_queue_rcv(sk, skb)) {
        // 3. 唤醒等待的进程
        sk->sk_data_ready(sk);
    }
}

// net/ipv4/udp.c
int udp_rcv(struct sk_buff *skb)
{
    // UDP 接收处理
    udp_queue_rcv_skb(sk, skb);
}

🖥️ 第四步:用户空间接收

应用程序读取数据

复制代码
// 用户空间应用程序
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, ...);

char buf[1024];
int n = recv(sock, buf, sizeof(buf), 0);  // 阻塞等待数据
printf("Received: %s\n", buf);

内核返回数据

复制代码
// net/socket.c
SYSCALL_DEFINE3(recvmsg, int, fd, struct msghdr __user *, msg, unsigned int, flags)
{
    struct socket *sock = sockfd_lookup(fd, &err);
    
    // 调用协议层的 recvmsg
    err = sock->ops->recvmsg(sock, msg, size, flags);
    
    // 将数据从内核拷贝到用户空间
    if (msg->msg_controllen)
        err = copy_msghdr_from_user(umsg, msg);
    
    return err;
}

📊 数据包格式变化(上行)

层级 数据包格式 方向
硬件 无线电信号 空中 → 硬件
驱动 802.11 帧 硬件 → mac80211
mac80211 802.11 帧 → 802.3 帧 转换
网络设备层 802.3 以太网帧 mac80211 → 网络栈
IP 层 IP 包 网络栈 → TCP/UDP
TCP/UDP 层 TCP 段 / UDP 包 协议栈 → socket
用户空间 应用数据 socket → 应用程序

🔄 完整上行数据流时序图

应用程序网络协议栈mac80211驱动层WiFi硬件应用程序网络协议栈mac80211驱动层WiFi硬件收到数据包,产生中断从 Rx Ring 读取数据ieee80211_rx(skb)802.11 帧解析解密、去重、重组802.11 → 802.3 转换netif_receive_skb(skb)IP 层处理Netfilter 钩子TCP/UDP 层处理唤醒等待的进程recv() 系统调用拷贝数据到用户空间


🎯 WiFi 上行 vs 下行对比

维度 下行 (发送) 上行 (接收)
入口 应用 socket 硬件中断
出口 硬件空中发送 应用 socket
数据转换 802.3 → 802.11 802.11 → 802.3
关键函数 dev_queue_xmit() netif_receive_skb()
驱动接口 ieee80211_ops->tx ieee80211_rx()
流控 qdisc 排队 TCP 接收窗口
中断 发送完成中断 接收数据中断

🔍 调试技巧

1. 查看接收统计

复制代码
# 查看网卡接收统计
cat /sys/class/net/wlan0/statistics/rx_bytes
cat /sys/class/net/wlan0/statistics/rx_packets

# 查看 mac80211 接收统计
cat /sys/kernel/debug/ieee80211/phy0/rx_stats

2. 抓取数据包

复制代码
# 在 wlan0 抓取 802.3 帧
tcpdump -i wlan0 -e -v

# 在 monitor 接口抓取 802.11 帧
iw dev wlan0 set monitor other
tcpdump -i mon0 -e -v

3. 跟踪内核接收路径

复制代码
# 跟踪接收函数
echo 'p:netif_receive_skb' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
cat /sys/kernel/debug/tracing/trace_pipe

💡 总结

问题 答案
上行入口是什么? 硬件中断
核心转换在哪里? mac80211 进行 802.11 → 802.3 转换
如何进入网络栈? netif_receive_skb()
如何到达应用? TCP/UDP → socket → recv()
与下行对称吗? 是的,方向相反,处理逻辑对称

一句话总结 :WiFi 上行数据流是下行的逆过程------硬件收到 802.11 帧后,驱动调用 ieee80211_rx() 交给 mac80211,经过解密、重组、802.11→802.3 转换后,通过 netif_receive_skb() 进入网络协议栈,最终通过 socket 到达应用程序。

对比:蓝牙的控制路径

蓝牙同样有控制路径和数据路径的分离 ,只是实现方式和 WiFi 有所不同。蓝牙的控制路径通过 HCI Socket 的特定通道实现,而不是像 WiFi 那样使用独立的 Netlink 协议。


🎯 蓝牙的控制路径 vs 数据路径

路径 用途 接口 数据内容
控制路径 设备管理、扫描、配对、连接 HCI Socket (HCI_CHANNEL_CONTROL / USER) HCI 命令和事件
数据路径 实际数据传输 RFCOMM Socket / L2CAP Socket / ATT 应用数据

📡 一、蓝牙控制路径架构

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        用户空间                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  管理工具                     │  应用                        │   │
│  │  (bluetoothctl / bluetoothd)  │  (蓝牙耳机 / 文件传输)      │   │
│  └───────────────────────────────┴─────────────────────────────┘   │
│              │                              │                       │
│              │ 控制路径                     │ 数据路径              │
│              ↓                              ↓                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  D-Bus (bluetoothd 对外接口)          RFCOMM / L2CAP Socket │   │
│  └─────────────────────────────────────────────────────────────┘   │
│              │                              │                       │
│              ↓                              ↓                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │              HCI Socket (AF_BLUETOOTH)                       │   │
│  │  ┌─────────────────┐  ┌─────────────────────────────────┐   │   │
│  │  │ HCI_CHANNEL_    │  │ HCI_CHANNEL_RAW / L2CAP        │   │   │
│  │  │ CONTROL / USER  │  │ (数据通道)                      │   │   │
│  │  │ (控制通道)      │  │                                 │   │   │
│  │  └─────────────────┘  └─────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│                        内核空间                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    HCI 核心层 (hci_core.c)                  │   │
│  │  - 命令队列管理                                              │   │
│  │  - 事件分发                                                  │   │
│  │  - 连接管理                                                  │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    蓝牙驱动 (btusb / hci_uart)              │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
├─────────────────────────────────────────────────────────────────────┤
│                        硬件层                                        │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    蓝牙控制器                                │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

🔧 二、蓝牙控制路径的具体实现

1. HCI Socket 的控制通道

HCI Socket 通过不同的 channel 区分控制和数据:

复制代码
// include/net/bluetooth/hci.h
#define HCI_CHANNEL_RAW        0  // 原始 HCI (数据+控制混合)
#define HCI_CHANNEL_USER       1  // 用户通道 (控制路径)
#define HCI_CHANNEL_MONITOR    2  // 监控通道
#define HCI_CHANNEL_CONTROL    3  // 控制通道 (bluetoothd 使用)

2. bluetoothd 使用控制通道

复制代码
// bluetoothd 源码简化
int sk = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);

struct sockaddr_hci addr = {
    .hci_family = AF_BLUETOOTH,
    .hci_dev = 0,                    // hci0
    .hci_channel = HCI_CHANNEL_CONTROL,  // 控制通道
};
bind(sk, (struct sockaddr *)&addr, sizeof(addr));

// 发送管理命令
struct hci_command_hdr cmd = {
    .opcode = htobs(HCI_OP_LE_SET_SCAN_ENABLE),
    .plen = 2,
};
send(sk, &cmd, sizeof(cmd), 0);

3. 内核中的控制路径处理

复制代码
// net/bluetooth/hci_sock.c
static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    struct sock *sk = sock->sk;
    struct hci_dev *hdev = hci_sock_dev(sk);
    
    // 根据 channel 区分处理
    switch (hci_pi(sk)->channel) {
    case HCI_CHANNEL_CONTROL:
        // 控制通道:管理命令
        return hci_sock_sendmsg_control(sk, msg, len, hdev);
        
    case HCI_CHANNEL_USER:
        // 用户通道:完整控制权限
        return hci_sock_sendmsg_user(sk, msg, len, hdev);
        
    case HCI_CHANNEL_RAW:
        // 原始通道:可发送任意 HCI 命令
        return hci_sock_sendmsg_raw(sk, msg, len, hdev);
    }
}

📊 三、蓝牙 vs WiFi 控制路径对比

维度 WiFi 控制路径 蓝牙控制路径
接口协议 Netlink (nl80211) HCI Socket (AF_BLUETOOTH)
通道区分 不同的 Netlink 协议族 HCI Socket 的不同 channel
用户空间工具 iw, wpa_supplicant bluetoothctl, bluetoothd
内核组件 nl80211, cfg80211 hci_sock.c, hci_core.c
命令格式 nl80211 命令 HCI 命令 (操作码 + 参数)
事件推送 Netlink 多播 HCI 事件通过 socket 返回
权限管理 Netlink 权限检查 HCI Socket 权限检查

🔄 四、蓝牙控制路径的典型流程

BLE 扫描为例,展示控制路径:

复制代码
用户: bluetoothctl scan on
        ↓
bluetoothctl 通过 D-Bus 调用 bluetoothd
        ↓
bluetoothd 构造 HCI 命令
        ↓
bluetoothd 通过 HCI Socket (HCI_CHANNEL_CONTROL) 发送
        ↓
内核 hci_sock.c 接收,根据 channel 识别为控制命令
        ↓
hci_core.c 处理命令,加入命令队列
        ↓
驱动 (btusb) 发送命令给硬件
        ↓
硬件开始扫描
        ↓
硬件收到扫描结果,触发中断
        ↓
驱动接收数据,调用 hci_recv_frame()
        ↓
hci_core.c 识别为 HCI 事件
        ↓
通过 HCI Socket 返回给 bluetoothd
        ↓
bluetoothd 解析事件,通过 D-Bus 发送给 bluetoothctl
        ↓
bluetoothctl 显示扫描到的设备

📝 五、验证蓝牙控制路径

1. 查看 HCI Socket 连接

复制代码
# 查看所有 HCI Socket
$ ss -A bluetooth -p
Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port
hci   UNCONN 0      0      hci0:3             *:*    users:(("bluetoothd",pid=789,fd=10))
#                          ↑ channel 3 = HCI_CHANNEL_CONTROL

$ ss -A bluetooth -p | grep "hci0:"
hci   UNCONN 0      0      hci0:1             *:*    users:(("bluetoothctl",pid=1234,fd=3))
#                          ↑ channel 1 = HCI_CHANNEL_USER

2. 跟踪控制命令

复制代码
# 使用 strace 跟踪 bluetoothd 的 HCI Socket 操作
$ strace -e sendto -p $(pidof bluetoothd)
sendto(10, "\x01\x0c\x20\x02\x01\x00", 6, 0, NULL, 0) = 6
#       ^^ ^^^^^^ ^^ ^^^^^
#       |  |      |  └─ 参数: 开启扫描
#       |  |      └──── 参数长度: 2
#       |  └─────────── 操作码: 0x200C (LE Set Scan Enable)
#       └────────────── 包类型: HCI_COMMAND_PKT

3. 使用 btmon 监控控制路径

复制代码
# btmon 使用 HCI_CHANNEL_MONITOR 监控所有 HCI 流量
$ btmon
< HCI Command: LE Set Scan Enable (0x08|0x200C) plen 2  # 控制命令
        Scanning: Enabled (0x01)
> HCI Event: LE Meta Event (0x3e) plen 12              # 控制事件
        LE Advertising Report (0x02)

💡 六、总结

问题 答案
蓝牙有控制路径吗? ✅ 有,通过 HCI Socket 的控制通道实现
控制路径用什么接口? HCI Socket (AF_BLUETOOTH) + HCI_CHANNEL_CONTROL/USER
数据路径用什么接口? RFCOMM Socket / L2CAP Socket / ATT
与 WiFi 控制路径有何不同? WiFi 用 Netlink,蓝牙用 HCI Socket 的 channel 区分
用户空间控制工具? bluetoothctl, bluetoothd, btmgmt

一句话总结 :蓝牙的控制路径和数据路径共用 HCI Socket ,但通过 channel 字段区分------控制通道 (HCI_CHANNEL_CONTROL/USER) 传输管理命令和事件,数据通道 (RFCOMM/L2CAP Socket) 传输实际应用数据。这种设计与 WiFi 用 Netlink 分离控制与数据的思路异曲同工。

相关推荐
feng68_2 小时前
MySQL集群主从复制
linux·运维·数据库·mysql·adb
qq_172805592 小时前
WSL 完整教学文档(Windows Linux 子系统)
linux·运维·windows·wsl
久绊A2 小时前
服务器磁盘只读/故障排
java·linux·服务器
yy_xzz2 小时前
【Linux开发】03 TCP 网络编程中的数据边界问题与解决方案
linux·网络·tcp/ip
hutengyi2 小时前
Redis基础——1、Linux下安装Redis(超详细)
linux·数据库·redis
LeocenaY2 小时前
Linux 内核 I/O栈 总结
linux·运维·服务器
学不完的2 小时前
Zrlog面试问答及问题解决方案
linux·运维·nginx·unity·游戏引擎
小邋遢2.02 小时前
Centos stream 9 安装后root不能远程登录问题
linux·运维·centos
学不完的3 小时前
ZrLog 博客系统部署指南(无 War 包版,Maven 构建 + 阿里云镜像优化)
java·linux·nginx·阿里云·maven