• <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>
  • Linux網絡代碼導讀v0.2

    發表于:2007-05-26來源:作者:點擊數: 標簽:
    1 前言 許多人在分析 linux 代碼時對 網絡 部分(主要是src/linux .net ,src/linux/include/net及 src/linux/include/linux目錄下的文件)比較感興趣,確實,盡管已經從書本上學到了大 量的TCP/IP原理,不讀源碼的話,頭腦中還是建立不起具體的印象。而分析
     1 前言

    許多人在分析linux代碼時對網絡部分(主要是src/linux.net,src/linux/include/net及
    src/linux/include/linux目錄下的文件)比較感興趣,確實,盡管已經從書本上學到了大

    量的TCP/IP原理,不讀源碼的話,頭腦中還是建立不起具體的印象。而分析這部分代碼的
    一個問題便是代碼眾多而資料很少。這篇文章的目的就是勾勒出一個框架,讓讀者能夠大致
    能夠了解TCP/IP究竟是怎么工作的。以前見到的許多代碼分析都是基于2.0內核的,在新的
    內核中許多函數變了名字,這尤其給初學者帶來了困難,本文是以2.4.0-test9的代碼作例子,
    這樣對照代碼時可能更清晰些。

    其實網絡部分的代碼我只對防火墻部分一行行仔細分析過,其他許多地方也只是一知半解,
    如果理解有誤,歡迎指正。

    建議在看本文的同時,用source insight(www.soucedyn.com)建立一個項目,同時看代碼,
    這樣可能效果更好點。我也用過其他的一些工具,但在分析大量的代碼的時候,沒有一個工
    具比它更方便的了。


    2 正文

    ISO的七層模型都非常熟悉了,當然,對于internet,用四層模型更為適合。在這兩份模型里,
    網絡協議以層次的形式出現。而LINUX的內核代碼中,嚴格分出清楚的層次卻比較困難,因為
    除了一些"內核線程(kernel thread外)",整個內核其實是個單一的進程。因此所謂"網絡層"
    ,只是一組相關的函數,而各層之間大多通過一般的函數調用的方式完成交互。

    而從邏輯上,網絡部分的代碼更應該這樣分層更為合理:
    .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目錄下。

    網絡部分還有很多其他文件,如防火墻,路由等,一般根據看到名字便能猜測出相應的處理,此處不再贅述。

    現在我要給出一張表,全文的內容就是為了說明這張表(如果你覺得我在文章中的語言比較乏味,盡可
    拋掉他們,結合這張表自己看代碼)。在我最初看網絡部分代碼時,比較喜歡《linux kernel internals》
    的第八章的一段,其中有一個進程A通過網絡遠程向另一進程B發包的例子,詳細介紹了一個數據包如何
    從網絡堆棧中走過的過程。我覺得這樣可以更迅速的幫助讀者看清森林的全貌,因此本文參照這種結構來
    敘述。

    ^
    | 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



    我們假設的環境如下:有兩臺主機通過互聯網聯在一起,其中一臺機子運行這一個進程A,
    另外一臺運行進程B,進程A將向進程B發出一條信息,比如"Hello",而B接受此信息。
    TCP處理本身非常復雜,為了便于敘述,在后面我們將用UDP作為例子。


    2.1 建立套接字

    在數據發送之前,要建立一個套接字(socket),在兩邊的程序中都會調用如下語句:

    ...
    int sockfd;
    sockfd=socket(AF_INET,SOCK_DGRAM,0);
    ...

    這是個系統調用,因此會通過0x80中斷進入系統內核,調用內核中的相應函數.當尋找
    系統調用在內核中的對應流程時,一般前面加入"sys_"再找就是了,如對fork來說,就是
    調用sys_fork。但是socket相關調用有些特殊,所有的這類調用都是通過一個入口,即
    sys_socketcall進入系統內核,然后再通過參數調用具體的sys_socket,socket_bind等函數。

    sys_socket會調用sock_create產生一個struct socket結構(見include/linux/net.h),
    每個套接字在內核中都有一個這樣的結構對應,在初始化了此結構的一些通用成員后(如
    分配inode,根據第二個參數為type項賦值等),會根據其一個參數作響應的調度,即這
    一句:
    ...
    net_families[family]->create(sock, protocol);
    ...

    我們的程序的第一個參數是AF_INET,所以此函數指針會指向inet_create();(net_families
    是個數組,保留了網絡協議族(net families)的信息,而這些協議族用sock_register加載。)

    在struct socket結構結構中最重要的信息保留在struct sock結構中,這個結構在網絡代碼中經常
    使用,建議把它和其他常見結構(如struct sk_buff)打印出來放在手邊。在inet_create會為此
    結構分配內存,并根據套接字類型(其實就是socket函數的第二個參數),作各自不同的初始化:
    ...
    if (sk->prot->init)
    sk->prot->init(sk);
    ...

    如果類型是SOCK_STREAM的話會調用tcp_v4_init_sock,而SOCK_DGRAM類型的socket沒有額外的初始化了,
    到此socket調用結束。

    還有一個值得注意的地方是當inet_create()調用完后,會接著調用sock_map_fd函數,這個
    函數中會為套接字分配一個文件描述符并分配一個file文件。在應用層便可象處理文件一樣
    處理套接字了。

    開始的時候可能有些流程難以跟下去,主要便是這些函數指針的實際指向會根據類型變化。


    2.2 發送數據

    當進程A想發送數據時,程序中會調用如下語句(如果用send函數的話會走類似的流程,略):
    ...
    write(sockfd,"Hello",strlen("Hello"));
    ...

    write在內核中對應的函數就是sys_write,此函數首先根據文件描述符找到struct file結構,如果此文件
    存在(file指針非空)且可寫(file->f_mode & FMODE_WRITE為true),便調用此文件結構的寫操作:
    ...
    if (file->f_op && (write = file->f_op->write) != NULL)
    ret = write(file, buf, count, &file->f_pos);
    ...

    其中f_op是個struct file_operations結構指針,在sock_map_fd中將其指向socket_file_ops,其
    定義如下(/net/socket.c):
    static struct file_operations socket_file_ops = {
    llseek: sock_lseek,
    read: sock_read,
    write: sock_write,
    poll: sock_poll,
    ioctl: sock_ioctl,
    mmap: sock_mmap,
    open: sock_no_open, /* special open code to disallow open via /proc */
    release: sock_close,
    fasync: sock_fasync,
    readv: sock_readv,
    writev: sock_writev
    };

    此時wirte函數指針顯然指向了sock_write,我們跟下去看,此函數將一個字符串緩沖整理成struct msghdr,
    最后調用了sock_sendmsg.

    sock_sendmsg中的scm_send我不了解(scm是Socket level control messages的簡寫),好在它也不是很關鍵,
    我們注意到這句:
    ...
    sock->ops->sendmsg(sock, msg, size, &scm);
    ...

    又是個函數指針,sock->ops在inet_create()函數中被初始化,由于我們我們是UDP的套接字,sock->ops
    指向了inet_dgram_ops(即sock->ops = &inet_dgram_ops;),其定義在net/ipv4/Af_inet.c中:
    struct proto_ops inet_dgram_ops = {
    family: PF_INET,

    release: inet_release,
    bind: inet_bind,
    connect: inet_dgram_connect,
    socketpair: sock_no_socketpair,
    aclearcase/" target="_blank" >ccept: sock_no_accept,
    getname: inet_getname,
    poll: datagram_poll,
    ioctl: inet_ioctl,
    listen: sock_no_listen,
    shutdown: inet_shutdown,
    setsockopt: inet_setsockopt,
    getsockopt: inet_getsockopt,
    sendmsg: inet_sendmsg,
    recvmsg: inet_recvmsg,
    mmap: sock_no_mmap,
    };

    因此我們要看得便是inet_sendmsg()函數了,而馬上,這個函數又通過函數指針調用了另一函數:
    ...
    sk->prot->sendmsg(sk, msg, size);
    ...

    我們不得不再次尋找其具體指向??吹竭@里,說點題外話,怎么才能找到其具體定義呢?我一般是這樣:
    對上例而言,sk是個struct sock結構,到其定義(linux/net/sock.h中)出看到prot是個struct proto
    結構,此時我們便在源代碼樹中尋找所有此結構的實例(這些諸如跳到定義,尋找引用等工作在source
    insight中實在太方便快速了^_^),很快便會發現諸如udp_prot,tcp_prot,raw_prot等,猜測是用了
    udp_prot,便再找一下它在源代碼中的引用情況,果然發現在inet_create中有這么一句:
    ...
    prot=&udp_prot;
    ...

    其實如果前面看inet_create函數時仔細一點會早點發現了,但我總沒有這么細心:)。

    我們順著udp_sendmsg往下走:
    在這個函數的主要作用是填充UDP頭(源端口,目的端口等),接著調用了
    ip_route_output,作用是查找出去的路由,而后:
    ...
    ip_build_xmit(sk,
    (sk->no_check == UDP_CSUM_NOXMIT ?

    udp_getfrag_nosum :
    udp_getfrag),
    &ufh, ulen, &ipc, rt, msg->msg_flags);
    ...

    ip_build_xmit函數的很大比例是生成sk_buff,并為數據包加入IP頭。
    后面有這么一句:
    ...
    NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,output_maybe_reroute);
    ...

    簡單的說,在沒有防火墻代碼干預的情況下,你可以將此處理解為直接調用output_maybe_reroute,
    (具體可參看綠盟月刊14期中的《內核防火墻netfilter入門 》)
    而output_maybe_reroute中只有一句:
    return skb->dst->output(skb);

    依舊照上面的方法(不過這個確實不太好找),發現其實這個指針是在ip_route_output中指定的,
    (提示:ip_route_output_slow中:rth->u.dst.output=ip_output;),ip_route_output的作用
    便是查找路由,并將結果記錄到skb->dst中。

    于是,我們開始看ip_output函數了,而它馬上又走向了ip_finish_output~~。
    每個網絡設備,如網卡,在內核中由一個net_device表示,在ip_finish_output中找到其用到的設備
    (也是在ip_route_output中初始化的),這個參數在會傳給netfilter在NF_IP_POST_ROUTING點登記
    的函數,結束后調用ip_finish_output2,而這個函數中又會調用:
    ...
    hh->hh_output(skb);
    ...

    閑話少敘,實際調用了dev_queue_xmit,到此我們完成了TCP/IP層的工作,開始數據鏈路層的處理。

    在做了一些判斷之后,實際的調用是這句:
    ...
    dev->hard_start_xmit(skb, dev);
    ...

    這個函數是在網卡的驅動程序中定義的,每個不同的網卡有不同的處理,我的網卡是比較通用的3c509
    (其驅動程序是3c509.c),在網卡處理化的時候(el3_probe),有:
    ...
    dev->hard_start_xmit = &el3_start_xmit;
    ...

    再往下便是IO操作,將數據包真正的發到網絡上去,至此發送過程結束。

    中間我說的有些草率,完全沒顧的上中間的如出錯,阻塞,分片等特殊處理,只是將理想的過程描述出來。
    這篇短文的目的也只是幫助大家建立個大致的印象,其實每個地方的都有非常復雜的處理(尤其是TCP部分)。


    2.3 接受數據

    當有數據到達網卡的時候,會產生一個硬件中斷,然后調用網卡驅動程序中的函數來處理,對我的3c509網卡來說,
    其處理函數為:el3_interrupt。(相應的IRQ號是在系統啟動,網卡初始化時通過request_irq函數決定的。)
    這個中斷處理程序首先要做的當然就是進行一些IO操作將數據讀入(讀IO用inw函數),當數據幀成功接受后,
    執行el3_rx(dev)進一步處理。

    在el3_rx中,收到的數據報會被封裝成struct sk_buff,并脫離驅動程序,轉到通用的處理函數netif_rx
    (dev.c)中。為了CPU的效率,上層的處理函數的將采用軟中斷的方式激活,netif_rx的一個重要工作就
    是將傳入的sk_buff放到等候隊列中,并置軟中斷標志位,然后便可放心返回,等待下一次網絡數據包的到來:
    ...
    __skb_queue_tail(&queue->input_pkt_queue,skb);
    __cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);
    ...

    這個地方在2.2內核中一直被稱為"底半"處理--bottom half,其內部實現基本類似,目的是快速的從中斷中返回。

    過了一段時間后,一次CPU調度會由于某些原因會發生(如某進程的時間片用完)。在進程調度函數即schedule()
    中,會檢查有沒有軟中斷發生,若有則運行相應的處理函數:
    ...
    if (softirq_active(this_cpu) & softirq_mask(this_cpu))
    goto handle_softirq;
    handle_softirq_back:
    ...
    ...
    handle_softirq:
    do_softirq();
    goto handle_softirq_back;
    ...

    在系統初始化的時候,具體說是在net_dev_init中,此軟中斷的處理函數被定為net_rx_action:
    ...
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
    ...

    當下一次進程調度被執行的時候,系統會檢查是否發生NET_TX_SOFTIRQ軟中斷,若有則調用net_rx_action。

    net_tx_action函數既是2.2版本中的net_bh函數,在內核中有兩個全局變量用來登記網絡層的,
    一個是鏈表ptype_all,另外一個是數組ptype_base[16],他們記載了所有內核能夠處理
    的第三層(按照OSI7層模型)協議。每個網絡層的接收處理由一個
    struct packet_type表示,而這個結構將通dev_add_pack函數將他們登記到ptype_all或
    ptype_base中。只有packet_type中的type項為ETH_P_ALL時,才會登記到ptype_all鏈表
    中,否則如ip_packet_type,會在數組ptype_base[16]找到相應的位置。兩者不同點是
    如果是以ETH_P_ALL類型登記,那么處理函數會受到所有類型的包,否則只能處理自己登記
    的類型的。

    skb->protocol是在el3_rx中賦值的,其實就是以太幀頭信息中提取出的上層協議名,對
    于我們的例子來說,這個值是ETH_P_IP,所以在net_tx_action中,會選擇IP層的接收處理
    函數,而從ip_packet_type 不難看出,這個函數便是ip_recv()。
    pt_prev->func(實際指向ip_recv)前面有一個atomic_inc(&skb->users)操作(在2.2
    內核中這個地方是一句skb_clone,原理類似),目的是增加這個sk_buff的引用數。網絡層
    的接收函數在處理完或因為某些原因要丟棄此sk_buff時(如防火墻)會調用kfree_skb,
    而kfree_skb中首先會檢查是否還有其他地方需要此函數,如果沒有地方再用,才真正釋放
    此內存(__kfree_skb),否則只是計數器減一。

    現在我們便來看看ip_recv(net/ipv4/ip_input.c)。這個函數的操作是非常清晰的:
    首先檢查這個包的合法性(版本號,長度,校驗和等是否正確),如果合法則進行接下來
    的處理。在2.4內核中,為了靈活處理防火墻代碼,將原來的一個ip_recv分成了兩部分,
    即將將原來的的ip_recv的后半段獨立出一個ip_rcv_finish函數。在ip_rcv_finish中,
    一部分是帶有IP選項(如源路由等)的IP包,例外就是通過ip_route_input查找路由,
    并將結果記錄到skb->dst中。此時接收到的包有兩種,發往本地進程(需要傳往上層協議)
    或轉發(用作網關時),此時需要的處理函數也不相同,如果傳往本地,則調用ip_local_deliver
    (/net/ipv4/ip_input.c),否則調用ip_forward(/net/ipv4/ip_forward.c).skb->dst->input
    這個函數指針會將數據報領上正確的道路。

    對我們的例子而言,此時應該是調用ip_local_deliver的時候了。
    發來的包很有可能是碎片包,這樣的話則首先應該把它們組裝好再傳給上層協議,這當然也是
    ip_local_deliver函數所做的第一份工作,如果組裝成功(返回的sk_buff不為空),則繼續處
    理(詳細的組裝算法可參見綠盟月刊13期中的《IP分片重組的分析和常見碎片攻擊》)。
    但此時代碼又被netfilter一分為二了,象前面一樣,我們直接到后半段,即ip_local_deliver_finish
    (/net/ipv4/ip_input.c)中去。

    傳輸層(如TCP,UDP,RAW)的處理被登記到了inet_protos中(通過inet_add_protocol)。
    ip_local_deliver_finish會根據
    IP頭信息中的上層協議信息(即iph->protocol),調用相應的處理函數。為了簡便,我們
    采用了udp,此時的ipprot->handler實際便是udp_rcv了。

    前面已經提到,在應用程序中建立的每個socket在內核中有一個struct socket/struct sock
    對應。udp_rcv會通過udp_v4_lookup首先找到在內核中的sock,然后將其作參數調用
    udp_queue_rcv_skb(/net/ipv4/udp.c)。馬上,sock_queue_rcv_skb函數被調用,
    此函數將sk_buff放入等待隊列,然后通知上層數據到達:
    ...
    kb_set_owner_r(skb, sk);
    skb_queue_tail(&sk->receive_queue, skb);
    if (!sk->dead)
    sk->data_ready(sk,skb->len);
    return 0;
    ...

    sk->data_ready的定義在sock結構初始化的時候(sock_init_data):
    ...
    sk->data_ready=sock_def_readable;
    ...

    現在我們便要從上往下看起了:
    進程B要接收數據報,在程序里調用:
    ...
    read(sockfd,buff,sizeof(buff));
    ...

    此系統調用在內核中的函數是sys_read(fs/read_write.c)以下的處理類似write的操作,
    不再詳述.udp_recvmsg函數會調用skb_recv_datagram,如果數據還沒有到達,且socket
    設為阻塞模式時,進程會掛起(signal_pending(current)),直到data_ready通知進程
    資源得到滿足后繼續處理(wake_up_interruptible(sk->sleep);)。

    2.4 skbuff

    網絡代碼中有大量的處理涉及對sk_buff的操作,盡管此文中盡量將其回避了,但在仔細分析的時候
    則必須對此作分析,數據包在網絡協議層是以sk_buff的形式傳送處理的,可以說它是網絡部分最重要
    的數據結構。具體分析建議參看alan cox的《Network Buffers And Memory Management》,這片發表
    在1996年10月的linux journal上。

    這里引用phrack 55-12期中的一幅圖,盡管它只描繪了sk_buff的極小的一個側面,但卻非常有用,
    尤其是當你像我一樣總忘記了skb_put是向前還是向后調指針的時候:)

    --- -----------------hand
    ^ | |
    | | | ^ skb_push
    | | | |
    | -----------------data--- ---
    | | | ^ |
    true | | | v skb_pull
    size | | len
    | | | | ^ skb_trim
    | | | v |
    | -----------------tail--- ---
    | | | |
    | | | v skb_put
    v | |
    --- -----------------end

    linux網絡層效率:在linux的網絡層代碼中指針被大量應用,其目的就是避免數據拷貝這類耗費系統資源的操作。
    一個數據包的數據段部分在讀入或發出時只經過兩次拷貝,即從網卡中考到核心態內存,和從核心態內存考到
    用戶態內存。前些天看到,在一些提高sniffer抓包效率的嘗試中,turbo packet(一個內核補丁)采用了核心態和
    用戶態共享一段內存的辦法,又減少了一次數據拷貝,進一步提高了效率。


    3 后記:
    這次的投稿又是到了最后關頭倉促寫出來的,看著里面拙劣的文筆,實在覺得有點對不住觀眾~~如果有時間我會把這部分好好重寫的,其實這也是我一直的愿望:)

    原文轉自:http://www.kjueaiud.com

    老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月

  • <ruby id="5koa6"></ruby>
    <ruby id="5koa6"><option id="5koa6"><thead id="5koa6"></thead></option></ruby>

    <progress id="5koa6"></progress>

  • <strong id="5koa6"></strong>