概念
在应用程序调用系统调用持有sock锁的时候,协议栈把数据包加入backlog,可以在应用程序释放锁的时候处理数据包。
可见backlog中的包很快便会在应用程序上下文中处理。
而应用程序没有锁的时候,则只能尝试把数据把放到另一个prequeue队列中。
tcp_recvmsg()中并不会一直占有锁,在处理backlog的时候会主动释放锁,在阻塞等待的时候也会释放锁,这就给了数据包加入prequeue的时机。
在delack定时器中,因为要尽可能多的ack数据,也会处理prequeue队列。因为delack也需要获得锁,因此在release_sock中处理backlog队列,如果有delack被推迟执行,则很可能也会处理prequeue队列。
因此协议栈先尝试把包加入backlog中, 在尝试加入prequeue中,如果都不能则在软中断上下文中处理数据包。
|
|
- 为什么不在协议栈中直接处理?
cache的角度
prequeue和backlog都是为了在应用程序上下文去处理数据包。
因为softirq上下文和应用程序上下文之间切换,会造成cache刷新。
比如ksoftirqd和应用程序不在一个cpu上; 或是协议栈处理完后通知应用程序,应用程序被唤醒,同样会造成造成cache刷新ack的角度
如果直接在协议栈快速处理包,则很可能导致快速ack,使对方快速达到很大的发送速率,但是本地用户进程可能并不活跃,就会导致接收缓存满了,对方瞬间停止发送。 造成很明显的抖动。
因此在应用程序处理数据包和ack的发送,可以反映用户进程的实时情况。
但同时对突发的小包和需要低延迟的消息,放入prequeue中,也会造成非常不好的影响锁的角度
softirq中不能睡眠,也不应该跟应用程序抢锁, 如果tcp_recvmsg读取一大块内存,tcp_v4_rcv就需要很久才能抢到锁。
因此使用prequeue就可以避免这样的情况。
backlog队列
sock_owned_by_user(sk)会被bh_lock_sock_nested(sk)保护,因此如果sock_owned_by_user(sk)并把skb加入backlog之前,应用程序不会release_sock。
tcp_add_backlog
|
|
release_sock
|
|
prequeue队列
如果不能加入backlog队列,则尝试加入prequeue队列。
如果sysctl_tcp_low_latency=1表示系统关闭prequeue队列, 并且如果有应用程序正在recvmsg,则才会把数据包放入prequeue中。
如果prequeue队列太长,则直接在softirq上下文处理该队列。 如果是prequeue第一个包则唤醒用户进程, 并设置dack定时器,
在dack定时器中也会处理prequeue队列。
|
|
dack定时器处理prequeue
在应用程序没有加锁的时候,调用tcp_delack_timer_handler,处理prequeue,并判断是否需要发送ack
|
|
tcp_recvmsg
读完sk_receive_queue之后,如果没有读满应用程序缓存,则会处理prequeue和backlog队列,并从中读取。
另外,及时读满了缓存,也会在函数退出前,处理prequeue和backlog队列。
|
|
fast path处理
在快速路径中如果当前是应用程序上下文,且ucopy中还有剩余缓存,则直接copy到ucopy中。
否则通过tcp_queue_rcv函数存放到sk_receive_queue中
|
|
slow path处理
上面可以看到在slow path通过tcp_data_queue来处理数据
同样的在slow path如果收到是非乱序包,且在用户上下文,则尝试copy到ucopy中,否则通过tcp_queue_rcv函数存放到sk_receive_queue中