一、報文跨層傳遞
所有的網絡協議棧都告訴我們:TCP/IP協議棧是分層的,低一層的協議無需也不能感覺到上層的協議,這個觀念在我的腦海中根深蒂固,並且由衷的贊嘆這種設計的思想,但是在經過一些簡單的思考就會發現,這種分層並不是絕對的,正如這世間的一切。一個直觀的問題是,一樣米養百樣人,同樣的網卡上,可以跑IP/ARP協議,也可以有ICMP/IGMP/TCP/UDP協議,低層協議棧的數據向上傳遞,如何根據這些不同的類型派發到下一層的不同邏輯也是一個問題;對應地,當上層通過網卡發送數據時,下層的協議棧同樣可能會需要知道上層的協議。
對於這種問題的處理,通常有兩種方法,一種就是在低層字段解析出上層的類型;另一種就是真的透明,在下一層的固定字段,通常最簡單的就是第一個字段,但是第一個字段通常還要有更加必不可少的報文長度信息,並且在實際應用中也沒有使用這個格式。更直觀的說,以以太網一個數據幀來說,它最為關鍵的三個信息時 Destination MAC Address、Source MAC Address、 Ether Type;其中的 Ether Type決定了上層的協議類型,可能是IP、ARP,在內核的if_ether.h文件中枚舉了大量我沒有見到更沒有用到過的數據幀類型。在IP協議中,Protocol字段表示了傳輸層的協議;也就是說,一個低層的協議中其實是包含了直接上層協議棧中的具體類型,按照Google protobuffer的格式,其實就相當於一個union結構的selector放在了自己依賴層而不是放在自己的結構中,這樣從實現上來看,個人感覺的確是沒有做到層與層之間的絕對透明。但是從程序或者說協議棧的實現來看,這個地方真真是極好的。現在考慮一個網卡收到一個報文,在這個報文自下向上傳遞的過程中,如果提前知道下一層的具體協議,可以非常方便報文向上層協議棧的派發。這里也使我們得到一個重要的經驗:如果一個字段是該層所有模塊都必須的,那么可以下放到下一層。
由於網卡是最為常見的介質,使用這個作為例子未免會缺少不同實現之間比較的意義,所以使用另一個基於tty設備的通訊介質通訊類型作為例子,其實我們常見的modem也就是tty的一種。
二、從介質層接收到報文
對於slip的處理位於linux-2.6.32.60\drivers\net\slip.c文件:slip_receive_buf--->>>slip_unesc--->>sl_bump。在這個函數中,SLIP將從tty設備上接收的數據上傳給網絡層處理,但是問題是在這個設備上並沒有指明上層協議類型,因為作為SLIP設備它只支持一種IP類型,ARP在SLIP中沒有意義,所以在該函數中直接寫死了網絡層協議類型,也就是IP協議:
skb->protocol = htons(ETH_P_IP);
作為對比,我們看下功能更為強大的,也是我們最常用的網卡設備中對於這個字段的處理。自然而然地,這個字段在以太網frame中自帶類型,驅動層只需要做一個簡單的處理(區分出 類型還是長度)之后透傳給上層,而這個透傳對於協議棧來說就是派發(dispatch)。
對於通用網卡上接收的以太幀來說,它的處理位於linux-2.6.32.60\net\ethernet\eth.c:eth_header(),其中對於上層協議棧類型的解析只有一點特殊:
if (type != ETH_P_802_3)
eth->h_proto = htons(type);
else
eth->h_proto = htons(len);
三、向網絡層的dispatch
對於介質層發送的報文,內核通常叫做packet,由於介質層已經指明了網絡層的類型,所以這個packet的派發就比較簡單了,大家把自己對應的selector注冊到一起,然后內核就可以根據介質層packet中的ether type決定由網絡層的哪一種類型來處理,這些協議的注冊就是一個一個的packet_type對象,我們常見的packet類型包含有af_inet.c:ip_packet_type;arp.c:arp_packet_type兩種類型。
當網絡設備接收到報文之后,解析出一個基本的sk_buff結構,這個結構關鍵的地方就是要設置結構中的protocol字段,在netif_receive_skb函數中會遍歷這個設備上所注冊的所有的packet類型:
type = skb->protocol;
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
if (ptype->type == type &&
(ptype->dev == null_or_orig || ptype->dev == skb->dev ||
ptype->dev == orig_dev)) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
四、向傳輸層的dispatch
網絡層協議的注冊通過net_protocol對象來注冊,在af_inet.c文件中定義了我們最為常見的igmp_protocol、tcp_protocol、udp_protocol、icmp_protocol。這些協議注冊之后,在IP層向上dispatch的時候就可以直接使用這個作為路由規則(或者認為是protocol selector,我更prefer這個叫法)。linux-2.6.32.60\net\ipv4\ip_input.c:ip_local_deliver_finish(struct sk_buff *skb)
int protocol = ip_hdr(skb)->protocol;
int hash, raw;
const struct net_protocol *ipprot;
……
hash = protocol & (MAX_INET_PROTOS - 1);
ipprot = rcu_dereference(inet_protos[hash]);
……
ret = ipprot->handler(skb);
到了控制層之后,不再有再上一層的protocol selector了,http、ftp都是自己來區分和定義協議。
五、為什么會又看到了這些
網絡這一塊感覺很久沒有再看了,再次看到這里,說來話長,簡單來說原因就是想知道一個問題的答案:假設A和B兩個主機建立了一個TCP連接,然后A close連接,在A經過一些FIN_WAIT1、FIN_WAIT2、TIME_WAIT延遲狀態之后,這個主機A上和B之間的TCP連接終將斷開,這個斷開意味着這個連接狀態從整個系統A中消失,這個也沒什么不合理,但是問題套接口的另一端B如果堅持不關閉這個socket,在B上將始終看到一個CLOSE_WAIT狀態的established狀態的socket連接,這個連接在默認情況下將會一直存在(我說的默認情況包括了沒有設置socket的keepalive選項)。再進一步,假設說A不斷的連接B的同一個端口,由於A主機上的本地端口會有一個端口范圍,隨着時間的推移,A機器上的本地端口會有一個輪回,也就是說B機器上現在還存在的、處於CLOSE_WAIT狀態中的established TCP socket連接將怎么看待這個新來的連接?
這個地方說起來有些拗口,所以通過一個代碼來演示下吧(代碼從
網路上一個代碼改寫,主要是在客戶端增加了設置TCP_LINGER2時間,從而讓客戶端快速釋放本地port):
tsecer@harry: cat server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
int main(int argc, char *argv[])
{
int listenfd = 0, connfd = 0;
struct sockaddr_in serv_addr;
char sendBuff[1025];
time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, '0', sizeof(serv_addr));
memset(sendBuff, '0', sizeof(sendBuff));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(5555);
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listenfd, 10);
while(1)
{
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
}
}
tsecer@harry: cat client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
int main(int argc, char *argv[])
{
int sockfd = 0, n = 0;
char recvBuff[1024];
struct sockaddr_in serv_addr;
if(argc != 2)
{
printf("\n Usage: %s <ip of server> \n",argv[0]);
return 1;
}
memset(recvBuff, '0',sizeof(recvBuff));
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Error : Could not create socket \n");
return 1;
}
int iset = 1;設置linger2時間為1,從而便於快速釋放本地port
iset = setsockopt(sockfd, SOL_TCP, TCP_LINGER2, &iset,sizeof(iset));
printf("iset %d\n", iset);
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5555);
if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)
{
printf("\n inet_pton error occured\n");
return 1;
}
if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\n Error : Connect Failed \n");
return 1;
}
return 0;
}
tsecer@harry: gcc server.c -o server
tsecer@harry: gcc client.c -o client
tsecer@harry: ./server &
[1] 19063
tsecer@harry: echo "32768 32770" > /proc/sys/net/ipv4/ip_local_port_range
為了便於測試,限制本地連接端口最多使用兩個,也即是最多使用了兩個本地端口之后本地端口開始回繞。
tsecer@harry: for (( n = 0; n < 3; n++)) do strace ./client 127.0.0.1 ; done
此處代碼省略,當執行了多次之后,我們看下偵聽進程的狀態:
secer@harry: lsof -Pnp 19063
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
server 19063 root cwd DIR 8,4 4096 5685857 /data/harry/harrywork/closewaitprobe
server 19063 root rtd DIR 8,1 4096 2 /
server 19063 root txt REG 8,4 10005 5685861 /data/harry/harrywork/closewaitprobe/server
server 19063 root mem REG 8,1 132822 24291 /lib64/ld-2.4.so
server 19063 root mem REG 8,1 1570761 24350 /lib64/libc-2.4.so
server 19063 root mem REG 0,0 0 [stack] (stat: No such file or directory)
server 19063 root 0u CHR 136,12 14 /dev/pts/12
server 19063 root 1u CHR 136,12 14 /dev/pts/12
server 19063 root 2u CHR 136,12 14 /dev/pts/12
server 19063 root 3u IPv4 158668992 TCP *:5555 (LISTEN)
server 19063 root 4u sock 0,4 158668993 can't identify protocol
server 19063 root 5u sock 0,4 158671295 can't identify protocol
server 19063 root 6u sock 0,4 158671303 can't identify protocol
server 19063 root 7u sock 0,4 158671472 can't identify protocol
server 19063 root 8u sock 0,4 158671608 can't identify protocol
server 19063 root 9u sock 0,4 158672286 can't identify protocol
server 19063 root 10u sock 0,4 158674733 can't identify protocol
server 19063 root 11u sock 0,4 158674800 can't identify protocol
server 19063 root 12u sock 0,4 158674864 can't identify protocol
server 19063 root 13u sock 0,4 158706082 can't identify protocol
server 19063 root 14u sock 0,4 158706230 can't identify protocol
server 19063 root 15u sock 0,4 158713114 can't identify protocol
server 19063 root 16u sock 0,4 158713422 can't identify protocol
server 19063 root 17u sock 0,4 158716774 can't identify protocol
server 19063 root 18u sock 0,4 158721739 can't identify protocol
server 19063 root 19u IPv4 158725544 TCP 127.0.0.1:5555->127.0.0.1:32769 (CLOSE_WAIT)
server 19063 root 20u IPv4 158728291 TCP 127.0.0.1:5555->127.0.0.1:32768 (CLOSE_WAIT)
tsecer@harry:
看到了大量的不在listen也不在established狀態的socket,這個比較奇怪,所以通過TCP抓包來看下系統交互:
tsecer@harry: tcpdump -ni any host 127.0.0.1 and tcp port 5555
tcpdump: WARNING: Promiscuous mode not supported on the "any" device
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 96 bytes
21:56:01.944826 IP 127.0.0.1.32768 > 127.0.0.1.5555:
S 474258987:474258987(0) win 32767 <mss 16396,sackOK,timestamp 1148830800 0,nop,wscale 7>
21:56:01.944931 IP 127.0.0.1.5555 > 127.0.0.1.32768: .
ack 4210751241 win 256<nop,nop,timestamp 1148830800 1148699191>
21:56:01.944989 IP 127.0.0.1.32768 > 127.0.0.1.5555:
R 4210751241:4210751241(0) win 0
21:56:04.942581 IP 127.0.0.1.32768 > 127.0.0.1.5555: S 474258987:474258987(0) win 32767 <mss 16396,sackOK,timestamp 1148831550 0,nop,wscale 7>
21:56:04.942606 IP 127.0.0.1.5555 > 127.0.0.1.32768: S 479567568:479567568(0) ack 474258988 win 32767 <mss 16396,sackOK,timestamp 1148831550 1148831550,nop,wscale 7>
21:56:04.942616 IP 127.0.0.1.32768 > 127.0.0.1.5555: . ack 1 win 256 <nop,nop,timestamp 1148831550 1148831550>
21:56:04.943397 IP 127.0.0.1.32768 > 127.0.0.1.5555: F 1:1(0) ack 1 win 256 <nop,nop,timestamp 1148831550 1148831550>
21:56:04.946575 IP 127.0.0.1.5555 > 127.0.0.1.32768: . ack 2 win 256 <nop,nop,timestamp 1148831551 1148831550>
8 packets captured
24 packets received by filter
0 packets dropped by kernel
tsecer@harry: cat /proc/sys/net/ipv4/tcp_retries1
3
這里從tcpdump抓包可以看到,系統中一個客戶端的連接比較曲折,第一次發送的syn包並沒有收到對應的FIN包,而是一個序列號和自己SYN序列號相差極大的一個SEQ,然后客戶端發送RST報文,導致對方重置,established socket釋放。接下來3秒鍾之后發送第二個SYN包,這個發送是由於寫定時器超時導致的重傳包,這個超時時間就是前面從
/proc/sys/net/ipv4/tcp_retries1中看到的值。
內核中客戶端發送RST的判斷邏輯為(服務器中代碼由於比較簡單,所以省略):
系統內代碼linux-2.6.32.60\net\ipv4\tcp_input.c:
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
……
if (th->ack) {
/* rfc793:
* "If the state is SYN-SENT then
* first check the ACK bit
* If the ACK bit is set
* If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
* a reset (unless the RST bit is set, if so drop
* the segment and return)"
*
* We do not send data with SYN, so that RFC-correct
* test reduces to:
*/
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt)
goto reset_and_undo;
……
reset_and_undo:
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
return 1;
}
從tcp_rcv_synsent_state_process函數返回之后,發送RST包的代碼:
tcp_rcv_synsent_state_process-->>tcp_rcv_state_process--->>tcp_v4_do_rcv
reset:
tcp_v4_send_reset(rsk, skb);
discard:
kfree_skb(skb);
/* Be careful here. If this function gets more complicated and
* gcc suffers from register pressure on the x86, sk (in %ebx)
* might be destroyed here. This current version compiles correctly,
* but you have been warned.
*/
return 0;