【转】高性能网络编程3----TCP消息的接收
- int tcp_v4_rcv(struct sk_buff *skb)
- {
- ... ...
- //是否有进程正在使用这个套接字,将会对处理流程产生影响
- //或者从代码层面上,只要在tcp_recvmsg里,执行lock_sock后只能进入else,而release_sock后会进入if
- if (!sock_owned_by_user(sk)) {
- {
- //当 tcp_prequeue 返回0时,表示这个函数没有处理该报文
- if (!tcp_prequeue(sk, skb))//如果报文放在prequeue队列,即表示延后处理,不占用软中断过长时间
- ret = tcp_v4_do_rcv(sk, skb);//不使用prequeue或者没有用户进程读socket时(图3进入此分支),立刻开始处理这个报文
- }
- } else
- sk_add_backlog(sk, skb);//如果进程正在操作套接字,就把skb指向的TCP报文插入到backlog队列(图3涉及此分支)
- ... ...
- }
图1第1步里,我们从网络上收到了序号为S1-S2的包。此时,没有用户进程在读取套接字,因此,sock_owned_by_user(sk)会返回0。所以,tcp_prequeue方法将得到执行。简单看看它:
- static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- //检查tcp_low_latency,默认其为0,表示使用prequeue队列。tp->ucopy.task不为0,表示有进程启动了拷贝TCP消息的流程
- if (!sysctl_tcp_low_latency && tp->ucopy.task) {
- //到这里,通常是用户进程读数据时没读到指定大小的数据,休眠了。直接将报文插入prequeue队列的末尾,延后处理
- __skb_queue_tail(&tp->ucopy.prequeue, skb);
- tp->ucopy.memory += skb->truesize;
- //当然,虽然通常是延后处理,但如果TCP的接收缓冲区不够用了,就会立刻处理prequeue队列里的所有报文
- if (tp->ucopy.memory > sk->sk_rcvbuf) {
- while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
- //sk_backlog_rcv就是下文将要介绍的tcp_v4_do_rcv方法
- sk->sk_backlog_rcv(sk, skb1);
- }
- } else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
- //prequeue里有报文了,唤醒正在休眠等待数据的进程,让进程在它的上下文中处理这个prequeue队列的报文
- wake_up_interruptible(sk->sk_sleep);
- }
- return 1;
- }
- //prequeue没有处理
- return 0;
- }
由于tp->ucopy.task此时是NULL,所以我们收到的第1个报文在tcp_prequeue函数里直接返回了0,因此,将由 tcp_v4_do_rcv方法处理。
- int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
- {
- if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
- //当TCP连接已经建立好时,是由tcp_rcv_established方法处理接收报文的
- if (tcp_rcv_established(sk, skb, skb->h.th, skb->len))
- goto reset;
- return 0;
- }
- ... ...
- }
tcp_rcv_established方法在图1里,主要调用tcp_data_queue方法将报文放入队列中,继续看看它又干了些什么事:
- static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- //如果这个报文是待接收的报文(看seq),它有两个出路:进入receive队列,正如图1;直接拷贝到用户内存中,如图3
- if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
- //滑动窗口外的包暂不考虑,篇幅有限,下次再细谈
- if (tcp_receive_window(tp) == 0)
- goto out_of_window;
- //如果有一个进程正在读取socket,且正准备要拷贝的序号就是当前报文的seq序号
- if (tp->ucopy.task == current &&
- tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
- sock_owned_by_user(sk) && !tp->urg_data) {
- //直接将报文内容拷贝到用户态内存中,参见图3
- if (!skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)) {
- tp->ucopy.len -= chunk;
- tp->copied_seq += chunk;
- }
- }
- if (eaten <= 0) {
- queue_and_out:
- //如果没有能够直接拷贝到用户内存中,那么,插入receive队列吧,正如图1中的第1、3步
- __skb_queue_tail(&sk->sk_receive_queue, skb);
- }
- //更新待接收的序号,例如图1第1步中,更新为S2
- tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
- //正如图1第4步,这时会检查out_of_order队列,若它不为空,需要处理它
- if (!skb_queue_empty(&tp->out_of_order_queue)) {
- //tcp_ofo_queue方法会检查out_of_order队列中的所有报文
- tcp_ofo_queue(sk);
- }
- }
- ... ...
- //这个包是无序的,又在接收滑动窗口内,那么就如图1第2步,把报文插入到out_of_order队列吧
- if (!skb_peek(&tp->out_of_order_queue)) {
- __skb_queue_head(&tp->out_of_order_queue,skb);
- } else {
- ... ...
- __skb_append(skb1, skb, &tp->out_of_order_queue);
- }
- }
图1第4步时,正是通过tcp_ofo_queue方法把之前乱序的S3-S4报文插入receive队列的。
- static void tcp_ofo_queue(struct sock *sk)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- __u32 dsack_high = tp->rcv_nxt;
- struct sk_buff *skb;
- //遍历out_of_order队列
- while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
- ... ...
- //若这个报文可以按seq插入有序的receive队列中,则将其移出out_of_order队列
- __skb_unlink(skb, &tp->out_of_order_queue);
- //插入receive队列
- __skb_queue_tail(&sk->sk_receive_queue, skb);
- //更新socket上待接收的下一个有序seq
- tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
- }
- }
- //参数里的len就是read、recv方法里的内存长度,flags正是方法的flags参数,nonblock则是阻塞、非阻塞标志位
- int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
- size_t len, int nonblock, int flags, int *addr_len)
- {
- //锁住socket,防止多进程并发访问TCP连接,告知软中断目前socket在进程上下文中
- lock_sock(sk);
- //初始化errno这个错误码
- err = -ENOTCONN;
- //如果socket是阻塞套接字,则取出SO_RCVTIMEO作为读超时时间;若为非阻塞,则timeo为0。下面会看到timeo是如何生效的
- timeo = sock_rcvtimeo(sk, nonblock);
- //获取下一个要拷贝的字节序号
- //注意:seq的定义为u32 *seq;,它是32位指针。为何?因为下面每向用户态内存拷贝后,会更新seq的值,这时就会直接更改套接字上的copied_seq
- seq = &tp->copied_seq;
- //当flags参数有MSG_PEEK标志位时,意味着这次拷贝的内容,当再次读取socket时(比如另一个进程)还能再次读到
- if (flags & MSG_PEEK) {
- //所以不会更新copied_seq,当然,下面会看到也不会删除报文,不会从receive队列中移除报文
- peek_seq = tp->copied_seq;
- seq = &peek_seq;
- }
- //获取SO_RCVLOWAT最低接收阀值,当然,target实际上是用户态内存大小len和SO_RCVLOWAT的最小值
- //注意:flags参数中若携带MSG_WAITALL标志位,则意味着必须等到读取到len长度的消息才能返回,此时target只能是len
- target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
- //以下开始读取消息
- do {
- //从receive队列取出1个报文
- skb = skb_peek(&sk->sk_receive_queue);
- do {
- //没取到退出当前循环
- if (!skb)
- break;
- //offset是待拷贝序号在当前这个报文中的偏移量,在图1、2、3中它都是0,只有因为用户内存不足以接收完1个报文时才为非0
- offset = *seq - TCP_SKB_CB(skb)->seq;
- //有些时候,三次握手的SYN包也会携带消息内容的,此时seq是多出1的(SYN占1个序号),所以offset减1
- if (skb->h.th->syn)
- offset--;
- //若偏移量还有这个报文之内,则认为它需要处理
- if (offset < skb->len)
- goto found_ok_skb;
- skb = skb->next;
- } while (skb != (struct sk_buff *)&sk->sk_receive_queue);
- //如果receive队列为空,则检查已经拷贝的字节数,是否达到了SO_RCVLOWAT或者长度len。满足了,且backlog队列也为空,则可以返回用户态了,正如图1的第11步
- if (copied >= target && !sk->sk_backlog.tail)
- break;
- //在tcp_recvmsg里,copied就是已经拷贝的字节数
- if (copied) {
- ... ...
- } else {
- //一个字节都没拷贝到,但如果shutdown关闭了socket,一样直接返回。当然,本文不涉及关闭连接
- if (sk->sk_shutdown & RCV_SHUTDOWN)
- break;
- //如果使用了非阻塞套接字,此时timeo为0
- if (!timeo) {
- //非阻塞套接字读取不到数据时也会返回,错误码正是EAGAIN
- copied = -EAGAIN;
- break;
- }
- ... ...
- }
- //tcp_low_latency默认是关闭的,图1、图2都是如此,图3则例外,即图3不会走进这个if
- if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
- //prequeue队列就是为了提高系统整体效率的,即prequeue队列有可能不为空,这是因为进程休眠等待时可能有新报文到达prequeue队列
- if (!skb_queue_empty(&tp->ucopy.prequeue))
- goto do_prequeue;
- }
- //如果已经拷贝了的字节数超过了最低阀值
- if (copied >= target) {
- //release_sock这个方法会遍历、处理backlog队列中的报文
- release_sock(sk);
- lock_sock(sk);
- } else
- sk_wait_data(sk, &timeo);//没有读取到足够长度的消息,因此会进程休眠,如果没有被唤醒,最长睡眠timeo时间
- if (user_recv) {
- if (tp->rcv_nxt == tp->copied_seq &&
- !skb_queue_empty(&tp->ucopy.prequeue)) {
- do_prequeue:
- //接上面代码段,开始处理prequeue队列里的报文
- tcp_prequeue_process(sk);
- }
- }
- //继续处理receive队列的下一个报文
- continue;
- found_ok_skb:
- /* Ok so how much can we use? */
- //receive队列的这个报文从其可以使用的偏移量offset,到总长度len之间,可以拷贝的长度为used
- used = skb->len - offset;
- //len是用户态空闲内存,len更小时,当然只能拷贝len长度消息,总不能导致内存溢出吧
- if (len < used)
- used = len;
- //MSG_TRUNC标志位表示不要管len这个用户态内存有多大,只管拷贝数据吧
- if (!(flags & MSG_TRUNC)) {
- {
- //向用户态拷贝数据
- err = skb_copy_datagram_iovec(skb, offset,
- msg->msg_iov, used);
- }
- }
- //因为是指针,所以同时更新copied_seq--下一个待接收的序号
- *seq += used;
- //更新已经拷贝的长度
- copied += used;
- //更新用户态内存的剩余空闲空间长度
- len -= used;
- ... ...
- } while (len > 0);
- //已经装载了接收器
- if (user_recv) {
- //prequeue队列不为空则处理之
- if (!skb_queue_empty(&tp->ucopy.prequeue)) {
- tcp_prequeue_process(sk);
- }
- //准备返回用户态,socket上不再装载接收任务
- tp->ucopy.task = NULL;
- tp->ucopy.len = 0;
- }
- //释放socket时,还会检查、处理backlog队列中的报文
- release_sock(sk);
- //向用户返回已经拷贝的字节数
- return copied;
- }
- int sk_wait_data(struct sock *sk, long *timeo)
- {
- //注意,它的自动唤醒条件有两个,要么timeo时间到达,要么receive队列不为空
- rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
- }
sk_wait_event也值得我们简单看下:
- #define sk_wait_event(__sk, __timeo, __condition) \
- ({ int rc; \
- release_sock(__sk); \
- rc = __condition; \
- if (!rc) { \
- *(__timeo) = schedule_timeout(*(__timeo)); \
- } \
- lock_sock(__sk); \
- rc = __condition; \
- rc; \
- })
注意,它在睡眠前会调用release_sock,这个方法会释放socket锁,使得下面的第5步中,新到的报文不再只能进入backlog队列。
- void fastcall release_sock(struct sock *sk)
- {
- mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
- spin_lock_bh(&sk->sk_lock.slock);
- //这里会遍历backlog队列中的每一个报文
- if (sk->sk_backlog.tail)
- __release_sock(sk);
- //这里是网络中断执行时,告诉内核,现在socket并不在进程上下文中
- sk->sk_lock.owner = NULL;
- if (waitqueue_active(&sk->sk_lock.wq))
- wake_up(&sk->sk_lock.wq);
- spin_unlock_bh(&sk->sk_lock.slock);
- }
再看看__release_sock方法是如何遍历backlog队列的:
- static void __release_sock(struct sock *sk)
- {
- struct sk_buff *skb = sk->sk_backlog.head;
- //遍历backlog队列
- do {
- sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
- bh_unlock_sock(sk);
- do {
- struct sk_buff *next = skb->next;
- skb->next = NULL;
- //处理报文,其实就是tcp_v4_do_rcv方法,上文介绍过,不再赘述
- sk->sk_backlog_rcv(sk, skb);
- cond_resched_softirq();
- skb = next;
- } while (skb != NULL);
- bh_lock_sock(sk);
- } while((skb = sk->sk_backlog.head) != NULL);
- }
此时遍历到S3-S4报文,但因为它是失序的,所以从backlog队列中移入out_of_order队列中(参见上文说过的tcp_ofo_queue方法)。
原文连接:https://blog.csdn.net/russell_tao/article/details/9950615
【转】高性能网络编程3----TCP消息的接收的更多相关文章
- 网络编程--使用TCP协议发送接收数据
package com.zhangxueliang.tcp; import java.io.IOException; import java.io.OutputStream; import java. ...
- 高性能网络编程3----TCP消息的接收
高性能网络编程3----TCP消息的接收 http://blog.csdn.net/russell_tao/article/details/9950615 http://blog.csdn.net/c ...
- 高性能网络编程2----TCP消息的发送
转 陶辉 taohui.org.cn 在上一篇中,我们已经建立好的TCP连接,对应着操作系统分配的1个套接字.操作TCP协议发送数据时,面对的是数据流.通常调用诸如send或者write方法来发送数据 ...
- 高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少
高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少 阅读(81374) | 评论(9)收藏16 淘帖1 赞3 JackJiang Lv.9 1 年前 | 前言 曾几何时我 ...
- 【转】高性能网络编程2----TCP消息的发送
在上一篇中,我们已经建立好的TCP连接,对应着操作系统分配的1个套接字.操作TCP协议发送数据时,面对的是数据流.通常调用诸如send或者write方法来发送数据到另一台主机,那么,调用这样的方法时, ...
- 【转】高性能网络编程7--tcp连接的内存使用
当服务器的并发TCP连接数以十万计时,我们就会对一个TCP连接在操作系统内核上消耗的内存多少感兴趣.socket编程方法提供了SO_SNDBUF.SO_RCVBUF这样的接口来设置连接的读写缓存,li ...
- UDP TCP 消息边界
先明确一个问题,如果定义了一个数据结构,大小是,比方说 32 个字节,然后 UDP 客户端连续向服务端发了两个包.现在假设这两个包都已经到达了服务器,那么服务端调用 recvfrom 来接收数据,并且 ...
- 一文读懂高性能网络编程中的I/O模型
1.前言 随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力.本文(和下篇<高性能网络编程(六):一文读懂高性能网络编程中的线程模型>)旨在为大家提供有用的 ...
- 【网络编程1】网络编程基础-TCP、UDP编程
网络基础知识 网络模型知识 OSI七层模型:(Open Systems Interconnection Reference Model)开放式通信系统互联参考模型,是国际标准化组织(ISO)提出的一个 ...
随机推荐
- Robotics Education and Research at Scale - A Remotely Accessible Robotics Development Platform
张宁 Robotics Education and Research at Scale - A Remotely Accessible Robotics Development Platform链接 ...
- Swift编码总结3
1.编码转换: dataString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" re ...
- java内置的四大函数式接口
一.Consumer:消费型接口(void accept(T t)) 举一个例子 public class LambdaTest { @Test public void test() { happyT ...
- SpringBoot 的拦截器
首先注册我们要有完整的一个可以开始的开发环境 先自己创建一个配置类 InterceptorConfig, 实现springboot自带的拦截器接口 WebMvcConfigurer. package ...
- 如何配置STP
一.搭建本次实验的拓扑结构 两台s5700交换机模拟核心交换,两台s3700交换机模拟接入交换机,核心上配置eth-trunk 二.开启所有交换机的stp功能 开启stp [S1]stp enab ...
- 【python小记】访问mysql数据库
题记: 最近因为工作需要,学习了python,瞬间对这个轻松快捷的语给吸引了,以前只知道js脚本是写网页的,没有想到python这个脚本语言的应用范围可以这么广泛,现在做一些简单或稍微复杂的操作,基本 ...
- python 之 前端开发( jQuery事件、动画效果、.each()、 .data())
11.58 事件 11.581 事件绑定方法与解绑 绑定事件: // 绑定方式一: $('.box1').click(function () { alert('绑定方式一') }); // 绑定方 ...
- DFS解决八皇后问题
2019-07-29 16:49:15 #include <bits/stdc++.h> using namespace std; ][]; int tot; int check(int ...
- MySQL直方图
MySQL8.0开始支持索引之外的数据分布统计信息可选项 我们知道,在DB中,优化器负责将SQL转换为很多个不同的执行计划,完了从中选择一个最优的来实际执行.但是有时候优化器选择的最终计划有可能随着D ...
- JOIN的区别
CREATE TABLE `j1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c1` varchar(20) NOT NULL DEFAU ...