第七章 tcp发送(传输层)--基于Linux3.10

2023-11-12

由第五章可知,sock_recvmsg和tcp_sendmsg用于tcp层和应用层的接口,由第四章可知,tcp_v4_rcv和tcp_tarnsmit_skb是传输层和网络层之间的接口,现在来看看tcp_sendmsg是如何到tcp_tarnsmit_skb,tcp_v4_rcv又是如何到sock_recvmsg的。


图7.1 套接字发送

sys_send的参数意义如下,fd是套接字ID,buff是要发送的内容,len是要发送的长度。

net/socket.c

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len, unsigned int, flags)
{
return sys_sendto(fd, buff, len, flags, NULL, 0);
}

flags的参数意义如表7-1: 

flags

说明

recv

send

 MSG_DONTROUTE

绕过路由表查找 

 

  •

 MSG_DONTWAIT

仅本操作非阻塞 

  •    

  •

 MSG_OOB

发送或接收带外数据

  •

  •

 MSG_PEEK

窥看外来消息

  •

 

 MSG_WAITALL

等待所有数据 

  •

 

char snd_buff[BUFFER_SIZE] = "tcp/ip protocal stack" ;
send(sock_fd,snd_buff,BUFFER_SIZE,0);

sys_sendto的参数fd是sock_fd,之前的应用程序中创建的,buff对应的是snd_buff,len对应的是BUFFER_SIZE,flags对应就是0,addr是NULL,addr_len是0。

这里的buff是用户空间的地址,内核空间和用户空间的交互需要使用copy_from_user和copy_from_user接口。

net/socket.c

1754 SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
1755         unsigned int, flags, struct sockaddr __user *, addr,
1756         int, addr_len)
1757 {
1758     struct socket *sock;
...
1761     struct msghdr msg;
1762     struct iovec iov;
 //根据套接字ID,查找应用程序socket()调用第6章的sys_sock()创建的套接字。
1767     sock = sockfd_lookup_light(fd, &err, &fput_needed);...
1771     iov.iov_base = buff;//用户空间的地址,被记录于这个字段了。
1772     iov.iov_len = len;  //此用于记录用户空间需要传递数据的字节数,对应这里就是BUFFER_SIZE。
1773     msg.msg_name = NULL; //如果查看之一文章,message在四层模型中位于应用层和传输层之间。后面的发送将使用msg结构体。
1774     msg.msg_iov = &iov;  //将地址信息存入msg结构体中。
1775     msg.msg_iovlen = 1;
1776     msg.msg_control = NULL;
1777     msg.msg_controllen = 0;
1778     msg.msg_namelen = 0;
...
1788     msg.msg_flags = flags;
1789     err = sock_sendmsg(sock, &msg, len);
...
1794     return err;
1795 }
sys_sendto将有价值的信息都保存了msg中,接着调用了sock_sendmsg函数,该函数的第一个参数是创建的套接字,第二个参数包含了要发送数据的一些信息,最后一个参数是待发送数据的长度,对于32为机型,该长度总是小于等于32位无符号数所能表示的最大长度。 msg 的各字段:

struct msghdr {
void *msg_name;/* Socket name,上例中其赋值为了NULL*/
int msg_namelen;/* Length of name ,长度被赋值为0了*/
struct iovec *msg_iov;/* Data blocks,这里存放了用户空间待发送数据的首地址和发送数据的字节数*/
__kernel_size_tmsg_iovlen;/* Number of blocks,发送的block数,只有一个,这里赋值等于1*/
void *msg_control;/* Per protocol magic (eg BSD file descriptor passing),协议相关控制信息,NULL */
__kernel_size_tmsg_controllen;/* Length of cmsg list ,长度同样为NULL*/
unsigned int  msg_flags; /*这个flag使用的是socket传递进来的参数 0*/
};

sock_sendmsg()是对__sock_sendmsg_nosec()函数的封装,

//net/socket.c

 616 static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
 617                        struct msghdr *msg, size_t size)
 618 {
 619     struct sock_iocb *si = kiocb_to_siocb(iocb);
 620 
 621     si->sock = sock;
 622     si->scm = NULL;
 623     si->msg = msg;
 624     si->size = size;
 625 
 626     return sock->ops->sendmsg(iocb, sock, msg, size);
 627 }
 628 
 629 static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,
 630                  struct msghdr *msg, size_t size)
 631 {
 632     int err = security_socket_sendmsg(sock, msg, size);
 633 
 634     return err ?: __sock_sendmsg_nosec(iocb, sock, msg, size);
 635 }
 636 
 637 int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
 638 {
 639     struct kiocb iocb;     //io控制块,每个IO请求都会对应一个该结构体。
 640     struct sock_iocb siocb; //套接字的io控制块,每个套接字IO请求会对应一个该结构体。
 641     int ret;
 642 
 643     init_sync_kiocb(&iocb, NULL); //该函数的工作见下文
 644     iocb.private = &siocb; //将套接字IO控制块,存放在私有字段。
//调用上面629行的函数,632行安全检查,目前只是个框架,无实质内容,检查符合安全后调用634行的__sock_sendmsg_nosec进行发送。
 645     ret = __sock_sendmsg(&iocb, sock, msg, size);
 646     if (-EIOCBQUEUED == ret)
 647         ret = wait_on_sync_kiocb(&iocb);
 648     return ret;
 649 }

__sock_sendmsg_nosec函数参数如下:

iocb:io控制块,在sock_sendmsg()中定义;

sock:对应根源是应用程序创建的套接字;

msg:存放的是用户空间待发送的数据地址和以字节计数的长度。

size:待发送数据的字节数。

 626的函数具体设置在第五章中提过了,是inet_sendmsg()函数。

//include/linux/aio.h
 74 static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
 75 {    
 76     *kiocb = (struct kiocb) {
 77             .ki_users = ATOMIC_INIT(1), //引用计数设置成一。
 78             .ki_ctx = NULL, //设置成0,表示是同步IO操作。
 79             .ki_filp = filp,  //filp传递的参数是NULL。
 80             .ki_obj.tsk = current, //描述当前进程的结构体
 81         };
 82 }

//net/ipv4/af_inet.c,该函数的参数意义参考上面__sock_sendmsg_nosec()。

 758 int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
 759          size_t size)
 760 {
 763     sock_rps_record_flow(sk); //软中断均衡(多核绑定)
 770     return sk->sk_prot->sendmsg(iocb, sk, msg, size);
 771 }

770行调用的函数在第五章提到过该函数,对于tcp协议调用的是tcp_sendmsg()函数。

net/ipv4/tcp.c

1016 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
1017         size_t size)
1018 {
1019     struct iovec *iov;
1020     struct tcp_sock *tp = tcp_sk(sk);
1021     struct sk_buff *skb;
1022     int iovlen, flags, err, copied = 0;
1023     int mss_now = 0, size_goal, copied_syn = 0, offset = 0;
1024     bool sg;
1025     long timeo;
1026 
1027     lock_sock(sk);
1028 
1029     flags = msg->msg_flags;
//如果设置了fastopen标志,则在发送SYN同步包时,也会发送一部分数据。
1030     if (flags & MSG_FASTOPEN) {
1031         err = tcp_sendmsg_fastopen(sk, msg, &copied_syn);
1032         if (err == -EINPROGRESS && copied_syn > 0)
1033             goto out;
1034         else if (err)
1035             goto out_err;
1036         offset = copied_syn;
1037     }
//该值是发送等待超时值,如果设置了MSG_DONTWAIT标志,则是非阻塞IO,发送完立即返回,否则则会等待一个超时值。
1039     timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
1040 
 /* 等待tcp建立连接,TCPF_ESTABLISHED 和TCPF_CLOSE_WAIT 均位于tcp已建立连接(connect函数会完成
  *三次握手)状态,对于TCP Fast Open模式,可以在连接完全建立前前发送数据包
 * 等待的时长是1039行获得的参数。  */
1045     if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
1046         !tcp_passive_fastopen(sk)) {
		  //不处在可发送情况下,则需要等待timeo时间,以便建立连接
1047         if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
1048             goto do_error;
1049     }
//tp->repair是指tcp连接收到了破坏,需要进行修复,这种情况在虚拟机或者LXC被切到新的物理设备上,可能使用的NICs
//改变,这就需要对tcp的收发队列进行处理,以使tcp连接不会因为切换而断开。
1051     if (unlikely(tp->repair)) {
1052         if (tp->repair_queue == TCP_RECV_QUEUE) {
1053             copied = tcp_send_rcvq(sk, msg, size);
1054             goto out;
1055         }
1057         err = -EINVAL;
1058         if (tp->repair_queue == TCP_NO_QUEUE) //如果设置了需要repair,但是队列又什么也没有,则出错
1059             goto out_err;
1062     }
1063 
1064     /* This should be in poll */
1065     clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
1066 
 /***获取mss值,根据pmtu和接收方通告窗口大小设置mss值,另外还要考虑自身NICs是否支持GSO,如果支持,则tcp
**向IP发送的数据包文大小将是MSS的整数倍,最大64K。
****/
1067     mss_now = tcp_send_mss(sk, &size_goal, flags);
1068 
1069     /* Ok commence sending. */
1070     iovlen = msg->msg_iovlen; //用户空间传输数据的字节长度
1071     iov = msg->msg_iov;  //用户空间待传递数据的用户空间地址。
1072     copied = 0;  //已经拷贝的数据字节长度
1073 
1074     err = -EPIPE;
1075     if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
1076         goto out_err;
//向量IO( scatter/gather) ,可以从一个stream向多个buffer写,或者从多个bufer向一个stream中写。
1078     sg = !!(sk->sk_route_caps & NETIF_F_SG);
 //判断应用程序要发送的数据是否全部发送了,如果没有则执行while循环体内的代码。
1080     while (--iovlen >= 0) {
1081         size_t seglen = iov->iov_len;
1082         unsigned char __user *from = iov->iov_base;
1083 
1084         iov++;
1085         if (unlikely(offset > 0)) {  /* Skip bytes copied in SYN */
1086             if (offset >= seglen) {
1087                 offset -= seglen;
1088                 continue;
1089             }
1090             seglen -= offset;
1091             from += offset;
1092             offset = 0;
1093         }
1094 
1095         while (seglen > 0) {
/*max=size_goal=tp->xmit_size_goal,表示发送数据报到达网络设备时数据段的最大长度,该长度用来分割数据,TCP发送报文时,每个SKB的大小不能超过该值。在不支持GSO或TSO情况下,xmit_size_goal就等于MSS;而如果支持GSO,则xmit_size_goal会是MSS的整数倍。数据报发送到网络设备后再由NICs根据MSS进行分割。*/

1096             int copy = 0;
1097             int max = size_goal;
//获取传输控制块发送队列的尾部的那个SKB,因为只有队尾的那个SKB才有可能存在剩余空间的
1099             skb = tcp_write_queue_tail(sk);
//判断sk_send_head节点上待发送的sk_buff是否为空,sk_buff在第二章中有过叙述。
1100             if (tcp_send_head(sk)) {
 //CHECKSUM_NONE表示csum值无意义,通常需要tcp层自己校验,但多数网卡有硬件校验功能。
1101                 if (skb->ip_summed == CHECKSUM_NONE)
1102                     max = mss_now;
1103                 copy = max - skb->len; //获得有效载荷(payload)
1104             }
 //如果copy小于零说明,传输的数据长度超出了tcp允许的长度,这时需要进行分片操作。
1106             if (copy <= 0) {
1107 new_segment:
1108                 /* Allocate new segment. If the interface is SG,
1109                  * allocate skb fitting to single page.
1110                  */
1111                 if (!sk_stream_memory_free(sk)) //判断发送缓冲区剩余空闲有没有
1112                     goto wait_for_sndbuf;
1113
1114                 skb = sk_stream_alloc_skb(sk,
1115                               select_size(sk, sg),
1116                               sk->sk_allocation);
1117                 if (!skb)
1118                     goto wait_for_memory;
1119 
1120                 /*
1121                  * Check whether we can use HW checksum.
1122                  */
1123                 if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
1124                     skb->ip_summed = CHECKSUM_PARTIAL;
1125 
1126                 skb_entail(sk, skb);
1127                 copy = size_goal;
1128                 max = size_goal;
1129             }
1130 
1131              /* Try to append data to the end of skb,预留填充以符合包长度规定 */
1132             if (copy > seglen)
1133                 copy = seglen;
1134 
1135             /* Where to copy to? */
//返回由sk_stream_alloc()在sk_buff末尾分配的空间。即sk_buff的线性存储区底部是否还有空间
1136             if (skb_availroom(skb) > 0) {
1137                 /* We have some space in skb head. Superb! */
1138                 copy = min_t(int, copy, skb_availroom(skb));
1139                 err = skb_add_data_nocache(sk, skb, from, copy); /如果还有,则将数据由用户空间进行复制到skb中。
1140                 if (err)
1141                     goto do_fault;
1142             } else { //如果没有空间,则将数据复制到scatter/gather IO类型的页中。
1143                 bool merge = true; //标识最后一个页中是否有数据。
1144                 int i = skb_shinfo(skb)->nr_frags; //获取分片的数量
1145                 struct page_frag *pfrag = sk_page_frag(sk); //获得cache中的分片页
 /*在分片列表(frags)中使用原有分片(返回相应分片的指针)或分配新页来存放数据,如果不成功则等待存储空间*/
1147                 if (!sk_page_frag_refill(sk, pfrag))
1148                     goto wait_for_memory;
 /*
     * 如果传输控制块(sock)中的缓存页pfrag,不是当前skb->shared_info中的最后一个分片(分散聚集IO页面)所在的页面,则直接使用该页面,
     * 将其添加 到分片列表(分散聚集IO页面数组)中,否则说明传输控制块(sock)中的缓存页pfrag就是分散聚集IO页面的最后一个页面, 则直接向其中拷贝数据即可。
1150                 if (!skb_can_coalesce(skb, i, pfrag->page,
1151                               pfrag->offset)) {
1152                     if (i == MAX_SKB_FRAGS || !sg) {
1153                         tcp_mark_push(tp, skb);
1154                         goto new_segment;
1155                     }
1156                     merge = false;
1157                 }
1158 
1159                 copy = min_t(int, copy, pfrag->size - pfrag->offset);
/*拷贝数据至skb中非线性区分片(分散聚集IO页面)中*/
1161                 if (!sk_wmem_schedule(sk, copy))
1162                     goto wait_for_memory;
1163 
1164                 err = skb_copy_to_page_nocache(sk, from, skb,
1165                                    pfrag->page,
1166                                    pfrag->offset,
1167                                    copy);
1168                 if (err)
1169                     goto do_error;
1170 
1171                 /* Update the skb. */
1172                 if (merge) {
  /*增加分片大小*/
1173                     skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
1174                 } else {
/*如果是复制到一个全新的页面分段中,则需要更新的有关分段信息就会多一些,如分段数据的长度、页内偏移、分段数量等。调用skb_fill_page_desc()来完成。如果标识最近一次分配页面的sk_sndmsg_page不为空,则增加对该页面的引用。否则说明复制了数据的页面是新分配的,且没有使用完,在增加对该页面的引用的同时,还需更新sk_sndmsg_page的值。如果新分配的页面已使用完,就无须更新sk_sndmsg_page的值了,因为如果SKB未超过段上限,那么下次必定还会分配新的页面,因此在此处就省去了对off+copy=PAGE_SIZE这条分支的处理。*/

1175                     skb_fill_page_desc(skb, i, pfrag->page,
1176                                pfrag->offset, copy);
1177                     get_page(pfrag->page);
1178                 }
1179                 pfrag->offset += copy;
1180             }
 /*如果复制的数据长度为零,则取消TCPHDR_PSH标志,将意味着数据不会被发送*/
1182             if (!copied)
1183                 TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH;
/*更新发送队列中的最后一个序号write_seq,以及数据包的最后一个序列end_seq,初始化gso分段数gso_segs。*/
1185             tp->write_seq += copy;
1186             TCP_SKB_CB(skb)->end_seq += copy;
1187             skb_shinfo(skb)->gso_segs = 0;
/*更新指向数据源的指针和已复制字节数。*/
1189             from += copy;
1190             copied += copy;
/*如果所有数据已全部复制到SKB中,则跳转到out处理。*/
1191             if ((seglen -= copy) == 0 && iovlen == 0)
1192                 goto out;
 /*如果当前SKB中的数据小于max,说明还可以往里填充数据,或者发送的是带外数据(MSG_OOB,紧急),则跳过以下发送过程,继续复制数据到SKB*/
1194             if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
1195                 continue;

/*检查是否必须立即发送,即检查自上次发送后产生的数据是否已超过对方曾经通告过的最大窗口值的一半。如果必须立即发送,则设置TCPHDR_PSH标志后调用__tcp_push_pending_frames(),在发送队列上从sk_send_head开始把SKB发送出去。*/
1197             if (forced_push(tp)) {
1198                 tcp_mark_push(tp, skb);
1199                 __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
1200             } else if (skb == tcp_send_head(sk))
 /*如果没有必要立即发送,且发送队列上只存在这个段,则调用tcp_push_one()只发送当前段。*/
1201                 tcp_push_one(sk, mss_now);
1202             continue;
/*套接口的发送缓存是有大小限制的,当发送队列中的数据段总长度超过发送缓冲区的长度上限时,就不能再分配SKB了,只能等待。设置SOCK_NOSPACE标志,表示套接口发送缓冲区已满。*/
1204 wait_for_sndbuf:
1205             set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
/*跳到这里,意味着内存分配失败*/
1206 wait_for_memory:
/*虽然分配SKB失败,但是如果之前有数据从用户空间复制过来,则调用tcp_push()将其发送出去。
            其中第三个参数中去掉MSG_MORE标志,表示本次发送没有更多的数据了。
            因为分配SKB失败,因此可以加上TCPHDR_PSH标志,第五个参数使用nagle算法,可能会推迟发送。*/
1207             if (copied)
1208                 tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
 /*调用sk_stream_wait_memory()进入睡眠,等待内存空闲的信号,如果在超时时间内没有得到该信号,则跳转到do_error处执行。*/
1210             if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
1211                 goto do_error;
 /*等待内存未超时,有空闲内存可用。睡眠后,MSS有可能发生了变化,所以重新获取当前的MSS和TSO分段段长,然后继续循环复制数据。*/
1213             mss_now = tcp_send_mss(sk, &size_goal, flags);
1214         }
1215     }
/*发送过程中正常的退出。*/
1217 out:
1218     if (copied)
/*如果已有复制的数据,则调用tcp_push()将其发送出去,是否立即发送取决于nagle算法。*/
1219         tcp_push(sk, flags, mss_now, tp->nonagle);
1220     release_sock(sk);
1221     return copied + copied_syn;
/*在复制数据异常时进入到这里。*/
1223 do_fault:
1224     if (!skb->len) {
1225         tcp_unlink_write_queue(skb, sk);
1226         /* It is the one place in all of TCP, except connection
1227          * reset, where we can be unlinking the send_head.
1228          */
1229         tcp_check_send_head(sk, skb);
1230         sk_wmem_free_skb(sk, skb);
1231     }
1232 /*如果已复制了部分数据,那么即使发生了错误,也可以发送数据包,因此跳转到out处*/
1233 do_error:
1234     if (copied + copied_syn)
1235         goto out;
/*如果没有复制数据,则调用sk_stream_error()来获取错误码。然后对传输层控制块解锁后返回错误码。*/
1236 out_err:
1237     err = sk_stream_error(sk, flags, err);
1238     release_sock(sk);
1239     return err;
1240 }              

第1220行和1222行都会调用tcp_write_xmit()发送数据。这里走1222行的路线

void tcp_push_one(struct sock *sk, unsigned int mss_now)
{
struct sk_buff *skb = tcp_send_head(sk);

tcp_write_xmit(sk, mss_now, TCP_NAGLE_PUSH, 1, sk->sk_allocation);
}

tcp_write_xmit的参数意义如下:

sk:源于应用程序,其使用AF_INET创建的socket套接字

mss_now:maximumsegment size,参考tso和gso后给出的值。

TCP_NAGLE_PUSH:nagle算法标志,起初用于解决拥塞;该标志表示当一个数据段不足MSS时,其会推迟这个数据段的发送,直到数据填充完一个MSS。

push_one:标志要发送的packet数,当其>0时,会确保至少发送一个packet。

gfp:分配内存时的标志
1811 static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
1812                int push_one, gfp_t gfp)
1813 {
1814     struct tcp_sock *tp = tcp_sk(sk);
1815     struct sk_buff *skb;
1816     unsigned int tso_segs, sent_pkts;
1817     int cwnd_quota;
1818     int result;
1819 
1820     sent_pkts = 0;        //记录发送的数据包的个数,初始值为0
1821     //push_one用于记录要发送报文的个数,对于只发送一个报文则跳过if语句。
1822     if (!push_one) {
1823         /* Do MTU probing. */
1824         result = tcp_mtu_probe(sk);  //要发送多个的话,会检查MTU值,这个MTU值会影响分片操作。
1825         if (!result) {
1826             return false;
1827         } else if (result > 0) {
1828             sent_pkts = 1;
1829         }
1830     }
1831 
1832     while ((skb = tcp_send_head(sk))) {  //while循环用于检测发送队列头部是否有sk_buffer需要发送。
1833         unsigned int limit;
1834 
1835       //tso/gso字段用于在数据传递到IP层之前进行分片。
1836         tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
1837         BUG_ON(!tso_segs);
1838 
1839         if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE)
1840             goto repair; /* Skip network transmission */
 //cwnd_quota是拥塞控制窗口中可以发送的segment数。如果cwnd_quota=0表示在发送的segment已经填满了拥塞控制窗//口,不能在发送了。
1842         cwnd_quota = tcp_cwnd_test(tp, skb);
1843         if (!cwnd_quota) {   //如果不能在发送了
1844             if (push_one == 2)
1845                 /* Force out a loss probe pkt. */
1846                 cwnd_quota = 1;       //如果设置push_one == 2,那么强制cwnd_quota =1,后面会发送至少一个数据包。
1847             else
1848                 break; //跳出循环,不发送数据了。
1849         }
1850 
1851         if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))      //检测是否SKB至少第一个segment位于发送窗口中,否,则跳出while循环。
1852             break;
1853 
1854         if (tso_segs == 1) {       //对于无tso/gso情况,即未分片。
1855             if (unlikely(!tcp_nagle_test(tp, skb, mss_now,         //unlikely预示着tcp_nagle_test返回值是true,true的返回值意味着Nagle拥塞控制算法允许发送该数据。
1856                              (tcp_skb_is_last(sk, skb) ?
1857                               nonagle : TCP_NAGLE_PUSH))))
1858                 break;
1859         } else {
1860             if (!push_one && tcp_tso_should_defer(sk, skb))  //多个skb时,需要计算是否延迟发送
1861                 break;
1862         }
1863 
1864         /* TSQ : sk_wmem_alloc accounts skb truesize,
1865          * including skb overhead. But thats OK.
1866          */
1867         if (atomic_read(&sk->sk_wmem_alloc) >= sysctl_tcp_limit_output_bytes) {  //发送数据是否大于用户使用sysctl设置/proc输出允许的最大字节数?
1868             set_bit(TSQ_THROTTLED, &tp->tsq_flags); 
1869             break;
1870         }
1871         limit = mss_now;
1872         if (tso_segs > 1 && !tcp_urg_mode(tp)) //存在分片,且非urgent模式,使用MSS计算发送数据的限制。
1873             limit = tcp_mss_split_point(sk, skb, mss_now,
1874                             min_t(unsigned int,
1875                               cwnd_quota,
1876                               sk->sk_gso_max_segs));
1877 
1878         if (skb->len > limit && 
1879             unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))     //如果发送数据大于上面计算的limit值,其启用tso_fragment()进行分片操作。
1880             break; 
 /* 以上6行:根据条件,可能需要对SKB中的报文进行分段处理,分段的报文包括两种: 
         * 一种是普通的用MSS分段的报文,另一种则是TSO分段的报文。 
         能否发送报文主要取决于两个条件:一是报文需完全在发送窗口中,而是拥塞窗口未满。 
        第一种报文,应该不会再分段了,因为在tcp_sendmsg()中创建报文的SKB时已经根据MSS处理了,而第二种报文,则一般情况下都会大于MSS,因为通过TSO分段的段有可能大于拥塞窗口的剩余空间,如果是这样,就需要以发送窗口和拥塞窗口的最小值作为段长对报文再次分段。

1881 
1882         TCP_SKB_CB(skb)->when = tcp_time_stamp;  //时间戳跟新。
1883 
1884         if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) //发送数据
1885             break;
1886 
1887 repair:
1888         /* Advance the send_head.  This one is sent out.
1889          * This call will increment packets_out.
1890          */
1891         tcp_event_new_data_sent(sk, skb);  //更新统计,启动重传定时器。
1892 
1893         tcp_minshall_update(tp, mss_now, skb);//更新struct tcp_sock中的snd_sml字段,用于表示是否启用Nagle算法。
1894         sent_pkts += tcp_skb_pcount(skb);
... 
1911 }

第1884行注释的比较简单,该函数将构建数据的tcp头,并将其由tcp传递到Ip层,函数定义同样位于tcp_output.c文件中。该函数参数的意义;

sk:关联到应用程序创建的套接字。

sk_buff:存放的是发送数据信息

clone_it:是否clone传递进来的数据报。上面设置了1,为传递。

gfp_mask:申请内存的标志

 828 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
 829                 gfp_t gfp_mask)
 830 {
 831     const struct inet_connection_sock *icsk = inet_csk(sk);
 832     struct inet_sock *inet;
 833     struct tcp_sock *tp;
 834     struct tcp_skb_cb *tcb;
 835     struct tcp_out_options opts;
 836     unsigned int tcp_options_size, tcp_header_size;
 837     struct tcp_md5sig_key *md5;
 838     struct tcphdr *th;
...
 843     /* 如果拥塞控制需要时间戳,则必须在复制前获得时间戳;
 844      *并不是所有拥塞算法都会用到时间戳,TCP_CONG_RTT_STAMP,高精度RTT,Round-Trip Time,传输延迟
 845      */
 846     if (icsk->icsk_ca_ops->flags & TCP_CONG_RTT_STAMP)
 847         __net_timestamp(skb);
 848 
 849     if (likely(clone_it)) {  //传递进来的参数指定是否需要复制报文
 850         const struct sk_buff *fclone = skb + 1;
 851 
 852         if (unlikely(skb->fclone == SKB_FCLONE_ORIG &&
 853                  fclone->fclone == SKB_FCLONE_CLONE))
 854             NET_INC_STATS_BH(sock_net(sk),
 855                      LINUX_MIB_TCPSPURIOUS_RTX_HOSTQUEUES);
 856 
 857         if (unlikely(skb_cloned(skb)))
 858             skb = pskb_copy(skb, gfp_mask); //执行copy操作
 859         else
 860             skb = skb_clone(skb, gfp_mask);  //执行clone操作
 861         if (unlikely(!skb))
 862             return -ENOBUFS;
 863     }
 864    /*获取INET层和TCP层的传输控制块、skb中的TCP私有数据块。*/
 865     inet = inet_sk(sk);
 866     tp = tcp_sk(sk);
 867     tcb = TCP_SKB_CB(skb);
 868     memset(&opts, 0, sizeof(opts));
 869
/*根据TCP选项重新调整TCP首部的长度。*/
    /*判断当前TCP报文是否是SYN段,因为有些选项只能出现在SYN报文中,需做特别处理。*/

 870     if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
 871         tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
 872     else
 873         tcp_options_size = tcp_established_options(sk, skb, &opts,
 874                                &md5); 
/*tcp首部的总长度等于可选长度加上struct tcphdr。*/
 875     tcp_header_size = tcp_options_size + sizeof(struct tcphdr); 
 876 
/*如果已发出但未确认的数据包数目为零,则只初始化拥塞控制,并开始跟踪该连接的RTT。*/
 877     if (tcp_packets_in_flight(tp) == 0)
 878         tcp_ca_event(sk, CA_EVENT_TX_START);
 879 
 880     /* if no packet is in qdisc/device queue, then allow XPS to select
 881      * another queue.
 882      */
 883     skb->ooo_okay = sk_wmem_alloc_get(sk) == 0;
 884 /*调用skb_push()在数据部分的头部添加TCP首部,长度即为之前计算得到的那个tcp_header_size,实际上是把data指针往上移。*/
 885     skb_push(skb, tcp_header_size);
 886     skb_reset_transport_header(skb);
 887 
 888     skb_orphan(skb);  //孤儿一个Buffer,如果skb有解析函数,则会调用它自带的解析函数,孤儿并不意味着buffer不存在,意味着不再为先前所有者所有,意味着可以修改一些字段//
//更新sk_buff 类型的skb的相关字段
 889     skb->sk = sk;
 890     skb->destructor = (sysctl_tcp_limit_output_bytes > 0) ?
 891               tcp_wfree : sock_wfree;
 892     atomic_add(skb->truesize, &sk->sk_wmem_alloc);
 893 
 894     /* Build TCP header and checksum it.构建tcp头并做校验 */
 895     th = tcp_hdr(skb);
 896     th->source      = inet->inet_sport;
 897     th->dest        = inet->inet_dport;
 898     th->seq         = htonl(tcb->seq);
 899     th->ack_seq     = htonl(tp->rcv_nxt);
 900     *(((__be16 *)th) + 6)   = htons(((tcp_header_size >> 2) << 12) |
 901                     tcb->tcp_flags);
 902  /*分两种情况设置TCP首部的接收窗口的大小*/
 903     if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
 904         /* RFC1323: The window in SYN & SYN/ACK segments
 905          * is never scaled.
 906          */
 /*如果是SYN段,则设置接收窗口初始值为rcv_wnd*/
 907         th->window  = htons(min(tp->rcv_wnd, 65535U));
 908     } else {
/*如果是其他的报文,则调用tcp_select_window()计算当前接收窗口的大小。*/
 909         th->window  = htons(tcp_select_window(sk));
 910     }
 /*初始化TCP首部的校验码和紧急指针,可选字段,具体请参考TCP协议中的首部定义。*/
 911     th->check       = 0;
 912     th->urg_ptr     = 0;
 913 
 914     /* The urg_mode check is necessary during a below snd_una win probe,紧急指针 */
 915     if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
 916         if (before(tp->snd_up, tcb->seq + 0x10000)) {
 917             th->urg_ptr = htons(tp->snd_up - tcb->seq);
 918             th->urg = 1;
 919         } else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
 920             th->urg_ptr = htons(0xFFFF);
 921             th->urg = 1;
 922         }
 923     }
 924 
 925     tcp_options_write((__be32 *)(th + 1), tp, &opts);   //可选字段
 926     if (likely((tcb->tcp_flags & TCPHDR_SYN) == 0)) 
/*ENC(explicit congestion notification),基于显示反馈的拥塞控制协议,此外还有DCA(delay-based congestion avoidance),基于路径延迟,LCA(loss-based congestion avoidance),基于丢包反馈。
TCP_ECN_send:为已建立连接的套接字的即将发送的packet设置该ECN标志
*/
 927         TCP_ECN_send(sk, skb, tcp_header_size);  
 928 
/*MD5(Message Digest Algorithm)算法,计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。
*/
 929 #ifdef CONFIG_TCP_MD5SIG
 930     /* Calculate the MD5 hash, as we have all we need now */
 931     if (md5) {
 932         sk_nocaps_add(sk, NETIF_F_GSO_MASK);
 933         tp->af_specific->calc_md5_hash(opts.hash_location,
 934                            md5, sk, NULL, skb);
 935     }
 936 #endif
 937 
 938     icsk->icsk_af_ops->send_check(sk, skb);
 939 
 940     if (likely(tcb->tcp_flags & TCPHDR_ACK))
 941         tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
 942 
 943     if (skb->len != tcp_header_size)
 944         tcp_event_data_sent(tp, sk);
 945 
 946     if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
 947         TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
 948                   tcp_skb_pcount(skb));
 949 
 /*调用发送接口queue_xmit发送报文,进入到ip层,如果失败返回错误码。在TCP中该接口实现函数为ip_queue_xmit()*/
 950     err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
 951     if (likely(err <= 0))
 952         return err;
 953 
 954     tcp_enter_cwr(sk, 1);
 955 
 956     return net_xmit_eval(err);
 957 }                                    

ip_queue_xmit参考网络层发送侧程序。最后将程序的流梳理一遍~!


图7.2 tcp发送函数调用流程


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

第七章 tcp发送(传输层)--基于Linux3.10 的相关文章

  • 使用 cmake 和 opencv 对符号“gzclose”的未定义引用[关闭]

    Closed 这个问题是无法重现或由拼写错误引起 help closed questions 目前不接受答案 我尝试构建该项目 doppia https bitbucket org rodrigob doppia 但发生链接错误 我想这是一
  • 打印堆栈指针的值

    如何在 Linux Debian 和 Ubuntu 中用 C 打印堆栈指针的当前值 我尝试谷歌但没有找到结果 一个技巧是简单地将本地地址作为指针打印出来 但它不可移植 甚至无法保证有效 void print stack pointer vo
  • PHP exec - 检查是否启用或禁用

    有没有办法检查 php 脚本是否exec 在服务器上启用还是禁用 这将检查该功能是否确实有效 权限 权利等 if exec echo EXEC EXEC echo exec works
  • 如果specfile中的某些条件不满足,如何中止rpm包的安装?

    还有一些事情Requires标签不满足 所以我写了一个脚本来验证这些东西 但是我把它们放在哪里呢 如果没有找到 那么我想退出安装 提示用户在尝试再次安装此 rpm 之前执行这些步骤 writing exit 1 in installtag
  • 启动jetty服务器时出现NoClassDefFoundError

    我正在尝试在码头服务器中托管我的网络应用程序 spring 我将 war 文件复制到 jetty 服务器中的 webapp 文件夹中 我并不是想嵌入jetty服务器 而是试图在jetty内托管应用程序 如tomcat 我没有安装jetty
  • 使用Linux虚拟鼠标驱动

    我正在尝试实施一个虚拟鼠标驱动程序根据基本 Linux 设备驱动程序书 有一个用户空间应用程序 它生成坐标以及内核模块 See 虚拟鼠标驱动程序和用户空间应用程序代码 http www embeddedlinux org cn Essent
  • 内核如何区分线程和进程

    Linux 中的线程被称为轻量级进程 无论是进程还是线程 它们的实现都是通过task struct数据结构 1 gt 那么 从这个意义上说 内核如何区分线程和进程 2 gt 当发生上下文切换时 线程如何在上下文切换中获得更少的开销 因为在此
  • Linux 中不使用 C++ 的 C 异常处理

    Linux 是否提供了 C 语言的异常处理而不求助于 C 或者 实现此类异常处理的最佳方法是什么 目标是避免检查每个调用的函数的返回码 而是执行类似于 C 的线程安全且易于移植的操作 您可以通过为其编写信号处理程序来处理信号 GNU 记录的
  • 从命令输出中设置 GDB 中的环境变量

    我试图在挑战中利用缓冲区溢出 缓冲区从环境变量中获取其值 在 GDB 中 我知道您可以使用以下命令设置环境变量 set environment username test 但是我需要传递用户名变量特殊字符 所以我需要执行以下操作 set e
  • 如何在汇编语言中换行打印多个字符串

    我试图在汇编中的不同行上打印多个字符串 但使用我的代码 它只打印最后一个字符串 我对汇编语言非常陌生 所以请耐心等待 section text global start start mov edx len mov edx len1 mov
  • 有没有比使用 backtrace() 更便宜的方法来查找调用堆栈的深度?

    我的日志记录代码使用的返回值回溯 http linux die net man 3 backtrace确定当前堆栈深度 出于漂亮的打印目的 但我可以从分析中看到这是一个相当昂贵的调用 我不认为有更便宜的方法吗 请注意 我不关心帧地址 只关心
  • 从 bash 脚本返回值

    我想创建一个返回值的 Bash 文件 意思是 在脚本 script a bash 中我有一定的计算 脚本 script b bash 会调用它 script a bash return 1 5 script b bash a value s
  • 使用正在运行的进程的共享内存收集核心转储

    核心转储仅收集进程空间 而不收集为进程间通信创建的共享内存 如何使核心转储也包含正在运行的进程的共享内存 设置核心文件过滤器 proc PID coredump filter per http man7 org linux man page
  • 在 Unix 中,我可以在目录中运行“make”而无需先 cd 到该目录吗?

    在 Unix 中 我可以运行make在没有的目录中cd首先进入该目录 make C path to dir
  • 套接字发送并发保证

    如果我在两个进程 或两个线程 之间共享一个套接字 并且在这两个进程中我尝试发送一条阻塞的大消息 大于下划线协议缓冲区 是否可以保证这两个消息将按顺序发送 或者消息可以在内核内部交错吗 我主要对 TCP over IP 行为感兴趣 但了解它是
  • 数百个空闲线程的影响

    我正在考虑使用可能数百个线程来实现通过网络管理设备的任务 这是一个在带有 Linux 内核的 powerpc 处理器上运行的 C 应用程序 在每个任务进行同步以将数据从设备复制到任务的初始阶段之后 任务变得空闲 并且仅在收到警报或需要更改一
  • bash 或 sh 中的“=”和“==”运算符有什么区别

    我意识到 和 运算符都可以在 if 语句中使用 例如 var some string if var some string then doing something fi if var some string then doing some
  • MYSQL插入GB大小的巨大SQL文件

    我正在尝试创建 Wikipedia DB 副本 大约 50GB 但在处理最大的 SQL 文件时遇到问题 我使用 linux split 实用程序将 GB 大小的文件拆分为 300 MB 的块 例如 split d l 50 enwiki 2
  • 从汇编程序获取命令行参数

    通读 专业汇编语言书籍 似乎它提供了用于读取命令行参数的错误代码 我纠正了一点 现在它从段错误变成了读取参数计数 然后是段错误 这是完整的代码 data output1 asciz There are d params n output2
  • 将一个文件写入.c中的另一个文件

    我有一个读取文件然后将其内容复制到另一个文件的代码 我需要使其仅复制每 20 个符号 然后跳过 10 个符号 然后再次跳过 20 个符号 依此类推 我必须使用 lseek 函数 但我不知道如何将所有这些放入循环中来执行此操作 main ar

随机推荐

  • ESP32 SIM800L:发送带有传感器读数的文本消息(SMS警报)

    在这个项目中 我们将使用T Call ESP32 SIM800L模块创建一个SMS通知系统 当传感器读数高于或低于特定阈值时 该模块会发送SMS 在此示例中 我们将使用DS18B20温度传感器 并在温度高于28 C时发送短信 一旦温度降低到
  • uniapp使用scroll-view实现左右,上下滑动

    uniapp使用scroll view实现左右 上下滑动 阐述 我们在项目中往往都能遇到实现左右滑动跟上下滑动的需求 不需要安装better scroll uniapp 自带的scroll view 就可以实现了 实现左右滑动
  • 开源项目,源码

    GitHub 优秀的 Android 开源项目 转自 http blog csdn net shulianghan article details 18046021 主要介绍那些不错个性化的View 包括ListView ActionBar
  • java基础03:final

    说明 final是java的一个关键字 是最终的意思 final 表示 最后的 最终的 含义 变量一旦赋值后 不能被重新赋值 被 final 修饰的实例变量 就是已经实例化的对象 必须显式指定初始值 final 修饰符通常和 static
  • Flash钓鱼->CS上线(免杀过火绒、360等)

    先看结果 访问钓鱼页面 点击立即升级即把马儿下载下来了 这个马儿是rar压缩的 做成的rar解压自启动 所以是个exe的文件 然后这里为了像一点 把图标给改了 双击运行 查看效果 首先CS是没东西的 解压路径现在也是没东西的 这里我把解压路
  • C#值参数和引用参数

    C 值参数和引用参数 一 值参数 未用ref或out修饰符声明的参数为值参数 使用值参数 通过将实参的值复制到形参的方式 把数据传递到方法 方法被调用时 系统做如下操作 在栈中为形参分配空间 复制实参到形参 值参数的实参不一定是变量 它可以
  • 几年的Unity学习总结

    stream 其中类Stream为抽象类 由此有三个派生类 需要引入命名空间 using System IO MemoryStream 对内存进行读取与写入 BufferedStream 对缓冲器进行读取 写入 FileStream 对文件
  • access统计班级人数_使用ACCESS查询统计分数段人数

    不少人都知道使用电子表格 excel 进行分数段统计 使用access的人也可以用它设计查询进行分数段人数统计 这里假设你有一个access表 也可以是基表的查询 名叫tblScore 当然可以是中文名称 只不过代码内也要作相应修改 表内是
  • 大数据挖掘简介

    大数据挖掘涉及如下的课程 机器学习 统计学 人工智能 数据库等 但是更多的注重如下的特性 1 可扩展性 Scalability 大数据 2 算法和架构 3 自动的处理大数据 我们需要学习挖掘不同类型的数据 1 高维的数据 2 图数据 3 无
  • Vue技术 v-cloak指令(用于在 Vue 实例加载和编译之前隐藏元素)

    1 v cloak 指令的用法 v cloak 指令通常与 CSS 配合使用 用于在 Vue 实例加载和编译之前隐藏元素 通过给元素添加 v cloak 属性 然后在 CSS 中定义对应的样式 可以确保在 Vue 实例加载完成前 该元素的内
  • flex布局——flex-direction属性

    1 flex布局原理 1 flex是flexible Box的缩写 意为 弹性布局 用来为盒状模型提供最大的灵活性 任何一个容器都可以指定为flex布局 当我们为父盒子设为flex布局以后 子元素的float clear 和vertical
  • CentOS 7.9搭建Discuz 3.5论坛(LNMP)

    这里写目录标题 安装规格 安装nginx 安装依赖 编译配置Nginx 安装MySQL 设置MySQL Yum源并安装MySQL 查看MySQL初始密码并修改 安装并配置PHP 下载并解压Discuz 3 5 安装Discuz 安装规格 安
  • MMSegmentation笔记06:推理

    1 单张图像预测 author Seaton Time 2023 8 19 15 38 IDE PyCharm Summary 使用训练好的模型进行单张图像推理 import cv2 import matplotlib pyplot as
  • 对git rebase 和git merge的理解

    一 是什么 在使用 git 进行版本管理的项目中 当完成一个特性的开发并将其合并到 master 分支时 会有两种方式 git merge git rebase git rebase 与 git merge都有相同的作用 都是将一个分支的提
  • winhex常见问题:无法创建i:\TEMP\Win\WinHex 001.tmp,请确定文件夹存在且文件未被写保护

    点击菜单帮助 设置 初始化设置 恢复默认设置即可
  • 在虚拟环境下使用pip时默认使用系统环境的pip该怎么办

    入门小菜鸟 希望像做笔记记录自己学的东西 也希望能帮助到同样入门的人 更希望大佬们帮忙纠错啦 侵权立删 服务器的系统环境装的是python3 6 我建的虚拟环境装的是python3 7 用conda安装依赖包令人火大的各种报错 也许我跟co
  • 3-2.http 请求头Content-Type 为application/x-www-form-urlencoded

    本文测试 Content Type 为 multipart form data 的请求详情 前端页面模仿用户输入 用户名 密码 性别 爱好 城市等 可以看到请求头中 Content Type application x www form u
  • windows8.1 打不开网页 除ie外打不开网页 firefox chrome 打不开网页解决方法

    左下角 开始菜单右键 使用管理员 打开命令提示符 cmd 输入netsh winsock reset 就可以了 如果还不行 重启试下
  • 【docker】docker 安装配置 nginx+php+composer

    1 安装php7 安装docker就不赘述了 现在要在docker中安装php7 先拉镜像 docker pull php 7 1 fpm 这个镜像是把php和php fpm整合到一起 可以看做是php fpm 要配合nginx使用 先运行
  • 第七章 tcp发送(传输层)--基于Linux3.10

    由第五章可知 sock recvmsg和tcp sendmsg用于tcp层和应用层的接口 由第四章可知 tcp v4 rcv和tcp tarnsmit skb是传输层和网络层之间的接口 现在来看看tcp sendmsg是如何到tcp tar