Linux內核分析 - 網絡[五]:vlan協議-802.1q

內核版本:2.6.34

802.1q

1. 註冊vlan網絡系統子空間,

err = register_pernet_subsys(&vlan_net_ops);
static struct pernet_operations vlan_net_ops = {
 .init = vlan_init_net,
 .exit = vlan_exit_net,
 .id   = &vlan_net_id,
 .size = sizeof(struct vlan_net),
};

        每個子空間註冊成功都會分配一個ID,在register_pernet_subsys() -> register_pernet_operations() -> ida_get_new_above()獲得,而vlan_net_ops中的vlan_net_id記錄了這個ID。註冊子空間的最後會調用子空間的初始化函數vlan_init_net(),它會把vlan_net(有關vlan的proc文件系統信息)加到全局的net->gen->ptr數組中去,下標爲之前分配的ID。這樣,通過vlan_net_id便可隨時查到vlan_net的信息,主要與proc有關。

2. 註冊vlan_notifier_block

err = register_netdevice_notifier(&vlan_notifier_block);
static struct notifier_block vlan_notifier_block __read_mostly = {
 .notifier_call = vlan_device_event,
};

        關於register_netdevice_notifier()做的工作並不複雜,首先會註冊vlan_notifier_block到netdev_chain:

err = raw_notifier_chain_register(&netdev_chain, nb);

        然後通知事件NETDEV_REGISTER和NETDEV_UP事件到網絡系統的中的每個設備:

for_each_net(net) {
 for_each_netdev(net, dev) {
  err = nb->notifier_call(nb, NETDEV_REGISTER, dev);
  err = notifier_to_errno(err);
  if (err)
   goto rollback;

  if (!(dev->flags & IFF_UP))
   continue;

  nb->notifier_call(nb, NETDEV_UP, dev);
 }
}

        此時nb就是vlan_notifier_block,調用通知函數vlan_device_event()。假設此時主機上擁有設備lo[環回接口], eth1[網卡], eth1.1[虛擬接口],來看vlan_device_event()函數:
         判斷是否爲vlan虛擬接口,則執行__vlan_device_event(),這個函數的作用就是在proc文件系統中添加或刪除vlan虛擬設備的相應項。顯然,符合條件的是eth1.1,而事件NETDEV_REGISTER會在/proc/net目錄下創建eth1.1的文件。

if (is_vlan_dev(dev))
 __vlan_device_event(dev, event);

        然後判斷dev是否在vlan_group_hash表中[參考最後”vlan設備組織結構”],它以dev->ifindex爲hash值,顯然,只有eth1纔有正確的ifindex,lo和eth1.1會因查詢失敗而退出vlan_device_event。

grp = __vlan_find_group(dev);
if (!grp)
 goto out;

        下面的事件處理只有eth1會執行,以NETDEV_UP爲例,通過vlan_group_hash表可以根據eth1查到所有在其上創建的虛擬網卡接口,如果這些網卡接口沒有開啓,則開啓它,這裏開啓用到的是dev_change_flags(vlandev, flgs | IFF_UP)。跟蹤該函數可以發現它僅僅是修改flags後,通知NETDEV_UP事件,等待設備去處理。這裏的含義可以這樣理解,如果ifconfig eth1 up,則在eth1上創建的所有vlan網卡接口都會被up。

case NETDEV_UP:
 /* Put all VLANs for this dev in the up state too.  */
 for (i = 0; i < VLAN_GROUP_ARRAY_LEN; i++) {
  vlandev = vlan_group_get_device(grp, i);
  if (!vlandev)
   continue;

  flgs = vlandev->flags;
  if (flgs & IFF_UP)
   continue;

  vlan = vlan_dev_info(vlandev);
  if (!(vlan->flags & VLAN_FLAG_LOOSE_BINDING))
   dev_change_flags(vlandev, flgs | IFF_UP);
  netif_stacked_transfer_operstate(dev, vlandev);
 }
 break;

        可以看出,vlan_device_event最後都是操作的vlan虛擬接口,這點是很重要的,不要越權處理其它設備。

3. 添加協議模塊vlan_packet_type到ptype_base中        

dev_add_pack(&vlan_packet_type);

        在[net\8021q]目錄,主要是關於報文接收的

static struct packet_type vlan_packet_type __read_mostly = {
 .type = cpu_to_be16(ETH_P_8021Q),
 .func = vlan_skb_recv, /* VLAN receive method */
};

        vlan_skb_recv() [net\8021q\vlan.c]
        檢查skb是否被多個協議模塊引用,如果是則拷貝一份,並遞減計數,必要時釋放skb,這部分要和netif_receive_skb()中的pt_prev連起來理解,就明白爲什麼要使用pt_prev而不是直接使用ptype。如果使用ptype,則會多出一次拷貝。

skb = skb_share_check(skb, GFP_ATOMIC);
static inline struct sk_buff *skb_share_check(struct sk_buff *skb, gfp_t pri)
{
 might_sleep_if(pri & __GFP_WAIT);
 if (skb_shared(skb)) {
  struct sk_buff *nskb = skb_clone(skb, pri);
  kfree_skb(skb);
  skb = nskb;
 }
 return skb;
}

         skb_share_check()會調用3個函數:skb_sharde(), skb_clone(), kfree_skb(),都很重要。
         skb_shared()檢查skb->users數目是否爲1,不爲1則表示有多個協議棧模塊要處理它,此時就需要使用skb_clone()來複制一份skb;kfree_skb()並不一定釋放skb,只有當skb->users爲1時,纔會釋放;否則只是遞減skb->users。

        這一步是核心,此時skb->dev爲真正的設備,經過vlan處理後,報文應該被上層協議看作是由vlan虛擬設備接收的,因此這裏設置skb->dev爲虛擬的vlan設備。

skb->dev = __find_vlan_dev(dev, vlan_id);

       以收到ARP請求報文後迴應爲例,看下skb->dev的變化,使得報文在協議棧中流轉:
 

        更新網卡接收報文的信息:

rx_stats->rx_packets++;
rx_stats->rx_bytes += skb->len;

        設置skb->len和skb->data指針,從而跑過vlan標籤,而對skb->csum的計算會忽略,因爲在網卡驅動收到報文時,skb->ip_summed== CHECKSUM_NONE。

skb_pull_rcsum(skb, VLAN_HLEN);

        重置skb->protocol爲vlan標籤後面接的協議類型,之前的protocol爲0x8100(即ETH_P_8021Q)

vlan_set_encap_proto(skb, vhdr);

        最後調用netif_rx(),它會將skb重新放入接收隊列中,讓skb在協議棧中繼續向上走。要注意的是這時候skb->protocol已經是vlan標籤後的協議標識,因此重新進入netif_receive_skb()時會被更上一層的ptype處理掉。

netif_rx(skb);

        此時協議模塊802.1q已經處理完,此時skb會被釋放掉,此時skb->users是1。

kfree_skb(skb);

         netif_rx()這個函數很重要,可以說是各個協議模塊之前報文流向的紐帶,這裏詳細講解下:
         獲取當前CPU的softnet_data結構體

queue = &__get_cpu_var(softnet_data);

         softnet_data這個結構體在設備初始化時會被賦值,見[net\core\dev.c中net_dev_init()];對於每個CPU,都會分配一個softnet_data,裏面重要的是backlog.poll = process_backlog;在軟中斷處理中,會調用poll_list鏈表上的poll方法,在稍後會看到加入poll_list鏈表的是queue->backlog,因此當再次在軟中斷中處理該報文時,會使用process_backlog()函數;作爲對比,可以看在網卡驅動中加入poll_list鏈表的是bp->napi,這時候的poll方法是網卡驅動自己的b44_poll()。

for_each_possible_cpu(i) {
 struct softnet_data *queue;
 queue = &per_cpu(softnet_data, i);
 skb_queue_head_init(&queue->input_pkt_queue);
 queue->completion_queue = NULL;
 INIT_LIST_HEAD(&queue->poll_list);
 queue->backlog.poll = process_backlog;
 queue->backlog.weight = weight_p;
 queue->backlog.gro_list = NULL;
 queue->backlog.gro_count = 0;
}

        判斷input_pkt_queue隊列長度,如果長度爲0,則將queue->backlog加入poll_list中,並觸發軟中斷,同時也將skb加入input_pkt_queue隊列;如果長度>=1,則表明input_pkt_queue隊列中還有未處理的skb,並且隊列頭的skb已經觸發了軟中斷,只是還未被處理,因此此時只需將skb加入input_pkt_queue隊列,而不用再次觸發軟中斷。
這裏有兩個地方要注意,第一是skb加入的鏈隊是input_pkt_queue,但加入poll_list的卻是backlog,這是因爲在軟中斷中調用的是backlog.poll方法,而它會處理input_pkt_queue;第二是軟中斷的觸發只在隊列爲空時再發生,因爲每次軟中斷net_rx_action()中,不只是處理一個skb,而是隊列上所有的skb:while (!list_empty(list))。

 if (queue->input_pkt_queue.qlen) {
enqueue:
  __skb_queue_tail(&queue->input_pkt_queue, skb);
  local_irq_restore(flags);
  return NET_RX_SUCCESS;
 }

 napi_schedule(&queue->backlog);
 goto enqueue;

        整體流程如圖所示: 

 4. 添加ioctl供用戶空間調用        

vlan_ioctl_set(vlan_ioctl_handler);

        添加IOCTL選 項,供用戶空間進行內核的vlan配置,比如ADD_VLAN_CMD會創建vlan虛擬接口;DEL_VLAN_CMD會刪除vlan虛擬接口。

        VLAN設備的組織結構
        如果只是vlan模塊的接收與發送,那瞭解到vlan_skb_recv()與vlan_dev_hard_start_xmit()函數就可以了。但vlan的實現考慮的要多很多,比如:新創建的eth1.1存儲在哪裏?eth1.1和eth1如果進行關聯?這些都是下面要講的。
數據結構vlan_group_hash是vlan虛擬網卡存儲與關聯的核心結構:
static struct hlist_head vlan_group_hash[VLAN_GRP_HASH_SIZE]; [net\8021q\vlan.c]
當通過vconfig創建了eth1.1, eth1.2, eth1.100三個虛擬網卡後,vlan_group_hash的整體結構如圖所示,先有個整體印象: 

         vlan_group_hash是大小爲32的hash表,所用的hash函數是:

static inline unsigned int vlan_grp_hashfn(unsigned int idx)
{
 return ((idx >> VLAN_GRP_HASH_SHIFT) ^ idx) & VLAN_GRP_HASH_MASK;
}

        而傳入參數idx就是dev->ifindex,比如eth1的就是1。因此可以這樣理解,vlan_group_hash表插入的是真實網卡設備信息(eth1)。對於一般主機來說,網卡不會太多,32個表項的hash表是完全足夠的。
在添加vlan時,會創建新的vlan虛擬網卡:
        register_vlan_device() -> register_vlan_dev()

        首先查找網卡是否已存在,這裏的real_dev一般是真實的網卡如eth1等。以real_dev->ifindex值作hash,取出vlan_group_hash的表項,由於可能存在多個網卡的hash值相同,因此還要匹配表項的real_dev是否與real_dev相同。

grp = __vlan_find_group(real_dev);

        如果不存在相應的表項,則分配表項struct vlan_group,並加入vlan_group_hash:

ngrp = grp = vlan_group_alloc(real_dev);

        結構定義如下,它可以代表在vlan下真實網卡的信息。real_dev指向真實網卡如eth1;nr_vlans表示網卡下創建的vlan數;vlan_devices_arrays用於存儲創建的vlan虛擬網卡:

struct vlan_group {
 struct net_device *real_dev;
 unsigned int  nr_vlans;
 int   killall;
 struct hlist_node hlist; /* linked list */
 struct net_device **vlan_devices_arrays[VLAN_GROUP_ARRAY_SPLIT_PARTS];
 struct rcu_head  rcu;
};

       創建完表項vlan_group,緊接初始化vlan_devices_arrays二維數組中相應元素

err = vlan_group_prealloc_vid(grp, vlan_id);

        最後,設置vlan_devices_arrays相應元素指向創建的vlan虛擬網卡(如eth1.1)的struct net_device。這裏值得注意的是vlan_devices_arrays是二維數組,內核支持的最大vlan數是4096,爲了查找效率,應用了二級目錄的概念。vlan_devices_arrays指向大小512的數組,數組中每個再指向大小8的數組,像eth1.100則位於第12組的第5個(vlan_devices_arrays[11][4])。

vlan_group_set_device(grp, vlan_id, dev);

        以一個例子來說明,當主機收到報文,交由vlan協議模塊處理後(vlan_rcv),此時需要更換skb->dev所指向的設備,以使上層協議認爲報文是來自於虛擬網卡(比如eth1.1),而不知道網卡eth1的存在。更換設備就需要知道skb->dev更換的目標。這由兩個因素決定:skb->dev和vlan_id。skb->dev即報文來自主機的哪個網卡,如來自eth1,則skb->dev->name=”eth1”;vlan_id即vlan號,這在報文中的vlan報文中可以提取出。有了這兩個信息,從vlan_group_hash出發,首先根據skb->dev->ifindex查找vlan_group_hash的相應項(eth1),取出vlan_group;然後,根據vlan_id,在vlan_devices_array中查找到虛擬網卡設備(eth1.1)。
一般支持的最大vlan數是4096,爲了查詢效率,vlan_devices_array並不是一個4096的數組,而是二維數組,將每8個vlan分爲一組,共512組,像eth1.100則位於第12組的第5個。

發佈了36 篇原創文章 · 獲贊 15 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章