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