歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[十二]:UDP模塊 - socket

Linux內核分析 - 網絡[十二]:UDP模塊 - socket

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

內核版本:2.6.34

這部分內容在於說明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

下面來看下創建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有值時才進行匹配。

Copyright © Linux教程網 All Rights Reserved