天天看點

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");

for (i = 0; i < NPROTO; i++)

  net_families[i] = NULL; 

sk_init();

#ifdef SLAB_SKB

skb_init();

#endif

#ifdef CONFIG_WAN_ROUTER 

wanrouter_init();

#endif

register_filesystem(&sock_fs_type);

sock_mnt = kern_mount(&sock_fs_type);

#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

#ifdef CONFIG_IPSEC            

pfkey_init();

#endif

}

    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;

}

   (void) sock_register(&inet_family_ops);

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;

}

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);

arp_init();

ip_init();

tcp_v4_init(&inet_family_ops);

tcp_init();

icmp_init(&inet_family_ops);

#ifdef CONFIG_NET_IPIP

ipip_init();

#endif

#ifdef CONFIG_NET_IPGRE

ipgre_init();

#endif

#if defined(CONFIG_IP_MROUTE)

ip_mr_init();

#endif

#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 

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->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 資料發送流程圖

Linux TCP/IP 協定棧源碼分析

各層主要函數以及位置功能說明:

        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 資料接收流程圖

Linux TCP/IP 協定棧源碼分析

各層主要函數以及位置功能說明:

        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

Linux通過同時對多種通信協定的支援來提供通用的底層基礎服務。它的第一個網絡模型的版本是4.3 BSD,也稱為Net/1,今天的Linux已經使用Net/4 (Linux 2.2),其中大多數代碼已經完全和BSD的版本不同,但是它依然支援UINX平台之間程式的移植。

Linux網絡套接字實作的模式是UNIX下的普遍标準。同時,Net/4的網絡層是完全另起爐竈重寫的。首先,新的網絡層盡可能地實行并行處理, 是以其伸縮性比起以前的版本,不可同日而語。其次,它包括了許多的優化,以便繞過不少流行作業系統網絡實作中的不合理處(例如Windows)。到目前為止,Linux 是唯一與IPv4和IPv6協定标準完全保持相容的作業系統,而Linux2.4的IPv4伸縮性又大有提高。

Linux支援的六種不同通信協定族:

1) TCP/IP (使用TCP/IP的Internet 協定族),本文讨論的重點。

2) UNIX域協定 (一種程序間通信的協定)

3) X25協定

4) AX25協定 (業餘無線X25)

5)IPX協定 (Novell IPX)

6) APPLETALK協定 (AppleTalk DDP)

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 所示。

Linux TCP/IP 協定棧源碼分析

繼續閱讀