NETDEV 協議 三

內核版本: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,
};

 

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個。

 

    看完了路由表,重新回到netif_receive_skb ()函數,在提交給上層協議處理前,會執行下面一句,這就是網橋的相關操作,也是這篇要講解的內容。

 
skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);

        網橋可以簡單理解爲交換機,以下圖爲例,一臺linux機器可以看作網橋和路由的結合,網橋將物理上的兩個局域網LAN1、LAN2當作一個局域網處理,路由連接了兩個子網1.0和2.0。從eth0和eth1網卡收到的報文在Bridge模塊中會被處理成是由Bridge收到的,因此Bridge也相當於一個虛擬網卡。
 

STP五種狀態
        DISABLED
        BLOCKING
        LISTENING
        LEARNING
        FORWARDING

創建新的網橋br_add_bridge [net\bridge\br_if.c]
當使用SIOCBRADDBR調用ioctl時,會創建新的網橋br_add_bridge。
        首先是創建新的網橋:

dev = new_bridge_dev(net, name);

        然後設置dev->dev.type爲br_type,而br_type是個全局變量,只初始化了一個名字變量

SET_NETDEV_DEVTYPE(dev, &br_type);
static struct device_type br_type = {
 .name = "bridge",
};

        然後註冊新創建的設備dev,網橋就相當一個虛擬網卡設備,註冊過的設備用ifconfig就可查看到:

ret = register_netdevice(dev);

        最後在sysfs文件系統中也創建相應項,便於查看和管理:

 
ret = br_sysfs_addbr(dev);


將端口加入網橋br_add_if() [net\bridge\br_if.c]
當使用SIOCBRADDIF調用ioctl時,會向網卡加入新的端口br_add_if。
        創建新的net_bridge_port p,會從br->port_list中分配一個未用的port_no,p->br會指向br,p->state設爲BR_STATE_DISABLED。這裏的p實際代表的就是網卡設備。

p = new_nbp(br, dev);

        將新創建的p加入CAM表中,CAM表是用來記錄mac地址與物理端口的對應關係;而剛剛創建了p,因此也要加入CAM表中,並且該表項應是local的[關係如下圖],可以看到,CAM表在實現中作爲net_bridge的hash表,以addr作爲hash值,鏈入net_bridge_fdb_entry,再由它的dst指向net_bridge_port。

 
err = br_fdb_insert(br, p, dev->dev_addr); 

        設備的br_port指向p。這裏要明白的是,net_bridge可以看作全局量,是網橋,而net_bridge_port則是與網卡相對應的端口,因此每個設備dev有個指針br_port指向該端口。

rcu_assign_pointer(dev->br_port, p);

        將新創建的net_bridge_port加入br的鏈表port_list中,在創建新的net_bridge_port時,會分配一個未用的port_no,而這個port_no就是根據br->port_list中的已經添加的net_bridge_port來找到未用的port_no的[具體如下圖]。 

list_add_rcu(&p->list, &br->port_list);

        重新計算網橋的ID,這裏根據br->port_list鏈表中的net_bridge_port的最小的addr來作爲網橋的ID。

br_stp_recalculate_bridge_id(br);

        網卡設備的刪除br_del_bridge()與端口的移除add_del_if()與添加差不多,不再詳述。


熟悉了網橋的創建與添加,再來看下網橋是如何工作的。
        當收到數據包,通過netif_receive_skb()->handle_bridge()處理網橋:

static inline struct sk_buff *handle_bridge(struct sk_buff *skb,
         struct packet_type **pt_prev, int *ret,
         struct net_device *orig_dev)
{
 struct net_bridge_port *port;

 if (skb->pkt_type == PACKET_LOOPBACK ||
     (port = rcu_dereference(skb->dev->br_port)) == NULL)
  return skb;

 if (*pt_prev) {
  *ret = deliver_skb(skb, *pt_prev, orig_dev);
  *pt_prev = NULL;
 }

 return br_handle_frame_hook(port, skb);
}

        1. 如果報文來自lo設備,或者dev->br_port爲空(skb->dev是收到報文的網卡設備,而在向網橋添加端口時,dev->br_port被賦予了創建的與網卡相對應的端口p),此時不需要網橋處理,直接返回報文;
        2. 如果報文匹配了之前的ptype_all中的協議,則pt_prev不爲空,此時要先進行ptype_all中協議的處理,再進行網橋的處理;
        3. br_handle_frame_hook是網橋處理鉤子函數,在br_init() [net\bridge\br.c]中
             br_handle_frame_hook = br_handle_frame;
             br_handle_frame() [net\bridge\br_input.c]是真正的網橋處理函數,

        下面進入br_handle_frame()開始網橋部分的處理:
        與前面802.1q講的一樣,首先檢查users來決定是否複製報文:

 
skb = skb_share_check(skb, GFP_ATOMIC);

        如果報文的目的地址是01:80:c2:00:00:0X,則是發往STP的多播地址,此時調用br_handle_local_finish()來完成報文的進一步處理:

if (unlikely(is_link_local(dest))){
……
if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
   NULL, br_handle_local_finish))
  return NULL; /* frame consumed by filter */
 else
  return skb;
}

        而br_handle_local_finish()所做的內容很簡單,因爲是多播報文,主機要做的僅僅是更新報文的源mac與接收端口的關係(在CAM表中)。

static int br_handle_local_finish(struct sk_buff *skb)
{
 struct net_bridge_port *p = rcu_dereference(skb->dev->br_port);

 if (p)
  br_fdb_update(p->br, p, eth_hdr(skb)->h_source);
 return 0;  /* process further */
}

        接着br_handle_frame()繼續往下看,然後根據端口的狀態來處理報文,如果端口state= BR_STATE_FORWARDING且設置了br_should_route_hook,則轉發後返回skb;否則繼續往下執行state=BR_STATE_LEARNING段的代碼:

rhook = rcu_dereference(br_should_route_hook);
if (rhook != NULL) {
 if (rhook(skb))
  return skb;
 dest = eth_hdr(skb)->h_dest;
}

        如果端口state= BR_STATE_LEARNING,如果是發往本機的報文,則設置pkt_type爲PACKET_HOST,然後執行br_handle_frame_finish來完成報文的進一步處理。要注意的是,這裏將報文發往本機的報文設爲PACKET_HOST,實現了經過網橋處理後,再次進入netif_receive_skb()時,不會再被網橋處理(結果進入網橋的條件理解):

if (!compare_ether_addr(p->br->dev->dev_addr, dest))
  skb->pkt_type = PACKET_HOST;
NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
  br_handle_frame_finish);

        除此之外,端口處於不可用狀態,此時丟棄掉報文:

kfree_skb(skb);


        下面來詳細看下br_handle_frame_finish()函數。
        首先還是會根據收到報文的源mac和端口更新CAM表,這是交換機區別於hub的重要特徵:

 
br_fdb_update(br, p, eth_hdr(skb)->h_source);

        然後如果端口處於LEARNING狀態,則只是學習到CAM表中,而不對報文作任何處理,所以丟棄掉報文:

if (p->state == BR_STATE_LEARNING)
  goto drop;

        否則端口已處於FORWARDING狀態,此時分情況:
            1. 如果報文是多播的,則br_flood_forward(br, skb, skb2);
            2. 如果報文是單播的,但不在網橋的CAM表中,則br_flood_forward(br, skb, skb2);
            3. 如果報文是單播的,在網橋的CAM表中,但不是發往本機,則br_forward(dst->dst, skb, skb2);
            4. 如果報文是單播的,在網橋的CAM表中,且是發往本機,則br_pass_frame_upbr_pass_frame_up(skb2);

        br_handle_frame_finish()處理完後,順着最後一種情況繼續往下走,br_pass_frame_up()。
        該函數比較簡單,我們知道,在底層報文的向上傳遞就是通過設備的更換來進行的(參考802.1q),這裏將skb的設備換成網橋設備,使上層協議不知道報文來自網卡,而是認爲報文來自於網橋;然後調用netif_receive_skb()再次進入接收棧:

skb->dev = brdev;
return NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,
         netif_receive_skb);

        經過網橋處理後,再次進入netif_receive_skb()->handle_bridge(),此時skb->dev已經不是網卡設備了,而是網橋設備,注意到在向網橋添加端口時,是相應網卡dev->br_port賦值爲創建的端口,網橋設備是沒有的,因此其br_port爲空,在這一句會直接返回,進入正常的協議棧流程:

if (skb->pkt_type == PACKET_LOOPBACK ||
     (port = rcu_dereference(skb->dev->br_port)) == NULL)
  return skb;


        當發送數據報文時,會調用br_dev_xmit()[net\bridge\br_device.c],大致會根據目的地址調用br_multicast_deliver()或br_flood_deliver()或br_deliver(),在其過程中會將skb->dev由原來的網橋設備brdev換面網卡設備dev,然後通過網卡變更向下傳遞報文;
        內核協議棧中,發送與接收是分離的,接收像是報文脫殼的過程,發送則是函數的嵌套調用。有關發送的流程,稍後專門詳述。

 

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