linux内核中TCP接收的实现入口函数是tcp_v4_rcv
1. 数据包检查处理
一开始做一些数据包详细检查处理,一旦出错,可能导致内核挂掉
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
struct tcphdr *th;
struct sock *sk;
int ret;
struct net *net = dev_net(skb->dev);
/****************************1. 数据包正确性检查**************************************/
/*非本机数据包丢弃*/
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* Count it even if it's bad */
TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);
/*检测TCP协议头正确性*/
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
th = tcp_hdr(skb);
/*检查doff数据域正确性*/
if (th->doff < sizeof(struct tcphdr) / 4)
goto bad_packet;
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
/* An explanation is required here, I think.
* Packet length and doff are validated by header prediction,
* provided case of th->doff==0 is eliminated.
* So, we defer the checks. */
/*检查csum正确性*/
if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
goto bad_packet;
/****************************2. 保存协议头信息**************************************/
th = tcp_hdr(skb);
iph = ip_hdr(skb);
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->when = 0;
TCP_SKB_CB(skb)->flags = iph->tos;
TCP_SKB_CB(skb)->sacked = 0;
/*.....*/
}
2. 数据包处理流程
int tcp_v4_rcv(struct sk_buff *skb)
{
/*...*/
/****************************3. 查看数据段是否属于某个套接字***************************************/
/*_inet_lookup_skb函数来查看接收到的数据包是否属于某个打开的套接字,查看的依据是接收数据包的网络接口、源端口号和目的端口号。如果数据段属于某个套接字,则将sk变量设置为指向打开的套接字的数据结构,接着继续处理数据段。*/
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
/****************************4. 数据段的处理***************************************/
process:
/*如果套接字的连接状态为TIME_WAIT,以特殊的方式立即处理数据包*/
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
NET_INC_STATS_BH(net, LINUX_MIB_TCPMINTTLDROP);
goto discard_and_relse;
}
/* IPsec策略检查和网络过滤。如果内核配置使用了IPsec协议栈,则对数据包进行IPsec策略检查,此项检查由网络过滤子系统完成。如果未通过检查,则扔掉数据包。*/
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
nf_reset(skb);
if (sk_filter(sk, skb))
goto discard_and_relse;
skb->dev = NULL;
/*在开始将数据向套接字传送前,首先要获取防止并发访问套接字的锁。*/
bh_lock_sock_nested(sk);
ret = 0;
if (!sock_owned_by_user(sk)) { /*如果锁定套接字成功,则将数据段放入prequeue队列中。一旦数据段放入prequeue队列后,就由用户程序来处理数据,而不是由内核进程来处理数据。这样作为TCP提供最高的执行效率,能将核进程和用户进程之间执行现场的切换最小化。*/
#ifdef CONFIG_NET_DMA
struct tcp_sock *tp = tcp_sk(sk);
if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
tp->ucopy.dma_chan = dma_find_channel(DMA_MEMCPY);
if (tp->ucopy.dma_chan)
ret = tcp_v4_do_rcv(sk, skb);
else
#endif
{
if (!tcp_prequeue(sk, skb)) /*数据段放入prequeue队列,走fast path*/
ret = tcp_v4_do_rcv(sk, skb); /*如果放入不成功,tcp_v4_do_rcv,走slow path*/
}
} else if (unlikely(sk_add_backlog(sk, skb))) { /*如果获取套接字的保护锁不成功,说明有其他进程锁定了套接字,这时套接字不能接收其他数据段,则调用sk_add_backlog函数将输入段放入backlog queue队列中。*/
bh_unlock_sock(sk);
NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
goto discard_and_relse;
}
bh_unlock_sock(sk);
sock_put(sk);
return ret;
/*未打开的套接字,做RST处理*/
no_tcp_socket:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(NULL, skb);
}
discard_it:
/* Discard frame. */
kfree_skb(skb);
return 0;
discard_and_relse:
sock_put(sk);
goto discard_it;
/*如果套接字的状态是TIME_WAIT, 做特殊处理*/
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
&tcp_hashinfo,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
/* Fall through to ACK */
}
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
goto no_tcp_socket;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
3. Fast Path和prequeue队列的处理
Linux TCP/IP协议栈中,在TCP层有两条路径处理输入数据包:“Fast Path” 和“SlowPath”。
“Fast Path”是内核优化TCP处理输入数据包的方式。当TCP协议实例收到一个数据包后,它首先通过协议头来预定向数据包的去处:“Fast Path”或“Slow Path”。
如果将数据包放入“Fast Path”处理,则需要满足以下条件:
- 收到的数据段中包含的是数据,而不是ACK。
- 数据段是顺序传送数据中的一个完整数据段,接收顺序正确
满足以上条件的数据段会放入prequeue队列中,这时用户进程被唤醒,在prequeue队列中的数据段就由用户层的进程来处理,这个过程省略很多“Slow Path”处理中的步骤,从而加大了数据吞吐量。
tcp_prequeue函数完成将Socket Buffer放入prequeue队列的功能。
static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
/*如果用户空间有进程在等待接收数据段,ucopy.task非空*/
if (sysctl_tcp_low_latency || !tp->ucopy.task)
return 0;
__skb_queue_tail(&tp->ucopy.prequeue, skb);
tp->ucopy.memory += skb->truesize;
/*如果prequeue队列的长度>套接字接收缓冲区的长度,将skb放入backlog queue走slow path*/
if (tp->ucopy.memory > sk->sk_rcvbuf) {
struct sk_buff *skb1;
BUG_ON(sock_owned_by_user(sk));
while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
sk_backlog_rcv(sk, skb1);
NET_INC_STATS_BH(sock_net(sk),
LINUX_MIB_TCPPREQUEUEDROPPED);
}
tp->ucopy.memory = 0;
} else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
/*如果prequeue队列上有一个skb,就唤醒用户进程接收数据*/
wake_up_interruptible_sync_poll(sk_sleep(sk),
POLLIN | POLLRDNORM | POLLRDBAND);
if (!inet_csk_ack_scheduled(sk))
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
(3 * tcp_rto_min(sk)) / 4,
TCP_RTO_MAX);
}
return 1;
}
4. slow path和Backlog队列
相对于“Fast Path”处理过程,TCP的Backlog队列处理,即“Slow Path”是常规输入数据包的处理方式。
将输入数据包放入backlog queue队列的前提条件是:
- 输入数据包中包含的是数据段,不是ACK段。
- 数据段完好无损。
- 套接字缓冲区已满或套接字被别的用户进程占用。
这个过程需要的处理步骤较多,一旦数据包缓冲区放入队列,套接字就被唤醒,进程调度器(scheduler)调度用户进程,开始从Backlog queue队列中读取数据包缓冲区。
“SlowPath”的处理过程由tcp_v4_do_rcv函数完成。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
/*如果套接字是established状态,可能可以走fast path,由tcp_rcv_established完成*/
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
sock_rps_save_rxhash(sk, skb->rxhash);
TCP_CHECK_TIMER(sk);
if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
TCP_CHECK_TIMER(sk);
return 0;
}
/*检查数据包正确性*/
if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb))
goto csum_err;
/*如果收到的是一个有效的SYN,且处于坚挺状态,tcp_v4_hnd_req创建一个新的套接字*/
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
/*原套接字sk继续侦听,调用tcp_child_process函数在子套接字nsk上处理接收*/
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
sock_rps_save_rxhash(sk, skb->rxhash);
/*tcp_rcv_state_process函数处理套接字的常规状态切换。*/
TCP_CHECK_TIMER(sk);
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
TCP_CHECK_TIMER(sk);
return 0;
reset:
tcp_v4_send_reset(rsk, skb);
discard:
kfree_skb(skb);
/* Be careful here. If this function gets more complicated and
* gcc suffers from register pressure on the x86, sk (in %ebx)
* might be destroyed here. This current version compiles correctly,
* but you have been warned.
*/
return 0;
csum_err:
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
5. 用户态接收数据
当用户进程通过获得信号得知在打开的套接字上有数据等待用户进程来接收时,用户进程调用receive或read系统调用来读取套接字缓冲区中的数据。
当这些系统调用将读取的数据传送到套接字层时,转而会调用tcp_recvmsg函数来执行具体的传送操作。
tcp_recvmsg函数从打开的套接字上将数据复制到用户缓冲区。
数据包接收的顺序可以假设为3个队列:backlog队列,prequeue队列,常规接收队列。每个队列,只有在前面的队列数据包处理完后才能处理