tcp_recvmsg 函数具体解释

看了非常多网上关于tcp_recvmsg的文章,感觉解释的不太到位,或者非常多都是空口说白话,昨天分析了一下午tcp_recvmsg。感觉了解了十之八九,如今贴出来和大家分享一下。

须要背景:了解tcp三个接收队列  prequeue,backlog,receive的各自用处。

/*
 *	This routine copies from a sock struct into the user buffer.
 *
 *	Technical note: in 2.3 we work on _locked_ socket, so that
 *	tricks with *seq access order and skb->users are not required.
 *	Probably, code can be easily improved even more.
 */

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
  size_t len, int nonblock, int flags, int *addr_len)
{
 struct tcp_sock *tp = tcp_sk(sk);
 int copied = 0;
 u32 peek_seq;
 u32 *seq;
 unsigned long used;
 int err;
 int target;	 /* Read at least this many bytes */
 long timeo;
 struct task_struct *user_recv = NULL;
 int copied_early = 0;
 struct sk_buff *skb;
 u32 urg_hole = 0;

//功能:“锁住sk”,并不是真正的加锁,而是运行sk->sk_lock.owned = 1
//目的:这样软中断上下文可以通过owned 。推断该sk是否处于进程上下文。
//提供一种同步机制。
 lock_sock(sk);

 TCP_CHECK_TIMER(sk);

 err = -ENOTCONN;
 if (sk->sk_state == TCP_LISTEN)
  goto out;

//获取延迟,假设用户设置为非堵塞,那么timeo ==0000 0000 0000 0000
//假设用户使用默认recv系统调用
//则为堵塞,此时timeo ==0111 1111 1111 1111
//timeo 就2个值
 timeo = sock_rcvtimeo(sk, nonblock);

 /* Urgent data needs to be handled specially. */
 if (flags & MSG_OOB)
  goto recv_urg;

//待拷贝的下一个序列号
 seq = &tp->copied_seq;

//设置了MSG_PEEK,表示不让数据从缓冲区移除。目的是下一次调用recv函数
//仍然可以读到同样数据
 if (flags & MSG_PEEK) {
  peek_seq = tp->copied_seq;
  seq = &peek_seq;
 }

//假设设置了MSG_WAITALL。则target  ==len,即recv函数中的參数len
//假设没设置MSG_WAITALL。则target  == 1
 target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);

//大循环
 do {
  u32 offset;

  /* Are we at urgent data? Stop if we have read anything or have SIGURG pending. */
  if (tp->urg_data && tp->urg_seq == *seq) {
   if (copied)
    break;
   if (signal_pending(current)) {
    copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;
    break;
   }
  }

  /* Next get a buffer. */

//小循环
  skb_queue_walk(&sk->sk_receive_queue, skb) {
   /* Now that we have two receive queues this
    * shouldn‘t happen.
    */
   if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
        KERN_INFO "recvmsg bug: copied %X "
           "seq %X rcvnxt %X fl %X\n", *seq,
           TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
           flags))
    break;

//假设用户的缓冲区(即用户malloc的buf)长度够大。offset通常是0。

//即 “下次准备拷贝数据的序列号”==此时获取报文的起始序列号
//什么情况下offset >0呢?非常简答。假设用户缓冲区12字节,而这个skb有120字节
//那么一次recv系统调用,仅仅能获取skb中的前12个字节,下一次运行recv系统调用
//offset就是12了。<span style="font-family: Arial, Helvetica, sans-serif;">offset</span><span style="font-family: Arial, Helvetica, sans-serif;">表示从第12个字节開始读取数据,前12个字节已经读取了。</span>
//那这个"已经读取12字节"这个消息。存在哪呢?
//在*seq = &tp->copied_seq;中
   offset = *seq - TCP_SKB_CB(skb)->seq;
   if (tcp_hdr(skb)->syn)
    offset--;
   if (offset < skb->len)
    goto found_ok_skb;
   if (tcp_hdr(skb)->fin)
    goto found_fin_ok;
   WARN(!(flags & MSG_PEEK), KERN_INFO "recvmsg bug 2: "
     "copied %X seq %X rcvnxt %X fl %X\n",
     *seq, TCP_SKB_CB(skb)->seq,
     tp->rcv_nxt, flags);
  }

//运行到了这里。表明小循环中break了,既然break了,说明sk_receive_queue中
//已经没有skb能够读取了
//假设没有运行到这里说明前面的小循环中运行了goto,读到实用的skb,或者读到fin都会goto。

//没有skb能够读取,说明什么?
//可能性1:当用户第一次调用recv时,压根没有数据到来
//可能性2:skb->len一共20字节,假设用户调用一次 recv。读取12字节,再调用recv,
//读取12字节,此时skb由于上次已经被读取了12字节,仅仅剩下8字节。
//于是代码的逻辑上。再会要求获取skb,来读取剩下的8字节。

//可能性1的情况下,copied == 0。肯定不会进这个if。兴许将运行休眠
//可能性2的情况下,情况比較复杂。可能性2表明数据没有读够用户想要的len长度
//尽管进程上下文中,没有读够数据。可是可能我们在读数据的时候
//软中断把数据放到backlog队列中了,而backlog对队列中的数据也许恰好让我们读够数
//据。

//copied了数据的,copied肯定>=1,而target 是1或者len
//copied仅仅能取0(可能性1),或者0~len(可能性2)
//copied >= target 表示我们取得我们想要的数据了,何必进行休眠。直接return
//假设copied 没有达到我们想要的数据。则看看sk_backlog是否为空
//空的话,尽力了。仅仅能尝试休眠
//非空的话,另一线希望。我们去sk_backlog找找数据,看看能否够达到我们想要的
//数据大小

//我认为copied == target是会出现的,可是出现的话,也不会进如今这个流程
//,如有不正确,请各位大神指正,告诉我
//说明情况下copied == target

  /* Well, if we have backlog, try to process it now yet. */
  if (copied >= target && !sk->sk_backlog.tail)
   break;

  if (copied) {
//可能性2,拷贝了数据。可是没有复制到指定大小
   if (sk->sk_err ||
       sk->sk_state == TCP_CLOSE ||
       (sk->sk_shutdown & RCV_SHUTDOWN) ||
       !timeo ||
       signal_pending(current))
    break;
  } else {
//可能性1
   if (sock_flag(sk, SOCK_DONE))
    break;

   if (sk->sk_err) {
    copied = sock_error(sk);
    break;
   }

   if (sk->sk_shutdown & RCV_SHUTDOWN)
    break;

   if (sk->sk_state == TCP_CLOSE) {
    if (!sock_flag(sk, SOCK_DONE)) {
     /* This occurs when user tries to read
      * from never connected socket.
      */
     copied = -ENOTCONN;
     break;
    }
    break;
   }

//是否是堵塞的。不是。就return了。
   if (!timeo) {
    copied = -EAGAIN;
    break;
   }

   if (signal_pending(current)) {
    copied = sock_intr_errno(timeo);
    break;
   }
  }

  tcp_cleanup_rbuf(sk, copied);

//sysctl_tcp_low_latency 默认0tp->ucopy.task == user_recv肯定也成立

  if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
   /* Install new reader */
   if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
    user_recv = current;
    tp->ucopy.task = user_recv;
    tp->ucopy.iov = msg->msg_iov;
   }

   tp->ucopy.len = len;

   WARN_ON(tp->copied_seq != tp->rcv_nxt &&
    !(flags & (MSG_PEEK | MSG_TRUNC)));

   /* Ugly... If prequeue is not empty, we have to
    * process it before releasing socket, otherwise
    * order will be broken at second iteration.
    * More elegant solution is required!!!
    *
    * Look: we have the following (pseudo)queues:
    *
    * 1. packets in flight
    * 2. backlog
    * 3. prequeue
    * 4. receive_queue
    *
    * Each queue can be processed only if the next ones
    * are empty. At this point we have empty receive_queue.
    * But prequeue _can_ be not empty after 2nd iteration,
    * when we jumped to start of loop because backlog
    * processing added something to receive_queue.
    * We cannot release_sock(), because backlog contains
    * packets arrived _after_ prequeued ones.
    *
    * Shortly, algorithm is clear --- to process all
    * the queues in order. We could make it more directly,
    * requeueing packets from backlog to prequeue, if
    * is not empty. It is more elegant, but eats cycles,
    * unfortunately.
    */
//
   if (!skb_queue_empty(&tp->ucopy.prequeue))
    goto do_prequeue;

   /* __ Set realtime policy in scheduler __ */
  }

  if (copied >= target) {
   /* Do not sleep, just process backlog. */
   release_sock(sk);
   lock_sock(sk);
  } else
     sk_wait_data(sk, &timeo);
//在此处睡眠了,将在tcp_prequeue函数中调用wake_up_interruptible_poll唤醒

//软中断会推断用户是正在读取检查而且睡眠了,假设是的话,就直接把数据拷贝
//到prequeue队列,然后唤醒睡眠的进程。由于进程睡眠,表示没有读到想要的字节数
//此时,软中断有数据到来,直接给进程。这样进程就能以最快的速度被唤醒。

  if (user_recv) {
   int chunk;

   /* __ Restore normal policy in scheduler __ */

   if ((chunk = len - tp->ucopy.len) != 0) {
    NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
    len -= chunk;
    copied += chunk;
   }

   if (tp->rcv_nxt == tp->copied_seq &&
       !skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
    tcp_prequeue_process(sk);

    if ((chunk = len - tp->ucopy.len) != 0) {
     NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
     len -= chunk;
     copied += chunk;
    }
   }
  }
  if ((flags & MSG_PEEK) &&
      (peek_seq - copied - urg_hole != tp->copied_seq)) {
   if (net_ratelimit())
    printk(KERN_DEBUG "TCP(%s:%d): Application bug, race in MSG_PEEK.\n",
           current->comm, task_pid_nr(current));
   peek_seq = tp->copied_seq;
  }
  continue;

 found_ok_skb:
  /* Ok so how much can we use?

*/
//skb中还有多少聚聚没有拷贝。

//正如前面所说的,offset是上次已经拷贝了的,这次从offset開始接下去拷贝
  used = skb->len - offset;
//非常有可能used的大小,即skb剩余长度。依旧大于用户的缓冲区大小(len)。所以依旧
//仅仅能拷贝len长度。一般来说,用户还得运行一次recv系统调用。直到skb中的数据读完
  if (len < used)
   used = len;

  /* Do we have urgent data here?

*/
  if (tp->urg_data) {
   u32 urg_offset = tp->urg_seq - *seq;
   if (urg_offset < used) {
    if (!urg_offset) {
     if (!sock_flag(sk, SOCK_URGINLINE)) {
      ++*seq;
      urg_hole++;
      offset++;
      used--;
      if (!used)
       goto skip_copy;
     }
    } else
     used = urg_offset;
   }
  }

  if (!(flags & MSG_TRUNC)) {
   {
    //一般都会进这个if。进行数据的拷贝,把能够读到的数据,放到用户的缓冲区
    err = skb_copy_datagram_iovec(skb, offset,
      msg->msg_iov, used);
    if (err) {
     /* Exception. Bailout! */
     if (!copied)
      copied = -EFAULT;
     break;
    }
   }
  }

//更新标志位,seq 是指针,指向了tp->copied_seq
//used是我们有能力拷贝的数据大小,即已经复制到用户缓冲区的大小
//正如前面所说。假设用户的缓冲区非常小。一次recv拷贝不玩skb中的数据,
//我们须要保存已经拷贝了的大小,下次recv时。从这个大小处继续拷贝。

//所以须要更新copied_seq。
  *seq += used;
  copied += used;
  len -= used;

  tcp_rcv_space_adjust(sk);

skip_copy:
  if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
   tp->urg_data = 0;
   tcp_fast_path_check(sk);
  }

//这个就是推断我们是否拷贝完了skb中的数据,假设没有continue
//这样的情况下,len经过 len -= used; 。已经变成0,所以continue的效果相当于
//退出了这个大循环。

能够理解,你仅仅能拷贝len长度。拷贝完之后。那就return了。

//另一种情况used + offset ==  skb->len,表示skb拷贝完了。这时我们仅仅须要释放skb
//以下会讲到
  if (used + offset < skb->len)
   continue;

//看看这个数据报文是否含有fin,含有fin,则goto到found_fin_ok
  if (tcp_hdr(skb)->fin)
   goto found_fin_ok;

//运行到这里,标明used + offset ==  skb->len,报文也拷贝完了,那就把skb摘链释放
  if (!(flags & MSG_PEEK)) {
   sk_eat_skb(sk, skb, copied_early);
   copied_early = 0;
  }
//这个cintinue不一定是退出大循环,可能还会运行循环。

//假设用户设置缓冲区12字节。你skb->len长度20字节。
//第一次recv读取了12字节,skb剩下8。下一次调用recv再想读取12,
//可是仅仅能读取到这8字节了。

//此时len 变量长度为4,那么这个continue依旧在这个循环中,
//函数还是再次从do開始。使用skb_queue_walk,找skb
//假设sk_receive_queue中skb仍旧有,那么继续读,直到len == 0
//假设没有skb了,我们怎么办?我们的len还有4字节怎么办?
//这得看用户设置的recv函数堵塞与否。即和timeo变量相关了。
  continue;

 found_fin_ok:
  /* Process the FIN. */
  ++*seq;
  if (!(flags & MSG_PEEK)) {
//把skb从sk_receive_queue中摘链
   sk_eat_skb(sk, skb, copied_early);
   copied_early = 0;
  }
  break;
 } while (len > 0);

//到这里是大循环退出
//休眠过的进程,然后退出大循环 。才满足 if (user_recv) 条件
 if (user_recv) {
  if (!skb_queue_empty(&tp->ucopy.prequeue)) {
   int chunk;

   tp->ucopy.len = copied > 0 ? len : 0;

   tcp_prequeue_process(sk);

   if (copied > 0 && (chunk = len - tp->ucopy.len) != 0) {
    NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
    len -= chunk;
    copied += chunk;
   }
  }

//数据读取完成。清零
  tp->ucopy.task = NULL;
  tp->ucopy.len = 0;
 }

 /* According to UNIX98, msg_name/msg_namelen are ignored
  * on connected socket. I was just happy when found this 8) --ANK
  */

 /* Clean up data we have read: This will do ACK frames. */
//非常重要。将更新缓存,而且适当的时候发送ack
 tcp_cleanup_rbuf(sk, copied);

 TCP_CHECK_TIMER(sk);
 release_sock(sk);
 return copied;

out:
 TCP_CHECK_TIMER(sk);
 release_sock(sk);
 return err;

recv_urg:
 err = tcp_recv_urg(sk, msg, len, flags);
 goto out;
}
时间: 2024-08-08 09:40:19

tcp_recvmsg 函数具体解释的相关文章

tcp_recvmsg 函数详解

看了很多网上关于tcp_recvmsg的文章,感觉解释的不太到位,或者很多都是空口说白话,昨天分析了一下午tcp_recvmsg,感觉了解了十之八九,现在贴出来和大家分享一下. 需要背景:了解tcp三个接收队列  prequeue,backlog,receive的各自用处. /* * This routine copies from a sock struct into the user buffer. * * Technical note: in 2.3 we work on _locked_

fscanf()函数具体解释

曾经解析有规律的文件的时候要么用正則表達式,要么就是傻傻的自己敲代码来解析有规律的文件.今天突然发现c的库函数中有一个现成的能够解析有规律的文件的函数,就是fscanf()函数.哎 曾经自己做了这么多无用功,在这里具体解析一下fscanf函数: fscanf()函数(有点像正則表達式): 功 能: 从一个流中运行格式化输入,fscanf遇到空格和换行时结束,注意空格时也结束. 用 法:int fscanf(FILE *stream, char *format,[argument...]); in

C语言scanf函数详细解释(转)

函数名: scanf 功 能: 执行格式化输入 用 法: int scanf(char *format[,argument,...]); scanf()函数是通用终端格式化输入函数,它从标准输入设备(键盘) 读取输入的信息.可以读入任何固有类型的数据并自动把数值变换成适当的机内格式. 其调用格式为:      scanf("<格式化字符串>",<地址表>); scanf()函数返回成功赋值的数据项数,出错时则返回EOF. 其控制串由三类字符构成: 1.格式化说明

malloc函数具体解释

一.原型:extern void *malloc(unsigned int num_bytes); 头文件:#include <malloc.h> 或 #include <alloc.h> (注意:alloc.h 与 malloc.h 的内容是全然一致的.) 功能:分配长度为num_bytes字节的内存块 说明:假设分配成功则返回指向被分配内存的指针,否则返回空指针NULL. 当内存不再使用时,应使用free()函数将内存块释放. 举例:#include<stdio.h>

Android中SensorManager.getRotationMatrix函数原理解释

SensorManager是Android中的一个类,其有一个函数getRotationMatrix,可以计算出旋转矩阵,进而通过getOrientation求得设备的方向(航向角.俯仰角.横滚角).函数getRotationMatrix的源码如下所示,源码中虽然对该函数整体进行了解释,但是对代码中各个参数的计算没有说明,如为什么加速度的数值要和磁力计的数值做差乘.在网上各种搜索后,找到一段老外对这个问题的英文解释,很好的回答了上述问题.大意翻译(包括自己的理解)如下:加速度数值和磁力计数值均是

CreateProcess函数具体解释

CreateProcess说明:WIN32API函数CreateProcess用来创建一个新的进程和它的主线程,这个新进程执行指定的可执行文件. 函数原型:BOOL CreateProcess(    LPCTSTR lpApplicationName,            LPTSTR lpCommandLine,            LPSECURITY_ATTRIBUTES lpProcessAttributes.    LPSECURITY_ATTRIBUTES lpThreadAt

C语言printf()函数具体解释和安全隐患

一.问题描写叙述 二.进一步说明 请细致注意看,有例如以下奇怪的现象 int a=5; floatx=a; //这里转换是没有问题的.%f打印x是 5.000000 printf("%d\n",a); printf("%f\n",a); //输出为什么是0.000000? -----问题1 printf("%f\n",x); printf("%d\n",x); //输出为什么是0? -----问题2 printf("

关于内联函数的解释

今天在函数里面看到一个代码块 static inline NSString * AFImageCacheKeyFromURLRequest(NSURLRequest *request) { return [[request URL] absoluteString]; } 这么写是一个C语言函数调用的时候直接用 函数名:(参数) 方式调用 关于inline的解释 1.内联函数可减少cpu的系统开销,并且程序的整体速度将加快,但当内联函数很大时,会有相反的作用,因此一般比较小的函数才使用内联函数.

linux中fork()函数具体解释(原创!!实例解说)

 一.fork入门知识 一个进程,包含代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程差点儿全然同样的进程,也就是两个进程能够做全然同样的事,但假设初始參数或者传入的变量不同,两个进程也能够做不同的事.    一个进程调用fork()函数后,系统先给新的进程分配资源,比如存储数据和代码的空间.然后把原来的进程的全部值都拷贝到新的新进程中,仅仅有少数值与原来的进程的值不同.相当于克隆了一个自己. 我们来看一个样例: /* * fork_test.c * version