diff --git a/study/kernel/04-net/01-description/README.md b/study/kernel/04-net/01-backgroud/README.md similarity index 100% rename from study/kernel/04-net/01-description/README.md rename to study/kernel/04-net/01-backgroud/README.md diff --git a/study/kernel/04-net/02-description/README.md b/study/kernel/04-net/02-description/README.md new file mode 100644 index 0000000..70552a7 --- /dev/null +++ b/study/kernel/04-net/02-description/README.md @@ -0,0 +1,268 @@ +进程虚拟地址空间 +======= + +| 日期 | 内核版本 | 架构| 作者 | 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 Linux 网络栈剖析 +------- + +**从socket 到设备驱动程序** + +`Linux®`操作系统的最大特性之一就是它的网络栈. 它最初源于 BSD 的网络栈,具有一套非常干净的接口,组织得非常好。其接口范围从协议无关层(例如通用 socket 层接口或设备层)到各种网络协议的具体层。本文将从分层角度对 Linux 网络栈的接口进行探索,并介绍其中的一些主要结构. + + +#2 协议简介 +------- + +虽然对于网络的正式介绍一般都参考了 OSI(Open Systems Interconnection)模型, 但是本文对 Linux 中基本网络栈的介绍分为四层的 Internet 模型(如图 1 所示). + + +![图 1. 网络栈的 Internet 模型](../images/network_stack_layout.gif) + + + +这个栈的最底部是**链路层**, 链路层是指提供对物理层访问的设备驱动程序,这可以是各种介质,例如串口链路或以太网设备 + +链路层上面是**网络层**, 它负责将报文定向到目标位置. 再上一层称为传输层,负责端到端的通信(例如,在一台主机内部). 尽管网络层负责管理主机之间的通信,但是传输层需要负责管理主机内部各端之间的通信。最后一层是应用层,它通常是一个语义层,能够理解要传输的数据. 例如,超文本传输协议(HTTP)就负责传输服务器和客户机之间对 Web 内容的请求与响应。 + + +实际来说,网络栈的各个层次有一些更为人所熟知的名字. + +* 在**链路层**上,可以找到以太网, 这是最常用的一种高速介质. 更早的链路层协议包括一些串口协议,例如 SLIP(Serial Line Internet Protocol)、CSLIP(Compressed SLIP)和PPP(Point-to-Point Protocol) + +* 最常见的**网络层**协议是 IP(Internet Protocol),但是网络层中还存在一些满足其他需求的协议,例如 ICMP(Internet Control Message Protocol)和ARP( Address Resolution Protocol) + +* 在**传输层**上是 TCP(Transmission Control Protocol)和 UDP(User Datagram Protocol) + +* 最后, **应用层**中包含很多大家都非常熟悉的协议,包括标准的 Web 协议 HTTP 和电子邮件协议 SMTP(Simple Mail Transfer Protocol). + +
+ +| TCP/IP层 | 协议 | +|:----------:|:-----:| +| 数据链路层 | ARP,RARP | +| 网络层 | IP,ICMP,IGMP | +| 传输层 | TCP ,UDP,UGP | +| 应用层 | Telnet,FTP,SMTP,SNMP | + + +#3 核心网络架构 +------- + +现在继续了解Linux网络栈的架构以及如何实现这种Internet模型. + +下图提供了 Linux 网络栈的高级视图 + + + +![图 2. Linux 高级网络栈架构](../images/advance_network_stack_layout.gif) + + + +最上面是用户空间层,或称为**应用层(Application layout)**,其中定义了网络栈的用户 + +底部是**物理设备(Physical device hardware)**, 提供了对网络的连接能力(串口或诸如以太网之类的高速网络). + +中间是内核空间, 即**网络子系统**, 也是本文介绍的重点. 流经网络栈内部的是socket缓冲区(sk_buffs), 它负责在源和汇点之间传递报文数据. + +| 层次 | 名称 |描述 | +|:-----:|:-----:|:----:| +| Application layout | 应用层 | 网络栈的用户, 即使用网络的各个应用程序 | +| Kernel Space | 内核空间的网络子系统 | 它简单地为用户空间的应用程序提供了一种访问内核网络子系统的方法
流经网络栈内部的是socket缓冲区(sk_buffs), 它负责在源和汇点之间传递报文数据 | +| Physical device hardware | 物理设备 | 提供了对网络的连接能力(串口或诸如以太网之类的高速网络). | + +
+>* 内核在应用程序与网络物理设备之间插入了一个内核空间 +> +>* 内核空间为驱动着网络设备的正常运转, 同时又为应用程序提供可供使用的接口 +
+ + +首先,让我们来快速浏览一下**Linux网络子系统的核心元素**, 后续章节中会更详细进行介绍. + +顶部是**系统调用接口(System call interface)**, 它简单地为用户空间的应用程序提供了一种访问内核网络子系统的方法 + +位于其下面的是一个**协议无关层(Protocol agnostic interface)**, 它提供了一种通用方法来使用底层传输层协议 + +然后是**实际协议(Network protocols)** , 在 Linux中包括内嵌的协议TCP、UDP, 当然还有IP. + +然后是另外一个**设备无关层(Device agnostic interface)**, 提供了与各个设备驱动程序通信的通用接口 + +最下面是**设备驱动程序(Device driders)**本身. + +如下所示 + +| 内核空间层次 | 名称 |描述 | +|:--------------:|:-----:|:----:| +| System call interface | 系统调用接口 | 它简单地为用户空间的应用程序提供了一种访问内核网络子系统的方法 | +| Protocol agnostic interface | 协议无关层 | 它提供了一种通用方法来使用底层传输层协议 | +| Network protocols | 实际协议 | 在 Linux中包括内嵌的协议TCP、UDP, 当然还有IP | +| Device agnostic interface | 设备无关层 | 提供了与各个设备驱动程序通信的通用接口 | +| Device driders | 设备驱动程序 | 提供了网络硬件设备的基础驱动 | + +
+>内核并没有简单的为内核网络子系统实现常规的系统调用接口, 协议层和设备驱动层次3个简单的层次, Linux内核必须支持各种不同的协议和不同的设备, 没有什么问题是通过一个虚拟层无法实现的, 因此内核在不同的层次之间插入了一个协议或者设备无关的层次 +> +>* 系统调用接口和实际协议层之间插入了一个协议无关层 +> +>* 实际协议层与设备驱动程序之间插入了一个设备无关层 +
+ + +##3.1 系统调用接口 +------- + + +系统调用接口可以从两个角度进行描述. 用户发起网络调用时, 通过系统调用接口进入内核的过程应该是多路的. 最后调用[`net/socket.c`](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2315)中的[`sys_socketcall` ](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2340)结束该过程, 然后进一步将调用分路发送到指定目标. + +系统调用接口的另一种描述是使用普通文件操作作为网络I/O. 例如, 典型的读写操作可以在网络`socket`上执行(`socket`使用一个文件描述符表示, 与一个普通文件一样). 因此, 尽管有很多操作是网络专用的(使用`socket`调用创建一个 `socket`, 使用`connect`调用连接一个收信方, 等等), 但是也有一些标准的文件操作可以应用于网络对象, 就像操作普通文件一样. + +最后, 系统调用接口提供了在用户空间应用程序和内核之间转移控制的方法. + + +| 系统调用 | 宏 |描述 | +|:---------:|:------:| +| socketcall | []() | socket系统调用 | +| socket | [SYS_SOCKET](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2341) | 建立socket | +| bind | [SYS_BIND](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2345) | 绑定socket到端口 | +| connect | [SYS_CONNECT](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2347) |连接远程主机 | +| listen | [SYS_LISTEN](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2350) | 监听socket端口 | +| accept | [SYS_ACCEPT](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2353) | 响应socket连接请求 | +| getsockname | [SYS_GETSOCKNAME](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2357) | 取得本地socket名字 | +| getpeername | [SYS_GETPEERNAME](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2362) | 获取通信对方的socket名字 | +| socketpair | [SYS_SOCKETPAIR](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2367) | 创建一对已联接的无名socket | +| send | [SYS_SEND](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2370) | 通过socket发送信息 | +| sendto | [SYS_SENDTO](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2373) | 发送UDP信息 | +| recv | [SYS_RECV](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2377) | 通过socket接收信息 | +| recvfrom | [SYS_RECVFROM](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2380) | 接收UDP信息 | +| recvmsg | 参见recv | +| select | 对多路同步I/O进行轮询 | +| shutdown | [SYS_SHUTDOWN](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2385) | 关闭socket上的连接 | +| getsockopt | [SYS_GETSOCKOPT](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2391) | 取端口设置 | +| setsockopt | [SYS_SETSOCKOPT](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2388) | 设置端口参数 | +| sendmsg | [SYS_SENDMSG](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2396) | 参见send | +| sendmmesg | [SYS_SENDMMSG](http://lxr.free-electrons.com/source/net/socket.c?v=4.7#L2399) | | +| | SYS_RECVMSG | | +| | SYS_RECVMMSG | | +| | SYS_ACCEPT4 | | +| sendfile | 在文件或端口间传输数据 | + + +##3.2 协议无关接口 +------- + +`socket`层是一个协议无关接口, 它提供了一组通用函数来支持各种不同协议. + +`socket`层不但可以支持典型的TCP和UDP协议, 而且还可以支持IP、裸以太网和其他传输协议, 例如 SCTP(Stream Control Transmission Protocol). + +通过网络栈进行的通信都需要对`socket`进行操作. + +`Linux`中的`socket`结构是`struct sock``, 这个结构是在`linux/include/net/sock.h`中定义的. 这个巨大的结构中包含了特定`socket`所需要的所有状态信息, 其中包括`socket`所使用的特定协议和在`socket`上可以执行的一些操作. + +网络子系统可以通过一个定义了自己功能的特殊结构来了解可用协议. 每个协议都维护了一个名为`proto`的结构(可以在`linux/include/net/sock.h`中找到). 这个结构定义了可以在从`socket`层到传输层中执行特定的`socket`操作(例如, 如何创建一个`socket`, 如何使用`socket`建立一个连接, 如何关闭一个`socket` 等等). + + + +##3.3 网络协议 +------- + + +网络协议这一节对一些可用的特定网络协议作出了定义(例如TCP、UDP等). 它们都是在[`linux/net/ipv4/af_inet.c`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c)文件中一个名为[`inet_init`的函数](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L1754)中进行初始化的(因为TCP和UDP都是inet簇协议的一部分). + +[`inet_init`函数](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L1754)使用[`proto_register`](http://lxr.free-electrons.com/source/net/core/sock.c?v=4.7#L2902)函数来注册每个内嵌协议. 这个函数是在[`linux/net/core/sock.c`](http://lxr.free-electrons.com/source/net/core/sock.c?v=4.7)中定义的, 除了可以将这个协议添加到活动协议列表中之外, 如果需要, 该函数还可以选择分配一到多个`slab` 缓存. + +通过[`net/ipv4`](http://lxr.free-electrons.com/source/net/ipv4/?v=4.7)目录中[`udp.c`](http://lxr.free-electrons.com/source/net/ipv4/udp.c?v=4.7)和[`raw.c`](http://lxr.free-electrons.com/source/net/ipv4/raw.c?v=4.7)文件中的`proto`接口, 您可以了解各个协议是如何标识自己的. 这些协议接口每个都按照类型和协议映射到[`inetsw_array`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L999), 该数组将内嵌协议与操作映射到一起. [`inetsw_array`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L999)结构及其关系如图3 所示 + +![图 3. Internet 协议数组结构](../images/data_struct.gif) + + +最初, 会调用[`inet_init`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L1754)中的[`inet_register_protosw`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L1037)将这个数组中的每个协议都初始化为[`inetsw`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L128), 函数[`inet_init`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L1754)也会对各个`inet`模块进行初始化, 例如ARP、ICMP和IP模块, 以及TCP和UDP模块. + +>Socket 协议的相互关系 +> +>回想以下在创建 socket 时,需要指定类型和协议,例如my_sock = socket( AF_INET, SOCK_STREAM, 0 )。AF_INET 表示一个 Internet 地址簇,它使用的是一个流 socket,定义为 SOCK_STREAM(如此处的 inetsw_array 所示). + + +注意在图中, [`proto`](http://lxr.free-electrons.com/source/include/net/sock.h?v=4.7#L970)结构定义了传输特有的方法, 而[`proto_ops`](http://lxr.free-electrons.com/source/include/linux/net.h?v=4.7#L132)结构则定义了通用的`socket`方法. 可以通过调用[`inet_register_protosw`](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.7#L1037)将其他协议加入到 inetsw协议中. 例如, SCTP就是通过调用[`net/sctp/protocol.c`](http://lxr.free-electrons.com/source/net/sctp/protocol.c?v=4.7) 中的 [`sctp_init`](http://lxr.free-electrons.com/source/net/sctp/protocol.c#L1353?v=4.7)加入其中的 + +有关`SCTP`的更多信息,请参阅[SCTP优化网络](http://www.ibm.com/developerworks/cn/linux/l-sctp/) + +`socket`中的数据移动是使用一个所谓的`socket`缓冲区([`sk_buff`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L626))的核心结构实现的. + +`sk_buff`中包含了报文数据, 以及涉及协议栈中多个层次的状态数据. 所发送或接收的每个报文都是使用一个`sk_buff `示的. `sk_buff`结构是在 [`linux/include/linux/skbuff.h`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7)中定义的, 如下图所示. + + +![图 4. Socket 缓冲区及其与其他结构的关系](../images/sk_buff_connection.gif) + + + + +多个`sk_buff`可以针对某个给定连接链接在一起. 每个`sk_buff`都在设备结构(`net_device`)中标识报文发送的目的地, 或者接收报文的来源地. 由于每个报文都是使用一个`sk_buff`表示的, 因此报文头都可以通过一组指针(th、iph 和mac[用于Media Access Control或者MAC头])方便地进行定位. + + +![多个`sk_buff`可以针对某个给定连接链接在一起](../images/dou_link.png) + + +由于`sk_buff`是`socket`数据管理的中心, 因此创建了很多支持函数来对它们进行管理. 其中有些函数用于创建和销毁`sk_buff`结构, 或对它进行克隆或排队管理. + + +针对给定的`socket`, `Socket`缓冲区可以链接在一起, 这样可以包含众多信息, 包括到协议头的链接、时间戳(报文是何时发送或接收的), 以及与这个报文相关的设备. + + +##3.4 设备无关接口 +------- + + +协议层下面是另外一个无关接口层, 它将协议与具有很多各种不同功能的硬件设备连接在一起。这一层提供了一组通用函数供底层网络设备驱动程序使用,让它们可以对高层协议栈进行操作. + + +* 首先, 设备驱动程序可能会通过调用[`register_netdevice`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L6983)或[`unregister_netdevice`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2361)在内核中进行注册或注销. + + 1. 调用者首先填写[`net_device`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L1607)结构 + + 2. 然后传递这个结构进行注册. 内核调用它的init函数(如果定义了这种函数), 然后执行一组健全性检查, 并创建一个`sysfs`条目, 然后将新设备添加到设备列表中(内核中的活动设备链表). 在[`linux/include/linux/netdevice.h`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7)中可以找到这个[`net_device`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L1607)结构 + + 3. 这些函数都是在[`net/core/dev.c`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7)中实现的 + +* 要从协议层向设备中发送`sk_buff`, 就需要使用[`dev_queue_xmit`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3432)函数. 这个函数可以对`sk_buff`进行排队, 从而由底层设备驱动程序进行最终传输(使用`sk_buff`中引用的[`net_device`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L1607)或 [`sk_buff->dev`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L641)所定义的网络设备). dev结构[`net_device`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L1607)中包含了一个名为hard_start_xmit的方法(位于`net_device->net_device_ops->ndo_start_xmit`), 其中保存有发起`sk_buff`传输所使用的驱动程序函数. + +* 报文的接收通常是使用[`netif_rx`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3830)执行的. + + 1. 当底层设备驱动程序接收一个报文(包含在所分配的`sk_buff`中)时, 就会通过调用[`netif_rx`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3830)将[`sk_buff`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L626)上传至网络层. + + 2. 然后, 这个函数通过`__napi_schedule`(早期内核通过`netif_rx_schedule`)将 sk_buff 在上层协议队列中进行排队, 供以后进行处理。可以在 linux/net/core/dev.c 中找到[`dev_queue_xmit`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3432)和[`netif_rx`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3830)函数. + + + + +后来, 内核中引入了一种新的应用程序编程接口(NAPI), 该接口允许驱动程序与设备无关层(dev)进行交互. 有些驱动程序使用的是NAPI, 但是大多数驱动程序仍然在使用老式的帧接收接口(比例大约是6比1). NAPI在高负载的情况下可以产生更好的性能, 它避免了为每个传入的帧都产生中断. + +加入了NAPI后, `netif_rx_schedule`这个函数从内核中删除, 取而代之的是`__napi_schedule`, 可以到[`LXR?v=2.6.30;i=netif_rx_schedule`](http://lxr.free-electrons.com/ident?v=2.6.30;i=netif_rx_schedule)查证. + + +##3.5 设备驱动程序 +------- + + +网络栈底部是负责管理物理网络设备的设备驱动程序. 例如, 包串口使用的 SLIP 驱动程序以及以太网设备使用的以太网驱动程序都是这一层的设备. + +在进行初始化时, 设备驱动程序会分配一个`net_device`结构, 然后使用必须的程序对其进行初始化. + +这些程序中有一个是`hard_start_xmit`的方法(位于`net_device->net_device_ops->ndo_start_xmit`), 它定义了上层应该如何对 `sk_buff`排队进行传输. 这个程序的参数为`sk_buff`. 这个函数的操作取决于底层硬件, 但是通常`sk_buff`所描述的报文都会被移动到硬件环或队列中. 就像是设备无关层中所描述的一样, 对于NAPI兼容的网络驱动程序来说, 帧的接收使用了`netif_rx`和`netif_receive_skb`接口. + +NAPI 驱动程序会对底层硬件的能力进行一些限制. 有关更详细的信息,请参阅[NAPI接口和设计](http://linux-net.osdl.org/index.php/NAPI) + + +设备驱动程序在`dev`结构中配置好自己的接口之后, 调用`register_netdevice`便可以使用该配置. 在[`drivers/net`](http://lxr.free-electrons.com/source/drivers/net?v=4.7)中可以找出网络设备专用的驱动程序. + + +#4 展望 +------- + + +Linux 源代码是学习有关大多数设备类型的设备驱动程序设计最佳方法, 包括网络设备驱动程序. 在这里可以找到的是各种设计的变化以及对可用内核API的使用, 但是所学到的每一点都会非常有用, 都可以作为新设备驱动程序的起点. 除非您需要一种新协议, 否则网络栈中的其余代码都是通用的, 都会非常有用. 即使现在, TCP(用于流协议)或 UDP(用于基于消息的协议)的实现都可以作为开始新开发有用模块使用. + + diff --git a/study/kernel/04-net/03-socket_buffer/README.md b/study/kernel/04-net/03-socket_buffer/README.md index efc357a..7e777fb 100644 --- a/study/kernel/04-net/03-socket_buffer/README.md +++ b/study/kernel/04-net/03-socket_buffer/README.md @@ -262,17 +262,91 @@ struct sk_buff ![套接字缓冲区在各个协议层之间传递时,对缓冲区的操作](../images/trans_data.png) -在字长32位的系统上,数据类型sk_buff_data_t用来表示各种类型为简单指针的数据, 定义在[`include/linux/skbuff.h?v=4.7, line 626`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L626), 在64位 CPU上, 可使用一点小技巧来节省一些空间。sk_buff_data_t的定义改为整型变量 + + + +内核实现了一些宏用来获取缓冲区数据段的结束位置. 这些函数定义在[`include/linux/skbuff.h?v=4.7, line 1165`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L1165) + + ```cpp #ifdef NET_SKBUFF_DATA_USES_OFFSET -typedef unsigned int sk_buff_data_t; +static inline unsigned char *skb_end_pointer(const struct sk_buff *skb) +{ + return skb->head + skb->end; +} + +static inline unsigned int skb_end_offset(const struct sk_buff *skb) +{ + return skb->end; +} #else -typedef unsigned char *sk_buff_data_t; +static inline unsigned char *skb_end_pointer(const struct sk_buff *skb) +{ + return skb->end; +} + +static inline unsigned int skb_end_offset(const struct sk_buff *skb) +{ + return skb->end - skb->head; +} #endif ``` -套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分. 由于网络子系统必须保证较低的内存占用和较高的处理速度, 因而对struct sk_buff来说,我们需要保持该结构的长度尽可能小. +内核实现了一些宏用来获取协议数据区域的结束的结束位置tail的函数. 这些函数定义在[`include/linux/skbuff.h?v=4.7, line 1849`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L1849) + +```cpp +#ifdef NET_SKBUFF_DATA_USES_OFFSET /* 64 bit */ +static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb) +{ + return skb->head + skb->tail; +} + +static inline void skb_reset_tail_pointer(struct sk_buff *skb) +{ + skb->tail = skb->data - skb->head; +} + +static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset) +{ + skb_reset_tail_pointer(skb); + skb->tail += offset; +} + +#else /* NET_SKBUFF_DATA_USES_OFFSET */ +static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb) +{ + return skb->tail; +} + +static inline void skb_reset_tail_pointer(struct sk_buff *skb) +{ + skb->tail = skb->data; +} + +static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset) +{ + skb->tail = skb->data + offset; +} + +#endif /* NET_SKBUFF_DATA_USES_OFFSET */ +``` + +在字长32位的系统上,数据类型sk_buff_data_t用来表示各种类型为简单指针的数据(`char *`), 定义在[`include/linux/skbuff.h?v=4.7, line 487`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L487), 在64位 CPU上, 可使用一点小技巧来节省一些空间. `sk_buff_data_t`的定义改为整型变量 + +```cpp +#if BITS_PER_LONG > 32 /* 64位的linux系统中long是8个字节 > 4 */ +#define NET_SKBUFF_DATA_USES_OFFSET 1 +#endif + +#ifdef NET_SKBUFF_DATA_USES_OFFSET /* 64位系统 */ +typedef unsigned int sk_buff_data_t; /* 4个字节 */ +#else /* 32位系统 */ +typedef unsigned char *sk_buff_data_t; /* 4个字节 */ +#endif +``` + +套接字缓冲区需要很多指针来表示缓冲区中内容的不同部分. 由于网络子系统必须保证较低的内存占用和较高的处理速度, 因而对`struct sk_buff`来说,我们需要保持该结构的长度尽可能小. @@ -447,3 +521,50 @@ struct sk_buff_head { 分组通常放置在等待队列中,例如分组等待处理时,或需要重新组合已经分析过的分组时. ![通过双链表管理套接字缓冲区](../images/dou_link.png) + + + +##4.3.3 skb_shared_info结构体 +------- + + +内核`skb_shared_info`结构体该类型用来管理数据包分片信息, 该结构定义在[`include/linux/skbuff.h?v=4.7, line 408`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L408) + +```cpp +/* This data is invariant across clones and lives at + * the end of the header data, ie. at skb->end. + */ +struct skb_shared_info { + unsigned char nr_frags; + __u8 tx_flags; + unsigned short gso_size; /* 尺寸 */ + /* Warning: this field is not always filled in (UFO)! */ + unsigned short gso_segs; /* 顺序 */ + unsigned short gso_type; + struct sk_buff *frag_list; + struct skb_shared_hwtstamps hwtstamps; /* 硬件时间戳 */ + u32 tskey; + __be32 ip6_frag_id; + + /* + * Warning : all fields before dataref are cleared in __alloc_skb() + */ + atomic_t dataref; /* 使用计数 */ + + /* Intermediate layers must ensure that destructor_arg + * remains valid until skb destructor */ + void * destructor_arg; + + /* must be last field, see pskb_expand_head() */ + skb_frag_t frags[MAX_SKB_FRAGS]; +}; +``` + + + 通过宏可以表示与skb的关系 + + +```cpp +/* Internal */ +#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB))) +``` \ No newline at end of file diff --git a/study/kernel/04-net/04-/README.md b/study/kernel/04-net/04-网络访问层/01-net_device/README.md similarity index 100% rename from study/kernel/04-net/04-/README.md rename to study/kernel/04-net/04-网络访问层/01-net_device/README.md diff --git a/study/kernel/04-net/05/README.md b/study/kernel/04-net/04-网络访问层/02-receive/README.md similarity index 58% rename from study/kernel/04-net/05/README.md rename to study/kernel/04-net/04-网络访问层/02-receive/README.md index ea08c89..0f11a3c 100644 --- a/study/kernel/04-net/05/README.md +++ b/study/kernel/04-net/04-网络访问层/02-receive/README.md @@ -1,215 +1,379 @@ -进程虚拟地址空间 -======= - -| 日期 | 内核版本 | 架构| 作者 | 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) | - - -#2 接受分组 -------- - - -分组到达内核的时间是不可预测的. 所有现代的设备驱动程序都使用中断来通知内核(或系统)有分组到达. 网络驱动程序对特定于设备的中断设置了一个处理例程, 因此每当该中断被引发时(即分组到达), 内核都调用该处理程序, 将数据从网卡传输到物理内存, 或通知内核在一定时间后进行处理. - -几乎所有的网卡都支持DMA模式, 能够自行将数据传输到物理内存. 但这些数据仍然需要解释和处理,这在稍后进行. - -##2.1 传统方法 -------- - -当前, 内核为分组的接收提供了两个框架. 其中一个很早以前就集成到内核中了, 因而称为传统方法. 但与超高速网络适配器协作时, 该API会出现问题,因而网络子系统的开发者已经设计了一种新的API(通常称为NAPI 1). 我们首先从传统方法开始, 因为它比较易于理解. 另外, 使用旧API的适配器较多, 而使用新API的较少. 这没有问题, 因为其物理传输速度没那么高, 不需要新方法. NAPI在稍后讨论 - -图12-10给出了在一个分组到达网络适配器之后,该分组穿过内核到达网络层函数的路径 - - -因为分组是在中断上下文中接收到的, 所以处理例程只能执行一些基本的任务,避免系统(或当前CPU)的其他任务延迟太长时间. - -在中断上下文中, 数据由3个短函数2处理, 执行了下列任务. - -![接收到的分组穿过内核的路径]() - - -1. net_interrupt是由设备驱动程序设置的中断处理程序. 它将确定该中断是否真的是由接收到的分组引发的(也存在其他的可能性, 例如, 报告错误或确认某些适配器执行的传输任务). 如果确实如此,则控制将转移到`net_rx`. - -2. `net_rx`函数也是特定于网卡的, 首先创建一个新的套接字缓冲区. 分组的内容接下来从网卡传输到缓冲区(也就是进入了物理内存), 然后使用内核源代码中针对各种传输类型的库函数来分析首部数据. 这项分析将确定分组数据所使用的网络层协议,例如IP协议. - -3. 与上述两个方法不同, `netif_rx`函数不是特定于网络驱动程序的,该函数位于`net/core/dev.c`. 调用该函数,标志着控制由特定于网卡的代码转移到了网络层的通用接口部分. - -该函数的作用在于, 将接收到的分组放置到一个特定于CPU的等待队列上, 并退出中断上下文, 使得CPU可以执行其他任务. - -内核在全局定义的softnet_data数组中管理进出分组的等待队列, 数组项类型为softnet_data. 为提高多处理器系统的性能, 对每个CPU都会创建等待队列, 支持分组的并行处理. - -不心使用显式的锁机制来保护等待队列免受并发访问, 因为每个CPU都只修改自身的队列, 不会干扰其他CPU的工作. 下文将忽略多处理器相关内容, 只考虑单"softnet_data等待队列", 避免过度复杂化. - - -定义[`include/linux/netdevice.h?v=4.7, line 2719`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2719) - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/netdevice.h#L2719 -/* - * Incoming packets are placed on per-CPU queues - */ -struct softnet_data { - struct list_head poll_list; - struct sk_buff_head process_queue; - - /* stats */ - unsigned int processed; - unsigned int time_squeeze; - unsigned int received_rps; -#ifdef CONFIG_RPS - struct softnet_data *rps_ipi_list; -#endif -#ifdef CONFIG_NET_FLOW_LIMIT - struct sd_flow_limit __rcu *flow_limit; -#endif - struct Qdisc *output_queue; - struct Qdisc **output_queue_tailp; - struct sk_buff *completion_queue; - -#ifdef CONFIG_RPS - /* input_queue_head should be written by cpu owning this struct, - * and only read by other cpus. Worth using a cache line. - */ - unsigned int input_queue_head ____cacheline_aligned_in_smp; - - /* Elements below can be accessed between CPUs for RPS/RFS */ - struct call_single_data csd ____cacheline_aligned_in_smp; - struct softnet_data *rps_ipi_next; - unsigned int cpu; - unsigned int input_queue_tail; -#endif - unsigned int dropped; - struct sk_buff_head input_pkt_queue; - struct napi_struct backlog; - -}; -``` - -目前只对该数据结构的一个成员`input_pkt_queue`感兴趣: - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2753 -struct softnet_data { - struct sk_buff_head input_pkt_queue; -} -``` - -* `input_pkt_queue`使用上文提到的`sk_buff_head`表头, 对所有进入的分组建立一个链表. - -* [`netif_rx`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3830)在结束工作之前将软中断`NET_RX_SOFTIRQ`标记为即将执行, 然后退出中断上下文. - -* [`net_rx_action`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L5177)用作该软中断的处理程序. 其代码流程图在图12-11给出. 请记住, 这里描述的是一个简化的版本. 完整版包含了对高速网络适配器引入的新方法, 将在下文介绍. - - -![图12-11 net_rx_action 的代码流程图]() - - -在一些准备任务之后,工作转移到[`process_backlog`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L4809), 该函数在循环中执行下列步骤. 为简化描述, 假定循环一直进行, 直至所有的待决分组都处理完成,不会被其他情况中断. - -1. [`__skb_dequeue`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L1741)从等待队列移除一个套接字缓冲区, 该缓冲区管理着一个接收到的分组. - -2. 由[`netif_receive_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L4290)函数分析分组类型, 以便根据分组类型将分组传递给网络层的接收函数(即传递到网络系统的更高一层). 为此, 该函数遍历所有可能负责当前分组类型的所有网络层函数, 一一调用[`deliver_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L1807). - -接下来[`deliver_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L1807)函数使用一个特定于分组类型的处理程序[`func`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2131), 承担对分组的更高层(例如互联网络层)的处理. - -[`netif_receive_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L4290)也处理诸如桥接之类的专门特性, 但讨论这些边角情况是不必要的, 至少在平均水准的系统中, 此类特性都属于边缘情况. - -所有用于从底层的网络访问层接收数据的网络层函数都注册在一个散列表中, 通过全局数组[`ptype_base`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L153)实现. - -新的协议通过[`dev_add_pack`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L397)增加. 各个数组项的类型为[`struct packet_type`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2128), 定义如下: - -```cpp -// http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2128 -struct packet_type { - __be16 type; /* This is really htons(ether_type). */ - struct net_device *dev; /* NULL is wildcarded here */ - int (*func) (struct sk_buff *, - struct net_device *, - struct packet_type *, - struct net_device *); - bool (*id_match)(struct packet_type *ptype, - struct sock *sk); - void *af_packet_priv; - struct list_head list; -}; -``` - -| 字段 | 描述 | -|:---:|:----:| -| type | 指定了协议的标识符,处理程序会使用该标识符 | -| dev | 将一个协议处理程序绑定到特定的网卡(NULL 指针表示该处理程序对系统中所有网络设备都有效) | -| func | 是该结构的主要成员。它是一个指向网络层函数的指针,如果分组的类型适当,将其传递给该函数. 其中一个处理程序就是 ip_rcv ,用于基于IPv4的协议,在下文讨论 | - -`netif_receive_skb`对给定的套接字缓冲区查找适当的处理程序, 并调用其`func`函数, 将处理分组的职责委托给网络层, 这是网络实现中更高的一层. - - - -##2.2 对高速接口的支持 -------- - -如果设备不支持过高的传输率, 那么此前讨论的旧式方法可以很好地将分组从网络设备传输到内核的更高层. 每次一个以太网帧到达时, 都使用一个IRQ来通知内核. - -这里暗含着"快"和"慢"的概念. 对低速设备来说, 在下一个分组到达之前, IRQ的处理通常已经结束. 由于下一个分组也通过IRQ通知, 如果前一个分组的IRQ尚未处理完成, 则会导致问题, 高速设备通常就是这样. 现代以太网卡的运作高达`10000 Mbit/s`, 如果使用旧式方法来驱动此类设备, 将造成所谓的"中断风暴". 如果在分组等待处理时接收到新的IRQ, 内核不会收到新的信息 : 在分组进入处理过程之前, 内核是可以接收IRQ的, 在分组的处理结束后, 内核也可以接收IRQ, 这些不过是"旧闻"而已. 为解决该问题, NAPI使用了IRQ和轮询的组合. - -假定某个网络适配器此前没有分组到达, 但从现在开始, 分组将以高频率频繁到达. 这就是NAPI设备的情况, 如下所述. - -1. 第一个分组将导致网络适配器发出IRQ. 为防止进一步的分组导致发出更多的IRQ, 驱动程序会关闭该适配器的Rx IRQ. 并将该适配器放置到一个轮询表上. - -2. 只要适配器上还有分组需要处理, 内核就一直对轮询表上的设备进行轮询. - -3. 重新启用Rx中断. - - -如果在新的分组到达时, 旧的分组仍然处于处理过程中, 工作不会因额外的中断而减速. 虽然对设备驱动程序(和一般意义上的内核代码)来说轮询通常是一个很差的方法, 但在这里该方法没有什么不利之处:在没有分组还需要处理时,将停止轮询,设备将回复到通常的IRQ驱动的运行方式. 在没有中断支持的情况下, 轮询空的接收队列将不必要地浪费时间, 但NAPI并非如此. - -NAPI的另一个优点是可以高效地丢弃分组. 如果内核确信因为有很多其他工作需要处理, 而导致无法处理任何新的分组,那么网络适配器可以直接丢弃分组,无须复制到内核. - -只有设备满足如下两个条件时,才能实现NAPI方法。 - -1. 设备必须能够保留多个接收的分组,例如保存到DMA环形缓冲区中. 下文将该缓冲区称为Rx缓冲区. - -2. 该设备必须能够禁用用于分组接收的IRQ. 而且, 发送分组或其他可能通过IRQ进行的操作, 都仍然必须是启用的。 - -如果系统中有多个设备, 会怎么样呢? 这是通过循环轮询各个设备来解决的. 图12-12概述了这种情况. - -![图12-12 NAPI机制和循环轮询表概览]() - - -回想前文提到的, 如果一个分组到达一个空的Rx缓冲区, 则将相应的设备置于轮询表中. 由于链表本身的性质, 轮询表可以包含多个设备. - -内核以循环方式处理链表上的所有设备 : 内核依次轮询各个设备, 如果已经花费了一定的时间来处理某个设备, 则选择下一个设备进行处理. 此外, 某个设备都带有一个相对权重, 表示与轮询表中其他设备相比, 该设备的相对重要性. 较快的设备权重较大,较慢的设备权重较小. 由于权重指定了在一个轮询的循环中处理多少分组, 这确保了内核将更多地注意速度较快的设备. - -现在我们已经弄清楚了NAPI的基本原理, 接下来将讨论其实现细节. 与旧的API相比, 关键性的变化在于, 支持NAPI的设备必须提供一个 poll函数. 该方法是特定于设备的, 在用`netif_napi_add`注册网卡时指定. 调用该函数注册, 表明设备可以且必须用新方法处理. - -```CPP - -static inline void netif_napi_add(struct net_device *dev, -struct napi_struct *napi, -int (*poll)(struct napi_struct *, int), -int weight); -``` - -| 参数 | 描述 | -|:----:|:----:| -| dev | 指向所述设备的`net_device`实例 | -| poll | 指定了在IRQ禁用时用来轮询设备的函数 | -| weight | 指定了设备接口的相对权重。实际上可以对 weight 指定任意整数值。通常10/100 Mbit网卡的驱动程序 -指定为16,而1 000/10 000 Mbit网卡的驱动程序指定为64。无论如何,权重都不能超过该设备可以在 -Rx缓冲区中存储的分组的数目。 -netif_napi_add 还需要另一个参数,是一个指向 struct napi_struct 实例的指针。该结构用于 -管理轮询表上的设备。其定义如下: - -struct napi_struct { -struct list_head poll_list; -}; -unsigned long state; -int weight; -int (*poll)(struct napi_struct *, int); -轮询表通过一个标准的内核双链表实现, poll_list 用作链表元素。 weight 和 poll 的语义同上文 -所述。 state 可以是 NAPI_STATE_SCHED 或 NAPI_STATE_DISABLE ,前者表示设备将在内核的下一次循 -环时被轮询,后者表示轮询已经结束且没有更多的分组等待处理,但设备尚未从轮询表移除。 -请注意, struct napi_struct 经常嵌入到一个更大的结构中,后者包含了与网卡有关的、特定 -于驱动程序的数据。这样在内核使用 poll 函数轮询网卡时,可用 container_of 机制获得相关信息 \ No newline at end of file +进程虚拟地址空间 +======= + +| 日期 | 内核版本 | 架构| 作者 | 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) | + + +#2 接受分组 +------- + + +分组到达内核的时间是不可预测的. 所有现代的设备驱动程序都使用中断来通知内核(或系统)有分组到达. 网络驱动程序对特定于设备的中断设置了一个处理例程, 因此每当该中断被引发时(即分组到达), 内核都调用该处理程序, 将数据从网卡传输到物理内存, 或通知内核在一定时间后进行处理. + +几乎所有的网卡都支持DMA模式, 能够自行将数据传输到物理内存. 但这些数据仍然需要解释和处理,这在稍后进行. + +##2.1 传统方法 +------- + +当前, 内核为分组的接收提供了两个框架. 其中一个很早以前就集成到内核中了, 因而称为传统方法. 但与超高速网络适配器协作时, 该API会出现问题,因而网络子系统的开发者已经设计了一种新的API(通常称为NAPI 1). 我们首先从传统方法开始, 因为它比较易于理解. 另外, 使用旧API的适配器较多, 而使用新API的较少. 这没有问题, 因为其物理传输速度没那么高, 不需要新方法. NAPI在稍后讨论 + +下图出了在一个分组到达网络适配器之后,该分组穿过内核到达网络层函数的路径 + + +因为分组是在中断上下文中接收到的, 所以处理例程只能执行一些基本的任务,避免系统(或当前CPU)的其他任务延迟太长时间. + +在中断上下文中, 数据由3个短函数2处理, 执行了下列任务. + +![接收到的分组穿过内核的路径](../images/receive_kernel_route.png) + + +1. net_interrupt是由设备驱动程序设置的中断处理程序. 它将确定该中断是否真的是由接收到的分组引发的(也存在其他的可能性, 例如, 报告错误或确认某些适配器执行的传输任务). 如果确实如此,则控制将转移到`net_rx`. + +2. `net_rx`函数也是特定于网卡的, 首先创建一个新的套接字缓冲区. 分组的内容接下来从网卡传输到缓冲区(也就是进入了物理内存), 然后使用内核源代码中针对各种传输类型的库函数来分析首部数据. 这项分析将确定分组数据所使用的网络层协议,例如IP协议. + +3. 与上述两个方法不同, `netif_rx`函数不是特定于网络驱动程序的,该函数位于`net/core/dev.c`. 调用该函数,标志着控制由特定于网卡的代码转移到了网络层的通用接口部分. + +该函数的作用在于, 将接收到的分组放置到一个特定于CPU的等待队列上, 并退出中断上下文, 使得CPU可以执行其他任务. + +内核在全局定义的softnet_data数组中管理进出分组的等待队列, 数组项类型为softnet_data. 为提高多处理器系统的性能, 对每个CPU都会创建等待队列, 支持分组的并行处理. + +不心使用显式的锁机制来保护等待队列免受并发访问, 因为每个CPU都只修改自身的队列, 不会干扰其他CPU的工作. 下文将忽略多处理器相关内容, 只考虑单"softnet_data等待队列", 避免过度复杂化. + + +定义[`include/linux/netdevice.h?v=4.7, line 2719`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2719) + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/netdevice.h#L2719 +/* + * Incoming packets are placed on per-CPU queues + */ +struct softnet_data { + struct list_head poll_list; + struct sk_buff_head process_queue; + + /* stats */ + unsigned int processed; + unsigned int time_squeeze; + unsigned int received_rps; +#ifdef CONFIG_RPS + struct softnet_data *rps_ipi_list; +#endif +#ifdef CONFIG_NET_FLOW_LIMIT + struct sd_flow_limit __rcu *flow_limit; +#endif + struct Qdisc *output_queue; + struct Qdisc **output_queue_tailp; + struct sk_buff *completion_queue; + +#ifdef CONFIG_RPS + /* input_queue_head should be written by cpu owning this struct, + * and only read by other cpus. Worth using a cache line. + */ + unsigned int input_queue_head ____cacheline_aligned_in_smp; + + /* Elements below can be accessed between CPUs for RPS/RFS */ + struct call_single_data csd ____cacheline_aligned_in_smp; + struct softnet_data *rps_ipi_next; + unsigned int cpu; + unsigned int input_queue_tail; +#endif + unsigned int dropped; + struct sk_buff_head input_pkt_queue; + struct napi_struct backlog; + +}; +``` + +目前只对该数据结构的一个成员`input_pkt_queue`感兴趣: + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2753 +struct softnet_data { + struct sk_buff_head input_pkt_queue; +} +``` + +* `input_pkt_queue`使用上文提到的`sk_buff_head`表头, 对所有进入的分组建立一个链表. + +* [`netif_rx`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L3830)在结束工作之前将软中断`NET_RX_SOFTIRQ`标记为即将执行, 然后退出中断上下文. + +* [`net_rx_action`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L5177)用作该软中断的处理程序. 其代码流程图在图12-11给出. 请记住, 这里描述的是一个简化的版本. 完整版包含了对高速网络适配器引入的新方法, 将在下文介绍. + + +![图12-11 net_rx_action 的代码流程图](../images/net_rx_action.png) + + +在一些准备任务之后,工作转移到[`process_backlog`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L4809), 该函数在循环中执行下列步骤. 为简化描述, 假定循环一直进行, 直至所有的待决分组都处理完成,不会被其他情况中断. + +1. [`__skb_dequeue`](http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=4.7#L1741)从等待队列移除一个套接字缓冲区, 该缓冲区管理着一个接收到的分组. + +2. 由[`netif_receive_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L4290)函数分析分组类型, 以便根据分组类型将分组传递给网络层的接收函数(即传递到网络系统的更高一层). 为此, 该函数遍历所有可能负责当前分组类型的所有网络层函数, 一一调用[`deliver_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L1807). + +接下来[`deliver_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L1807)函数使用一个特定于分组类型的处理程序[`func`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2131), 承担对分组的更高层(例如互联网络层)的处理. + +[`netif_receive_skb`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L4290)也处理诸如桥接之类的专门特性, 但讨论这些边角情况是不必要的, 至少在平均水准的系统中, 此类特性都属于边缘情况. + +所有用于从底层的网络访问层接收数据的网络层函数都注册在一个散列表中, 通过全局数组[`ptype_base`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L153)实现. + +新的协议通过[`dev_add_pack`](http://lxr.free-electrons.com/source/net/core/dev.c?v=4.7#L397)增加. 各个数组项的类型为[`struct packet_type`](http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2128), 定义如下: + +```cpp +// http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=4.7#L2128 +struct packet_type { + __be16 type; /* This is really htons(ether_type). */ + struct net_device *dev; /* NULL is wildcarded here */ + int (*func) (struct sk_buff *, + struct net_device *, + struct packet_type *, + struct net_device *); + bool (*id_match)(struct packet_type *ptype, + struct sock *sk); + void *af_packet_priv; + struct list_head list; +}; +``` + +| 字段 | 描述 | +|:---:|:----:| +| type | 指定了协议的标识符,处理程序会使用该标识符 | +| dev | 将一个协议处理程序绑定到特定的网卡(NULL 指针表示该处理程序对系统中所有网络设备都有效) | +| func | 是该结构的主要成员。它是一个指向网络层函数的指针,如果分组的类型适当,将其传递给该函数. 其中一个处理程序就是 ip_rcv ,用于基于IPv4的协议,在下文讨论 | + +`netif_receive_skb`对给定的套接字缓冲区查找适当的处理程序, 并调用其`func`函数, 将处理分组的职责委托给网络层, 这是网络实现中更高的一层. + + + +##2.2 对高速接口的支持 +------- + + + +###2.2.1 NAPI机制与轮询 +------- + + +如果设备不支持过高的传输率, 那么此前讨论的旧式方法可以很好地将分组从网络设备传输到内核的更高层. 每次一个以太网帧到达时, 都使用一个IRQ来通知内核. + +这里暗含着"快"和"慢"的概念. 对低速设备来说, 在下一个分组到达之前, IRQ的处理通常已经结束. 由于下一个分组也通过IRQ通知, 如果前一个分组的IRQ尚未处理完成, 则会导致问题, 高速设备通常就是这样. 现代以太网卡的运作高达`10000 Mbit/s`, 如果使用旧式方法来驱动此类设备, 将造成所谓的"中断风暴". 如果在分组等待处理时接收到新的IRQ, 内核不会收到新的信息 : 在分组进入处理过程之前, 内核是可以接收IRQ的, 在分组的处理结束后, 内核也可以接收IRQ, 这些不过是"旧闻"而已. 为解决该问题, NAPI使用了IRQ和轮询的组合. + +假定某个网络适配器此前没有分组到达, 但从现在开始, 分组将以高频率频繁到达. 这就是NAPI设备的情况, 如下所述. + +1. 第一个分组将导致网络适配器发出IRQ. 为防止进一步的分组导致发出更多的IRQ, 驱动程序会关闭该适配器的Rx IRQ. 并将该适配器放置到一个轮询表上. + +2. 只要适配器上还有分组需要处理, 内核就一直对轮询表上的设备进行轮询. + +3. 重新启用Rx中断. + + +如果在新的分组到达时, 旧的分组仍然处于处理过程中, 工作不会因额外的中断而减速. 虽然对设备驱动程序(和一般意义上的内核代码)来说轮询通常是一个很差的方法, 但在这里该方法没有什么不利之处:在没有分组还需要处理时,将停止轮询,设备将回复到通常的IRQ驱动的运行方式. 在没有中断支持的情况下, 轮询空的接收队列将不必要地浪费时间, 但NAPI并非如此. + +NAPI的另一个优点是可以高效地丢弃分组. 如果内核确信因为有很多其他工作需要处理, 而导致无法处理任何新的分组,那么网络适配器可以直接丢弃分组,无须复制到内核. + + +只有设备满足如下两个条件时,才能实现NAPI方法. + +1. 设备必须能够保留多个接收的分组,例如保存到DMA环形缓冲区中. 下文将该缓冲区称为Rx缓冲区. + +2. 该设备必须能够禁用用于分组接收的IRQ. 而且, 发送分组或其他可能通过IRQ进行的操作, 都仍然必须是启用的。 + +如果系统中有多个设备, 会怎么样呢? 这是通过循环轮询各个设备来解决的. 图12-12概述了这种情况. + +![图12-12 NAPI机制和循环轮询表概览](../images/napi.png) + + +回想前文提到的, 如果一个分组到达一个空的Rx缓冲区, 则将相应的设备置于轮询表中. 由于链表本身的性质, 轮询表可以包含多个设备. + +内核以循环方式处理链表上的所有设备 : 内核依次轮询各个设备, 如果已经花费了一定的时间来处理某个设备, 则选择下一个设备进行处理. 此外, 某个设备都带有一个相对权重, 表示与轮询表中其他设备相比, 该设备的相对重要性. 较快的设备权重较大,较慢的设备权重较小. 由于权重指定了在一个轮询的循环中处理多少分组, 这确保了内核将更多地注意速度较快的设备. + + +###2.2.2 napi机制的实现细节 +------- + + +现在我们已经弄清楚了NAPI的基本原理, 接下来将讨论其实现细节. 与旧的API相比, 关键性的变化在于, 支持NAPI的设备必须提供一个 poll函数. 该方法是特定于设备的, 在用`netif_napi_add`注册网卡时指定. 调用该函数注册, 表明设备可以且必须用新方法处理. + +```CPP +/** + * netif_napi_add - initialize a NAPI context + * @dev: network device + * @napi: NAPI context + * @poll: polling function + * @weight: default weight + * + * netif_napi_add() must be used to initialize a NAPI context prior to calling + * *any* of the other NAPI-related functions. + */ +void netif_napi_add( + struct net_device *dev, + struct napi_struct *napi, + int (*poll)(struct napi_struct *, int), + int weight); +``` + +| 参数 | 描述 | +|:----:|:----:| +| dev | 指向所述设备的`net_device`实例 | +| napi | 该结构用于管理轮询表上的设备 | +| poll | 指定了在IRQ禁用时用来轮询设备的函数 | +| weight | 指定了设备接口的相对权重。实际上可以对 weight 指定任意整数值。通常10/100 Mbit网卡的驱动程序, 指定为16,而1 000/10 000 Mbit网卡的驱动程序指定为64。无论如何,权重都不能超过该设备可以在Rx缓冲区中存储的分组的数目 | + + +`netif_napi_add`还需要另一个参数,是一个指向`struct napi_struct`实例的指针. 该结构用于管理轮询表上的设备. 其定义如下 : + +```cpp +/* + * Structure for NAPI scheduling similar to tasklet but with weighting + */ +struct napi_struct { + /* The poll_list must only be managed by the entity which + * changes the state of the NAPI_STATE_SCHED bit. This means + * whoever atomically sets that bit can add this napi_struct + * to the per-CPU poll_list, and whoever clears that bit + * can remove from the list right before clearing the bit. + */ + struct list_head poll_list; + + unsigned long state; + int weight; + unsigned int gro_count; + int (*poll)(struct napi_struct *, int); +#ifdef CONFIG_NETPOLL + spinlock_t poll_lock; + int poll_owner; +#endif + struct net_device *dev; + struct sk_buff *gro_list; + struct sk_buff *skb; + struct hrtimer timer; + struct list_head dev_list; + struct hlist_node napi_hash_node; + unsigned int napi_id; +}; +``` + +轮询表通过一个标准的内核双链表实现, `poll_list`用作链表元素. `weight`和 `poll`的语义同上文所述. `state` 以是`NAPI_STATE_SCHED`或`NAPI_STATE_DISABLE`, 前者表示设备将在内核的下一次循环时被轮询, 后者表示轮询已经结束且没有更多的分组等待处理,但设备尚未从轮询表移除. + +请注意, `struct napi_struct`经常嵌入到一个更大的结构中,后者包含了与网卡有关的、特定于驱动程序的数据. 这样在内核使用`poll`函数轮询网卡时,可用 `container_of`机制获得相关信息. + + + +##2.2.3 实现poll函数 +------- + + +`poll`函数需要两个参数 : 一个指向`napi_struct`实例的指针和一个指定了"预算"的整数, 预算表示内核允许驱动程序处理的分组数目. + +我们并不打算处理真实网卡的可能的奇异之处, 因此讨论一个伪函数, 该函数用于一个需要`NAPI`的超高速适配器 : + + +```cpp +static int hyper_card_poll(struct napi_struct *napi, int budget) +{ + struct nic *nic = container_of(napi, struct nic, napi); + struct net_device *netdev = nic->netdev; + int work_done; + work_done = hyper_do_poll(nic, budget); + if (work_done < budget) + { + netif_rx_complete(netdev, napi); + hcard_reenable_irq(nic); + } + return work_done; +} +``` + + +在从`napi_struct`的容器获得特定于设备的信息之后, 调用一个特定于硬件的方法(这里是`hyper_do_poll`)来执行所需要的底层操作从网络适配器获取分组, 并使用像此前那样使用`netif_receive_skb`将分组传递到网络实现中更高的层. + +`hyper_do_poll`最多允许处理`budget`个分组. 该函数返回实际上处理的分组的数目. 必须区分以下两种情况. + +* 如果处理分组的数目小于预算, 那么没有更多的分组, Rx缓冲区为空, 否则, 肯定还需要处理剩余的分组(亦即,返回值不可能小于预算). 因此,netif_rx_complete将该情况通知内核,内核将从轮询表移除该设备。接下来,驱动程序必须通过特定于硬件的适当方法来重新启用IRQ. + +* 已经完全用掉了预算,但仍然有更多的分组需要处理。设备仍然留在轮询表上,不启用中断. + + +###2.2.4 实现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`的单一调用来处理队列中的分组, 而不管分组的来源设备. + + diff --git a/study/kernel/04-net/04-网络访问层/03-send/README.md b/study/kernel/04-net/04-网络访问层/03-send/README.md new file mode 100644 index 0000000..ef69fee --- /dev/null +++ b/study/kernel/04-net/04-网络访问层/03-send/README.md @@ -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结构中都以函数指针的形式出现, 由硬件设备驱动程序实现. \ No newline at end of file diff --git a/study/kernel/04-net/04-网络访问层/images/napi.png b/study/kernel/04-net/04-网络访问层/images/napi.png new file mode 100644 index 0000000..201b6a9 Binary files /dev/null and b/study/kernel/04-net/04-网络访问层/images/napi.png differ diff --git a/study/kernel/04-net/04-网络访问层/images/net_rx_action.png b/study/kernel/04-net/04-网络访问层/images/net_rx_action.png new file mode 100644 index 0000000..adece9d Binary files /dev/null and b/study/kernel/04-net/04-网络访问层/images/net_rx_action.png differ diff --git a/study/kernel/04-net/04-网络访问层/images/net_rx_action2.png b/study/kernel/04-net/04-网络访问层/images/net_rx_action2.png new file mode 100644 index 0000000..d70103e Binary files /dev/null and b/study/kernel/04-net/04-网络访问层/images/net_rx_action2.png differ diff --git a/study/kernel/04-net/04-网络访问层/images/receive_kernel_route.png b/study/kernel/04-net/04-网络访问层/images/receive_kernel_route.png new file mode 100644 index 0000000..f513cc9 Binary files /dev/null and b/study/kernel/04-net/04-网络访问层/images/receive_kernel_route.png differ diff --git a/study/kernel/04-net/05-网络层/README.md b/study/kernel/04-net/05-网络层/README.md new file mode 100644 index 0000000..65abd95 --- /dev/null +++ b/study/kernel/04-net/05-网络层/README.md @@ -0,0 +1,138 @@ +| 日期 | 内核版本 | 架构| 作者 | 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 IP首部的结构](./images/ip_header.png) + + +| 字段 | 描述 | +|:-----:|:-----:| +| version(版本) |指定了所用IP协议的版本。当前,该字段的有效值为4或6。 在支持两种协议版本的主机上,所使用的版本由前一章讨论的传输协议标识符确定。对协议的两个版本来说,该标识符中保存的值是不同的 | +| IHL(IP首部长度) | 定义了首部的长度,由于选项数量可变,这个值并不总是相同的 | +| Codepoint(代码点)
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 " +#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/route.png) + + +该函数定义在[`net/ipv4/ip_input.c?v=4.7, line 405`](http://lxr.free-electrons.com/source/net/ipv4/ip_input.c?v=4.7#L405) + + +| 函数 | 功能 | 定义 | +|:-----:|:-----:|:-----:| +| ip_rcv | 对驱动送上来的数据报文进行ip头的合法性检查, 并调用netfilter过滤器上NF_INET_PRE_ROUTING | [net/ipv4/ip_input.c?v=4.7, line 405](http://lxr.free-electrons.com/source/net/ipv4/ip_input.c?v=4.7#L405) | +| ip_local_deliver | | [net/ipv4/ip_input.c?v=4.7, line 192](http://lxr.free-electrons.com/source/net/ipv4/ip_input.c?v=4.7#L192) | + +
+ +| 函数 | 功能 | 定义 | +|:-----:|:-----:|:-----:| +| ip_queue_xmit | | [net/ipv4/ip_output.c?v=4.7, line 376](http://lxr.free-electrons.com/source/net/ipv4/ip_output.c?v=4.7#L376) | +| ip_output | | [net/ipv4/ip_output.c?v=4.7, line 346](http://lxr.free-electrons.com/source/net/ipv4/ip_output.c?v=4.7#L346) | + +```cpp + +``` + +发送和接收操作的程序流程并不总是分离的, 如果分组只通过当前计算机转发, 那么发送和接收操作是交织的. 这种分组不会传递到更高的协议层(或应用程序), 而是立即离开计算机, 发往新的目的地. + + + +#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。具体 +选择哪个函数,取决于分组是交付到本地计算机下一个更高协议层的例程,还是转发到网络中的另一 \ No newline at end of file diff --git a/study/kernel/04-net/05-网络层/images/ip_header.png b/study/kernel/04-net/05-网络层/images/ip_header.png new file mode 100644 index 0000000..0cb7841 Binary files /dev/null and b/study/kernel/04-net/05-网络层/images/ip_header.png differ diff --git a/study/kernel/04-net/05-网络层/images/route.png b/study/kernel/04-net/05-网络层/images/route.png new file mode 100644 index 0000000..d07f42b Binary files /dev/null and b/study/kernel/04-net/05-网络层/images/route.png differ diff --git a/study/kernel/04-net/1.c b/study/kernel/04-net/1.c deleted file mode 100644 index 0128897..0000000 --- a/study/kernel/04-net/1.c +++ /dev/null @@ -1,260 +0,0 @@ -struct net_device { - char name[IFNAMSIZ]; - struct hlist_node name_hlist; - char *ifalias; - /* - * I/O specific fields - * FIXME: Merge these and struct ifmap into one - */ - unsigned long mem_end; - unsigned long mem_start; - unsigned long base_addr; - int irq; - - atomic_t carrier_changes; - - /* - * Some hardware also needs these fields (state,dev_list, - * napi_list,unreg_list,close_list) but they are not - * part of the usual set specified in Space.c. - */ - - unsigned long state; - - struct list_head dev_list; - struct list_head napi_list; - struct list_head unreg_list; - struct list_head close_list; - struct list_head ptype_all; - struct list_head ptype_specific; - - struct { - struct list_head upper; - struct list_head lower; - } adj_list; - - struct { - struct list_head upper; - struct list_head lower; - } all_adj_list; - - netdev_features_t features; - netdev_features_t hw_features; - netdev_features_t wanted_features; - netdev_features_t vlan_features; - netdev_features_t hw_enc_features; - netdev_features_t mpls_features; - netdev_features_t gso_partial_features; - - int ifindex; - int group; - - struct net_device_stats stats; - - atomic_long_t rx_dropped; - atomic_long_t tx_dropped; - atomic_long_t rx_nohandler; - -#ifdef CONFIG_WIRELESS_EXT - const struct iw_handler_def *wireless_handlers; - struct iw_public_data *wireless_data; -#endif - const struct net_device_ops *netdev_ops; - const struct ethtool_ops *ethtool_ops; -#ifdef CONFIG_NET_SWITCHDEV - const struct switchdev_ops *switchdev_ops; -#endif -#ifdef CONFIG_NET_L3_MASTER_DEV - const struct l3mdev_ops *l3mdev_ops; -#endif - - const struct header_ops *header_ops; - - unsigned int flags; - unsigned int priv_flags; - - unsigned short gflags; - unsigned short padded; - - unsigned char operstate; - unsigned char link_mode; - - unsigned char if_port; - unsigned char dma; - - unsigned int mtu; - unsigned short type; - unsigned short hard_header_len; - - unsigned short needed_headroom; - unsigned short needed_tailroom; - - /* Interface address info. */ - unsigned char perm_addr[MAX_ADDR_LEN]; - unsigned char addr_assign_type; - unsigned char addr_len; - unsigned short neigh_priv_len; - unsigned short dev_id; - unsigned short dev_port; - spinlock_t addr_list_lock; - unsigned char name_assign_type; - bool uc_promisc; - struct netdev_hw_addr_list uc; - struct netdev_hw_addr_list mc; - struct netdev_hw_addr_list dev_addrs; - -#ifdef CONFIG_SYSFS - struct kset *queues_kset; -#endif - unsigned int promiscuity; - unsigned int allmulti; - - - /* Protocol-specific pointers */ - -#if IS_ENABLED(CONFIG_VLAN_8021Q) - struct vlan_info __rcu *vlan_info; -#endif -#if IS_ENABLED(CONFIG_NET_DSA) - struct dsa_switch_tree *dsa_ptr; -#endif -#if IS_ENABLED(CONFIG_TIPC) - struct tipc_bearer __rcu *tipc_ptr; -#endif - void *atalk_ptr; - struct in_device __rcu *ip_ptr; - struct dn_dev __rcu *dn_ptr; - struct inet6_dev __rcu *ip6_ptr; - void *ax25_ptr; - struct wireless_dev *ieee80211_ptr; - struct wpan_dev *ieee802154_ptr; -#if IS_ENABLED(CONFIG_MPLS_ROUTING) - struct mpls_dev __rcu *mpls_ptr; -#endif - -/* - * Cache lines mostly used on receive path (including eth_type_trans()) - */ - unsigned long last_rx; - - /* Interface address info used in eth_type_trans() */ - unsigned char *dev_addr; - -#ifdef CONFIG_SYSFS - struct netdev_rx_queue *_rx; - - unsigned int num_rx_queues; - unsigned int real_num_rx_queues; -#endif - - unsigned long gro_flush_timeout; - rx_handler_func_t __rcu *rx_handler; - void __rcu *rx_handler_data; - -#ifdef CONFIG_NET_CLS_ACT - struct tcf_proto __rcu *ingress_cl_list; -#endif - struct netdev_queue __rcu *ingress_queue; -#ifdef CONFIG_NETFILTER_INGRESS - struct list_head nf_hooks_ingress; -#endif - - unsigned char broadcast[MAX_ADDR_LEN]; -#ifdef CONFIG_RFS_ACCEL - struct cpu_rmap *rx_cpu_rmap; -#endif - struct hlist_node index_hlist; - -/* - * Cache lines mostly used on transmit path - */ - struct netdev_queue *_tx ____cacheline_aligned_in_smp; - unsigned int num_tx_queues; - unsigned int real_num_tx_queues; - struct Qdisc *qdisc; - unsigned long tx_queue_len; - spinlock_t tx_global_lock; - int watchdog_timeo; - -#ifdef CONFIG_XPS - struct xps_dev_maps __rcu *xps_maps; -#endif -#ifdef CONFIG_NET_CLS_ACT - struct tcf_proto __rcu *egress_cl_list; -#endif -#ifdef CONFIG_NET_SWITCHDEV - u32 offload_fwd_mark; -#endif - - /* These may be needed for future network-power-down code. */ - struct timer_list watchdog_timer; - - int __percpu *pcpu_refcnt; - struct list_head todo_list; - - struct list_head link_watch_list; - - enum { NETREG_UNINITIALIZED=0, - NETREG_REGISTERED, /* completed register_netdevice */ - NETREG_UNREGISTERING, /* called unregister_netdevice */ - NETREG_UNREGISTERED, /* completed unregister todo */ - NETREG_RELEASED, /* called free_netdev */ - NETREG_DUMMY, /* dummy device for NAPI poll */ - } reg_state:8; - - bool dismantle; - - enum { - RTNL_LINK_INITIALIZED, - RTNL_LINK_INITIALIZING, - } rtnl_link_state:16; - - void (*destructor)(struct net_device *dev); - -#ifdef CONFIG_NETPOLL - struct netpoll_info __rcu *npinfo; -#endif - - possible_net_t nd_net; - - /* mid-layer private */ - union { - void *ml_priv; - struct pcpu_lstats __percpu *lstats; - struct pcpu_sw_netstats __percpu *tstats; - struct pcpu_dstats __percpu *dstats; - struct pcpu_vstats __percpu *vstats; - }; - - struct garp_port __rcu *garp_port; - struct mrp_port __rcu *mrp_port; - - struct device dev; - const struct attribute_group *sysfs_groups[4]; - const struct attribute_group *sysfs_rx_queue_group; - - const struct rtnl_link_ops *rtnl_link_ops; - - /* for setting kernel sock attribute on TCP connection setup */ -#define GSO_MAX_SIZE 65536 - unsigned int gso_max_size; -#define GSO_MAX_SEGS 65535 - u16 gso_max_segs; - -#ifdef CONFIG_DCB - const struct dcbnl_rtnl_ops *dcbnl_ops; -#endif - u8 num_tc; - struct netdev_tc_txq tc_to_txq[TC_MAX_QUEUE]; - u8 prio_tc_map[TC_BITMASK + 1]; - -#if IS_ENABLED(CONFIG_FCOE) - unsigned int fcoe_ddp_xid; -#endif -#if IS_ENABLED(CONFIG_CGROUP_NET_PRIO) - struct netprio_map __rcu *priomap; -#endif - struct phy_device *phydev; - struct lock_class_key *qdisc_tx_busylock; - bool proto_down; -}; \ No newline at end of file diff --git a/study/kernel/04-net/README.md b/study/kernel/04-net/README.md new file mode 100644 index 0000000..fb9163c --- /dev/null +++ b/study/kernel/04-net/README.md @@ -0,0 +1,47 @@ +进程虚拟地址空间 +======= + +| 日期 | 内核版本 | 架构| 作者 | 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) | + +http://blog.sina.com.cn/s/blog_4b9eab320102vme0.html + +http://blog.csdn.net/geekcome/article/details/7971463 + +http://blog.chinaunix.net/uid-29478572-id-4119158.html + +https://yq.aliyun.com/articles/5862 + +http://blog.sina.com.cn/s/blog_4b9eab320102v9qn.html + +http://blog.csdn.net/shanshanpt/article/details/19918171 + +http://blog.chinaunix.net/uid-21768364-id-2936798.html + +http://www.linuxidc.com/Linux/2012-09/70700.htm + +http://www.oschina.net/question/234345_47845 + +http://xuela-net.iteye.com/blog/1899566 + +http://blog.chinaunix.net/uid-22359610-id-1626525.html + +http://www.360doc.com/content/15/0522/07/18252487_472352496.shtml + +http://www.openstack.cn/?p=4756 + +http://weibo.com/p/1001603837806467238143 + +大家都知道TCP/IP协议栈现在是世界上最流行的网络协议栈,恐怕它的普及的最重要的原因就是其清晰的层次结构以及清晰定义的原语和接口。不仅使得上层应用开发者可以无需关心下层架构或者内部机制,从而相对透明的操作网络。这个明显的层次结构也可以在Linux内核的网络协议栈中观察到。 +主要的参考文献是:Linux网络栈剖析(中文版)/Anatomy of Linux networking stack(英文原版)by Tim Jones. +以及:Linux内核2.4.x的网络接口结构 +另外一些参考资料可以从这个页面找到:http://www.ecsl.cs.sunysb.edu/elibrary/linux/network/ (纽约州立大学石溪分校的页面) +Linux内核网络协议栈采用了如下的层次结构: + +http://www.ibm.com/developerworks/cn/linux/l-linux-networking-stack/ +http://www.ibm.com/developerworks/linux/library/l-linux-networking-stack/ + +http://gjiwwa.blu.livefilestore.com/y1p99O_6n29_58mKHdfUgbvzgKOsD5_7unXA5noegNcJ9Y2h3QMaQ-m0Rb9JGP1v_X4zI7-n7J6aqbOm1xr-HMy56E3YJZbWajO/Linux%E5%86%85%E6%A0%B82.4.x%E7%9A%84%E7%BD%91%E7%BB%9C%E6%8E%A5%E5%8F%A3%E7%BB%93%E6%9E%84.pdf?download + +http://www.ecsl.cs.sunysb.edu/elibrary/linux/network/ \ No newline at end of file diff --git a/study/kernel/04-net/images/advance_network_stack_layout.gif b/study/kernel/04-net/images/advance_network_stack_layout.gif new file mode 100644 index 0000000..6f13a12 Binary files /dev/null and b/study/kernel/04-net/images/advance_network_stack_layout.gif differ diff --git a/study/kernel/04-net/images/data_struct.gif b/study/kernel/04-net/images/data_struct.gif new file mode 100644 index 0000000..f9479ae Binary files /dev/null and b/study/kernel/04-net/images/data_struct.gif differ diff --git a/study/kernel/04-net/images/network_stack_layout.gif b/study/kernel/04-net/images/network_stack_layout.gif new file mode 100644 index 0000000..3eacf56 Binary files /dev/null and b/study/kernel/04-net/images/network_stack_layout.gif differ diff --git a/study/kernel/04-net/images/sk_buff_connection.gif b/study/kernel/04-net/images/sk_buff_connection.gif new file mode 100644 index 0000000..5409a92 Binary files /dev/null and b/study/kernel/04-net/images/sk_buff_connection.gif differ