NETDEV 協議 七

這部分內容在於說明socket創建後如何被內核協議棧訪問到,只關注兩個問題:sock何時插入內核表的,sock如何被內核訪問的。對於核心的sock的插入、查找函數都給出了流程圖。

sock如何插入內核表
      socket創建後就可以用來與外部網絡通信,用戶可以通過文件描述符fd來找到要操作的socket,內核則通過查表來找到要操作的socket。這意味着socket創建時會在文件系統中生成相應項,同時還會插入到存儲socket的表中,方便用戶和內核通過兩種方式進行訪問。
      以創建如下udp socket爲例,這裏的創建僅僅指定socket的協議簇是AF_INET,類型是SOCK_DGRAM,協議是0,此時創建了socket,相應文件描述符,但仍缺少其它信息,此時socket並未插入到內核表中,還是處於遊離態,除了用戶通過fd操作,內核是看不到的socket的。

fd = socket(AF_INET, SOCK_DGRAM, 0);

      根據作爲的角色(服務器或客戶端)不同,接下來執行的動作也不相同。這兩句分條時服務器和客戶端與外部通信的第一句,執行後,與外部連接建立,socket的插入內核表也是由這兩句觸發的。
      服務器端udp socket

 
bind(fd, &serveraddr, sizeof(serveraddr));

      客戶端udp socket

 
sendto(fd, buff, len, 0, &serveraddr, sizeof(serveraddr));

      下面來看下創建socket的具體動作,只涉及與socket存儲相關的代碼,這些系統調用的其它方面以後再具體分析。
      sys_socket() 創建socket,映射文件描述符fd

retval = sock_create(family, type, protocol, &sock);
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));

      在內核中,有struct socket,也就是通常所說的socket,表示網絡的接口,還有struct sock,則是AF_INET域的接口。一般struct socket成員叫sock,struct sock成員叫sk,在代碼中不要混淆。
      sock_create() -- > __sock_create() 
      最終執行__sock_create()來創建,注意__sock_create()最後一個參數是0,表示是由用戶創建的;如果是1,則表示是由內核創建的。
      分配socket並設置sock->type爲SOCK_DGRAM。

sock = sock_alloc();
sock->type = type;

      從net_families中取得AF_INET(也即PF_INET)協議族的參數,net_families數組存儲不同協議族的參數,像AF_INET協議族是在加載IP模塊時註冊的,inet_init() -> sock_register(&inet_family_ops),sock_register()就是將參數加入到net_families數組中,inet_family_ops定義如下:

pf = rcu_dereference(net_families[family]);
static const struct net_proto_family inet_family_ops = {
 .family = PF_INET,
 .create = inet_create,
 .owner = THIS_MODULE,
};

      最後調用相應協議簇的創建方法,這裏的pf->create()就是inet_create(),它創建INET域的結構sock。

err = pf->create(net, sock, protocol, kern);

      從__sock_create()代碼看到創建包含兩步:sock_alloc()和pf->create()。sock_alloc()分配了sock內存空間並初始化inode;pf->create()初始化了sk。

sock_alloc()
       分配空間,通過new_inode()分配了節點(包括socket),然後通過SOCKET_I宏獲得sock,實際上inode和sock是在new_inode()中一起分配的,結構體叫作sock_alloc。

inode = new_inode(sock_mnt->mnt_sb);
sock = SOCKET_I(inode);

      設置inode的參數,並返回sock。

inode->i_mode = S_IFSOCK | S_IRWXUGO;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
return sock;

      繼續往下看具體的創建過程:new_inode(),在分配後,會設置i_ino和i_state的值。

struct inode *new_inode(struct super_block *sb)
{
 ……
 inode = alloc_inode(sb);
 if (inode) {
  spin_lock(&inode_lock);
  __inode_add_to_lists(sb, NULL, inode);
  inode->i_ino = ++last_ino;
  inode->i_state = 0;
  spin_unlock(&inode_lock);
 }
 return inode;
}

      其中的alloc_inode() -> sb->s_op->alloc_inode(),sb是sock_mnt->mnt_sb,所以alloc_inode()指向的是sockfs的操作函數sock_alloc_inode。

static const struct super_operations sockfs_ops = {
 .alloc_inode = sock_alloc_inode,
 .destroy_inode =sock_destroy_inode,
 .statfs = simple_statfs,
};

      sock_alloc_inode()中通過kmem_cache_alloc()分配了struct socket_alloc結構體大小的空間,而struct socket_alloc結構體定義如下,但只返回了inode,實際上socket和inode都已經分配了空間,在之後就可以通過container_of取到socket。

static struct inode *sock_alloc_inode(struct super_block *sb)
{
 struct socket_alloc *ei;
 ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
 …..
 return &ei->vfs_inode;
}
struct socket_alloc {
 struct socket socket;
 struct inode vfs_inode;
};


inet_create()
      從inetsw中根據類型、協議查找相應的socket interface。

list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
 ……
 if (IPPROTO_IP == answer->protocol)
  break;
 ……
}

      inetsw是在inet_init()時被註冊的,有三種:tcp, udp, raw,由於我們創建的是udp socket,所以查到的是第二項,udp_prot。

static struct inet_protosw inetsw_array[] =
{
 {
  .type =       SOCK_STREAM,
  .protocol =   IPPROTO_TCP,
  .prot =       &tcp_prot,
  .ops =        &inet_stream_ops,
  .no_check =   0,
  .flags =      INET_PROTOSW_PERMANENT |
         INET_PROTOSW_ICSK,
 },

 {
  .type =       SOCK_DGRAM,
  .protocol =   IPPROTO_UDP,
  .prot =       &udp_prot,
  .ops =        &inet_dgram_ops,
  .no_check =   UDP_CSUM_DEFAULT,
  .flags =      INET_PROTOSW_PERMANENT,
       },


       {
        .type =       SOCK_RAW,
        .protocol =   IPPROTO_IP, /* wild card */
        .prot =       &raw_prot,
        .ops =        &inet_sockraw_ops,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_REUSE,
       }
};

      sock->ops指向inet_dgram_ops,然後創建sk,sk->proto指向udp_prot,注意這裏分配的大小是struct udp_sock,而不僅僅是struct sock大小。

 
sock->ops = answer->ops;
……
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);

      然後設置inet的一些參數,這裏直接將sk類型轉換爲inet,因爲在sk_alloc()中分配的是struct udp_sock結構大小,返回的是struct sock,利用了第一個成員的特性,三者之間的關係如下圖:

inet = inet_sk(sk);
…..
inet->inet_id = 0;

      此時sock和sk都已經分配了空間,再設置sock與sk關係,即sock->sk=sk,並做一些初始化操作,如sk的隊列初始化。初後調用sk_prot->init(),inet_dgram_ops->init()爲NULL,這裏沒做任何事情。

sock_init_data(sock, sk);
if (sk->sk_prot->init) {
 err = sk->sk_prot->init(sk);
 if (err)
  sk_common_release(sk);
}

      當創建的是一個SOCK_RAW類型的socket時,還會額外執行下列語句。當協議值賦給inet->inet_num與inet->inet_sport,然後sk->sk_prot->hash(sk)將sk插入到內核的sock表中,使用的索引值是協議號。這個可以這樣理解,如果創建的是UDP或TCP的socket,它們是標準的套接字,用[sip, sport, tip, tport]這樣的四元組來查找,socket()時還缺少這些信息,還不能插入到內核的sock表中。但如果創建的是RAW的socket,它只屬於某一特定協議,查找它使用的應是協議號而不是套接字的四元組,因此,socket()時就通過hash()插入到內核sock表中。

if (SOCK_RAW == sock->type) {
 inet->inet_num = protocol;
 if (IPPROTO_RAW == protocol)
  inet->hdrincl = 1;
}
if (inet->inet_num) {
 inet->inet_sport = htons(inet->inet_num);
 sk->sk_prot->hash(sk);
}

      那麼sock是在什麼時候插入到內核表中的,答案是sk->sk_prot->get_port()函數,對於UDP來講,它指向udp_v4_get_port()函數,根據服務器和客戶端的行爲不同,bind()和sendto()都會調用到get_port(),也就是說,在bind()或sendto()調用時,sock才被插入到內核表中。
bind() 綁定地址
      sys_bind() -> sock->ops->bind() -> inet_bind() -> sk->sk_prot->get_port()
      sk->sk_prot是udp_prot,這裏實際調用udp_v4_get_port()函數。

sendto() 發送到指定地址
      sys_sendto() -> sock_sendmsg() -> __sock_sendmsg()() -> sock->ops->sendmsg()
      由於創建的是udp socket,因此sock->ops指向inet_dgram_ops,sendmsg()實際調用inet_sendmsg()函數。該函數中的有如下語句:

if (!inet_sk(sk)->inet_num && inet_autobind(sk))
 return -EAGAIN;

      客戶端在執行sendto()前僅僅執行了socket()操作,此時inet_num=0,因此執行了inet_autobind(),該函數會調用sk->sk_prot->get_port()。從而回到了udp_v4_get_port()函數,它會將sk插入到內核表udp_table中。

下面重點看下插入sk的函數udp_v4_get_port():
udp_v4_get_port() 插入sk到內核表udptable中
      哈希值hash2_nulladdr由[INADDR_ANY, snum]得到,hash2_partial由[inet_rcv_saddr, 0]得到,即前者用本地端口作哈希,後者用本地地址作哈希。udp_portaddr_hash存儲後者的值hash2_partial,便於計算最後的哈希值。

unsigned int hash2_nulladdr = udp4_portaddr_hash(sock_net(sk), INADDR_ANY, snum);
unsigned int hash2_partial = udp4_portaddr_hash(sock_net(sk), inet_sk(sk)->inet_rcv_saddr, 0);
udp_sk(sk)->udp_portaddr_hash = hash2_partial;

      最後調用udp_lib_get_port(),ipv4_rcv_saddr_equal()是比較地址是否相等的函數,snum是本地端口,hash2_nulladdr是由它得到的哈殺值,sk是要插入的表項。

return udp_lib_get_port(sk, snum, ipv4_rcv_saddr_equal, hash2_nulladdr);


udp_lib_get_port()
      取得內核存放sock的表,對於udp socket來說,就是udp_table,它在udp_prot中被定義。在udp_table的創建過程中已經看到,udp_table有兩個hash表:hash和hash2,兩者大小相同,只是前者用snum作哈希值,後者用saddr, snum作哈希值。使用兩個hash表的目的在於加速查找,先用snum在hash中查找,再用saddr, snum在hash2中查找,最後根據效率決定在hash或hash2中查找。

struct udp_table *udptable = sk->sk_prot->h.udp_table;

      根據snum的不同會執行不同的操作,snum爲0則先選擇一個可用端口號,再插入;snum不爲0則先確定之前沒有存儲相應sk,再插入。

if (!snum) {
 snum==0代碼段
} else {
 snum!=0代碼段
}

      如果snum!=0,此時執行else部分代碼。hslot是從udp_table中hash表取出的表項,鍵值是snum。

 
hslot = udp_hashslot(udptable, net, snum);

      如果hslot->count大於10,即在hash表中以snum爲鍵值的項的數目在於10,此時改用在hash2表中查找。如果hslot->count不足10,那麼直接在hash表中查找就可以了。這樣劃分是出於效率的考慮。
      先看數目大於10的情況,hslot2是udptable中hash2表取出的表項,鍵值是[inet_rcv_addr, snum],如果hslot2項的數目比hslot還多,那麼查找hash2表是不划算的,返回直接查找hash表。如果hslot2更少(這也是設計hash2的目的),使用udp_lib_lport_inuse2()查找是否有匹配項;如果沒有找到,則使用新的鍵值hash2_nulladdr,即[INADDR_ANY, snum]從hash2中取出表項,再使用udp_lib_lport_inuse2()查找是否有匹配項。如果有,表明要插入的sk已經存在於內核表中,直接返回;如果沒有,則執行sk的插入操作。scan_primary_hash代碼段是在hash表的hslot項中查找,只有當在hash2中查找更費時時纔會執行。

if (hslot->count > 10) {
 int exist;
 unsigned int slot2 = udp_sk(sk)->udp_portaddr_hash ^ snum;

 slot2          &= udptable->mask;
 hash2_nulladdr &= udptable->mask;

 hslot2 = udp_hashslot2(udptable, slot2);
 if (hslot->count < hslot2->count)
  goto scan_primary_hash;

 exist = udp_lib_lport_inuse2(net, snum, hslot2, sk, saddr_comp);
 if (!exist && (hash2_nulladdr != slot2)) {
  hslot2 = udp_hashslot2(udptable, hash2_nulladdr);
  exist = udp_lib_lport_inuse2(net, snum, hslot2,
     sk, saddr_comp);
 }
 if (exist)
  goto fail_unlock;
 else
  goto found;
}
scan_primary_hash:
 if (udp_lib_lport_inuse(net, snum, hslot, NULL, sk,
  saddr_comp, 0))
  goto fail_unlock;
}

流程圖:

      如果snum==0,即沒有綁定本地端口,此時執行if部分代碼段,這種情況一般發生在客戶端使用socket,此時內核會爲它選擇一個未使用的端口,下面來看下內核選擇臨時端口的策略。
      在說明下列參數含義前要先弄清楚udptable中hash公式:(num + net_hash_mix(net)) & mask,net_hash_mix(net)返回一般爲0,hash公式可簡寫爲num&mask。即本地端口對udptable大小取模。因此表項是循環、均勻地分佈在hash表中的。假設udptable大小爲8,現插入16個表項,結果會如下圖: 

      聲明bitmap數組,大小爲udp_table每個鍵值最多存儲的表項,即最大端口號/哈希表大小。端口號的值規定範圍是1-65536,而哈希表一般大小是256,因此實際分配bitmap[8]。low和high代表可用本地端口的下限和上限;remaining代表位於low和high間的端口號數目。用隨機值rand生成first,注意它是unsigned short類型,16位,表示起始查找位置;last表示終止查找位置,first和last相差表大小保證了所有鍵值都會被查詢一次。隨機值rand最後處理成哈希表大小的奇數倍,之所以要是奇數倍,是爲了保證哈希到同一個鍵值的所有端口號都能被遍歷,可以試着1開始,每次+2和每次+3,直到回到1,所遍歷的數有哪些不同,就會明白rand處理的意義。

DECLARE_BITMAP(bitmap, PORTS_PER_CHAIN);
inet_get_local_port_range(&low, &high);
remaining = (high - low) + 1;
rand = net_random();
first = (((u64)rand * remaining) >> 32) + low;
rand = (rand | 1) * (udptable->mask + 1);
last = first + udptable->mask + 1;

      使用first值作爲端口號,從udptable的hash表中找到hslot項,重置bitmap數組全0,調用函數udp_lib_lport_inuse()遍歷hslot項的所有表項,將所有已經使用的sport對應於bitmap的位置置1。

do {
 hslot = udp_hashslot(udptable, net, first);
 bitmap_zero(bitmap, PORTS_PER_CHAIN);
 spin_lock_bh(&hslot->lock);
 udp_lib_lport_inuse(net, snum, hslot, bitmap, sk,
  addr_comp, udptable->log);

      此時bitmap中包含了所有哈希到hslot的端口的使用情況,下面要做的就是從first位置開始,每次遞增rand(保證哈希值不變),查找符合條件的端口:端口在low~high的可用範圍內;端口還沒有被佔用。do{}while循環的判斷條件snum!=first和snum+=rand一起保證了所有哈希到hslot的端口號都會被遍歷到。如果找到了可用端口號,即跳出,執行插入sk的操作,否則++first,查找下一個鍵值,直到fisrt==last,表明所有鍵值都已輪循一遍,仍沒有結果,則退出,sk插入失敗。

 snum = first;
 do {
  if (low <= snum && snum <= high &&
   !test_bit(snum >> udptable->log, bitmap))
   goto found;
  snum += rand;
 } while (snum != first);
 spin_unlock_bh(&hslot->lock);
} while (++first != last);
goto fail;

流程圖: 

      當沒有在當前內核udp_table中找到匹配項時,執行插入新sk的操作。首先給sk參數賦值:inet_num, udp_port_hash, udp_portaddr_hash。然後將sk加入到hash表和hash2表中,並增加相應計數。

found:
 inet_sk(sk)->inet_num = snum;
 udp_sk(sk)->udp_port_hash = snum;
 udp_sk(sk)->udp_portaddr_hash ^= snum;
 if (sk_unhashed(sk)) {
  sk_nulls_add_node_rcu(sk, &hslot->head);
  hslot->count++;
  sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);

  hslot2 = udp_hashslot2(udptable, udp_sk(sk)->udp_portaddr_hash);
  spin_lock(&hslot2->lock);
  hlist_nulls_add_head_rcu(&udp_sk(sk)->udp_portaddr_node,
      &hslot2->head);
  hslot2->count++;
  spin_unlock(&hslot2->lock);
 }


sock如何被內核訪問
      創建的udp socket成功後,當使用該socket與外部通信時,協議棧會收到發往該socket的udp報文。
      udp_rcv() -> __udp4_lib_rcv() -> __udp4_lib_lookup()
      在該函數中有關於udp socket的查找代碼段,它以[saddr, sport, daddr, dport, iif]爲鍵值在udptable中查找相應的sk。

return __udp4_lib_lookup(dev_net(skb_dst(skb)->dev), iph->saddr, sport,
      iph->daddr, dport, inet_iif(skb), udptable); 


__udp4_lib_lookup() sock在udptable中查找
      查找的過程與插入sock的過程很相似,先以hnum作哈希得到hslot,daddr, hnum作哈希得到hslot2,如果hslot數目不足10或hslot的表項數少於hslot2的,則在hslot中查找(begin代碼段)。否則,在hslot2中查找。查找時使用udp4_lib_lookup2()函數,它返回與收到報文相匹配的sock。

if (hslot->count > 10) {
 hash2 = udp4_portaddr_hash(net, daddr, hnum);
 slot2 = hash2 & udptable->mask;
 hslot2 = &udptable->hash2[slot2];
 if (hslot->count < hslot2->count)
  goto begin;

 result = udp4_lib_lookup2(net, saddr, sport,
    daddr, hnum, dif, hslot2, slot2);

      如果在hslot2中沒有查找結果,則用INADDR_ANY, hnum作哈希得到重新得到hslot2,因爲服務器端的udp socket只綁定了本地端口,沒有綁定本地地址,所以查找時需要先使用[saddr, sport]查找,沒有時再使用[INADDR_ANY, sport]查找。如果hslot2->count比hslot->count要多,或者在hslot2中沒有查找到,則在hslot中查找(begin代碼段)。

if (!result) {
  hash2 = udp4_portaddr_hash(net, INADDR_ANY, hnum);
  slot2 = hash2 & udptable->mask;
  hslot2 = &udptable->hash2[slot2];
  if (hslot->count < hslot2->count)
   goto begin;

  result = udp4_lib_lookup2(net, saddr, sport,
     INADDR_ANY, hnum, dif, hslot2, slot2);
 }

      只有當不必或不能在hslot2中查找時,纔會執行下面的查找,它在hslot中查找,遍歷每一項,使用comute_score()計算匹配值。最後返回查找的結果。

begin:
 result = NULL;
 badness = -1;
 sk_nulls_for_each_rcu(sk, node, &hslot->head) {
  score = compute_score(sk, net, saddr, hnum, sport,
          daddr, dport, dif);
  if (score > badness) {
   result = sk;
   badness = score;
  }
 }

流程圖: 

      #對比udp socket的插入和查找的流程圖,可以發現兩者是有差別的,在使用INADDR_ANY作爲本地地址重新計算hslot2後,前者並沒有比較hslot2->count與hslot->count。雖然不礙查找結果,但個人認爲,插入的流程是少了hslot2->count與hslot->count比較。

udp4_lib_lookup2()
      遍歷hslot2的鏈表項,compute_score2計算與[saddr, sport, daddr, dport, dif]相匹配的表項,返回score作爲匹配值,匹配值發越大表明匹配度越高。score==SCORE2_MAX表示與傳入參數完全匹配,找到匹配項,goto exact_match;score==-1表示與傳入參數完全不匹配;score==中間值表示部分匹配,如果沒有更高的匹配項存在,則使用該項。

udp_portaddr_for_each_entry_rcu(sk, node, &hslot2->head) {
 score = compute_score2(sk, net, saddr, sport, daddr, hnum, dif);
 if (score > badness) {
  result = sk;
  badness = score;
  if (score == SCORE2_MAX)
   goto exact_match;
 }
}

      其中compute_score2()用來計算匹配度,並用返回值作爲匹配度,以通常的udp socket爲例,只用到了本地地址、本地端口(如果是作爲服務器,則本地地址也省略了)。因此compute_score2()要求本地地址和本地端口完全匹配,共餘參數只要求當插入的socket有值時才進行匹配。

UDP報文接收
       UDP報文的接收可以分爲兩個部分:協議棧收到udp報文,插入相應隊列中;用戶調用recvfrom()或recv()系統調用從隊列中取出報文,這裏的隊列就是sk->sk_receive_queue,它是報文中轉的紐帶,兩部分的聯繫如下圖所示。

第一部分:協議棧如何收取udp報文的。
      udp模塊的註冊在inet_init()中,當收到的是udp報文,會調用udp_protocol中的handler函數udp_rcv()。

if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
 printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");

      udp_rcv() -> __udp4_lib_rcv() 完成udp報文接收,初始化udp的校驗和,並不驗證校驗和的正確性。

if (udp4_csum_init(skb, uh, proto))
 goto csum_error;

      在udptable中以套接字的[saddr, sport, daddr, dport]查找相應的sk,在上一篇中已詳細講過”sk的查找”,這裏報文的source源端口相當於源主機的端口,dest目的端口相當於本地端口。

 
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

      如果udptable中存在相應的sk,即有socket在接收,則通過udp_queue_rcv_skb()將報文skb入隊列,該函數稍後分析,總之,報文會被放到sk->sk_receive_queue隊列上,然後sock_put()減少sk的引用計算,並返回。之後的接收工作的完成將有賴於用戶的操作。

if (sk != NULL) {
 int ret = udp_queue_rcv_skb(sk, skb);
 sock_put(sk);
if (ret > 0)
  return -ret;
 return 0;
}

      當沒有在udptable中找到sk時,則本機沒有socket會接收它,因此要發送icmp不可達報文,在此之前,還要驗證校驗和udp_lib_checksum_complete(),如果校驗和錯誤,則直接丟棄報文;如果校驗和正確,則會增加mib中的統計,併發送icmp端口不可達報文,然後丟棄該報文。

 
if (udp_lib_checksum_complete(skb))
 goto csum_error;
UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
kfree_skb(skb);


udp_queue_rcv_skb() 報文入隊列
      sock_woned_by_user()判斷sk->sk_lock.owned的值,如果等於1,表示sk處於佔用狀態,此時不能向sk接收隊列中添加skb,執行else if部分,sk_add_backlog()將skb添加到sk->sk_backlog隊列上;如果等於0,表示sk沒被佔用,執行if部分,__udp_queue_rcv_skb()將skb添加到sk->sk_receive_queue隊列上。

bh_lock_sock(sk);
if (!sock_owned_by_user(sk))
 rc = __udp_queue_rcv_skb(sk, skb);
else if (sk_add_backlog(sk, skb)) {
 bh_unlock_sock(sk);
 goto drop;
}
bh_unlock_sock(sk);

      那麼何時sk會被佔用?何時sk->sk_backlog上的skb被處理的?
      創建socket時,sys_socket() -> inet_create() -> sk_alloc() -> sock_lock_init() -> sock_lock_init_class_and_name()初始化sk->sk_lock_owned=0。
      比如當銷燬socket時,udp_destroy_sock()會調用lock_sock()對sk加鎖,操作完後,調用release_sock()對sk解鎖。

void udp_destroy_sock(struct sock *sk)
{
 lock_sock(sk);
 udp_flush_pending_frames(sk);
 release_sock(sk);
}

      實際上,lock_sock()設置sk->sk_lock.owned=1;而release_sock()設置sk->sk_lock.owned=0,並處理sk_backlog隊列上的報文,release_sock() -> __release_sock(),對於sk_backlog隊列上的每個報文,調用sk_backlog_rcv() -> sk->sk_backlog_rcv()。同樣是在socket的創建中,sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv()即__udp_queue_rcv_skb(),這個函數的作用上面已經講過,將skb添加到sk_receive_queue,這樣,所有的sk_backlog上的報文轉移到了sk_receive_queue上。簡單來說,sk_backlog隊列的作用就是,鎖定時報文臨時存放在此,解鎖時,報文移到sk_receive_queue隊列。 

第二部分:用戶如何收取報文
      用戶可以調用sys_recvfrom()或sys_recv()來接收報文,所不同的是,sys_recvfrom()可能通過參數獲得報文的來源地址,而sys_recv()則不可以,但對接收報文並沒有影響。在用戶調用recvfrom()或recv()接收報文前,發給該socket的報文都會被添加到sk->sk_receive_queue上,recvfrom()和recv()要做的就是從sk_receive_queue上取出報文,拷貝到用戶空間,供用戶使用。
      sys_recv() -> sys_recvfrom()
      sys_recvfrom() -> sk->ops->recvmsg() 
                            ==> sock_common_recvmsg() -> sk->sk_prot->recvmsg()
                            ==> udp_recvmsg()

sys_recvfrom()
      調用sock_recvmsg()接收udp報文,存放在msg中,如果接收到報文,從內核到用戶空間拷貝報文的源地址到addr中,addr是recvfrom()調用的傳入參數,表示報文源的地址。而報文的內容是在udp_recvmsg()中從內核拷貝到用戶空間的。

err = sock_recvmsg(sock, &msg, size, flags);
if (err >= 0 && addr != NULL) {
 err2 = move_addr_to_user((struct sockaddr *)&address,
    msg.msg_namelen, addr, addr_len);
 if (err2 < 0)
  err = err2;
}


udp_recvmsg() 接收udp報文
      這個函數有三個關鍵操作:
        1. 取到數據包 -- __skb_recv_datagram()
        2. 拷貝數據 -- skb_copy_datagram_iovec()或skb_copy_and csum_datagram_iovec()
        3. 必要時計算校驗和 – skb_copy_and_csum_datagram_iovec() 

      __skb_recv_datagram(),它會從sk->sk_receive_queue上取出一個skb,前面已經分析到,內核收到發往該socket的報文會放在sk->sk_receive_queue。

skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0), &peeked, &err);

      如果沒有報文,有兩種情況:使用了非阻塞接收,且用戶接收時還沒有報文到來;使用阻塞接收,但之前沒有報文,且在sk->sk_rcvtimeo時間內都沒有報文到來。沒有報文,返回錯誤值。

if (!skb)
 goto out;

      len是recvfrom()傳入buf的大小,ulen是報文內容的長度,如果ulen > len,那麼只需要使用buf的ulen長度就可以了;如果len < ulen,那麼buf不夠報文填充,只能對報文截斷,取前len個字節。

ulen = skb->len - sizeof(struct udphdr);
if (len > ulen)
 len = ulen;
else if (len < ulen)
 msg->msg_flags |= MSG_TRUNC;

      如果報文被截斷或使用UDP-Lite,那麼需要提前驗證校驗和,udp_lib_checksum_complete()完成校驗和計算,函數在下面具體分析。

if (len < ulen || UDP_SKB_CB(skb)->partial_cov) {
 if (udp_lib_checksum_complete(skb))
  goto csum_copy_err;
}

      如果報文不用驗證校驗和,那麼執行if部分,調用skb_copy_datagram_iovec()直接拷貝報文到buf中就可以了;如果報文需要驗證校驗和,那麼執行else部分,調用skb_copy_and_csum_datagram_iovec()拷貝報文到buf,並在拷貝過程中計算校驗和。這也是爲什麼在內核收到udp報文時爲什麼先驗證校驗和再處理的原因,udp報文可能很大,校驗和的計算可能很耗時,將其放在拷貝過程中可以節約開銷,當然它的代價是一些校驗和錯誤的報文也會被添加到socket的接收隊列上,直到用戶真正接收時它們纔會被丟棄。

if (skb_csum_unnecessary(skb))
 err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov, len);
else {
 err = skb_copy_and_csum_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov);
 if (err == -EINVAL)
  goto csum_copy_err;
}

      拷貝地址到msg->msg_name中,在sys_recvfrom()中msg->msg_name=&address,然後address會從內核拷貝給用戶空間的addr。

if (sin) {
 sin->sin_family = AF_INET;
 sin->sin_port = udp_hdr(skb)->source;
 sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
 memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
}

      下面來重點看核心操作的三個函數:
__skb_recv_datagram()   從sk_receive_queue上取一個skb
      核心代碼段如下,skb_peek()從sk->sk_receive_queue中取出一個skb,如果有的話,則返回skb,作爲用戶此次接收的報文,當然還有對skb的後續處理,但該函數只是取出一個skb;如果還沒有的話,則使用wait_for_packet()等待報文到來,其中參數timeo代表等待的時間,如果使用非阻塞接收的話,timeo會設置爲0(即當前沒有skb的話則直接返回,不進行等待),否則設置爲sk->sk_rcvtimeo。

do {
 ……
 skb = skb_peek(&sk->sk_receive_queue);
 if (skb) {
  *peeked = skb->peeked;
  if (flags & MSG_PEEK) {
   skb->peeked = 1;
   atomic_inc(&skb->users);
  } else
   __skb_unlink(skb, &sk->sk_receive_queue);
 }
 if (skb)
  return skb;
……
} while (!wait_for_packet(sk, err, &timeo));


skb_copy_datagram_iovec()   拷貝skb內容到msg中
      拷貝可以分三部分:線性地址空間的拷貝,聚合/發散地址空間的拷貝,非線性地址空間的拷貝。第二部分需要硬件的支持,這裏討論另兩部分。
      在skb的buff中的是線性地址空間,在skb的frag_list上的是非線性地址空間;當沒有分片發生的,用線性地址空間就足夠了,但是當報文過長而分片時,第一個分片會使用線性地址空間,其餘的分片將被鏈到skb的frag_list上,即非線性地址空間,具體可以參考”ipv4模塊”中分片部分。
      拷貝報文內容時,就要將線性和非線性空間的內容都拷貝過去。下面是拷貝線性地址空間的代碼段,start是報文的線性部分長度(skb->len-skb->datalen),copy是線性地址空間的大小,offset是相對skb的偏移(即此次拷貝從哪裏開始),以udp報文爲例,這幾個值如下圖所示。memcpy_toiovec()拷貝內核到to中,要注意的是它改變了to的成員變量。

int start = skb_headlen(skb);
int i, copy = start - offset;
if (copy > 0) {
 if (copy > len)
  copy = len;
 if (memcpy_toiovec(to, skb->data + offset, copy))
  goto fault;
 if ((len -= copy) == 0)
  return 0;
 offset += copy;
}

      下面是拷貝非線性地址空間的代碼段,遍歷skb的frag_list鏈表,對上面的每個分片,拷貝內容到to中,這裏start, end的值不重要,重要的是它們的差值end-start,表示了當前分片frag_iter的長度,使用skb_copy_datagram_iovec()拷貝當前分片內容,即把每個分片都作爲單獨報文來處理。不過對於分片,感覺只有拷貝的第一部分和第二部分,在IP層分片重組時,並沒有將分片鏈在分片的frag_list上的情況,而都鏈在頭分片的frag_list上。

skb_walk_frags(skb, frag_iter) {
 int end;
 end = start + frag_iter->len;
 if ((copy = end - offset) > 0) {
  if (copy > len)
   copy = len;
  if (skb_copy_datagram_iovec(frag_iter,
    offset - start, to, copy))
   goto fault;
  if ((len -= copy) == 0)
   return 0;
  offset += copy;
 }
 start = end;
}

      還是以一個例子來說明,主機收到一個udp報文,內容長度爲4000 bytes,MTU是1500,傳入buff數組大小也爲4000。根據MTU,報文會會被分成三片,分片IP報內容大小依次是1480, 1480, 1040。每個分片都有一個20節字的IP報文,第一個分片還有一個8節字的udp報頭。接收時數據拷貝情況如下: 

 

      分片一是第一個分片,包含UDP報文,在拷貝時要跳過,因爲使用的是udp socket接收,只要報文內容就可以了。三張圖片代表了三次調用skb_copy_datagram_iovec()的情況,iov是存儲內容的buff,最終結果是三個分片共4000字節拷貝到了iov中。
memcpy_toiovec()函數需要注意,不僅因爲它改變了iovec的成員值,還因爲最後的iov++。在udp socket的接收recvfrom()中,msg.msg_iov = &iov,而iov定義成struct iovec iov,即傳入參數iov實際只有一個的空間,那麼在iov++後,iov將指向非法的地址。這裏只考慮udp使用時的情況,memcpy_toiovec()調用的前一句是,這裏len是接收buff的長度:

if (copy > len)
 copy = len;

      而memcpy_toiovec()中又有int copy = min_t(unsigned int, iov->iov_len, len),這裏len是上面傳入的copy,iov_len是接收buff長度,這兩句保證了函數中copy值與len相等,即完成一次拷貝後,len-=copy會使len==0,雖然iov++指向了非法內存,但由於while(len > 0)已退出,所以不會使用iov做任何事情。其次,函數中的iov++並不會對參數iov產生影響,即函數完成iov還是傳入的值。最後,拷貝完後會修改iov_len和iov_base的值,iov_len表示可用長度,iov_base表示起始拷貝位置。

int memcpy_toiovec(struct iovec *iov, unsigned char *kdata, int len)
{
 while (len > 0) {
  if (iov->iov_len) {
   int copy = min_t(unsigned int, iov->iov_len, len);
   if (copy_to_user(iov->iov_base, kdata, copy))
    return -EFAULT;
   kdata += copy;
   len -= copy;
   iov->iov_len -= copy;
   iov->iov_base += copy;
  }
  iov++;
 }
 return 0;
}


skb_copy_and_csum_datagram_iovec()   拷貝skb內容到msg中,同時計算校驗和
      這個函數提高了校驗和計算效率,因爲它合併了拷貝與計算操作,這樣只要一次遍歷操作就可以了。與skb_copy_datagram_iovec()相比,它在每次拷貝skb內容時,計算下這次拷貝內容的校驗和。

 
csum = csum_partial(skb->data, hlen, skb->csum);
if (skb_copy_and_csum_datagram(skb, hlen, iov->iov_base, chunk, &csum))
 goto fault; 


UDP報文發送
      發送時有兩種調用方式:sys_send()和sys_sendto(),兩者的區別在於sys_sendto()需要給入目的地址的參數;而sys_send()調用前需要調用sys_connect()來綁定目的地址信息;兩者的後續調用是相同的。如果調用sys_sendto()發送,地址信息在sys_sendto()中從用戶空間拷貝到內核空間,而報文內容在udp_sendmsg()中從用戶空間拷貝到內核空間。
      sys_send() -> sys_sendto()
      sys_sendto() -> sock_sendmsg() -> __sock_sendmsg() -> sock->ops->sendmsg()
                         ==> inet_sendmsg() -> sk->sk_prot->sendmsg()
                         ==> udp_sendmsg()
      udp_sendmsg()的核心流程如下圖所示,只列出了核心的函數調用了參數賦值,大致步驟是:獲取信息 -> 獲取路由項rt -> 添加數據 -> 發送數據。 

      udp_sock結構體中的pending用於標識當前udp_sock上是否有待發送數據,如果有的話,則直接goto do_append_data繼續添加數據;否則先要做些初始化工作,再才添加數據。實際上,pending!=0表示此調用前已經有數據在udp_sock中的,每次調和sendto()發送數據時,pending初始等於0;在添加數據時,設置up->pending = AF_INET。直到最後調用udp_push_pending_frames()將數據發送給IP層或skb_queue_empty(&sk->sk_write_queue)發送鏈表上爲空,這時設置up->pending = 0。因此,這裏可以看到,報文發送時pending值的變化: 

      通常使用sendto()發送都是一次調用對應一個報文,即pending=0->AF_INET->0;但如果調用sendto()時參數用到了MSG_MORE標誌,則pending=0->AF_INET,直到調用sendto()時未使用MSG_MORE標誌,表示此次發送數據是最後一部分數據時,pending=AF_INET->0。

if (up->pending) {
 lock_sock(sk);
 if (likely(up->pending)) {
  if (unlikely(up->pending != AF_INET)) {
   release_sock(sk);
   return -EINVAL;
  }
  goto do_append_data;
 }
 release_sock(sk);
}

      如果pending=0沒有待發送數據,執行初始化操作:報文長度、地址信息、路由項。
      ulen初始爲sendto()傳入的數據長度,由於是第一部分數據(如果沒有後續數據,則就是報文),ulen要添加udp報頭的8字節。

ulen += sizeof(struct udphdr);

      這段代碼獲取要發送數據的目的地址和端口號。一種情況是調用sendto()發送數據,此時目的的信息以參數傳入,存儲在msg->msg_name中,因此從中取出daddr和dport;另一種情況是調用connect(), send()發送數據,在connect()調用時綁定了目的的信息,存儲在inet中,並且由於是調用了connect(),sk->sk_state會設置爲TCP_ESTABLISHED。以後調用send()發送數據時,無需要再給入目的信息參數,因此從inet中取出dadr和dport。而connected表示了該socket是否已綁定目的。

if (msg->msg_name) {
 struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;
 if (msg->msg_namelen < sizeof(*usin))
  return -EINVAL;
 if (usin->sin_family != AF_INET) {
  if (usin->sin_family != AF_UNSPEC)
   return -EAFNOSUPPORT;
 }

 daddr = usin->sin_addr.s_addr;
 dport = usin->sin_port;
 if (dport == 0)
  return -EINVAL;
} else {
 if (sk->sk_state != TCP_ESTABLISHED)
  return -EDESTADDRREQ;
 daddr = inet->inet_daddr;
 dport = inet->inet_dport;
 connected = 1;
}

      下一步是獲取路由項rt,如果已連接(調用過connect),則路由信息在connect()時已獲取,直接拿就可以了;如果未連接或拿到的路由項已被刪除,則需要重新在路由表中查找,還是使用ip_route_output_flow()來查找,如果是連接狀態的socket,則要用新找到的rt來更新socket,當然,前提條件是之前的rt已過期。

if (rt == NULL) {
 ……
 err = ip_route_output_flow(net, &rt, &fl, sk, 1);
 ……
 if (connected)
  sk_dst_set(sk, dst_clone(&rt->u.dst));
}

      存儲信息daddr, dport, saddr, sport到cork.fl中,它們會在生成udp報頭和計算udp校驗和時用到。up->pending=AF_INET標識了數據添加的開始,下面將開始數據的添加工作。

inet->cork.fl.fl4_dst = daddr;
inet->cork.fl.fl_ip_dport = dport;
inet->cork.fl.fl4_src = saddr;
inet->cork.fl.fl_ip_sport = inet->inet_sport;
up->pending = AF_INET;

      如果pending!=0或執行完初始化操作,則直接執行添加數據操作:
      up->len表示要發送數據的總長度,包括udp報頭,因此每發送一部分數據就要累加它的長度,在發送後up->len被清0。然後調用ip_append_data()添加數據到sk->sk_write_queue,它會處理數據分片等問題,在 ”ICMP模塊” 中有詳細分析過。

up->len += ulen;
getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;
err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,
  sizeof(struct udphdr), &ipc, &rt,
  corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

      ip_append_data()添加數據正確會返回0,否則udp_flush_pending_frames()丟棄將添加的數據;如果添加數據正確,且沒有後續的數據到來(由MSG_MORE來標識),則udp_push_pending_frames()將數據發送給IP層,下面將詳細分析這個函數。最後一種情況是當sk_write_queue上爲空時,它觸發的條件必須是發送多個報文且sk_write_queue上爲空,而實際上在ip_append_data過後sk_write_queue不會爲空的,因此正常情況下並不會發生。哪種情況會發生呢?重置pending值爲0就是在這裏完成的,三個條件語句都會將pending設置爲0。

 
if (err)
 udp_flush_pending_frames(sk);
else if (!corkreq)
 err = udp_push_pending_frames(sk);
else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
 up->pending = 0;

       數據已經處理完成,釋放取到的路由項rt,如果有IP選項,也釋放它。如果發送數據成功,返回發送的長度len;否則根據錯誤值err進行錯誤處理並返回err。

ip_rt_put(rt);
if (free)
 kfree(ipc.opt);
if (!err)
 return len;
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
 UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;

      在 “ICMP模塊” 中往IP層發送數據使用的是ip_push_pending_frames()。而在UDP模塊中往IP層發送數據使用的是ip_push_pending_frames()。而在UDP模塊中往IP層發送數據的udp_push_pending_frames()只是對ip_push_pending_frames()的封裝,主要是增加對UDP的報頭的處理。同理,udp_flush_pending_frames()也是,只是它更簡單,僅僅重置了up->len和up->pending的值,重置後可以開始一個新報文。那麼udp_push_pending_frames()封裝了哪些處理呢。

udp_push_pending_frames() 發送數據給IP層
      設置udp報頭,包括源端口source,目的端口dest,報文長度len。

uh = udp_hdr(skb);
uh->source = fl->fl_ip_sport;
uh->dest = fl->fl_ip_dport;
uh->len = htons(up->len);
uh->check = 0;

      計算udp報頭中的校驗和,包括了僞報頭、udp報頭和報文內容。

if (is_udplite)
 csum  = udplite_csum_outgoing(sk, skb);
else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */
 skb->ip_summed = CHECKSUM_NONE;
 goto send;
} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */
 udp4_hwcsum_outgoing(sk, skb, fl->fl4_src, fl->fl4_dst, up->len);
 goto send;
} else       /*   `normal' UDP    */
 csum = udp_csum_outgoing(sk, skb);
uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len, sk->sk_protocol, csum);

      將報文發送給IP層,這個函數已經分析過了。

err = ip_push_pending_frames(sk);

      同樣,在發送完報文後,重置len和pending的值,以便開始下一個報文發送。

up->len = 0;
up->pending = 0;

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章