[网络]网卡是如何接受数据包的

网卡是如何将数据帧发送到网络层

本文章内容参考:深入理解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)
}

本文首先将会介绍数据包是怎么从网卡到达网络层的。

一、从宏观看网卡是如何接受数据的.

image-20220828163643181

这里先总结一下大概流程:

当网线有数据送达到网卡时,网卡会直接将帧直接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静态变量中。 而硬中断注册是在分配RXTX发送接受队列是进行和CPU绑定操作的。当内核线程ksoftirqd监听到有软中断发生时,将会去执行相应软中断注册的回调函数。

当为每个CPU都注册软中断之后,将会去初始化协议栈,自下而上进行初始化,Linux内核会将网络层的回调函数注册到静态变量ptyps_base中,该静态变量为hash类型,当网络软中断从poll函数中读取出来数据时,会从ptyps_base变量中获取网络层的回调函数,并将将数据包转到网络层。

Linux内核注册传输层协议是分别将udptcp协议的回调函数指针地址存入到inet_protos数组中。之后由网络层将数据包转到传输层时只需要根据network获取对应的action并调用执行即可将数据包转到对应的传输层。

当上面协议栈初始化完毕之后,将会去执行网卡驱动初始化了。对于linux操作系统来说,会让每一个驱动程序去向内核注册一个初始化函数,用来对该驱动进行初始化,这个初始化其实就是将该驱动的详细信息报告给linux内核。下面给出linux内核宏观初始化网卡的流程图。

image-20220828202937073

linux内核识别到该网卡的相信信息之后,就会调用网卡的probe函数,而该probe函数首先会去实现ethtool相关函数,并且去注册网卡打开函数open等,最后将网卡的poll函数注册到内核中。注:硬中断在启动网卡过程中注册,而软中断在初始化网络子系统的时候为每个CPU分配softnet_data数据结构时进行注册到CPU软中断结构中。

我们平常可能会偶尔用到ethtool去查看网络包的一些相关信息,但是确不知道其为什么要这样去实现,那么到了这里你就一定明白了,其实每个网卡都默认去实现了ethtool相关函数,并且注册到了内核相关信息,当linux识别用户调用了ethtool函数的时候,内核将会去调用网卡驱动的相应方法。到了这里网卡驱动的相关打开函数以及ethtool工具已经完成注册,那么剩下的就是启动网卡,并且打开硬中断等待数据包的到来。

网卡启动过程如下:

image-20220828204545450

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

下面我们来看看RingBuffer的数据结构

skb是数据包.

image-20220828211628601

RingBuffer其实并不是单纯是一个环形队列,每一个RingBuffer中包含两个指针数组,指针指向的就是本次网卡接受的数据包skb.其中一个数组供内核使用,一个数组供网卡使用。

如果本文有哪些地方理解错了,欢迎指出,谢谢!

欢迎关注公众号(如果本文对您有帮助):考拉小同学。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值