int tcp_v4_rcv(struct sk_buff *skb)
{
... ...
//是否有進程正在使用這個套接字,將會對處理流程產生影響
//或者從代碼層面上,只要在tcp_recvmsg裏,執行lock_sock後只能進入else,而release_sock後會進入if
if (!sock_owned_by_user(sk)) {
{
//當 tcp_prequeue 返回0時,表示這個函數沒有處理該報文
if (!tcp_prequeue(sk, skb))//如果報文放在prequeue隊列,即表示延後處理,不佔用軟中斷過長時間
ret = tcp_v4_do_rcv(sk, skb);//不使用prequeue或者沒有用戶進程讀socket時(圖3進入此分支),立刻開始處理這個報文
}
} else
sk_add_backlog(sk, skb);//如果進程正在操作套接字,就把skb指向的TCP報文插入到backlog隊列(圖3涉及此分支)
... ...
}
圖1第1步裏,我們從網絡上收到了序號爲S1-S2的包。此時,沒有用戶進程在讀取套接字,因此,sock_owned_by_user(sk)會返回0。所以,tcp_prequeue方法將得到執行。簡單看看它:
static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
//檢查tcp_low_latency,默認其爲0,表示使用prequeue隊列。tp->ucopy.task不爲0,表示有進程啓動了拷貝TCP消息的流程
if (!sysctl_tcp_low_latency && tp->ucopy.task) {
//到這裏,通常是用戶進程讀數據時沒讀到指定大小的數據,休眠了。直接將報文插入prequeue隊列的末尾,延後處理
__skb_queue_tail(&tp->ucopy.prequeue, skb);
tp->ucopy.memory += skb->truesize;
//當然,雖然通常是延後處理,但如果TCP的接收緩衝區不夠用了,就會立刻處理prequeue隊列裏的所有報文
if (tp->ucopy.memory > sk->sk_rcvbuf) {
while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
//sk_backlog_rcv就是下文將要介紹的tcp_v4_do_rcv方法
sk->sk_backlog_rcv(sk, skb1);
}
} else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
//prequeue裏有報文了,喚醒正在休眠等待數據的進程,讓進程在它的上下文中處理這個prequeue隊列的報文
wake_up_interruptible(sk->sk_sleep);
}
return 1;
}
//prequeue沒有處理
return 0;
}
由於tp->ucopy.task此時是NULL,所以我們收到的第1個報文在tcp_prequeue函數裏直接返回了0,因此,將由 tcp_v4_do_rcv方法處理。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
//當TCP連接已經建立好時,是由tcp_rcv_established方法處理接收報文的
if (tcp_rcv_established(sk, skb, skb->h.th, skb->len))
goto reset;
return 0;
}
... ...
}
tcp_rcv_established方法在圖1裏,主要調用tcp_data_queue方法將報文放入隊列中,繼續看看它又幹了些什麼事:
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
//如果這個報文是待接收的報文(看seq),它有兩個出路:進入receive隊列,正如圖1;直接拷貝到用戶內存中,如圖3
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
//滑動窗口外的包暫不考慮,篇幅有限,下次再細談
if (tcp_receive_window(tp) == 0)
goto out_of_window;
//如果有一個進程正在讀取socket,且正準備要拷貝的序號就是當前報文的seq序號
if (tp->ucopy.task == current &&
tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
sock_owned_by_user(sk) && !tp->urg_data) {
//直接將報文內容拷貝到用戶態內存中,參見圖3
if (!skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)) {
tp->ucopy.len -= chunk;
tp->copied_seq += chunk;
}
}
if (eaten <= 0) {
queue_and_out:
//如果沒有能夠直接拷貝到用戶內存中,那麼,插入receive隊列吧,正如圖1中的第1、3步
__skb_queue_tail(&sk->sk_receive_queue, skb);
}
//更新待接收的序號,例如圖1第1步中,更新爲S2
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
//正如圖1第4步,這時會檢查out_of_order隊列,若它不爲空,需要處理它
if (!skb_queue_empty(&tp->out_of_order_queue)) {
//tcp_ofo_queue方法會檢查out_of_order隊列中的所有報文
tcp_ofo_queue(sk);
}
}
... ...
//這個包是無序的,又在接收滑動窗口內,那麼就如圖1第2步,把報文插入到out_of_order隊列吧
if (!skb_peek(&tp->out_of_order_queue)) {
__skb_queue_head(&tp->out_of_order_queue,skb);
} else {
... ...
__skb_append(skb1, skb, &tp->out_of_order_queue);
}
}
圖1第4步時,正是通過tcp_ofo_queue方法把之前亂序的S3-S4報文插入receive隊列的。
static void tcp_ofo_queue(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
__u32 dsack_high = tp->rcv_nxt;
struct sk_buff *skb;
//遍歷out_of_order隊列
while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
... ...
//若這個報文可以按seq插入有序的receive隊列中,則將其移出out_of_order隊列
__skb_unlink(skb, &tp->out_of_order_queue);
//插入receive隊列
__skb_queue_tail(&sk->sk_receive_queue, skb);
//更新socket上待接收的下一個有序seq
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
}
}
//參數裏的len就是read、recv方法裏的內存長度,flags正是方法的flags參數,nonblock則是阻塞、非阻塞標誌位
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
//鎖住socket,防止多進程併發訪問TCP連接,告知軟中斷目前socket在進程上下文中
lock_sock(sk);
//初始化errno這個錯誤碼
err = -ENOTCONN;
//如果socket是阻塞套接字,則取出SO_RCVTIMEO作爲讀超時時間;若爲非阻塞,則timeo爲0。下面會看到timeo是如何生效的
timeo = sock_rcvtimeo(sk, nonblock);
//獲取下一個要拷貝的字節序號
//注意:seq的定義爲u32 *seq;,它是32位指針。爲何?因爲下面每向用戶態內存拷貝後,會更新seq的值,這時就會直接更改套接字上的copied_seq
seq = &tp->copied_seq;
//當flags參數有MSG_PEEK標誌位時,意味着這次拷貝的內容,當再次讀取socket時(比如另一個進程)還能再次讀到
if (flags & MSG_PEEK) {
//所以不會更新copied_seq,當然,下面會看到也不會刪除報文,不會從receive隊列中移除報文
peek_seq = tp->copied_seq;
seq = &peek_seq;
}
//獲取SO_RCVLOWAT最低接收閥值,當然,target實際上是用戶態內存大小len和SO_RCVLOWAT的最小值
//注意:flags參數中若攜帶MSG_WAITALL標誌位,則意味着必須等到讀取到len長度的消息才能返回,此時target只能是len
target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
//以下開始讀取消息
do {
//從receive隊列取出1個報文
skb = skb_peek(&sk->sk_receive_queue);
do {
//沒取到退出當前循環
if (!skb)
break;
//offset是待拷貝序號在當前這個報文中的偏移量,在圖1、2、3中它都是0,只有因爲用戶內存不足以接收完1個報文時才爲非0
offset = *seq - TCP_SKB_CB(skb)->seq;
//有些時候,三次握手的SYN包也會攜帶消息內容的,此時seq是多出1的(SYN佔1個序號),所以offset減1
if (skb->h.th->syn)
offset--;
//若偏移量還有這個報文之內,則認爲它需要處理
if (offset < skb->len)
goto found_ok_skb;
skb = skb->next;
} while (skb != (struct sk_buff *)&sk->sk_receive_queue);
//如果receive隊列爲空,則檢查已經拷貝的字節數,是否達到了SO_RCVLOWAT或者長度len。滿足了,且backlog隊列也爲空,則可以返回用戶態了,正如圖1的第11步
if (copied >= target && !sk->sk_backlog.tail)
break;
//在tcp_recvmsg裏,copied就是已經拷貝的字節數
if (copied) {
... ...
} else {
//一個字節都沒拷貝到,但如果shutdown關閉了socket,一樣直接返回。當然,本文不涉及關閉連接
if (sk->sk_shutdown & RCV_SHUTDOWN)
break;
//如果使用了非阻塞套接字,此時timeo爲0
if (!timeo) {
//非阻塞套接字讀取不到數據時也會返回,錯誤碼正是EAGAIN
copied = -EAGAIN;
break;
}
... ...
}
//tcp_low_latency默認是關閉的,圖1、圖2都是如此,圖3則例外,即圖3不會走進這個if
if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
//prequeue隊列就是爲了提高系統整體效率的,即prequeue隊列有可能不爲空,這是因爲進程休眠等待時可能有新報文到達prequeue隊列
if (!skb_queue_empty(&tp->ucopy.prequeue))
goto do_prequeue;
}
//如果已經拷貝了的字節數超過了最低閥值
if (copied >= target) {
//release_sock這個方法會遍歷、處理backlog隊列中的報文
release_sock(sk);
lock_sock(sk);
} else
sk_wait_data(sk, &timeo);//沒有讀取到足夠長度的消息,因此會進程休眠,如果沒有被喚醒,最長睡眠timeo時間
if (user_recv) {
if (tp->rcv_nxt == tp->copied_seq &&
!skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
//接上面代碼段,開始處理prequeue隊列裏的報文
tcp_prequeue_process(sk);
}
}
//繼續處理receive隊列的下一個報文
continue;
found_ok_skb:
/* Ok so how much can we use? */
//receive隊列的這個報文從其可以使用的偏移量offset,到總長度len之間,可以拷貝的長度爲used
used = skb->len - offset;
//len是用戶態空閒內存,len更小時,當然只能拷貝len長度消息,總不能導致內存溢出吧
if (len < used)
used = len;
//MSG_TRUNC標誌位表示不要管len這個用戶態內存有多大,只管拷貝數據吧
if (!(flags & MSG_TRUNC)) {
{
//向用戶態拷貝數據
err = skb_copy_datagram_iovec(skb, offset,
msg->msg_iov, used);
}
}
//因爲是指針,所以同時更新copied_seq--下一個待接收的序號
*seq += used;
//更新已經拷貝的長度
copied += used;
//更新用戶態內存的剩餘空閒空間長度
len -= used;
... ...
} while (len > 0);
//已經裝載了接收器
if (user_recv) {
//prequeue隊列不爲空則處理之
if (!skb_queue_empty(&tp->ucopy.prequeue)) {
tcp_prequeue_process(sk);
}
//準備返回用戶態,socket上不再裝載接收任務
tp->ucopy.task = NULL;
tp->ucopy.len = 0;
}
//釋放socket時,還會檢查、處理backlog隊列中的報文
release_sock(sk);
//向用戶返回已經拷貝的字節數
return copied;
}
int sk_wait_data(struct sock *sk, long *timeo)
{
//注意,它的自動喚醒條件有兩個,要麼timeo時間到達,要麼receive隊列不爲空
rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
}
sk_wait_event也值得我們簡單看下:
#define sk_wait_event(__sk, __timeo, __condition) \
({ int rc; \
release_sock(__sk); \
rc = __condition; \
if (!rc) { \
*(__timeo) = schedule_timeout(*(__timeo)); \
} \
lock_sock(__sk); \
rc = __condition; \
rc; \
})
注意,它在睡眠前會調用release_sock,這個方法會釋放socket鎖,使得下面的第5步中,新到的報文不再只能進入backlog隊列。
void fastcall release_sock(struct sock *sk)
{
mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
spin_lock_bh(&sk->sk_lock.slock);
//這裏會遍歷backlog隊列中的每一個報文
if (sk->sk_backlog.tail)
__release_sock(sk);
//這裏是網絡中斷執行時,告訴內核,現在socket並不在進程上下文中
sk->sk_lock.owner = NULL;
if (waitqueue_active(&sk->sk_lock.wq))
wake_up(&sk->sk_lock.wq);
spin_unlock_bh(&sk->sk_lock.slock);
}
再看看__release_sock方法是如何遍歷backlog隊列的:
static void __release_sock(struct sock *sk)
{
struct sk_buff *skb = sk->sk_backlog.head;
//遍歷backlog隊列
do {
sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
bh_unlock_sock(sk);
do {
struct sk_buff *next = skb->next;
skb->next = NULL;
//處理報文,其實就是tcp_v4_do_rcv方法,上文介紹過,不再贅述
sk->sk_backlog_rcv(sk, skb);
cond_resched_softirq();
skb = next;
} while (skb != NULL);
bh_lock_sock(sk);
} while((skb = sk->sk_backlog.head) != NULL);
}
此時遍歷到S3-S4報文,但因爲它是失序的,所以從backlog隊列中移入out_of_order隊列中(參見上文說過的tcp_ofo_queue方法)。