我们平时编写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状态
*/
}