arp
上节看到了数据包到达IP层后,进入链路层。需要获取这个数据包的MAC地址。这个地址有邻居系统arp协议提供。
arp机制
Linux内核中的arp高速缓存是通过:rtable + dst_entry + hh_cache 实现的。
arp协议的功能是为了维护 IP ----> MAC 的映射关系。
rarp协议的功能为了查询 MAC ----> IP的映射。
arp的请求应答都是广播。
rarp的请求是广播,应该是单播,由rarp服务器响应。
先看一下arp的初始化:
void __init arp_init(void) { neigh_table_init(&arp_tbl); dev_add_pack(&arp_packet_type); arp_proc_init(); #ifdef CONFIG_SYSCTL neigh_sysctl_register(NULL, &arp_tbl.parms, NET_IPV4, NET_IPV4_NEIGH, "ipv4", NULL); #endif register_netdevice_notifier(&arp_netdev_notifier); }
arp_tbl是一个类型为neigh_table的全局指针,也称为邻居表。
struct neigh_table { struct neigh_table *next; int family; // 一个邻居表项的实际大小,因为neighbour的尾端是一个primary_key[0] int entry_size; int key_len; // 一个neighbour的hash值计算函数 __u32 (*hash)(const void *pkey, const struct net_device *); int (*constructor)(struct neighbour *); int (*pconstructor)(struct pneigh_entry *); void (*pdestructor)(struct pneigh_entry *); void (*proxy_redo)(struct sk_buff *skb); char *id; // 这是一个链表,系统每个网络接口的设备对应parms的一个节点,表示该设备对应的邻居的一些参数, // 比如该接口的arp刷新频率,允许不同的接口有不同的频率。 struct neigh_parms parms; int gc_interval; int gc_thresh1; int gc_thresh2; int gc_thresh3; unsigned long last_flush; struct timer_list gc_timer; struct timer_list proxy_timer; struct sk_buff_head proxy_queue; // 邻居的总数 atomic_t entries; rwlock_t lock; unsigned long last_rand; struct neigh_parms *parms_list; kmem_cache_t *kmem_cachep; struct neigh_statistics *stats; // hash表 struct neighbour **hash_buckets; unsigned int hash_mask; __u32 hash_rnd; unsigned int hash_chain_gc; struct pneigh_entry **phash_buckets; #ifdef CONFIG_PROC_FS struct proc_dir_entry *pde; #endif };
neigh_table,neighbour,net_device之间的关系如下
arp响应报文的处理是在arp_process():
static int arp_process(struct sk_buff *skb) { struct net_device *dev = skb->dev; struct in_device *in_dev = in_dev_get(dev); unsigned char *arp_ptr; unsigned char *sha, *tha; u32 sip, tip; u16 dev_type = dev->type; int addr_type; struct neighbour *n; arp = skb->nh.arph; n = __neigh_lookup(&arp_tbl, &sip, dev, 0); neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0); }
注意:在neigh_resolve_output中会先在neigb_table中插入一条neighbour表项,然后发送arp请求。
所以,__neigh_lookup会返回一个没有被arp更新的表项,然后调用neigh_update更新。
设备驱动层
在IP层把arp的协助下把mac地址填上去后,就走完了最后一层协议。
在ip_finish_output2中判断dst->neighbour->hh非空,直接把dst_entry->hh->hh_data拷贝成以太网的首部,hh->output发送出去了。
hh->output实际指向的是dev_queue_xmit,这个函数是设备无关的,IP层或其他底层需要发送报文,都要调用这个函数。
int dev_queue_xmit(struct sk_buff *skb) { struct net_device *dev = skb->dev; struct Qdisc *q; int rc = -ENOMEM; // 关闭中断,并且停止抢占 local_bh_disable(); // 流量控制(也是需要重点研究的对象) q = rcu_dereference(dev->qdisc); if (q->enqueue) { spin_lock(&dev->queue_lock); rc = q->enqueue(skb, q); qdisc_run(dev); spin_unlock(&dev->queue_lock); rc = rc == NET_XMIT_BYPASS ? NET_XMIT_SUCCESS : rc; goto out; } // loopback等 soft devices直接通过hard_start_xmit发送 if (dev->flags & IFF_UP) { int cpu = smp_processor_id(); /* ok because BHs are off */ if (dev->xmit_lock_owner != cpu) { HARD_TX_LOCK(dev, cpu); if (!netif_queue_stopped(dev)) { if (netdev_nit) dev_queue_xmit_nit(skb, dev); rc = 0; if (!dev->hard_start_xmit(skb, dev)) { HARD_TX_UNLOCK(dev); goto out; } } HARD_TX_UNLOCK(dev); if (net_ratelimit()) printk(KERN_CRIT "Virtual device %s asks to " "queue packet!\n", dev->name); } else { if (net_ratelimit()) printk(KERN_CRIT "Dead loop on virtual device " "%s, fix it urgently!\n", dev->name); } } local_bh_enable(); }
looback的hard_start_xmit指向了loopback_xmit,下面进入loopback_xmit函数
static int loopback_xmit(struct sk_buff *skb, struct net_device *dev) { struct net_device_stats *lb_stats; skb_orphan(skb); skb->protocol=eth_type_trans(skb,dev); skb->dev=dev; if (skb_shinfo(skb)->tso_size) { BUG_ON(skb->protocol != htons(ETH_P_IP)); BUG_ON(skb->nh.iph->protocol != IPPROTO_TCP); emulate_large_send_offload(skb); return 0; } dev->last_rx = jiffies; lb_stats = &per_cpu(loopback_stats, get_cpu()); lb_stats->rx_bytes += skb->len; lb_stats->tx_bytes += skb->len; lb_stats->rx_packets++; lb_stats->tx_packets++; put_cpu(); netif_rx(skb); return(0); }
对于loopback设备,在它的hard_start_xmit函数中,直接把skb通过netif_rx接收过去了。
从中断到网络层
终于到了收数据包的流程了。
报文接收路径:
PKT Arrive INT --> Driver --> 0) alloc_skb; 1) netif_rx --> RX_SOFTIRQ --> net_rx_action软中断处理函数 (dev->poll) --> process_backlog --> netif_receive_skb ![4_18](https://img.alicdn.com/L1/461/1/62df474ba6bdd22f47c8a5dc97784504aba815a7.png) 注意软中断产生的位置
硬中断 netif_rx
在硬件中断中分配完skb之后,就要把这个skb挂载在一个queue上,然后mark软中断。
对于有backlog_dev的设备来说,netif_rx发生在硬件中断中,这个函数的任务是把新接收进来的skb挂载到queue->input_pkt_queue中,然后触发软中断。
这个函数再终端服务程序中调用。
int netif_rx(struct sk_buff *skb) { int this_cpu; struct softnet_data *queue; unsigned long flags; if (!skb->stamp.tv_sec) net_timestamp(&skb->stamp); local_irq_save(flags); this_cpu = smp_processor_id(); queue = &__get_cpu_var(softnet_data); __get_cpu_var(netdev_rx_stat).total++; // 队列的长度还没有超长,注意此处是一个丢包点 if (queue->input_pkt_queue.qlen <= netdev_max_backlog) { // 下面这几行代码很精妙 // queue->input_pkt_queue.qlen非0,说明接收队列input_pkt_queue还有数据包没有被及时处理,那么,本次接收进来的数据包,就没有必要触发软中断,只需要把这个数据包加入到队列中即可。 if (queue->input_pkt_queue.qlen) { // 如果发送队列非空,并且此时触发了throttle阀值,则丢弃当前这个包 if (queue->throttle) goto drop; enqueue: // 无需触发软中断,直接加入队列,并且返回。 dev_hold(skb->dev); __skb_queue_tail(&queue->input_pkt_queue, skb); local_irq_restore(flags); return queue->cng_level; } if (queue->throttle) queue->throttle = 0; // 如果队列已经空了,则触发一次软中断,然后跳转到enqueue,very good!!! netif_rx_schedule(&queue->backlog_dev); goto enqueue; } drop: // 在ISR中丢包的过程超级简单,直接不予理睬,把dropped更新即可。 __get_cpu_var(netdev_rx_stat).dropped++; local_irq_restore(flags); kfree_skb(skb); return NET_RX_DROP; }
这个函数执行完,skb就进入了内核,接下来等待软中断的处理。
软中断 net_rx_action
static void net_rx_action(struct softirq_action *h) { // softnet_data是数据包的停靠的地方,这个结构每个CPU都会有一个。 struct softnet_data *queue = &__get_cpu_var(softnet_data); unsigned long start_time = jiffies; int budget = netdev_max_backlog; local_irq_disable(); // 遍历poll_list while (!list_empty(&queue->poll_list)) { struct net_device *dev; if (budget <= 0 || jiffies - start_time > 1) goto softnet_break; local_irq_enable(); dev = list_entry(queue->poll_list.next, struct net_device, poll_list); // dev->poll,对于backlog_dev的poll是process_backlog // 可以看出:软中断的服务程序中仅仅对softnet_data中的poll_list依次调用dev->poll方法 // 对于有数据包要处理的net_device调用其poll()方法,至于如何处理这个net_device上数据包,则有具体的net_device决定 // 这样比较灵活。 if (dev->quota <= 0 || dev->poll(dev, &budget)) { local_irq_disable(); list_del(&dev->poll_list); list_add_tail(&dev->poll_list, &queue->poll_list); if (dev->quota < 0) dev->quota += dev->weight; else dev->quota = dev->weight; } else { dev_put(dev); local_irq_disable(); } } out: local_irq_enable(); return; softnet_break: __get_cpu_var(netdev_rx_stat).time_squeeze++; __raise_softirq_irqoff(NET_RX_SOFTIRQ); goto out; }
在ping本地的时候,looback_xmit中会通过netif_rx(skb)直接把这个skb插入到softnet_data中,并标记软中断。此时的skb的dev是dev_backlog而它的poll函数是process_backlog
最终会进入netif_receive_skb。
net_rx_action() --> process_backlog --> netif_receive_skb()
先看process_backlog
static int process_backlog(struct net_device *backlog_dev, int *budget) { int work = 0; int quota = min(backlog_dev->quota, *budget); struct softnet_data *queue = &__get_cpu_var(softnet_data); unsigned long start_time = jiffies; // 在softnet_dat中的backlog_dev的poll函数中,会处理这个设备上所有的数据包。 for (;;) { struct sk_buff *skb; struct net_device *dev; local_irq_disable(); // 取出skb skb = __skb_dequeue(&queue->input_pkt_queue); if (!skb) goto job_done; local_irq_enable(); dev = skb->dev; // 开始解析数据包 netif_receive_skb(skb); dev_put(dev); work++; if (work >= quota || jiffies - start_time > 1) break; } backlog_dev->quota -= work; *budget -= work; return -1; job_done: backlog_dev->quota -= work; *budget -= work; list_del(&backlog_dev->poll_list); smp_mb__before_clear_bit(); netif_poll_enable(backlog_dev); if (queue->throttle) queue->throttle = 0; local_irq_enable(); return 0; }
netif_receive_skb开始解析数据包
int netif_receive_skb(struct sk_buff *skb) { struct packet_type *ptype, *pt_prev; int ret = NET_RX_DROP; unsigned short type; if (!skb->stamp.tv_sec) net_timestamp(&skb->stamp); skb_bond(skb); __get_cpu_var(netdev_rx_stat).total++; skb->h.raw = skb->nh.raw = skb->data; skb->mac_len = skb->nh.raw - skb->mac.raw; pt_prev = NULL; rcu_read_lock(); // 遍历 ptype_all,把数据包通过deliver_skb提交给上层 // tcpdump就是通过创建af_packet套接字,向ptype_all注册一个packet_type的处理函数实现把数据包递交用户层 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); pt_prev = ptype; } } handle_diverter(skb); // Linux网桥的入口 if (handle_bridge(&skb, &pt_prev, &ret)) goto out; type = skb->protocol; // 通过type取得ptype_base, // 判断type是否一致 // 目前ptype_base有:arp_recv, rarp_recv, ip_recv list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) { if (ptype->type == type && (!ptype->dev || ptype->dev == skb->dev)) { if (pt_prev) ret = deliver_skb(skb, pt_prev); pt_prev = ptype; } } if (pt_prev) { ret = pt_prev->func(skb, skb->dev, pt_prev); } else { kfree_skb(skb); ret = NET_RX_DROP; } out: rcu_read_unlock(); return ret; }
这个函数的目标是数据包级别的处理。根据数据帧中的type,提交给对应的处理函数。
对于IP报文pt_prev->func为ip_rcv,然后进入ip_rcv_finish
static inline int ip_rcv_finish(struct sk_buff *skb) { struct net_device *dev = skb->dev; struct iphdr *iph = skb->nh.iph; // 如果skb->dst为空,说明这个数据包的目的地址还没设置 // ip_route_input在路由系统中找到数据包的路由项,以此来确认如何处理这个数据包,是转发还是本地分发 if (skb->dst == NULL) { if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev)) goto drop; } if (iph->ihl > 5) { struct ip_options *opt; if (skb_cow(skb, skb_headroom(skb))) { IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS); goto drop; } iph = skb->nh.iph; if (ip_options_compile(NULL, skb)) goto inhdr_error; opt = &(IPCB(skb)->opt); if (opt->srr) { struct in_device *in_dev = in_dev_get(dev); if (in_dev) { if (!IN_DEV_SOURCE_ROUTE(in_dev)) { if (IN_DEV_LOG_MARTIANS(in_dev) && net_ratelimit()) printk(KERN_INFO "source route option %u.%u.%u.%u -> %u.%u.%u.%u\n", NIPQUAD(iph->saddr), NIPQUAD(iph->daddr)); in_dev_put(in_dev); goto drop; } in_dev_put(in_dev); } if (ip_options_rcv_srr(skb)) goto drop; } } return dst_input(skb); inhdr_error: IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS); drop: kfree_skb(skb); return NET_RX_DROP; }
在ip_route_input中,会找到一个dst_entry,并设置input的函数指针是ip_local_deliver
int ip_local_deliver(struct sk_buff *skb) { if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) { skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER); if (!skb) return 0; } return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish); } static inline int ip_local_deliver_finish(struct sk_buff *skb) { int ihl = skb->nh.iph->ihl*4; __skb_pull(skb, ihl); nf_reset(skb); skb->h.raw = skb->data; rcu_read_lock(); { int protocol = skb->nh.iph->protocol; int hash; struct sock *raw_sk; struct net_protocol *ipprot; resubmit: hash = protocol & (MAX_INET_PROTOS - 1); raw_sk = sk_head(&raw_v4_htable[hash]); // 对每个报文都要先检查是否是raw报文。 if (raw_sk) raw_v4_input(skb, skb->nh.iph, hash); // skb->nh.iph->protocol根据IP报文头部,直接找到相应的协议! // 对应的handler分别为:tcp_v4_rcv,icmp_rcv,udp_rcv,igmp_rcv if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) { int ret; if (!ipprot->no_policy && !xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { kfree_skb(skb); goto out; } ret = ipprot->handler(skb); if (ret < 0) { protocol = -ret; goto resubmit; } IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS); } else { if (!raw_sk) { if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { IP_INC_STATS_BH(IPSTATS_MIB_INUNKNOWNPROTOS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0); } } else IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS); kfree_skb(skb); } } out: rcu_read_unlock(); return 0; }
总结一下;一个skb从软中断上来的时候,必须根据mac头部的type从ptype_base[]中找到相应的处理函数,目前内核中有3个和IP相关ip_packet_type,arp_packet_type,rarp_packet_type。
如果是IP报文,在ip_local_deliver_finish时,从inet_protos[]找到对应的函数:icmp, udp, tcp。
前者的回调是func(),处理的是packet;
后者的回调是handler(),处理的是协议。
到目前为止skb都是在内核空间中的,并没有和哪个套接字关联起来。下面的函数通过protocol, saddr, daddr, ifindex查找这个数据包所属于的socket,并把skb递交给sock上的skb_receive_queue。
void raw_v4_input(struct sk_buff *skb, struct iphdr *iph, int hash) { struct sock *sk; struct hlist_head *head; read_lock(&raw_v4_lock); head = &raw_v4_htable[hash]; if (hlist_empty(head)) goto out; // 通过protocol, saddr, daddr, ifindex查找这个数据包所属于的socket !!! sk = __raw_v4_lookup(__sk_head(head), iph->protocol, iph->saddr, iph->daddr, skb->dev->ifindex); while (sk) { if (iph->protocol != IPPROTO_ICMP || !icmp_filter(sk, skb)) { struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC); if (clone) raw_rcv(sk, clone); } sk = __raw_v4_lookup(sk_next(sk), iph->protocol, iph->saddr, iph->daddr, skb->dev->ifindex); } out: read_unlock(&raw_v4_lock); }