Linux網路程式碼導讀v0.2 (轉)

a400發表於2007-08-12
Linux網路程式碼導讀v0.2 (轉)[@more@]◆ 程式碼導讀v0.2

作者:yawl < yawl@nocus.>
主頁:http://www.nsfocus.com/
日期:2000-12-14

 1 前言

許多人在分析linux程式碼時對網路部分(主要是src/linux,src/linux/include/net及src/linux/include/linux目錄下的)比較感興趣,確實,儘管已經從書本上學到了大量的原理,不讀原始碼的話,頭腦中還是建立不起具體的印象。而分析這部分程式碼的一個問題便是程式碼眾多而資料很少。這篇文章的目的就是勾勒出一個,讓讀者能夠大致能夠了解TCP/IP究竟是怎麼工作的。以前見到的許多程式碼分析都是基於2.0核心的,在新的核心中許多變了名字,這尤其給初學者帶來了困難,本文是以2.4.0-test9的程式碼作例子,這樣對照程式碼時可能更清晰些。

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

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


2 正文

ISO的七層模型都非常熟悉了,當然,對於internet,用四層模型更為適合。在這兩份模型裡,網路以層次的形式出現。而LINUX的核心程式碼中,嚴格分出清楚的層次卻比較困難,因為除了一些"核心執行緒(kernel thread外)",整個核心其實是個單一的程式。因此所謂"網路層",只是一組相關的函式,而各層之間大多透過一般的函式的方式完成互動。

而從邏輯上,網路部分的程式碼更應該這樣分層更為合理:
. socket層:這一部分處理BSD socket相關操作,每個socket在核心中以struct socket結構體現。
 這一部分的檔案主要有:/net/socket.c/protocols.c etc

.INET socket層:BSD socket是個可以用於各種網路協議的介面,而當用於tcp/ip,即建立了AF_INET形式的socket時,還需要保留些額外的引數,於是就有了struct sock結構。
 檔案主要有:/net//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中,
 驅動程式都在//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想傳送資料時,程式中會呼叫如下語句(如果用sendto函式的話會走類似的流程,略):
...
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,
 accept: 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)中。為了的,上層的處理函式的將採用軟中斷的方式啟用,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 後記:
這次的投稿又是到了最後關頭倉促寫出來的,看著裡面拙劣的文筆,實在覺得有點對不住觀眾~~如果有時間我會把這部分好好重寫的,其實這也是我一直的願望:)


4 參考文獻:

[1.] phrack 55-12期
[2.] 2nd Edition
[3.] Network Buffers And Memory Management Alan Cox

[4.] 浙大原始碼分析報告《Linux網路裝置分析》潘綱
[5.] Linux Networking--A Gu to the Implementation and Modification of theLinux Poptocol Stack
 Glenn Herrin May 31,2000

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10790690/viewspace-951618/,如需轉載,請註明出處,否則將追究法律責任。

相關文章