Linux 网络初步阅读

    技术2022-07-10  151

    文章目录

    一些名词初始化网卡接收分组NAPI详细步骤 高层协议IPv4接收ip_rcvip_rcv_finiship_local_deliverip_local_deliver_finish 发送ip_forwardip_forward_finish UDPudp_rcvudp_queue_rcv_skb TCP主图被动建立主动建立 一些文章

    一些名词

    NIC:网络接口控制器,Network Interface Controller,就是网卡NAPI(New API):适配超高速网络适配器的网络APIigb:英特尔千兆网卡驱动程序

    初始化网卡

    以igb驱动为例

    igb_init_module { pci_register_driver }

    内核会根据不同网卡选择不同的函数指针,然后启动网卡。然后会执行下面几步:

    注册 struct net_device_ops 结构,这个结构是用来保存一些关于网卡的操作函数指针。把Mac地址赋给网卡。创建一个struct net_device结构,这个结构就代表一个网络设备。

    接收分组

    NAPI

    先看NAPI:主要是用来适配高速设备 意思就是如果在高速设备上,每来一个数据包就发一个中断给CPU,CPU的时间都花费在处理中断上面了,因为是高速设备,一次可能来几个包,这几个包只给一次中断,让CPU一次处理完几个包,就可以释放一些CPU时间出来。

    详细步骤

    数据到达网卡:网卡通过DMA方式把数据包放到一个环形队列里面(ring buffer)【struct softnet_data】,这个队列在初始化的时候会分配物理内存。这个队列里面有一个sk_buff_head,所以这个环形队列里面的成员就是一个个struct sk_buff。如果网卡支持RSS/multiqueue的话,那么可以有多个RX Queue,中断只能是一个CPU来处理,但是可以通过设置中断亲和性来把RX Queue和 CPU关联起来。 netif_rx 激活 NET_RX_SOFTIRQ中断,处理函数为:net_rx_action。net_rx_action会调用网卡注册过的NAPI的poll函数,从上面的队列里面取出分组,主要是process_backlog。process_backlog会调用__skb_dequeue从队列头取第一个sk_buff,取出之后调用__netif_receive_skb,内部会调用deliver_skb开始交付到高层协议。deliver_skb 这个函数会根据不同的协议簇去调用注册好的函数。比如IPv4的处理函数就是ip_rcv。并且会发一份数据给tcpdump之类的抓包工具(libpcap)。

    高层协议

    IPv4

    接收

    ip_rcv

    deliver_skb之后,对于IP包,就会进入ip_rcv函数对skb进行进一步的处理

    iph = ip_hdr(skb); CHECK_IP_HEADER(iph); NF_HOOK(NF_INET_PRE_ROUTING, ip_rcv_finish);

    先从skb里面把IP头取出来,然后做一些校验,校验通过之后就放到netfilter的PRE_ROUTING链走一遍,调用iptables注册的一些函数,如果这个包不丢掉的话,那么就会跳到ip_rcv_finish这个函数继续处理。

    ip_rcv_finish
    ip_route_input_noref -- > ip_route_input_common; //选路由 如果是转发:ip_forward 如果是本机:ip_local_deliver
    ip_local_deliver

    ip分片重组 ip_defrag(),再把skb放到netfilter,如果不丢包的话,则调用ip_local_deliver_finish

    ip_local_deliver_finish

    这个函数会找协议类型,是UDP,还是TCP,还是其他协议,然后根据协议的不同来调用不同的handler函数 每个协议的handler是: 下一步就是更高层协议的处理了。

    发送

    ip_forward

    这种情况本机要把这个包转发,会检查这个包ttl字段,重新计算check_sum,再查找路由表,找到合适的dst,最后进入netfilter进行包过滤,如果没有丢掉则调用ip_forward_finish

    ip_forward_finish

    dst_output() --> ip_output() --> ip_finish_output() --> 判断时候需要分片,如果需要则调用ip_fragment,再把分片后的数据通过ip_finish_output2()发送出去。

    UDP

    udp_rcv

    udp_rcv --> __udp4_lib_rcv 可以看到会去检查udp的check_sum字段,如果校验失败就直接丢掉这个包了。 如果检查成功并且找到相关的监听套接字的话,那么调用udp_queue_rcv_skb。

    udp_queue_rcv_skb

    会检查socket缓冲区还有没有空间,满了就丢掉。 这个revbuf很熟悉,这个套接字缓冲区的大小是可以通过setsockopt函数来设置的(SO_RCVBUF),但是 这个revbuf最大值不能超过net.core.rmem_max,这个net.core.rmem_max可以通过sysctl来设置

    sudo sysctl -w net.core.rmem_max=8388608

    然后再调用__udp_queue_rcv_skb --> sock_queue_rcv_skb --> __skb_queue_tail。到这一步数据包就确实到了套接字的缓冲区里面了。最后看这个套接字是否存活,存活的话调用sk_data_ready来通知套接字缓冲区有东西可以读取了(read/readv/epoll)这个会唤醒在sk_sleep队列上面的进程。

    TCP

    主图

    tcp_v4_do_rcv这个函数会根据当前的状态去调用不同的函数。

    被动建立

    主动建立

    关于TCP的东西有些多,在这里先不展开了。

    一些文章

    Rec Data Send Data How to receive a million packets per second Ring Buffer

    Processed: 0.012, SQL: 9