關於send和sendto阻塞和非阻塞模式的底層細節
對於sendto,Linux 核心最後會呼叫udp_sendmsg,大概的呼叫堆疊是
udp_sendmsg
security_socket_sendmsg
__sock_sendmsg
sock_sendmsg
sendto
在sento裡面,會根據socket的模式把一個標誌傳遞給核心:
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
這個flags的值在後面會有用處
udp_sendmsg的第三個引數msg儲存著等待發送的資料,即msg.msg_iov.iov_base,這個指標目前仍然是最上面使用者層傳遞進來的函式臨時變數,
udp_sendmsg裡面有大量的細節暫且不管,涉及資料拷貝和發動的是ip_append_data,而它呼叫的是__ip_append_data,ip_append_data會傳遞一個getfrag函式指標作為回撥,getfrag的目的就是執行傳送資料的拷貝。getfrag在哪裡被執行呢,繼續往下看
在__ip_append_data裡,程式碼分成2個執行分支,1
1呼叫ip_ufo_append_data,
2呼叫sock_alloc_send_skb
如果走分支2,直接呼叫sock_alloc_send_skb,sock_alloc_send_skb只有一個呼叫sock_alloc_send_pskb,sock_alloc_send_pskb是阻塞與非阻塞的關鍵所在,此函式首先呼叫timeo = sock_sndtimeo(sk, noblock);
noblock由最初的flags決定,意思是阻塞模式的話會一直等到到超時為止,如果是非阻塞則不等待,那麼在哪裡等待呢,下面一段程式碼展示了基本的邏輯
if (atomic_read(&sk->sk_wmem_alloc) >= sk->sk_sndbuf) {set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
err = -EAGAIN;
if (!timeo)
goto failure;
if (signal_pending(current))
goto interrupted;
timeo = sock_wait_for_wmem(sk, timeo);
continue;
}
意思是需要傳送的資料長度是大於傳送快取情況下,並且timeo為0,返回EAGAIN給使用者層,如果timeo非0,則呼叫sock_wait_for_wmem一直檢查傳送快取的情況,直到傳送快取足夠長或者超時。
在非阻塞情況下,sock_alloc_send_skb返回EAGAIN,會跳轉到error。而在阻塞情況下如果返回成功,後面才會呼叫getfrag執行真正的資料拷貝。
補充一下,不管阻塞還是非阻塞模式,在申請skb之前都會呼叫lock_sock,從而可能進入鎖競爭狀態,其實也不絕對的非阻塞。
lock_sock這個函式也值得研究,他既呼叫了自旋鎖,又呼叫了互斥鎖。
除了申請kbuffer存在阻塞,發現還需要研究一下真正send資料的時候存在阻塞,傳送資料會從udp->ip分層的往下逐層進行呼叫,最後到達dev,即網路驅動。裡面細節太多,主要會呼叫下面幾個函式:
ip_send_skb 》 ip_local_out 》ip_output 》ip_finish_output2 》__dev_queue_xmit
最後呼叫的是__dev_queue_xmit,這個函式比較複雜,如果想看詳細的分析可以看這裡http://shaojiashuai123456.iteye.com/blog/842236,
__dev_queue_xmit獲取邏輯裝置物件,然後判斷其是否有傳送佇列,如果有傳送佇列則呼叫__dev_xmit_skb,並依次呼叫__qdisc_run-》qdisc_restart->sch_direct_xmit->dev_hard_start_xmit
dev_hard_start_xmit 直接呼叫的是 rc = ops->ndo_start_xmit(skb, dev);
ndo_start_xmit則屬於驅動層的呼叫,典型的是ethernet,對應的函式是cvm_oct_xmit,驅動層會把這個skb放入等待發送佇列,然後直接返回NETDEV_TX_OK。
從這裡可以看到,不管阻塞還是非阻塞send,都會經歷漫長的呼叫堆疊,知道驅動層把skb放入傳送佇列,才會返回,其中會有多次對rcu鎖的申請。