网卡是如何将数据帧发送到网络层
本文章内容参考:深入理解Linux网络。
看计算机底层的前提基础是我们有某个方面的知识不是很理解,从而根据这个点在去理解该技术的基础底层,这样的话学习起来不会很枯燥。
我们平常可能都接触过TCP
网络编程等,但是对于socket.read
函数读取client
传送过来的数据其底层是怎么传过来的,我们可能不是很理解,我们看下面的代码,如果不特意的去查看看这方面的资料,很可能我们都处于一种这种状态:我调用socket.read
可以接受对方的数据,但是怎么接受的,就不是很理解了,换句话说就是:我们知道TCP是面向链接的传输层通信协议,但是在一个链接中,server
端每次具体要读取多少个字节的数据,我们可能并不知道其底层原理,通常我们只需要知道调用socket.read
就可以获取到client
发送的一次数据包。所以该系列文章将会以该问题为维度去探究网络底层原理。
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
.其中一个数组供内核使用,一个数组供网卡使用。
如果本文有哪些地方理解错了,欢迎指出,谢谢!
欢迎关注公众号(如果本文对您有帮助):考拉小同学。