Linux内核网络数据包收发

106 阅读29分钟

待看

www.bilibili.com/read/cv1742…

www.bilibili.com/read/cv1743…

网卡的组成

网卡(Network Interface Card,NIC),也称网络适配器,是电脑与局域网相互连接的设备。工作在物理层和数据链路层,主要由 PHY/MAC 芯片、Tx/Rx FIFO、DMA 等组成,其中网线通过变压器接 PHY 芯片、PHY 芯片通过 MII 接 MAC 芯片、MAC 芯片接 PCI 总线。

准备工作

在准备好收包前,Linux需做很多准备工作,例:网络子系统的初始化、协议栈的注册、网卡驱动的初始化、启动网卡等,只有这些都准备好了后,才能开始收包。

注册硬中断

1般是在probe中使用devm_request_irq注册硬中断及handler。

err = devm_request_irq(eth->dev, eth->irq[1],
				       mtk_handle_irq_tx, 0,
				       dev_name(eth->dev), eth);
err = devm_request_irq(eth->dev, eth->irq[2],
				       mtk_handle_irq_rx, 0,
				       dev_name(eth->dev), &eth->rx_napi[0]);

创建 ksoftirqd 内核线程

Linux的软中断都是在专门的内核线程ksoftirqd中进行的,因此有必要看下这些进程怎么初始化,这样才能在后面更准确地了解收包过程。该进程数量不是1个,而是N个,N=机器核数。

系统初始化时在kernel/smpboot.c中调用了smpboot_register_percpu_thread, 该函数进1步会执行到spawn_ksoftirqd(位于kernel/softirq.c)来创建出softirqd进程。

创建ksoftirqd内核线程:

相关代码如下:

//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);

当ksoftirqd被创建出来后,就会进入自己的线程循环函数ksoftirqd_should_run和run_ksoftirqd了。不停地判断有无软中断需被处理。

软中断不仅只有网络软中断,还有其它类型:

//file: include/linux/interrupt.h
enum{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,  
};

网络子系统初始化

linux内核通过调用subsys_initcall来初始化各子系统,在源代码目录里可grep出许多对此函数的调用。

网络子系统的初始化,会执行到net_dev_init:

//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);

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

另外open_softirq每种软中断都注册1个处理函数。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;
}

协议栈注册

OS内核实现了网络层的ip,也实现了传输层的tcp和udp。这些协议对应的实现函数分别是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和平时写代码的方式不1样,内核通过注册的方式来实现。

Linux内核中的fs_initcall和subsys_initcall类似,也是初始化模块的入口。fs_initcall调用inet_init后开始网络协议栈注册。通过inet_init,将这些函数注册到了inet_protos和ptype_base数据结构中。

//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;
}

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()。

扩展下,若看下ip_rcv和udp_rcv等函数的代码能看到很多协议的处理过程。

例:ip_rcv中会处理netfilter和iptable过滤,若有很多或很复杂的 netfilter规则,这些规则都是在软中断的上下文中执行的,会加大网络延迟。

再例:udp_rcv中会判断socket收队列是否满了。对应的内核参数是net.core.rmem_max和net.core.rmem_default。若有兴趣,建议大家好好读1下inet_init此函数的代码。

linux要实现许多协议,如arp,icmp,ip,udp,tcp,每个协议都会将自己的处理函数注册下,方便包来了迅速找到对应的处理函数

首先来看协议族是如何注册到内核,并被 socket 子系统使用的。

sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

内核会查找UDP对应的ops,并赋给 socket 的相应字段。

inet_init

内核初始化的很早阶段就执行了 inet_init,此函数会注册 AF_INET 协议族 ,及该协议族内的各协议栈(TCP,UDP,ICMP 和 RAW),并调用初始化函数使协议栈准备好处理包。inet_init 定义在net/ipv4/af_inet.c 。

rc = proto_register(&udp_prot, 1);
/*
*	Tell SOCKET that we are alive...
*/
(void)sock_register(&inet_family_ops);

AF_INET 协议族导出1个包含 create 方法的 struct net_proto_family 类型实例。当从APP创建 socket 时,内核会调用inet_create(用socket注册时的AF_INET参数,定位到inet_family_ops。再用inet_family_ops定位到inet_create):

static const struct net_proto_family inet_family_ops = {
	.family = PF_INET,//同AF_INET
	.create = inet_create,
	.owner	= THIS_MODULE,
};

inet_create 根据传递的 socket 参数,在已注册的协议中查找对应的协议:

lookup_protocol:
	err = -ESOCKTNOSUPPORT;
	rcu_read_lock();
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
		err = 0;
		/* Check the non-wild match. */
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		} else {
			/* Check for the two wild cases. */
			if (IPPROTO_IP == protocol) {
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}

然后,将该协议的ops赋给此新创建的 socket:

sock->ops = answer->ops;

可在 af_inet.c 中看到所有协议的初始化参数。 下面是UDP的初始化参数:

/* Upon startup we insert all the elements in inetsw_array[] into
 * the linked list inetsw.
 */
static struct inet_protosw inetsw_array[] =
{
        {
                .type =       SOCK_DGRAM,
                .protocol =   IPPROTO_UDP,
                .prot =       &udp_prot,//prot绑定udp对应操作
                .ops =        &inet_dgram_ops,//ops中绑定socket对应操作
                .no_check =   UDP_CSUM_DEFAULT,
                .flags =      INET_PROTOSW_PERMANENT,
       },
            /* .... more protocols ... */

再找到IPPROTO_UDP的ops(由IPPROTO_UDP定位其ops prot):

const struct proto_ops inet_dgram_ops = {
	.family		   = PF_INET,
	.owner		   = THIS_MODULE,
	.release	   = inet_release,
	.bind		   = inet_bind,
	.connect	   = inet_dgram_connect,
	.socketpair	   = sock_no_socketpair,
	.accept		   = sock_no_accept,
	.getname	   = inet_getname,
	.poll		   = udp_poll,
	.ioctl		   = inet_ioctl,
	.gettstamp	   = sock_gettstamp,
	.listen		   = sock_no_listen,
	.shutdown	   = inet_shutdown,
	.setsockopt	   = sock_common_setsockopt,
	.getsockopt	   = sock_common_getsockopt,
	.sendmsg	   = inet_sendmsg,
	.read_sock	   = udp_read_sock,
	.recvmsg	   = inet_recvmsg,
	.mmap		   = sock_no_mmap,
	.sendpage	   = inet_sendpage,
	.set_peek_off	   = sk_set_peek_off,
#ifdef CONFIG_COMPAT
	.compat_ioctl	   = inet_compat_ioctl,
#endif
};

struct proto udp_prot = {
	.name			= "UDP",
	.owner			= THIS_MODULE,
	.close			= udp_lib_close,
	.pre_connect	= udp_pre_connect,
	.connect		= ip4_datagram_connect,
	.disconnect		= udp_disconnect,
	.ioctl			= udp_ioctl,
	.init			= udp_init_sock,
	.destroy		= udp_destroy_sock,
	.setsockopt		= udp_setsockopt,
	.getsockopt		= udp_getsockopt,
	.sendmsg		= udp_sendmsg,
	.recvmsg		= udp_recvmsg,
	.sendpage		= udp_sendpage,
	.release_cb		= ip4_datagram_release_cb,
	.hash			= udp_lib_hash,
	.unhash			= udp_lib_unhash,
	.rehash			= udp_v4_rehash,
	.get_port		= udp_v4_get_port,
#ifdef CONFIG_BPF_SYSCALL
	.psock_update_sk_prot	= udp_bpf_update_proto,
#endif
	.memory_allocated	= &udp_memory_allocated,
	.sysctl_mem		= sysctl_udp_mem,
	.sysctl_wmem_offset	= offsetof(struct net, ipv4.sysctl_udp_wmem_min),
	.sysctl_rmem_offset	= offsetof(struct net, ipv4.sysctl_udp_rmem_min),
	.obj_size		= sizeof(struct udp_sock),
	.h.udp_table		= &udp_table,
	.diag_destroy		= udp_abort,
};

kerneltravel.net/blog/2020/n…

网卡驱动初始化

每个驱动程序(不仅是网卡驱动)会使用 module_init 向内核注册1个初始化函数,当驱动被加载时,内核会调用此函数。

如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下。

主要执行的操作:

第5步中:网卡驱动实现了ethtool所需接口,也在这里完成函数地址的注册。当 ethtool 发起1个系统调用后,内核会找到对应操作的回调函数。对于igb网卡,其实现函数都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。

相信这次能彻底理解ethtool的工作原理了吧?此命令之所以能查看网卡收发包统计、能修改网卡自适应模式、能调整RX 队列的数量和大小,是因为ethtool命令最终调用到了网卡驱动的相应方法,而不是ethtool本身有此超能力。

第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,
 ......

第7步:在igb_probe初始化过程中,还调用到了igb_alloc_q_vector。他注册了1个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);
}

启动网卡

当上面的初始化都完后,就可启动网卡了。

回忆前面网卡驱动初始化时,提到了驱动向内核注册了 structure net_device_ops 变量,它包含着网卡启用、发包、设置MAC等回调函数(函数指针)。当启用1个网卡时(例,通过 ifconfig eth0 up),net_device_ops 中的 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这1步操作中,分配了RingBuffer,并建立内存和Rx队列的映射关系。(Rx Tx 队列的数量和大小可通过 ethtool 配置)。

再接着看中断函数注册igb_request_irq:

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的绑定行为)。

当做好以上准备工作以后,就可开门迎客了!

网络协议栈

收包

包到达网卡,通过DMA方式将数据映射到内存中,到达环形buffer,并触发硬中断,通知cpu有包来了,这时cpu会通过中断表调用1个已注册的中断函数,此中断函数会调用驱动程序,驱动程序先禁用网卡的中断,告诉网卡下次来了包就直接写进内存,不用写进cpu了,这样可提高cpu效率,避免cpu被不停中断。硬中断后执行软中断,硬中断立即触发,而软中断交给1个内核线程去处理,特点是会延迟执行。后软中断处理程序会调用1些处理函数处理包,把环形buffer内的包打包,做成1个个的sk_buffer再交给协议栈,处理完后将应用数据放进socket的buffer中,后APP就可读取数据了。

网卡是计算机里的1个硬件,专门负责收和发包。包到达网卡后,按照FIFO顺序被存入网卡的收队列,网卡通过 DMA 技术,将包写入到Ring Buffer。

Ring Buffer是在网卡驱动程序启动时创建和初始化的,存储sk_buff buffer的描述符(物理地址和大小等)。

当包到达时,从Ring Buffer获取指向的sk_buff描述符,通过DMA将数据写入该地址。等sk_buff中的数据交由上层协议栈处理后,Ring Buffer中的描述更新为新分配的sk_buff。

硬件中断

首先:当数据帧从网线到达网卡上时,第1站是网卡的收队列。

网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡前关联的内存里,这时CPU都是无感的。当DMA操作完后,网卡会像CPU发起1个硬中断,通知CPU有包到达。

网卡数据硬中断处理过程:

注意:当RingBuffer满时,新来的包将丢弃。ifconfig查看网卡时,有个overruns,表示因为环形队列满被丢弃的包。若发现丢包,可能需通过ethtool命令来加大环形队列的长度。

在启动网卡1节,说到了网卡的硬中断注册的处理函数是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只是记录1下硬件中断频率(据说目的是减少对CPU的中断频率)。

顺着napi_schedule调用1路跟踪下去,__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是1个双向列表,其中的设备都带有输入帧等着被处理。紧接着__raise_softirq_irqoff触发了1个软中断NET_RX_SOFTIRQ, 此所谓的触发过程只是对1个变量进行了1次或运算而已。

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))

Linux在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断的。

硬中断处理过程很短。只记录了1个寄存器,修改了下CPU的poll_list,然后发出个软中断。就这么简单,硬中断工作就算是完了。

接着网卡向 CPU 发起硬件中断(当设备上有数据到达时:会给CPU的相关引脚上触发1个电压变化,通知CPU来处理数据),当 CPU 收到硬件中断请求后,根据中断注册表,找到注册的中断处理函数。

因此Linux中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可慢慢处理。Linux 2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。硬中断是通过给CPU物理引脚施加电压变化,而软中断是通过给内存中的1个变量的2进制值以通知软中断处理程序。

硬件中断处理函数会做如下事:

1、屏蔽网卡的中断

避免CPU被频繁中断而无法处理其他任务,屏蔽中断是告诉网卡已知道内存中有数据了,下次再收到包直接写内存,不要再通知 CPU 了。

2、发起软中断,恢复刚才屏蔽的中断

内核中的 ksoftirqd 线程收到软中断后,调用软中断的处理函数来轮询处理数据,即:从Ring Buffer 中获取1个数据帧,用 sk_buff 表示,作为1个包交给网络协议栈从下到上逐层处理。

ksoftirqd内核线程处理软中断

ksoftirqd内核线程:

内核线程初始化时,介绍了ksoftirqd中2个线程函数ksoftirqd_should_run和run_ksoftirqd。

其中ksoftirqd_should_run代码如下:

static int ksoftirqd_should_run(unsigned int cpu){
    return local_softirq_pending();
}
#define local_softirq_pending() \    __IRQ_STAT(smp_processor_id(), __softirq_pending)

这里看到和硬中断中调用了同1个函数local_softirq_pending。使用方式不同的是硬中断位置是为了写入标记,这里仅读取。若硬中断中设置了NET_RX_SOFTIRQ,这里自然能读取的到。

接下来会真正进入线程函数中run_ksoftirqd处理:

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();
}

在__do_softirq中,判断根据当前CPU的软中断类型,调用其注册的action方法。

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函数就会被执行到了。

硬中断中设置软中断标记,和ksoftirq的判断是否有软中断到达,都是基于smp_processor_id()。意味着只要硬中断在哪个CPU上被响应,软中断也是在此CPU上处理的。所以说,若发现的Linux软中断CPU消耗都集中在1个核上,需调整硬中断的CPU亲和性,来将硬中断打散到不同的CPU核上去。

再来把精力集中到此核心函数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上取下来。

为何需2个函数?因为有可能帧要占多个RingBuffer,所以是在1个循环中获取的,直到帧尾部。获取下来的1个数据帧用1个sk_buff来表示。收取完数据后,对其进行1些校验,然后开始设置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特性,把相关小包合并成1个大包,减少传送给网络栈的包数,有助于减少 CPU 的使用量。

此函数主要就是调用了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中,包将被送到协议栈中。声明,以下的5.3、5.4、5.5也都属于软中断的处理过程,只不过由于篇幅太长,单独拿出来成小节。

网络协议栈

对包的处理流程:

1、网络接口层-netif_receive_skb

检查报文的合法性和正确性,若非法或报文校验错误则丢弃,否则找出上层协议类型(IPv4/IPv6),去掉帧头、帧尾,然后交给网络层处理。

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);
            //ptype_base是1个hash table。ip_rcv函数地址就是存在此hash table中的。 作者:补给站Linux内核 https://www.bilibili.com/read/cv17435249/ 出处:bilibili
            pt_prev = ptype;
        }
    }


} 

//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)。

2、网络层-ip_rcv()

取出IP头,判断包下步的走向,是转发/交给上层。当确认包是发给本机后,就取出上层协议的类型(如TCP/UDP),去掉IP头,然后交给传输层处理。

在inet_init中注册了类型为ETH_P_IP协议的包回调函数ip_rcv

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
	   struct net_device *orig_dev)
{
	struct net *net = dev_net(dev);
	skb = ip_rcv_core(skb, net);//IP包主处理函数,包合法性检测等
	if (skb == NULL)
		return NET_RX_DROP;
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,//包转发前,先进行neifilter检测
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
} 

NF_HOOK是个钩子函数,当执行完注册的钩子后就会执行到最后1个参数指向的函数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

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包将会进1步被派送到更上层的协议中,udp和tcp。

3、传输层-udp_rcv()

传输层取出 TCP/UDP 头后,根据4元组,找出对应的 Socket,并把数据拷贝到 Socket 的收buffer。

udp模块的注册在inet_init()中,当收到udp报文,会调用udp_protocol中的handler函数udp_rcv()。

if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
 printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");

udp_rcv() -> __udp4_lib_rcv() 完成udp报文收,初始化udp的校验和,并不验证校验和的正确性。

@skb: 输入包
@udptable:已绑定端口的UDP传输控制块,将从该哈希表查找给skb属于哪个socket
@proto:L4协议号,到这里可能是IPPROTO_UDP或者IPPROTO_UDPLITE
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){
    //在udptable中以4元组查找相应的sk,找到后将包放到sk->sk_receive_queue
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);


    if (sk != NULL) {
        int ret = udp_queue_rcv_skb(sk, skb
    }
    //若没找到,则发1个目标不可达的icmp包
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);


} 

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判断用户是否正在此socket上进行系统调用(socket被占用),若否,那就可直接放到socket的收队列中。若有,那就通过sk_add_backlog把包添加到backlog队列。当用户释放socket时,内核会检查backlog队列,若有数据再移动到收队列中。

sk_rcvqueues_full收队列若满了,将直接丢包。收队列大小受内核参数net.core.rmem_max和net.core.rmem_default影响。

何时sk会被占用?何时sk->sk_backlog上的skb被处理的?

创建socket时,sys_socket() -> inet_create() -> sk_alloc() -> sock_lock_init() -> sock_lock_init_class_and_name()初始化sk->sk_lock_owned=0。

比如当销毁socket时,udp_destroy_sock()会调用lock_sock()对sk加锁,操作完后,调用release_sock()对sk解锁。

void udp_destroy_sock(struct sock *sk)
{
 lock_sock(sk);
 udp_flush_pending_frames(sk);
 release_sock(sk);
}

实际上,lock_sock()设置sk->sk_lock.owned=1;而release_sock()设置sk->sk_lock.owned=0,并处理sk_backlog队列上的报文,release_sock() -> __release_sock(),对于sk_backlog队列上的每个报文,调用sk_backlog_rcv() -> sk->sk_backlog_rcv()。同样是在socket的创建中,sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv()即__udp_queue_rcv_skb(),此函数的作用上面已经讲过,将skb添加到sk_receive_queue,这样,所有的sk_backlog上的报文转移到了sk_receive_queue上。简单来说,sk_backlog队列的作用就是,锁定时报文临时存放在此,解锁时,报文移到sk_receive_queue队列。

4、应用层-recvfrom系统调用

最后,APP调用 Socket 接口,将内核的 Socket 收buffer的数据拷贝到应用层的buffer。

在用户调用recvfrom()/recv()收报文前,发给该socket的报文都会被添加到sk->sk_receive_queue上,recvfrom()和recv()要做的就是从sk_receive_queue上取出报文,拷贝到用户空间,供用户使用。

socket数据结构中的const struct proto_ops对应的是协议的方法集合。每个协议都会实现不同的方法集,对于IPv4 Internet协议族,每种协议都有对应的处理方法。对于udp,是通过inet_dgram_ops来定义的,其中注册了inet_recvmsg。

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数据结构中的另1个数据结构struct sock *sk是1个很大、很重要的子结构体。其中的sk_prot又定义了2级处理函数。对于udp,会被设置成udp实现的方法集udp_prot。

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的实现过程。 在inet_recvmsg调用了sk->sk_prot->recvmsg。

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。

int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int noblock, int flags, int *addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
	struct sk_buff *skb;
	unsigned int ulen, copied;
	int peeked;
	int err;
	int is_udplite = IS_UDPLITE(sk);


	// 需返回源地址信息,设置源地址长度
	if (addr_len)
		*addr_len = sizeof(*sin);


	// 若设置了MSG_ERRQUEUE标记,只读取错误信息
	if (flags & MSG_ERRQUEUE)
		return ip_recv_error(sk, msg, len);


try_again:
	// 根据是否需阻塞,从收队列中取出1个skb
	skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0), &peeked, &err);
	if (!skb)
		goto out;


	// ulen为该skb中包含的数据载荷长度
	ulen = skb->len - sizeof(struct udphdr);
	// len为应用程序指定的buffer大小,所以下面的逻辑含义为:
	// 1. 若应用提供的buffer超过了该包的数据长度,调整要拷贝的数据量为该skb的实际数据量
	// 2. 若应用提供的buffer不够大,需截断包,并设置截断标记
	copied = len;
	if (copied > ulen)
		copied = ulen;
	else if (copied < ulen)
		msg->msg_flags |= MSG_TRUNC;


	/*
	 * If checksum is needed at all, try to do it while copying the
	 * data.  If the data is truncated, or if we only want a partial
	 * coverage checksum (UDP-Lite), do it before the copy.
	 */
	// 对于截断的包和尚未完成校验的包,先校验,校验出错则尝试读取下1个包
	// 条件2实际上只用于UDPLite,因为UDP协议的校验在收过程的第1步就完成了
	if (copied < ulen || UDP_SKB_CB(skb)->partial_cov) {
		if (udp_lib_checksum_complete(skb))
			goto csum_copy_err;
	}
	// 根据是否需校验,调用不同的数据拷贝函数
	if (skb_csum_unnecessary(skb))
		err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov, copied);
	else {
	    // 在数据拷贝过程中进行校验和计算
		err = skb_copy_and_csum_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov);
		if (err == -EINVAL)
			goto csum_copy_err;
	}
	// 数据拷贝失败,返回错误
	if (err)
		goto out_free;
	// 只有非PEEK读取才更新统计信息
	if (!peeked)
		UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_INDATAGRAMS, is_udplite);
	// 更新包收的时间戳到sk->sk_stamp中
	sock_recv_timestamp(msg, sk, skb);


	// 拷贝包源地址信息,该地址会返回给应用程序
	if (sin) {
		sin->sin_family = AF_INET;
		sin->sin_port = udp_hdr(skb)->source;
		sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
		memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
	}
	// 获取控制信息
	if (inet->cmsg_flags)
		ip_cmsg_recv(msg, skb);


	// 读取成功,返回值err表示的是已经读取到的字节数
	err = copied;
	if (flags & MSG_TRUNC)
		err = ulen;


out_free:
	// 释放该skb的数据
	skb_free_datagram_locked(sk, skb);
out:
	return err;


csum_copy_err:
	lock_sock(sk);
	if (!skb_kill_datagram(sk, skb, flags))
		UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
	release_sock(sk);


	if (noblock)
		return -EAGAIN;
	goto try_again;
}

从收队列中获取skb

struct sk_buff *__skb_recv_datagram(struct sock *sk,
				    struct sk_buff_head *sk_queue,
				    unsigned int flags, int *off, int *err)
{
	struct sk_buff *skb, *last;
	long timeo;
    // 根据是否设置了非阻塞标记,决定超时等待时间。对于非阻塞模式,timeo为0
	timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);


	do {
		skb = __skb_try_recv_datagram(sk, sk_queue, flags, off, err,
					      &last);
		if (skb)
			return skb;


		if (*err != -EAGAIN)
			break;
	} while (timeo &&
		 !__skb_wait_for_more_packets(sk, sk_queue, err,
					      &timeo, last));


	return NULL;
}

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

Linux收1个包的CPU开销

1)第1块是用户进程调用系统调用陷入内核态的开销;

2)第2块是CPU响应包的硬中断的CPU开销;

3)第3块是ksoftirqd内核线程的软中断上下文花费的。

发包

应用层

首先,APP系统调用 Socket (如 sendto,sendmsg 等)发包的接口。从用户态陷入到内核态的socket层中。

ret = sendto(socket, buffer, buflen, 0, &dest, sizeof(dest));

socket层会申请1个内核态的 sk_buff 内存,将用户待发的数据拷贝到 sk_buff 内存,并将其加入到Socket发buffer等待网络协议栈的处理。数据穿过协议层,这1过程(在许多情况下)会将data转换成packet。

由于包从APP传到内核时是原始数据,协议栈要在原始数据中加入通信约定才能保证数据到达服务端能被正确识别。网络协议栈从 Socket 发buffer中,取出包,然后按照 TCP/IP 栈的分层,从上到下逐层处理,各层将协议的头信息不断插入到包中。

1、传输层udp_sendmsg

会为其添加TCP头,同时拷贝1个新的 sk_buff 副本 ,这是因为 sk_buff 在到达网卡发成时,会被释放掉,而TCP是支持重传的,为确保包可靠传输,在收到对方的 ACK 之前,此 sk_buff 不能被删除。

发时有2种调用方式:sys_send()和sys_sendto(),2者的区别在于sys_sendto()需给入目的地址的参数;而sys_send()调用前需调用sys_connect()来绑定目的地址信息;2者的后续调用是相同的。若调用sys_sendto()发,地址信息在sys_sendto()中从用户空间拷贝到内核空间,而报文内容在udp_sendmsg()中从用户空间拷贝到内核空间。

      sys_send() -> sys_sendto()
      sys_sendto() -> sock_sendmsg() -> __sock_sendmsg() -> sock->ops->sendmsg()
                         ==> inet_sendmsg() -> sk->sk_prot->sendmsg()
                         ==> udp_sendmsg()


inet_sendmsg() - net/ipv4/af_inet.c
    return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg, sk, msg, size)
        udp_sendmsg() - net/ipv4/udp.c
            udp_send_skb() - net/ipv4/udp.c
                ip_send_skb() - net/ipv4/ip_output.c
                    ip_local_out() - net/ipv4/ip_output.c
                        ip_local_out() - net/ipv4/ip_output.c
                            dst_output() - net/dst.h
                                return INDIRECT_CALL_INET(skb_dst(skb)->output, ip6_output, ip_output, net, sk, skb);

udp_sock结构体中的pending用于标识当前udp_sock上是否有待发数据,若有,则直接goto do_append_data继续添加数据;否则先要做些初始化工作,再才添加数据。实际上,pending!=0表示此调用前已经有数据在udp_sock中的,每次调和sendto()发数据时,pending初始等于0;在添加数据时,设置up->pending = AF_INET。直到最后调用udp_push_pending_frames()将数据发给IP层或skb_queue_empty(&sk->sk_write_queue)发链表上为空,这时设置up->pending = 0。因此,这里可以看到,报文发时pending值的变化:

通常使用sendto()发都是1次调用对应1个报文,即pending=0->AF_INET->0;但若调用sendto()时参数用到了MSG_MORE标志,则pending=0->AF_INET,直到调用sendto()时未使用MSG_MORE标志,表示此次发数据是最后1部分数据时,pending=AF_INET->0。

if (up->pending) {
 lock_sock(sk);
 if (likely(up->pending)) {
  if (unlikely(up->pending != AF_INET)) {
   release_sock(sk);
   return -EINVAL;
  }
  goto do_append_data;
 }
 release_sock(sk);
}

若pending=0没有待发数据,执行初始化操作:报文长度、地址信息、路由项。

ulen初始为sendto()传入的数据长度,由于是第1部分数据(若没有后续数据,则就是报文),ulen要添加udp报头的8字节。

ulen += sizeof(struct udphdr);

这段代码获取要发数据的目的地址和端口号。1种情况是调用sendto()发数据,此时目的信息以参数传入,存储在msg->msg_name中,因此从中取出daddr和dport;另1种情况是调用connect(), send()发数据,在connect()调用时绑定了目的的信息,存储在inet中,并且由于是调用了connect(),sk->sk_state会设置为TCP_ESTABLISHED。以后调用send()发数据时,无需再给入目的信息参数,因此从inet中取出dadr和dport。而connected表示了该socket是否已绑定目的。

if (msg->msg_name) {

struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;

if (msg->msg_namelen < sizeof(*usin))

return -EINVAL;

if (usin->sin_family != AF_INET) {

if (usin->sin_family != AF_UNSPEC)

return -EAFNOSUPPORT;

}

daddr = usin->sin_addr.s_addr;

dport = usin->sin_port;

if (dport == 0)

return -EINVAL;

} else {

if (sk->sk_state != TCP_ESTABLISHED)

return -EDESTADDRREQ;

daddr = inet->inet_daddr;

dport = inet->inet_dport;

connected = 1;

}

下1步是获取路由项rt,若已连接(调用过connect),则路由信息在connect()时已获取,直接拿就可以了;若未连接或拿到的路由项已被删除,则需重新在路由表中查找,还是使用ip_route_output_flow()来查找,若是连接状态的socket,则要用新找到的rt来更新socket,当然,前提条件是之前的rt已过期。

if (rt == NULL) {

……

err = ip_route_output_flow(net, &rt, &fl, sk, 1);

……

if (connected)

sk_dst_set(sk, dst_clone(&rt->u.dst));

}

存储信息daddr, dport, saddr, sport到cork.fl中,它们会在生成udp报头和计算udp校验和时用到。up->pending=AF_INET标识了数据添加的开始,下面将开始数据的添加工作。

inet->cork.fl.fl4_dst = daddr;

inet->cork.fl.fl_ip_dport = dport;

inet->cork.fl.fl4_src = saddr;

inet->cork.fl.fl_ip_sport = inet->inet_sport;

up->pending = AF_INET;

若pending!=0或执行完初始化操作,则直接执行添加数据操作:

up->len表示要发数据的总长度,包括udp报头,因此每发1部分数据就要累加它的长度,在发后up->len被清0。然后调用ip_append_data()添加数据到sk->sk_write_queue,它会处理数据分片等问题,在 ”ICMP模块” 中有详细分析过。

up->len += ulen;

getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;

err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,

sizeof(struct udphdr), &ipc, &rt,

corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

ip_append_data()添加数据正确会返回0,否则udp_flush_pending_frames()丢弃将添加的数据;若添加数据正确,且没有后续的数据到来(由MSG_MORE来标识),则udp_push_pending_frames()将数据发给IP层,下面将详细分析此函数。最后1种情况是当sk_write_queue上为空时,它触发的条件必须是发多个报文且sk_write_queue上为空,而实际上在ip_append_data过后sk_write_queue不会为空的,因此正常情况下并不会发生。哪种情况会发生呢?重置pending值为0就是在这里完成的,3个条件语句都会将pending设置为0。

if (err)

udp_flush_pending_frames(sk);

else if (!corkreq)

err = udp_push_pending_frames(sk);

else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))

up->pending = 0;

数据已经处理完成,释放取到的路由项rt,若有IP选项,也释放它。若发数据成功,返回发的长度len;否则根据错误值err进行错误处理并返回err。

ip_rt_put(rt);
if (free)
 kfree(ipc.opt);
if (!err)
 return len;
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
 UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;

在 “ICMP模块” 中往IP层发数据使用的是ip_push_pending_frames()。而在UDP模块中往IP层发数据使用的是ip_push_pending_frames()。而在UDP模块中往IP层发数据的udp_push_pending_frames()只是对ip_push_pending_frames()的封装,主要是增加对UDP的报头的处理。同理,udp_flush_pending_frames()也是,只是它更简单,仅重置了up->len和up->pending的值,重置后可以开始1个新报文。udp_push_pending_frames()封装了哪些处理呢。

udp_push_pending_frames() 发数据给IP层

设置udp报头,包括源端口source,目的端口dest,报文长度len。

uh = udp_hdr(skb);

uh->source = fl->fl_ip_sport;

uh->dest = fl->fl_ip_dport;

uh->len = htons(up->len);

uh->check = 0;

计算udp报头中的校验和,包括了伪报头、udp报头和报文内容。

if (is_udplite)
 csum  = udplite_csum_outgoing(sk, skb);
else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */
 skb->ip_summed = CHECKSUM_NONE;
 goto send;
} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */
 udp4_hwcsum_outgoing(sk, skb, fl->fl4_src, fl->fl4_dst, up->len);
 goto send;
} else       /*   `normal' UDP    */
 csum = udp_csum_outgoing(sk, skb);
uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len, sk->sk_protocol, csum);

将报文发给IP层,此函数已经分析过了。

err = ip_push_pending_frames(sk);

同样,在发完报文后,重置len和pending的值,以便开始下1个报文发。

up->len = 0;
up->pending = 0;

2、网络层ip_output

选取路由(确认下1跳的 IP)、填充 IP 头、netfilter 过滤、对超过 MTU 的包分片。处理完后会交给网络接口层处理。

ip_output() - net/ipv4/ip_output.c
    ip_finish_output() - net/ipv4/ip_output.c
        __ip_finish_output() - net/ipv4/ip_output.c
            ip_finish_output_gso() - net/ipv4/ip_output.c
                ip_finish_output2() - net/ipv4/ip_output.c
                    neigh_output() - net/neighbor.h

3、网络接口层

通过 ARP 获得下1跳的 MAC(若目的 MAC 不在 ARP 缓存表中,将触发1次 ARP 广播来查找 MAC),填充帧头和帧尾,将其放到发队列中。然后触发软中断告诉网卡驱动程序:队列中有新包需发。驱动程序收到通知会通过 DMA ,从发包队列中读出网络帧,并通过DMA将数据写入网卡的FIFO发队列。

neigh_output() - net/neighbor.h
    return n->output(n, skb) - net/neighbor.h
    neigh_hh_output() - net/neighbor.h
        dev_queue_xmit() - net/core/dev.c

4、网卡设备dev_queue_xmit

网卡设备从FIFO发qdisc中取出包,将其发到网络;当发完时,网卡设备会触发1个硬中断来释放内存,主要是释放 sk_buff内存和清理 RingBuffer 内存。最后,当收到此 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff。

dev_queue_xmit() - net/core/dev.c//用此来
    __dev_queue_xmit() - net/core/dev.c
        dev_hard_start_xmit() - net/core/dev.c
            xmit_one() - net/core/dev.c
                netdev_start_xmit() - include/linux/netdevice.h
                    __netdev_start_xmit() - include/linux/netdevice.h
                        return ops->ndo_start_xmit(skb, dev)

继续看dev_hard_start_xmit,此函数较简单,调用xmit_one来发1个到多个包了

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
				    struct netdev_queue *txq, int *ret)
{
	struct sk_buff *skb = first;
	int rc = NETDEV_TX_OK;


	while (skb) {
		struct sk_buff *next = skb->next;/*取出skb的下1个数据单元*/


		skb_mark_not_on_list(skb);
		rc = xmit_one(skb, dev, txq, next != NULL);/*将此包送到driver Tx函数,因为dequeue的数据也会从这里发,所以会有netx!*/
		if (unlikely(!dev_xmit_complete(rc))) {/*若发不成功,next还原到skb->next 退出*/
			skb->next = next;
			goto out;
		}


		skb = next;/*若发成功,把next置给skb,1般的next为空 这样就返回,若不为空就继续发!*/
		/*若txq被stop,并且skb需发,就产生TX Busy的问题!*/
        if (netif_tx_queue_stopped(txq) && skb) {
			rc = NETDEV_TX_BUSY;
			break;
		}
	}


out:
	*ret = rc;
	return skb;
}

static int xmit_one(struct sk_buff *skb, struct net_device *dev,
		    struct netdev_queue *txq, bool more)
{
	unsigned int len;
	int rc;
/*若有抓包的工具,此地方会进行抓包,such as Tcpdump*/
	if (dev_nit_active(dev))
		dev_queue_xmit_nit(skb, dev);
	len = skb->len;
	PRANDOM_ADD_NOISE(skb, dev, txq, len + jiffies);
/*调用netdev_start_xmit,快到driver的tx函数了*/
	trace_net_dev_start_xmit(skb, dev);
	rc = netdev_start_xmit(skb, dev, txq, more);
	trace_net_dev_xmit(skb, rc, dev, len);
	return rc;
}

驱动程序处理函数

网络设备层调用驱动注册的ndo_start_xmit()发包到驱动。以igb驱动为例,ndo_start_xmit()即igb_xmit_frame()。

igb_xmit_frame() - drivers/net/ethernet/intel/igb/igb_main.c
    igb_xmit_frame_ring() - drivers/net/ethernet/intel/igb/igb_main.c
        igb_tx_map() - drivers/net/ethernet/intel/igb/igb_main.c

包内存释放函数

网卡发完包后,会给cpu发1个硬中断通知cpu,cpu简单处理后,发出软中断NET_RX_SOFTIRQ。最后的中断处理函数net_rx_action()中完成包的释放。

硬中断处理函数:

igb_msix_ring() - drivers/net/ethernet/intel/igb/igb_main.c
    __napi_schedule() - net/core/dev.c
        ____napi_schedule() - net/core/dev.c
            __raise_softirq_irqoff(NET_RX_SOFTIRQ) - net/core/dev.c

软中断处理函数:

ksoftirqd为软中断处理进程,ksoftirqd收到NET_RX_SOFTIRQ软中断后,执行软中断处理函数net_rx_action(),调用网卡驱动poll()收包。在poll()中调用igb_clean_tx_irq()完成包的释放。

run_ksoftirqd() - kernel/softirqd.c
    __do_softirq() - kernel/softirqd.c
        h->action(h) - kernel/softirqd.c
            net_rx_action() - net/core/dev.c
                napi_poll() - net/core/dev.c
                    __napi_poll - net/core/dev.c
                        work = n->poll(n, weight) - net/core/dev.c
                            igb_poll() - drivers/net/ethernet/intel/igb/igb_main.c
                                igb_clean_tx_irq() - drivers/net/ethernet/intel/igb/igb_main.c