1. file_operations
-
在epoll_ctl(add)中有这样的调用链:
主要涉及了file->f_op和private_data中的socket结构 tfile->f_op->poll(tfile, &epq.pt) |->sock_poll |->sock->ops->poll |->tcp_poll(应该也是类似sk->sk_prot->recvmsg???) /* 这边重要的是拿到了sk_sleep用于KSE(进程/线程)的唤醒 */ |->sock_poll_wait(file, sk->sk_sleep, wait); |->poll_wait |->p->qproc(filp, wait_address, p); /* p为&epq.pt,而且&epq.pt->qproc= ep_ptable_queue_proc*/ |-> ep_ptable_queue_proc(filp,wait_address,p);
-
recv的相关调用链:
file->f_op->read_iter(sock_read_iter) |->sock_recvmsg |->sock->ops->recvmsg |->tcp_recvmsg(sk->sk_prot->recvmsg)
大致过程是相同的,调用file的f_op中注册的勾子函数(file->f_op->read_iter / poll)。
然后从private_data中的sock结构中,调用其中的sock相关函数(sock->ops->recvmsg / poll)。
最后再从sock结构的sk结构中,调用其中的sk->sk_prot->recvmsg / poll,跳转到tcp_recvmsg / poll中。
2. 队列入队逻辑
recv中共有三个队列:
-
Receive Queue,sk->sk_backlog,主要的接收队列,在这个队列上的包都已经有序 + 进行了处理,
可以直接复制到用户空间。其他队列的sk_buff均未处理,就是没去掉head等信息,并且可能乱序;
-
Prequeue,tp->ucopy.prequeue,一般当前socket有用户在读 + 读的进程因为读取的量不够陷入睡眠时,
会将sk_buff不做处理,直接放到这个队列中;
-
Backlog,sk->sk_receive_queue,后备队列,当socket有用户在读 + 没睡眠所以已经上了锁时,
因为前两个队列正在被处理,所以数据会放到后备队列backlog中;
2.1 Receive Queue
入队情况:
-
sk->sk_lock.owned == 0 && tp->ucopy.task == null。
要么就是压根没调用tcp_recvmsg,要么是调用后,退出了tcp_recvmsg。
就是建立这个链接,但是接收方并没有调用recv(),或者上一次的调用刚好完成退出了。
-
sk->sk_lock.owned == 0 && tp->ucopy.task != null,并且socket内存超过了sk_rcvbuf时。
也就是在tcp_recvmsg中,但因为读取的数据不够,已经进入了睡眠。
这时应该把数据放入prequeue的,但是占的内存太大了,所以将prequeue的数据进去了去head
等处理放入receive queue。
如果还是过大怎么办?丢包了。
2.2 Prequeue
入队情况:
-
sk->sk_lock.owned == 0 && tp->ucopy.task != null,并且socket内存小于sk_rcvbuf时。
也就是在tcp_recvmsg中,但因为读取的数据不够,已经进入了睡眠。
原因有二:
-
主要原因,因为放入receive queue后,将会给发送方发送ack,但这时其实都还没唤醒进程。
这个时间差可能导致发送方高估了接收方的处理速度,不但加快发送速度,最后导致
接收方处理不及只能丢包了,从而影响整体效率。
-
次要原因,如果socket并没有因为正在读而上锁,那么本来的设计是会调用tcp_v4_do_rcv,
进过去head、验证等步骤,放入receive queue的。
这些都会在软中断中进行,而中断一般是希望越快处理完越好的。加了prequeue机制后,
sk_buff放入prequeue中时是不进行处理的,而等到推出中断唤醒进程后,在进程中进行处理,
这就可以一定程度上减少在软中断的执行时间;
-
2.3 Backlog
入队情况:
-
sk->sk_lock.owned ==1 && tp->ucopy.task == null
或
sk->sk_lock.owned ==1 && tp->ucopy.task != null
且
backlog还有空间。
也就是在tcp_recvmsg中,并且没有进入睡眠的情况,且backlog还有空间时。
这时候因为正在处理prequeue和receive queue,那为了同步肯定不能写这两个队列的,
所以就需要backlog,用来在这种情况接收数据包。
3. 队列出队逻辑
3.1 len和target
- target是最低水位,为系统中的SO_RCVLOWAT,最少是1字节;
- len就是用户传进去的参数,表示想要获取的数据量;
3.2 出队流程
除了prequeue导致socket内存超过sk_rcvbuf的特殊情况,一般会在tcp_recvmsg()中出队。
大致的处理顺序是:
- receive_queue;
- prequeue;
- backlog;
tcp_recvmsg()中三个队列的大致处理逻辑:
3.3 返回的几种情况
上图已经尽量画的完善,但还是不得不省去了很多细节,比如返回情况。
-
非阻塞读,会把超时时间timeo设置为0,不可能运行到睡眠阻塞的那一步。
只进行receive_queue的循环,循环完之后会直接跳出大循环,收尾之后返回;
-
阻塞读,除非遇到 (已读数据copied大于最低水位target && backlog为空)
|| (超时),否则会按上图这样尽量读取len字节。
不过copied应该是累加的,一旦大于target之后,应该不会进入睡眠,
也就不断循环到超时或者上述另一跳出情况;
4.Socket Buffer 管理
-
分配内存时是统一分配的,sk->sk_forward_alloc;
-
为了发送端和接收端不要互相影响,砍成了两半 sk->sk_rcvbuf 和sk->sk_sndbuf
-
三种设置方式
sysctl -w net.core.rmem_max=8388608 sysctl -w net.core.rmem_default=8388608 setsockopt传递 SO_RCVBUF
-
有四个队列,三个接收队列 + out_of_order队列,
前三个队列如果有很多数据在排队,由于rcvbuf大小有限制,所以out_of_order队列的空间就会被压缩,
然后导致tcp接收窗口大小减少。