內核版本: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個。