开关中断:
local_irq_disable()
local_irq_enable()
下面的方法更安全:
local_irq_save(flags) //保存中断环境并关中断
local_irq_restore(flags) //恢复中断环境
如果本来就在中断环境中,然后调用local_irq_disable不会做事情,但local_irq_enable会开中断,而这是实际是要保持原来的中断环境。
中断的上半部和下半部都是针对硬件中断来说的,上半部就是硬件中断,需要在中断环境中执行,下半部就是软中断、tasklet或工作队列机制的处理函数,可以在开中断中执行,但是下半部的处理函数的某些代码不能被中断,就要在关中断中执行。
xxx_schedule()函数就是把某某东西加入到处理队列,然后调用相应的软件中断,这个函数先检查这个软中断是否已被调度了,如果是则只需直接返回。
以太网收包的硬件中断的中断号是多少?处理函数是哪个?注册中断的函数为request_irq(),可以在以太网驱动里找找看gmac的各种中断注册时的中断号和处理函数。同时可以看一下内核中arch/mips/include/asm/mach-atheros/xxxx.h和arch/mips/xxxxxxx/irq.c这两个文件,定义了xxxxxxx使用的中断相关的宏和函数。
192.168.1.102 -> 192.168.2.100发tcp包的时候走的hook点:
NF_INET_PRE_ROUTING (skb->dev_name=br0) -400, -200, -100, 2147483647
NF_INET_FORWARD (skb->dev_name=br0) 0, 2147483647
NF_INET_POST_ROUTING (skb->dev_name=eth0.2) 100, 2147483647, 2147483647
在ip_rcv()之前:
netif_rx的主要任务如下:
- 对sk_buf的一些字段初始化,如帧接收时间;
- 把接收的帧存储到CPU的输入队列(如果队列有空间的话),然后触发相关联的软IRQ NET_RX_SOFTIRQ;
- 更新有关拥塞等级的统计数据。
这个输入队列就是softnet_data->input_pkt_queue,如果队列长度为空就触发NET_RX_SOFTIRQ,如果不为空,说明NET_RX_SOFTIRQ已经被调度了,直接返回就好。触发NET_RX_SOFTIRQ是通过napi_schedule(&queue->backlog)完成的。
NET_RX_SOFTIRQ的处理函数为net_rx_action(),该函数调用process_backlog()从softnet_data->input_pkt_queue中拿出一个数据包,然后调用netif_receive_skb(skb)进行收包。
athr_receive_pkt()没有接受参数,它从gmac的ring buffer中拿数据包。
在netif_receive_skb()的最后加打印查看pt_prev->func指针,发现func指针指向两个函数:vlan_skb_recv和ip_rcv,所以可以看出函数调用顺序为:
xxxx_receive_pkt -> netif_receive_skb -> vlan_skb_recv -> … -> netif_receive_skb -> ip_rcv
802.1Q虚拟设备:IEEE标准,以VLAN报头扩充802.3/ethernet帧头,得以建立VLAN。
虚拟设备也会分配一个net_device结构,但不是所有的,如别名设备。
vlan_skb_recv会根据vlan id将skb->dev由eth0转换为eth0.1或eth0.2,这样在发包时才会再走vlan协议,然后通过vlan_check_reorder_header()函数将skb->data的前12字节后移4字节,即覆盖掉vlan字段,然后将skb->mac_header后移4字节,指向新的mac头部。这样,数据包已经是内核可识别的了,接着调用netif_rx将数据包放入CPU的收包队列。
当需要走br时,上面流程的…就用下面的填充:
netif_rx -> net_rx_action -> process_backlog -> netif_receive_skb -> handle_bridge -> br_handle_frame_hook
如果handle_bridge()发现skb->dev的br_port不为空,就会进入br的netfilter处理,否则直接返回。
br_handle_frame_hook()就是br_handle_frame(),br_handle_frame()中就会走br的netfilter,
br_handle_frame_finish()会更新fdb表来检查是否数据包源mac与本地mac冲突,然后查找fdb表,如果数据包目的mac在fdb表中并且是本地mac,就交由本地处理(如上交到IP层),如果不存在则需要从这个br下面的port转发,如果刚才在fdb表中找到了目的mac地址,就转发到指定port,如果找不到就广播到所有port。
如果需要交往IP层,br_pass_frame_up()函数就会在BR_LOCAL_IN这个钩子上通过netif_receive_skb进入ip_rcv。
进入br_handle_frame()时,skb->dev还是eth0.1,skb->dev->br_port->dev->name为eth0.1,skb->dev->br_port->br->dev->name为br0。在br_pass_frame_up()中,会先把skb->dev变为br设备,即br0,然后交给NF_BR_LOCAL_IN钩子处理,最后调用netif_receive_skb交给L3层处理。其中将skb->dev变为br0以及进入NF_BR_LOCAL_IN的钩子函数br_nf_local_in这两个操作都是为了防止数据包到netif_receive_skb之后再次成功调用handle_bridge进入br的netfilter,这样就死循环了,由于内核中禁止br设备继续桥接,因此br设备的br_port就是空的,handle_bridge就会直接返回,br_nf_local_in函数通过skb_dst_drop()将数据包的dst_entry清除也是这个目的。
从ip_rcv函数的dump_stack也可以看出来:
=>=>=>[ip_rcv]
Call Trace:
[<801d840c>] dump_stack+0x8/0x34
[<80154f90>] ip_rcv+0x30/0x36c
[<801cab90>] br_handle_frame_finish+0x144/0x1a0
[<801cae2c>] br_handle_frame+0x240/0x274
[<80133568>] netif_receive_skb+0x2fc/0x464
[<80133780>] process_backlog+0xb0/0x110
[<80133ef0>] net_rx_action+0x9c/0x1bc
所以以太网收包过程:
gmac初始化函数中注册收包硬中断,中断上半部把包放到ring buffer中,下半部使用tasklet机制收包,处理函数对应athr_receive_pkt,它从ring buffer中获取一个包,并通过netif_receive_pkt来收包。
无线驱动中,是tasklet获得一个包后,把包放到CPU的收包队列softnet_data(通过netif_rx)中,交给NET_RX_SOFTIRQ软中断,软中断从收包队列中拿包,交给协议栈处理。
ip协议栈走完之后,会进入dev_queue_xmit()发包,从上面可以看到,netfilter之后发包设备已经指定好了(skb->dev_name)。dev_queue_xmit()的调用着主要有三个路径:
- 本地发送数据包,经过邻居协议后调用dev_queue_xmit();
- 经过ip层转发的包,经过邻居协议后调用dev_queue_xmit();
- 直接通过br转发的包,经过邻居协议后调用dev_queue_xmit();
上面三种调用都是通过arp协议的hook函数调用的。
dev_queue_xmit()的实现中,如果dev有队列,就会进行enqueue和通过qdisc_run来发包。如果dev没有队列(如环回设备和tunnels等),则直接调用dev_hard_start_xmit来发包。
struct netdev_queue *txq;
struct Qdisc *q;
struct net_device *dev = skb->dev;
txq = dev_pick_tx(dev, skb);
q = rcu_dereference(txq->qdisc);
qdisc_run(q);
qdisc_run()是一个while循环,通过dev_hard_start_xmit()发送一个包,然后查看设备队列中是否还有没发送的包,如果有就使用__netif_reschedule(q)重新加入调度队列(如果已经被调度了,就不做事情),__netif_reschedule()主要的逻辑代码为:
local_irq_save(flags); //关中断
struct softnet_data *sd;
sd = &__get_cpu_var(softnet_data); //每个CPU都有一个softnet_data结构
q->next_sched = sd->output_queue;
sd->output_queue = q;
raise_softirq_irqoff(NET_TX_SOFTIRQ); //触发发包软中断
local_irq_restore(flags);
net_tx_action()是软中断NET_TX_SOFTIRQ的处理函数,因为进入这个软中断的设备就肯定是有队列的,所以net_tx_action()使用qdisc_run(q)发包,最终还是dev_hard_start_xmit来发包。
在net_tx_action()中主要做了两件事:
- 如果sd->completion_queue不为空,释放sd->completion_queue队列中的数据包,因为这些包已经发完了;
- 如果sd->output_queue不为空,发送sd->output_queue队列中的数据包。
dev_hard_start_xmit()函数调用设备发包函数:dev->netdev_ops->ndo_start_xmit(skb, dev),如果发送成功,就更新设备中skb所在发包队列(即上面的dev_pick_tx())的最近发包时间。
每个CPU都有一个softnet_data结构,这个结构存储着发包队列、收包队列等,那是不是也就是说一个CPU只有一个收包队列,所有设备都使用这一个收包队列?
在dev_hard_start_xmit的开始加打印查看ops->ndo_start_xmit指针,发现有两个指向:br_dev_xmit和vlan_dev_hard_start_xmit,说明可能的发包函数调用顺序为:
dev_hard_start_xmit -> br_dev_xmit -> dev_hard_start_xmit -> vlan_dev_hard_start_xmit
打印信息如下:
=>=>=>[dev_hard_start_xmit], start xmit = 801c8cc0
=>=>=>[dev_hard_start_xmit], start xmit = 801d07ec
=>=>=>[vlan_dev_hard_start_xmit], vlan dev is eth0.1, real dev is eth0
=>=>=>[dev_hard_start_xmit], start xmit = 87b83e88
可以看到vlan_dev_hard_start_xmit中dev的name为eth0.1,dev的private中保存的设备为eth0,所以,最后一行打印应该是调用的eth0的发包函数,但是在System.map中找不到,因为以太网驱动被编译成ko了。从/proc/kallsyms文件可以看到所有的函数地址,所以在这个函数里找到地址为0x87b83e88的函数为xxxx_gmac_hard_start,确实是gmac的发包函数(即eth0的发包函数)。
所以,比较完整的发包函数调用顺序为:
dev_hard_start_xmit -> br_dev_xmit -> dev_hard_start_xmit -> vlan_dev_hard_start_xmit -> dev_hard_start_xmit -> xxxx_gmac_hard_start
如果是发往WAN口的,就没有br_dev_xmit这一步了,之所以发往LAN口的会先到br0,我猜是因为路由表中这样写的。
从路由器里向外发包,如ping或tftp,也会走上面一样的路径。
发包时还要关注下面的函数,我们的好多包是放在fifo里的:
pfifo_fast_enqueue(), pfifo_fast_dequeue()
skb_queue_tail()
qdisc_enqueue_tail()
dev_queue_xmit()
vlan设备:
br_dev_xmit() -> br_deliver() -> vlan_dev_hard_start_xmit()
vlan设备的真实设备存放在vlan_dev_info(dev)->real_dev中,这是在dev的private字段。
vlan_dev_hard_start_xmit()的最后,获得真实设备赋值给skb,然后调用dev_queue_xmit(skb)来调用真实设备的发包函数。
vlan_dev_hard_header()
deliver_skb()一直没人调用,看代码貌似和下面两个变量有关,我猜可能是socket收发包时会调用:
static struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
static struct list_head ptype_all __read_mostly;
确实是这样,比如SOCK_PACKET创建的socket的内核收包函数就在packet_create()中注册为packet_rcv_spkt(),而socket收包时deliver_skb()中打印func就是packet_rcv_spkt。而SOCK_PACKET, SOCK_STREAM和SOCK_DGRAM的proto_ops列表分别对应socket.c中注册的结构体:packet_ops, stream_ops和msg_ops。
软中断到底有什么好处 — 可以延迟执行,而普通的函数调用会立即执行。