Linux网络协议栈之TCP send/recv

我们平时编写socket程序时调用了一些API,比如send()/sendto()/sendmsg()/write()/writev(),最终都会调用__sock_sendmsg(),而__sock_sendmsg中会调用xxxops->socket_sendmsg(),这是函数指针,对于TCP,这个函数就是tcp_sendmsg(),真正发送tcp报文的函数是tcp_transmit_skb()。

而对于recv()/recvfrom()/recvmsg()/read()/readv(),sys_recv()-->sys_recvfrom()-->sock_recvmsg()-->__sock_recvmsg()-->sock_common_recvmsg()-->tcp_recvmsg(),下半部接收TCP报文的函数是tcp_v4_rcv()。

本文把Linux-2.6.11.12源码中文注释版中的注释收集出来,整理在下面。如有侵权,请告知。

sys_recv()-->sys_recvfrom()-->sock_recvmsg()-->__sock_recvmsg()-->sock_common_recvmsg()-->tcp_recvmsg()

tcp_sendmsg()
{
  /* 获取套接口的锁 */
  /* 根据标志计算阻塞超时时间 */
  /* 判断是TCPF_ESTABLISHED/TCPF_CLOSE_WAIT状态才能发送消息 */
  /* 获得有效的MSS,如果支持OOB,则不能支持TSO,MSS则应当是比较小的值 */
  /* 获取待发送数据块数及数据块指针 */
  /* 处理所有待发送数据块 while*/
  /* 处理单个数据块中的所有数据 while*/
  /* 发送队列为空,前面取得的skb无效 或者 如果skb有效,但是它已经没有多余的空间复制新数据了*/
     /* 分配新的skb */
/* 根据路由网络设备的特性,确定是否由硬件执行校验和 */
/* 将SKB添加到发送队列尾部 */
  /* 要复制的数据不能大于当前段的长度(截断)*/
  /* skb线性存储区底部还有空间 */
     /* 本次只复制skb存储区底部剩余空间大小的数据量 */
     /* 从用户空间复制指定长度的数据到skb中,如果失败,则退出 (copy_from_user)*/
  /* 线性存储区底部已经没有空间了,复制到分散/聚集存储区中 */
     /* 是否在页中添加数据 */
/* 分散/聚集片断数 */
     /* 分片页页 */
/* 分片内的偏移 */
/* 目前skb中的页不能添加数据,这里判断是否能再分配页 */
   /* SKB可以提交了 */
/* 分页数量未达到上限,判断当前页是否还有空间 */
   /* 最后一个分页数据已经满,需要分配新页 */
/* 需要分配新页 */
   /* 分配新页,如果内存不足则等待内存 (睡眠)*/
/* 待复制的数据不能大于页中剩余空间 */
/* 从用户态复制数据到页中 */
/* 更新skb的分段信息 */
/* 更新页内偏移 */
/* 如果没有复制数据,则取消PSH标志 */
/* 更新发送队列最后一个包的序号 */
/* 更新skb的序号 */
/* 更新数据复制的指针 */
/* 如果所有数据已经复制完毕则退出 */
/* 如果当前skb中的数据小于mss,说明可以往里面继续复制数据。或者发送的是OOB数据,则也跳过发送过程,继续复制数据 */
/* 如果必须立即发送数据,即上次发送后产生的数据已经超过通告窗口值的一半 */
   /* 设置PSH标志后发送数据 */
/* 否则虽然不是必须发送数据,但是发送队列上只存在当前段,也将其发送出去 */
/* 最终发送报文调用函数tcp_transmit_skb()*/
}

tcp_transmit_skb()
{
  /* 根据TCP选项调整TCP首部长度 */
  /* 如果当前段是SYN段,需要特殊处理一下 */
     /* SYN段必须通告MSS,因此报头加上MSS通告选项的长度 */
/* 启用了时间戳 则报头加上时间戳标志 */
/* 处理窗口扩大因子选项 */
/* 处理SACK选项 */
/* 非SYN段,但是有SACK块 */
   /* 根据SACK块数调整TCP首部长度 */
  /* 在报文首部中加入TCP首部 */
  /* 更新TCP首部指针 */
  /* 设置报文的传输控制块 */
  /* 填充TCP首部中的数据 */
  /* 设置TCP首部的接收窗口 */
  /* 对SYN段来说,接收窗口初始值为rcv_wnd */
  /* 对其他段来说,调用tcp_select_window计算当前接收窗口的大小 */
  /* 初始化校验码和带外数据指针,带外是指加急数据,与普通数据用同一个连接,但是不用排队 */
  /* 如果发送时设置了紧急方式 且 紧急指针在报文序号开始的65535范围内 */
     /* 设置紧急指针和带外数据标志位 */
  /* 开始构建TCP首部选项 */
    /* 如果是SYN包,则调用tcp_syn_build_options构建SYN段的首部 */
/* 否则是普通包,构建普通段的首部 */
  /* 计算传输层的校验和 */
  /* 如果发送的段有ACK标志,则通知延时确认模块,递减快速发送ACK段的数量,同时停止延时确认定时器 */
  /* 发送的段有负载,则检测拥塞窗口闲置是否超时 */
  /* 调用IP层的发送函数发送报文 */
  /* 如果发送失败,则类似于接收到显式拥塞通知的处理 */
}


tcp_recvmsg()
{
  /* 首先获取套接口的锁 */
  /* LISTEN状态的套接口是不能读取数据的 */
  /* 计算超时时间 */
  /* 带外数据的处理比较复杂,特殊处理 */
    goto recv_urg:
  /* 默认情况下,是将报文从内核态读到用户态,需要更新copied_seq */
  /* 如果只查看而不取走数据,则不能更新copied_seq,后面就只更新临时变量peek_seq了 */
  /* target是本次复制数据的长度,如果指定了MSG_WAITALL,就需要读取用户指定长度的数据,否则可以只读取部分数据 */
  /* 当前遇到了带外数据 */
     /* 如果已经复制了部分数据到用户态,则退出 */
     /* 如果接收到信号,也退出 */
  /* 获取下一个待读取的段 */
     /* 如果接收队列为空,退出,处理prequeue队列和后备队列 */
/* 如果下一个段不是预期读取的段,只能退出处理prequeue队列和后备队列,实际上这不可能发生 */
/* 计算我们应当在该段的何处开始复制数据,因为上次recv调用可能已经读取了部分数据 */
/* 如果syn标志占用一个序号,因此需要调整偏移 */
/* 如果偏移还在段范围内,说明当前段是有效的,从该段中读取数据 */
   goto found_ok_skb:
/* 如果该段的数据已经读取完毕,如果fin标志,那么不能继续处理后面的数据了 */
   goto found_fin_ok:
  /* 如果数据已经读完,并且后备队列为空,直接退出 */
  /* 如果复制了部分数据,检查是否有退出事件需要处理 */
     /* 如果SOCK发生了错误 或者 套口已经关闭 或者 停止接收 或者 超时时间到 或者 接收到信号 仅仅查看数据 这些事件都导致接收过程退出 */
  /* 否则 */
     /* 如果TCP会话已经结束,收到了FIN报文 */
/* 如果有错误发生,退出 */
/* 如果需要停止接收RCV_SHUTDOWN,退出 */
/* 如果状态为TCP_CLOSE可能是连接还没有建立 */
/* 如果非阻塞读,退出 */
/* 如果接收到信号,退出 EINTR*/
  /* 检测是否有确认需要发送 */
  /* 如果数据读取完毕 */
     /* 释放锁,主要是处理后备队列 */
/* 再次获取锁 */
  /* 否则等待新数据到来,或者超时。在此期间软中断可能复制数据到用户态 */
  /* 用户睡眠期间,复制了数据到用户态 */
  /* 如果接收队列中的数据已经全部复制到用户态 并且 prequeue还有数据 */
     /* 处理prequeue队列 */
/* 如果从prequeue队列复制了数据到用户态 */
   /* 更新计数 */
  /* 继续处理待读取的段 */
  found_ok_skb:
    /* 本段中可以读取的长度 */
/* 如果可读的长度较长,则只读取用户期望读取的长度 */
/* 有带外数据 */
  /* 如果带外数据在可读数据内,表示带外数据有效 */
     /* 如果偏移为0,表示当前要读的位置正好是带外数据 */
        /* 如果带外数据不放入数据流 */
       /* 调整读取位置 */
   /* 如果调整后可读数据为0,说明没有数据可读,跳过 */
  goto skip_copy:
     /* 否则当前位置不是带外数据,则调整位置,只读到带外数据处 */
/* 如果不是截断数据,表示要将数据复制到用户态 */
  /* 将数据复制到用户态 */
/* 调整一些参数 */
/* 调整合理的TCP接收缓冲区大小 */
  skip_copy:
    /* 如果完成了带外数据的处理,则清除标志,设置首部预测标志 */
/* 如果还有数据没有复制到用户态,就不能删除这个段 */
/* 如果处理完该段,检测FIN标志 */
 goto found_fin_ok:
/* 如果是读取而不是查看报文,并且处理完本段报文,则删除它 */
/* 继续处理下一个段 */
  found_fin_ok:
    /* FIN占用一个序号,因此递增序号 */
/* 如果不是查看数据,将其从队列中删除 */
/* 收到FIN,不需要继续处理后续的段,退出 */
/* 如果prequeue队列不为空 */
  /* 处理prequeue队列 */
  /* 在处理prequeue的过程中,有数据复制到用户态 */
/* 清除task和len,表示用户当前没有读取数据。这样处理prequeue队列时就不会向用户态复制了 */
/* 再次检查是否立即发送ACK */
/* 解锁传输控制块 */
/* 返回复制的字节数 */
  recv_urg:
    /* 调用tcp_recv_urg处理带外数据  tcp_recv_urg()*/
}

tcp_v4_rcv()
{
  /* 不是发送到本机的报文直接略过 */
  /* 从报文中取得TCP头部数据,如果由于内存不足等原因导致读取失败则退出 */
  /* 如果TCP首部长度小于最小的首部长度,说明报文有异常,退出 */
  /* 根据报头中的长度字段读取完整的报头,如果失败退出 */
  /* 验证TCP首部中的校验和,如果失败则退出 */
  /* 根据TCP首部中的信息来设置TCP控制块中的值,这里要进行字节序的转换 */
  /* __tcp_v4_lookup在ehash或者bhask中查找传输控制块。 */
  /* 如果在两个hask中都没有找到,则退出 */
  /* 运行到这里,说明找到相应的传输套接口 */
  /* 套接口处于TIME_WAIT状态,不应该再接收报文了,单独处理这种情况 */
  /* 防火墙处理,如果没有通过安全策略则退出 */
  /* 如果传输控制块安全了过滤器,则只有符合过滤规则的报文才放行,不符合的都丢弃 */
  /* 马上要将报文传递到传输层,该层不关心接收报文的dev,将其设置为空 */
  /* 在软中断中对套接口加锁 */
  /* 如果用户态进程没有访问传输控制块,则进行正常接收 (tcp_v4_do_rcv)*/
  /* 否则将报文添加到后备队列中,待用户进程解锁控制块时处理 (backlog)*/
  /* 如果没有相应的传输控制块,通常给对方发送RST段 */
  /* 如果IPSec策略不允许给对方发送消息,则退出 */
  /* 如果报文被损坏,则无法向对方发送RST消息,丢弃段 */
  /* 否则发送RST段给对方 */
  /* 处理传输控制块为TIME_WAIT状态时接收到的报文 */
  /* 处理防火墙 */
  /* 检查报文长度和校验和 */
  /* 由tcp_timewait_state_process处理在TIME_WAIT和FIN_WAIT_2状态下接收到的段 */
     /* 接收到连接请求,且可接受该请求 */
   /* 根据目的地址和端口,在bhask散列表中查找对应的传输控制块 */
/* 找到控制块 */
  /* 释放tw控制块,处理正常的请求 */
/* 需要向对方发送ACK */
/* 收到无效段,需要向对方发送RST段 */
}
tcp_v4_do_rcv()
{
  /* 传输层处理TCP段的主入口 */
  /* 当连接已经建立时,用快速路径处理报文 */
  /* 验证报文长度和校验码 */
  /* 如果是侦听套口,则处理被动连接 */
     /* tcp_v4_hnd_req返回的套接字不是侦听套接字,说明已经建立了半连接 */
   /* 初始化子传输控制块,如果失败则向客户端发送rst段 */
  /**
* 其他情况由tcp_rcv_state_process处理,包含SYN消息
* 当套接口处于TCP_LISTEN,TCP_SYN_RECV,TCP_SYN_SENT,TCP_FIN_WAIT1,TCP_FIN_WAIT2,TCP_LAST_ACK,TCP_CLOSING状态
*/
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值