TCP协议Fast Path

1、Fast Path

Linux TCP/IP协议栈中,TCP曾有两条路径处理输入数据包:"Fast Path"、"Slow Path",Fast Path是内核优化TCP处理输入数据包方式,他是根据协议头来预定数据包的去向,Fast Path处理的条件是:

(1)、收到的数据段中包含的是数据,不是ACK。

(2)、数据段是顺序传送数据中的一个完整数据段,接受顺序正确。

(3)、收到数据段的套接字状态是ESTABLISHED。

满足以上条件数据段会加入到prequeue队列中,只是用户程序会被唤醒,比Slow Path处理省略了很多步骤,提高数据包的接受效率,prequeue队列定义在struct ucopy数据结构中。

	/* Data for direct copy to user */
	struct {
		struct sk_buff_head	prequeue;	    //prequeue队列
		struct task_struct	*task;			//打开套接字的应用程序
		struct iovec		*iov;			//数据存放缓冲区结构
		int			memory;				    //prequeu队列中数据长度
		int			len;                    //prequeue队列上缓冲区个数
#ifdef CONFIG_NET_DMA				//DMA处理
		/* members for async copy */
		struct dma_chan		*dma_chan;
		int			wakeup;
		struct dma_pinned_list	*pinned_list;
		dma_cookie_t		dma_cookie;
#endif
	} ucopy;

2、Fast Path的初始化

Fast Path的初始化函数是tcp_prequeue_init,其实就是初始化struct ucopy结构体,struct ucopy结构体是struct tcp_sock的一个元素,所以Fast Path的初始化是在应用层打开AF_INET地址族上SOCK_STREAM类型套接字是调用tcp_v4_init_sock初始化套接字是调用,以下是tcp_prequeu_init函数:

static inline void tcp_prequeue_init(struct tcp_sock *tp)
{
	tp->ucopy.task = NULL;	//应用层处理数据包的进程
	tp->ucopy.len = 0;		//prequeue队列上缓冲区个数
	tp->ucopy.memory = 0;	//prequeue队列上数据包长度
	skb_queue_head_init(&tp->ucopy.prequeue);	//队列初始化
#ifdef CONFIG_NET_DMA
	tp->ucopy.dma_chan = NULL;
	tp->ucopy.wakeup = 0;
	tp->ucopy.pinned_list = NULL;
	tp->ucopy.dma_cookie = 0;
#endif
}

3、prequeue队列处理

将数据包加入到prequeue队列的函数是tcp_prequeue,加入prequeu队列的前提是应用层有进程打开了套接字在等待接受数据,通过struct ucopy数据域的task来判断应用层是否有套接字等待接受数据,加入prequeue队列后要更新队列中数据包的长度memory,如果应用层没有打开的套接字等待接受数据就返回0,加入prequeue队列失败。

static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);

	//task域为空说明用户进程没
	//有打开的套接字等待接受数据就返回0
	
	if (sysctl_tcp_low_latency || !tp->ucopy.task)
		return 0;
	//将数据包加入到prequeue队列中
	__skb_queue_tail(&tp->ucopy.prequeue, skb);
	//更新prequeue队列中数据包的长度
	tp->ucopy.memory += skb->truesize;

...

}

将数据段加入到prequeue队列后要判断队列中数据包的总长度是否大于接受缓冲区的长度,如果prequeue队列中的数据包中长度已经大于接受缓冲区长度,就要调用sk_backlog_rcv函数将prequeue队列中所有数据段转移到backlog queue队列中,由Slow Path路径函数处理,当prequeue队列中数据段转移完毕,将memory域置为0。

...

//如果prequeu队列长度超过接受缓冲区长度就
	//将preuque队列中的数据包加入到 Slow Path队列中处理
	if (tp->ucopy.memory > sk->sk_rcvbuf) {
		struct sk_buff *skb1;

		BUG_ON(sock_owned_by_user(sk));

		//循环将prequeue队列中数据通过sk_backlog_rcv加入到backlog queue队列中
		while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
			sk_backlog_rcv(sk, skb1);
			NET_INC_STATS_BH(sock_net(sk),
					 LINUX_MIB_TCPPREQUEUEDROPPED);
		}
		//prequeue队列长度置为0
		tp->ucopy.memory = 0;
		//prequeue队列中只要有一个数据包就要唤醒用户进程
...

当prequeue队列中数据段长度没有小于接受缓冲区就唤醒用户进程接受数据包,tcp_prequeue返回1表示加入prequeue队列成功,返回0表示加入失败。

以下是完整代码:

static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);

	//task域为空说明用户进程没
	//有打开的套接字等待接受数据就返回0
	
	if (sysctl_tcp_low_latency || !tp->ucopy.task)
		return 0;
	//将数据包加入到prequeue队列中
	__skb_queue_tail(&tp->ucopy.prequeue, skb);
	//更新prequeue队列中数据包的长度
	tp->ucopy.memory += skb->truesize;

	//如果prequeu队列长度超过接受缓冲区长度就
	//将preuque队列中的数据包加入到 Slow Path队列中处理
	if (tp->ucopy.memory > sk->sk_rcvbuf) {
		struct sk_buff *skb1;

		BUG_ON(sock_owned_by_user(sk));

		//循环将prequeue队列中数据通过sk_backlog_rcv加入到backlog queue队列中
		while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
			sk_backlog_rcv(sk, skb1);
			NET_INC_STATS_BH(sock_net(sk),
					 LINUX_MIB_TCPPREQUEUEDROPPED);
		}
		//prequeue队列长度置为0
		tp->ucopy.memory = 0;
		//prequeue队列中只要有一个数据包就要唤醒用户进程
	} else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
		//唤醒用户进程接受数据包
		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;
}

猜你喜欢

转载自blog.csdn.net/City_of_skey/article/details/84641308
今日推荐