1. 程式人生 > 其它 >Linux網橋工作原理與實現

Linux網橋工作原理與實現

Linux網橋工作原理與實現

Linux 的 網橋 是一種虛擬裝置(使用軟體實現),可以將 Linux 內部多個網路介面連線起來,如下圖所示:

而將網路介面連線起來的結果就是,一個網路介面接收到網路資料包後,會複製到其他網路介面中,如下圖所示:

如上圖所示,當網路介面A接收到資料包後,網橋 會將資料包複製並且傳送給連線到 網橋 的其他網路介面(如上圖中的網絡卡B和網絡卡C)。

Docker 就是使用 網橋 來進行容器間通訊的,我們來看看 Docker 是怎麼利用 網橋 來進行容器間通訊的,原理如下圖:

Docker 在啟動時,會建立一個名為 docker0網橋,並且把其 IP 地址設定為 172.17.0.1/16

(私有 IP 地址)。然後使用虛擬裝置對 veth-pair 來將容器與 網橋 連線起來,如上圖所示。而對於 172.17.0.0/16 網段的資料包,Docker 會定義一條 iptables NAT 的規則來將這些資料包的 IP 地址轉換成公網 IP 地址,然後通過真實網路介面(如上圖的 ens160 介面)傳送出去。

接下來,我們主要通過程式碼來分析 網橋 的實現。

網橋的實現

1. 網橋的建立

我們可以通過下面命令來新增一個名為 br0網橋 裝置物件:

[root@vagrant]# brctl addbr br0

然後,我們可以通過命令 brctl show 來檢視系統中所有的 網橋 裝置列表,如下:

[root@vagrant]# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.000000000000       no
docker0         8000.000000000000       no

當使用命令建立一個新的 網橋 裝置時,會觸發核心呼叫 br_add_bridge() 函式,其實現如下:

int br_add_bridge(char *name)
{
    struct net_bridge *br;

    if ((br = new_nb(name)) == NULL) // 建立一個網橋裝置物件
        return -ENOMEM;

    if (__dev_get_by_name(name) != NULL) { // 裝置名是否已經註冊過?
        kfree(br);
        return -EEXIST; // 返回錯誤, 不能重複註冊相同名字的裝置
    }

    // 新增到網橋列表中
    br->next = bridge_list;
    bridge_list = br;
    ...
    register_netdev(&br->dev); // 把網橋註冊到網路裝置中

    return 0;
}

br_add_bridge() 函式主要完成以下幾個工作:

  • 呼叫 new_nb() 函式建立一個 網橋 裝置物件。
  • 呼叫 __dev_get_by_name() 函式檢查裝置名是否已經被註冊過,如果註冊過返回錯誤資訊。
  • 網橋 裝置物件新增到 bridge_list 連結串列中,核心使用 bridge_list 連結串列來儲存所有 網橋 裝置。
  • 呼叫 register_netdev() 將網橋設備註冊到網路裝置中。

從上面的程式碼可知,網橋 裝置使用了 net_bridge 結構來描述,其定義如下:

struct net_bridge
{
    struct net_bridge           *next;               // 連線核心中所有的網橋物件
    rwlock_t                    lock;                // 鎖
    struct net_bridge_port      *port_list;          // 網橋埠列表
    struct net_device           dev;                 // 網橋裝置資訊
    struct net_device_stats     statistics;          // 資訊統計
    rwlock_t                    hash_lock;           // 用於鎖定CAM表
    struct net_bridge_fdb_entry *hash[BR_HASH_SIZE]; // CAM表
    struct timer_list           tick;

    /* STP */
    ...
};

net_bridge 結構中,比較重要的欄位為 port_listhash

  • port_list:網橋埠列表,儲存著繫結到 網橋 的網路介面列表。
  • hash:儲存著以網路介面 MAC地址 為鍵值,以網橋埠為值的雜湊表。

網橋埠 使用結構體 net_bridge_port 來描述,其定義如下:

struct net_bridge_port
{
    struct net_bridge_port  *next;   // 指向下一個埠
    struct net_bridge       *br;     // 所屬網橋裝置物件
    struct net_device       *dev;    // 網路介面裝置物件
    int                     port_no; // 埠號

    /* STP */
    ...
};

net_bridge_fdb_entry 結構用於描述網路介面裝置 MAC地址網橋埠 的對應關係,其定義如下:

struct net_bridge_fdb_entry
{
    struct net_bridge_fdb_entry *next_hash;
    struct net_bridge_fdb_entry **pprev_hash;
    atomic_t                    use_count;
    mac_addr                    addr;  // 網路介面裝置MAC地址
    struct net_bridge_port      *dst;  // 網橋埠
    ...
};

這三個結構的對應關係如下圖所示:

可見,要將 網路介面裝置 繫結到一個 網橋 上,需要使用 net_bridge_port 結構來關聯的,下面我們來分析怎麼將一個 網路介面裝置 繫結到一個 網橋 中。

網橋是工作在 TCP/IP 協議棧的第二層,也就是說,網橋能夠根據目標 MAC 地址對資料包進行廣播或者單播。當目標 MAC 地址能夠從網橋的 hash 表中找到對應的網橋埠,說明此資料包是單播的資料包,否則就是廣播的資料包。

2. 將網路介面繫結到網橋

要將一個 網路介面裝置 繫結到一個 網橋 上,可以使用以下命令:

[root@vagrant]# brctl addif br0 eth0

上面的命令讓網路介面 eth0 繫結到網橋 br0 上。

當呼叫命令將網路介面裝置繫結到網橋上時,核心會觸發呼叫 br_add_if() 函式來實現,其程式碼如下:

int br_add_if(struct net_bridge *br, struct net_device *dev)
{
    struct net_bridge_port *p;
    ...
    write_lock_bh(&br->lock);

    // 建立一個新的網橋埠物件, 並新增到網橋的port_list連結串列中
    if ((p = new_nbp(br, dev)) == NULL) { 
        write_unlock_bh(&br->lock);
        dev_put(dev);
        return -EXFULL;
    }

    // 設定網路介面裝置為混雜模式
    dev_set_promiscuity(dev, 1);
    ...
    // 新增到網路介面MAC地址與網橋埠對應的雜湊表中
    br_fdb_insert(br, p, dev->dev_addr, 1);
    ...
    write_unlock_bh(&br->lock);

    return 0;
}

br_add_if() 函式主要完成以下工作:

  • 呼叫 new_nbp() 函式建立一個新的 網橋埠 並且新增到 網橋port_list 連結串列中。
  • 將網路介面裝置設定為 混雜模式
  • 呼叫 br_fdb_insert() 函式將新建的 網橋埠 插入到網路介面 MAC地址 對應的雜湊表中。

也就是說,br_add_if() 函式主要建立 網路介面裝置網橋 的關係。

3. 網橋中的網路介面接收資料

當某個 網路介面 接收到資料包時,會判斷這個 網路介面 是否繫結到某個 網橋 上,如果綁定了,那麼就呼叫 handle_bridge() 函式處理這個資料包。handle_bridge() 函式實現如下:

static int __inline__
handle_bridge(struct sk_buff *skb, struct packet_type *pt_prev)
{
    int ret = NET_RX_DROP;
    ...
    br_handle_frame_hook(skb);
    return ret;
}

br_handle_frame_hook 是一個函式指標,其指向 br_handle_frame() 函式,我們來分析 br_handle_frame() 函式的實現:

void br_handle_frame(struct sk_buff *skb)
{
    struct net_bridge *br;

    br = skb->dev->br_port->br; // 獲取裝置連線的網橋物件

    read_lock(&br->lock);   // 對網橋上鎖
    __br_handle_frame(skb); // 呼叫__br_handle_frame()函式處理資料包
    read_unlock(&br->lock);
}

br_handle_frame() 函式的實現比較簡單,首先對 網橋 進行上鎖操作,然後呼叫 __br_handle_frame() 處理資料包,我們來分析 __br_handle_frame() 函式的實現:

static void __br_handle_frame(struct sk_buff *skb)
{
    struct net_bridge *br;
    unsigned char *dest;
    struct net_bridge_fdb_entry *dst;
    struct net_bridge_port *p;
    int passedup;

    dest = skb->mac.ethernet->h_dest; // 目標MAC地址
    p = skb->dev->br_port;            // 網路介面繫結的埠
    br = p->br;
    passedup = 0;
    ...
    // 將學習到的MAC地址插入到網橋的hash表中
    if (p->state == BR_STATE_LEARNING || p->state == BR_STATE_FORWARDING)
        br_fdb_insert(br, p, skb->mac.ethernet->h_source, 0);
    ...
    if (dest[0] & 1) {        // 如果是一個廣播包
        br_flood(br, skb, 1); // 把資料包傳送給連線到網橋上的所有網路介面
        if (!passedup)
            br_pass_frame_up(br, skb);
        else
            kfree_skb(skb);
        return;
    }

    dst = br_fdb_get(br, dest);    // 獲取目標MAC地址對應的網橋埠
    ...
    if (dst != NULL) {             // 如果目標MAC地址對應的網橋埠存在
        br_forward(dst->dst, skb); // 那麼只將資料包轉發給此埠
        br_fdb_put(dst);
        return;
    }

    br_flood(br, skb, 0); // 否則傳送給連線到此網橋上的所有網路介面
    return;
    ...
}

__br_handle_frame() 函式主要完成以下幾個工作:

  • 首先將從資料包中學習到的MAC地址插入到網橋的hash表中。
  • 如果資料包是一個廣播包(目標MAC地址的第一位為1),那麼呼叫 br_flood() 函式把資料包傳送給連線到網橋上的所有網路介面。
  • 呼叫 br_fdb_get() 獲取目標MAC地址對應的網橋埠,如果目標MAC地址對應的網橋埠存在,那麼呼叫 br_forward() 函式把資料包轉發給此埠。
  • 否則呼叫 呼叫 br_flood() 函式把資料包傳送給連線到網橋上的所有網路介面。

函式 br_forward() 用於把資料包傳送給指定的網橋埠,其實現如下:

static void __br_forward(struct net_bridge_port *to, struct sk_buff *skb)
{
    skb->dev = to->dev;
    dev_queue_xmit(skb);
}

void br_forward(struct net_bridge_port *to, struct sk_buff *skb)
{
    if (should_forward(to, skb)) { // 埠是否能夠接收資料?
        __br_forward(to, skb);
        return;
    }
    kfree_skb(skb);
}

br_forward() 函式通過呼叫 __br_forward() 函式來發送資料給指定的網橋埠,__br_forward() 函式首先將資料包的輸出介面裝置設定為網橋埠繫結的裝置,然後呼叫 dev_queue_xmit() 函式將資料包傳送出去。

br_flood() 函式用於將資料包傳送給繫結到 網橋 上的所有網路介面裝置,其實現如下:

void br_flood(struct net_bridge *br, struct sk_buff *skb, int clone)
{
    struct net_bridge_port *p;
    struct net_bridge_port *prev;
    ...
    prev = NULL;

    p = br->port_list;
    while (p != NULL) {              // 遍歷繫結到網橋的所有網路介面裝置
        if (should_forward(p, skb)) { // 埠是否能夠接收資料包?
            if (prev != NULL) {
                struct sk_buff *skb2;

                // 克隆一個數據包
                if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL) { 
                    br->statistics.tx_dropped++;
                    kfree_skb(skb);
                    return;
                }

                __br_forward(prev, skb2); // 把資料包傳送給裝置
            }

            prev = p;
        }

        p = p->next;
    }

    if (prev != NULL) {
        __br_forward(prev, skb);
        return;
    }

    kfree_skb(skb);
}

br_flood() 函式的實現也比較簡單,主要是遍歷繫結到網橋的所有網路介面裝置,然後呼叫 __br_forward() 函式將資料包轉發給裝置對應的埠。