TCP段是封裝在IP數據報中傳輸的,而IP數據報的傳輸是不可靠的。因此,不能將TCP段發送出去後就不再管它們了,相反必須跟蹤它們,直到出現三種情況爲止:一是在規定時間內接收方確認已收到該段;二是發送超時,即規定時間內未收到接收方的確認;三是確定數據包已丟失,在後兩種情況下需從未接收的位置開始重新發送該數據報。
從圖中可以看出TCP傳輸控制塊中sk_write_queue字段存儲的是發送隊列雙向鏈表的表頭。而另外一個成員sk_send_head指向發送隊列中下一個要發送的數據包,該字段是用來跟蹤哪些包還未發送的,而不是用來進行發送的,如果爲空,則意味着發送隊列上的所有數據包都已發送過了。
在發送方從接收方接收到ACK段後,可擴大發送窗口,從sk_send_head開始遍歷發送隊列發送更多的段。
在TCP輸出引擎中,無論是首次發送TCP段,還是重傳,或是建立TCP連接時發送SYN段,都會調用tcp_transmit_skb()
1、在最上層的tcp_sendmsg()和tcp_sendpage()都是用來獲取數據到SKB中的,無論數據是來自用戶層還是頁面緩存,最後將套接口緩存加入到傳輸控制塊的發送隊列sk_write_queue中,並在適當的時候調用tcp_write_xmit()或tcp_push_one()盡力將這些數據報發送出去。
2、在TCP接收處理ACK段的過程中,會調用tcp_data_snd_check()來檢測發送隊列中是否還有數據包要發送,如果有,則同樣調用tcp_write_xmit()來處理髮送
3、當要重傳數據時,無論是超時重傳還是迴應收到的SACK信息,都會調用tcp_retransmit_skb()來處理重傳,而該函數最終還是調用tcp_transmit_skb()來重傳數據報。
當接收者發送回與發送隊列sk_write_queue上的SKB項對應的ACK段,此時才能從發送隊列上刪除釋放SKB。
TCP的輸出涉及以下文件:
net/ipv4/tcp.c 傳輸控制塊與應用層之間的接口實現
net/ipv4/tcp_ipv4.c 傳輸控制塊與網絡層之間的接口實現
net/ipv4/tcp_input.c TCP的輸入
net/ipv4/tcp_output.c TCP的輸出
最大段長度(MSS)
TCP提供的是一種面向連接的、可靠的字節流服務。TCP提供可靠性的一種重要的措施就是MSS。通過MSS,數據被分割成TCP認爲適合發送的數據塊,稱爲段(segment)。段不包括協議首部,只包含數據。與MSS最爲相關的一個參數就是網絡設備接口的MTU,以太網的MTU是1500B,其中扣除不帶選項的基本IP首部和基本TCP首部長度各20B,因此MSS值可達1460B。
TCP三次握手過程中可以看到,雙方都通過TCP選項通告本端能接收的MSS值,該值來源於tcp_sock結構的成員advmss,而advmss又來自路由項中的MSS度量值metrics[RTAX_MAX](參見tcp_connect_init())。路由項中的MSS度量值直接由網絡設備接口的MTU減去IP首部和TCP首部計算得到的。
/* Do all connect socket setups that can be done AF independent. */
static void tcp_connect_init(struct sock *sk)
{
struct dst_entry *dst = __sk_dst_get(sk);
struct tcp_sock *tp = tcp_sk(sk);
__u8 rcv_wscale;
/* We'll fix this up when we get a response from the other end.
* See tcp_input.c:tcp_rcv_state_process case TCP_SYN_SENT.
*/
tp->tcp_header_len = sizeof(struct tcphdr) +
(sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);
#ifdef CONFIG_TCP_MD5SIG
if (tp->af_specific->md5_lookup(sk, sk) != NULL)
tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
#endif
/* If user gave his TCP_MAXSEG, record it to clamp */
if (tp->rx_opt.user_mss)
tp->rx_opt.mss_clamp = tp->rx_opt.user_mss;
tp->max_window = 0;
tcp_mtup_init(sk);
tcp_sync_mss(sk, dst_mtu(dst));
if (!tp->window_clamp)
tp->window_clamp = dst_metric(dst, RTAX_WINDOW);
tp->advmss = dst_metric(dst, RTAX_ADVMSS);
if (tp->rx_opt.user_mss && tp->rx_opt.user_mss < tp->advmss)
tp->advmss = tp->rx_opt.user_mss;
tcp_initialize_rcv_mss(sk);
tcp_select_initial_window(tcp_full_space(sk),
tp->advmss - (tp->rx_opt.ts_recent_stamp ? tp->tcp_header_len - sizeof(struct tcphdr) : 0),
&tp->rcv_wnd,
&tp->window_clamp,
sysctl_tcp_window_scaling,
&rcv_wscale);
tp->rx_opt.rcv_wscale = rcv_wscale;
tp->rcv_ssthresh = tp->rcv_wnd;
sk->sk_err = 0;
sock_reset_flag(sk, SOCK_DONE);
tp->snd_wnd = 0;
tcp_init_wl(tp, 0);
tp->snd_una = tp->write_seq;
tp->snd_sml = tp->write_seq;
tp->snd_up = tp->write_seq;
tp->rcv_nxt = 0;
tp->rcv_wup = 0;
tp->copied_seq = 0;
inet_csk(sk)->icsk_rto = TCP_TIMEOUT_INIT;
inet_csk(sk)->icsk_retransmits = 0;
tcp_clear_retrans(tp);
}
tcp_sock成員rx_opt,爲tcp_options_received結構類型,記錄來自對端的TCP選項通告,其中user_mss是用戶通告TCP_MAXSEG選項設置的MSS上限,它和建立連接時對端SYN段中的MSS通告(RFC1122明確說明通告MSS不包含TCP和IP選項)兩者中取最小值作爲該連接的MSS上限,存儲在mss_clamp中。表示對端的MSS。如果沒有收到來自對端通告的MSS且也沒有設置user_mss,則將對端的MSS設置爲默認值536B(加上首部,允許576B的IP數據報協議)。事實上,表示對端MSS的mss_clamp其初始值就定位536(tcp_v4_connect()),收到來自對端的MSS通告後,纔對其進行修正。
與最大段長度有關的一個函數是tcp_current_mss(),用來計算當前有效的MSS,需要考慮TCP首部中的SACK選項和IP選項,以及PMTU。
/* Compute the current effective MSS, taking SACKs and IP options,
* and even PMTU discovery events into account.
*/
unsigned int tcp_current_mss(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct dst_entry *dst = __sk_dst_get(sk);
u32 mss_now;
unsigned header_len;
struct tcp_out_options opts;
struct tcp_md5sig_key *md5;
mss_now = tp->mss_cache;
if (dst) {
u32 mtu = dst_mtu(dst);
if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
mss_now = tcp_sync_mss(sk, mtu);
}
header_len = tcp_established_options(sk, NULL, &opts, &md5) +
sizeof(struct tcphdr);
/* The mss_cache is sized based on tp->tcp_header_len, which assumes
* some common options. If this is an odd packet (because we have SACK
* blocks etc) then our calculated header_len will be different, and
* we have to adjust mss_now correspondingly */
if (header_len != tp->tcp_header_len) {
int delta = (int) header_len - tp->tcp_header_len;
mss_now -= delta;
}
return mss_now;
}
sendmsg系統調用在TCP中實現
sendmsg系統調用在TCP中實現共分爲兩層---套接口層和傳輸接口層,而主要的實現在傳輸接口層中。TCP的發送工作大部分是在傳輸層接口中完成的,因此整個實現過程比較複雜,涉及從用戶空間複製數據到內核空間、分割TCP段等。
int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
size_t size)
{
struct sock *sk = sock->sk;
struct iovec *iov;
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
int iovlen, flags;
int mss_now, size_goal;
int err, copied;
long timeo;
lock_sock(sk);
TCP_CHECK_TIMER(sk);
flags = msg->msg_flags;
timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
/* Wait for a connection to finish. */
if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
goto out_err;
/* This should be in poll */
clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
mss_now = tcp_send_mss(sk, &size_goal, flags);
/* Ok commence sending. */
iovlen = msg->msg_iovlen;
iov = msg->msg_iov;
copied = 0;
err = -EPIPE;
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
goto out_err;
while (--iovlen >= 0) {
size_t seglen = iov->iov_len;
unsigned char __user *from = iov->iov_base;
iov++;
while (seglen > 0) {
int copy = 0;
int max = size_goal;
skb = tcp_write_queue_tail(sk);
if (tcp_send_head(sk)) {
if (skb->ip_summed == CHECKSUM_NONE)
max = mss_now;
copy = max - skb->len;
}
if (copy <= 0) {
new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf;
skb = sk_stream_alloc_skb(sk, select_size(sk),
sk->sk_allocation);
if (!skb)
goto wait_for_memory;
/*
* Check whether we can use HW checksum.
*/
if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
skb->ip_summed = CHECKSUM_PARTIAL;
skb_entail(sk, skb);
copy = size_goal;
max = size_goal;
}
/* Try to append data to the end of skb. */
if (copy > seglen)
copy = seglen;
/* Where to copy to? */
if (skb_tailroom(skb) > 0) {
/* We have some space in skb head. Superb! */
if (copy > skb_tailroom(skb))
copy = skb_tailroom(skb);
if ((err = skb_add_data(skb, from, copy)) != 0)
goto do_fault;
} else {
int merge = 0;
int i = skb_shinfo(skb)->nr_frags;
struct page *page = TCP_PAGE(sk);
int off = TCP_OFF(sk);
if (skb_can_coalesce(skb, i, page, off) &&
off != PAGE_SIZE) {
/* We can extend the last page
* fragment. */
merge = 1;
} else if (i == MAX_SKB_FRAGS ||
(!i &&
!(sk->sk_route_caps & NETIF_F_SG))) {
/* Need to add new fragment and cannot
* do this because interface is non-SG,
* or because all the page slots are
* busy. */
tcp_mark_push(tp, skb);
goto new_segment;
} else if (page) {
if (off == PAGE_SIZE) {
put_page(page);
TCP_PAGE(sk) = page = NULL;
off = 0;
}
} else
off = 0;
if (copy > PAGE_SIZE - off)
copy = PAGE_SIZE - off;
if (!sk_wmem_schedule(sk, copy))
goto wait_for_memory;
if (!page) {
/* Allocate new cache page. */
if (!(page = sk_stream_alloc_page(sk)))
goto wait_for_memory;
}
/* Time to copy data. We are close to
* the end! */
err = skb_copy_to_page(sk, from, skb, page,
off, copy);
if (err) {
/* If this page was new, give it to the
* socket so it does not get leaked.
*/
if (!TCP_PAGE(sk)) {
TCP_PAGE(sk) = page;
TCP_OFF(sk) = 0;
}
goto do_error;
}
/* Update the skb. */
if (merge) {
skb_shinfo(skb)->frags[i - 1].size +=
copy;
} else {
skb_fill_page_desc(skb, i, page, off, copy);
if (TCP_PAGE(sk)) {
get_page(page);
} else if (off + copy < PAGE_SIZE) {
get_page(page);
TCP_PAGE(sk) = page;
}
}
TCP_OFF(sk) = off + copy;
}
if (!copied)
TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH;
tp->write_seq += copy;
TCP_SKB_CB(skb)->end_seq += copy;
skb_shinfo(skb)->gso_segs = 0;
from += copy;
copied += copy;
if ((seglen -= copy) == 0 && iovlen == 0)
goto out;
if (skb->len < max || (flags & MSG_OOB))
continue;
if (forced_push(tp)) {
tcp_mark_push(tp, skb);
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
} else if (skb == tcp_send_head(sk))
tcp_push_one(sk, mss_now);
continue;
wait_for_sndbuf:
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
goto do_error;
mss_now = tcp_send_mss(sk, &size_goal, flags);
}
}
out:
if (copied)
tcp_push(sk, flags, mss_now, tp->nonagle);
TCP_CHECK_TIMER(sk);
release_sock(sk);
return copied;
do_fault:
if (!skb->len) {
tcp_unlink_write_queue(skb, sk);
/* It is the one place in all of TCP, except connection
* reset, where we can be unlinking the send_head.
*/
tcp_check_send_head(sk, skb);
sk_wmem_free_skb(sk, skb);
}
do_error:
if (copied)
goto out;
out_err:
err = sk_stream_error(sk, flags, err);
TCP_CHECK_TIMER(sk);
release_sock(sk);
return err;
}
Nagle算法
爲減少網絡通信的開銷,提升性能及吞吐速度,系統默認採用Nagle算法。若應用程序請求發送一批數據(量較大),那麼系統在接收了那些數據之後,可能會延遲一段時間,等待數據積累到一定程度後一起發送出去。當然如果在規定的時間內沒有新數據加入,那麼原先的數據也會被髮送出去。這樣會使得在單個TCP段內數據量增大。與之相反的則是使用多個TCP段,使每個段負載的數據量都比較少。如果是後一種情況,那麼必然會涉及一項開銷,即每個段的TCP首部都要佔用20B。如果假定只發送2B,那麼20B的首部就顯得有點多。採用Nagle算法之後,能有效地利用數據包的可用空間。該算法的另一個功能是確認消息的延遲發送。系統收到TCP數據之後,必須向對方反饋一個ACK,採用該算法後,主機會暫時等待一段時間,看是否有數據發送給對方,以便能隨發送數據一起反饋ACK,從而節省一個數據包的通信量。
但在某些情況下,這樣反而會產生不利影響。例如網絡應用通常只需發送很少量的數據,同時要求能得到極其迅速的響應,那麼再使用這種算法,反而會影響性能。Telnet便是這樣的一個典型例子。Telnet的本質是一種交互式的應用,用戶可通過它登錄一臺遠程機器,然後向其傳送命令。通常,用戶每秒中用戶只會進行少量的鍵擊,若再使用Nagle算法,便會造成響應遲鈍,甚至產生對方主機不予應答的感覺。
往返時間測量
TCP傳輸往返時間是指從發送方TCP段開始,到發送方接收到該段立即響應所耗費的傳輸時間。當接收方和發送方同時支持TCP時間戳選項時,發送記錄在TCP首部選項內的時間戳會被接收方隨響應反射回來,發送方就可以利用響應段反射的時間戳計算出發送段的即時往返傳輸時間。在接收方應答不反射時間戳的情況下,發送方利用重發隊列中非重傳響應所確認的最先數據片段的時間戳來取樣RTT。
發送方每接收一次新的確認,都會產生一個新的RTT樣本。爲了避免RTT樣本的隨機抖動,系統利用加權平均算法對樣本進行平滑。爲了迴避浮點運算,RTT的平滑值SRTT是實際RTT均值的8倍,迭代過程中SRTT收斂於8倍的RTT。
路徑MTU發現
當網絡上一臺IP主機有數據要發送給另外一臺IP主機時,數據最終被封裝成IP數據報來傳輸。最理想的情況是數據報的大小是在源主機到目的主機的路徑上無需分片的最大尺寸。這種數據報的尺寸稱作路徑MTU(PMTU),等於路徑上每一條MTU中的最小值。
PMTU實現的技術簡單,在IP首部中使用不分片位DF動態發現一條路徑的PMTU。基本思想就是源主機一開始假定一條路徑的PMTU是其已知的該路徑的第一跳的MTU,在這條路徑上發送的數據報都設置DF位。如果有數據報太大,不被路徑中的某個路由器分片就不能轉發,那麼該路由器將丟棄這個數據報,然後返回一個“需要分片,但設置了DF位”的ICMP目的不可達報文。在收到這樣一條報文後,源主機將減小其假定的該路徑PMTU。當主機對PMTU的估計值小到其發送的數據報無需分片也能支持轉發的時候,PMTU發現過程結束。