0x01 緣由
昨天簡單過了下tcp層的syn的過程,其中對幾個隊列處理有點疑問,對資料如何到使用者态的過程也存在的疑問?帶着這幾個問題網上查找了相關資料,發現一位大神講解得比較清楚。傳送:http://blog.csdn.net/russell_tao/article/details/9950615 講解得比較到位,博文編寫也是我後面要改進的方向。原理已經講解得比較清楚,下面主要做一下單步跟蹤分析。
0x02 單步跟蹤分析
1.recv調用
1.1 recv調用過程
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
unsigned, flags, struct sockaddr __user *, addr,
int __user *, addr_len)
{
.......
//根據fd找對象的socket結構
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
//相關結構填充,io位址等
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_iovlen = 1;
msg.msg_iov = &iov;
iov.iov_len = size;
iov.iov_base = ubuf;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = sizeof(address);
//阻塞與非阻塞調用
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
//sock接收消息
err = sock_recvmsg(sock, &msg, size, flags);
.......
}
1.2 sock_recvmsg調用過程
int sock_recvmsg(struct socket *sock, struct msghdr *msg,
size_t size, int flags)
{
//相關io操作,這裡還不太明白
struct kiocb iocb;
struct sock_iocb siocb;
int ret;
init_sync_kiocb(&iocb, NULL);
iocb.private = &siocb;
//繼續調用
ret = __sock_recvmsg(&iocb, sock, msg, size, flags);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&iocb);
return ret;
}
1.3 sock_recvmsg調用過程
static inline int __sock_recvmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size, int flags)
{
int err;
struct sock_iocb *si = kiocb_to_siocb(iocb);
si->sock = sock;
si->scm = NULL;
si->msg = msg;
si->size = size;
si->flags = flags;
err = security_socket_recvmsg(sock, msg, size, flags);
if (err)
return err;
//根據sock回調指針情況執行,有inet\unix\netlink。tcp則調用tcp.c中tcp_recvmsg函數。這裡就是我傳送的那篇文章内容;
return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}
0x03 隊列分析
上層應用調用和底層tcp收包通過隊列進行異步緩沖等解耦。是以通過多種隊列來進行各種場景下的資料緩沖。下面根據對tcp_recvmsg的分析來處理。
/*
* 這個函數是将sock結構拷貝到使用者的緩沖區。
* 參數: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)
{
struct tcp_sock *tp = tcp_sk(sk);//對應結構
......
//鎖住socket,防止多程序并發通路TCP連接配接,告知軟中斷目前socket在程序上下文中。
lock_sock(sk);
TCP_CHECK_TIMER(sk);
//初始化errno這個錯誤碼
err = -ENOTCONN;
//sock狀态監聽狀态,還未建立連接配接,是以釋放目前處理sock。
if (sk->sk_state == TCP_LISTEN)
goto out;
//如果socket是阻塞套接字,則取出SO_RCVTIMEO作為讀逾時時間;若為非阻塞,則timeo為0。下面會看到timeo是如何生效的
timeo = sock_rcvtimeo(sk, nonblock);
/* 帶有URG的緊急資料特殊處理*/
if (flags & MSG_OOB)
goto recv_urg;
//擷取下一個要拷貝的位元組序号
//注意: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);
#ifdef CONFIG_NET_DMA //配置網絡DMA
tp->ucopy.dma_chan = NULL;
preempt_disable();
skb = skb_peek_tail(&sk->sk_receive_queue);
{
int available = 0;
if (skb)
available = TCP_SKB_CB(skb)->seq + skb->len - (*seq);
if ((available < target) &&
(len > sysctl_tcp_dma_copybreak) && !(flags & MSG_PEEK) &&
!sysctl_tcp_low_latency &&
dma_find_channel(DMA_MEMCPY)) {
preempt_enable_no_resched();
tp->ucopy.pinned_list =
dma_pin_iovec_pages(msg->msg_iov, len);
} else {
preempt_enable_no_resched();
}
}
#endif
//以下開始讀取消息
do {
u32 offset;
/* urgent data 處理*/
if (tp->urg_data && tp->urg_seq == *seq) {
if (copied)
break;
if (signal_pending(current)) {
copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;
break;
}
}
//從sk_receive_queue隊列中擷取skb
skb_queue_walk(&sk->sk_receive_queue, skb) {
//不應該有兩個recv隊列
if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
KERN_INFO "recvmsg bug: copied %X "
"seq %X rcvnxt %X fl %X\n", *seq,
TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
flags))
break;
//offset是待拷貝序号在目前這個封包中的偏移量
offset = *seq - TCP_SKB_CB(skb)->seq;
//syn偏移-1
if (tcp_hdr(skb)->syn)
offset--;
//偏移小于skb長度
if (offset < skb->len)
goto found_ok_skb;
//fin辨別
if (tcp_hdr(skb)->fin)
goto found_fin_ok; //處理skb
WARN(!(flags & MSG_PEEK), KERN_INFO "recvmsg bug 2: "
"copied %X seq %X rcvnxt %X fl %X\n",
*seq, TCP_SKB_CB(skb)->seq,
tp->rcv_nxt, flags);
}
/*backLog隊列的處理*/
if (copied >= target && !sk->sk_backlog.tail)
break;
......
/* 如果prequeue不是空的,我們必須在釋放套接字之前處理它,否則在第二次疊代時将會被打破。
* 需要更優雅的解決方案!
* 看:我們有以下(僞)隊列:
*
* 1. packets in flight --在處理的隊列
* 2. backlog -- 後備隊列
* 3. prequeue -- 處理隊列
* 4. receive_queue --接收隊列
* 每個隊列僅僅在下一個隊列為空時才能處理。
*/
if (!skb_queue_empty(&tp->ucopy.prequeue))
goto do_prequeue;
/* __ Set realtime policy in scheduler __ */
}
if (copied >= target) {
/* Do not sleep, just process backlog. */
release_sock(sk);
lock_sock(sk);
} else
sk_wait_data(sk, &timeo);
#ifdef CONFIG_NET_DMA
tp->ucopy.wakeup = 0;
#endif
if (user_recv) {
int chunk;
/* __ Restore normal policy in scheduler __ */
if ((chunk = len - tp->ucopy.len) != 0) {
NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
len -= chunk;
copied += chunk;
}
if (tp->rcv_nxt == tp->copied_seq &&
!skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
tcp_prequeue_process(sk);
if ((chunk = len - tp->ucopy.len) != 0) {
NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
len -= chunk;
copied += chunk;
}
}
}
if ((flags & MSG_PEEK) &&
(peek_seq - copied - urg_hole != tp->copied_seq)) {
if (net_ratelimit())
printk(KERN_DEBUG "TCP(%s:%d): Application bug, race in MSG_PEEK.\n",
current->comm, task_pid_nr(current));
peek_seq = tp->copied_seq;
}
continue;
found_ok_skb:
/* receive隊列的這個封包從其可以使用的偏移量offset,到總長度len之間,可以拷貝的長度為used */
used = skb->len - offset;
//len是使用者态空閑記憶體,len更小時,當然隻能拷貝len長度消息,總不能導緻記憶體溢出吧
if (len < used)
used = len;
/* 有緊急辨別資料嗎? */
if (tp->urg_data) {
u32 urg_offset = tp->urg_seq - *seq;
if (urg_offset < used) {
if (!urg_offset) {
if (!sock_flag(sk, SOCK_URGINLINE)) {
++*seq;
urg_hole++;
offset++;
used--;
if (!used)
goto skip_copy;
}
} else
used = urg_offset;
}
}
//MSG_TRUNC标志位表示不要管len這個使用者态記憶體有多大,隻管拷貝資料吧
if (!(flags & MSG_TRUNC)) {
......
{
向使用者态拷貝資料,開始複制資料到iovec
err = skb_copy_datagram_iovec(skb, offset,
msg->msg_iov, used);
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
}
}
*seq += used;
copied += used;
len -= used;
/*每次将資料複制到使用者空間時,都應該調用此函數。它計算适當的TCP接收緩沖區空間。*/
tcp_rcv_space_adjust(sk);
skip_copy:
if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
tp->urg_data = 0;
tcp_fast_path_check(sk);
}
if (used + offset < skb->len)
continue;
if (tcp_hdr(skb)->fin)
goto found_fin_ok;
if (!(flags & MSG_PEEK)) {
sk_eat_skb(sk, skb, copied_early);
copied_early = 0;
}
continue;
found_fin_ok:
/* Process the FIN. */
++*seq;
if (!(flags & MSG_PEEK)) {
sk_eat_skb(sk, skb, copied_early); //釋放相關隊列
copied_early = 0;
}
break;
} while (len > 0);
//已經裝載了接收器
if (user_recv) {
//prequeue隊列不為空則處理之
if (!skb_queue_empty(&tp->ucopy.prequeue)) {
int chunk;
tp->ucopy.len = copied > 0 ? len : 0;
tcp_prequeue_process(sk);
if (copied > 0 && (chunk = len - tp->ucopy.len) != 0) {
NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
len -= chunk;
copied += chunk;
}
}
/準備傳回使用者态,socket上不再裝載接收任務
tp->ucopy.task = NULL;
tp->ucopy.len = 0;
}
......
TCP_CHECK_TIMER(sk);
//釋放socket時,還會檢查、處理backlog隊列中的封包
release_sock(sk);
return copied;
out:
TCP_CHECK_TIMER(sk);
//釋放socket時,還會檢查、處理backlog隊列中的封包
release_sock(sk);
return err;
recv_urg:
err = tcp_recv_urg(sk, msg, len, flags);
goto out;
}
0x04 總結
此次結合大牛的分析,重新學習了一次核心态資料如何到使用者态的,且了解了幾個關鍵隊列。(學習過程,大牛勿噴,多指教)