# 网卡是如何将数据帧发送到网络层
> 本文章内容参考:深入理解Linux网络。
看计算机底层的前提基础是我们有某个方面的知识不是很理解,从而根据这个点在去理解该技术的基础底层,这样的话学习起来不会很枯燥。
我们平常可能都接触过`TCP`网络编程等,但是对于`socket.read`函数读取`client`传送过来的数据其底层是怎么传过来的,我们可能不是很理解,**我们看下面的代码**,如果不特意的去查看看这方面的资料,很可能我们都处于一种这种状态:我调用`socket.read`可以接受对方的数据,但是怎么接受的,就不是很理解了,换句话说就是:我们知道TCP是面向链接的传输层通信协议,但是在一个链接中,`server`端每次具体要读取多少个字节的数据,我们可能并不知道其底层原理,通常我们只需要知道调用`socket.read`就可以获取到`client`发送的一次数据包。所以该系列文章将会以该问题为维度去探究网络底层原理。
```go
func main() {
// server
go func() {
ls, err := net.Listen("tcp", "127.0.0.1:8899")
if err != nil {
log.Fatal(err)
}
for {
accept, err := ls.Accept()
if err != nil {
continue
}
go func(conn net.Conn) {
req := make([]byte, 1024)
size, err := conn.Read(req)
if err != nil {
log.Fatal(err)
}
log.Printf("result: %v", string(req[:size]))
}(accept)
}
}()
time.Sleep(3 * time.Second)
// client.
go func() {
conn, err := net.Dial("tcp", "127.0.0.1:8899")
if err != nil {
log.Fatalln(err)
}
_, err = conn.Write([]byte("hello word"))
if err != nil {
log.Fatalln(err)
}
fmt.Println("client send suc")
}()
time.Sleep(10 * time.Second)
}
```
本文首先将会介绍数据包是怎么从网卡到达网络层的。
## 一、从宏观看网卡是如何接受数据的.

这里先总结一下大概流程:
当网线有数据送达到网卡时,网卡会直接将帧直接`DMA`到内存中(`RingBuffer`)该结构体的具体结构后文会介绍,这个时候数据已经从网卡转移到内存中,此时网卡向`CPU`发送一个硬中断(**对于多队列网卡来说,每个队列可以实现将该队列的硬中断和单个CPU进行绑定从而实现该队列只能由该CPU进行处理**),硬中断只做简单的处理,如果做的处理多了,会导致`CPU`占用时间过程造成`IO`输入输出设备卡顿等,所以在硬中断的回调函数中,只是简简单单的记录了下该类型硬中断的发生频率,并立马调用该类型的软中断,从而尽快释放`CPU`的资源。在硬中断函数内会调用已经注册过的软中断回调函数,在该函数内部将会调用网卡注册过的`poll`函数来进行收包。软中断函数是由`ksoftirqd`内核线程来执行的,每个`Linux`实例中,内核线程`ksoftirqd`的数量和该`CPU`的核数一致,网卡注册的`poll`函数会从`RingBuffer`中将数据帧以`skb_buffer`的形式取下来,然后调用网络层注册的`IP`协议回调函数(回调函数存在每个`CPU`数据结构下`ptypes_base`)将当前帧转送到网络层,到了这里数据已经到达了我们耳熟能详的网络层了,只不过在网络层之前会经过`iptables`过滤,这个后面会稍微介绍以下。
> 我觉得有必要介绍上面出现过的名词的含义.
- DMA:网卡直接将数据放到内存上,而不经过CPU,从而减少使用CPU的资源。
- 硬中断:网卡向CPU的某个引脚发送一个电压变化用来告知该CPU发生了硬中断。硬中断和某个具体的`RingBuffer`相关联。
- 软中断:CPU通过或运算来修改某个内存变量用来通知内核线程`ksoftirqd`发生了软中断,软中断的类型有很多,并不局限与网络软中断。
- `Poll`函数:在子系统初始化的时候,初始化CPU的时候会为每个CPU初始化一个`softnet_data`数据结构,该数据结构中包含了一个`poll_list`双向链表用来保存所有驱动注册的回调函数,而`List`的每个节点中都包含该节点的输入设备的帧等着被软中断回调函数进行处理。而软中断就是调用`poll`函数中的节点来从硬件读取数据。
- `RingBuffer`:网卡收发数据的中转站。
## 二、、网卡初始化和启动.
### 2.1 初始化网络子系统.
`Linux`初始化网络子系统的时候会先为每个CPU初始化一个`softnet_data`数据结构,并且将每个`CPU`的网络读写软中断赋值到该CPU静态变量中。 而硬中断注册是在分配`RX`和`TX`发送接受队列是进行和`CPU`绑定操作的。当内核线程`ksoftirqd`监听到有软中断发生时,将会去执行相应软中断注册的回调函数。
当为每个CPU都注册软中断之后,将会去初始化`协议栈`,自下而上进行初始化,`Linux内核`会将网络层的回调函数注册到静态变量`ptyps_base`中,该静态变量为`hash`类型,当网络软中断从`poll`函数中读取出来数据时,会从`ptyps_base`变量中获取网络层的回调函数,并将将数据包转到网络层。
而`Linux内核`注册传输层协议是分别将`udp`和`tcp`协议的回调函数指针地址存入到`inet_protos`数组中。之后由网络层将数据包转到传输层时只需要根据`network`获取对应的`action`并调用执行即可将数据包转到对应的传输层。
当上面协议栈初始化完毕之后,将会去执行网卡驱动初始化了。对于`linux`操作系统来说,会让每一个驱动程序去向内核注册一个初始化函数,用来对该驱动进行初始化,这个初始化其实就是将该驱动的详细信息报告给`linux内核`。下面给出`linux内核`宏观初始化网卡的流程图。

当`linux`内核识别到该网卡的相信信息之后,就会调用网卡的`probe`函数,而该`probe`函数首先会去实现`ethtool`相关函数,并且去注册网卡打开函数`open`等,最后将网卡的`poll`函数注册到内核中。注:硬中断在启动网卡过程中注册,而软中断在初始化网络子系统的时候为每个CPU分配`softnet_data`数据结构时进行注册到CPU软中断结构中。
我们平常可能会偶尔用到`ethtool`去查看网络包的一些相关信息,但是确不知道其为什么要这样去实现,那么到了这里你就一定明白了,其实每个网卡都默认去实现了`ethtool`相关函数,并且注册到了内核相关信息,当`linux`识别用户调用了`ethtool`函数的时候,内核将会去调用网卡驱动的相应方法。到了这里网卡驱动的相关打开函数以及`ethtool`工具已经完成注册,那么剩下的就是启动网卡,并且打开硬中断等待数据包的到来。
网卡启动过程如下:

网卡初始化完之后(网卡驱动函数相关指针以及注册到内核中),就开始启动网卡了,内核会直接调用open函数去启动网卡,并且根据网卡的相关信息去初始化`RX`和`TX`队列内存,并且会为每个队列初始化并绑定硬中断函数(小知识点:如果`CPU1`发生了硬中断,那么执行软中断也将会在`CPU1`中进行执行),这个队列的数量是可以控制的,对于多队列网卡情况来说,队列的数量并不一定得是CPU的核数。举个情况:当`RingBuffer`出现丢包时,不仅仅可以扩大单个`RingBuffer`队列的内存,还可以多设置几个`RIngBuffer`并绑定多个不同的CPU进而来更加高效的处理更多的网络包数据。当网卡所需要的队列内存分配好之后就可以接受网络传过来的数据(打开硬中断来接受数据)。
**下面我们来看看`RingBuffer`的数据结构**
> skb是数据包.

`RingBuffer`其实并不是单纯是一个环形队列,每一个RingBuffer中包含两个指针数组,指针指向的就是本次网卡接受的数据包`skb`.其中一个数组供内核使用,一个数组供网卡使用。
**如果本文有哪些地方理解错了,欢迎指出,谢谢!**
**欢迎关注公众号(如果本文对您有帮助):考拉小同学。**