1. 程式人生 > >Linux TCP/IP 協議棧原始碼分析

Linux TCP/IP 協議棧原始碼分析

一.linux核心網路棧程式碼的準備知識 1. linux核心ipv4網路部分分層結構
BSD socket層: 這一部分處理BSD socket相關操作,每個socket在核心中以struct socket結構體現。這一部分的檔案 主要有:/net/socket.c /net/protocols.c etc

INET socket層:BSD socket是個可以用於各種網路協議的介面,而當用於tcp/ip,即建立了AF_INET形式的socket時, 還需要保留些額外的引數,於是就有了struct sock結構。檔案主要 有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c etc

TCP/UDP層:
處理傳輸層的操作,傳輸層用struct inet_protocol和struct proto兩個結構表示。檔案主要 有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c /net/ipv4/tcp_input.c /net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_output.c /net/ipv4/tcp_timer.c etc  
     
IP層:處理網路層的操作,網路層用struct packet_type結構表示。檔案主要有:/net/ipv4/ip_forward.c ip_fragment.c ip_input.c ip_output.c etc.

資料鏈路層和驅動程式:
每個網路裝置以struct net_device表示,通用的處理在dev.c中,驅動程式都在/driver/net目 錄下。
2. 兩臺主機建立udp通訊所走過的函式列表 ^
|       sys_read                fs/read_write.c
|       sock_read               net/socket.c
|       sock_recvmsg            net/socket.c
|       inet_recvmsg            net/ipv4/af_inet.c
|       udp_recvmsg             net/ipv4/udp.c
|       skb_recv_datagram       net/core/datagram.c
|       -------------------------------------------
|       sock_queue_rcv_skb      include/net/sock.h
|       udp_queue_rcv_skb       net/ipv4/udp.c
|       udp_rcv                 net/ipv4/udp.c
|       ip_local_deliver_finish net/ipv4/ip_input.c
|       ip_local_deliver        net/ipv4/ip_input.c
|       ip_recv                 net/ipv4/ip_input.c
|       net_rx_action           net/dev.c
|       -------------------------------------------
|       netif_rx                net/dev.c
|       el3_rx                  driver/net/3c309.c
|       el3_interrupt           driver/net/3c309.c

==========================

|       sys_write               fs/read_write.c
|       sock_writev             net/socket.c                    
|       sock_sendmsg            net/socket.c
|       inet_sendmsg            net/ipv4/af_inet.c
|       udp_sendmsg             net/ipv4/udp.c
|       ip_build_xmit           net/ipv4/ip_output.c
|       output_maybe_reroute    net/ipv4/ip_output.c
|       ip_output               net/ipv4/ip_output.c
|       ip_finish_output        net/ipv4/ip_output.c
|       dev_queue_xmit          net/dev.c
|       --------------------------------------------
|       el3_start_xmit          driver/net/3c309.c
V 3. 網路路徑圖、重要資料結構sk_buffer及路由介紹
linux-net.pdf 第2.1章 第2.3章 第2.4章 4. 從連線、傳送、到接收資料包的過程     linux-net.pdf 第4、5、6章詳細闡述 二.linux的tcp-ip棧程式碼的詳細分析 1.資料結構(msghdr,sk_buff,socket,sock,proto_ops,proto) bsd套接字層,操作的物件是socket,資料存放在msghdr這樣的資料結構: 建立socket需要傳遞family,type,protocol三個引數,建立socket其實就是建立一個socket例項,然後建立一個檔案描述符結構,並且互相建立一些關聯,即建立互相連線的指標,並且初始化這些對檔案的寫讀操作對映到socket的read,write函式上來。 同時初始化socket的操作函式(proto_ops結構),如果傳入的type引數是STREAM型別,那麼就初始化為SOCKET->ops為inet_stream_ops,如果是DGRAM型別,則SOCKET-ops為inet_dgram_ops。對於inet_stream_ops其實是一個結構體,包含了stream型別的socket操作的一些入口函式,在這些函式裡主要做的是對socket進行相關的操作,同時通過呼叫下面提到的sock中的相關操作完成socket到sock層的傳遞。比如在inet_stream_ops裡有個inet_release的操作,這個操作除了釋放socket的型別空間操作外,還通過呼叫socket連線的sock的close操作,對於stream型別來說,即tcp_close來關閉sock 釋放sock。 建立socket同時還建立sock資料空間,初始化sock,初始化過程主要做的事情是初始化三個佇列,receive_queue(接收到的資料包sk_buff連結串列佇列),send_queue(需要傳送資料包的sk_buff連結串列佇列),backlog_queue(主要用於tcp中三次握手成功的那些資料包,自己猜的),根據family、type引數,初始化sock的操作,比如對於family為inet型別的,type為stream型別的,sock->proto初始化為tcp_prot.其中包括stream型別的協議sock操作對應的入口函式。 在一端對socket進行write的過程中,首先會把要write的字串緩衝區整理成msghdr的資料結構形式(參見linux核心2.4版原始碼分析大全),然後呼叫sock_sendmsg把msghdr的資料傳送至inet層,對於msghdr結構中資料區中的每個資料包,建立sk_buff結構,填充資料,掛至傳送佇列。一層層往下層協議傳遞。一下每層協議不再對資料進行拷貝。而是對sk_buff結構進行操作。 inet套接字及以下層 資料存放在sk_buff這樣的資料結構裡: 路由:     在linux的路由系統主要儲存了三種與路由相關的資料,第一種是在物理上和本機相連線的主機地址資訊表,第二種是儲存了在網路訪問中判斷一個網路地址應該走什麼路由的資料表;第三種是最新使用過的查詢路由地址的快取地址資料表。     1.neighbour結構  neighbour_table{ }是一個包含和本機所連線的所有鄰元素的資訊的資料結構。該結構中有個元素是neighbour結構的陣列,陣列的每一個元素都是一個對應於鄰機的neighbour結構,系統中由於協議的不同,會有不同的判斷鄰居的方式,每種都有neighbour_table{}型別的例項,這些例項是通過neighbour_table{}中的指標next串聯起來的。在neighbour結構中,包含有與該鄰居相連的網路介面裝置net_device的指標,網路介面的硬體地址,鄰居的硬體地址,包含有neigh_ops{}指標,這些函式指標是直接用來連線傳輸資料的,包含有queue_xmit(struct * sk_buff)函式入口地址,這個函式可能會呼叫硬體驅動程式的傳送函式。     2.FIB結構 在FIB中儲存的是最重要的路由規則,通過對FIB資料的查詢和換算,一定能夠獲得路由一個地址的方法。系統中路由一般採取的手段是:先到路由快取中查詢表項,如果能夠找到,直接對應的一項作為路由的規則;如果不能找到,那麼就到FIB中根據規則換算傳算出來,並且增加一項新的,在路由快取中將專案新增進去。     3.route結構(即路由快取中的結構) 資料鏈路層:    net_device{}結構,對應於每一個網路介面裝置。這個結構中包含很多可以直接獲取網絡卡資訊的函式和變數,同時包含很多對於網絡卡操作的函式,這些直接指向該網絡卡驅動程式的許多函式入口,包括髮送接收資料幀到緩衝區等。當這些完成後,比如資料接收到緩衝區後便由netif_rx(在net/core/dev.c各種裝置驅動程式的上層框架程式)把它們組成sk_buff形式掛到系統接收的backlog佇列然後交由上層網路協議處理。同樣,對於上層協議處理下來的那些sk_buff。便由dev_queue_xmit函式放入網路緩衝區,交給網絡卡驅動程式的傳送程式處理。    在系統中存在一張連結串列dev_base將系統中所有的net_device{}結構連在一起。對應於核心初始化而言,系統啟動時便為每個所有可能支援的網路介面裝置申請了一個net_device{}空間並串連起來,然後對每個接點執行檢測過程,如果檢測成功,則在dev_base連結串列中保留這個接點,否則刪除。對應於模組載入來說,則是呼叫register_netdev()註冊net_device,在這個函式中執行檢測過程,如果成功,則加到dev_base連結串列。否則就返回檢測不到資訊。刪除同理,呼叫 unregister_netdev。 2.啟動分析     2.1 初始化程序 :start-kernel(main.c)---->do_basic_setup(main.c)---->sock_init(/net/socket.c)---->do_initcalls(main.c) void __init sock_init(void)
{
 int i;  printk(KERN_INFO "Linux NET4.0 for Linux 2.4/n");
 printk(KERN_INFO "Based upon Swansea University Computer Society NET3.039/n");  /*
  * Initialize all address (protocol) families. 每一項表示的是針對一個地址族的操作集合,例如對於ipv4來說,在net/ipv4/af_inet.c檔案中的函式inet_proto_init()就呼叫sock_register()函式將inet_families_ops初始化到屬於IPV4的net_families陣列中的一項。
  */
 
 for (i = 0; i < NPROTO; i++)
  net_families[i] = NULL;    /*
  * Initialize sock SLAB cache.初始化對於sock結構預留的記憶體的slab快取。
  */
 
 sk_init(); #ifdef SLAB_SKB
 /*
  * Initialize skbuff SLAB cache 初始化對於skbuff結構的slab快取。以後對於skbuff的申請可以通過函式kmem_cache_alloc()在這個快取中申請空間。
  */
 skb_init();
#endif  /*
  * Wan router layer.
  */ #ifdef CONFIG_WAN_ROUTER 
 wanrouter_init();
#endif  /*
  * Initialize the protocols module. 向系統登記sock檔案系統,並且將其安裝到系統上來。
  */  register_filesystem(&sock_fs_type);
 sock_mnt = kern_mount(&sock_fs_type);
 /* The real protocol initialization is performed when
  *  do_initcalls is run. 
  */
 /*
  * The netlink device handler may be needed early.
  */ #ifdef CONFIG_NET
 rtnetlink_init();
#endif
#ifdef CONFIG_NETLINK_DEV
 init_netlink();
#endif
#ifdef CONFIG_NETFILTER
 netfilter_init();
#endif #ifdef CONFIG_BLUEZ
 bluez_init();
#endif /*yfhuang ipsec*/
#ifdef CONFIG_IPSEC            
 pfkey_init();
#endif
/*yfhuang ipsec*/
}     2.2 do_initcalls() 中做了其它的初始化,其中包括                 協議初始化,路由初始化,網路介面裝置初始化 (例如inet_init函式以_init開頭表示是系統初始化時做,函式結束後跟module_init(inet_init),這是一個巨集,在include/linux/init.c中定義,展開為_initcall(inet_init),表示這個函式在do_initcalls被呼叫了)     2.3 協議初始化 此處主要列舉inet協議的初始化過程。 static int __init inet_init(void)
{
 struct sk_buff *dummy_skb;
 struct inet_protocol *p;
 struct inet_protosw *q;
 struct list_head *r;  printk(KERN_INFO "NET4: Linux TCP/IP 1.0 for NET4.0/n");  if (sizeof(struct inet_skb_parm) > sizeof(dummy_skb->cb)) {
  printk(KERN_CRIT "inet_proto_init: panic/n");
  return -EINVAL;
 }  /*
  * Tell SOCKET that we are alive... 註冊socket,告訴socket inet型別的地址族已經準備好了
  */
  
   (void) sock_register(&inet_family_ops);  /*
  * Add all the protocols. 包括arp,ip、ICMP、UPD、tcp_v4、tcp、igmp的初始化,主要初始化各種協議對應的inode和socket變數。 其中arp_init完成系統中路由部分neighbour表的初始化 ip_init完成ip協議的初始化。在這兩個函式中,都通過定義一個packet_type結構的變數將這種資料包對應的協議傳送資料、允許傳送裝置都做初始化。
  */  printk(KERN_INFO "IP Protocols: ");
 for (p = inet_protocol_base; p != NULL;) {
  struct inet_protocol *tmp = (struct inet_protocol *) p->next;
  inet_add_protocol(p);
  printk("%s%s",p->name,tmp?", ":"/n");
  p = tmp;
 }  /* Register the socket-side information for inet_create. */
 for(r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
  INIT_LIST_HEAD(r);  for(q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
  inet_register_protosw(q);  /*
  * Set the ARP module up 
  */  arp_init();    /*
    * Set the IP module up
    */  ip_init();  tcp_v4_init(&inet_family_ops);  /* Setup TCP slab cache for open requests. */
 tcp_init();
 /*
  * Set the ICMP layer up
  */  icmp_init(&inet_family_ops);  /* I wish inet_add_protocol had no constructor hook...
    I had to move IPIP from net/ipv4/protocol.c :-( --ANK
  */
#ifdef CONFIG_NET_IPIP
 ipip_init();
#endif
#ifdef CONFIG_NET_IPGRE
 ipgre_init();
#endif  /*
  * Initialise the multicast router
  */
#if defined(CONFIG_IP_MROUTE)
 ip_mr_init();
#endif  /*
  * Create all the /proc entries.
  */
#ifdef CONFIG_PROC_FS
 proc_net_create ("raw", 0, raw_get_info);
 proc_net_create ("netstat", 0, netstat_get_info);
 proc_net_create ("snmp", 0, snmp_get_info);
 proc_net_create ("sockstat", 0, afinet_get_info);
 proc_net_create ("tcp", 0, tcp_get_info);
 proc_net_create ("udp", 0, udp_get_info);
#endif  /* CONFIG_PROC_FS */  ipfrag_init();  return 0;
}    module_init(inet_init);
     2.4 路由初始化(包括neighbour表、FIB表、和路由快取表的初始化工作)             2.4.1 rtcache表 ip_rt_init()函式 在net/ipv4/ip_output中呼叫,net/ipv4/route.c中定義             2.4.2 FIB初始化 在ip_rt_init()中呼叫 在net/ipv4/fib_front.c中定義            2.4.3 neigbour表初始化  arp_init()函式中定義       2.5 網路介面裝置初始化              在系統中網路介面都是由一個dev_base連結串列進行管理的。通過核心的啟動方式也是通過這個連結串列進行操作的。在系統啟動之初,將所有核心能夠支援的網路介面都初始化成這個連結串列中的一個節點,並且每個節點都需要初始化出init函式指標,用來檢測網路介面裝置。然後,系統遍歷整個dev_base連結串列,對每個節點分別呼叫init函式指標,如果成功,證明網路介面裝置可用,那麼這個節點就可以進一步初始化,如果返回失敗,那麼證明該網路裝置不存在或是不可用,只能將該節點刪除。啟動結束之後,在dev_base中剩下的都是可以用的網路介面裝置。             2.5.1 do_initcalls---->net_dev_init()(net/core/dev.c)------>ethif_probe()(drivers/net/Space.c,在netdevice{}結構的init中呼叫,這邊ethif_probe是乙太網卡針對的呼叫) 3.網路裝置驅動程式(略) 4.網路連線      4.1 連線的建立和關閉             tcp連線建立的程式碼如下:                     server=gethostbyname(SERVER_NAME);                     sockfd=socket(AF_INET,SOCK_STREAM,0);                     address.sin_family=AF_INET;                     address.sin_port=htons(PORT_NUM);                     memcpy(&address.sin_addr,server->h_addr,server->h_length);                     connect(sockfd,&address,sizeof(address));        連線的初始化與建立期間主要發生的事情如下:        1)sys_socket呼叫:呼叫socket_creat(),創建出一個滿足傳入引數family、type、和protocol的socket,呼叫sock_map_fd()獲取一個未被使用的檔案描述符,並且申請並初始化對應的file{}結構。        2)sock_creat():建立socket結構,針對每種不同的family的socket結構的初始化,就需要呼叫不同的create函式來完成。對應於inet型別的地址來說,在網路協議初始化時呼叫sock_register()函式中完成註冊的定義如下:         struct net_proto_family inet_family_ops={                 PF_INET;                 inet_create         };所以inet協議最後會呼叫inet_create函式。        3)inet_create: 初始化sock的狀態設定為SS_UNCONNECTED,申請一個新的sock結構,並且初始化socket的成員ops初始化為inet_stream_ops,而sock的成員prot初始化為tcp_prot。然後呼叫sock_init_data,將該socket結構的變數sock和sock型別的變數關聯起來。        4)在系統初始化完畢後便是進行connect的工作,系統呼叫connect將一個和socket結構關聯的檔案描述符和一個sockaddr{}結構的地址對應的遠端機器相關聯,並且呼叫各個協議自己對應的connect連線函式。對應於tcp型別,則sock->ops->connect便為inet_stream_connect。        5)inet_stream_connect: 得到sk,sk=sock->sk,鎖定sk,對自動獲取sk的埠號存放在sk->num中,並且用htons()函式轉換存放在sk-&amp;gt;sport中。然後呼叫sk->prot->connect()函式指標,對tcp協議來說就是tcp_v4_connect()函式。然後將sock->state狀態字設定為SS_CONNECTING,等待後面一系列的處理完成之後,就將狀態改成SS_CONNECTTED。        6) tcp_v4_connect():呼叫函式ip_route_connect(),尋找合適的路由存放在rt中。ip_route_connect找兩次,第一次找到下一跳的ip地址,在路由快取或fib中找到,然後第二次找到下一跳的具體鄰居,到neigh_table中找到。然後申請出tcp頭的空間存放在buff中。將sk中相關地址資料做一些針對路由的變動,並且初始化一個tcp連線的序列號,呼叫函式tcp_connect(),初始化tcp頭,並設定tcp處理需要的定時器。一次connect()建立的過程就結束了。        連線的關閉主要如下:         1)close: 一個socket檔案描述符對應的file{}結構中,有一個file_operations{}結構的成員f_ops,它的初始化關閉函式為sock_close函式。         2)sock_close:呼叫函式sock_release(),引數為一個socket{}結構的指標。         3)sock_release:呼叫inet_release,並釋放socket的指標和檔案空間         4)inet_release: 呼叫和該socket對應協議的關閉函式inet_release,如果是tcp協議,那麼呼叫的是tcp_close;最後釋放sk。         4.2 資料傳送流程圖   各層主要函式以及位置功能說明:         1)sock_write:初始化msghdr{}結構 net/socket.c         2)sock_sendmsg:net/socket.c         3)inet_sendmsg:net/ipv4/af_net.c         4)tcp_sendmsg:申請sk_buff{}結構的空間,把msghdr{}結構中的資料填入sk_buff空間。net/ipv4/tcp.c         5)tcp_send_skb:net/ipv4/tcp_output.c         6)tcp_transmit_skb:net/ipv4/tcp_output.c         7)ip_queue_xmit:net/ipv4/ip_output.c         8)ip_queue_xmit2:net/ipv4/ip_output.c         9)ip_output:net/ipv4/ip_output.c         10)ip_finish_output:net/ipv4/ip_output.c         11)ip_finish_output2:net/ipv4/ip_output.c         12)neigh_resolve_output:net/core/neighbour.c         13)dev_queue_xmit:net/core/dev.c         4.3 資料接收流程圖 各層主要函式以及位置功能說明:         1)sock_read:初始化msghdr{}的結構型別變數msg,並且將需要接收的資料存放的地址傳給msg.msg_iov->iov_base.      net/socket.c         2)sock_recvmsg: 呼叫函式指標sock->ops->recvmsg()完成在INET Socket層的資料接收過程.其中sock->ops被初始化為inet_stream_ops,其成員recvmsg對應的函式實現為inet_recvmsg()函式. net/socket.c         3)sys_recv()/sys_recvfrom():分別對應著面向連線和麵向無連線的協議兩種情況. net/socket.c         4)inet_recvmsg:呼叫sk->prot->recvmsg函式完成資料接收,這個函式對於tcp協議便是tcp_recvmsg net/ipv4/af_net.c         5)tcp_recvmsg:從網路協議棧接收資料的動作,自上而下的觸發動作一直到這個函式為止,出現了一次等待的過程.函式tcp_recvmsg可能會被動地等待在sk的接收資料佇列上,也就是說,系統中肯定有其他地方會去修改這個佇列使得tcp_recvmsg可以進行下去.入口引數sk是這個網路連線對應的sock{}指標,msg用於存放接收到的資料.接收資料的時候會去遍歷接收佇列中的資料,找到序列號合適的.         但讀取佇列為空時tcp_recvmsg就會呼叫tcp_v4_do_rcv使用backlog佇列填充接收佇列.         6)tcp_v4_rcv:tcp_v4_rcv被ip_local_deliver函式呼叫,是從IP層協議向INET Socket層提交的"資料到"請求,入口引數skb存放接收到的資料,len是接收的資料的長度,這個函式首先移動skb->data指標,讓它指向tcp頭,然後更新tcp層的一些資料統計,然後進行tcp的一些值的校驗.再從INET Socket層中已經建立的sock{}結構變數中查詢正在等待當前到達資料的哪一項.可能這個sock{}結構已經建立,或者還處於監聽埠、等待資料連線的狀態。返回的sock結構指標存放在sk中。然後根據其他程序對sk的操作情況,將skb傳送到合適的位置.呼叫如下:         TCP包接收器(tcp_v4_rcv)將TCP包投遞到目的套接字進行接收處理. 當套接字正被使用者鎖定,TCP包將暫時排入該套接字的後備佇列(sk_add_backlog).這時如果某一使用者執行緒企圖鎖定該套接字(lock_sock),該執行緒被排入套接字的後備處理等待佇列(sk->lock.wq).當用戶釋放上鎖的套接字時(release_sock,在tcp_recvmsg中呼叫),後備佇列中的TCP包被立即注入TCP包處理器(tcp_v4_do_rcv)進行處理,然後喚醒等待佇列中最先的一個使用者來獲得其鎖定權. 如果套接字未被上鎖,當用戶正在讀取該套接字時, TCP包將被排入套接字的預備佇列(tcp_prequeue),將其傳遞到該使用者執行緒上下文中進行處理.如果新增到sk->prequeue不成功,便可以新增到 sk->receive_queue佇列中(使用者執行緒可以登記到預備佇列,當預備佇列中出現第一個包時就喚醒等待執行緒.)   /net/tcp_ipv4.c         7)ip_rcv、ip_rcv_finish:從乙太網接收資料,放到skb裡,作ip層的一些資料及選項檢查,呼叫ip_route_input()做路由處理,判斷是進行ip轉發還是將資料傳遞到高一層的協議.呼叫skb->dst->input函式指標,這個指標的實現可能有多種情況,如果路由得到的結果說明這個資料包應該轉發到其他主機,這裡的input便是ip_forward;如果資料包是給本機的,那麼input指標初始化為ip_local_deliver函式./net/ipv4/ip_input.c         8)ip_local_deliver、ip_local_deliver_finish:入口引數skb存放需要傳送到上層協議的資料,從ip頭中獲取是否已經分拆的資訊,如果已經分拆,則呼叫函式ip_defrag將資料包重組。然後通過呼叫ip_prot->handler指標呼叫tcp_v4_rcv(tcp)。ip_prot是inet_protocol結構指標,是用來ip層登記協議的,比如由udp,tcp,icmp等協議。 /net/ipv4/ip_input.c

++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1.1 核心原始碼的組織

表1是本文要使用的Linux Net/4網路原始碼的,其中大部分位於目錄/usr/src/linux-2.2.x/net,列表如下,

插口層 
BSD Socket 
/net/socket.c 
/net/protocols.c 
INET Socket 
/ipv4/protocol.c 
/ipv4/af_inet.c 
/net/ipv4/core/sock.c 
協議層 
TCP/UDP 
/net/ipv4/udp.c 
/net/ipv4/datagram.c 
/net/ipv4/tcp_input.c 
/net/ipv4//tcp_output.c 
/net/ipv4/tcp.c 
/net/ipv4/tcp_minisocks.c 
/net/ipv4/tcp_timer.c etc... 
IP 
/net/ipv4/ip_forward.c 
/net/ipv4/ip_fragment.c 
/net/ipv4/ip_input.c 
/net/ipv4/ip_output.c 
介面層 
Ethernet 
......

1.2  Linux中TCP/IP網路層次結構與實現 
Linux通過一組相鄰的軟體層實現了TCP/IP模型,它由BSD Socket層、INET Socket層、傳輸層、網路層,和鏈路層構成。應用程式使用系統呼叫向核心函式傳遞引數和資料從而進入核心空間,由核心中註冊的核心函式對相應的資料結構進行處理。

Linux的TCP/IP層次結構和實現方式如圖 1 所示。