Skip to contentSkip to conversation

locking/rtqspinlock: Realtime queued spinlocks

RFC PATCH In review Waiman Long 5 participants 24 messages Jan 3, 2017 – Jan 5, 2017

Subsystems (2)
lore
Share & tools
lore

Waiman Long

author reviewer
#0 lore raw

cover locking/rtqspinlock: Realtime queued spinlocks

This patchset introduces a new variant of queued spinlocks - the realtime queued spinlocks. The purpose of this new variant is to support real spinlock in a realtime environment where high priority RT tasks should be allowed to complete its work ASAP. This means as little waiting time for spinlocks as possible.

Non-RT tasks will wait for spinlocks in the MCS waiting queue as usual. RT tasks and interrupts will spin directly on the spinlocks and use the priority value in the pending byte to arbitrate who get the lock first.

Patch 1 removes the unused spin_lock_bh_nested() API.

Patch 2 introduces the basic realtime queued spinlocks where the pending byte is used for storing the priority of the highest priority RT task that is waiting on the spinlock. All the RT tasks will spin directly on the spinlock instead of waiting in the queue.

Patch 3 moves all interrupt context lock waiters to RT spinning.

Patch 4 overrides the spin_lock_nested() call with special code to enabled RT lock spinning for nested spinlock.

Patch 5 handles priority boosting by periodically checking its priority and unqueuing from the waiting queue and do RT spinning if applicable.

Patch 6 allows voluntary CPU preemption to happen when a CPU is waiting for a spinlock.

Patch 7 enables event counts to be collected by the qspinlock stat package so that we could monitor what have happened within the kernel.

With a locking microbenchmark running on a 2-socket 36-core E5-2699 v3 system, the elapsed times to complete 2M locking loop per non-RT thread were as follows:

   # of threads   qspinlock   rt-qspinlock  % change
   ------------   ---------   ------------  --------
        2           0.29s        1.97s       +580%
	3           1.46s        2.05s        +40%
	4           1.81s        2.38s        +31%
	5           2.36s        2.87s        +22%
	6           2.73s        3.58s        +31%
	7           3.17s        3.74s        +18%
	8           3.67s        4.70s        +28%
	9           3.89s        5.28s        +36%
       10           4.35s        6.58s        +51%

The RT qspinlock is slower than the non-RT qspinlock which is expected.

This patchset hasn't included any patch to modify the call sites of spin_lock_nested() to include the outer lock of the nest spinlock pair yet. That will be included in a later version of this patchset once it is determined that RT qspinlocks is worth pursuing.

Only minimal testing to build and boot the patched kernel was done. More extensive testing will be done with later versions of this patchset.

Waiman Long (7):
locking/spinlock: Remove the unused spin_lock_bh_nested API locking/rtqspinlock: Introduce realtime queued spinlocks locking/rtqspinlock: Use static RT priority when in interrupt context locking/rtqspinlock: Override spin_lock_nested with special RT variants locking/rtqspinlock: Handle priority boosting locking/rtqspinlock: Voluntarily yield CPU when need_sched() locking/rtqspinlock: Enable collection of event counts

 arch/x86/Kconfig                 |  18 +-
 include/linux/spinlock.h         |  43 +++-
 include/linux/spinlock_api_smp.h |   9 +-
 include/linux/spinlock_api_up.h  |   1 -
 kernel/Kconfig.locks             |   9 +
 kernel/locking/qspinlock.c       |  51 +++-
 kernel/locking/qspinlock_rt.h    | 543 +++++++++++++++++++++++++++++++++++++++
 kernel/locking/qspinlock_stat.h  |  81 +++++-
 kernel/locking/spinlock.c        |   8 -
 9 files changed, 721 insertions(+), 42 deletions(-)
 create mode 100644 kernel/locking/qspinlock_rt.h
Signature · 1 line
1.8.3.1
author reviewer
#1 lore raw

RFC PATCH 1/7 locking/spinlock: Remove the unused spin_lock_bh_nested API

The spin_lock_bh_nested() API is defined but is not used anywhere in the kernel. So all spin_lock_bh_nested() and related APIs are now removed.

---
 include/linux/spinlock.h         | 8 --------
 include/linux/spinlock_api_smp.h | 2 --
 include/linux/spinlock_api_up.h  | 1 -
 kernel/locking/spinlock.c        | 8 --------
 4 files changed, 19 deletions(-)
include/linux/spinlock.h +0 -8
index 47dd0ce..59248dc 100644
@@ -180,8 +180,6 @@ static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
 #ifdef CONFIG_DEBUG_LOCK_ALLOC
 # define raw_spin_lock_nested(lock, subclass) \
 	_raw_spin_lock_nested(lock, subclass)
-# define raw_spin_lock_bh_nested(lock, subclass) \
-	_raw_spin_lock_bh_nested(lock, subclass)
 
 # define raw_spin_lock_nest_lock(lock, nest_lock)			\
 	 do {								\
@@ -197,7 +195,6 @@ static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
 # define raw_spin_lock_nested(lock, subclass)		\
 	_raw_spin_lock(((void)(subclass), (lock)))
 # define raw_spin_lock_nest_lock(lock, nest_lock)	_raw_spin_lock(lock)
-# define raw_spin_lock_bh_nested(lock, subclass)	_raw_spin_lock_bh(lock)
 #endif
 
 #if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK)
@@ -317,11 +314,6 @@ static __always_inline int spin_trylock(spinlock_t *lock)
 	raw_spin_lock_nested(spinlock_check(lock), subclass);	\
 } while (0)
 
-#define spin_lock_bh_nested(lock, subclass)			\
-do {								\
-	raw_spin_lock_bh_nested(spinlock_check(lock), subclass);\
-} while (0)
-
 #define spin_lock_nest_lock(lock, nest_lock)				\
 do {									\
 	raw_spin_lock_nest_lock(spinlock_check(lock), nest_lock);	\
include/linux/spinlock_api_smp.h +0 -2
index 5344268..42dfab8 100644
@@ -22,8 +22,6 @@
 void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)		__acquires(lock);
 void __lockfunc _raw_spin_lock_nested(raw_spinlock_t *lock, int subclass)
 								__acquires(lock);
-void __lockfunc _raw_spin_lock_bh_nested(raw_spinlock_t *lock, int subclass)
-								__acquires(lock);
 void __lockfunc
 _raw_spin_lock_nest_lock(raw_spinlock_t *lock, struct lockdep_map *map)
 								__acquires(lock);
include/linux/spinlock_api_up.h +0 -1
index d3afef9..d0d1888 100644
@@ -57,7 +57,6 @@
 
 #define _raw_spin_lock(lock)			__LOCK(lock)
 #define _raw_spin_lock_nested(lock, subclass)	__LOCK(lock)
-#define _raw_spin_lock_bh_nested(lock, subclass) __LOCK(lock)
 #define _raw_read_lock(lock)			__LOCK(lock)
 #define _raw_write_lock(lock)			__LOCK(lock)
 #define _raw_spin_lock_bh(lock)			__LOCK_BH(lock)
kernel/locking/spinlock.c +0 -8
index db3ccb1..4b082b5 100644
@@ -363,14 +363,6 @@ void __lockfunc _raw_spin_lock_nested(raw_spinlock_t *lock, int subclass)
 }
 EXPORT_SYMBOL(_raw_spin_lock_nested);
 
-void __lockfunc _raw_spin_lock_bh_nested(raw_spinlock_t *lock, int subclass)
-{
-	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_LOCK_OFFSET);
-	spin_acquire(&lock->dep_map, subclass, 0, _RET_IP_);
-	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
-}
-EXPORT_SYMBOL(_raw_spin_lock_bh_nested);
-
 unsigned long __lockfunc _raw_spin_lock_irqsave_nested(raw_spinlock_t *lock,
 						   int subclass)
 {
Signature · 1 line
1.8.3.1
author reviewer
#2 lore raw

RFC PATCH 2/7 locking/rtqspinlock: Introduce realtime queued spinlocks

In reply to Waiman Long

Realtime queued spinlock is a variant of the queued spinlock that is suitable for use in a realtime running environment where the highest priority task should always get its work done as soon as possible. That means a minimal wait for spinlocks. To make that happen, RT tasks will not wait in the MCS wait queue of the queued spinlocks. Instead, they will spin directly on the lock and put the RT priority value of the highest priority RT task into the pending byte of the lock.

With the assumption that the number of RT tasks actively running on a system is relatively small, the performance overhead of such a RT variant should be small too.

Realtime queued spinlock is currently mutually exclusive of paravirtual spinlocks.

---
 arch/x86/Kconfig              |   2 +-
 kernel/Kconfig.locks          |   9 +++
 kernel/locking/qspinlock.c    |  34 +++++++++-
 kernel/locking/qspinlock_rt.h | 145 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 186 insertions(+), 4 deletions(-)
 create mode 100644 kernel/locking/qspinlock_rt.h
arch/x86/Kconfig +1 -1
index e487493..7a97b31 100644
@@ -723,7 +723,7 @@ config PARAVIRT_DEBUG
 
 config PARAVIRT_SPINLOCKS
 	bool "Paravirtualization layer for spinlocks"
-	depends on PARAVIRT && SMP
+	depends on PARAVIRT && SMP && !REALTIME_QUEUED_SPINLOCKS
 	---help---
 	  Paravirtualized spinlocks allow a pvops backend to replace the
 	  spinlock implementation with something virtualization-friendly
kernel/Kconfig.locks +9 -0
index 84d882f..93f5bb5 100644
@@ -248,3 +248,12 @@ config ARCH_USE_QUEUED_RWLOCKS
 config QUEUED_RWLOCKS
 	def_bool y if ARCH_USE_QUEUED_RWLOCKS
 	depends on SMP
+
+config REALTIME_QUEUED_SPINLOCKS
+	bool "Realtime queued spinlocks"
+	default n
+	depends on QUEUED_SPINLOCKS && PREEMPT && NR_CPUS < 16384
+	help
+	  This enables support for realtime environment where tasks with
+	  realtime priority will get the spinlocks ASAP instead of waiting
+	  in a queue.
kernel/locking/qspinlock.c +31 -3
index b2caec7..cb5c2e5 100644
@@ -268,6 +268,19 @@ static __always_inline u32  __pv_wait_head_or_lock(struct qspinlock *lock,
 #endif
 
 /*
+ * Realtime queued spinlock is mutual exclusive with native and PV spinlocks.
+ */
+#ifdef CONFIG_REALTIME_QUEUED_SPINLOCKS
+#include "qspinlock_rt.h"
+#else
+static __always_inline u32 rt_wait_head_or_retry(struct qspinlock *lock)
+						{ return 0; }
+#define rt_pending(v)		0
+#define rt_enabled()		false
+#define rt_spin_trylock(l)	false
+#endif
+
+/*
  * Various notes on spin_is_locked() and spin_unlock_wait(), which are
  * 'interesting' functions:
  *
@@ -415,7 +428,7 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 
 	BUILD_BUG_ON(CONFIG_NR_CPUS >= (1U << _Q_TAIL_CPU_BITS));
 
-	if (pv_enabled())
+	if (pv_enabled() || rt_enabled())
 		goto queue;
 
 	if (virt_spin_lock(lock))
@@ -490,6 +503,9 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 	 * queuing.
 	 */
 queue:
+	if (rt_spin_trylock(lock))
+		return;
+
 	node = this_cpu_ptr(&mcs_nodes[0]);
 	idx = node->count++;
 	tail = encode_tail(smp_processor_id(), idx);
@@ -573,6 +589,13 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 	if ((val = pv_wait_head_or_lock(lock, node)))
 		goto locked;
 
+	/*
+	 * The RT rt_wait_head_or_lock function, if active, will acquire the
+	 * lock and return a non-zero value.
+	 */
+	if ((val = rt_wait_head_or_retry(lock)))
+		goto locked;
+
 	val = smp_cond_load_acquire(&lock->val.counter, !(VAL & _Q_LOCKED_PENDING_MASK));
 
 locked:
@@ -587,7 +610,9 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 	 * to grab the lock.
 	 */
 	for (;;) {
-		/* In the PV case we might already have _Q_LOCKED_VAL set */
+		/*
+		 * In the PV & RT cases we might already have _Q_LOCKED_VAL set.
+		 */
 		if ((val & _Q_TAIL_MASK) != tail) {
 			set_locked(lock);
 			break;
@@ -596,8 +621,11 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 		 * The smp_cond_load_acquire() call above has provided the
 		 * necessary acquire semantics required for locking. At most
 		 * two iterations of this loop may be ran.
+		 *
+		 * In the RT case, the pending byte needs to be preserved.
 		 */
-		old = atomic_cmpxchg_relaxed(&lock->val, val, _Q_LOCKED_VAL);
+		old = atomic_cmpxchg_relaxed(&lock->val, val,
+					     rt_pending(val) | _Q_LOCKED_VAL);
 		if (old == val)
 			goto release;	/* No contention */
 
kernel/locking/qspinlock_rt.h new file +145 -0

145 lines not on this page · raw message

Signature · 1 line
1.8.3.1
author reviewer
#3 lore raw

RFC PATCH 3/7 locking/rtqspinlock: Use static RT priority when in interrupt context

In reply to Waiman Long

When in interrupt context, the priority of the interrupted task is meaningless. So static RT priority is assigned in this case to make sure that it can get lock ASAP to reduce latency to the interrupted task.

---
 kernel/locking/qspinlock_rt.h | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)
kernel/locking/qspinlock_rt.h +13 -2
index 69513d6..b6289fb 100644
@@ -19,6 +19,13 @@
  *
  * As RT qspinlock needs the whole pending byte, it cannot be used on kernel
  * configured to support 16K or more CPUs (CONFIG_NR_CPUS).
+ *
+ * In interrupt context, the priority of the interrupted task is not
+ * meaningful. So a fixed static RT priority is used and they won't go into
+ * the MCS wait queue.
+ *  1) Soft IRQ = 1
+ *  2) Hard IRQ = MAX_RT_PRIO
+ *  3) NMI	= MAX_RT_PRIO+1
  */
 #include <linux/sched.h>
 
@@ -60,11 +67,15 @@ static inline int rt_pending(int val)
 static bool rt_spin_trylock(struct qspinlock *lock)
 {
 	struct __qspinlock *l = (void *)lock;
-	u8 prio = rt_task_priority(current);
+	struct task_struct *task = in_interrupt() ? NULL : current;
+	u8 prio;
 
 	BUILD_BUG_ON(_Q_PENDING_BITS != 8);
 
-	if (!prio)
+	if (!task)
+		prio = in_nmi() ? MAX_RT_PRIO + 1
+		     : in_irq() ? MAX_RT_PRIO : 1;
+	else if (!(prio = rt_task_priority(task)))
 		return false;
 
 	/*
Signature · 1 line
1.8.3.1
author reviewer
#4 lore raw

RFC PATCH 4/7 locking/rtqspinlock: Override spin_lock_nested with special RT variants

In reply to Waiman Long

In general, the spinlock critical section is typically small enough that once a CPU acquires a spinlock, it should be able to release the lock in a reasonably short time. Waiting for the lock, however, can take a while depending on how many CPUs are contending for the lock.

The exception here is when the lock holder needs to acquire another spinlock. In this case, it may need to hold the spinlock for a relatively long time. This can be problematic for RT tasks as it can cause lock inversion problem where a lower priority task is holding back a higher priority task.

The maximum level of spinlock nesting in the Linux kernel is 2, and all those nested spinlock calls are annotated by the spin_lock_nested() or its variants for lock dependency check. As a result, we can actually override those APIs with a special version of RT spinlock function _rt_raw_spinlock_nested() that can solve the lock inversion problem. This is done by passing in the outer lock to the nested spinlock call to the RT spinlock function which can query the priority of the highest priority task that is waiting on the outer lock. It can then boost its priority in the inner lock waiting accordingly. If the outer lock isn't provided, the function default to a RT priority of 1.

Two new APIs are provided to allow the passing of the required outer lock parameter:

1) spin_lock_nested2(lock, subclass, nest_lock)
2) spin_lock_irqsave_nested2(lock, flags, subclass, nest_lock)

There are about 70 call sites in the kernel that use spin_lock_nested() or its variants. They can be modified later on to use the new APIs.

---
 include/linux/spinlock.h         |  37 +++++++++-
 include/linux/spinlock_api_smp.h |   7 ++
 kernel/locking/qspinlock_rt.h    | 143 +++++++++++++++++++++++++++++++--------
 3 files changed, 156 insertions(+), 31 deletions(-)
include/linux/spinlock.h +35 -2
index 59248dc..7cd71ab 100644
@@ -177,7 +177,15 @@ static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
 
 #define raw_spin_lock(lock)	_raw_spin_lock(lock)
 
-#ifdef CONFIG_DEBUG_LOCK_ALLOC
+#ifdef CONFIG_REALTIME_QUEUED_SPINLOCKS
+# define raw_spin_lock_nested(lock, subclass)		\
+	_rt_raw_spin_lock_nested(lock, subclass, NULL)
+# define raw_spin_lock_nested2(lock, subclass, nest_lock)\
+	_rt_raw_spin_lock_nested(lock, subclass, nest_lock)
+# define raw_spin_lock_nest_lock(lock, nest_lock)	\
+	_rt_raw_spin_lock_nested(lock, 0, &(nest_lock)->rlock)
+
+#elif defined(CONFIG_DEBUG_LOCK_ALLOC)
 # define raw_spin_lock_nested(lock, subclass) \
 	_raw_spin_lock_nested(lock, subclass)
 
@@ -205,7 +213,16 @@ static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
 		flags = _raw_spin_lock_irqsave(lock);	\
 	} while (0)
 
-#ifdef CONFIG_DEBUG_LOCK_ALLOC
+#ifdef CONFIG_REALTIME_QUEUED_SPINLOCKS
+#define raw_spin_lock_irqsave_nested(lock, flags, subclass)		 \
+	raw_spin_lock_irqsave_nested2(lock, flags, subclass, NULL)
+#define raw_spin_lock_irqsave_nested2(lock, flags, subclass, nest_lock)	 \
+	do {								 \
+		typecheck(unsigned long, flags);			 \
+		flags = _rt_raw_spin_lock_irqsave_nested(lock, subclass, \
+							 nest_lock);	 \
+	} while (0)
+#elif defined(CONFIG_DEBUG_LOCK_ALLOC)
 #define raw_spin_lock_irqsave_nested(lock, flags, subclass)		\
 	do {								\
 		typecheck(unsigned long, flags);			\
@@ -232,6 +249,15 @@ static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
 
 #endif
 
+#ifndef raw_spin_lock_nested2
+#define raw_spin_lock_nested2(lock, subclass, nest_lock)		\
+	raw_spin_lock_nested(lock, subclass)
+#endif
+#ifndef raw_spin_lock_irqsave_nested2
+#define raw_spin_lock_irqsave_nested2(lock, flags, subclass, nest_lock)	\
+	raw_spin_lock_irqsave_nested(lock, flags, subclass)
+#endif
+
 #define raw_spin_lock_irq(lock)		_raw_spin_lock_irq(lock)
 #define raw_spin_lock_bh(lock)		_raw_spin_lock_bh(lock)
 #define raw_spin_unlock(lock)		_raw_spin_unlock(lock)
@@ -314,6 +340,10 @@ static __always_inline int spin_trylock(spinlock_t *lock)
 	raw_spin_lock_nested(spinlock_check(lock), subclass);	\
 } while (0)
 
+#define spin_lock_nested2(lock, subclass, nest_lock)		\
+	raw_spin_lock_nested2(spinlock_check(lock), subclass,	\
+			      spinlock_check(nest_lock))
+
 #define spin_lock_nest_lock(lock, nest_lock)				\
 do {									\
 	raw_spin_lock_nest_lock(spinlock_check(lock), nest_lock);	\
@@ -334,6 +364,9 @@ static __always_inline void spin_lock_irq(spinlock_t *lock)
 	raw_spin_lock_irqsave_nested(spinlock_check(lock), flags, subclass); \
 } while (0)
 
+#define spin_lock_irqsave_nested2(lock, flags, subclass, nest_lock)	\
+	raw_spin_lock_irqsave_nested2(spinlock_check(lock), flags, subclass)
+
 static __always_inline void spin_unlock(spinlock_t *lock)
 {
 	raw_spin_unlock(&lock->rlock);
include/linux/spinlock_api_smp.h +7 -0
index 42dfab8..57b9585 100644
@@ -43,6 +43,13 @@ unsigned long __lockfunc _raw_spin_lock_irqsave(raw_spinlock_t *lock)
 _raw_spin_unlock_irqrestore(raw_spinlock_t *lock, unsigned long flags)
 								__releases(lock);
 
+#ifdef CONFIG_REALTIME_QUEUED_SPINLOCKS
+void __lockfunc _rt_raw_spin_lock_nested(raw_spinlock_t *lock, int subclass,
+		raw_spinlock_t *outerlock)		 __acquires(lock);
+unsigned long __lockfunc _rt_raw_spin_lock_irqsave_nested(raw_spinlock_t *lock,
+		int subclass, raw_spinlock_t *outerlock) __acquires(lock);
+#endif
+
 #ifdef CONFIG_INLINE_SPIN_LOCK
 #define _raw_spin_lock(lock) __raw_spin_lock(lock)
 #endif
kernel/locking/qspinlock_rt.h +114 -29

194 lines not on this page · raw message

Signature · 1 line
1.8.3.1
author reviewer
#5 lore raw

RFC PATCH 5/7 locking/rtqspinlock: Handle priority boosting

In reply to Waiman Long

As the priority of a task may get boosted due to an acquired rtmutex, we will need to periodically check the task priority to see if it gets boosted. For an originally non-RT task, that means unqueuing from the MCS wait queue before doing an RT spinning. So the unqueuing code from osq_lock is borrowed to handle the unqueuing aspect of it.

---
 kernel/locking/qspinlock.c    |  25 ++++-
 kernel/locking/qspinlock_rt.h | 235 +++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 253 insertions(+), 7 deletions(-)
kernel/locking/qspinlock.c +21 -4
index cb5c2e5..b25ad58 100644
@@ -270,10 +270,18 @@ static __always_inline u32  __pv_wait_head_or_lock(struct qspinlock *lock,
 /*
  * Realtime queued spinlock is mutual exclusive with native and PV spinlocks.
  */
+#define RT_RETRY	((u32)-1)
+
 #ifdef CONFIG_REALTIME_QUEUED_SPINLOCKS
 #include "qspinlock_rt.h"
 #else
-static __always_inline u32 rt_wait_head_or_retry(struct qspinlock *lock)
+static __always_inline void rt_init_node(struct mcs_spinlock *node, u32 tail) {}
+static __always_inline bool rt_wait_node_or_unqueue(struct qspinlock *lock,
+						    struct mcs_spinlock *node,
+						    struct mcs_spinlock *prev)
+						{ return false; }
+static __always_inline u32  rt_spin_lock_or_retry(struct qspinlock *lock,
+						  struct mcs_spinlock *node)
 						{ return 0; }
 #define rt_pending(v)		0
 #define rt_enabled()		false
@@ -514,6 +522,7 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 	node->locked = 0;
 	node->next = NULL;
 	pv_init_node(node);
+	rt_init_node(node, tail);
 
 	/*
 	 * We touched a (possibly) cold cacheline in the per-cpu queue node;
@@ -552,6 +561,10 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 		WRITE_ONCE(prev->next, node);
 
 		pv_wait_node(node, prev);
+
+		if (rt_wait_node_or_unqueue(lock, node, prev))
+			goto queue;	/* Retry RT locking */
+
 		arch_mcs_spin_lock_contended(&node->locked);
 
 		/*
@@ -591,10 +604,14 @@ void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
 
 	/*
 	 * The RT rt_wait_head_or_lock function, if active, will acquire the
-	 * lock and return a non-zero value.
+	 * lock, release the MCS lock and return a non-zero value. We need to
+	 * retry with RT spinning when RT_RETRY is returned.
 	 */
-	if ((val = rt_wait_head_or_retry(lock)))
-		goto locked;
+	val = rt_spin_lock_or_retry(lock, node);
+	if (val == RT_RETRY)
+		goto queue;
+	else if (val)
+		return;
 
 	val = smp_cond_load_acquire(&lock->val.counter, !(VAL & _Q_LOCKED_PENDING_MASK));
 
kernel/locking/qspinlock_rt.h +232 -3

288 lines not on this page · raw message

Signature · 1 line
1.8.3.1
author reviewer
#6 lore raw

RFC PATCH 6/7 locking/rtqspinlock: Voluntarily yield CPU when need_sched()

In reply to Waiman Long

Ideally we want the CPU to be preemptible even when inside or waiting for a lock. We cannot make it preemptible when inside a lock critical section, but we can try to make the task voluntarily yield the CPU when waiting for a lock.

This patch checks the need_sched() flag and yields the CPU when the preemption count is 1. IOW, the spin_lock() call isn't done in a region that doesn't allow preemption. Otherwise, it will just perform RT spinning with a minimum priority of 1.

---
 kernel/locking/qspinlock_rt.h | 68 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 65 insertions(+), 3 deletions(-)
kernel/locking/qspinlock_rt.h +65 -3
index 0c4d051..18ec1f8 100644
@@ -43,6 +43,16 @@
  * it will have to break out of the MCS wait queue just like what is done
  * in the OSQ lock. Then it has to retry RT spinning if it has been boosted
  * to RT priority.
+ *
+ * Another RT requirement is that the CPU need to be preemptible even when
+ * waiting for a spinlock. If the task has already acquired the lock, we
+ * will let it run to completion to release the lock and reenable preemption.
+ * For non-nested spinlock, a spinlock waiter will periodically check
+ * need_resched flag to see if it should break out of the waiting loop and
+ * yield the CPU as long as the preemption count indicates just one
+ * preempt_disabled(). For nested spinlock with outer lock acquired, it will
+ * boost its priority to the highest RT priority level to try to acquire the
+ * inner lock, finish up its work, release the locks and reenable preemption.
  */
 #include <linux/sched.h>
 
@@ -51,6 +61,15 @@
 #endif
 
 /*
+ * Rescheduling is only needed when it is in the task context, the
+ * PREEMPT_NEED_RESCHED flag is set and the preemption count is one.
+ * If only the TIF_NEED_RESCHED flag is set, it will be moved to RT
+ * spinning with a minimum priority of 1.
+ */
+#define rt_should_resched()	(preempt_count() == \
+				(PREEMPT_OFFSET | PREEMPT_NEED_RESCHED))
+
+/*
  * For proper unqueuing from the MCS wait queue, we need to store the encoded
  * tail code as well the previous node pointer into the extra MCS node. Since
  * CPUs in interrupt context won't use the per-CPU MCS nodes anymore. So only
@@ -133,9 +152,12 @@ static bool __rt_spin_trylock(struct qspinlock *lock,
 	if (!task)
 		min_prio = in_nmi() ? MAX_RT_PRIO + 1
 			 : in_irq() ? MAX_RT_PRIO : 1;
+	else if (need_resched() && !min_prio)
+		min_prio = 1;
 	if (!(prio = rt_task_priority(task, min_prio)))
 		return false;
 
+
 	/*
 	 * Spin on the lock and try to set its priority into the pending byte.
 	 */
@@ -189,6 +211,33 @@ static bool __rt_spin_trylock(struct qspinlock *lock,
 		prio = MAX(ol ? ol->pending : 0,
 			   rt_task_priority(task, min_prio));
 
+		/*
+		 * If another task needs this CPU, we will yield it if in
+		 * the process context and it is not a nested spinlock call.
+		 * Otherwise, we will raise our RT priority to try to get
+		 * the lock ASAP.
+		 */
+		if (!task || !rt_should_resched())
+			continue;
+
+		if (outerlock) {
+			if (min_prio < MAX_RT_PRIO)
+				min_prio = MAX_RT_PRIO;
+			continue;
+		}
+
+		/*
+		 * In the unlikely event that we need to relinquish the CPU,
+		 * we need to make sure that we are not the highest priority
+		 * task waiting for the lock.
+		 */
+		if (mypdprio) {
+			lockpend = READ_ONCE(l->locked_pending);
+			pdprio = (u8)(lockpend >> _Q_PENDING_OFFSET);
+			if (pdprio == mypdprio)
+				cmpxchg_relaxed(&l->pending, pdprio, 0);
+		}
+		schedule_preempt_disabled();
 	}
 	return true;
 }
@@ -293,7 +342,7 @@ static bool rt_wait_node_or_unqueue(struct qspinlock *lock,
 	rt_write_prev(node, prev);	/* Save previous node pointer */
 
 	while (!READ_ONCE(node->locked)) {
-		if (rt_task_priority(current, 0))
+		if (rt_task_priority(current, 0) || need_resched())
 			goto unqueue;
 		cpu_relax();
 	}
@@ -354,6 +403,12 @@ static bool rt_wait_node_or_unqueue(struct qspinlock *lock,
 	 */
 	__this_cpu_dec(mcs_nodes[0].count);
 
+	/*
+	 * Yield the CPU if needed by another task with the right condition.
+	 */
+	if (rt_should_resched())
+		schedule_preempt_disabled();
+
 	return true;	/* Need to retry RT spinning */
 }
 
@@ -385,9 +440,10 @@ static u32 rt_spin_lock_or_retry(struct qspinlock *lock,
 		}
 		/*
 		 * We need to break out of the non-RT wait queue and do
-		 * RT spinnning if we become an RT task.
+		 * RT spinnning if we become an RT task or another task needs
+		 * the CPU.
 		 */
-		if (rt_task_priority(current, 0)) {
+		if (rt_task_priority(current, 0) || need_resched()) {
 			retry = true;
 			goto unlock;
 		}
@@ -427,6 +483,12 @@ static u32 rt_spin_lock_or_retry(struct qspinlock *lock,
 	 */
 	__this_cpu_dec(mcs_nodes[0].count);
 
+	/*
+	 * Yield the CPU if needed by another task with the right condition.
+	 */
+	if (retry && rt_should_resched())
+		schedule_preempt_disabled();
+
 	return retry ? RT_RETRY : 1;
 }
 
Signature · 1 line
1.8.3.1
#7 lore raw

Re: [RFC PATCH 6/7] locking/rtqspinlock: Voluntarily yield CPU when need_sched()

Quoted 38 lines · Waiman Long

Ideally we want the CPU to be preemptible even when inside or waiting for a lock. We cannot make it preemptible when inside a lock critical section, but we can try to make the task voluntarily yield the CPU when waiting for a lock.

This patch checks the need_sched() flag and yields the CPU when the preemption count is 1. IOW, the spin_lock() call isn't done in a region that doesn't allow preemption. Otherwise, it will just perform RT spinning with a minimum priority of 1.

---
 kernel/locking/qspinlock_rt.h | 68 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 65 insertions(+), 3 deletions(-)
kernel/locking/qspinlock_rt.h +10 -0
index 0c4d051..18ec1f8 100644
@@ -43,6 +43,16 @@
  * it will have to break out of the MCS wait queue just like what is done
  * in the OSQ lock. Then it has to retry RT spinning if it has been boosted
  * to RT priority.
+ *
+ * Another RT requirement is that the CPU need to be preemptible even when
+ * waiting for a spinlock. If the task has already acquired the lock, we
+ * will let it run to completion to release the lock and reenable preemption.
+ * For non-nested spinlock, a spinlock waiter will periodically check
+ * need_resched flag to see if it should break out of the waiting loop and
+ * yield the CPU as long as the preemption count indicates just one
+ * preempt_disabled(). For nested spinlock with outer lock acquired, it will
+ * boost its priority to the highest RT priority level to try to acquire the
+ * inner lock, finish up its work, release the locks and reenable preemption.
  */
 #include <linux/sched.h>
 
@@ -51,6 +61,15 @@
 #endif

kernel/locking/qspinlock_rt.h line 71

Quoted 10 lines
kernel/locking/qspinlock_rt.h +8 -0
 
 /*
+ * Rescheduling is only needed when it is in the task context, the
+ * PREEMPT_NEED_RESCHED flag is set and the preemption count is one.
+ * If only the TIF_NEED_RESCHED flag is set, it will be moved to RT
+ * spinning with a minimum priority of 1.
+ */
+#define rt_should_resched()	(preempt_count() == \
+				(PREEMPT_OFFSET | PREEMPT_NEED_RESCHED))
+

Maybe I am missing something... but

On x86, PREEMPT_NEED_RESCHED is used in an inverting style, i.e. 0 indicates "need to reschedule" and preempt_count() masks away this very bit, which makes rt_should_resched() always false. So...

Regards,
Boqun

author reviewer
#8 lore raw

Re: [RFC PATCH 6/7] locking/rtqspinlock: Voluntarily yield CPU when need_sched()

Quoted 51 lines · Boqun Feng
Quoted 38 lines · Waiman Long

Ideally we want the CPU to be preemptible even when inside or waiting for a lock. We cannot make it preemptible when inside a lock critical section, but we can try to make the task voluntarily yield the CPU when waiting for a lock.

This patch checks the need_sched() flag and yields the CPU when the preemption count is 1. IOW, the spin_lock() call isn't done in a region that doesn't allow preemption. Otherwise, it will just perform RT spinning with a minimum priority of 1.

---
 kernel/locking/qspinlock_rt.h | 68 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 65 insertions(+), 3 deletions(-)
kernel/locking/qspinlock_rt.h +10 -0
index 0c4d051..18ec1f8 100644
@@ -43,6 +43,16 @@
  * it will have to break out of the MCS wait queue just like what is done
  * in the OSQ lock. Then it has to retry RT spinning if it has been boosted
  * to RT priority.
+ *
+ * Another RT requirement is that the CPU need to be preemptible even when
+ * waiting for a spinlock. If the task has already acquired the lock, we
+ * will let it run to completion to release the lock and reenable preemption.
+ * For non-nested spinlock, a spinlock waiter will periodically check
+ * need_resched flag to see if it should break out of the waiting loop and
+ * yield the CPU as long as the preemption count indicates just one
+ * preempt_disabled(). For nested spinlock with outer lock acquired, it will
+ * boost its priority to the highest RT priority level to try to acquire the
+ * inner lock, finish up its work, release the locks and reenable preemption.
  */
 #include <linux/sched.h>
 
@@ -51,6 +61,15 @@
 #endif

kernel/locking/qspinlock_rt.h line 71

Quoted 10 lines
kernel/locking/qspinlock_rt.h +8 -0
 
 /*
+ * Rescheduling is only needed when it is in the task context, the
+ * PREEMPT_NEED_RESCHED flag is set and the preemption count is one.
+ * If only the TIF_NEED_RESCHED flag is set, it will be moved to RT
+ * spinning with a minimum priority of 1.
+ */
+#define rt_should_resched()	(preempt_count() == \
+				(PREEMPT_OFFSET | PREEMPT_NEED_RESCHED))
+

Maybe I am missing something... but

Quoted 6 lines

On x86, PREEMPT_NEED_RESCHED is used in an inverting style, i.e. 0 indicates "need to reschedule" and preempt_count() masks away this very bit, which makes rt_should_resched() always false. So...

Regards,
Boqun

You are right. I think I misunderstood what the preemption code is doing. I think I need to revise the code to fix that. Thanks for spotting this.

Cheers,
Longman

#9 lore raw

Re: [RFC PATCH 6/7] locking/rtqspinlock: Voluntarily yield CPU when need_sched()

In reply to Waiman Long
Quoted 3 lines · Waiman Long

For nested spinlock with outer lock acquired, it will
+ * boost its priority to the highest RT priority level to try to acquire the
+ * inner lock, finish up its work, release the locks and reenable preemption.

DL scheduler turns the definition of "highest RT priority" more complicated. One may try to move a task to the DL class with a very closer deadline, but a DL task can always be rejected by the admission test....

-- Daniel

author reviewer
#10 lore raw

RFC PATCH 7/7 locking/rtqspinlock: Enable collection of event counts

In reply to Waiman Long

This patch enables the collection of event counts in the slowpath of the realtime queued spinlocks. The following events are being tracked if CONFIG_QUEUED_LOCK_STAT is defined:

- # of interrupt context RT spinnings
- # of task context RT spinnings
- # of nested spinlock RT spinnings
- # of unqueue operations due to RT priority
- # of unqueue operations due to need_resched()
- # of CPU yieldings

---
 arch/x86/Kconfig                | 16 ++++----
 kernel/locking/qspinlock_rt.h   | 15 +++++++-
 kernel/locking/qspinlock_stat.h | 81 ++++++++++++++++++++++++++++++++++++-----
 3 files changed, 92 insertions(+), 20 deletions(-)
arch/x86/Kconfig +8 -8
index 7a97b31..e0dc3c8 100644
@@ -694,6 +694,14 @@ config SCHED_OMIT_FRAME_POINTER
 
 	  If in doubt, say "Y".
 
+config QUEUED_LOCK_STAT
+	bool "Queued spinlock statistics"
+	depends on (PARAVIRT_SPINLOCKS || REALTIME_QUEUED_SPINLOCKS) && DEBUG_FS
+	---help---
+	  Enable the collection of statistical data on the slowpath
+	  behavior of paravirtualized or realtime queued spinlocks
+	  and report them on debugfs.
+
 menuconfig HYPERVISOR_GUEST
 	bool "Linux guest support"
 	---help---
@@ -734,14 +742,6 @@ config PARAVIRT_SPINLOCKS
 
 	  If you are unsure how to answer this question, answer Y.
 
-config QUEUED_LOCK_STAT
-	bool "Paravirt queued spinlock statistics"
-	depends on PARAVIRT_SPINLOCKS && DEBUG_FS
-	---help---
-	  Enable the collection of statistical data on the slowpath
-	  behavior of paravirtualized queued spinlocks and report
-	  them on debugfs.
-
 source "arch/x86/xen/Kconfig"
 
 config KVM_GUEST
kernel/locking/qspinlock_rt.h +13 -2
index 18ec1f8..f6f8498 100644
@@ -55,6 +55,7 @@
  * inner lock, finish up its work, release the locks and reenable preemption.
  */
 #include <linux/sched.h>
+#include "qspinlock_stat.h"
 
 #ifndef MAX
 #define MAX(a, b)	(((a) >= (b)) ? (a) : (b))
@@ -157,6 +158,7 @@ static bool __rt_spin_trylock(struct qspinlock *lock,
 	if (!(prio = rt_task_priority(task, min_prio)))
 		return false;
 
+	qstat_inc_either(qstat_rt_spin_task, qstat_rt_spin_irq, task);
 
 	/*
 	 * Spin on the lock and try to set its priority into the pending byte.
@@ -237,6 +239,7 @@ static bool __rt_spin_trylock(struct qspinlock *lock,
 			if (pdprio == mypdprio)
 				cmpxchg_relaxed(&l->pending, pdprio, 0);
 		}
+		qstat_inc(qstat_rt_resched, true);
 		schedule_preempt_disabled();
 	}
 	return true;
@@ -349,6 +352,9 @@ static bool rt_wait_node_or_unqueue(struct qspinlock *lock,
 	return false;
 
 unqueue:
+	qstat_inc_either(qstat_rt_unqueue_sched, qstat_rt_unqueue_prio,
+			 need_resched());
+
 	/*
 	 * Step - A  -- stabilize @prev
 	 *
@@ -406,8 +412,10 @@ static bool rt_wait_node_or_unqueue(struct qspinlock *lock,
 	/*
 	 * Yield the CPU if needed by another task with the right condition.
 	 */
-	if (rt_should_resched())
+	if (rt_should_resched()) {
+		qstat_inc(qstat_rt_resched, true);
 		schedule_preempt_disabled();
+	}
 
 	return true;	/* Need to retry RT spinning */
 }
@@ -486,8 +494,10 @@ static u32 rt_spin_lock_or_retry(struct qspinlock *lock,
 	/*
 	 * Yield the CPU if needed by another task with the right condition.
 	 */
-	if (retry && rt_should_resched())
+	if (retry && rt_should_resched()) {
+		qstat_inc(qstat_rt_resched, true);
 		schedule_preempt_disabled();
+	}
 
 	return retry ? RT_RETRY : 1;
 }
@@ -514,6 +524,7 @@ void __lockfunc _rt_raw_spin_lock_nested(raw_spinlock_t *lock, int subclass,
 				  _RET_IP_);
 	}
 #endif
+	qstat_inc(qstat_rt_spin_nest, true);
 	__acquire(lock);
 	__rt_spin_trylock(&lock->raw_lock,
 			  outerlock ? &outerlock->raw_lock : NULL, 1);
kernel/locking/qspinlock_stat.h +71 -10

153 lines not on this page · raw message

Signature · 1 line
1.8.3.1
maintainer
#11 lore raw
In reply to Waiman Long
Quoted 11 lines · Waiman Long

This patchset introduces a new variant of queued spinlocks - the realtime queued spinlocks. The purpose of this new variant is to support real spinlock in a realtime environment where high priority RT tasks should be allowed to complete its work ASAP. This means as little waiting time for spinlocks as possible.

Non-RT tasks will wait for spinlocks in the MCS waiting queue as usual. RT tasks and interrupts will spin directly on the spinlocks and use the priority value in the pending byte to arbitrate who get the lock first.

Quoted 7 lines

Patch 1 removes the unused spin_lock_bh_nested() API.

Patch 2 introduces the basic realtime queued spinlocks where the pending byte is used for storing the priority of the highest priority RT task that is waiting on the spinlock. All the RT tasks will spin directly on the spinlock instead of waiting in the queue.

OK, so a single numerical field isn't sufficient to describe priority anymore, since we added DEADLINE support things have gotten a lot more complex.

Also, the whole approach worries me, it has the very real possibility of re-introducing a bunch of starvation cases avoided by the fair lock.

Is there a real problem with -RT that inspired these patches?

author reviewer
#12 lore raw
Quoted 20 lines · Peter Zijlstra
Quoted 11 lines · Waiman Long

This patchset introduces a new variant of queued spinlocks - the realtime queued spinlocks. The purpose of this new variant is to support real spinlock in a realtime environment where high priority RT tasks should be allowed to complete its work ASAP. This means as little waiting time for spinlocks as possible.

Non-RT tasks will wait for spinlocks in the MCS waiting queue as usual. RT tasks and interrupts will spin directly on the spinlocks and use the priority value in the pending byte to arbitrate who get the lock first.

Quoted 7 lines

Patch 1 removes the unused spin_lock_bh_nested() API.

Patch 2 introduces the basic realtime queued spinlocks where the pending byte is used for storing the priority of the highest priority RT task that is waiting on the spinlock. All the RT tasks will spin directly on the spinlock instead of waiting in the queue.

Quoted 5 lines

OK, so a single numerical field isn't sufficient to describe priority anymore, since we added DEADLINE support things have gotten a lot more complex.

From what I read from the code, DL tasks all have the same priority that

is higher than any of the RT tasks. So you mean DL tasks have other property that kind of categorizing them into different sub-priorities that is not being reflected in their priority level. Is that right?

Quoted 2 lines

Also, the whole approach worries me, it has the very real possibility of re-introducing a bunch of starvation cases avoided by the fair lock.

Starvation can happen when there is a constant stream of RT or DL tasks grabbing the lock, or when there is an interrupt storm. However I am making the assumption that RT systems should have sufficient resource available that the RT tasks won't saturate the hardware or we can't have RT guarantee in this case.

Quoted 1 line

Is there a real problem with -RT that inspired these patches?

I know that in -RT kernel, all the non-raw spinlocks are replaced by rtmutex which is a sleeping lock. This can have a real performance impact on systems with more than a few cores. The rtmutex isn't fair either.

Do you think it is better to keep the raw spinlocks fair and only have the non-raw spinlocks use the RT version?

Cheers,
Longman

#13 lore raw
Quoted 3 lines · On Wed, 4 Jan 2017 10:25:14 -0500 Waiman Long <[email protected]> wrote:

I know that in -RT kernel, all the non-raw spinlocks are replaced by rtmutex which is a sleeping lock. This can have a real performance impact on systems with more than a few cores. The rtmutex isn't fair either.

We do fine on 80+ CPUs. Is that enough cores for you ;-)

Note, it's not a true sleeping lock, because of the adaptive nature. That is, it spins unless the owner of the lock is sleeping, in which case, it too will sleep (why spin waiting for a task that isn't running). But if the owner is running, it will spin too.

We also have tricks to keep normal preemption (like SCHED_OTHER tasks running out of their time slot) when they have a lock. This keeps contention down on tasks owning locks while sleeping.

Quoted 3 lines

Do you think it is better to keep the raw spinlocks fair and only have the non-raw spinlocks use the RT version?

Yes.

Note, I also want to get rt_mutex into the kernel first for all sleeping locks. That is, get the logic in before we convert spin_locks to sleeping locks.

-- Steve

author reviewer
#14 lore raw
Quoted 8 lines · Steven Rostedt
Quoted 3 lines · On Wed, 4 Jan 2017 10:25:14 -0500 Waiman Long <[email protected]> wrote:

I know that in -RT kernel, all the non-raw spinlocks are replaced by rtmutex which is a sleeping lock. This can have a real performance impact on systems with more than a few cores. The rtmutex isn't fair either.

We do fine on 80+ CPUs. Is that enough cores for you ;-)

It depends on what you mean fine. I haven't done the actual benchmark yet, but I believe that a -RT system with 80+ CPUs will feel noticeably slower for non-RT tasks than a system with non-RT kernel. I am not saying that the system will slow down significantly, but the slow down will certainly be noticeable.

Quoted 4 lines

Note, it's not a true sleeping lock, because of the adaptive nature. That is, it spins unless the owner of the lock is sleeping, in which case, it too will sleep (why spin waiting for a task that isn't running). But if the owner is running, it will spin too.

I think this is a recent change by Davidlohr. However, the spinning is limited to the top waiter. The rests will still go to sleep.

Quoted 3 lines

We also have tricks to keep normal preemption (like SCHED_OTHER tasks running out of their time slot) when they have a lock. This keeps contention down on tasks owning locks while sleeping.

Yes, that certainly help.

Quoted 3 lines
Quoted 2 lines

Do you think it is better to keep the raw spinlocks fair and only have the non-raw spinlocks use the RT version?

Yes.

OK, I can certainly do that.

Quoted 6 lines

Note, I also want to get rt_mutex into the kernel first for all sleeping locks. That is, get the logic in before we convert spin_locks to sleeping locks.

-- Steve

The rtmutex code is already in the upstream kernel. It is just that there isn't any code yet that can let you flip a switch (a config option) and change all sleeping locks to use rtmutex.

Cheers,
Longman

#15 lore raw
Quoted 8 lines · On Wed, 4 Jan 2017 15:02:23 -0500 Waiman Long <[email protected]> wrote:
Quoted 7 lines · Steven Rostedt
Quoted 3 lines · On Wed, 4 Jan 2017 10:25:14 -0500 Waiman Long <[email protected]> wrote:

I know that in -RT kernel, all the non-raw spinlocks are replaced by rtmutex which is a sleeping lock. This can have a real performance impact on systems with more than a few cores. The rtmutex isn't fair either.

Quoted 7 lines
Quoted 1 line

We do fine on 80+ CPUs. Is that enough cores for you ;-)

It depends on what you mean fine. I haven't done the actual benchmark yet, but I believe that a -RT system with 80+ CPUs will feel noticeably slower for non-RT tasks than a system with non-RT kernel. I am not saying that the system will slow down significantly, but the slow down will certainly be noticeable.

Really matters what the work load is. Yes, java apps will be much slower, because java runs 1000s of threads, and the real bottle neck is with the read-writer locks (the mmap rwsem causes issues here).

But, really, I've done kernel builds on these large boxes running -rt, and those still do well.

Quoted 8 lines
Quoted 5 lines

Note, it's not a true sleeping lock, because of the adaptive nature. That is, it spins unless the owner of the lock is sleeping, in which case, it too will sleep (why spin waiting for a task that isn't running). But if the owner is running, it will spin too.

I think this is a recent change by Davidlohr. However, the spinning is limited to the top waiter. The rests will still go to sleep.

No, adaptive spinlocks were there for some time. It's copyright in 2008 by Gregory Haskins, Sven Dietrich and Peter Morreale.

As for the top waiter, that could probably be optimized to not do so. Good project to try out and see how that speeds things up.

Quoted 9 lines
Quoted 4 lines

Note, I also want to get rt_mutex into the kernel first for all sleeping locks. That is, get the logic in before we convert spin_locks to sleeping locks.

The rtmutex code is already in the upstream kernel. It is just that there isn't any code yet that can let you flip a switch (a config option) and change all sleeping locks to use rtmutex.

The pi code is upstream, but it's only used by futexes and maybe even rcu boosting. But normal kernel mutexes don't have pi.

-- Steve

#16 lore raw
In reply to Waiman Long
Quoted 7 lines · Waiman Long
Quoted 3 lines

OK, so a single numerical field isn't sufficient to describe priority anymore, since we added DEADLINE support things have gotten a lot more complex.

From what I read from the code, DL tasks all have the same priority that is higher than any of the RT tasks. So you mean DL tasks have other property that kind of categorizing them into different sub-priorities that is not being reflected in their priority level. Is that right?

DL tasks are scheduled according to their absolute deadline, which stored in the u64 curr->dl.deadline.

It is more complex because the priority of a deadline task is always changing. The priority of a DL task changes on every new periodic/sporadic replenishment/activation.

-- Daniel

maintainer
#17 lore raw
In reply to Waiman Long
Quoted 21 lines · Waiman Long
Quoted 20 lines · Peter Zijlstra
Quoted 18 lines · Waiman Long

This patchset introduces a new variant of queued spinlocks - the realtime queued spinlocks. The purpose of this new variant is to support real spinlock in a realtime environment where high priority RT tasks should be allowed to complete its work ASAP. This means as little waiting time for spinlocks as possible.

Non-RT tasks will wait for spinlocks in the MCS waiting queue as usual. RT tasks and interrupts will spin directly on the spinlocks and use the priority value in the pending byte to arbitrate who get the lock first.

Patch 1 removes the unused spin_lock_bh_nested() API.

Patch 2 introduces the basic realtime queued spinlocks where the pending byte is used for storing the priority of the highest priority RT task that is waiting on the spinlock. All the RT tasks will spin directly on the spinlock instead of waiting in the queue.

Quoted 8 lines
Quoted 3 lines

OK, so a single numerical field isn't sufficient to describe priority anymore, since we added DEADLINE support things have gotten a lot more complex.

From what I read from the code, DL tasks all have the same priority that is higher than any of the RT tasks. So you mean DL tasks have other property that kind of categorizing them into different sub-priorities that is not being reflected in their priority level. Is that right?

Correct, primarily their deadline. That is, the scheduling function for the class picks the task with the earliest deadline.

Quoted 8 lines
Quoted 2 lines

Also, the whole approach worries me, it has the very real possibility of re-introducing a bunch of starvation cases avoided by the fair lock.

Starvation can happen when there is a constant stream of RT or DL tasks grabbing the lock, or when there is an interrupt storm. However I am making the assumption that RT systems should have sufficient resource available that the RT tasks won't saturate the hardware or we can't have RT guarantee in this case.

That only works on UP, on SMP you only need a combined utilization of 1 to completely saturate a lock.

Quoted 9 lines
Quoted 2 lines

Is there a real problem with -RT that inspired these patches?

I know that in -RT kernel, all the non-raw spinlocks are replaced by rtmutex which is a sleeping lock. This can have a real performance impact on systems with more than a few cores. The rtmutex isn't fair either.

Do you think it is better to keep the raw spinlocks fair and only have the non-raw spinlocks use the RT version?

I don't get what you're saying here. Are you proposing to replace the rtmutex with this rtspinlock? That will very fundamentally not work. The important part of the conversion of spinlock -> rtmutex is acquiring the preemptability. Using this rtspinlock looses that and breaks the entirety of what -rt is about.

author reviewer
#18 lore raw
Quoted 24 lines · Peter Zijlstra
Quoted 23 lines · Waiman Long
Quoted 22 lines · Peter Zijlstra
Quoted 11 lines · Waiman Long

This patchset introduces a new variant of queued spinlocks - the realtime queued spinlocks. The purpose of this new variant is to support real spinlock in a realtime environment where high priority RT tasks should be allowed to complete its work ASAP. This means as little waiting time for spinlocks as possible.

Non-RT tasks will wait for spinlocks in the MCS waiting queue as usual. RT tasks and interrupts will spin directly on the spinlocks and use the priority value in the pending byte to arbitrate who get the lock first.

Quoted 7 lines

Patch 1 removes the unused spin_lock_bh_nested() API.

Patch 2 introduces the basic realtime queued spinlocks where the pending byte is used for storing the priority of the highest priority RT task that is waiting on the spinlock. All the RT tasks will spin directly on the spinlock instead of waiting in the queue.

OK, so a single numerical field isn't sufficient to describe priority anymore, since we added DEADLINE support things have gotten a lot more complex.

Quoted 6 lines
Quoted 4 lines

From what I read from the code, DL tasks all have the same priority that is higher than any of the RT tasks. So you mean DL tasks have other property that kind of categorizing them into different sub-priorities that is not being reflected in their priority level. Is that right?

Correct, primarily their deadline. That is, the scheduling function for the class picks the task with the earliest deadline.

OK, I need to rethink how to deal with those DL tasks.

Quoted 9 lines
Quoted 7 lines
Quoted 2 lines

Also, the whole approach worries me, it has the very real possibility of re-introducing a bunch of starvation cases avoided by the fair lock.

Starvation can happen when there is a constant stream of RT or DL tasks grabbing the lock, or when there is an interrupt storm. However I am making the assumption that RT systems should have sufficient resource available that the RT tasks won't saturate the hardware or we can't have RT guarantee in this case.

That only works on UP, on SMP you only need a combined utilization of 1 to completely saturate a lock.

An RT task in a spinlock loop won't be able to completely monopolize the lock because of the small window between unlock and lock that others can come in and get the lock. You will need at least 2 RT tasks in lockstep to completely own the lock and starve the others.

We could implement some kind of policy to increase the dynamic priority of a task the longer it waits for the lock to make sure that there will be no lock starvation.

Quoted 5 lines
Quoted 5 lines
Quoted 1 line

Is there a real problem with -RT that inspired these patches?

I know that in -RT kernel, all the non-raw spinlocks are replaced by rtmutex which is a sleeping lock. This can have a real performance impact on systems with more than a few cores. The rtmutex isn't fair either.

Quoted 7 lines
Quoted 2 lines

Do you think it is better to keep the raw spinlocks fair and only have the non-raw spinlocks use the RT version?

I don't get what you're saying here. Are you proposing to replace the rtmutex with this rtspinlock? That will very fundamentally not work. The important part of the conversion of spinlock -> rtmutex is acquiring the preemptability. Using this rtspinlock looses that and breaks the entirety of what -rt is about.

What I am saying that we don't need to change spinlock to rtmutex in a -RT kernel. Instead, we can use rtqspinlock for this purpose. All the sleeping locks will still be converted to rtmutex.

Conversion of rtmutex does allow forced CPU preemption when there is a need for that. What rtqspinlock can provide is voluntary preemption where the lock waiters explicitly yield the CPU while waiting for the lock. I use the need_resched() to detect if CPU yielding is necessary. However, if the CPU was in a preempt disabled region before the spin_lock() call, we can't yield the CPU. The only way is to raise its priority and try to get the lock ASAP. I still have some work to do in this area and I need to figure out how to convey the information about the priority of the task that is waiting for the CPU.

Cheers,
Longman

maintainer
#19 lore raw
Quoted 3 lines · Waiman Long

What I am saying that we don't need to change spinlock to rtmutex in a -RT kernel. Instead, we can use rtqspinlock for this purpose. All the sleeping locks will still be converted to rtmutex.

No-no-no..

Quoted 7 lines

Conversion of rtmutex does allow forced CPU preemption when there is a need for that. What rtqspinlock can provide is voluntary preemption where the lock waiters explicitly yield the CPU while waiting for the lock. I use the need_resched() to detect if CPU yielding is necessary. However, if the CPU was in a preempt disabled region before the spin_lock() call, we can't yield the CPU. The only way is to raise its priority and try to get the lock ASAP.

And here you've lost your finger because the saw-blade didn't stop in time.

RT very fundamentally relies on the spinlock->rtmutex conversion to allow preempting things when a higher priority task comes along. A spinlock, of any kind, requires having preemption disabled while holding the lock. If the critical section is of unbounded latency, you have unbounded preemption latency and RT is no more.

Its not about PI on contention, although that helps inversion scenarios. Its about allowing preemption, which fundamentally requires a sleeping lock to be used.

Many of the spinlock sections of mainline are not well behaved in an RT sense and therefore must not disable preemption. Similar for the IRQ disable regions and hence we have the whole threaded interrupt stuff.

Please stop writing code and read up on things..

author reviewer
#20 lore raw
Quoted 13 lines · Peter Zijlstra
Quoted 3 lines · Waiman Long

What I am saying that we don't need to change spinlock to rtmutex in a -RT kernel. Instead, we can use rtqspinlock for this purpose. All the sleeping locks will still be converted to rtmutex.

No-no-no..

Quoted 7 lines

Conversion of rtmutex does allow forced CPU preemption when there is a need for that. What rtqspinlock can provide is voluntary preemption where the lock waiters explicitly yield the CPU while waiting for the lock. I use the need_resched() to detect if CPU yielding is necessary. However, if the CPU was in a preempt disabled region before the spin_lock() call, we can't yield the CPU. The only way is to raise its priority and try to get the lock ASAP.

Quoted 2 lines

And here you've lost your finger because the saw-blade didn't stop in time.

Well, I lost my virtual fingers all the time;-)

This is one way that I learn and become stronger.

Quoted 6 lines

RT very fundamentally relies on the spinlock->rtmutex conversion to allow preempting things when a higher priority task comes along. A spinlock, of any kind, requires having preemption disabled while holding the lock. If the critical section is of unbounded latency, you have unbounded preemption latency and RT is no more.

Quoted 7 lines

Its not about PI on contention, although that helps inversion scenarios. Its about allowing preemption, which fundamentally requires a sleeping lock to be used.

Many of the spinlock sections of mainline are not well behaved in an RT sense and therefore must not disable preemption. Similar for the IRQ disable regions and hence we have the whole threaded interrupt stuff.

I do make the assumption that spinlock critical sections are behaving well enough. Apparently, that is not a valid assumption. I sent these RFC patches out to see if it was an idea worth pursuing. If not, I can drop these patches. Anyway, thanks for the feedback.

Cheers,
Longman

#21 lore raw
Quoted 4 lines · On Thu, 5 Jan 2017 12:07:21 -0500 Waiman Long <[email protected]> wrote:

I do make the assumption that spinlock critical sections are behaving well enough. Apparently, that is not a valid assumption. I sent these RFC patches out to see if it was an idea worth pursuing. If not, I can drop these patches. Anyway, thanks for the feedback.

Yes, the assumption is incorrect. There are places that can hold a spin lock for several hundreds of microseconds. If you can't preempt them, you'll never get below several hundreds of microseconds in latency.

And it would be hard to pick and choose (we already do this to decide what can be a raw_spin_lock), because you need to audit all use cases of a spin_lock as well as all the locks taken while holding that spin_lock.

-- Steve

author reviewer
#22 lore raw
Quoted 12 lines · Steven Rostedt
Quoted 4 lines · On Thu, 5 Jan 2017 12:07:21 -0500 Waiman Long <[email protected]> wrote:

I do make the assumption that spinlock critical sections are behaving well enough. Apparently, that is not a valid assumption. I sent these RFC patches out to see if it was an idea worth pursuing. If not, I can drop these patches. Anyway, thanks for the feedback.

Yes, the assumption is incorrect. There are places that can hold a spin lock for several hundreds of microseconds. If you can't preempt them, you'll never get below several hundreds of microseconds in latency.

Quoted 6 lines

And it would be hard to pick and choose (we already do this to decide what can be a raw_spin_lock), because you need to audit all use cases of a spin_lock as well as all the locks taken while holding that spin_lock.

-- Steve

Thank for the information.

It has come to my attention that scalability problem may be present in the -RT kernel because of the longer wait time in the raw_spin_lock side as the number of CPUs increases. I will look into this some more to see if my patch set can help under those circumstances.

Cheers,
Longman

#23 lore raw
In reply to Peter Zijlstra
Quoted 9 lines · Peter Zijlstra

RT very fundamentally relies on the spinlock->rtmutex conversion to allow preempting things when a higher priority task comes along. A spinlock, of any kind, requires having preemption disabled while holding the lock. If the critical section is of unbounded latency, you have unbounded preemption latency and RT is no more.

Its not about PI on contention, although that helps inversion scenarios. Its about allowing preemption, which fundamentally requires a sleeping lock to be used.

2 cents complementing this:

Spinlocks algorithms which disable the preemption while holding the lock resembles a theoretical algorithm named Immediate Priority Ceiling Protocol. The immediate priority ceiling protocol avoids unbounded priority inversion, like the Priority Inheritance Protocol used on rt mutex. However, although the Immediate Priority Ceiling Protocol may help to reduce the response time of tasks, it causes penalty on the activation/scheduling delay (latency on linux -rt dictionary) of tasks with the priority higher than the task holding the lock, but lower than the Ceiling priority. As it is not possible to know on beforehand the Ceiling priority of a lock on Linux, the implementation needs to use the highest priority, that is only possible by disabling the preemption on Linux, causing scheduling latency even for the highest -rt task.

Hence, the penalty of the immediate priority ceiling protocol is right in the main metric of the PREEMPT_RT: the scheduling latency.

This is an old article:

http://www.jsoftware.us/vol7/jsw0703-03.pdf

for uni processors... so it does not completely fit in this case but it shows some results of using immediate priority ceiling rather than PI on rt-mutex. It shows that the immediate priority ceiling causes scheduling delays/latency

This paper mentions the Multicore Priority Ceiling Protocol causing scheduling latency as well:

https://people.mpi-sws.org/~bbb/papers/pdf/rtlws12.pdf

That is why RT Mutex with PIP is better for the PREEMPT than any protocol that disables preemption.

-- Daniel