CategoryResourceRepost/极客时间专栏/趣谈Linux操作系统/核心原理篇:第八部分 网络系统/46 | 发送网络包(下):如何表达我们想让合作伙伴做什么?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

893 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="46 | 发送网络包(下):如何表达我们想让合作伙伴做什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/99/e7ee28d7548651dffc3504e3b4e65299.mp3"></audio>
上一节我们讲网络包的发送讲了上半部分也即从VFS层一直到IP层这一节我们接着看下去看IP层和MAC层是如何发送数据的。
## 解析ip_queue_xmit函数
从ip_queue_xmit函数开始我们就要进入IP层的发送逻辑了。
```
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
inet_opt = rcu_dereference(inet-&gt;inet_opt);
fl4 = &amp;fl-&gt;u.ip4;
rt = skb_rtable(skb);
/* Make sure we can route this packet. */
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) {
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet-&gt;inet_daddr;
......
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet-&gt;inet_saddr,
inet-&gt;inet_dport,
inet-&gt;inet_sport,
sk-&gt;sk_protocol,
RT_CONN_FLAGS(sk),
sk-&gt;sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &amp;rt-&gt;dst);
}
skb_dst_set_noref(skb, &amp;rt-&gt;dst);
packet_routed:
/* OK, we know where to send it, allocate and build IP header. */
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt-&gt;opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 &lt;&lt; 12) | (5 &lt;&lt; 8) | (inet-&gt;tos &amp; 0xff));
if (ip_dont_fragment(sk, &amp;rt-&gt;dst) &amp;&amp; !skb-&gt;ignore_df)
iph-&gt;frag_off = htons(IP_DF);
else
iph-&gt;frag_off = 0;
iph-&gt;ttl = ip_select_ttl(inet, &amp;rt-&gt;dst);
iph-&gt;protocol = sk-&gt;sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb-&gt;h.foo itself. */
if (inet_opt &amp;&amp; inet_opt-&gt;opt.optlen) {
iph-&gt;ihl += inet_opt-&gt;opt.optlen &gt;&gt; 2;
ip_options_build(skb, &amp;inet_opt-&gt;opt, inet-&gt;inet_daddr, rt, 0);
}
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)-&gt;gso_segs ?: 1);
/* TODO : should we use skb-&gt;sk here instead of sk ? */
skb-&gt;priority = sk-&gt;sk_priority;
skb-&gt;mark = sk-&gt;sk_mark;
res = ip_local_out(net, sk, skb);
......
}
```
在ip_queue_xmit中也即IP层的发送函数里面有三部分逻辑。
第一部分,选取路由,也即我要发送这个包应该从哪个网卡出去。
这件事情主要由ip_route_output_ports函数完成。接下来的调用链为ip_route_output_ports-&gt;ip_route_output_flow-&gt;__ip_route_output_key-&gt;ip_route_output_key_hash-&gt;ip_route_output_key_hash_rcu。
```
struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
struct net_device *dev_out = NULL;
int orig_oif = fl4-&gt;flowi4_oif;
unsigned int flags = 0;
struct rtable *rth;
......
err = fib_lookup(net, fl4, res, 0);
......
make_route:
rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
......
}
```
ip_route_output_key_hash_rcu先会调用fib_lookup。
**FIB**全称是Forwarding Information Base**转发信息表。**其实就是咱们常说的路由表。
```
static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
{ struct fib_table *tb;
......
tb = fib_get_table(net, RT_TABLE_MAIN);
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
......
}
```
路由表可以有多个一般会有一个主表RT_TABLE_MAIN。然后fib_table_lookup函数在这个表里面进行查找。
路由表是一个什么样的结构呢?
路由就是在Linux服务器上的路由表里面配置的一条一条规则。这些规则大概是这样的想访问某个网段从某个网卡出去下一跳是某个IP。
之前我们讲过一个简单的拓扑图里面的三台Linux机器的路由表都可以通过ip route命令查看。
<img src="https://static001.geekbang.org/resource/image/f6/0e/f6982eb85dc66bd04200474efb3a050e.png" alt="">
```
# Linux服务器A
default via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 metric 100
# Linux服务器B
default via 192.168.2.1 dev eth0
192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.100 metric 100
# Linux服务器做路由器
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
```
其实对于两端的服务器来讲我们没有太多路由可以选但是对于中间的Linux服务器做路由器来讲这里有两条路可以选一个是往左面转发一个是往右面转发就需要路由表的查找。
fib_table_lookup的代码逻辑比较复杂好在注释比较清楚。因为路由表要按照前缀进行查询希望找到最长匹配的那一个例如192.168.2.0/24和192.168.0.0/16都能匹配192.168.2.100/24。但是我们应该使用192.168.2.0/24的这一条。
为了更方面的做这个事情我们使用了Trie树这种结构。比如我们有一系列的字符串{bcs#, badge#, baby#, back#, badger#, badness#}。之所以每个字符串都加上#是希望不要一个字符串成为另外一个字符串的前缀。然后我们把它们放在Trie树中如下图所示
<img src="https://static001.geekbang.org/resource/image/3f/11/3f0a99cf1c47afcd0bd740c4b7802511.png" alt="">
对于将IP地址转成二进制放入trie树也是同样的道理可以很快进行路由的查询。
找到了路由,就知道了应该从哪个网卡发出去。
然后ip_route_output_key_hash_rcu会调用__mkroute_output创建一个struct rtable表示找到的路由表项。这个结构是由rt_dst_alloc函数分配的。
```
struct rtable *rt_dst_alloc(struct net_device *dev,
unsigned int flags, u16 type,
bool nopolicy, bool noxfrm, bool will_cache)
{
struct rtable *rt;
rt = dst_alloc(&amp;ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
(will_cache ? 0 : DST_HOST) |
(nopolicy ? DST_NOPOLICY : 0) |
(noxfrm ? DST_NOXFRM : 0));
if (rt) {
rt-&gt;rt_genid = rt_genid_ipv4(dev_net(dev));
rt-&gt;rt_flags = flags;
rt-&gt;rt_type = type;
rt-&gt;rt_is_input = 0;
rt-&gt;rt_iif = 0;
rt-&gt;rt_pmtu = 0;
rt-&gt;rt_gateway = 0;
rt-&gt;rt_uses_gateway = 0;
rt-&gt;rt_table_id = 0;
INIT_LIST_HEAD(&amp;rt-&gt;rt_uncached);
rt-&gt;dst.output = ip_output;
if (flags &amp; RTCF_LOCAL)
rt-&gt;dst.input = ip_local_deliver;
}
return rt;
}
```
最终返回struct rtable实例第一部分也就完成了。
第二部分就是准备IP层的头往里面填充内容。这就要对着IP层的头的格式进行理解。
<img src="https://static001.geekbang.org/resource/image/6b/2b/6b2ea7148a8e04138a2228c5dbc7182b.png" alt="">
在这里面服务类型设置为tos标识位里面设置是否允许分片frag_off。如果不允许而遇到MTU太小过不去的情况就发送ICMP报错。TTL是这个包的存活时间为了防止一个IP包迷路以后一直存活下去每经过一个路由器TTL都减一减为零则“死去”。设置protocol指的是更上层的协议这里是TCP。源地址和目标地址由ip_copy_addrs设置。最后设置options。
第三部分就是调用ip_local_out发送IP包。
```
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;
err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb);
return err;
}
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph-&gt;tot_len = htons(skb-&gt;len);
skb-&gt;protocol = htons(ETH_P_IP);
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)-&gt;dev,
dst_output);
}
```
ip_local_out先是调用__ip_local_out然后里面调用了nf_hook。这是什么呢nf的意思是Netfilter这是Linux内核的一个机制用于在网络发送和转发的关键节点上加上hook函数这些函数可以截获数据包对数据包进行干预。
一个著名的实现就是内核模块ip_tables。在用户态还有一个客户端程序iptables用命令行来干预内核的规则。
<img src="https://static001.geekbang.org/resource/image/75/4d/75c8257049eed99499e802fcc2eacf4d.png" alt="">
iptables有表和链的概念最终要的是两个表。
filter表处理过滤功能主要包含以下三个链。
- INPUT链过滤所有目标地址是本机的数据包
- FORWARD链过滤所有路过本机的数据包
- OUTPUT链过滤所有由本机产生的数据包
nat表主要处理网络地址转换可以进行SNAT改变源地址、DNAT改变目标地址包含以下三个链。
- PREROUTING链可以在数据包到达时改变目标地址
- OUTPUT链可以改变本地产生的数据包的目标地址
- POSTROUTING链在数据包离开时改变数据包的源地址
<img src="https://static001.geekbang.org/resource/image/76/da/765e5431fe4b17f62b1b5712cc82abda.png" alt="">
在这里网络包马上就要发出去了因而是NF_INET_LOCAL_OUT也即ouput链如果用户曾经在iptables里面写过某些规则就会在nf_hook这个函数里面起作用。
ip_local_out再调用dst_output就是真正的发送数据。
```
/* Output packet to network from transport. */
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
return skb_dst(skb)-&gt;output(net, sk, skb);
}
```
这里调用的就是struct rtable成员dst的ouput函数。在rt_dst_alloc中我们可以看到output函数指向的是ip_output。
```
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)-&gt;dev;
skb-&gt;dev = dev;
skb-&gt;protocol = htons(ETH_P_IP);
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)-&gt;flags &amp; IPSKB_REROUTED));
}
```
在ip_output里面我们又看到了熟悉的NF_HOOK。这一次是NF_INET_POST_ROUTING也即POSTROUTING链处理完之后调用ip_finish_output。
## 解析ip_finish_output函数
从ip_finish_output函数开始发送网络包的逻辑由第三层到达第二层。ip_finish_output最终调用ip_finish_output2。
```
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst-&gt;dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
u32 nexthop;
......
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)-&gt;daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&amp;arp_tbl, &amp;nexthop, dev, false);
if (!IS_ERR(neigh)) {
int res;
sock_confirm_neigh(skb, neigh);
res = neigh_output(neigh, skb);
return res;
}
......
}
```
在ip_finish_output2中先找到struct rtable路由表里面的下一跳下一跳一定和本机在同一个局域网中可以通过二层进行通信因而通过__ipv4_neigh_lookup_noref查找如何通过二层访问下一跳。
```
static inline struct neighbour *__ipv4_neigh_lookup_noref(struct net_device *dev, u32 key)
{
return ___neigh_lookup_noref(&amp;arp_tbl, neigh_key_eq32, arp_hashfn, &amp;key, dev);
}
```
__ipv4_neigh_lookup_noref是从本地的ARP表中查找下一跳的MAC地址。ARP表的定义如下
```
struct neigh_table arp_tbl = {
.family = AF_INET,
.key_len = 4,
.protocol = cpu_to_be16(ETH_P_IP),
.hash = arp_hash,
.key_eq = arp_key_eq,
.constructor = arp_constructor,
.proxy_redo = parp_redo,
.id = &quot;arp_cache&quot;,
......
.gc_interval = 30 * HZ,
.gc_thresh1 = 128,
.gc_thresh2 = 512,
.gc_thresh3 = 1024,
};
```
如果在ARP表中没有找到相应的项则调用__neigh_create进行创建。
```
struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, bool want_ref)
{
u32 hash_val;
int key_len = tbl-&gt;key_len;
int error;
struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
struct neigh_hash_table *nht;
memcpy(n-&gt;primary_key, pkey, key_len);
n-&gt;dev = dev;
dev_hold(dev);
/* Protocol specific setup. */
if (tbl-&gt;constructor &amp;&amp; (error = tbl-&gt;constructor(n)) &lt; 0) {
......
}
......
if (atomic_read(&amp;tbl-&gt;entries) &gt; (1 &lt;&lt; nht-&gt;hash_shift))
nht = neigh_hash_grow(tbl, nht-&gt;hash_shift + 1);
hash_val = tbl-&gt;hash(pkey, dev, nht-&gt;hash_rnd) &gt;&gt; (32 - nht-&gt;hash_shift);
for (n1 = rcu_dereference_protected(nht-&gt;hash_buckets[hash_val],
lockdep_is_held(&amp;tbl-&gt;lock));
n1 != NULL;
n1 = rcu_dereference_protected(n1-&gt;next,
lockdep_is_held(&amp;tbl-&gt;lock))) {
if (dev == n1-&gt;dev &amp;&amp; !memcmp(n1-&gt;primary_key, pkey, key_len)) {
if (want_ref)
neigh_hold(n1);
rc = n1;
goto out_tbl_unlock;
}
}
......
rcu_assign_pointer(n-&gt;next,
rcu_dereference_protected(nht-&gt;hash_buckets[hash_val],
lockdep_is_held(&amp;tbl-&gt;lock)));
rcu_assign_pointer(nht-&gt;hash_buckets[hash_val], n);
......
}
```
__neigh_create先调用neigh_alloc创建一个struct neighbour结构用于维护MAC地址和ARP相关的信息。这个名字也很好理解大家都是在一个局域网里面可以通过MAC地址访问到当然是邻居了。
```
static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
struct neighbour *n = NULL;
unsigned long now = jiffies;
int entries;
......
n = kzalloc(tbl-&gt;entry_size + dev-&gt;neigh_priv_len, GFP_ATOMIC);
if (!n)
goto out_entries;
__skb_queue_head_init(&amp;n-&gt;arp_queue);
rwlock_init(&amp;n-&gt;lock);
seqlock_init(&amp;n-&gt;ha_lock);
n-&gt;updated = n-&gt;used = now;
n-&gt;nud_state = NUD_NONE;
n-&gt;output = neigh_blackhole;
seqlock_init(&amp;n-&gt;hh.hh_lock);
n-&gt;parms = neigh_parms_clone(&amp;tbl-&gt;parms);
setup_timer(&amp;n-&gt;timer, neigh_timer_handler, (unsigned long)n);
NEIGH_CACHE_STAT_INC(tbl, allocs);
n-&gt;tbl = tbl;
refcount_set(&amp;n-&gt;refcnt, 1);
n-&gt;dead = 1;
......
}
```
在neigh_alloc中我们先分配一个struct neighbour结构并且初始化。这里面比较重要的有两个成员一个是arp_queue所以上层想通过ARP获取MAC地址的任务都放在这个队列里面。另一个是timer定时器我们设置成过一段时间就调用neigh_timer_handler来处理这些ARP任务。
__neigh_create然后调用了arp_tbl的constructor函数也即调用了arp_constructor在这里面定义了ARP的操作arp_hh_ops。
```
static int arp_constructor(struct neighbour *neigh)
{
__be32 addr = *(__be32 *)neigh-&gt;primary_key;
struct net_device *dev = neigh-&gt;dev;
struct in_device *in_dev;
struct neigh_parms *parms;
......
neigh-&gt;type = inet_addr_type_dev_table(dev_net(dev), dev, addr);
parms = in_dev-&gt;arp_parms;
__neigh_parms_put(neigh-&gt;parms);
neigh-&gt;parms = neigh_parms_clone(parms);
......
neigh-&gt;ops = &amp;arp_hh_ops;
......
neigh-&gt;output = neigh-&gt;ops-&gt;output;
......
}
static const struct neigh_ops arp_hh_ops = {
.family = AF_INET,
.solicit = arp_solicit,
.error_report = arp_error_report,
.output = neigh_resolve_output,
.connected_output = neigh_resolve_output,
};
```
__neigh_create最后是将创建的struct neighbour结构放入一个哈希表从里面的代码逻辑比较容易看出这是一个数组加链表的链式哈希表先计算出哈希值hash_val得到相应的链表然后循环这个链表找到对应的项如果找不到就在最后插入一项。
我们回到ip_finish_output2在__neigh_create之后会调用neigh_output发送网络包。
```
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
......
return n-&gt;output(n, skb);
}
```
按照上面对于struct neighbour的操作函数arp_hh_ops 的定义output调用的是neigh_resolve_output。
```
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
if (!neigh_event_send(neigh, skb)) {
......
rc = dev_queue_xmit(skb);
}
......
}
```
在neigh_resolve_output里面首先neigh_event_send触发一个事件看能否激活ARP。
```
int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
int rc;
bool immediate_probe = false;
if (!(neigh-&gt;nud_state &amp; (NUD_STALE | NUD_INCOMPLETE))) {
if (NEIGH_VAR(neigh-&gt;parms, MCAST_PROBES) +
NEIGH_VAR(neigh-&gt;parms, APP_PROBES)) {
unsigned long next, now = jiffies;
atomic_set(&amp;neigh-&gt;probes,
NEIGH_VAR(neigh-&gt;parms, UCAST_PROBES));
neigh-&gt;nud_state = NUD_INCOMPLETE;
neigh-&gt;updated = now;
next = now + max(NEIGH_VAR(neigh-&gt;parms, RETRANS_TIME),
HZ/2);
neigh_add_timer(neigh, next);
immediate_probe = true;
}
......
} else if (neigh-&gt;nud_state &amp; NUD_STALE) {
neigh_dbg(2, &quot;neigh %p is delayed\n&quot;, neigh);
neigh-&gt;nud_state = NUD_DELAY;
neigh-&gt;updated = jiffies;
neigh_add_timer(neigh, jiffies +
NEIGH_VAR(neigh-&gt;parms, DELAY_PROBE_TIME));
}
if (neigh-&gt;nud_state == NUD_INCOMPLETE) {
if (skb) {
.......
__skb_queue_tail(&amp;neigh-&gt;arp_queue, skb);
neigh-&gt;arp_queue_len_Bytes += skb-&gt;truesize;
}
rc = 1;
}
out_unlock_bh:
if (immediate_probe)
neigh_probe(neigh);
.......
}
```
在__neigh_event_send中激活ARP分两种情况第一种情况是马上激活也即immediate_probe。另一种情况是延迟激活则仅仅设置一个timer。然后将ARP包放在arp_queue上。如果马上激活就直接调用neigh_probe如果延迟激活则定时器到了就会触发neigh_timer_handler在这里面还是会调用neigh_probe。
我们就来看neigh_probe的实现在这里面会从arp_queue中拿出ARP包来然后调用struct neighbour的solicit操作。
```
static void neigh_probe(struct neighbour *neigh)
__releases(neigh-&gt;lock)
{
struct sk_buff *skb = skb_peek_tail(&amp;neigh-&gt;arp_queue);
......
if (neigh-&gt;ops-&gt;solicit)
neigh-&gt;ops-&gt;solicit(neigh, skb);
......
}
```
按照上面对于struct neighbour的操作函数arp_hh_ops 的定义solicit调用的是arp_solicit在这里我们可以找到对于arp_send_dst的调用创建并发送一个arp包得到结果放在struct dst_entry里面。
```
static void arp_send_dst(int type, int ptype, __be32 dest_ip,
struct net_device *dev, __be32 src_ip,
const unsigned char *dest_hw,
const unsigned char *src_hw,
const unsigned char *target_hw,
struct dst_entry *dst)
{
struct sk_buff *skb;
......
skb = arp_create(type, ptype, dest_ip, dev, src_ip,
dest_hw, src_hw, target_hw);
......
skb_dst_set(skb, dst_clone(dst));
arp_xmit(skb);
}
```
我们回到neigh_resolve_output中当ARP发送完毕就可以调用dev_queue_xmit发送二层网络包了。
```
/**
* __dev_queue_xmit - transmit a buffer
* @skb: buffer to transmit
* @accel_priv: private data used for L2 forwarding offload
*
* Queue a buffer for transmission to a network device.
*/
static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
struct net_device *dev = skb-&gt;dev;
struct netdev_queue *txq;
struct Qdisc *q;
......
txq = netdev_pick_tx(dev, skb, accel_priv);
q = rcu_dereference_bh(txq-&gt;qdisc);
if (q-&gt;enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
......
}
```
就像咱们在讲述硬盘块设备的时候讲过,每个块设备都有队列,用于将内核的数据放到队列里面,然后设备驱动从队列里面取出后,将数据根据具体设备的特性发送给设备。
网络设备也是类似的对于发送来说有一个发送队列struct netdev_queue *txq。
这里还有另一个变量叫做struct Qdisc这个是什么呢如果我们在一台Linux机器上运行ip addr我们能看到对于一个网卡都有下面的输出。
```
# ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1400 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:16:3e:75:99:08 brd ff:ff:ff:ff:ff:ff
inet 10.173.32.47/21 brd 10.173.39.255 scope global noprefixroute dynamic eth0
valid_lft 67104sec preferred_lft 67104sec
inet6 fe80::f816:3eff:fe75:9908/64 scope link
valid_lft forever preferred_lft forever
```
这里面有个关键字qdisc pfifo_fast是什么意思呢qdisc全称是queueing discipline中文叫排队规则。内核如果需要通过某个网络接口发送数据包都需要按照为这个接口配置的qdisc排队规则把数据包加入队列。
最简单的qdisc是pfifo它不对进入的数据包做任何的处理数据包采用先入先出的方式通过队列。pfifo_fast稍微复杂一些它的队列包括三个波段band。在每个波段里面使用先进先出规则。
三个波段的优先级也不相同。band 0的优先级最高band 2的最低。如果band 0里面有数据包系统就不会处理band 1里面的数据包band 1和band 2之间也是一样。
数据包是按照服务类型Type of ServiceTOS被分配到三个波段里面的。TOS是IP头里面的一个字段代表了当前的包是高优先级的还是低优先级的。
pfifo_fast分为三个先入先出的队列我们能称为三个Band。根据网络包里面的TOS看这个包到底应该进入哪个队列。TOS总共四位每一位表示的意思不同总共十六种类型。
<img src="https://static001.geekbang.org/resource/image/ab/d9/ab6af2f9e1a64868636080a05cfde0d9.png" alt="">
通过命令行tc qdisc show dev eth0我们可以输出结果priomap也是十六个数字。在0到2之间和TOS的十六种类型对应起来。不同的TOS对应不同的队列。其中Band 0优先级最高发送完毕后才轮到Band 1发送最后才是Band 2。
```
# tc qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
```
接下来__dev_xmit_skb开始进行网络包发送。
```
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev,
struct netdev_queue *txq)
{
......
rc = q-&gt;enqueue(skb, q, &amp;to_free) &amp; NET_XMIT_MASK;
if (qdisc_run_begin(q)) {
......
__qdisc_run(q);
}
......
}
void __qdisc_run(struct Qdisc *q)
{
int quota = dev_tx_weight;
int packets;
while (qdisc_restart(q, &amp;packets)) {
/*
* Ordered by possible occurrence: Postpone processing if
* 1. we've exceeded packet quota
* 2. another process needs the CPU;
*/
quota -= packets;
if (quota &lt;= 0 || need_resched()) {
__netif_schedule(q);
break;
}
}
qdisc_run_end(q);
}
```
__dev_xmit_skb会将请求放入队列然后调用__qdisc_run处理队列中的数据。qdisc_restart用于数据的发送。根据注释中的说法qdisc的另一个功能是用于控制网络包的发送速度因而如果超过速度就需要重新调度则会调用__netif_schedule。
```
static void __netif_reschedule(struct Qdisc *q)
{
struct softnet_data *sd;
unsigned long flags;
local_irq_save(flags);
sd = this_cpu_ptr(&amp;softnet_data);
q-&gt;next_sched = NULL;
*sd-&gt;output_queue_tailp = q;
sd-&gt;output_queue_tailp = &amp;q-&gt;next_sched;
raise_softirq_irqoff(NET_TX_SOFTIRQ);
local_irq_restore(flags);
}
```
__netif_schedule会调用__netif_reschedule发起一个软中断NET_TX_SOFTIRQ。咱们讲设备驱动程序的时候讲过设备驱动程序处理中断分两个过程一个是屏蔽中断的关键处理逻辑一个是延迟处理逻辑。当时说工作队列是延迟处理逻辑的处理方案软中断也是一种方案。
在系统初始化的时候我们会定义软中断的处理函数。例如NET_TX_SOFTIRQ的处理函数是net_tx_action用于发送网络包。还有一个NET_RX_SOFTIRQ的处理函数是net_rx_action用于接收网络包。接收网络包的过程咱们下一节解析。
```
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
```
这里我们来解析一下net_tx_action。
```
static __latent_entropy void net_tx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&amp;softnet_data);
......
if (sd-&gt;output_queue) {
struct Qdisc *head;
local_irq_disable();
head = sd-&gt;output_queue;
sd-&gt;output_queue = NULL;
sd-&gt;output_queue_tailp = &amp;sd-&gt;output_queue;
local_irq_enable();
while (head) {
struct Qdisc *q = head;
spinlock_t *root_lock;
head = head-&gt;next_sched;
......
qdisc_run(q);
}
}
}
```
我们会发现net_tx_action还是调用了qdisc_run还是会调用__qdisc_run然后调用qdisc_restart发送网络包。
我们来看一下qdisc_restart的实现。
```
static inline int qdisc_restart(struct Qdisc *q, int *packets)
{
struct netdev_queue *txq;
struct net_device *dev;
spinlock_t *root_lock;
struct sk_buff *skb;
bool validate;
/* Dequeue packet */
skb = dequeue_skb(q, &amp;validate, packets);
if (unlikely(!skb))
return 0;
root_lock = qdisc_lock(q);
dev = qdisc_dev(q);
txq = skb_get_tx_queue(dev, skb);
return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}
```
qdisc_restart将网络包从Qdisc的队列中拿下来然后调用sch_direct_xmit进行发送。
```
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev, struct netdev_queue *txq,
spinlock_t *root_lock, bool validate)
{
int ret = NETDEV_TX_BUSY;
if (likely(skb)) {
if (!netif_xmit_frozen_or_stopped(txq))
skb = dev_hard_start_xmit(skb, dev, txq, &amp;ret);
}
......
if (dev_xmit_complete(ret)) {
/* Driver sent out skb successfully or skb was consumed */
ret = qdisc_qlen(q);
} else {
/* Driver returned NETDEV_TX_BUSY - requeue skb */
ret = dev_requeue_skb(skb, q);
}
......
}
```
在sch_direct_xmit中调用dev_hard_start_xmit进行发送如果发送不成功会返回NETDEV_TX_BUSY。这说明网络卡很忙于是就调用dev_requeue_skb重新放入队列。
```
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret)
{
struct sk_buff *skb = first;
int rc = NETDEV_TX_OK;
while (skb) {
struct sk_buff *next = skb-&gt;next;
rc = xmit_one(skb, dev, txq, next != NULL);
skb = next;
if (netif_xmit_stopped(txq) &amp;&amp; skb) {
rc = NETDEV_TX_BUSY;
break;
}
}
......
}
```
在dev_hard_start_xmit中是一个while循环。每次在队列中取出一个sk_buff调用xmit_one发送。
接下来的调用链为xmit_one-&gt;netdev_start_xmit-&gt;__netdev_start_xmit。
```
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops, struct sk_buff *skb, struct net_device *dev, bool more)
{
skb-&gt;xmit_more = more ? 1 : 0;
return ops-&gt;ndo_start_xmit(skb, dev);
}
```
这个时候已经到了设备驱动层了。我们能看到drivers/net/ethernet/intel/ixgb/ixgb_main.c里面有对于这个网卡的操作的定义。
```
static const struct net_device_ops ixgb_netdev_ops = {
.ndo_open = ixgb_open,
.ndo_stop = ixgb_close,
.ndo_start_xmit = ixgb_xmit_frame,
.ndo_set_rx_mode = ixgb_set_multi,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = ixgb_set_mac,
.ndo_change_mtu = ixgb_change_mtu,
.ndo_tx_timeout = ixgb_tx_timeout,
.ndo_vlan_rx_add_vid = ixgb_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = ixgb_vlan_rx_kill_vid,
.ndo_fix_features = ixgb_fix_features,
.ndo_set_features = ixgb_set_features,
};
```
在这里面我们可以找到对于ndo_start_xmit的定义调用ixgb_xmit_frame。
```
static netdev_tx_t
ixgb_xmit_frame(struct sk_buff *skb, struct net_device *netdev)
{
struct ixgb_adapter *adapter = netdev_priv(netdev);
......
if (count) {
ixgb_tx_queue(adapter, count, vlan_id, tx_flags);
/* Make sure there is space in the ring for the next send. */
ixgb_maybe_stop_tx(netdev, &amp;adapter-&gt;tx_ring, DESC_NEEDED);
}
......
return NETDEV_TX_OK;
}
```
在ixgb_xmit_frame中我们会得到这个网卡对应的适配器然后将其放入硬件网卡的队列中。
至此,整个发送才算结束。
## 总结时刻
这一节,我们继续解析了发送一个网络包的过程,我们整个过程的图画在了下面。
<img src="https://static001.geekbang.org/resource/image/79/6f/79cc42f3163d159a66e163c006d9f36f.png" alt="">
这个过程分成几个层次。
- VFS层write系统调用找到struct file根据里面的file_operations的定义调用sock_write_iter函数。sock_write_iter函数调用sock_sendmsg函数。
- Socket层从struct file里面的private_data得到struct socket根据里面ops的定义调用inet_sendmsg函数。
- Sock层从struct socket里面的sk得到struct sock根据里面sk_prot的定义调用tcp_sendmsg函数。
- TCP层tcp_sendmsg函数会调用tcp_write_xmit函数tcp_write_xmit函数会调用tcp_transmit_skb在这里实现了TCP层面向连接的逻辑。
- IP层扩展struct sock得到struct inet_connection_sock根据里面icsk_af_ops的定义调用ip_queue_xmit函数。
- IP层ip_route_output_ports函数里面会调用fib_lookup查找路由表。FIB全称是Forwarding Information Base转发信息表也就是路由表。
- 在IP层里面要做的另一个事情是填写IP层的头。
- 在IP层还要做的一件事情就是通过iptables规则。
- MAC层IP层调用ip_finish_output进行MAC层。
- MAC层需要ARP获得MAC地址因而要调用___neigh_lookup_noref查找属于同一个网段的邻居他会调用neigh_probe发送ARP。
- 有了MAC地址就可以调用dev_queue_xmit发送二层网络包了它会调用__dev_xmit_skb会将请求放入队列。
- 设备层网络包的发送会触发一个软中断NET_TX_SOFTIRQ来处理队列中的数据。这个软中断的处理函数是net_tx_action。
- 在软中断处理函数中会将网络包从队列上拿下来调用网络设备的传输函数ixgb_xmit_frame将网络包发到设备的队列上去。
## 课堂练习
上一节你应该通过tcpdump看到了TCP包头的格式这一节请你查看一下IP包的格式以及ARP的过程。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">