linux TCP傳送原始碼學習(1)--tcp_sendmsg
阿新 • • 發佈:2019-02-09
一、tcp_sendmsg()函式分析:
-
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
-
size_t size)
-
{
-
struct iovec *iov;
-
/*從通用的struct sock *sk得到struct tcp_sock *tp,其實只是一個強制型別轉換,因為strcut sock是所有其它socket型別的第一個成員,所有可以直接對指標進行強制型別轉換*/
-
struct tcp_sock *tp =
-
struct sk_buff *skb;
-
int iovlen, flags;
-
int mss_now, size_goal;
-
int sg, err, copied;
-
long timeo;
-
lock_sock(sk);
-
TCP_CHECK_TIMER(sk);
-
flags = msg->msg_flags;
-
/*設定傳送等待時間,如果設定了DONTWAIT,則timeo為0. 如果沒有該標誌,則timeo就為sock->sk_sndtimeo。
-
傳送超時時間儲存在sock結構的sk_sndtimeo成員中。*/
-
timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
-
/* Wait for a connection to finish. */
- /*TCP只在ESTABLISHED和CLOSE_WAIT這兩種狀態下,接收視窗是開啟的,才能接收資料。
-
因此如果不處於這兩種狀態,則呼叫sk_stream_wait_connect()等待建立起連線,一旦超時則跳轉到out_err處做出錯處理*/
-
if ((1 << sk->sk_state)
-
if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
-
goto out_err;
- /* This should be in poll */
- /*清除套介面傳送緩衝佇列已滿的標誌。
- struct socket ->flags一組標誌位,如下:
- SOCK_ASYNC_NOSPACE:標識該套介面的傳送佇列是否已滿。
-
SOCK_ASYNC_WAITDATA:標識應用程式通過recv呼叫時,是否在等待資料的接收。
- SOCK_NOSPACE:標識非非同步的情況下該套介面的傳送佇列是否已滿。
- SOCK_PASSCRED:用於標識是否設定了SO_PASSCRE套介面選項。
- SOCK_PASSSEC:用於標識是否設定了SO_PASSSEC選項。*/
- clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
- /*呼叫tcp_send_mss獲取當前有效mss即mss_now和資料段的最大長度即size_goal。
-
在此傳入是否標識MSG_OOB位,這是因為MSG_OOB是判斷是否支援GSO的條件之一,而緊急資料不支援GSO。
-
mss_now:當前的最大報文分段長度(Maxitum Segment Size)。
- size_goal:傳送資料報到達網路裝置時資料段的最大長度,該長度用來分割資料。TCP傳送報文時,每個SKB的大小不能超過該值。
- 在不支援GSO的情況下,size_goal就等於mss_now,而如果支援GSO,則size_goal會是MSS的整數倍。資料報傳送到網路裝置後再由網路裝置根據MSS進行分割。*/
-
mss_now = tcp_send_mss(sk, &size_goal, flags);
-
/* Ok commence sending. */
- /*獲取待發送資料塊塊數和資料指標,同時清零copied,copied是已經從使用者資料塊複製到SKB的位元組數。*/
-
iovlen = msg->msg_iovlen;
-
iov = msg->msg_iov;
-
copied = 0;
- /*在開始分段前,初始化錯誤碼為EPIPE,然後判斷此時套介面是否存在錯誤,以及該套介面是否允許傳送資料,如果有錯誤或不允許傳送資料,則跳轉到do_err處做出錯處理。*/
-
err = -EPIPE;
-
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
-
goto out_err;
- /*獲取裝置是否支援離散聚合屬性。*/
-
sg = sk->sk_route_caps & NETIF_F_SG;
- /*分段過程是由兩個迴圈來控制的,外層迴圈控制是否所有使用者資料塊都已複製完成。
- 首先獲取每個資料塊的長度及指標,同時將資料塊指標指向下一個資料塊,為複製下一個資料塊做準備。*/
-
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;
- /*max=size_goal=tp->xmit_size_goal,表示傳送資料報到達網路裝置時資料段的最大長度,該長度用來分割資料,TCP傳送報文時,每個SKB的大小不能超過該值。
- 在不支援GSO情況下,xmit_size_goal就等於MSS;而如果支援GSO,則xmit_size_goal會是MSS的整數倍。資料報傳送到網路裝置後再由網路裝置根據MSS進行分割。*/
- /*獲取傳輸控制塊傳送佇列的尾部的那個SKB,因為只有隊尾的那個SKB才有可能存在剩餘空間的。*/
-
skb = tcp_write_queue_tail(sk);
-
if (tcp_send_head(sk)) {
-
if (skb->ip_summed == CHECKSUM_NONE)//校驗和的相關知識詳見段三。
-
max = mss_now;
- /*本次迴圈copy的資料長度。max是當前SKB的最大資料長度,skb->len是當前skb的資料長度,相減得到當前skb的剩餘資料空間。*/
-
copy = max - skb->len;
-
}
- /*copy小於等於0,說明當前SKB已使用空間大於等於size_goal,則需要分配新的SKB*/
-
if (copy <= 0) {
-
new_segment:
-
/* Allocate new segment. If the interface is SG,
-
* allocate skb fitting to single page.
-
*/
- /*判斷髮送佇列中報文總長度是否已達到傳送緩衝區的上限,如果超過,則只能跳到wait_for_sndbuf處理。
-
sk_stream_memory_free中兩個引數的說明:
- sk->sk_wmem_queued:表示傳送緩衝佇列中已分配的位元組數,一般來說,分配一個struct sk_buff是用於存放一個tcp資料報,其分配位元組數應該是MSS+協議首部長度。
- 在我的實驗環境中,MSS值是1448,協議首部取最大長度 MAX_TCP_HEADER,在我的實驗環境中為224。經資料對齊處理後,最後struct sk_buff的truesize為1956。
- 也就是佇列中每分配一個struct sk_buff,sk->sk_wmem_queued的值就增加1956。
- sk->sk_rcvbuf、sk->sk_sndbuf,這兩個值分別代表每個sock的接收發送佇列的最大限制。*/
-
if (!sk_stream_memory_free(sk))
-
goto wait_for_sndbuf;
- /*開始alloc一個新的skb,alloc的大小一般都等於mss的大小,這裡是通過select_size得到的。*/
-
skb = sk_stream_alloc_skb(sk,
-
select_size(sk, sg),
-
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插入到傳送佇列的尾部。*/
-
skb_entail(sk, skb);
- /*最後初始化copy變數為傳送資料報到網路裝置時的最大資料段的長度,copy表示每次複製到skb的資料長度。*/
-
copy = size_goal;
-
max = size_goal;
-
}
-
/* Try to append data to the end of skb. */
- /*copy不能大於當前資料塊剩餘待複製的資料長度,如果大於,則需要調整copy的值。*/
-
if (copy > seglen)
-
copy = seglen;
-
/* Where to copy to? */
-
/*判斷skb的線性儲存區底部是否還有空間。*/
-
if (skb_tailroom(skb) > 0) {
-
/* We have some space in skb head. Superb! */
- /*判斷skb的線性儲存區底部是否還有空間。如果還有,則進一步測試底部剩餘空間是否小於copy,如果是則再次調整待複製資料長度copy。
- 到此為止已經計算除了本次需要複製資料的長度,接下來呼叫skb_add_data從使用者空間複製長度為copy的資料到skb中。如果複製失敗,則跳轉到do_fault處。*/
- if (copy > skb_tailroom(skb))
-
copy = skb_tailroom(skb);
-
if ((err = skb_add_data(skb, from, copy)) != 0)
-
goto do_fault;
- } else {
-
/*如果SKB線性儲存區底部已經沒有空間了,那就需要把資料複製到支援分散聚合的分頁中*/
-
/*merge標識是否在最後一個分頁中新增資料,初始化為0*/
-
int merge = 0;
-
/*獲取當前SKB的分片段數,在skb_shared_info中用nr_frags表示。*/
-
int i = skb_shinfo(skb)->nr_frags;
- /*通過巨集TCP_PAGE獲取最後一個分片的頁面page。
-
通過巨集TCP_OFF獲取已複製資料尾端在最後一個分片的頁面的頁內偏移。
- #define TCP_PAGE(sk) (sk->sk_sndmsg_page)
- #define TCP_OFF(sk) (sk->sk_sndmsg_off)
- sk_sndmsg_page:指向為本傳輸控制塊最近一次分配的頁面,通常是當前套介面傳送佇列中最後一個SKB的分片資料的最後一頁。
- sk_sndmsg_off:表示最後一頁分片的頁內偏移,新的資料可以直接從這個位置複製到該分片中。*/
-
struct page *page = TCP_PAGE(sk);
-
int off = TCP_OFF(sk);
- /*呼叫skb_can_coalesce(),判斷SKB上最後一個分散聚合頁面是否有效,即能否將資料新增到該分頁上,如果可以則設定merge標誌。*/
-
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 || !sg) {
- /*如果不能往最後一個分片內追加資料,則需要判斷分片數量是否已達到上限,如果達到上限,則說明不能再往此SKB複製資料了,需要分配新的SKB。
- 或者網路裝置不支援分散聚合I/O,則也說明不能往分片複製資料。
- 在這種情況下,對當前的TCP報文設定TCPHDR_PSH標誌,並更新pushed_seq成員,表示到pushed_seq為止都是希望能儘快傳送出去的。
-
最後跳轉到new_segment處,又開始分配新的SKB,因為資料還沒複製完。*/
-
/* 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
- /*到此處只剩下一種情況了,既不能在最後一個分頁追加資料,又不能分配新的SKB,那麼不管這個SKB是否存在分頁,資料必定