歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[五]:vlan協議-802.1q

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

日期:2017/3/3 16:38:21   编辑:Linux內核

內核版本: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) {
nqueue:
 __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個。

Copyright © Linux教程網 All Rights Reserved