Skip to contentSkip to conversation

Lockless neighbour table

PATCH In review Stephen Hemminger 3 participants 19 messages Aug 28, 2006 – Aug 30, 2006

Subsystems (6)
lore
Share & tools
lore
#0 lore raw

cover Lockless neighbour table

This is an updated version of the changes to make the neighbour (ARP) table lockless for normal accesses. It uses RCU and seqlock in several places. I separated the hlist conversions from RCU changes (in case we need to bisect for some reason).

--
Stephen Hemminger <[email protected]>

#1 lore raw

PATCH 1/6 net neighbor: convert top level list to RCU

Change the top level list of neighbor tables to use RCU. Minor change to BUG() if a neighbor table is registered twice.

---
 include/net/neighbour.h |    2 -
 net/core/neighbour.c    |   89 ++++++++++++++++++++++--------------------------
 2 files changed, 42 insertions(+), 49 deletions(-)
net/core/neighbour.c +41 -48
@@ -61,7 +61,7 @@ static void neigh_app_notify(struct neig
 static int pneigh_ifdown(struct neigh_table *tbl, struct net_device *dev);
 void neigh_changeaddr(struct neigh_table *tbl, struct net_device *dev);
 
-static struct neigh_table *neigh_tables;
+static LIST_HEAD(neigh_tables);
 #ifdef CONFIG_PROC_FS
 static struct file_operations neigh_stat_seq_fops;
 #endif
@@ -93,11 +93,11 @@ static struct file_operations neigh_stat
    It is supposed, that dev->hard_header is simplistic and does
    not make callbacks to neighbour tables.
 
-   The last lock is neigh_tbl_lock. It is pure SMP lock, protecting
-   list of neighbour tables. This list is used only in process context,
+   The last lock is neigh_tbl_lock. It controls update to the
+   set of neighbor tables. Typically used during initialization
  */
 
-static DEFINE_RWLOCK(neigh_tbl_lock);
+static DEFINE_SPINLOCK(neigh_tbl_lock);
 
 static int neigh_blackhole(struct sk_buff *skb)
 {
@@ -1387,26 +1387,18 @@ void neigh_table_init(struct neigh_table
 	struct neigh_table *tmp;
 
 	neigh_table_init_no_netlink(tbl);
-	write_lock(&neigh_tbl_lock);
-	for (tmp = neigh_tables; tmp; tmp = tmp->next) {
-		if (tmp->family == tbl->family)
-			break;
-	}
-	tbl->next	= neigh_tables;
-	neigh_tables	= tbl;
-	write_unlock(&neigh_tbl_lock);
-
-	if (unlikely(tmp)) {
-		printk(KERN_ERR "NEIGH: Registering multiple tables for "
-		       "family %d\n", tbl->family);
-		dump_stack();
+
+	spin_lock(&neigh_tbl_lock);
+	list_for_each_entry(tmp, &neigh_tables, list) {
+		BUG_ON(tmp->family == tbl->family);
 	}
+
+	list_add_rcu(&tbl->list, &neigh_tables);
+	spin_unlock(&neigh_tbl_lock);
 }
 
 int neigh_table_clear(struct neigh_table *tbl)
 {
-	struct neigh_table **tp;
-
 	/* It is not clean... Fix it to unload IPv6 module safely */
 	del_timer_sync(&tbl->gc_timer);
 	del_timer_sync(&tbl->proxy_timer);
@@ -1414,14 +1406,12 @@ int neigh_table_clear(struct neigh_table
 	neigh_ifdown(tbl, NULL);
 	if (atomic_read(&tbl->entries))
 		printk(KERN_CRIT "neighbour leakage\n");
-	write_lock(&neigh_tbl_lock);
-	for (tp = &neigh_tables; *tp; tp = &(*tp)->next) {
-		if (*tp == tbl) {
-			*tp = tbl->next;
-			break;
-		}
-	}
-	write_unlock(&neigh_tbl_lock);
+
+	spin_lock(&neigh_tbl_lock);
+	list_del_rcu(&tbl->list);
+	spin_unlock(&neigh_tbl_lock);
+
+	synchronize_rcu();
 
 	neigh_hash_free(tbl->hash_buckets, tbl->hash_mask + 1);
 	tbl->hash_buckets = NULL;
@@ -1456,13 +1446,12 @@ int neigh_delete(struct sk_buff *skb, st
 		}
 	}
 
-	read_lock(&neigh_tbl_lock);
-	for (tbl = neigh_tables; tbl; tbl = tbl->next) {
+	rcu_read_lock();
+	list_for_each_entry_rcu(tbl, &neigh_tables, list) {
 		struct neighbour *neigh;
 
 		if (tbl->family != ndm->ndm_family)
 			continue;
-		read_unlock(&neigh_tbl_lock);
 
 		if (nla_len(dst_attr) < tbl->key_len)
 			goto out_dev_put;
@@ -1487,12 +1476,12 @@ int neigh_delete(struct sk_buff *skb, st
 		neigh_release(neigh);
 		goto out_dev_put;
 	}
-	read_unlock(&neigh_tbl_lock);
 	err = -EAFNOSUPPORT;
 
 out_dev_put:
 	if (dev)
 		dev_put(dev);
+	rcu_read_unlock();
 out:
 	return err;
 }
@@ -1525,15 +1514,14 @@ int neigh_add(struct sk_buff *skb, struc
 			goto out_dev_put;
 	}
 
-	read_lock(&neigh_tbl_lock);
-	for (tbl = neigh_tables; tbl; tbl = tbl->next) {
+	rcu_read_lock();
+	list_for_each_entry_rcu(tbl, &neigh_tables, list) {
 		int flags = NEIGH_UPDATE_F_ADMIN | NEIGH_UPDATE_F_OVERRIDE;
 		struct neighbour *neigh;
 		void *dst, *lladdr;
 
 		if (tbl->family != ndm->ndm_family)
 			continue;
-		read_unlock(&neigh_tbl_lock);
 
 		if (nla_len(tb[NDA_DST]) < tbl->key_len)
 			goto out_dev_put;
@@ -1578,12 +1566,12 @@ int neigh_add(struct sk_buff *skb, struc
 		goto out_dev_put;
 	}
 
-	read_unlock(&neigh_tbl_lock);
 	err = -EAFNOSUPPORT;
 
 out_dev_put:
 	if (dev)
 		dev_put(dev);
+	rcu_read_unlock();
 out:
 	return err;
 }
@@ -1788,8 +1776,8 @@ int neightbl_set(struct sk_buff *skb, st
 	}
 
 	ndtmsg = nlmsg_data(nlh);
-	read_lock(&neigh_tbl_lock);
-	for (tbl = neigh_tables; tbl; tbl = tbl->next) {
+	rcu_read_lock();
+	list_for_each_entry_rcu(tbl, &neigh_tables, list) {
 		if (ndtmsg->ndtm_family && tbl->family != ndtmsg->ndtm_family)
 			continue;
 
@@ -1889,26 +1877,26 @@ int neightbl_set(struct sk_buff *skb, st
 errout_tbl_lock:
 	write_unlock_bh(&tbl->lock);
 errout_locked:
-	read_unlock(&neigh_tbl_lock);
+	rcu_read_unlock();
 errout:
 	return err;
 }
 
 int neightbl_dump_info(struct sk_buff *skb, struct netlink_callback *cb)
 {
-	int family, tidx, nidx = 0;
+	int family, tidx = 0, nidx = 0;
 	int tbl_skip = cb->args[0];
 	int neigh_skip = cb->args[1];
 	struct neigh_table *tbl;
 
 	family = ((struct rtgenmsg *) nlmsg_data(cb->nlh))->rtgen_family;
 
-	read_lock(&neigh_tbl_lock);
-	for (tbl = neigh_tables, tidx = 0; tbl; tbl = tbl->next, tidx++) {
+	rcu_read_lock();
+	list_for_each_entry_rcu(tbl, &neigh_tables, list) {
 		struct neigh_parms *p;
 
 		if (tidx < tbl_skip || (family && tbl->family != family))
-			continue;
+			goto next_tbl;
 
 		if (neightbl_fill_info(skb, tbl, NETLINK_CB(cb->skb).pid,
 				       cb->nlh->nlmsg_seq, RTM_NEWNEIGHTBL,
@@ -1928,9 +1916,11 @@ int neightbl_dump_info(struct sk_buff *s
 		}
 
 		neigh_skip = 0;
+next_tbl:
+		++tidx;
 	}
 out:
-	read_unlock(&neigh_tbl_lock);
+	rcu_read_unlock();
 	cb->args[0] = tidx;
 	cb->args[1] = nidx;
 
@@ -2020,22 +2010,25 @@ out:
 int neigh_dump_info(struct sk_buff *skb, struct netlink_callback *cb)
 {
 	struct neigh_table *tbl;
-	int t, family, s_t;
+	int t = 0, family, s_t;
+
 
-	read_lock(&neigh_tbl_lock);
 	family = ((struct rtgenmsg *) nlmsg_data(cb->nlh))->rtgen_family;
 	s_t = cb->args[0];
 
-	for (tbl = neigh_tables, t = 0; tbl; tbl = tbl->next, t++) {
+	rcu_read_lock();
+	list_for_each_entry_rcu(tbl, &neigh_tables, list) {
 		if (t < s_t || (family && tbl->family != family))
-			continue;
+			goto next;
 		if (t > s_t)
 			memset(&cb->args[1], 0, sizeof(cb->args) -
 						sizeof(cb->args[0]));
 		if (neigh_dump_table(tbl, skb, cb) < 0)
 			break;
+next:
+		++t;
 	}
-	read_unlock(&neigh_tbl_lock);
+	rcu_read_unlock();
 
 	cb->args[0] = t;
 	return skb->len;
include/net/neighbour.h +1 -1
@@ -136,7 +136,7 @@ struct pneigh_entry
 
 struct neigh_table
 {
-	struct neigh_table	*next;
+	struct list_head	list;
 	int			family;
 	int			entry_size;
 	int			key_len;

--
Stephen Hemminger <[email protected]>

#2 lore raw

PATCH 2/6 neighbour: convert neighbour hash table to hlist

In reply to Stephen Hemminger

Change the neighbour table hash list to hlist from list.h to allow for easier later conversion to RCU.

---
 include/net/neighbour.h |    6 -
 net/core/neighbour.c    |  160 +++++++++++++++++++++++++-----------------------
 2 files changed, 88 insertions(+), 78 deletions(-)
include/net/neighbour.h +3 -3
@@ -88,10 +88,10 @@ struct neigh_statistics
 
 struct neighbour
 {
-	struct neighbour	*next;
+	struct hlist_node	hlist;
 	struct neigh_table	*tbl;
 	struct neigh_parms	*parms;
-	struct net_device		*dev;
+	struct net_device	*dev;
 	unsigned long		used;
 	unsigned long		confirmed;
 	unsigned long		updated;
@@ -161,7 +161,7 @@ struct neigh_table
 	unsigned long		last_rand;
 	kmem_cache_t		*kmem_cachep;
 	struct neigh_statistics	*stats;
-	struct neighbour	**hash_buckets;
+	struct hlist_head	*hash_buckets;
 	unsigned int		hash_mask;
 	__u32			hash_rnd;
 	unsigned int		hash_chain_gc;
net/core/neighbour.c +85 -75

380 lines not on this page · raw message

--
Stephen Hemminger <[email protected]>

#3 lore raw

PATCH 3/6 neighbour: convert pneigh hash table to hlist

In reply to Stephen Hemminger

Change the pneigh_entry table to hlist from list.h to allow for easier later conversion to RCU.

---
 include/net/neighbour.h |    6 ++--
 net/core/neighbour.c    |   58 ++++++++++++++++++++++++------------------------
 2 files changed, 33 insertions(+), 31 deletions(-)
include/net/neighbour.h +3 -3
@@ -124,8 +124,8 @@ struct neigh_ops
 
 struct pneigh_entry
 {
-	struct pneigh_entry	*next;
-	struct net_device		*dev;
+	struct hlist_node	hlist;
+	struct net_device	*dev;
 	u8			key[0];
 };
 
@@ -165,7 +165,7 @@ struct neigh_table
 	unsigned int		hash_mask;
 	__u32			hash_rnd;
 	unsigned int		hash_chain_gc;
-	struct pneigh_entry	**phash_buckets;
+	struct hlist_head	*phash_buckets;
 #ifdef CONFIG_PROC_FS
 	struct proc_dir_entry	*pde;
 #endif
net/core/neighbour.c +30 -28

143 lines not on this page · raw message

--
Stephen Hemminger <[email protected]>

#4 lore raw

PATCH 4/6 net neighbour: convert to RCU

In reply to Stephen Hemminger

Use RCU to allow for lock less access to the neighbour table. This should speedup the send path because no atomic operations will be needed to lookup ARP entries, etc.

---
 include/net/neighbour.h |    4 -
 net/core/neighbour.c    |  158 +++++++++++++++++++++++++-----------------------
 2 files changed, 87 insertions(+), 75 deletions(-)
include/net/neighbour.h +3 -1
@@ -108,6 +108,7 @@ struct neighbour
 	struct sk_buff_head	arp_queue;
 	struct timer_list	timer;
 	struct neigh_ops	*ops;
+	struct rcu_head		rcu;
 	u8			primary_key[0];
 };
 
@@ -126,6 +127,7 @@ struct pneigh_entry
 {
 	struct hlist_node	hlist;
 	struct net_device	*dev;
+	struct rcu_head		rcu;
 	u8			key[0];
 };
 
@@ -157,7 +159,7 @@ struct neigh_table
 	struct timer_list 	proxy_timer;
 	struct sk_buff_head	proxy_queue;
 	atomic_t		entries;
-	rwlock_t		lock;
+	spinlock_t		lock;
 	unsigned long		last_rand;
 	kmem_cache_t		*kmem_cachep;
 	struct neigh_statistics	*stats;
net/core/neighbour.c +84 -74

472 lines not on this page · raw message

--
Stephen Hemminger <[email protected]>

#5 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Hello!

net/core/neighbour.c struct neighbour *neigh_lookup(struct ne · line 361

Quoted 11 lines
net/core/neighbour.c +2 -2
@@ -346,8 +354,8 @@ struct neighbour *neigh_lookup(struct ne
 
 	NEIGH_CACHE_STAT_INC(tbl, lookups);
 
-	read_lock_bh(&tbl->lock);
-	hlist_for_each_entry(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
 		if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
 			neigh_hold(n);
 			NEIGH_CACHE_STAT_INC(tbl, hits);

Sure? Seems, you cannot grab refcnt here, the entry can be already released.

Probably, you should do atomic_inc_and_test() here and restart lookup, if it fails.

Alexey

#6 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Quoted 17 lines · On Tue, 29 Aug 2006 19:28:16 +0400 Alexey Kuznetsov <[email protected]>…

Hello!

net/core/neighbour.c struct neighbour *neigh_lookup(struct ne · line 361

Quoted 11 lines
net/core/neighbour.c +2 -2
@@ -346,8 +354,8 @@ struct neighbour *neigh_lookup(struct ne
 
 	NEIGH_CACHE_STAT_INC(tbl, lookups);
 
-	read_lock_bh(&tbl->lock);
-	hlist_for_each_entry(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
 		if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
 			neigh_hold(n);
 			NEIGH_CACHE_STAT_INC(tbl, hits);

Sure? Seems, you cannot grab refcnt here, the entry can be already released.

Quoted 4 lines

Probably, you should do atomic_inc_and_test() here and restart lookup, if it fails.

Alexey

atomic_inc_and_test is true iff result is zero, so that won't work. But the following should work:

	hlist_for_each_entry_rcu(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
		if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
			if (unlikely(&atomic_inc_return(&n->refcnt) == 1)) {
				neigh_release(n);
				continue;
			}

NEIGH_CACHE_STAT_INC(tbl, hits);

I'll wrap the atomic_inc_return inside neigh_hold_return()

Signature · 1 line
Stephen Hemminger <[email protected]>
#7 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Quoted 6 lines · Stephen Hemminger
Quoted 4 lines

Probably, you should do atomic_inc_and_test() here and restart lookup, if it fails.

Alexey

atomic_inc_and_test is true iff result is zero, so that won't work.

Wouldn't atomic_inc_not_zero() do what you want?

Signature · 1 line
/Martin
#8 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Quoted 11 lines · On Tue, 29 Aug 2006 20:34:01 +0200 Martin Josefsson <[email protected]>…
Quoted 6 lines · Stephen Hemminger
Quoted 4 lines

Probably, you should do atomic_inc_and_test() here and restart lookup, if it fails.

Alexey

atomic_inc_and_test is true iff result is zero, so that won't work.

Wouldn't atomic_inc_not_zero() do what you want?

Yes, but that code looks more complex for the common case. It ends up doing a compare exchange and retrying.

Signature · 1 line
Stephen Hemminger <[email protected]>
#9 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

In reply to Stephen Hemminger

Hello!

Quoted 1 line

atomic_inc_and_test is true iff result is zero, so that won't work.

I meant atomic_inc_not_zero(), as Martin noticed.

Quoted 6 lines

But the following should work:

	hlist_for_each_entry_rcu(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
		if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
			if (unlikely(&atomic_inc_return(&n->refcnt) == 1)) {
				neigh_release(n);

I do not think it will work. It has exactly the same race condition.

Yes, atomic_inc_not_zero() is expensive. But it looks like it is the cheapest variant, which works correctly without more work.

Another variant would be rework use of refcnt. It can be done like rt cache:
when release of the last reference does not mean anything.

Also, probably, it makes sense to add neigh_lookup_light(), which does not take refcnt, but required to call
neigh_release_light() (which is just rcu_read_unlock_bh()).

Alexey

#10 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Quoted 19 lines · On Wed, 30 Aug 2006 01:17:22 +0400 Alexey Kuznetsov <[email protected]>…

Hello!

Quoted 1 line

atomic_inc_and_test is true iff result is zero, so that won't work.

I meant atomic_inc_not_zero(), as Martin noticed.

Quoted 6 lines

But the following should work:

	hlist_for_each_entry_rcu(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
		if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
			if (unlikely(&atomic_inc_return(&n->refcnt) == 1)) {
				neigh_release(n);

I do not think it will work. It has exactly the same race condition.

Yes, atomic_inc_not_zero() is expensive. But it looks like it is the cheapest variant, which works correctly without more work.

Quoted 6 lines

Another variant would be rework use of refcnt. It can be done like rt cache:
when release of the last reference does not mean anything.

Also, probably, it makes sense to add neigh_lookup_light(), which does not take refcnt, but required to call
neigh_release_light() (which is just rcu_read_unlock_bh()).

Which code paths would that make sense on?
fib_detect_death (ok)
infiniband (ok)
wireless/strip (ok) -- hey, this code is crap it has
a refcount leak already!
arp_req_get (ok)
ndisc (ok)

Perhaps killing the refcount all together, and just changing everybody to neigh_lookup_rcu(). Nobody holds a long term reference to the entries.

Quoted 2 lines

Alexey

Or using the existing dead flag?

@@ -346,17 +359,24 @@ struct neighbour *neigh_lookup(struct ne

NEIGH_CACHE_STAT_INC(tbl, lookups);

- read_lock_bh(&tbl->lock);
- hlist_for_each_entry(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
+ rcu_read_lock();
+ hlist_for_each_entry_rcu(n, tmp, &tbl->hash_buckets[hash_val], hlist) {
if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) {
neigh_hold(n);
+ /* Don't rescuitate dead entries */
+ if (unlikely(n->dead)) {
+ neigh_release(n);
+ continue;
+ }
+
NEIGH_CACHE_STAT_INC(tbl, hits);
goto found;
}
}
n = NULL;
found:
- read_unlock_bh(&tbl->lock);
+ rcu_read_unlock();
return n;
}

#11 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Hello!

Quoted 12 lines
Quoted 3 lines

Also, probably, it makes sense to add neigh_lookup_light(), which does not take refcnt, but required to call
neigh_release_light() (which is just rcu_read_unlock_bh()).

Which code paths would that make sense on?
fib_detect_death (ok)
infiniband (ok)
wireless/strip (ok) -- hey, this code is crap it has
a refcount leak already!
arp_req_get (ok)
ndisc (ok)

Quoted 3 lines

Perhaps killing the refcount all together, and just changing everybody to neigh_lookup_rcu(). Nobody holds a long term reference to the entries.

The only real user of refcnt is destination cache.

It uses __neigh_lookup/__neigh_lookup_errno, which could also use neigh_lookup_rcu(), then do atomic_inc_not_zero() and, if it fails fall to neigh_create().

Alexey

#12 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Here is the full version of neigh_lookup_rcu().

The race of interest is what happens when neigh_lookup_rcu() returns a table entry and it gets deleted at the same time. See Documentation/RCU/listRCU.txt for more info.

There are a couple different scenario's.

1) update or reader looking at dead entry. The neighbour entry is queued on the rcu callback list, and will not be invoked until all cpu's have cleared the barrier. The reader's do see stale data but it is harmless. After reader has cleared critical section, the neigh_rcu_release is run and that calls neigh_release which sees last reference and calls neigh_destroy.

neigh_lookup_rcu checks for dead entries, but this is racy and can miss. It is just an attempt to close the window slightly.

2) caller wants to create or update entry (__neigh_lookup). caller gets just deleted entry (refcount still 1), then grabs refcount (now 2). Similar to above, but the actual neigh_destroy() happens either from rcu callback or neigh_release() in caller, which ever is last.

This should not be any more racy than the existing code.

---
 drivers/infiniband/core/addr.c |    7 -
 drivers/net/wireless/strip.c   |   11 +-
 include/net/neighbour.h        |   43 +++++---
 net/core/neighbour.c           |  197 ++++++++++++++++++++++-------------------
 net/ipv4/arp.c                 |   10 +-
 net/ipv6/ndisc.c               |    7 -
 6 files changed, 155 insertions(+), 120 deletions(-)
include/net/neighbour.h +28 -15
@@ -108,6 +108,7 @@ struct neighbour
 	struct sk_buff_head	arp_queue;
 	struct timer_list	timer;
 	struct neigh_ops	*ops;
+	struct rcu_head		rcu;
 	u8			primary_key[0];
 };
 
@@ -126,6 +127,7 @@ struct pneigh_entry
 {
 	struct hlist_node	hlist;
 	struct net_device	*dev;
+	struct rcu_head		rcu;
 	u8			key[0];
 };
 
@@ -157,7 +159,7 @@ struct neigh_table
 	struct timer_list 	proxy_timer;
 	struct sk_buff_head	proxy_queue;
 	atomic_t		entries;
-	rwlock_t		lock;
+	spinlock_t		lock;
 	unsigned long		last_rand;
 	kmem_cache_t		*kmem_cachep;
 	struct neigh_statistics	*stats;
@@ -181,9 +183,9 @@ struct neigh_table
 extern void			neigh_table_init(struct neigh_table *tbl);
 extern void			neigh_table_init_no_netlink(struct neigh_table *tbl);
 extern int			neigh_table_clear(struct neigh_table *tbl);
-extern struct neighbour *	neigh_lookup(struct neigh_table *tbl,
-					     const void *pkey,
-					     struct net_device *dev);
+extern struct neighbour *	neigh_lookup_rcu(struct neigh_table *tbl,
+						 const void *pkey,
+						 const struct net_device *dev);
 extern struct neighbour *	neigh_lookup_nodev(struct neigh_table *tbl,
 						   const void *pkey);
 extern struct neighbour *	neigh_create(struct neigh_table *tbl,
@@ -313,27 +315,38 @@ static inline int neigh_event_send(struc
 
 static inline struct neighbour *__neigh_lookup(struct neigh_table *tbl,
 					       const void *pkey,
-					       struct net_device *dev, int creat)
+					       struct net_device *dev,
+					       int creat)
 {
-	struct neighbour *n = neigh_lookup(tbl, pkey, dev);
-
-	if (n || !creat)
-		return n;
+	struct neighbour *n;
 
-	n = neigh_create(tbl, pkey, dev);
-	return IS_ERR(n) ? NULL : n;
+	rcu_read_lock();
+	n = neigh_lookup_rcu(tbl, pkey, dev);
+	if (n)
+		neigh_hold(n);
+	else if (creat) {
+		n = neigh_create(tbl, pkey, dev);
+		if (IS_ERR(n))
+			n = NULL;
+	}
+	rcu_read_unlock();
+	return n;
 }
 
 static inline struct neighbour *__neigh_lookup_errno(struct neigh_table *tbl,
 						     const void *pkey,
 						     struct net_device *dev)
 {
-	struct neighbour *n = neigh_lookup(tbl, pkey, dev);
+	struct neighbour *n;
 
+	rcu_read_lock();
+	n = neigh_lookup_rcu(tbl, pkey, dev);
 	if (n)
-		return n;
-
-	return neigh_create(tbl, pkey, dev);
+		neigh_hold(n);
+	else
+		n = neigh_create(tbl, pkey, dev);
+	rcu_read_unlock();
+	return n;
 }
 
 struct neighbour_cb {
net/core/neighbour.c +106 -91

560 lines not on this page · raw message

drivers/infiniband/core/addr.c +4 -3
@@ -165,10 +165,11 @@ static int addr_resolve_remote(struct so
 		goto put;
 	}
 
-	neigh = neigh_lookup(&arp_tbl, &rt->rt_gateway, rt->idev->dev);
+	rcu_read_lock();
+	neigh = neigh_lookup_rcu(&arp_tbl, &rt->rt_gateway, rt->idev->dev);
 	if (!neigh) {
 		ret = -ENODATA;
-		goto put;
+		goto release;
 	}
 
 	if (!(neigh->nud_state & NUD_VALID)) {
@@ -183,7 +184,7 @@ static int addr_resolve_remote(struct so
 
 	ret = copy_addr(addr, neigh->dev, neigh->ha);
 release:
-	neigh_release(neigh);
+	rcu_read_unlock();
 put:
 	ip_rt_put(rt);
 out:
drivers/net/wireless/strip.c +7 -4
@@ -467,17 +467,20 @@ static int arp_query(unsigned char *hadd
 		     struct net_device *dev)
 {
 	struct neighbour *neighbor_entry;
+	int ret = 0;
 
-	neighbor_entry = neigh_lookup(&arp_tbl, &paddr, dev);
+	rcu_read_lock();
+	neighbor_entry = neigh_lookup_rcu(&arp_tbl, &paddr, dev);
 
-	if (neighbor_entry != NULL) {
+	if (neighbor_entry) {
 		neighbor_entry->used = jiffies;
 		if (neighbor_entry->nud_state & NUD_VALID) {
 			memcpy(haddr, neighbor_entry->ha, dev->addr_len);
-			return 1;
+			ret = 1;
 		}
 	}
-	return 0;
+	rcu_read_unlock();
+	return ret;
 }
 
 static void DumpData(char *msg, struct strip *strip_info, __u8 * ptr,
net/ipv4/arp.c +6 -4
@@ -1067,7 +1067,8 @@ static int arp_req_get(struct arpreq *r,
 	struct neighbour *neigh;
 	int err = -ENXIO;
 
-	neigh = neigh_lookup(&arp_tbl, &ip, dev);
+	rcu_read_lock();
+	neigh = neigh_lookup_rcu(&arp_tbl, &ip, dev);
 	if (neigh) {
 		read_lock_bh(&neigh->lock);
 		memcpy(r->arp_ha.sa_data, neigh->ha, dev->addr_len);
@@ -1075,9 +1076,9 @@ static int arp_req_get(struct arpreq *r,
 		read_unlock_bh(&neigh->lock);
 		r->arp_ha.sa_family = dev->type;
 		strlcpy(r->arp_dev, dev->name, sizeof(r->arp_dev));
-		neigh_release(neigh);
 		err = 0;
 	}
+	rcu_read_unlock();
 	return err;
 }
 
@@ -1118,14 +1119,15 @@ static int arp_req_delete(struct arpreq 
 			return -EINVAL;
 	}
 	err = -ENXIO;
-	neigh = neigh_lookup(&arp_tbl, &ip, dev);
+	rcu_read_lock();
+	neigh = neigh_lookup_rcu(&arp_tbl, &ip, dev);
 	if (neigh) {
 		if (neigh->nud_state&~NUD_NOARP)
 			err = neigh_update(neigh, NULL, NUD_FAILED, 
 					   NEIGH_UPDATE_F_OVERRIDE|
 					   NEIGH_UPDATE_F_ADMIN);
-		neigh_release(neigh);
 	}
+	rcu_read_unlock();
 	return err;
 }
 
net/ipv6/ndisc.c +4 -3
@@ -944,8 +944,9 @@ static void ndisc_recv_na(struct sk_buff
 		in6_ifa_put(ifp);
 		return;
 	}
-	neigh = neigh_lookup(&nd_tbl, &msg->target, dev);
 
+	rcu_read_lock();
+	neigh = neigh_lookup_rcu(&nd_tbl, &msg->target, dev);
 	if (neigh) {
 		u8 old_flags = neigh->flags;
 
@@ -969,9 +970,9 @@ static void ndisc_recv_na(struct sk_buff
 				ip6_del_rt(rt);
 		}
 
-out:
-		neigh_release(neigh);
 	}
+out:
+	rcu_read_unlock();
 }
 
 static void ndisc_recv_rs(struct sk_buff *skb)
#13 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Hello!

Quoted 1 line

This should not be any more racy than the existing code.

Existing code is not racy.

Critical place is interpretation of refcnt==1. Current code assumes, that when refcnt=1 and entry is in hash table, nobody can take this entry (table is locked). So, it can be unlinked from the table.

See? __neigh_lookup()/__neigh_lookup_errno() _MUST_ return alive and hashed entry. And will stay hashed until the reference is held. Or until neigh entry is forced for destruction by device going down, in this case referring dst entries will die as well.

If dst cache grabs an entry, which is purged from table because for some time it had refcnt==1, you got a valid dst entry referring to dead neighbour entry.

Alexey

#14 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Quoted 5 lines · On Wed, 30 Aug 2006 03:21:26 +0400 Alexey Kuznetsov <[email protected]>…

Hello!

Quoted 1 line

This should not be any more racy than the existing code.

Existing code is not racy.

Race 1: w/o RCU
Cpu 0: is in neigh_lookup
gets read_lock() finds entry ++refcount to 2 updates it
Cpu 1: is in forced_gc()
waits at write_lock()
releases read_lock() drops ref count to 1.
sees ref count is 1
deletes it

Race 1: w RCU
Cpu 0: is in __neigh_lookup
updates it
Cpu 1: is in forced_gc()
leaves refcount=1
sees ref count is 1
deletes it

Quoted 9 lines

Critical place is interpretation of refcnt==1. Current code assumes, that when refcnt=1 and entry is in hash table, nobody can take this entry (table is locked). So, it can be unlinked from the table.

See? __neigh_lookup()/__neigh_lookup_errno() _MUST_ return alive and hashed entry. And will stay hashed until the reference is held. Or until neigh entry is forced for destruction by device going down, in this case referring dst entries will die as well.

Why must it be hashed, it could always get zapped just after the update.

Quoted 3 lines

If dst cache grabs an entry, which is purged from table because for some time it had refcnt==1, you got a valid dst entry referring to dead neighbour entry.

Hmm.. Since this is a slow path, grabbing the write_lock on the neighbour entry would block the gc from zapping it.

in __neigh_lookup()

		neigh_hold();
		write_lock(&n->lock);
		if (n->dead) {
			write_unlock()
			neigh_release()
			goto rescan;
		}

		write_unlock()
Signature · 1 line
Stephen Hemminger <[email protected]>
#15 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

Hello!

Quoted 4 lines

Race 1: w/o RCU
Cpu 0: is in neigh_lookup
gets read_lock() finds entry

Quoted 8 lines

++refcount to 2 updates it
Cpu 1: is in forced_gc()
waits at write_lock()
releases read_lock() drops ref count to 1.
sees ref count is 1
deletes it

Do you mean it is purged, though it is actually fresh? It is harmless race condition.

Quoted 1 line

Why must it be hashed, it could always get zapped just after the update.

Because otherwise we have to check its validity on level of dst_cache and to rebind. We do not want this. Actually dst->neighbour is supposed to be immutable.

It means that if some valid dst refers to a neighbour, it must remain hashed.

Quoted 1 line

Hmm.. Since this is a slow path,

All the neighbour lookups are in slow path. Actually, lookup of neighbour entry for use in dst cache is the most loaded path, the rest of them are pure maintanance paths.

Yes, this would work. RCU is a little spoiled, but at least global tbl->lock is replaced with per neighbour lock.

Alexey

#16 lore raw

Re: [PATCH 4/6] net neighbour: convert to RCU

In reply to Stephen Hemminger

Hello!

Yes, I forgot to say I take back my suggestion about atomic_inc_test_zero(). It would not work.

Seems, it is possible to add some barriers around setting n->dead and testing it in neigh_lookup_rcu(), but it would be scary and ugly. To be honest, I just do not know how to do this. :-)

#17 lore raw

PATCH 5/6 neighbour: convert lookup to sequence lock

In reply to Stephen Hemminger

The reading of neighbour table entries can be converted from a slow reader/writer lock to a fast lockless sequence number check.

---
 include/net/neighbour.h |    2 
 net/core/neighbour.c    |  117 +++++++++++++++++++++++++++++-------------------
 net/ipv4/arp.c          |  101 +++++++++++++++++++++++++----------------
 net/ipv6/ndisc.c        |   16 +++---
 net/ipv6/route.c        |   12 ++--
 net/sched/sch_teql.c    |   11 +++-
 6 files changed, 155 insertions(+), 104 deletions(-)
include/net/neighbour.h +1 -1
@@ -100,7 +100,7 @@ struct neighbour
 	__u8			type;
 	__u8			dead;
 	atomic_t		probes;
-	rwlock_t		lock;
+	seqlock_t		lock;
 	unsigned char		ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
 	struct hh_cache		*hh;
 	atomic_t		refcnt;
net/core/neighbour.c +72 -45

310 lines not on this page · raw message

net/ipv4/arp.c +61 -40

161 lines not on this page · raw message

net/ipv6/ndisc.c +8 -8
@@ -1412,15 +1412,15 @@ void ndisc_send_redirect(struct sk_buff 
 		return;
 	}
 
-	if (dev->addr_len) {
-		read_lock_bh(&neigh->lock);
-		if (neigh->nud_state & NUD_VALID) {
+	if (dev->addr_len && (neigh->nud_state & NUD_VALID)) {
+		unsigned seq;
+		do {
+			seq = read_seqbegin(&neigh->lock);
 			memcpy(ha_buf, neigh->ha, dev->addr_len);
-			read_unlock_bh(&neigh->lock);
-			ha = ha_buf;
-			len += ndisc_opt_addr_space(dev);
-		} else
-			read_unlock_bh(&neigh->lock);
+		} while (read_seqretry(&neigh->lock, seq));
+
+		ha = ha_buf;
+		len += ndisc_opt_addr_space(dev);
 	}
 
 	rd_len = min_t(unsigned int,
net/ipv6/route.c +5 -7
@@ -279,20 +279,19 @@ static void rt6_probe(struct rt6_info *r
 	 */
 	if (!neigh || (neigh->nud_state & NUD_VALID))
 		return;
-	read_lock_bh(&neigh->lock);
+
 	if (!(neigh->nud_state & NUD_VALID) &&
-	    time_after(jiffies, neigh->updated + rt->rt6i_idev->cnf.rtr_probe_interval)) {
+	    time_after(jiffies,
+		       neigh->updated + rt->rt6i_idev->cnf.rtr_probe_interval)) {
 		struct in6_addr mcaddr;
 		struct in6_addr *target;
 
 		neigh->updated = jiffies;
-		read_unlock_bh(&neigh->lock);
 
 		target = (struct in6_addr *)&neigh->primary_key;
 		addrconf_addr_solict_mult(target, &mcaddr);
 		ndisc_send_ns(rt->rt6i_dev, NULL, target, &mcaddr, NULL);
-	} else
-		read_unlock_bh(&neigh->lock);
+	}
 }
 #else
 static inline void rt6_probe(struct rt6_info *rt)
@@ -323,10 +322,9 @@ static int inline rt6_check_neigh(struct
 	    !(rt->rt6i_flags & RTF_GATEWAY))
 		m = 1;
 	else if (neigh) {
-		read_lock_bh(&neigh->lock);
+		smp_rmb();
 		if (neigh->nud_state & NUD_VALID)
 			m = 2;
-		read_unlock_bh(&neigh->lock);
 	}
 	return m;
 }
net/sched/sch_teql.c +8 -3
@@ -248,9 +248,14 @@ __teql_resolve(struct sk_buff *skb, stru
 	}
 	if (neigh_event_send(n, skb_res) == 0) {
 		int err;
-		read_lock(&n->lock);
-		err = dev->hard_header(skb, dev, ntohs(skb->protocol), n->ha, NULL, skb->len);
-		read_unlock(&n->lock);
+		unsigned seq;
+
+		do {
+			seq = read_seqbegin(&n->lock);
+			err = dev->hard_header(skb, dev, ntohs(skb->protocol),
+					       n->ha, NULL, skb->len);
+		} while (read_seqretry(&n->lock, seq));
+
 		if (err < 0) {
 			neigh_release(n);
 			return -EINVAL;

--
Stephen Hemminger <[email protected]>

#18 lore raw

PATCH 6/6 neighbour: convert hard header cache to sequence number

In reply to Stephen Hemminger

The reading of the hard header cache in the output path can be made lockless using seqlock.

---
 include/linux/netdevice.h |    3 ++-
 include/net/neighbour.h   |    2 ++
 net/core/neighbour.c      |   40 +++++++++++++++++++++++++++++++++++-----
 net/ipv4/ip_output.c      |   13 +++----------
 net/ipv6/ip6_output.c     |   13 +++----------
 5 files changed, 45 insertions(+), 26 deletions(-)
include/linux/netdevice.h +2 -1
@@ -193,7 +193,7 @@ struct hh_cache
                                          */
 	int		hh_len;		/* length of header */
 	int		(*hh_output)(struct sk_buff *skb);
-	rwlock_t	hh_lock;
+	seqlock_t	hh_lock;
 
 	/* cached hardware header; allow for machine alignment needs.        */
 #define HH_DATA_MOD	16
@@ -217,6 +217,7 @@ struct hh_cache
 #define LL_RESERVED_SPACE_EXTRA(dev,extra) \
 	((((dev)->hard_header_len+extra)&~(HH_DATA_MOD - 1)) + HH_DATA_MOD)
 
+
 /* These flag bits are private to the generic network queueing
  * layer, they may not be explicitly referenced by any other
  * code.
net/core/neighbour.c +35 -5
@@ -591,9 +591,11 @@ void neigh_destroy(struct neighbour *nei
 	while ((hh = neigh->hh) != NULL) {
 		neigh->hh = hh->hh_next;
 		hh->hh_next = NULL;
-		write_lock_bh(&hh->hh_lock);
+
+		write_seqlock_bh(&hh->hh_lock);
 		hh->hh_output = neigh_blackhole;
-		write_unlock_bh(&hh->hh_lock);
+		write_sequnlock_bh(&hh->hh_lock);
+
 		if (atomic_dec_and_test(&hh->hh_refcnt))
 			kfree(hh);
 	}
@@ -912,9 +914,9 @@ static void neigh_update_hhs(struct neig
 
 	if (update) {
 		for (hh = neigh->hh; hh; hh = hh->hh_next) {
-			write_lock_bh(&hh->hh_lock);
+			write_seqlock_bh(&hh->hh_lock);
 			update(hh, neigh->dev, neigh->ha);
-			write_unlock_bh(&hh->hh_lock);
+			write_sequnlock_bh(&hh->hh_lock);
 		}
 	}
 }
@@ -1105,7 +1107,7 @@ static void neigh_hh_init(struct neighbo
 			break;
 
 	if (!hh && (hh = kzalloc(sizeof(*hh), GFP_ATOMIC)) != NULL) {
-		rwlock_init(&hh->hh_lock);
+		seqlock_init(&hh->hh_lock);
 		hh->hh_type = protocol;
 		atomic_set(&hh->hh_refcnt, 0);
 		hh->hh_next = NULL;
@@ -1128,6 +1130,33 @@ static void neigh_hh_init(struct neighbo
 	}
 }
 
+
+/*
+ * Add header to skb from hard header cache
+ * Handle case where cache gets changed.
+ */
+int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
+{
+	int len, alen;
+	unsigned seq;
+	int (*output)(struct sk_buff *);
+
+	for(;;) {
+		seq = read_seqbegin(&hh->hh_lock);
+		len = hh->hh_len;
+		alen = HH_DATA_ALIGN(len);
+		output = hh->hh_output;
+		memcpy(skb->data - alen, hh->hh_data, alen);
+		skb_push(skb, len);
+
+		if (likely(!read_seqretry(&hh->hh_lock, seq)))
+			return output(skb);
+
+		/* undo and try again */
+		__skb_pull(skb, len);
+	}
+}
+
 /* This function can be used in contexts, where only old dev_queue_xmit
    worked, f.e. if you want to override normal output path (eql, shaper),
    but resolution is not made yet.
@@ -2767,6 +2796,7 @@ EXPORT_SYMBOL(neigh_delete);
 EXPORT_SYMBOL(neigh_destroy);
 EXPORT_SYMBOL(neigh_dump_info);
 EXPORT_SYMBOL(neigh_event_ns);
+EXPORT_SYMBOL(neigh_hh_output);
 EXPORT_SYMBOL(neigh_ifdown);
 EXPORT_SYMBOL(neigh_lookup);
 EXPORT_SYMBOL(neigh_lookup_nodev);
net/ipv4/ip_output.c +3 -10
@@ -182,16 +182,9 @@ static inline int ip_finish_output2(stru
 		skb = skb2;
 	}
 
-	if (hh) {
-		int hh_alen;
-
-		read_lock_bh(&hh->hh_lock);
-		hh_alen = HH_DATA_ALIGN(hh->hh_len);
-  		memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
-		read_unlock_bh(&hh->hh_lock);
-	        skb_push(skb, hh->hh_len);
-		return hh->hh_output(skb);
-	} else if (dst->neighbour)
+	if (hh)
+		return neigh_hh_output(hh, skb);
+	else if (dst->neighbour)
 		return dst->neighbour->output(skb);
 
 	if (net_ratelimit())
net/ipv6/ip6_output.c +3 -10
@@ -76,16 +76,9 @@ static inline int ip6_output_finish(stru
 	struct dst_entry *dst = skb->dst;
 	struct hh_cache *hh = dst->hh;
 
-	if (hh) {
-		int hh_alen;
-
-		read_lock_bh(&hh->hh_lock);
-		hh_alen = HH_DATA_ALIGN(hh->hh_len);
-		memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
-		read_unlock_bh(&hh->hh_lock);
-	        skb_push(skb, hh->hh_len);
-		return hh->hh_output(skb);
-	} else if (dst->neighbour)
+	if (hh)
+		return neigh_hh_output(hh, skb);
+	else if (dst->neighbour)
 		return dst->neighbour->output(skb);
 
 	IP6_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
include/net/neighbour.h +2 -0
@@ -193,6 +193,8 @@ extern struct neighbour *	neigh_create(s
 					     struct net_device *dev);
 extern void			neigh_destroy(struct neighbour *neigh);
 extern int			__neigh_event_send(struct neighbour *neigh, struct sk_buff *skb);
+extern int			neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb);
+
 extern int			neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, 
 					     u32 flags);
 extern void			neigh_changeaddr(struct neigh_table *tbl, struct net_device *dev);

--
Stephen Hemminger <[email protected]>