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。