PF_RING實現分析

http://bbs.chinaunix.net/thread-1943951-1-1.html
版權所有,轉載請註明出處

獨孤九賤

內核版本:Linux 2.6.30.9

PF_RING版本:4.1.0

最近看了一個PF_RING的實現,看了個大概,發上來大家討論討論,共同學習。

一、什麼是PF_RING
PF_RING是一個第三方的內核數據包捕獲接口,類似於libpcap,它的官方網址是:http://www.ntop.org/PF_RING.html

二、爲什麼需要PF_RING
一切爲了效率,按照其官方網站上的測試數據,在Linux平臺之上,其效率至少高於libpcap 50% - 60%,甚至是一倍。更好的是,PF_RING提供了一個修改版本的libpcap,使之建立在PF_RING接口之上。這樣,原來使用libpcap的程序,就可以自然過渡了。

三、聲明
1、這不是“零拷貝”,研究“零拷貝”的估計要失望,如果繼續看下去的話;
2、這不是包截獲接口,如果需要攔截、修改內核數據包,請轉向Netfilter;
3、本文只分析了PF_RING最基礎的部份。關於DNA、TNAPI,BPF等內容不包含在內。

四、源碼的獲取
svn co https://svn.ntop.org/svn/ntop/trunk/PF_RING/ 

最近好像全流行svn了。

五、編譯和使用
接口分爲兩部份,一個是內核模塊,一個是用戶態的庫
cd my_pf_ring_goes_here 
cd kernel 
make 
sudo insmod ./pf_ring.ko 
cd ../userland 
make 

在源碼目錄中,關於用戶態的庫有使用的現成的例子,很容易依葫蘆畫瓢。後文也會提到用戶態庫的實現的簡單分析,可以兩相比照,很容易上手。而且源碼目錄中有一個PDF文檔,有詳細的API介紹,建議使用前閱讀。

六、實現分析初步
1、核心思路
A、在內核隊列層註冊Hook,獲取數據幀。
B、在內核創建一個環形隊列(這也是叫RING的原因),用於存儲數據,並使用mmap映射到用戶空間。這樣,避免用戶態的系統調用,也是提高性能的關鍵所在。
C、創建了一個新的套接字類型PF_RING,用戶態通過它與內核通信。

2、模塊初始化
模塊源碼只有一個文件,在目錄樹kernel/pf_ring.c,嗯,還有一個頭文件,在kernel/linux下
  1. static int __init ring_init(void)
  2. {
  3.   int i, rc;

  4.   printk("[PF_RING] Welcome to PF_RING %s ($Revision: 4012 $)\n"
  5.          "(C) 2004-09 L.Deri <[email][email protected][/email]>\n", RING_VERSION);
  6.         
  7.         //註冊名爲PF_RING的新協議
  8.   if((rc = proto_register(&ring_proto, 0)) != 0)
  9.     return(rc);
複製代碼


ring_proto的定義爲
  1. #if(LINUX_VERSION_CODE > KERNEL_VERSION(2,6,11))
  2. static struct proto ring_proto = {
  3.   .name = "PF_RING",
  4.   .owner = THIS_MODULE,
  5.   .obj_size = sizeof(struct ring_sock),
  6. };
  7. #endif
複製代碼


初始化四個鏈表,它們的作用,後文會分析到:
  1.         //初始化四個鏈表
  2.   INIT_LIST_HEAD(&ring_table);                                        /* List of all ring sockets. */
  3.   INIT_LIST_HEAD(&ring_cluster_list);                        /* List of all clusters */
  4.   INIT_LIST_HEAD(&ring_aware_device_list);                /* List of all devices on which PF_RING has been registered */
  5.   INIT_LIST_HEAD(&ring_dna_devices_list);                /* List of all dna (direct nic access) devices */
複製代碼


device_ring_list是一個指針數組,它的每一個元素對應一個網絡設備,後文也會分析它的使用:
  1. /*
  2.   For each device, pf_ring keeps a list of the number of
  3.   available ring socket slots. So that a caller knows in advance whether
  4.   there are slots available (for rings bound to such device)
  5.   that can potentially host the packet
  6. */
  7.   for (i = 0; i < MAX_NUM_DEVICES; i++)
  8.     INIT_LIST_HEAD(&device_ring_list[i]);
複製代碼

  1.         //爲新協議註冊sock
  2.   sock_register(&ring_family_ops);
複製代碼


ring_family_ops定義爲
  1. static struct net_proto_family ring_family_ops = {
  2.   .family = PF_RING,
  3.   .create = ring_create,
  4.   .owner = THIS_MODULE,
  5. };
複製代碼

這樣,當用戶空間創建PF_RING時,例如,
  1. fd = socket(PF_RING, SOCK_RAW, htons(ETH_P_ALL));
複製代碼

ring_create將會被調用

  1.   //註冊通知鏈表  
  2.   register_netdevice_notifier(&ring_netdev_notifier);

  3.   /* 工作模式語法檢查 */
  4.   if(transparent_mode > driver2pf_ring_non_transparent)
  5.     transparent_mode = standard_linux_path;
複製代碼


PF_RING一共有三種工作模式:
  1. typedef enum {
  2.   standard_linux_path = 0, /* Business as usual */
  3.   driver2pf_ring_transparent = 1, /* Packets are still delivered to the kernel */
  4.   driver2pf_ring_non_transparent = 2 /* Packets not delivered to the kernel */
  5. } direct2pf_ring;
複製代碼

第一種最簡單,這裏僅分析第一種

  1.         //輸出工作信息參數
  2.   printk("[PF_RING] Ring slots       %d\n", num_slots);
  3.   printk("[PF_RING] Slot version     %d\n",
  4.          RING_FLOWSLOT_VERSION);
  5.   printk("[PF_RING] Capture TX       %s\n",
  6.          enable_tx_capture ? "Yes [RX+TX]" : "No [RX only]");
  7.   printk("[PF_RING] Transparent Mode %d\n",
  8.          transparent_mode);
  9.   printk("[PF_RING] IP Defragment    %s\n",
  10.          enable_ip_defrag ? "Yes" : "No");
  11.   printk("[PF_RING] Initialized correctly\n");
複製代碼


num_slots爲槽位總數,系統採用數組來實現雙向環形隊列,它也就代表數組的最大元素。
版本信息:不用多說了。不過我的版本在2.6.18及以下都沒有編譯成功,後來使用2.6.30.9搞定之。
enable_tx_capture:是否啓用發送時的數據捕獲,對於大多數應用而言,都是在接收時處理。
enable_ip_defrag:爲用戶提供一個接口,是否在捕獲最重組IP分片。

  1.         //創建/proc目錄
  2.   ring_proc_init();
  3.   
  4.   //註冊設備句柄
  5.   register_device_handler();

  6.   pfring_enabled = 1;                //工作標誌
  7.   return 0;
  8. }
複製代碼


register_device_handler註冊了一個協議,用於數據包的獲取:
  1. /* Protocol hook */
  2. static struct packet_type prot_hook;

  3. void register_device_handler(void) {
  4.         //只有在第一種模式下,才用這種方式接收數據
  5.   if(transparent_mode != standard_linux_path) return;

  6.   prot_hook.func = packet_rcv;
  7.   prot_hook.type = htons(ETH_P_ALL);
  8.   dev_add_pack(&prot_hook);
  9. }

  10. void register_device_handler(void) {
  11.   if(transparent_mode != standard_linux_path) return;

  12.   prot_hook.func = packet_rcv;
  13.   prot_hook.type = htons(ETH_P_ALL);
  14.   dev_add_pack(&prot_hook);
  15. }
複製代碼


2、創建套接字
Linux的套按字的內核接口,使用了兩個重要的數據結構:
struct socket和struct sock,這本來並沒有什麼,不過令人常常迷惑的是,前者常常被縮寫爲sock,即:
struct socket *sock;
這樣,“sock”就容易造成混淆了。還好,後者常常被縮寫爲sk……
我這裏寫sock指前者,sk指後者,如果不小心寫混了,請參考上下文區分 。

關於這兩個結構的含義,使用等等,可以參考相關資料以獲取詳細信息,如《Linux情景分析》。我的個人網站www.skynet.org.cn上也分析了Linux socket的實現。可以參考。這裏關於socket的進一步信息,就不詳細分析了。

這裏的創建套接字,內核已經在系統調用過程中,準備好了sock,主要就是分析sk,併爲sk指定一系列的操作函數,如bind、mmap、poll等等。
如前所述,套接字的創建,是通過調用ring_create函數來完成的:
  1. static int ring_create(
  2. #if(LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,24))
  3.                        struct net *net,
  4. #endif
  5.                        struct socket *sock, int protocol)
  6. {
  7.   struct sock *sk;
  8.   struct ring_opt *pfr;
  9.   int err;

  10. #if defined(RING_DEBUG)
  11.   printk("[PF_RING] ring_create()\n");
  12. #endif

  13.   /* 權限驗證 ? */
  14.   if(!capable(CAP_NET_ADMIN))
  15.     return -EPERM;

  16.   //協議簇驗證
  17.   if(sock->type != SOCK_RAW)
  18.     return -ESOCKTNOSUPPORT;

  19.   //協議驗證
  20.   if(protocol != htons(ETH_P_ALL))
  21.     return -EPROTONOSUPPORT;

  22.   err = -ENOMEM;

  23.   // 分配sk
  24.   // options are.
  25. #if(LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,11))
  26.   sk = sk_alloc(PF_RING, GFP_KERNEL, 1, NULL);
  27. #else
  28. #if(LINUX_VERSION_CODE < KERNEL_VERSION(2,6,24))
  29.     // BD: API changed in 2.6.12, ref:
  30.     // [url]http://svn.clkao.org/svnweb/linux/revision/?rev=28201[/url]
  31.     sk = sk_alloc(PF_RING, GFP_ATOMIC, &ring_proto, 1);
  32. #else
  33.     sk = sk_alloc(net, PF_INET, GFP_KERNEL, &ring_proto);
  34. #endif
  35. #endif

  36.   //分配失敗
  37.   if(sk == NULL)
  38.     goto out;
  39.   
  40.   //這裏很重要,設定sock的ops,即對應用戶態的bind、connect諸如此類操作的動作
  41.   sock->ops = &ring_ops;

  42.   //初始化sock結構(即sk)各成員,並設定與套接字socket(即sock)的關聯
  43.   sock_init_data(sock, sk);
  44. #if(LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,11))
  45.   sk_set_owner(sk, THIS_MODULE);
  46. #endif

  47.   err = -ENOMEM;

  48. #define ring_sk_datatype(__sk) ((struct ring_opt *)__sk)
  49. #define ring_sk(__sk) ((__sk)->sk_protinfo)

  50. //作者喜歡用小寫的宏
  51. //這裏分配一個struct ring_opt結構,這個結構比較重要,其記錄了ring的選項信息。
  52. 在sk中,使用sk_protinfo成員指向之,這樣就建立了sock->sk->ring_opt的關聯。可以通過套接字很容易獲取Ring的信息。
  53.   ring_sk(sk) = ring_sk_datatype(kmalloc(sizeof(*pfr), GFP_KERNEL));
  54.   //分配失敗
  55.   if(!(pfr = ring_sk(sk))) {
  56.     sk_free(sk);
  57.     goto out;
  58.   }

  59.   //初始化各成員
  60.   memset(pfr, 0, sizeof(*pfr));
  61. //激活標誌
  62.   pfr->ring_active = 0;        /* We activate as soon as somebody waits for packets */
  63.   //通道ID
  64.   pfr->channel_id = RING_ANY_CHANNEL;
  65.   //RING的每個槽位的桶的大小,其用來存儲捕獲的數據幀,這個值,用戶態也可以使用setsocketopt來調整
  66.   pfr->bucket_len = DEFAULT_BUCKET_LEN;
  67. //過濾器hash桶
  68.   pfr->handle_hash_rule = handle_filtering_hash_bucket;
  69.   //初始化等待隊列
  70.   init_waitqueue_head(&pfr->ring_slots_waitqueue);
  71.   //初始化RING的鎖
  72.   rwlock_init(&pfr->ring_index_lock);
  73.   rwlock_init(&pfr->ring_rules_lock);
  74.   //初始化使用計數器
  75.   atomic_set(&pfr->num_ring_users, 0);
  76.   INIT_LIST_HEAD(&pfr->rules);
  77.   //設定協議簇
  78.   sk->sk_family = PF_RING;
  79. //設定sk的destuct函數
  80.   sk->sk_destruct = ring_sock_destruct;

  81.   //sk入隊
  82.   ring_insert(sk);

  83. #if defined(RING_DEBUG)
  84.   printk("[PF_RING] ring_create() - created\n");
  85. #endif

  86.   return(0);
  87. out:
  88.   return err;
複製代碼


在模塊初始化中,初始化過四個鏈表。其中一個是ring_table,ring_insert將剛剛創建的套接字插入其中。其封裝引進了一個struct ring_element 結構:
  1. /*
  2. * ring_insert()
  3. *
  4. * store the sk in a new element and add it
  5. * to the head of the list.
  6. */
  7. static inline void ring_insert(struct sock *sk)
  8. {
  9.   struct ring_element *next;
  10.   struct ring_opt *pfr;

  11. #if defined(RING_DEBUG)
  12.   printk("[PF_RING] ring_insert()\n");
  13. #endif

  14. //分配element
  15.   next = kmalloc(sizeof(struct ring_element), GFP_ATOMIC);
  16.   if(next != NULL) {
  17.     //記錄sk
  18.     next->sk = sk;
  19.     write_lock_bh(&ring_mgmt_lock);
  20.     //入隊
  21.     list_add(&next->list, &ring_table);
  22.     write_unlock_bh(&ring_mgmt_lock);
  23.   } else {
  24.     if(net_ratelimit())
  25.       printk("[PF_RING] net_ratelimit() failure\n");
  26.   }

  27.   //累計使用計數器
  28.   ring_table_size++;
  29.   //ring_proc_add(ring_sk(sk));
  30.   //記錄進程PID
  31.   pfr = (struct ring_opt *)ring_sk(sk);
  32.   pfr->ring_pid = current->pid;
  33. }
複製代碼


3 、分配隊列空間
用戶態在創建了套接字後,接下來就調用bind函數,綁定套接字,而PF_RING實際做的就是爲RING分配相應的空間。也就是說,一個套接字,都有一個與之對應的RING。這樣,有多個進程同時使用PF_RING,也沒有問題:
  1.                 sa.sa_family   = PF_RING;
  2.                 snprintf(sa.sa_data, sizeof(sa.sa_data), "%s", device_name);
  3.                 rc = bind(ring->fd, (struct sockaddr *)&sa, sizeof(sa));
複製代碼


因爲前一步創建套接字時,爲sk指定了其ops:
  1. static struct proto_ops ring_ops = {
  2.   .family = PF_RING,
  3.   .owner = THIS_MODULE,

  4.   /* Operations that make no sense on ring sockets. */
  5.   .connect = sock_no_connect,
  6.   .socketpair = sock_no_socketpair,
  7.   .accept = sock_no_accept,
  8.   .getname = sock_no_getname,
  9.   .listen = sock_no_listen,
  10.   .shutdown = sock_no_shutdown,
  11.   .sendpage = sock_no_sendpage,
  12.   .sendmsg = sock_no_sendmsg,

  13.   /* Now the operations that really occur. */
  14.   .release = ring_release,
  15.   .bind = ring_bind,
  16.   .mmap = ring_mmap,
  17.   .poll = ring_poll,
  18.   .setsockopt = ring_setsockopt,
  19.   .getsockopt = ring_getsockopt,
  20.   .ioctl = ring_ioctl,
  21.   .recvmsg = ring_recvmsg,
  22. };
複製代碼


這樣,當bing系統調用觸發時,ring_bind函數將被調用:

  1. * Bind to a device */
  2. static int ring_bind(struct socket *sock, struct sockaddr *sa, int addr_len)
  3. {
  4.   struct sock *sk = sock->sk;
  5.   struct net_device *dev = NULL;

  6. #if defined(RING_DEBUG)
  7.   printk("[PF_RING] ring_bind() called\n");
  8. #endif

  9.   /*
  10.    *      Check legality
  11.    */
  12.   if(addr_len != sizeof(struct sockaddr))
  13.     return -EINVAL;
  14.   if(sa->sa_family != PF_RING)
  15.     return -EINVAL;
  16.   if(sa->sa_data == NULL)
  17.     return -EINVAL;

  18.   /* Safety check: add trailing zero if missing */
  19.   sa->sa_data[sizeof(sa->sa_data) - 1] = '\0';

  20. #if defined(RING_DEBUG)
  21.   printk("[PF_RING] searching device %s\n", sa->sa_data);
  22. #endif

  23.   if((dev = __dev_get_by_name(
  24. #if(LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,24))
  25.                                &init_net,
  26. #endif
  27.                                sa->sa_data)) == NULL) {
  28. #if defined(RING_DEBUG)
  29.     printk("[PF_RING] search failed\n");
  30. #endif
  31.     return(-EINVAL);
  32.   } else
  33.     return(packet_ring_bind(sk, dev));
  34. }
複製代碼


在做了一些必要的語法檢查後,函數轉向packet_ring_bind:
  1. /*
  2. * We create a ring for this socket and bind it to the specified device
  3. */
  4. static int packet_ring_bind(struct sock *sk, struct net_device *dev)
  5. {
  6.   u_int the_slot_len;
  7.   u_int32_t tot_mem;
  8.   struct ring_opt *pfr = ring_sk(sk);
  9.   // struct page *page, *page_end;

  10.   if(!dev)
  11.     return(-1);

  12. #if defined(RING_DEBUG)
  13.   printk("[PF_RING] packet_ring_bind(%s) called\n", dev->name);
  14. #endif

  15.   /* **********************************************

  16.    * *************************************
  17.    * *                                   *
  18.    * *        FlowSlotInfo               *
  19.    * *                                   *
  20.    * ************************************* <-+
  21.    * *        FlowSlot                   *   |
  22.    * *************************************   |
  23.    * *        FlowSlot                   *   |
  24.    * *************************************   +- num_slots
  25.    * *        FlowSlot                   *   |
  26.    * *************************************   |
  27.    * *        FlowSlot                   *   |
  28.    * ************************************* <-+
  29.    *
  30.    * ********************************************** */

  31.         //計算每一個槽位所需的內存空間
  32.   the_slot_len = sizeof(u_char)        /* flowSlot.slot_state */
  33. #ifdef RING_MAGIC
  34.     + sizeof(u_char)
  35. #endif
  36.     + sizeof(struct pfring_pkthdr)
  37.     + pfr->bucket_len /* flowSlot.bucket */ ;
  38. /*    
  39.         對於槽位空間的計算,有意思的是
  40. typedef struct flowSlot {
  41. #ifdef RING_MAGIC
  42.   u_char     magic;      /* It must alwasy be zero */
  43. #endif
  44.   u_char     slot_state; /* 0=empty, 1=full   */
  45.   u_char     bucket;     /* bucket[bucketLen] */
  46. } FlowSlot;
  47. 對照結構定義和上面的計算公式:
  48. 1、作者好像把magic和slog_state的順序給搞反了,不過還好,它們都是u_char,對結果不影響
  49. 2、bucket,桶的大小,這個桶就是拿來裝要捕獲的數據包了,雖然它在結構中定義是一個成員,事實上,
  50. 它由兩個部份組成,一個是包的首部信息,這個結構的定義同libpcap很接近。另一個纔是包的空間。
  51. */

  52.         //總共的環形隊列內存所需空間,包含一個隊列控制信息FlowSlotInfo和若干個(由變量num_slots決定)槽位空間
  53.   tot_mem = sizeof(FlowSlotInfo) + num_slots * the_slot_len;
  54.   
  55.   //確保按整數頁分配,mmap也要求這樣
  56.   if(tot_mem % PAGE_SIZE)
  57.     tot_mem += PAGE_SIZE - (tot_mem % PAGE_SIZE);

  58.         //分配內存空間
  59.   pfr->ring_memory = rvmalloc(tot_mem);

  60.   if(pfr->ring_memory != NULL) {
  61.     printk("[PF_RING] successfully allocated %lu bytes at 0x%08lx\n",
  62.            (unsigned long)tot_mem, (unsigned long)pfr->ring_memory);
  63.   } else {
  64.     printk("[PF_RING] ERROR: not enough memory for ring\n");
  65.     return(-1);
  66.   }

  67.   // memset(pfr->ring_memory, 0, tot_mem); // rvmalloc does the memset already
  68.   //初始化各成員
  69.         //內存指定,因爲分配的內存開始部份是sizeof(FlowSlotInfo),所以可以做這樣的強制轉換,很容易互相取值
  70.   pfr->slots_info = (FlowSlotInfo *) pfr->ring_memory;
  71.   //跳過控制信息,指向槽位指針.事實上,它就是一個一維數組了,可以計算出合適的索引值,取到數組(RING)中的任意槽位值
  72.   pfr->ring_slots = (char *)(pfr->ring_memory + sizeof(FlowSlotInfo));

  73.   //版本信息
  74.   pfr->slots_info->version = RING_FLOWSLOT_VERSION;
  75.   //登記單個槽的大小
  76.   pfr->slots_info->slot_len = the_slot_len;
  77.   //登記bucket大小,從前面特別註釋的bucket的分配看,bucket_len這個大小並不代表bucket成員的實際大小——它不包含struct pfring_pkthdr
  78.   pfr->slots_info->data_len = pfr->bucket_len;
  79.   //登記實際分配到的槽位數量,這裏不用num_slots,難道是怕rvmalloc偷吃?
  80.   pfr->slots_info->tot_slots =
  81.     (tot_mem - sizeof(FlowSlotInfo)) / the_slot_len;
  82.   //登記實際分配的內存總數  
  83.   pfr->slots_info->tot_mem = tot_mem;
  84.   //採樣速率??
  85.   pfr->slots_info->sample_rate = 1;

  86.   printk("[PF_RING] allocated %d slots [slot_len=%d][tot_mem=%u]\n",
  87.          pfr->slots_info->tot_slots, pfr->slots_info->slot_len,
  88.          pfr->slots_info->tot_mem);

  89. #ifdef RING_MAGIC
  90.   {
  91.     int i;

  92.     for (i = 0; i < pfr->slots_info->tot_slots; i++) {
  93.       unsigned long idx = i * pfr->slots_info->slot_len;
  94.       FlowSlot *slot = (FlowSlot *) & pfr->ring_slots[idx];
  95.       slot->magic = RING_MAGIC_VALUE;
  96.       slot->slot_state = 0;
  97.     }
  98.   }
  99. #endif
  100.   //這些控制變量可以在環的入隊操作中看到它們的作用
  101.   pfr->sample_rate = 1;        /* No sampling */
  102.   pfr->insert_page_id = 1, pfr->insert_slot_id = 0;
  103.   pfr->rules_default_accept_policy = 1, pfr->num_filtering_rules = 0;
  104.   ring_proc_add(ring_sk(sk), dev);

  105.   //記錄與之相應的設備信息,例如,如果在eth0上打開了5 個PF_RING, bind
  106.   //被調用5次,分配了5個環形隊列空間。eth0上隨之分配5個elem,它們指向與
  107.   //之對應的ring,然後根據設備索引號民全部加入至了device_ring_list
  108.   //當有數據報文從指定接口進入時,可以很容易地在device_ring_list中找到相應的設備
  109.   //然後再遍歷鏈表,再找到與之相應的ring
  110.   if(dev->ifindex < MAX_NUM_DEVICES) {
  111.     device_ring_list_element *elem;

  112.     /* printk("[PF_RING] Adding ring to device index %d\n", dev->ifindex); */

  113.     elem = kmalloc(sizeof(device_ring_list_element), GFP_ATOMIC);
  114.     if(elem != NULL) {
  115.       elem->the_ring = pfr;
  116.       INIT_LIST_HEAD(&elem->list);
  117.       write_lock(&ring_list_lock);
  118.       list_add(&elem->list, &device_ring_list[dev->ifindex]);
  119.       write_unlock(&ring_list_lock);
  120.       /* printk("[PF_RING] Added ring to device index %d\n", dev->ifindex); */
  121.     }
  122.   }

  123.   /*
  124.     IMPORTANT
  125.     Leave this statement here as last one. In fact when
  126.     the ring_netdev != NULL the socket is ready to be used.
  127.   */
  128.   pfr->ring_netdev = dev;

  129.   return(0);
  130. }
複製代碼

這個函數中,最重要的三點:
1、整個空間的詳細構成,作者畫了一個簡單的草圖,清晰明瞭。
2、如果取得某個槽位。
3、device_ring_list鏈表的使用。

一些作者有詳細註釋的地方,我就不再重重了。

這一步進行完了後,就有一塊內存了(系統將其看成一個數組),用來存儲捕獲的數據幀。接下來要做的事情。就是把它映射到用戶態。

4、mmap操作
用戶態的接下來調用:
  1.                         ring->buffer = (char *)mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE,
  2.                                 MAP_SHARED, ring->fd, 0);
複製代碼

進行內存映射。
同樣地,內核調用相應的ring_mmap進行處理。
Ring選項結構通過ring_sk宏與sk 建立關聯
  1. struct ring_opt *pfr = ring_sk(sk);
複製代碼

pfr->ring_memory 即爲分配的環形隊列空間。所以,要mmap操作,實際上就是調用remap_pfn_range函數把pfr->ring_memory 映射到用戶空間即可。這個函數的原型爲:
  1. /**
  2. * remap_pfn_range - remap kernel memory to userspace
  3. * @vma: user vma to map to
  4. * @addr: target user address to start at
  5. * @pfn: physical address of kernel memory
  6. * @size: size of map area
  7. * @prot: page protection flags for this mapping
  8. *
  9. *  Note: this is only safe if the mm semaphore is held when called.
  10. */
  11. int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
  12.                     unsigned long pfn, unsigned long size, pgprot_t prot)
  13. {
複製代碼

關於remap_pfn_range函數的進一步說明,可以參考LDD3,上面有詳細說明和現成的例子。

  1. static int ring_mmap(struct file *file,
  2.                      struct socket *sock, struct vm_area_struct *vma)
  3. {
  4.   struct sock *sk = sock->sk;
  5.   struct ring_opt *pfr = ring_sk(sk);                //取得pfr指針,也就是相應取得環形隊列的內存空間地址指針
  6.   int rc;
  7.   unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);

  8.   if(size % PAGE_SIZE) {
  9. #if defined(RING_DEBUG)
  10.     printk("[PF_RING] ring_mmap() failed: "
  11.            "len is not multiple of PAGE_SIZE\n");
  12. #endif
  13.     return(-EINVAL);
  14.   }
  15. #if defined(RING_DEBUG)
  16.   printk("[PF_RING] ring_mmap() called, size: %ld bytes\n", size);
  17. #endif

  18.   if((pfr->dna_device == NULL) && (pfr->ring_memory == NULL)) {
  19. #if defined(RING_DEBUG)
  20.     printk("[PF_RING] ring_mmap() failed: "
  21.            "mapping area to an unbound socket\n");
  22. #endif
  23.     return -EINVAL;
  24.   }

  25.   //dns設備爲空,即沒有使用dns技術
  26.   if(pfr->dna_device == NULL) {
  27.     /* if userspace tries to mmap beyond end of our buffer, fail */
  28.     //映射空間超限
  29.     if(size > pfr->slots_info->tot_mem) {
  30. #if defined(RING_DEBUG)
  31.       printk("[PF_RING] ring_mmap() failed: "
  32.              "area too large [%ld > %d]\n",
  33.              size, pfr->slots_info->tot_mem);
  34. #endif
  35.       return(-EINVAL);
  36.     }
  37. #if defined(RING_DEBUG)
  38.     printk("[PF_RING] mmap [slot_len=%d]"
  39.            "[tot_slots=%d] for ring on device %s\n",
  40.            pfr->slots_info->slot_len, pfr->slots_info->tot_slots,
  41.            pfr->ring_netdev->name);
  42. #endif
  43.         //進行內存映射
  44.     if((rc =
  45.          do_memory_mmap(vma, size, pfr->ring_memory, VM_LOCKED,
  46.                         0)) < 0)
  47.       return(rc);
  48.   } else {
  49.     /* DNA Device */
  50.     if(pfr->dna_device == NULL)
  51.       return(-EAGAIN);

  52.     switch (pfr->mmap_count) {
  53.     case 0:
  54.       if((rc = do_memory_mmap(vma, size,
  55.                                (void *)pfr->dna_device->
  56.                                packet_memory, VM_LOCKED,
  57.                                1)) < 0)
  58.         return(rc);
  59.       break;

  60.     case 1:
  61.       if((rc = do_memory_mmap(vma, size,
  62.                                (void *)pfr->dna_device->
  63.                                descr_packet_memory, VM_LOCKED,
  64.                                1)) < 0)
  65.         return(rc);
  66.       break;

  67.     case 2:
  68.       if((rc = do_memory_mmap(vma, size,
  69.                                (void *)pfr->dna_device->
  70.                                phys_card_memory,
  71.                                (VM_RESERVED | VM_IO), 2)) < 0)
  72.         return(rc);
  73.       break;

  74.     default:
  75.       return(-EAGAIN);
  76.     }

  77.     pfr->mmap_count++;
  78.   }

  79. #if defined(RING_DEBUG)
  80.   printk("[PF_RING] ring_mmap succeeded\n");
  81. #endif

  82.   return 0;
  83. }
複製代碼


實際上的內存映射工作,是由do_memory_mmap來完成的,這個函數實際上基本就是remap_pfn_range的包裹函數。
不過因爲系統支持dna等技術,相應的mode參數有些變化,這裏只分析了最基本的方法:mode == 0

  1. static int do_memory_mmap(struct vm_area_struct *vma,
  2.                           unsigned long size, char *ptr, u_int flags, int mode)
  3. {
  4.   unsigned long start;
  5.   unsigned long page;

  6.   /* we do not want to have this area swapped out, lock it */
  7.   vma->vm_flags |= flags;
  8.   start = vma->vm_start;

  9.   while (size > 0) {
  10.     int rc;

  11.     if(mode == 0) {
  12. #if(LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,11))
  13.           //根據地址,計算要映射的頁幀
  14.       page = vmalloc_to_pfn(ptr);
  15.       //進行內存映射
  16.       rc = remap_pfn_range(vma, start, page, PAGE_SIZE,
  17.                            PAGE_SHARED);
  18. #else
  19.       page = vmalloc_to_page(ptr);
  20.       page = kvirt_to_pa(ptr);
  21.       rc = remap_page_range(vma, start, page, PAGE_SIZE,
  22.                             PAGE_SHARED);
  23. #endif
  24.     } else if(mode == 1) {
  25.       rc = remap_pfn_range(vma, start,
  26.                            __pa(ptr) >> PAGE_SHIFT,
  27.                            PAGE_SIZE, PAGE_SHARED);
  28.     } else {
  29.       rc = remap_pfn_range(vma, start,
  30.                            ((unsigned long)ptr) >> PAGE_SHIFT,
  31.                            PAGE_SIZE, PAGE_SHARED);
  32.     }

  33.     if(rc) {
  34. #if defined(RING_DEBUG)
  35.       printk("[PF_RING] remap_pfn_range() failed\n");
  36. #endif
  37.       return(-EAGAIN);
  38.     }

  39.     start += PAGE_SIZE;
  40.     ptr += PAGE_SIZE;
  41.     if(size > PAGE_SIZE) {
  42.       size -= PAGE_SIZE;
  43.     } else {
  44.       size = 0;
  45.     }
  46.   }

  47.   return(0);
  48. }
複製代碼


嗯,跳過了太多的細節,不過其mmap最核心的東東已經呈現出來。
如果要共享內核與用戶空間內存,這倒是個現成的可借鑑的例子。

5、數據包的入隊操作

做到這一步,準備工作基本上就完成了。因爲PF_RING在初始化中,註冊了prot_hook。其func指針指向packet_rcv函數:
當數據報文進入Linux網絡協議棧隊列時,netif_receive_skb會遍歷這些註冊的Hook:
  1. int netif_receive_skb(struct sk_buff *skb)
  2. {
  3.         list_for_each_entry_rcu(ptype, &ptype_all, list) {
  4.                 if (ptype->dev == null_or_orig || ptype->dev == skb->dev ||
  5.                     ptype->dev == orig_dev) {
  6.                         if (pt_prev)
  7.                                 ret = deliver_skb(skb, pt_prev, orig_dev);
  8.                         pt_prev = ptype;
  9.                 }
  10.         }
  11. }
複製代碼


相應的Hook函數得到調用:

  1. static inline int deliver_skb(struct sk_buff *skb,
  2.                               struct packet_type *pt_prev,
  3.                               struct net_device *orig_dev)
  4. {
  5.         atomic_inc(&skb->users);        //注意,這裏引用計數器被增加了
  6.         return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
  7. }
複製代碼


packet_rcv隨之執行環形隊列的入隊操作:
  1. static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
  2.                       struct packet_type *pt, struct net_device *orig_dev)
  3. {
  4.   int rc;

  5.   //忽略本地環回報文
  6.   if(skb->pkt_type != PACKET_LOOPBACK) {
  7.           //進一步轉向,最後一個參數直接使用-1,從上下文來看,寫爲RING_ANY_CHANNEL(其實也是-1)似乎可讀性更強,
  8.           //這裏表示,如果從packet_rcv進入隊列,由通道ID是“未指定的”,由skb_ring_handler來處理
  9.     rc = skb_ring_handler(skb,
  10.                           (skb->pkt_type == PACKET_OUTGOING) ? 0 : 1,
  11.                           1, -1 /* unknown channel */);

  12.   } else
  13.     rc = 0;

  14.   kfree_skb(skb);                                //所以,這裏要做相應的減少
  15.   return(rc);
  16. }
複製代碼



static int skb_ring_handler(struct sk_buff *skb,                                //要捕獲的數據包
                            u_char recv_packet,                                                                //數據流方向,>0表示是進入(接收)方向
                            u_char real_skb /* 1=real skb, 0=faked skb */ ,
                            short channel_id)                                                                //通道ID
{
  struct sock *skElement;
  int rc = 0, is_ip_pkt;
  struct list_head *ptr;
  struct pfring_pkthdr hdr;
  int displ;
  struct sk_buff *skk = NULL;
  struct sk_buff *orig_skb = skb;

#ifdef PROFILING
  uint64_t rdt = _rdtsc(), rdt1, rdt2;
#endif

  //skb合法檢查,包括數據流的方向
  if((!skb)                /* Invalid skb */
      ||((!enable_tx_capture) && (!recv_packet))) {
    /*
      An outgoing packet is about to be sent out
      but we decided not to handle transmitted
      packets.
    */
    return(0);
  }
#if defined(RING_DEBUG)
  if(1) {
    struct timeval tv;

    skb_get_timestamp(skb, &tv);
    printk
      ("[PF_RING] skb_ring_handler() [skb=%p][%u.%u][len=%d][dev=%s][csum=%u]\n",
       skb, (unsigned int)tv.tv_sec, (unsigned int)tv.tv_usec,
       skb->len,
       skb->dev->name == NULL ? "<NULL>" : skb->dev->name,
       skb->csum);
  }
#endif

        //如果通道ID未指定,根據進入的報文設備索引,設定之
#if(LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,21))
  if(channel_id == RING_ANY_CHANNEL /* Unknown channel */ )
    channel_id = skb->iif;        /* Might have been set by the driver */
#endif

#if defined (RING_DEBUG)
  /* printk("[PF_RING] channel_id=%d\n", channel_id); */
#endif

#ifdef PROFILING
  rdt1 = _rdtsc();
#endif

  if(recv_packet) {
    /* Hack for identifying a packet received by the e1000 */
    if(real_skb)
      displ = SKB_DISPLACEMENT;
    else
      displ = 0;        /* Received by the e1000 wrapper */
  } else
    displ = 0;
        
  //解析數據報文,並判斷是否爲IP報文
  is_ip_pkt = parse_pkt(skb, displ, &hdr);

  //分片處理,是一個可選的功能項,事實上,對大多數包捕獲工具而言,它們好像都不使用底層庫來完成這一功能
  /* (de)Fragmentation <[email protected]> */
  if(enable_ip_defrag
      && real_skb && is_ip_pkt && recv_packet && (ring_table_size > 0)) {
    } else {
#if defined (RING_DEBUG)
        printk("[PF_RING] Do not seems to be a fragmented ip_pkt[iphdr=%p]\n",
               iphdr);
#endif
      }
    }
  }

  //按慣例,在報文的捕獲首部信息中記錄捕獲的時間戳
  /* BD - API changed for time keeping */
#if(LINUX_VERSION_CODE < KERNEL_VERSION(2,6,14))
  if(skb->stamp.tv_sec == 0)
    do_gettimeofday(&skb->stamp);
  hdr.ts.tv_sec = skb->stamp.tv_sec, hdr.ts.tv_usec = skb->stamp.tv_usec;
#elif(LINUX_VERSION_CODE < KERNEL_VERSION(2,6,22))
  if(skb->tstamp.off_sec == 0)
    __net_timestamp(skb);
  hdr.ts.tv_sec = skb->tstamp.off_sec, hdr.ts.tv_usec =
    skb->tstamp.off_usec;
#else /* 2.6.22 and above */
  if(skb->tstamp.tv64 == 0)
    __net_timestamp(skb);
  hdr.ts = ktime_to_timeval(skb->tstamp);
#endif

  //除了時間,還有長度,熟悉libpcap的話,這些操作應該很眼熟
  hdr.len = hdr.caplen = skb->len + displ;

  /* Avoid the ring to be manipulated while playing with it */
  read_lock_bh(&ring_mgmt_lock);

  /* 前面在創建sk時,已經看過ring_insert的入隊操作了,現在要檢查它的成員
  * 它們的關係是,通過ring_table的成員,獲取到element,它裏面封裝了sk,
  *通過ring_sk宏,就可以得到ring_opt指針
   */
  list_for_each(ptr, &ring_table) {
    struct ring_opt *pfr;
    struct ring_element *entry;

    entry = list_entry(ptr, struct ring_element, list);

    skElement = entry->sk;
    pfr = ring_sk(skElement);
        
        //看來要加入社團,條件還是滿多的,pfr不能爲空,未指定集羣cluster_id,槽位不能爲空,方向要正確,綁定的網絡設備
        //得對上號
        //另一種可能就是對bonding的支持,如果設備是從屬設備,則應校驗其主設備
    if((pfr != NULL)
        && (pfr->cluster_id == 0 /* No cluster */ )
        && (pfr->ring_slots != NULL)
        && is_valid_skb_direction(pfr->direction, recv_packet)
        && ((pfr->ring_netdev == skb->dev)
            || ((skb->dev->flags & IFF_SLAVE)
                && (pfr->ring_netdev == skb->dev->master)))) {
      /* We've found the ring where the packet can be stored */
      /* 從新計算捕獲幀長度,是因爲可能因爲巨型幀的出現——超過了桶能容納的長度 */
      int old_caplen = hdr.caplen;        /* Keep old lenght */
      hdr.caplen = min(hdr.caplen, pfr->bucket_len);
      /* 入隊操作 */
      add_skb_to_ring(skb, pfr, &hdr, is_ip_pkt, displ, channel_id);
      hdr.caplen = old_caplen;
      rc = 1;        /* Ring found: we've done our job */
    }
  }

  /* [2] Check socket clusters */
  list_for_each(ptr, &ring_cluster_list) {
    ring_cluster_element *cluster_ptr;
    struct ring_opt *pfr;

    cluster_ptr = list_entry(ptr, ring_cluster_element, list);

    if(cluster_ptr->cluster.num_cluster_elements > 0) {
      u_int skb_hash = hash_pkt_cluster(cluster_ptr, &hdr);

      skElement = cluster_ptr->cluster.sk[skb_hash];

      if(skElement != NULL) {
        pfr = ring_sk(skElement);

        if((pfr != NULL)
            && (pfr->ring_slots != NULL)
            && ((pfr->ring_netdev == skb->dev)
                || ((skb->dev->flags & IFF_SLAVE)
                    && (pfr->ring_netdev ==
                        skb->dev->master)))
            && is_valid_skb_direction(pfr->direction, recv_packet)
            ) {
          /* We've found the ring where the packet can be stored */
          add_skb_to_ring(skb, pfr, &hdr,
                          is_ip_pkt, displ,
                          channel_id);
          rc = 1;        /* Ring found: we've done our job */
        }
      }
    }
  }

  read_unlock_bh(&ring_mgmt_lock);

#ifdef PROFILING
  rdt1 = _rdtsc() - rdt1;
#endif

#ifdef PROFILING
  rdt2 = _rdtsc();
#endif

  /* Fragment handling */
  if(skk != NULL)
    kfree_skb(skk);

  if(rc == 1) {
    if(transparent_mode != driver2pf_ring_non_transparent) {
      rc = 0;
    } else {
      if(recv_packet && real_skb) {
#if defined(RING_DEBUG)
        printk("[PF_RING] kfree_skb()\n");
#endif

        kfree_skb(orig_skb);
      }
    }
  }
#ifdef PROFILING
  rdt2 = _rdtsc() - rdt2;
  rdt = _rdtsc() - rdt;

#if defined(RING_DEBUG)
  printk
    ("[PF_RING] # cycles: %d [lock costed %d %d%%][free costed %d %d%%]\n",
     (int)rdt, rdt - rdt1,
     (int)((float)((rdt - rdt1) * 100) / (float)rdt), rdt2,
     (int)((float)(rdt2 * 100) / (float)rdt));
#endif
#endif

  //printk("[PF_RING] Returned %d\n", rc);
  return(rc);                /*  0 = packet not handled */
}

上面跳過了對cluster(集羣)的分析,PF_RING允許同時對多個接口捕獲報文,而並不是一個。這就是集羣。看一下它用戶態的註釋就一目瞭然了:
  1.                         /* Syntax
  2.                         ethX@1,5       channel 1 and 5
  3.                         ethX@1-5       channel 1,2...5
  4.                         ethX@1-3,5-7   channel 1,2,3,5,6,7
  5.                         */
複製代碼

進一步的入隊操作,是通過add_skb_to_ring來完成的:

  1. static int add_skb_to_ring(struct sk_buff *skb,
  2.                            struct ring_opt *pfr,
  3.                            struct pfring_pkthdr *hdr,
  4.                            int is_ip_pkt, int displ, short channel_id)
  5. {
  6.       //add_skb_to_ring函數比較複雜,因爲它要處理過濾器方面的問題。
  7.       //關於PF_RING的過濾器,可以參考[url]http://luca.ntop.org/Blooms.pdf[/url]
  8.       //獲取更多內容。這裏不做詳細討論了。或者留到下回分解吧。
  9.       
  10.       //最終入隊操作,是通過調用dd_pkt_to_ring來實現的。
  11.       add_pkt_to_ring(skb, pfr, hdr, displ, channel_id,
  12.                       offset, mem);        
  13. }
複製代碼

  1. static void add_pkt_to_ring(struct sk_buff *skb,
  2.                             struct ring_opt *pfr,
  3.                             struct pfring_pkthdr *hdr,
  4.                             int displ, short channel_id,
  5.                             int offset, void *plugin_mem)
  6. {
  7.   char *ring_bucket;
  8.   int idx;
  9.   FlowSlot *theSlot;
  10.   int32_t the_bit = 1 << channel_id;

  11. #if defined(RING_DEBUG)
  12.   printk("[PF_RING] --> add_pkt_to_ring(len=%d) [pfr->channel_id=%d][channel_id=%d]\n",
  13.          hdr->len, pfr->channel_id, channel_id);
  14. #endif

  15.   //檢查激活標誌
  16.   if(!pfr->ring_active)
  17.     return;

  18.   if((pfr->channel_id != RING_ANY_CHANNEL)
  19.       && (channel_id != RING_ANY_CHANNEL)
  20.       && ((pfr->channel_id & the_bit) != the_bit))
  21.     return; /* Wrong channel */

  22.   //寫鎖
  23.   write_lock_bh(&pfr->ring_index_lock);
  24.   //獲取前一次插入的位置索引
  25.   idx = pfr->slots_info->insert_idx;
  26.   //調用get_insert_slot獲取當前要捕獲數據報文的合適的槽位
  27.   //這裏idx++後,指向了下一次插入的位置索引
  28.   idx++, theSlot = get_insert_slot(pfr);
  29.   //累計計數器
  30.   pfr->slots_info->tot_pkts++;

  31.   //沒位子了,累計丟包計數器,返回之
  32.   if((theSlot == NULL) || (theSlot->slot_state != 0)) {
  33.     /* No room left */
  34.     pfr->slots_info->tot_lost++;
  35.     write_unlock_bh(&pfr->ring_index_lock);
  36.     return;
  37.   }

  38.   //獲取當前槽位的桶
  39.   ring_bucket = &theSlot->bucket;

  40.   //支持插件??在最開始處記錄插件信息??
  41.   if((plugin_mem != NULL) && (offset > 0))
  42.     memcpy(&ring_bucket[sizeof(struct pfring_pkthdr)], plugin_mem, offset);  

  43.   if(skb != NULL) {
  44.           //重新計算捕獲幀長度
  45.     hdr->caplen = min(pfr->bucket_len - offset, hdr->caplen);

  46.     if(hdr->caplen > 0) {
  47. #if defined(RING_DEBUG)
  48.       printk("[PF_RING] --> [caplen=%d][len=%d][displ=%d][parsed_header_len=%d][bucket_len=%d][sizeof=%d]\n",
  49.          hdr->caplen, hdr->len, displ,
  50.              hdr->parsed_header_len, pfr->bucket_len,
  51.              sizeof(struct pfring_pkthdr));
  52. #endif
  53.       //拷貝捕獲的數據報文,前面空了兩個欄位:一個是pkthdr首部,一個是插件offset長度
  54.       //這裏經過了一次數據拷貝,對於完美主義者,這並不是一個好的方法。但是PF_RING定位於一個
  55.       //通用的接口庫,似乎只有這麼做了。否則,追求“零拷貝”,爲了避免這一次拷貝,只有逐個修改網卡驅動了。
  56.       skb_copy_bits(skb, -displ,
  57.                     &ring_bucket[sizeof(struct pfring_pkthdr) + offset], hdr->caplen);
  58.     } else {
  59.       if(hdr->parsed_header_len >= pfr->bucket_len) {
  60.         static u_char print_once = 0;

  61.         if(!print_once) {
  62.           printk("[PF_RING] WARNING: the bucket len is [%d] shorter than the plugin parsed header [%d]\n",
  63.              pfr->bucket_len, hdr->parsed_header_len);
  64.           print_once = 1;
  65.         }
  66.       }
  67.     }
  68.   }

  69.   //記錄首部
  70.   memcpy(ring_bucket, hdr, sizeof(struct pfring_pkthdr)); /* Copy extended packet header */

  71.   //前面idx已經自加過了,判斷是否隊列已滿,若滿,歸零,否則更新插入索引
  72.   if(idx == pfr->slots_info->tot_slots)
  73.     pfr->slots_info->insert_idx = 0;
  74.   else
  75.     pfr->slots_info->insert_idx = idx;

  76. #if defined(RING_DEBUG)
  77.   printk("[PF_RING] ==> insert_idx=%d\n", pfr->slots_info->insert_idx);
  78. #endif

  79.   //累計插入計數器
  80.   pfr->slots_info->tot_insert++;
  81.   //槽位就緒標記,用戶空間可以來取了
  82.   theSlot->slot_state = 1;
  83.   write_unlock_bh(&pfr->ring_index_lock);

  84.   //有的時候會出現,用戶空間取不到的情況,如隊列爲空。這樣,用戶空間調用poll等待數據。這裏做相應的喚醒處理
  85.   /* wakeup in case of poll() */
  86.   if(waitqueue_active(&pfr->ring_slots_waitqueue))
  87.     wake_up_interruptible(&pfr->ring_slots_waitqueue);
  88. }
複製代碼


槽位的計算:

  1. 在ring_bind函數中,分配空間後,使用ring_slots做爲槽位指針。事實上,這裏要計算槽位,就是通過索引號 * 槽位長度來得到:
  2. static inline FlowSlot *get_insert_slot(struct ring_opt *pfr)
  3. {
  4.   if(pfr->ring_slots != NULL) {
  5.     FlowSlot *slot =
  6.       (FlowSlot *) & (pfr->
  7.                       ring_slots[pfr->slots_info->insert_idx *
  8.                                  pfr->slots_info->slot_len]);
  9. #if defined(RING_DEBUG)
  10.     printk
  11.       ("[PF_RING] get_insert_slot(%d): returned slot [slot_state=%d]\n",
  12.        pfr->slots_info->insert_idx, slot->slot_state);
  13. #endif
  14.     return(slot);
  15.   } else {
  16. #if defined(RING_DEBUG)
  17.     printk("[PF_RING] get_insert_slot(%d): NULL slot\n",
  18.            pfr->slots_info->insert_idx);
  19. #endif
  20.     return(NULL);
  21.   }
  22. }
複製代碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章