【INT的内核笔记】tcp接收端相关实现

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接收窗口大小减少。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
由于连续ARQ协议的接收端需要涉及到数据包的接收、确认、重传等操作,因此可以将其实现为一个单独的线程。 下面是一个基本的连续ARQ协议接收端实现: ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ContinuousARQReceiver { class ContinuousARQReceiver { private int expectedSeqNum = 0; private bool isRunning = false; public void Start() { isRunning = true; // 开启一个新线程来处理接收数据包的操作 Task.Factory.StartNew(() => { while (isRunning) { // 从网络层接收数据包 DataPacket packet = NetworkLayer.ReceivePacket(); if (packet.SeqNum == expectedSeqNum) { // 如果收到的数据包的序号与期望的序号一致,则将数据包交给应用层处理 ApplicationLayer.ReceivePacket(packet.Data); expectedSeqNum++; // 更新期望的序号 } else { // 如果收到的数据包的序号与期望的序号不一致,则返回确认消息,等待重传 NetworkLayer.SendPacket(new AckPacket(expectedSeqNum - 1)); } } }); } public void Stop() { isRunning = false; } } } ``` 在上面的代码中,我们使用了两个类:`DataPacket`和`AckPacket`。其中,`DataPacket`类表示数据包,包含了数据和序号等信息;`AckPacket`类表示确认消息,包含了确认的序号信息。 在实现过程中,我们使用了一个`expectedSeqNum`变量来表示期望的数据包序号。在接收到数据包时,如果其序号与期望的序号一致,则将数据包交给应用层处理,并更新期望的序号。如果序号不一致,则返回确认消息,等待重传。同时,我们使用了一个`isRunning`变量来表示线程是否正在运行,以便在需要停止接收端时可以终止线程。 在实际应用中,我们还需要根据实际情况进行一些优化,比如设置超时重传时间、设置接收窗口等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值