IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 服务器正文22:linux内核网络模块笔记:收包、发包、各种内核参数上限、网络内核优化和容器网络虚拟化(8/2) -> 正文阅读

[系统运维]服务器正文22:linux内核网络模块笔记:收包、发包、各种内核参数上限、网络内核优化和容器网络虚拟化(8/2)


最近毕业了,所以整理下网络相关的八股文,本文全程都是基于linux的3.10版本,网卡都是采用intel的igb网卡驱动

一、内核如何接受网络包

  • 以udp的代码举例
int main(){
    int serverSocketFd = socket(AF_INET, SOCK_DGRAM, 0);
    bind(serverSocketFd, ...);

    char buff[BUFFSIZE];
    int readCount = recvfrom(serverSocketFd, buff, BUFFSIZE, 0, ...);
    buff[readCount] = '\0';
    printf("Receive from client:%s\n", buff);
}

1)linux网络层收包总览(按TCP/IP分层)

在这里插入图片描述

  • 背景知识了解(内核和网络设备驱动是通过中断的方式来处理的)
    1)当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据
    2)上半部是硬中断只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。(硬中断是通过给CPU物理引脚施加电压变化)
    3)2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程在软中断中全权处理。(软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。)

  • 问题:为什么不在中断函数中完成所有的处理?
    对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。因此Linux中断处理函数是分上半部和下半部的。

  • 总体收包流程预览(后面的章节会更详细介绍)
    在这里插入图片描述

1)网络驱动会以DMA的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达
2)当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数。网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。
3)ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后交由各级协议栈处理
4)帧被从ringbuffer上保存成一个skb存储数据的结构体。对于UDP包来说,会被放到用户socket的接收队列中。

  • 源码位置(网络模块)
    在Linux的源代码中,网络设备驱动对应的逻辑位于driver/net/ethernet, 其中intel系列网卡的驱动在driver/net/ethernet/intel目录下。协议栈模块代码位于kernel和net目录。
    在这里插入图片描述在这里插入图片描述

2)linux启动预备流程(准备工作,初始化流程)

(1)创建ksoftirqd内核线程

Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的,该进程数量不是1个,而是N个,其中N等于你的机器的核数。(博主服务器有四核)
在这里插入图片描述
系统初始化的时候在kernel/smpboot.c中调用了smpboot_register_percpu_thread, 该函数进一步会执行到spawn_ksoftirqd(位于kernel/softirq.c)来创建出softirqd进程。
在这里插入图片描述

  • 相关代码如下:
//file: kernel/softirq.c

static struct smp_hotplug_thread softirq_threads = {

    .store          = &ksoftirqd,
    .thread_should_run  = ksoftirqd_should_run,
    .thread_fn      = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",};
static __init int spawn_ksoftirqd(void){
    register_cpu_notifier(&cpu_nfb);

    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
    return 0;
}
early_initcall(spawn_ksoftirqd);

(2)网络子系统初始化(例如给ksoftirqd线程的变量绑定处理函数)

在这里插入图片描述
linux内核通过调用subsys_initcall来初始化各个子系统,在源代码目录里你可以grep出许多对这个函数的调用。这里我们要说的是网络子系统的初始化,会执行到net_dev_init函数。

//下面的init_module是个函数指针,是可以注册的
/* Each module must use one module_init(). */
#define module_init(initfn)					\
	static inline initcall_t __maybe_unused __inittest(void)		\
	{ return initfn; }					\
	int init_module(void) __attribute__((alias(#initfn)));

//--------------
//file: net/core/dev.c

static int __init net_dev_init(void){

    ......

    for_each_possible_cpu(i) {
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        memset(sd, 0, sizeof(*sd));
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
        sd->completion_queue = NULL;
        INIT_LIST_HEAD(&sd->poll_list);
        ......
    }
    ......
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

}

subsys_initcall(net_dev_init);

在这个net_dev_init函数里,会为每个CPU都申请一个softnet_data数据结构,在这个数据结构里的poll_list是等待驱动程序将其poll函数注册进来,稍后网卡驱动初始化的时候我们可以看到这一过程。

  • 软中断后,ksoftirqd线程根据变量找到对应的处理函数并执行
    另外open_softirq注册了每一种软中断都注册一个处理函数。NET_TX_SOFTIRQ的处理函数为net_tx_action,NET_RX_SOFTIRQ的为net_rx_action。继续跟踪open_softirq后发现这个注册的方式是记录在softirq_vec变量里的。后面ksoftirqd线程收到软中断的时候,也会使用这个变量来找到每一种软中断对应的处理函数。
//file: kernel/softirq.c

void open_softirq(int nr, void (*action)(struct softirq_action *)){
    softirq_vec[nr].action = action;
}
  • 变量枚举(每个变量绑定对应的处理函数)
enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,   //绑定处理函数net_tx_action
	NET_RX_SOFTIRQ,   //绑定处理函数net_rx_action
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

(3)协议栈注册(对传输层的tcp、udp协议注册具体的实现函数)

内核实现了网络层的ip协议,也实现了传输层的tcp协议和udp协议。这些协议对应的实现函数分别是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我们平时写代码的方式不一样的是,内核是通过注册的方式来实现的。Linux内核中的fs_initcall和subsys_initcall类似,也是初始化模块的入口。fs_initcall调用inet_init后开始网络协议栈注册。通过inet_init,将这些函数注册到了inet_protos和ptype_base数据结构中了。如下图:
图5 AF_INET协议栈注册

  • 相关代码如下
//file: net/ipv4/af_inet.c

static struct packet_type ip_packet_type __read_mostly = {

    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,};static const struct net_protocol udp_protocol = {
    .handler =  udp_rcv,
    .err_handler =  udp_err,
    .no_policy =    1,
    .netns_ok = 1,};static const struct net_protocol tcp_protocol = {
    .early_demux    =   tcp_v4_early_demux,
    .handler    =   tcp_v4_rcv,
    .err_handler    =   tcp_v4_err,
    .no_policy  =   1,
    .netns_ok   =   1,

};

static int __init inet_init(void){

    ......
    if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        pr_crit("%s: Cannot add ICMP protocol\n", __func__);
    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
        pr_crit("%s: Cannot add UDP protocol\n", __func__);
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        pr_crit("%s: Cannot add TCP protocol\n", __func__);
    ......
    dev_add_pack(&ip_packet_type);

}

上面的代码中我们可以看到,udp_protocol结构体中的handler是udp_rcv,tcp_protocol结构体中的handler是tcp_v4_rcv,通过inet_add_protocol被初始化了进来。

int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){
    if (!prot->netns_ok) {
        pr_err("Protocol %u is not namespace aware, cannot register.\n",
            protocol);
        return -EINVAL;
    }

    return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
            NULL, prot) ? 0 : -1;
}
  • 具体注册函数的实现(把udp、tcp的处理函数放到inet_protos数组,ptype_base哈希表中存放ip_rcv()函数的地址
    inet_add_protocol函数将tcp和udp对应的处理函数都注册到了inet_protos数组中了。再看dev_add_pack(&ip_packet_type);这一行,ip_packet_type结构体中的type是协议名,func是ip_rcv函数,在dev_add_pack中会被注册到ptype_base哈希表中。
//file: net/core/dev.c

void dev_add_pack(struct packet_type *pt){

    struct list_head *head = ptype_head(pt);
    ......

}

static inline struct list_head *ptype_head(const struct packet_type *pt){

    if (pt->type == htons(ETH_P_ALL))
        return &ptype_all;
    else
        return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];

}

这里我们需要记住inet_protos记录着udp,tcp的处理函数地址,ptype_base存储着ip_rcv()函数的处理地址。后面我们会看到软中断中会通过ptype_base找到ip_rcv函数地址,进而将ip包正确地送到ip_rcv()中执行。在ip_rcv中将会通过inet_protos找到tcp或者udp的处理函数,再而把包转发给udp_rcv()或tcp_v4_rcv()函数。

流程:
1)ip_rcv中将会通过inet_protos找到tcp或者udp的处理函数
2)再而把包转发给udp_rcv()或tcp_v4_rcv()函数

  • iptables的过滤和netfilter的设置参数也在这里(inet_init函数
    扩展一下,如果看一下ip_rcv和udp_rcv等函数的代码能看到很多协议的处理过程。例如,ip_rcv中会处理netfilter和iptable过滤,如果你有很多或者很复杂的 netfilter 或 iptables 规则,这些规则都是在软中断的上下文中执行的,会加大网络延迟。再例如,udp_rcv中会判断socket接收队列是否满了。对应的相关内核参数是net.core.rmem_max和net.core.rmem_default。如果有兴趣,建议大家好好读一下inet_init这个函数的代码。

(4)网卡驱动初始化(注册加载驱动时调用的处理函数、获取电脑物理地址等)

每一个驱动程序(不仅仅只是网卡驱动)会使用 module_init 向内核注册一个初始化函数,当驱动被加载时,内核会调用这个函数。比如igb网卡驱动的代码位于drivers/net/ethernet/intel/igb/igb_main.c

//file: drivers/net/ethernet/intel/igb/igb_main.c

static struct pci_driver igb_driver = {

    .name     = igb_driver_name,
    .id_table = igb_pci_tbl,
    .probe    = igb_probe,
    .remove   = igb_remove,
    ......

};

static int __init igb_init_module(void){

    ......
    ret = pci_register_driver(&igb_driver);
    return ret;

}

驱动的pci_register_driver调用完成后,Linux内核就知道了该驱动的相关信息,比如igb网卡驱动的igb_driver_name和igb_probe函数地址等等。当网卡设备被识别以后,内核会调用其驱动的probe方法(igb_driver的probe方法是igb_probe)。驱动probe方法执行的目的就是让设备ready,对于igb网卡,其igb_probe位于drivers/net/ethernet/intel/igb/igb_main.c下。主要执行的操作如下:
在这里插入图片描述

  • 流程
1)启动
2)调用probe函数
3)获取电脑物理地址,例如mac地址
4)DMA初始化
5)注册ethtool实现函数
6)注册net_device_ops\netdev等变量
7)NAPI初始化,注册poll函数
  • 备注
    1)第5步
    中我们看到,网卡驱动实现了ethtool所需要的接口,也在这里注册完成函数地址的注册。当 ethtool 发起一个系统调用之后,内核会找到对应操作的回调函数。对于igb网卡来说,其实现函数都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。相信你这次能彻底理解ethtool的工作原理了吧?这个命令之所以能查看网卡收发包统计、能修改网卡自适应模式、能调整RX 队列的数量和大小,是因为ethtool命令最终调用到了网卡驱动的相应方法,而不是ethtool本身有这个超能力。
    2)第6步
    注册的igb_netdev_ops中包含的是igb_open等函数,该函数在网卡被启动的时候会被调用。(以下是对应的注册函数)
//file: drivers/net/ethernet/intel/igb/igb_main.c

static const struct net_device_ops igb_netdev_ops = {

  .ndo_open               = igb_open,
  .ndo_stop               = igb_close,
  .ndo_start_xmit         = igb_xmit_frame,
  .ndo_get_stats64        = igb_get_stats64,
  .ndo_set_rx_mode        = igb_set_rx_mode,
  .ndo_set_mac_address    = igb_set_mac,
  .ndo_change_mtu         = igb_change_mtu,
  .ndo_do_ioctl           = igb_ioctl,

 ......

3)第7步
在igb_probe初始化过程中,还调用到了igb_alloc_q_vector。他注册了一个NAPI机制所必须的poll函数,对于igb网卡驱动来说,这个函数就是igb_poll,如下代码所示。

static int igb_alloc_q_vector(struct igb_adapter *adapter,
                  int v_count, int v_idx,
                  int txr_count, int txr_idx,
                  int rxr_count, int rxr_idx){
    ......
    /* initialize NAPI */
    netif_napi_add(adapter->netdev, &q_vector->napi,
               igb_poll, 64);
}

(5)启动网卡(按照前面的初始化函数,注册一堆启动回调函数和参数变量)

当上面的初始化都完成以后,就可以启动网卡了。回忆前面网卡驱动初始化时,我们提到了驱动向内核注册了 structure net_device_ops 变量,它包含着网卡启用、发包、设置mac 地址等回调函数(函数指针)。当启用一个网卡时(例如,通过 ifconfig eth0 up),net_device_ops 中的 igb_open方法会被调用。它通常会做以下事情:

在这里插入图片描述

  • 流程解析
1)启动网卡
2)调用net_device_ops中注册的open函数和__igb_open函数
3)分配RX、TX队列内存
4)注册中断处理函数
5)打开硬中断,等待包进来
  • __igb_open源码解析
//file: drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming){

    /* allocate transmit descriptors */
    err = igb_setup_all_tx_resources(adapter);

    /* allocate receive descriptors */
    err = igb_setup_all_rx_resources(adapter);

    /* 注册中断处理函数 */
    err = igb_request_irq(adapter);
    if (err)
        goto err_req_irq;

    /* 启用NAPI */
    for (i = 0; i < adapter->num_q_vectors; i++)
        napi_enable(&(adapter->q_vector[i]->napi));
    ......

}

在上面__igb_open函数调用了igb_setup_all_tx_resources,和igb_setup_all_rx_resources。在igb_setup_all_rx_resources这一步操作中,分配了RingBuffer,并建立内存和Rx队列的映射关系。(Rx Tx 队列的数量和大小可以通过 ethtool 进行配置)。我们再接着看中断函数注册igb_request_irq:

  • __igb_open函数后面的流程( __igb_open => igb_request_irq => igb_request_msix)
static int igb_request_irq(struct igb_adapter *adapter){
    if (adapter->msix_entries) {
        err = igb_request_msix(adapter);
        if (!err)
            goto request_done;
        ......
    }

}

static int igb_request_msix(struct igb_adapter *adapter){

    ......
    for (i = 0; i < adapter->num_q_vectors; i++) {
        ...
        err = request_irq(adapter->msix_entries[vector].vector,
                  igb_msix_ring, 0, q_vector->name,
    }

在上面的代码中跟踪函数调用, __igb_open => igb_request_irq => igb_request_msix, 在igb_request_msix中我们看到了,对于多队列的网卡,为每一个队列都注册了中断,其对应的中断处理函数是igb_msix_ring(该函数也在drivers/net/ethernet/intel/igb/igb_main.c下)。我们也可以看到,msix方式下,每个 RX 队列有独立的MSI-X 中断,从网卡硬件中断的层面就可以设置让收到的包被不同的 CPU处理。(可以通过 irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity能够修改和CPU的绑定行为)。

当做好以上准备工作以后,就可以开门迎客(数据包)了!

3)内核具体收包流程(之前的准备工作做好了)

(1)数据从网卡送到ringbuffer,DMA把数据送到内存,通知CPU开启硬中断处理

首先当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。
在这里插入图片描述

  • 流程
1)数据帧从外部网络到达网卡
2)网卡把帧DMA到内存
3)对CPU发出IRQ硬中断
4)CPU调用驱动注册的硬中断处理函数
5)对应的处理函数启动NAPI,发出软中断
  • 注意
    当RingBuffer满的时候,新来的数据包将给丢弃。ifconfig查看网卡的时候,可以里面有个overruns,表示因为环形队列满被丢弃的包。如果发现有丢包,可能需要通过ethtool命令来加大环形队列的长度。

  • 硬中断处理函数igb_msix_ring(网卡的硬中断注册的处理函数)

//file: drivers/net/ethernet/intel/igb/igb_main.c

static irqreturn_t igb_msix_ring(int irq, void *data){

    struct igb_q_vector *q_vector = data;

    /* Write the ITR value calculated from the previous interrupt. */
    igb_write_itr(q_vector);

    napi_schedule(&q_vector->napi);
    return IRQ_HANDLED;
}

igb_write_itr只是记录一下硬件中断频率(据说目的是在减少对CPU的中断频率时用到)。顺着napi_schedule调用一路跟踪下去,__napi_schedule=>____napi_schedule

/* Called with irq disabled */

static inline void ____napi_schedule(struct softnet_data *sd,

                     struct napi_struct *napi){
    list_add_tail(&napi->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);

}

这里我们看到,list_add_tail修改了CPU变量softnet_data里的poll_list,将驱动napi_struct传过来的poll_list添加了进来。其中softnet_data中的poll_list是一个双向列表,其中的设备都带有输入帧等着被处理。紧接着__raise_softirq_irqoff触发了一个软中断NET_RX_SOFTIRQ, 这个所谓的触发过程只是对一个变量进行了一次或运算而已。

void __raise_softirq_irqoff(unsigned int nr){
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);

}

//file: include/linux/irq_cpustat.h

#define or_softirq_pending(x)  (local_softirq_pending() |= (x))
  • 硬中断做的事(记录一个寄存器,修改了CPU变量softnet_data里的poll_list,然后发出软中断)
void __raise_softirq_irqoff(unsigned int nr){
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);

}

//file: include/linux/irq_cpustat.h

#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

(2)ksoftirqd内核线程处理软中断

在这里插入图片描述

  • 整体流程
1)启动ksoftirqd线程,这个内核线程无线loop循环
2)线程判断softirq_pending标志,执行__do_softirq
3)调用驱动注册的poll函数
4)从ringbuffer取下数据包
5)调用igb_clean_rx_irq把包送到协议栈
  • 备注
    1)第二步
    线程无线循环调用ksoftirqd_should_run函数,这函数再调用local_softirq_pending()函数,读取硬中断设置的NET_RX_SOFTIRQ,然后进去线程函数中run_ksoftirqd处理,在__do_softirq中,判断根据当前CPU的软中断类型,调用其注册的action方法。
static void run_ksoftirqd(unsigned int cpu){
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        rcu_note_context_switch(cpu);
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();

}

//-----------------
asmlinkage void __do_softirq(void){
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();
            ...
            trace_softirq_entry(vec_nr);
            h->action(h);
            trace_softirq_exit(vec_nr);
            ...
        }
        h++;
        pending >>= 1;
    } while (pending);

}

在网络子系统初始化小节, 我们看到我们为NET_RX_SOFTIRQ注册了处理函数net_rx_action。所以net_rx_action函数就会被执行到了。

  • 打散硬中断到不同CPU中去
    这里需要注意一个细节,硬中断中设置软中断标记,和ksoftirq的判断是否有软中断到达,都是基于smp_processor_id()的。这意味着只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的。所以说,如果你发现你的Linux软中断CPU消耗都集中在一个核上的话,做法是要把调整硬中断的CPU亲和性,来将硬中断打散到不同的CPU核上去

  • NET_RX_SOFTIRQ对应处理函数net_rx_action

static void net_rx_action(struct softirq_action *h){
    struct softnet_data *sd = &__get_cpu_var(softnet_data);
    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget;
    void *have;

    local_irq_disable();
    while (!list_empty(&sd->poll_list)) {
        ......
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            work = n->poll(n, weight);
            trace_napi_poll(n);
        }
        budget -= work;
    }

}

函数开头的time_limit和budget是用来控制net_rx_action函数主动退出的,目的是保证网络包的接收不霸占CPU不放。等下次网卡再有硬中断过来的时候再处理剩下的接收数据包。其中budget可以通过内核参数调整。这个函数中剩下的核心逻辑是获取到当前CPU变量softnet_data,对其poll_list进行遍历, 然后执行到网卡驱动注册到的poll函数。对于igb网卡来说,就是igb驱动力的igb_poll函数了。

static int igb_poll(struct napi_struct *napi, int budget){
    ...
    if (q_vector->tx.ring)
        clean_complete = igb_clean_tx_irq(q_vector);

    if (q_vector->rx.ring)
        clean_complete &= igb_clean_rx_irq(q_vector, budget);
    ...

}
//在读取操作中,igb_poll的重点工作是对igb_clean_rx_irq的调用。
static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget){
    ...
    do {
        /* retrieve a buffer from the ring */
        skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);

        /* fetch next buffer in frame if non-eop */
        if (igb_is_non_eop(rx_ring, rx_desc))
            continue;
        }

        /* verify the packet layout is correct */
        if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {
            skb = NULL;
            continue;
        }

        /* populate checksum, timestamp, VLAN, and protocol */
        igb_process_skb_fields(rx_ring, rx_desc, skb);

        napi_gro_receive(&q_vector->napi, skb);
}

igb_fetch_rx_buffer和igb_is_non_eop的作用就是把数据帧从RingBuffer上取下来。为什么需要两个函数呢?因为有可能帧要占多多个RingBuffer,所以是在一个循环中获取的,直到帧尾部。获取下来的一个数据帧用一个sk_buff来表示。收取完数据以后,对其进行一些校验,然后开始设置sbk变量的timestamp, VLAN id, protocol等字段。接下来进入到napi_gro_receive中:

//file: net/core/dev.c

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb){

    skb_gro_reset_offset(skb);
    return napi_skb_finish(dev_gro_receive(napi, skb), skb);

}

dev_gro_receive这个函数代表的是网卡GRO特性,可以简单理解成把相关的小包合并成一个大包就行,目的是减少传送给网络栈的包数,这有助于减少 CPU 的使用量。我们暂且忽略,直接看napi_skb_finish, 这个函数主要就是调用了netif_receive_skb。

//file: net/core/dev.c

static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb){

    switch (ret) {
    case GRO_NORMAL:
        if (netif_receive_skb(skb))
            ret = GRO_DROP;
        break;
    ......

}

在netif_receive_skb中,数据包将被送到协议栈中。声明,以下的3.3, 3.4, 3.5也都属于软中断的处理过程,只不过由于篇幅太长,单独拿出来成小节。

(3)网络协议栈处理

  • 根据包的协议调用对应处理函数
    netif_receive_skb函数会根据包的协议,假如是udp包,会将包依次送到ip_rcv(),udp_rcv()协议处理函数中进行处理。

在这里插入图片描述

//file: net/core/dev.c

int netif_receive_skb(struct sk_buff *skb){

    //RPS处理逻辑,先忽略    ......
    return __netif_receive_skb(skb);

}

static int __netif_receive_skb(struct sk_buff *skb){

    ......  
    ret = __netif_receive_skb_core(skb, false);}static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){
    ......

    //pcap逻辑,这里会将数据送入抓包点。tcpdump就是从这个入口获取包的    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (!ptype->dev || ptype->dev == skb->dev) {
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = ptype;
        }
    }
    ......
    list_for_each_entry_rcu(ptype,
            &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
        if (ptype->type == type &&
            (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
             ptype->dev == orig_dev)) {
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = ptype;
        }
    }

}
  • 关于tcpdump
    在__netif_receive_skb_core中,我看着原来经常使用的tcpdump的抓包点,很是激动,看来读一遍源代码时间真的没白浪费。接着__netif_receive_skb_core取出protocol,它会从数据包中取出协议信息,然后遍历注册在这个协议上的回调函数列表。ptype_base 是一个 hash table,在协议注册小节我们提到过。ip_rcv 函数地址就是存在这个 hash table中的。

  • 进入协议层注册的处理函数(ip就调用ip_rcv,arp就调用arp_rcv)

//file: net/core/dev.c

static inline int deliver_skb(struct sk_buff *skb,

                  struct packet_type *pt_prev,
                  struct net_device *orig_dev){
    ......
    return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

}

pt_prev->func这一行就调用到了协议层注册的处理函数了。对于ip包来讲,就会进入到ip_rcv(如果是arp包的话,会进入到arp_rcv)。

(4)IP协议层处理(ip包就调用到ip_rcv函数,inet_protos中保存着tcp_rcv()和udp_rcv()的函数地址)

  • 源码接口
//file: net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){

    ......
    //这里NF_HOOK是一个钩子函数,
    //当执行完注册的钩子后就会执行到最后一个参数指向的函数
    //ip_rcv_finish。
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
               ip_rcv_finish);

}

//---------------
static int ip_rcv_finish(struct sk_buff *skb){
    ......
    if (!skb_dst(skb)) {
        int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                           iph->tos, skb->dev);
        ...
    }
    ......
    return dst_input(skb);

}

跟踪ip_route_input_noref 后看到它又调用了 ip_route_input_mc。在ip_route_input_mc中,函数ip_local_deliver被赋值给了dst.input, 如下:

//file: net/ipv4/route.c

static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev, int our){

    if (our) {
        rth->dst.input= ip_local_deliver;
        rth->rt_flags |= RTCF_LOCAL;
    }

}

所以回到ip_rcv_finish中的return dst_input(skb);。

/* Input packet from network to transport.  */

static inline int dst_input(struct sk_buff *skb){

    return skb_dst(skb)->input(skb);

}

skb_dst(skb)->input调用的input方法就是路由子系统赋的ip_local_deliver。

//file: net/ipv4/ip_input.c

int ip_local_deliver(struct sk_buff *skb){

    /*     *  Reassemble IP fragments.     */
    if (ip_is_fragment(ip_hdr(skb))) {
        if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }

    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
               ip_local_deliver_finish);

}

static int ip_local_deliver_finish(struct sk_buff *skb){

    ......
    int protocol = ip_hdr(skb)->protocol;
    const struct net_protocol *ipprot;

    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot != NULL) {
    	//调用对应的处理函数,传入数据包
        ret = ipprot->handler(skb);
    }

}

如协议注册小节看到inet_protos中保存着tcp_rcv()和udp_rcv()的函数地址。这里将会根据包中的协议类型选择进行分发,在这里skb包将会进一步被派送到更上层的协议中,udp和tcp。

/* This is used to register protocols. */
struct net_protocol {
	//early_demux提前查找skb数据包的监听sock和输入路由dst,提前分
	int			(*early_demux)(struct sk_buff *skb);
	int			(*early_demux_handler)(struct sk_buff *skb);
	//对应的数据处理函数
	int			(*handler)(struct sk_buff *skb);
	void			(*err_handler)(struct sk_buff *skb, u32 info);
	unsigned int		no_policy:1,
				netns_ok:1,
				/* does the protocol do more stringent
				 * icmp tag validation than simple
				 * socket lookup?
				 */
				icmp_strict_tag_validation:1;
};

(5)UDP协议层处理

  • 前情提要
    在协议注册小节的时候我们说过,udp协议的处理函数是udp_rcv。
  • 源码(也就是上一节里面handler函数指针存的udp_rcv)
//file: net/ipv4/udp.c

int udp_rcv(struct sk_buff *skb){

    return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);

}
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,

           int proto){
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

    if (sk != NULL) {
        int ret = udp_queue_rcv_skb(sk, skb
    }
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

}

__udp4_lib_lookup_skb是根据skb来寻找对应的socket,当找到以后将数据包放到socket的缓存队列里。

在这里插入图片描述

  • 套接字源码信息
struct sock {

  struct options      *opt;/*IP选项缓存于此处*/

  volatile unsigned long   wmem_alloc;/*当前写缓冲区大小,该值不可大于系统规定的最大值*/

  volatile unsigned long   rmem_alloc;/*当前读缓冲区大小,该值不可大于系统规定最大值*/

  unsigned long                 write_seq;/* write_seq 表示应用程序下一次写数据时所对应的第一个字节的序列号*/

  unsigned long                 sent_seq;/* sent_seq 表示本地将要发送的下一个数据包中第一个字节对应的序列号*/

  unsigned long                 acked_seq;/* acked_seq 表示本地希望从远端接收的下一个数据的序列号*/

   unsigned long                 copied_seq; /*  应用程序有待读取(但尚未读取)数据的第一个序列号。*/

  unsigned long                 rcv_ack_seq; /*  表示目前本地接收到的对本地发送数据的应答序列号。*/

   unsigned long                 window_seq;/* 窗口大小,是一个绝对值,表示本地将要发送数据包中所包含最后一个数据的序列号,不可大于 window_seq.*/

  unsigned long                 fin_seq; /*  该字段在对方发送 FIN数据包时使用,在接收到远端发送的 FIN数据包后,fin_seq 被初始化为对方的 FIN 数据包最后一个字节的序列号加 1,表示本地对此 FIN 数据包进行应答的序列号*/

   unsigned long                 urg_seq;

  unsigned long                 urg_data;

/*  以上两个字段用于紧急数据处理,urg_seq 表示紧急数据最大序列号。urg_data 是一个标志位,当设置为 1 时,表示接收到紧急数据。*/

  volatile char                 inuse,/*inuse=1 表示其它进程正在使用该 sock 结构,本进程需等待*/

                      dead,/* dead=1 表示该 sock 结构已处于释放状态*/

                      urginline,/* urginline=1 表示紧急数据将被当作普通数据处理。*/

                      intr,

                      blog,/* blog=1 表示对应套接字处于节制状态,此时接收的数据包均被丢弃*/

                      done,

                      reuse,

                      keepopen,/* keepopen=1 表示使用保活定时器 */

                      linger,/* linger=1 表示在关闭套接字时需要等待一段时间以确认其已关闭。*/

                      delay_acks,/* delay_acks=1表示延迟应答,可一次对多个数据包进行应答 */

                      destroy,/* destroy=1 表示该 sock 结构等待销毁*/

                      ack_timed,

                      no_check,

                      zapped,   /* In ax25 & ipx means not linked */

                      broadcast,

                      nonagle;/* noagle=1 表示不使用 NAGLE 算法*/

  unsigned long                   lingertime;/*表示等待关闭操作的时间,只有当 linger 标志位为 1 时,该字段才有意义。*/

  int                    proc;/* 该 sock 结构(即该套接字)所属的进程的进程号。*/

  struct sock                *next;

  struct sock                *prev;

  struct sock                *pair;

/* 以上三个字段用于 sock 的连接*/

 

  struct sk_buff      * volatile send_head;

  struct sk_buff      * volatile send_tail;

/* send_head, send_tail 用于 TCP协议重发队列。*/

 

  struct sk_buff_head       back_log;/* back_log为接收的数据包缓存队列。用于计算目前累计的应发送而未发送的应答数据包的个数*/

  struct sk_buff      *partial;/*创建最大长度的待发送数据包。*/

  struct timer_list         partial_timer;/*按时发送 partial 指针指向的数据包,以免缓存(等待)时间过长。*/

  long                      retransmits;/* 重发次数*/

/*

write_queue 指向待发送数据包,其与 send_head,send_tail 队列的不同之处在于send_head,send_tail 队列中数据包均已经发送出去,但尚未接收到应答。而 write_queue 中数据包尚未发送。 receive-queue为读队列,其不同于 back_log 队列之处在于 back_log 队列缓存从网络层传 上来的数据包,在用户进行读取操作时,不可操作 back_log 队列,而是从 receive_queue 队列中去数据包读取其中的数据,即数据包首先缓存在 back_log 队列中,然后从 back_log 队列中移动到 receive_queue队列中方可被应用程序读取。而并非所有back_log 队列中缓 存的数据包都可以成功的被移动到 receive_queue队列中,如果此刻读缓存区太小,则当 前从back_log 队列中被取下的被处理的数据包将被直接丢弃,而不会被缓存到receive_queue 队列中。如果从应答的角度看,在back_log队列中的数据包由于有可能被 丢弃,故尚未应答,而将一个数据包从 back_log 移动到 receive_queue时,表示该数据包 已被正式接收,即会发送对该数据包的应答给远端表示本地已经成功接收该数据包。 */

  struct sk_buff_head       write_queue,

                      receive_queue;

 

  struct proto               *prot;/*指向传输层处理函数集*/

  struct wait_queue           **sleep;/*进程等待sock的地位*/

  unsigned long                 daddr;/*套接字的远端地址*/

  unsigned long                 saddr;/*套接字的本地地址*/

  unsigned short          max_unacked;/* 最大未处理请求连接数(应答数) */

  unsigned short          window;/* 远端窗口大小 */

  unsigned short          bytes_rcv;/* 已接收字节总数*/

/* mss is min(mtu, max_window) */

  unsigned short          mtu;       /*最大传输单元*/

  volatile unsigned short  mss;       /*最大报文长度:MSS=MTU-IP 首部长度-TCP首部长度 */

  volatile unsigned short  user_mss;  /*用户指定的 MSS值*/

 

  volatile unsigned short  max_window;

  unsigned long          window_clamp;/*最大窗口大小和窗口大小钳制值 */

 

  unsigned short          num;/* 本地端口号*/

/*

以下三个字段用于拥塞算法

*/  

  volatile unsigned short  cong_window;

  volatile unsigned short  cong_count;

  volatile unsigned short  ssthresh;

  volatile unsigned short  packets_out;/* 本地已发送出去但尚未得到应答的数据包数目*/

  volatile unsigned short  shutdown;/* 本地关闭标志位,用于半关闭操作*/

  volatile unsigned long   rtt;/* 往返时间估计值*/

  volatile unsigned long   mdev;/* mean deviation, 即RTTD,  绝对偏差*/

  volatile unsigned long   rto;/* RTO是用 RTT 和 mdev 用算法计算出的延迟时间值*/

  volatile unsigned short  backoff;/* 退避算法度量值 */

  volatile short        err;/* 错误标志值*/

  unsigned char                 protocol;/* 传输层协议值*/

  volatile unsigned char   state;/* 套接字状态值,如 TCP_ESTABLISHED */

  volatile unsigned char   ack_backlog;/* 缓存的未应答数据包个数*/

  unsigned char                 max_ack_backlog;/* 最大缓存的未应答数据包个数*/

  unsigned char                 priority;/* 该套接字优先级,在硬件缓存发送数据包时使用 */

  unsigned char                 debug;

  unsigned short          rcvbuf;/* 最大接收缓冲区大小*/

  unsigned short          sndbuf;/* 最大发送缓冲区大小*/

  unsigned short          type;/* 类型值如 SOCK_STREAM */

  unsigned char                 localroute;    /* localroute=1 表示只使用本地路由,一般目的端在相同子网时使用。*/

#ifdef CONFIG_IPX

  ipx_address               ipx_dest_addr;

  ipx_interface             *ipx_intrfc;

  unsigned short          ipx_port;

  unsigned short          ipx_type;

#endif

#ifdef CONFIG_AX25

  ax25_address            ax25_source_addr,ax25_dest_addr;

  struct sk_buff *volatile   ax25_retxq[8];

  char                      ax25_state,ax25_vs,ax25_vr,ax25_lastrxnr,ax25_lasttxnr;

  char                      ax25_condition;

  char                      ax25_retxcnt;

  char                      ax25_xx;

  char                      ax25_retxqi;

  char                      ax25_rrtimer;

  char                      ax25_timer;

  unsigned char                 ax25_n2;

  unsigned short          ax25_t1,ax25_t2,ax25_t3;

  ax25_digi              *ax25_digipeat;

#endif 

#ifdef CONFIG_ATALK

  struct atalk_sock      at;

#endif

 

/* IP 'private area' or will be eventually */

  int                    ip_ttl;       /* IP首部 TTL 字段值,实际上表示路由器跳数*/

  int                    ip_tos;           /* IP首部 TOS字段值,服务类型值*/

  struct tcphdr             dummy_th;/* 缓存的 TCP首部,在 TCP协议中创建一个发送数据包时可以利用此字段快速创建 TCP 首部。*/

  struct timer_list         keepalive_timer;     /*保活定时器,用于探测对方窗口大小,防止对方通报窗口大小的数据包丢弃,从而造成 本地发送通道被阻塞。*/

  struct timer_list         retransmit_timer;    /*重发定时器,用于数据包超时重发*/

  struct timer_list         ack_timer;          /*延迟应答定时器,延迟应答可以减少应答数据包的个数,但不可无限延迟以免造成远端 重发,所以设置定时器定期发送应答数据包。 */

  int                    ip_xmit_timeout;     /*该字段为标志位组合字段,用于表示下文中 timer定时器超时的原因*/

#ifdef CONFIG_IP_MULTICAST 

  int                    ip_mc_ttl;                

  int                    ip_mc_loop;            

  char                      ip_mc_name[MAX_ADDR_LEN]; 

  struct ip_mc_socklist          *ip_mc_list;     

#endif 

/*以上4 个字段用于 IP多播*/

 

  int                    timeout;  

  struct timer_list         timer;

/* 以上两个字段用于通用定时,timeout 表示定时时间值,ip_xmit_timeout表示此次定时的 原因,timer为定时器。 */

  struct timeval       stamp;/* 时间戳*/

  struct socket             *socket;/*对应的socket结构体*/

 

  void                       (*state_change)(struct sock *sk);

  void                       (*data_ready)(struct sock *sk,int bytes);

  void                       (*write_space)(struct sock *sk);

  void                       (*error_report)(struct sock *sk);

/* 以上四个函数指针字段指向回调函数。这些字段的设置为自定义回调函数提供的很大的

灵活性,内核在发生某些时间时,会调用这些函数,如此可以实现自定义响应。目前这

种自定义响应还是完全有内核控制。 */  

};
  • 如果没有找到,则发送一个目标不可达的icmp包。
//file: net/ipv4/udp.c

int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){  

    ......
    if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
        goto drop;

    rc = 0;

    ipv4_pktinfo_prepare(skb);
    bh_lock_sock(sk);
    if (!sock_owned_by_user(sk))
        rc = __udp_queue_rcv_skb(sk, skb);
    else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
        bh_unlock_sock(sk);
        goto drop;
    }
    bh_unlock_sock(sk);
    return rc;

}

sock_owned_by_user判断的是用户是不是正在这个socker上进行系统调用(socket被占用),如果没有,那就可以直接放到socket的接收队列中。如果有,那就通过sk_add_backlog把数据包添加到backlog队列。当用户释放的socket的时候,内核会检查backlog队列,如果有数据再移动到接收队列中。

  • 接受队列参数
    sk_rcvqueues_full接收队列如果满了的话,将直接把包丢弃。接收队列大小受内核参数net.core.rmem_max和net.core.rmem_default影响。

3)内核收包结束,用户使用recvfrom系统调用接受数据(UDP)

花开两朵,各表一枝。上面我们说完了整个Linux内核对数据包的接收和处理过程,最后把数据包放到socket的接收队列中了。那么我们再回头看用户进程调用recvfrom后是发生了什么。我们在代码里调用的recvfrom是一个glibc的库函数,该函数在执行后会将用户进行陷入到内核态,进入到Linux实现的系统调用sys_recvfrom。在理解Linux对sys_revvfrom之前,我们先来简单看一下socket这个核心数据结构。这个数据结构太大了,我们只把对和我们今天主题相关的内容画出来,如下:
在这里插入图片描述
socket数据结构中的const struct proto_ops对应的是协议的方法集合。每个协议都会实现不同的方法集,对于IPv4 Internet协议族来说,每种协议都有对应的处理方法,如下。对于udp来说,是通过inet_dgram_ops来定义的,其中注册了inet_recvmsg方法。

//file: net/ipv4/af_inet.c

const struct proto_ops inet_stream_ops = {

    ......
    .recvmsg       = inet_recvmsg,
    .mmap          = sock_no_mmap,
    ......

}

const struct proto_ops inet_dgram_ops = {

    ......
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    ......

}

socket数据结构中的另一个数据结构struct sock *sk是一个非常大,非常重要的子结构体。其中的sk_prot又定义了二级处理函数。对于UDP协议来说,会被设置成UDP协议实现的方法集udp_prot。

//file: net/ipv4/udp.c

struct proto udp_prot = {

    .name          = "UDP",
    .owner         = THIS_MODULE,
    .close         = udp_lib_close,
    .connect       = ip4_datagram_connect,
    ......
    .sendmsg       = udp_sendmsg,
    .recvmsg       = udp_recvmsg,
    .sendpage      = udp_sendpage,
    ......

}
  • 看完了socket变量之后,我们再来看sys_revvfrom的实现过程。
    在这里插入图片描述
    1)库函数调用recv_from
    2)Glibc库调用系统调用sys_recvfrom

在inet_recvmsg调用了sk->sk_prot->recvmsg。

//file: net/ipv4/af_inet.c

int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,size_t size, int flags){  

    ......
    err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
                   flags & ~MSG_DONTWAIT, &addr_len);
    if (err >= 0)
        msg->msg_namelen = addr_len;
    return err;

}

上面我们说过这个对于udp协议的socket来说,这个sk_prot就是net/ipv4/udp.c下的struct proto udp_prot。由此我们找到了udp_recvmsg方法。

//file:net/core/datagram.c:EXPORT_SYMBOL(__skb_recv_datagram);

struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned int flags,int *peeked, int *off, int *err){

    ......
    do {
        struct sk_buff_head *queue = &sk->sk_receive_queue;
        skb_queue_walk(queue, skb) {
            ......
        }

        /* User doesn't want to wait */
        error = -EAGAIN;
        if (!timeo)
            goto no_packet;
    } while (!wait_for_more_packets(sk, err, &timeo, last));

}

终于我们找到了我们想要看的重点,在上面我们看到了所谓的读取过程,就是访问sk->sk_receive_queue。如果没有数据,且用户也允许等待,则将调用wait_for_more_packets()执行等待操作,它加入会让用户进程进入睡眠状态。

4)总结

1)收包前准备工作

1. 创建ksoftirqd线程,为它设置好它自己的线程函数,后面指望着它来处理软中断呢
2. 协议栈注册,linux要实现许多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数
注册一下,方便包来了迅速找到对应的处理函数
3. 网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,把
自己的DMA准备好,把NAPI的poll函数地址告诉内核
4. 启动网卡,分配RX,TX队列,注册中断对应的处理函数

2)内核收包流程

1. 网卡将数据帧DMA到内存的RingBuffer中,然后向CPU发起中断通知
2. CPU响应中断请求,调用网卡启动时注册的中断处理函数
3. 中断处理函数几乎没干啥,就发起了软中断请求
4. 内核线程ksoftirqd线程发现有软中断请求到来,先关闭硬中断
5. ksoftirqd线程开始调用驱动的poll函数收包
6. poll函数将收到的包送到协议栈注册的ip_rcv函数中
7. ip_rcv函数再讲包送到udp_rcv函数中(对于tcp包就送到tcp_rcv)

3)相关实际问题

1)内核ringbuffer到底是什么?为什么会丢包?丢包怎么办?
2)网络相关的软硬中断是什么?
3)ksoftirqd内核线程是干嘛的?
4)为什么网卡开多队列能提升网络性能?
5)tcpdump是如何工作的?
6)iptable/netfilter是在那一层实现的?
7)tcpdump能否抓到被iptable封禁的包?
8)网路接受过程中的CPU开销怎么看?

1)内核ringbuffer到底是什么?为什么会丢包?丢包怎么办?
①ringbuffer内部是有两个环形队列数组:

1)igb_rx_buffer:内核使用的,通过vzalloc申请的
2)e1000_adv_rx_desc数组:网卡硬件使用的,通过dma_alloc_coherent分配

②内核ringbuffer使用流程
1、网卡收到数据,以DMA方式将包写到ringbuffer中。
2、软中断收包把skb取走,并申请新的skb重新挂上去
③这两个ringbuffer的指针数组数是预先分配好的,而skb则会随着收包过程动态申请
④这个ringbuffer是有大小和长度限制的,长度可以通过ethtool工具查看
在这里插入图片描述

Pre-set maximums   指的是ringbuffer的最大值
Current hardware settings: 当前的设置

⑤如果内核数据得不到及时处理,满了,后面的数据就会丢弃,通过ethtool或ifconfig工具可以查看是否有ringbuffer溢出发生

[root@VM-4-14-centos 4.18.0-305.3.1.el8.x86_64]# ethtool -S eth0
NIC statistics:
     rx_queue_0_drops: 0             //0代表的是第0个RingBuffer的丢包数
     rx_queue_1_drops: 0             //1代表的是第1个RingBuffer的丢包数
     tx_queue_0_xdp_tx_drops: 0  //
     tx_queue_1_xdp_tx_drops: 0   //

若有丢弃:
1、增加ringbuffer的大小(但是排队包过多会增加网络包的延时)

ethtool -G eth1 rx 4096 tx 4096

2、硬中断打散到其他CPU
3、多增加网卡队列数

2)网络相关的软硬中断是什么?
1、硬中断
网卡将数据放到ringbuffer,发起硬中断(将传过来的poll_list添加到CPU变量的softnet_data的poll_list里)通知CPU处理
2、软中断
CPU修改寄存器的值,触发软中断。(对softnet_data里面的poll_list遍历,执行网卡驱动提供的poll来接受网络包,处理后送到协议栈的ip_rcv、udp_rcv等函数中)

3)ksoftirqd内核线程是干嘛的?
根据软中断的枚举类型,执行对应的中断处理函数。

  • 软中断信息可以从/proc/softirqs读取
[root@VM-4-14-centos 4.18.0-305.3.1.el8.x86_64]# cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3       
          HI:          0          0          0          1
       TIMER:   81618865  172954307  134791511  118752523
      NET_TX:          4          3          1          5
      NET_RX:   84212506   68906362   15767705   12088615
       BLOCK:          0          0          0   26111592
    IRQ_POLL:          0          0          0          0
     TASKLET:      43809      33931        218        213
       SCHED:  138842691  223803035  182385156  165390901
     HRTIMER:      47006      31494      51602      38942
         RCU:  230638347  256538894  248099181  237309069

CPU执行了84212506次NET_RX和4次NET_TX
4)为什么网卡开多队列能提升网络性能?
先查看网卡信息

[root@VM-4-14-centos 4.18.0-305.3.1.el8.x86_64]# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:             n/a
TX:             n/a
Other:          n/a
Combined:       2
Current hardware settings:
RX:             n/a
TX:             n/a
Other:          n/a
Combined:       2

当晚网卡支持的最大队列是2,当前开启了2条队列

  • 增大队列数
ethtool -L eth0 combined 32
  • 查看各个队列对应的硬件中断号
[root@VM-4-14-centos 4.18.0-305.3.1.el8.x86_64]# cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3       
  1:          0          0          0          9   IO-APIC   1-edge      i8042
  4:          0        478          8          0   IO-APIC   4-edge      ttyS0
  8:          0          0          0          0   IO-APIC   8-edge      rtc0
  9:          0          0          0          0   IO-APIC   9-fasteoi   acpi
 11:          0          7          0          0   IO-APIC  11-fasteoi   virtio3, uhci_hcd:usb1, virtio2
 12:          0          0         15          0   IO-APIC  12-edge      i8042
 14:          0          0          0   10603856   IO-APIC  14-edge      ata_piix
 15:          0          0          0          0   IO-APIC  15-edge      ata_piix
 24:          0          0          0          0   PCI-MSI 98304-edge      virtio1-config
 25:          0          0          0   20852942   PCI-MSI 98305-edge      virtio1-req.0
 26:          0          0          0          0   PCI-MSI 81920-edge      virtio0-config
 27:   26934181          0          1          0   PCI-MSI 81921-edge      virtio0-input.0
 28:   24303428          0          0          1   PCI-MSI 81922-edge      virtio0-output.0
 29:          1   17653502          0          0   PCI-MSI 81923-edge      virtio0-input.1
 30:          0   24229158          0          0   PCI-MSI 81924-edge      virtio0-output.1

这里网卡输入队列i8042的中断号是1,tty50对应的中断号是4,共开了15个接受队列

  • 查看中断号对应的smp_affinity,可以看到亲和的CPU核是哪一个
[root@VM-4-14-centos 4.18.0-305.3.1.el8.x86_64]# cat /proc/irq/29/smp_affinity
2
[root@VM-4-14-centos 4.18.0-305.3.1.el8.x86_64]# cat /proc/irq/27/smp_affinity
1

这就意味着哪个核相应的硬中断,那么该核发起的软中断任务就必须由这个核来处理

  • 理解
    如果网络包的接受频率高,导致个别核si偏高,可以通过
    1、加大网卡队列数
    2、硬中断打散到其他核心,这样软中断CPU开销也将由多个核分担
  • top命令看si
top - 10:59:13 up 421 days, 22:38,  1 user,  load average: 0.95, 0.62, 0.65
Tasks: 207 total,   1 running, 206 sleeping,   0 stopped,   0 zombie
%Cpu0  : 15.6 us,  3.1 sy,  0.0 ni, 80.9 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu1  : 15.2 us,  3.1 sy,  0.0 ni, 81.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  : 14.9 us,  3.5 sy,  0.0 ni, 81.3 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu3  : 14.6 us,  3.5 sy,  0.0 ni, 81.6 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu4  : 27.4 us,  3.4 sy,  0.0 ni, 68.9 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu5  : 25.8 us,  3.4 sy,  0.0 ni, 69.8 id,  0.0 wa,  0.0 hi,  1.0 si,  0.0 st
%Cpu6  : 27.2 us,  3.7 sy,  0.0 ni, 68.4 id,  0.0 wa,  0.0 hi,  0.7 si,  0.0 st
%Cpu7  : 25.0 us,  3.7 sy,  0.0 ni, 69.9 id,  0.0 wa,  0.0 hi,  1.4 si,  0.0 st
KiB Mem :  8010720 total,   138484 free,  1621728 used,  6250508 buff/cache
KiB Swap: 16777212 total, 15481716 free,  1295496 used.  6024296 avail Mem 

5)tcpdump是如何工作的?
当内核收包的时候,会调用igb_poll函数,最终调用到__netif_receive_skb_core,这个函数会在将包送到协议栈(ip_rcv、arp_rcv等)之前,将包先送到ptype_all抓包

6)iptable/netfilter是在那一层实现的?
在IP/ARP层实现的,可以通过对NF_HOOK函数的引用来深入理解netfilter的实现。如果配置太多,会消耗太多CPU,加大网络延迟

7)tcpdump能否抓到被iptable封禁的包?

  • 举例接受
    硬件:硬中断
    内核态:软中断、驱动系统、网络设备层(tcpdump)、协议栈(传输层、网络层,有netfilter)、用户进程
    收包抓不到,发包tcpdump抓到了netfilter过滤的包
    8)网络接受过程中的CPU开销怎么看

  • top命令看(si:CPU处理软中断,hi:CPU处理硬中断)

top - 10:59:13 up 421 days, 22:38,  1 user,  load average: 0.95, 0.62, 0.65
Tasks: 207 total,   1 running, 206 sleeping,   0 stopped,   0 zombie
%Cpu0  : 15.6 us,  3.1 sy,  0.0 ni, 80.9 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu1  : 15.2 us,  3.1 sy,  0.0 ni, 81.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  : 14.9 us,  3.5 sy,  0.0 ni, 81.3 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu3  : 14.6 us,  3.5 sy,  0.0 ni, 81.6 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu4  : 27.4 us,  3.4 sy,  0.0 ni, 68.9 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
%Cpu5  : 25.8 us,  3.4 sy,  0.0 ni, 69.8 id,  0.0 wa,  0.0 hi,  1.0 si,  0.0 st
%Cpu6  : 27.2 us,  3.7 sy,  0.0 ni, 68.4 id,  0.0 wa,  0.0 hi,  0.7 si,  0.0 st
%Cpu7  : 25.0 us,  3.7 sy,  0.0 ni, 69.9 id,  0.0 wa,  0.0 hi,  1.4 si,  0.0 st
KiB Mem :  8010720 total,   138484 free,  1621728 used,  6250508 buff/cache
KiB Swap: 16777212 total, 15481716 free,  1295496 used.  6024296 avail Mem 


I try to explain  these:
us: is meaning of "user CPU time"
sy: is meaning of "system CPU time"
ni: is meaning of" nice CPU time"
id: is meaning of "idle"
wa: is meaning of "iowait" 
hi:is meaning of "hardware irq"
si : is meaning of "software irq"
st : is meaning of "steal time"
 
中文翻译为:
 
us 用户空间占用CPU百分比
sy 内核空间占用CPU百分比
ni 用户进程空间内改变过优先级的进程占用CPU百分比
id 空闲CPU百分比
wa 等待输入输出的CPU时间百分比
hi 硬件中断
si 软件中断 
st: 实时

二、内核如何与用户进程协作

三、内核如何发送网络包

四、深度理解本机网络IO

五、深度理解TCP连接建立过程

六、一条TCP连接消耗多大内存

七、一台机器最多能支持多少条TCP连接

八、网络性能优化建议

九、容器网络虚拟化

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 11:16:52  更:2022-08-06 11:18:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 11:59:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码