tcp_sendmsg 这个函数比较长,我们分多次来看它。先看这一段
//file: net/ipv4/tcp.c  
int tcp_sendmsg(...)  
{  
 while(...){  
  while(...){  
   //获取发送队列  
   skb = tcp_write_queue_tail(sk);  
  
   //申请skb 并拷贝  
   ......  
  }  
 }  
}  
//file: include/net/tcp.h  
static inline struct sk_buff *tcp_write_queue_tail(const struct sock *sk)  
{  
 return skb_peek_tail(&sk->sk_write_queue);  
}  
理解对 socket 调用 tcp_write_queue_tail 是理解发送的前提。如上所示,这个函数是在获取 socket 发送队列中的最后一个 skb。skb 是 struct sk_buff 对象的简称,用户的发送队列就是该对象组成的一个链表。

我们再接着看 tcp_sendmsg 的其它部分。
//file: net/ipv4/tcp.c  
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,  
  size_t size)  
{  
 //获取用户传递过来的数据和标志  
 iov = msg->msg_iov; //用户数据地址  
 iovlen = msg->msg_iovlen; //数据块数为1  
 flags = msg->msg_flags; //各种标志  
  
 //遍历用户层的数据块  
 while (--iovlen >= 0) {  
  
  //待发送数据块的地址  
  unsigned char __user *from = iov->iov_base;  
  
  while (seglen > 0) {  
  
   //需要申请新的 skb  
   if (copy <= 0) {  
  
    //申请 skb,并添加到发送队列的尾部  
    skb = sk_stream_alloc_skb(sk,  
         select_size(sk, sg),  
         sk->sk_allocation);  
  
    //把 skb 挂到socket的发送队列上  
    skb_entail(sk, skb);  
   }  
  
   // skb 中有足够的空间  
   if (skb_availroom(skb) > 0) {  
    //拷贝用户空间的数据到内核空间,同时计算校验和  
    //from是用户空间的数据地址   
    skb_add_data_nocache(sk, skb, from, copy);  
   }   
   ......  
这个函数比较长,不过其实逻辑并不复杂。其中 msg->msg_iov 存储的是用户态内存的要发送的数据的 buffer。接下来在内核态申请内核内存,比如 skb,并把用户内存里的数据拷贝到内核态内存中。这就会涉及到一次或者几次内存拷贝的开销。

至于内核什么时候真正把 skb 发送出去。在 tcp_sendmsg 中会进行一些判断。
//file: net/ipv4/tcp.c  
int tcp_sendmsg(...)  
{  
 while(...){  
  while(...){  
   //申请内核内存并进行拷贝  
  
   //发送判断  
   if (forced_push(tp)) {  
    tcp_mark_push(tp, skb);  
    __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);  
   } else if (skb == tcp_send_head(sk))  
    tcp_push_one(sk, mss_now);    
   }  
   continue;  
  }  
 }  
}  
只有满足 forced_push(tp) 或者 skb == tcp_send_head(sk) 成立的时候,内核才会真正启动发送数据包。其中 forced_push(tp) 判断的是未发送的数据数据是否已经超过最大窗口的一半了。
条件都不满足的话,这次的用户要发送的数据只是拷贝到内核就算完事了!
2)传输层发送
假设现在内核发送条件已经满足了,我们再来跟踪一下实际的发送过程。对于上小节函数中,当满足真正发送条件的时候,无论调用的是 __tcp_push_pending_frames 还是 tcp_push_one 最终都实际会执行到 tcp_write_xmit。
所以我们直接从 tcp_write_xmit 看起,这个函数处理了传输层的拥塞控制、滑动窗口相关的工作。满足窗口要求的时候,设置一下 TCP 头然后将 skb 传到更低的网络层进行处理。

我们来看下 tcp_write_xmit 的源码。
//file: net/ipv4/tcp_output.c  
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,  
      int push_one, gfp_t gfp)  
{  
 //循环获取待发送 skb  
 while ((skb = tcp_send_head(sk)))   
 {  
  //滑动窗口相关  
  cwnd_quota = tcp_cwnd_test(tp, skb);  
  tcp_snd_wnd_test(tp, skb, mss_now);  
  tcp_mss_split_point(...);  
  tso_fragment(sk, skb, ...);  
  ......  
  
  //真正开启发送  
  tcp_transmit_skb(sk, skb, 1, gfp);  
 }  
}  
可以看到我们之前在网络协议里学的滑动窗口、拥塞控制就是在这个函数中完成的,这部分就不过多展开了,感兴趣同学自己找这段源码来读。我们今天只看发送主过程,那就走到了 tcp_transmit_skb。
