//file: net/ipv4/tcp_output.c
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
//1.克隆新 skb 出来
if (likely(clone_it)) {
skb = skb_clone(skb, gfp_mask);
......
}
//2.封装 TCP 头
th = tcp_hdr(skb);
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->window = ...;
th->urg = ...;
......
//3.调用网络层发送接口
err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
}
第一件事是先克隆一个新的 skb,这里重点说下为什么要复制一个 skb 出来呢?
是因为 skb 后续在调用网络层,最后到达网卡发送完成的时候,这个 skb 会被释放掉。而我们知道 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 skb 不能被删除。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是 skb 的一个拷贝。等收到 ACK 再真正删除。
第二件事是修改 skb 中的 TCP header,根据实际情况把 TCP 头设置好。这里要介绍一个小技巧,skb 内部其实包含了网络协议中所有的 header。在设置 TCP 头的时候,只是把指针指向 skb 的合适位置。后面再设置 IP 头的时候,在把指针挪一挪就行,避免频繁的内存申请和拷贝,效率很高。
tcp_transmit_skb 是发送数据位于传输层的最后一步,接下来就可以进入到网络层进行下一层的操作了。调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit()。
在下面的这个源码中,我们的知道了 queue_xmit 其实指向的是 ip_queue_xmit 函数。
//file: net/ipv4/tcp_ipv4.c
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
...
}
自此,传输层的工作也就都完成了。数据离开了传输层,接下来将会进入到内核在网络层的实现里。
4.3 网络层发送处理
Linux 内核网络层的发送的实现位于 net/ipv4/ip_output.c 这个文件。传输层调用到的 ip_queue_xmit 也在这里。(从文件名上也能看出来进入到 IP 层了,源文件名已经从 tcp_xxx 变成了 ip_xxx。)
在网络层里主要处理路由项查找、IP 头设置、netfilter 过滤、skb 切分(大于 MTU 的话)等几项工作,处理完这些工作后会交给更下层的邻居子系统来处理。
我们来看网络层入口函数 ip_queue_xmit 的源码:
//file: net/ipv4/ip_output.c
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
//检查 socket 中是否有缓存的路由表
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
//没有缓存则展开查找
//则查找路由项, 并缓存到 socket 中
rt = ip_route_output_ports(...);
sk_setup_caps(sk, &rt->dst);
}
//为 skb 设置路由表
skb_dst_set_noref(skb, &rt->dst);
//设置 IP header
iph = ip_hdr(skb);
iph->protocol = sk->sk_protocol;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->frag_off = ...;
//发送
ip_local_out(skb);
}
ip_queue_xmit 已经到了网络层,在这个函数里我们看到了网络层相关的功能路由项查找,如果找到了则设置到 skb 上(没有路由的话就直接报错返回了)。
在 Linux 上通过 route 命令可以看到你本机的路由配置。
在路由表中,可以查到某个目的网络应该通过哪个 Iface(网卡),哪个 Gateway(网卡)发送出去。查找出来以后缓存到 socket 上,下次再发送数据就不用查了。
接着把路由表地址也放到 skb 里去。
//file: include/linux/skbuff.h
struct sk_buff {
//保存了一些路由相关信息
unsigned long _skb_refdst;
}
接下来就是定位到 skb 里的 IP 头的位置上,然后开始按照协议规范设置 IP header。