This commit is contained in:
gatieme
2016-09-05 23:53:01 +08:00
parent 128e98ba0b
commit 284689f848
8 changed files with 232 additions and 2 deletions
@@ -173,7 +173,8 @@ struct packet_type {
NAPI的另一个优点是可以高效地丢弃分组. 如果内核确信因为有很多其他工作需要处理, 而导致无法处理任何新的分组,那么网络适配器可以直接丢弃分组,无须复制到内核.
只有设备满足如下两个条件时,才能实现NAPI方法。
只有设备满足如下两个条件时,才能实现NAPI方法.
1. 设备必须能够保留多个接收的分组,例如保存到DMA环形缓冲区中. 下文将该缓冲区称为Rx缓冲区.
@@ -188,9 +189,11 @@ NAPI的另一个优点是可以高效地丢弃分组. 如果内核确信因为
内核以循环方式处理链表上的所有设备 : 内核依次轮询各个设备, 如果已经花费了一定的时间来处理某个设备, 则选择下一个设备进行处理. 此外, 某个设备都带有一个相对权重, 表示与轮询表中其他设备相比, 该设备的相对重要性. 较快的设备权重较大,较慢的设备权重较小. 由于权重指定了在一个轮询的循环中处理多少分组, 这确保了内核将更多地注意速度较快的设备.
###2.2.2 napi机制的实现细节
-------
现在我们已经弄清楚了NAPI的基本原理, 接下来将讨论其实现细节. 与旧的API相比, 关键性的变化在于, 支持NAPI的设备必须提供一个 poll函数. 该方法是特定于设备的, 在用`netif_napi_add`注册网卡时指定. 调用该函数注册, 表明设备可以且必须用新方法处理.
```CPP
@@ -292,7 +295,85 @@ static int hyper_card_poll(struct napi_struct *napi, int budget)
* 已经完全用掉了预算,但仍然有更多的分组需要处理。设备仍然留在轮询表上,不启用中断.
###2.2.4 实现IRQ处理程序
-------
NAPI也需要对网络设备的IRQ处理程序做一些改动. 这里仍然不求助于任何具体的硬件, 而介绍针对虚构设备的代码:
NAPI也需要对网络设备的IRQ处理程序做一些改动. 这里仍然不求助于任何具体的硬件, 而介绍针对虚构设备的代码:
```cpp
static irqreturn_t e100_intr(int irq, void *dev_id)
{
struct net_device *netdev = dev_id;
struct nic *nic = netdev_priv(netdev);
u8 stat_ack = ioread8(&nic->csr->scb.stat_ack);
netif_printk(nic, intr, KERN_DEBUG, nic->netdev,
"stat_ack = 0x%02X\n", stat_ack);
if (stat_ack == stat_ack_not_ours || /* Not our interrupt */
stat_ack == stat_ack_not_present) /* Hardware is ejected */
return IRQ_NONE;
/* Ack interrupt(s) */
iowrite8(stat_ack, &nic->csr->scb.stat_ack);
/* We hit Receive No Resource (RNR); restart RU after cleaning */
if (stat_ack & stat_ack_rnr)
nic->ru_running = RU_SUSPENDED;
if (likely(napi_schedule_prep(&nic->napi))) {
e100_disable_irq(nic);
__napi_schedule(&nic->napi);
}
return IRQ_HANDLED;
}
```
假定特定于接口的数据保存在`net_device->private`中, 这是大多数网卡驱动程序使用的方法.
使用辅助函数`netdev_priv`访问该字段.
现在需要通知内核有新的分组可用. 这需要如下二阶段的方法.
1. `netif_rx_schedule_prep`准备将设备放置到轮询表上. 本质上, 这会安置`napi_struct->flags`中的`NAPI_STATE_SCHED`标志.
2. 如果设置该标志成功(仅当NAPI已经处于活跃状态时, 才会失败), 驱动程序必须用特定于设备的适当方法来禁用相应的IRQ. 调用`__netif_rx_schedule`将设备的`napi_struct`添加到轮询表, 并引发软中断`NET_RX_SOFTIRQ`. 这通知内核在`net_rx_action`中开始轮询.
###2.2.5 处理Rx软中断
-------
在讨论了为支持NAPI驱动程序需要做哪些改动之后, 我们来考察一下内核需要承担的职责.
`net_rx_action`依旧是软中断`NET_RX_SOFTIRQ`的处理程序. 在前一节给出了该函数的一个简化版本.
随着有关NAPI的更多细节尘埃落定, 现在可以讨论该函数的所有细节了.
![图12-13给出了其代码流程图](../images/net_rx_action.png)
本质上, 内核通过依次调用各个设备特定的poll方法, 处理轮询表上当前的所有设备. 设备的权重用作该设备本身的预算, 即轮询的一步中可能处理的分组数目.
必须确保在这个软中断的处理程序中, 不会花费过多时间. 如果如下两个条件成立, 则放弃处理.
1. 处理程序已经花费了超出一个jiffie的时间.
2. 所处理分组的总数, 已经超过了`netdev_budget`指定的预算总值. 通常, 总值设置为300, 但可以通过`/proc/sys/net/core/netdev_budget`修改.
这个预算不能与各个网络设备本身的预算混淆! 在每个轮询步之后, 都从全局预算中减去处理的分组数目, 如果该预算值下降到0, 则退出软中断处理程序.
在轮询了一个设备之后, 内核会检查所处理的分组数目, 与该设备的预算是否相等.
如果相等, 那么尚未获得该设备上所有等待的分组, 即代码流程图中`work == weight`所表示的情况. 内核接下来将该设备移动到轮询表末尾, 在链表中所有其他设备都处理过之后, 继续轮询该设备. 显然, 这实现了网络设备之间的循环调度.
###2.2.6 在NAPI之上实现旧式API
-------
最后, 请注意旧的API是如何在NAPI上实现的. 内核的常规行为, 由一个与`softnet`队列关联的伪网络设备控制, `net/core/dev.c`中的`process_backlog`标准函数用作poll方法. 如果没有网络适配器将其自身添加到该队列的轮询表, 其中只包含这个伪适配器, 那么net_rx_action的行为就是通过对`process_backlog`的单一调用来处理队列中的分组, 而不管分组的来源设备.
@@ -0,0 +1,30 @@
进程虚拟地址空间
=======
| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN |
| ------- |:-------:|:-------:|:-------:|:-------:|:-------:|
| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) |
#3 发送分组
-------
在网络层中特定于协议的函数通知网络访问层处理由套接字缓冲区定义的一个分组时, 将发送完成的分组.
当信息从计算机发送出去时, 必须注意哪些事项? 除了特定协议需要完成的首部和校验和, 以及由高层协议实例生成的数据之外, 分组的路由是最重要的.(即使计算机只有一个网卡, 内核仍然需要区分发送到外部目标的分组和针对环回接口的分组.)
因为该问题只能由更高层的协议实例决定(特别是, 如果可以选择到预期目标的路由时), 所以设备驱动程序假定高层协议已经做出了决策.
在分组可以发送到下一个正确的计算机之前(通常不同于目标计算机,因为除非存在直接的硬件链路, 否则IP分组通常通过网关发送), 必须确定接收方网卡的硬件地址.
这是一个复杂的过程, 将在12.8.5节详细讲述. 此时, 我们假定已经知道接收方的MAC地址. 网络访问层的所需的另一个首部, 通常由特定于协议的函数产生.
`net/core/dev.c`中的`dev_queue_xmit`用于将分组放置到发出分组的队列上.
这里将忽略这个特定于设备的队列的实现, 因为它并没有揭示什么网络层的运作机制. 只要知道, 在分组放置到等待队列上一定的时间之后, 分组将发出即可.
这是通过特定于适配器的函数`hard_start_xmit`完成的, 在每个net_device结构中都以函数指针的形式出现, 由硬件设备驱动程序实现.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

+119
View File
@@ -0,0 +1,119 @@
| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN |
| ------- |:-------:|:-------:|:-------:|:-------:|:-------:|
| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) |
#1 网络层
-------
网络访问层仍然受到传输介质的性质以及相关适配器的设备驱动程序的很大影响. 网络层(具体地说是IP协议)与网络适配器的硬件性质几乎是完全分离的。为什么说是几乎?
读者稍后会看到, 该层不仅负责发送和接收数据, 还负责在彼此不直接连接的系统之间转发和路由分组. 查找最佳路由并选择适当的网络设备来发送分组,也涉及对底层地址族的处理(如特定于硬件的MAC地址), 这是该层至少要与网卡松散关联的原因. 在网络层地址和网络访问层之间的指派是由这一层完成的, 这也是
互联网络层无法与硬件完全脱离的原因.
如果不考虑底层硬件, 是无法将较大的分组分割为较小单位的(事实上, 硬件的性质是需要分割分组的首要原因). 因为每一种传输技术所支持的分组长度都有一个最大值, IP协议必须方法将较大的分组划分为较小的单位, 由接收方重新组合, 更高层协议不会注意到这一点. 划分后分组的长度取决于特定传输协议的能力.
IP在1981年正式定义(在RFC791中), 现在已经进入暮年
尽管事实上的情况与公司新闻稿的说法截然不同, 例如, 后者可能将电子表格的每个新版本都称赞为人类有史以来最伟大的发明, 但过去的20年确实在当今的技术上留下了印痕. 此前的缺陷和未能预料到的问题, 随着因特网的发展, 现在
变得越来越明显. 这也是开发IPv6标准作为目前IPv4后继者的原因. 遗憾的是, 因为缺乏核心的权威机构, 对这个未来标准的采用比较缓慢. 本章主要关注IPv4算法的实现, 但也会略看一看可用于未来的技术, 及其在Linux内核的实现.
为理解IP协议在内核中的实现, 必须简要介绍其工作方式. 很自然, 这是个非常大的领域, 我们只能略微谈谈相关的主题.
#2 IPv4
-------
IP分组使用的协议首部如图12-14所示.
![图12-14 IP首部的结构](../images/)
| 字段 | 描述 |
|:-----:|:-----:|
| version(版本) |指定了所用IP协议的版本。当前,该字段的有效值为4或6。 在支持两种协议版本的主机上,所使用的版本由前一章讨论的传输协议标识符确定。对协议的两个版本来说,该标识符中保存的值是不同的 |
| IHL(IP首部长度) | 定义了首部的长度,由于选项数量可变,这个值并不总是相同的 |
| Codepoint(代码点)<br>Type of Service(服务类型) | 用于更复杂的协议选项,我们在这里无须关注 |
| Length(长度) | 指定了分组的总长度,即首部加数据的长度 |
| fragment ID(分片标识) | 标识了一个分片的IP分组的各个部分。分片方法将同一分片ID指定到同一原始分组的各个数据片,使之可标识为同一分组的成员。各个部分的相对位置由fragment offset(分片偏移量)字段定义。偏移量的单位是64 bit |
第三个标志位"保留供未来使用", 但考虑到IPv6的存在, 这是不太可能的.
| 字段 | 描述 |
|:-----:|:-----:|
| TTL意为"Time to Live" | 指定了从发送者到接收者的传输路径上中间站点的最大数目(或跳数) |
| Protocol | 标识了IP分组承载的高层协议(传输层)。例如,TCP和UDP协议都有对应的唯一值 |
| Checksum | 包含了一个校验和,根据首部和数据的内容计算。如果指定的校验和与接收方计算的值不一致,那么可能发生了传输错误,应该丢弃该分组 |
| src和dest | 指定了源和目标的32位IP地址 |
| options | 用于扩展IP选项 |
| data | 保存了分组数据(净荷) |
IP首部中所有的数值都以网络字节序存储(大端序).
在内核源代码中, 该首部由iphdr数据结构实现, 定义在[include/uapi/linux/ip.h?v=4.7, line 85](http://lxr.free-electrons.com/source/include/uapi/linux/ip.h?v=4.7#L85)
```cpp
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
/*The options start here. */
};
```
`ip_rcv`函数是网络层的入口点. 分组向上穿过内核的路线如下图所示
![图12-15 分组穿过互联网络层的路线](../images/)
发送和接收操作的程序流程并不总是分离的, 如果分组只通过当前计算机转发, 那么发送和接收操作是交织的. 这种分组不会传递到更高的协议层(或应用程序), 而是立即离开计算机, 发往新的目的地.
#3 接收分组
-------
在分组(以及对应的套接字缓冲区,其中的指针已经设置了适当的值)转发到ip_rcv之后, 必须检查接收到的信息, 确保它是正确的。 主要检查计算的校验和与首部中存储的校验和是否一致.
其他的检查包括分组是否达到了IP首部的最小长度,分组的协议是否确实是IPv4(IPv6的接收例程是另一个).
在进行了这些检查之后, 内核并不立即继续对分组的处理, 而是调用一个`netfilter`挂钩, 使得用户空间可以对分组数据进行操作.
`netfilter`挂钩插入到内核源代码中定义好的各个位置, 使得分组能够被
外部动态操作. 挂钩存在于网络子系统的各个位置, 每种挂钩都有一个特别的标记, 例如NF_IP_POST_ROUTING.
在内核到达一个挂钩位置时, 将在用户空间调用对该标记支持的例程. 接下来, 在另一个内核函数中继续内核端的处理(分组可能被修改过).
12.8.6节讨论了netfilter机制的实现。
在下一步中,接收到的分组到达一个十字路口,此时需要判断该分组的目的地是本地系统还是远
程计算机。根据对分组目的地的判断,需要将分组转发到更高层,或转到互联网络层的输出路径上(这
里不打算讨论第三种选项,即通过多播将分组发送到一组计算机)。
ip_route_input负责选择路由。这个相对复杂的决策过程在12.8.5节详细讨论。判断路由的结果
是,选择一个函数,进行进一步的分组处理。可用的函数是ip_local_deliver和ip_forward。具体
选择哪个函数,取决于分组是交付到本地计算机下一个更高协议层的例程,还是转发到网络中的另一