From 6e3ac5c605d5062279178b3ea0b853d0e9cf7cc9 Mon Sep 17 00:00:00 2001 From: rpj Date: Thu, 10 Mar 2011 13:40:16 +0000 Subject: Replace global Critical Sections with MCS Queue locks --- ChangeLog | 23 +++++++++++++++++++++++ global.c | 12 ++++++------ implement.h | 15 ++++++++------- pthread_cond_destroy.c | 12 +++++++----- pthread_cond_init.c | 6 ++++-- pthread_detach.c | 5 +++-- pthread_join.c | 5 +++-- pthread_kill.c | 5 +++-- pthread_mutex_destroy.c | 12 +++++------- pthread_rwlock_destroy.c | 5 +++-- pthread_spin_destroy.c | 6 ++++-- pthread_timechange_handler_np.c | 5 +++-- ptw32_cond_check_need_init.c | 22 +++------------------- ptw32_mutex_check_need_init.c | 26 +++----------------------- ptw32_processInitialize.c | 10 ---------- ptw32_processTerminate.c | 15 +++------------ ptw32_reuse.c | 10 ++++++---- ptw32_rwlock_check_need_init.c | 22 +++------------------- ptw32_spinlock_check_need_init.c | 9 +++------ tests/SIZES.GC | 2 +- tests/SIZES.VC | 2 +- 21 files changed, 95 insertions(+), 134 deletions(-) diff --git a/ChangeLog b/ChangeLog index ae308b7..363297e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,26 @@ +2011-03-09 Ross Johnson + + * implement.h (ptw32_thread_t_): Add process unique sequence number. + * global.c: Replace global Critical Section objects with MCS + queue locks. + * implement.h: Likewise. + * pthread_cond_destroy.c: Likewise. + * pthread_cond_init.c: Likewise. + * pthread_detach.c: Likewise. + * pthread_join.c: Likewise. + * pthread_kill.c: Likewise. + * pthread_mutex_destroy.c: Likewise. + * pthread_rwlock_destroy.c: Likewise. + * pthread_spin_destroy.c: Likewise. + * pthread_timechange_handler_np.c: Likewise. + * ptw32_cond_check_need_init.c: Likewise. + * ptw32_mutex_check_need_init.c: Likewise. + * ptw32_processInitialize.c: Likewise. + * ptw32_processTerminate.c: Likewise. + * ptw32_reuse.c: Likewise. + * ptw32_rwlock_check_need_init.c: Likewise. + * ptw32_spinlock_check_need_init.c: Likewise. + 2011-03-06 Ross Johnson * several (MINGW64): Cast and call fixups for 64 bit compatibility; diff --git a/global.c b/global.c index 420b8e4..df4806b 100644 --- a/global.c +++ b/global.c @@ -62,37 +62,37 @@ DWORD (*ptw32_register_cancelation) (PAPCFUNC, HANDLE, DWORD) = NULL; /* * Global lock for managing pthread_t struct reuse. */ -CRITICAL_SECTION ptw32_thread_reuse_lock; +ptw32_mcs_lock_t ptw32_thread_reuse_lock; /* * Global lock for testing internal state of statically declared mutexes. */ -CRITICAL_SECTION ptw32_mutex_test_init_lock; +ptw32_mcs_lock_t ptw32_mutex_test_init_lock; /* * Global lock for testing internal state of PTHREAD_COND_INITIALIZER * created condition variables. */ -CRITICAL_SECTION ptw32_cond_test_init_lock; +ptw32_mcs_lock_t ptw32_cond_test_init_lock; /* * Global lock for testing internal state of PTHREAD_RWLOCK_INITIALIZER * created read/write locks. */ -CRITICAL_SECTION ptw32_rwlock_test_init_lock; +ptw32_mcs_lock_t ptw32_rwlock_test_init_lock; /* * Global lock for testing internal state of PTHREAD_SPINLOCK_INITIALIZER * created spin locks. */ -CRITICAL_SECTION ptw32_spinlock_test_init_lock; +ptw32_mcs_lock_t ptw32_spinlock_test_init_lock; /* * Global lock for condition variable linked list. The list exists * to wake up CVs when a WM_TIMECHANGE message arrives. See * w32_TimeChangeHandler.c. */ -CRITICAL_SECTION ptw32_cond_list_lock; +ptw32_mcs_lock_t ptw32_cond_list_lock; #ifdef _UWIN /* diff --git a/implement.h b/implement.h index aaad665..1cd24f2 100644 --- a/implement.h +++ b/implement.h @@ -131,7 +131,8 @@ struct ptw32_thread_t_ #ifdef _UWIN DWORD dummy[5]; #endif - DWORD thread; + UINT64 seqNumber; /* Process-unique thread sequence number */ + DWORD thread; /* Win32 thread ID */ HANDLE threadH; /* Win32 thread handle - POSIX thread is invalid if threadH == 0 */ pthread_t ptHandle; /* This thread's permanent pthread_t handle */ ptw32_thread_t * prevReuse; /* Links threads on reuse stack */ @@ -535,12 +536,12 @@ extern int ptw32_concurrency; extern int ptw32_features; -extern CRITICAL_SECTION ptw32_thread_reuse_lock; -extern CRITICAL_SECTION ptw32_mutex_test_init_lock; -extern CRITICAL_SECTION ptw32_cond_list_lock; -extern CRITICAL_SECTION ptw32_cond_test_init_lock; -extern CRITICAL_SECTION ptw32_rwlock_test_init_lock; -extern CRITICAL_SECTION ptw32_spinlock_test_init_lock; +extern ptw32_mcs_lock_t ptw32_thread_reuse_lock; +extern ptw32_mcs_lock_t ptw32_mutex_test_init_lock; +extern ptw32_mcs_lock_t ptw32_cond_list_lock; +extern ptw32_mcs_lock_t ptw32_cond_test_init_lock; +extern ptw32_mcs_lock_t ptw32_rwlock_test_init_lock; +extern ptw32_mcs_lock_t ptw32_spinlock_test_init_lock; #ifdef _UWIN extern int pthread_count; diff --git a/pthread_cond_destroy.c b/pthread_cond_destroy.c index 53f7a53..40d4a08 100644 --- a/pthread_cond_destroy.c +++ b/pthread_cond_destroy.c @@ -126,7 +126,8 @@ pthread_cond_destroy (pthread_cond_t * cond) if (*cond != PTHREAD_COND_INITIALIZER) { - EnterCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_local_node_t node; + ptw32_mcs_lock_acquire(&ptw32_cond_list_lock, &node); cv = *cond; @@ -154,7 +155,7 @@ pthread_cond_destroy (pthread_cond_t * cond) if (result != 0) { - LeaveCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_lock_release(&node); return result; } @@ -213,14 +214,15 @@ pthread_cond_destroy (pthread_cond_t * cond) (void) free (cv); } - LeaveCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_lock_release(&node); } else { + ptw32_mcs_local_node_t node; /* * See notes in ptw32_cond_check_need_init() above also. */ - EnterCriticalSection (&ptw32_cond_test_init_lock); + ptw32_mcs_lock_acquire(&ptw32_cond_test_init_lock, &node); /* * Check again. @@ -244,7 +246,7 @@ pthread_cond_destroy (pthread_cond_t * cond) result = EBUSY; } - LeaveCriticalSection (&ptw32_cond_test_init_lock); + ptw32_mcs_lock_release(&node); } return ((result != 0) ? result : ((result1 != 0) ? result1 : result2)); diff --git a/pthread_cond_init.c b/pthread_cond_init.c index d2de232..f28fd67 100644 --- a/pthread_cond_init.c +++ b/pthread_cond_init.c @@ -138,7 +138,9 @@ FAIL0: DONE: if (0 == result) { - EnterCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_local_node_t node; + + ptw32_mcs_lock_acquire(&ptw32_cond_list_lock, &node); cv->next = NULL; cv->prev = ptw32_cond_list_tail; @@ -155,7 +157,7 @@ DONE: ptw32_cond_list_head = cv; } - LeaveCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_lock_release(&node); } *cond = cv; diff --git a/pthread_detach.c b/pthread_detach.c index b110f2b..19bc24d 100644 --- a/pthread_detach.c +++ b/pthread_detach.c @@ -77,8 +77,9 @@ pthread_detach (pthread_t thread) int result; BOOL destroyIt = PTW32_FALSE; ptw32_thread_t * tp = (ptw32_thread_t *) thread.p; + ptw32_mcs_local_node_t node; - EnterCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_acquire(&ptw32_thread_reuse_lock, &node); if (NULL == tp || thread.x != tp->ptHandle.x) @@ -120,7 +121,7 @@ pthread_detach (pthread_t thread) } } - LeaveCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_release(&node); if (result == 0) { diff --git a/pthread_join.c b/pthread_join.c index 72a3a74..c49484d 100644 --- a/pthread_join.c +++ b/pthread_join.c @@ -85,8 +85,9 @@ pthread_join (pthread_t thread, void **value_ptr) int result; pthread_t self; ptw32_thread_t * tp = (ptw32_thread_t *) thread.p; + ptw32_mcs_local_node_t node; - EnterCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_acquire(&ptw32_thread_reuse_lock, &node); if (NULL == tp || thread.x != tp->ptHandle.x) @@ -102,7 +103,7 @@ pthread_join (pthread_t thread, void **value_ptr) result = 0; } - LeaveCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_release(&node); if (result == 0) { diff --git a/pthread_kill.c b/pthread_kill.c index d360187..21939d8 100644 --- a/pthread_kill.c +++ b/pthread_kill.c @@ -77,8 +77,9 @@ pthread_kill (pthread_t thread, int sig) { int result = 0; ptw32_thread_t * tp; + ptw32_mcs_local_node_t node; - EnterCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_acquire(&ptw32_thread_reuse_lock, &node); tp = (ptw32_thread_t *) thread.p; @@ -89,7 +90,7 @@ pthread_kill (pthread_t thread, int sig) result = ESRCH; } - LeaveCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_release(&node); if (0 == result && 0 != sig) { diff --git a/pthread_mutex_destroy.c b/pthread_mutex_destroy.c index 95509b3..c9460dc 100644 --- a/pthread_mutex_destroy.c +++ b/pthread_mutex_destroy.c @@ -71,10 +71,6 @@ pthread_mutex_destroy (pthread_mutex_t * mutex) * be too late invalidating the mutex below since another thread * may already have entered mutex_lock and the check for a valid * *mutex != NULL. - * - * Note that this would be an unusual situation because it is not - * common that mutexes are destroyed while they are still in - * use by other threads. */ *mutex = NULL; @@ -112,10 +108,13 @@ pthread_mutex_destroy (pthread_mutex_t * mutex) } else { + ptw32_mcs_local_node_t node; + /* * See notes in ptw32_mutex_check_need_init() above also. */ - EnterCriticalSection (&ptw32_mutex_test_init_lock); + + ptw32_mcs_lock_acquire(&ptw32_mutex_test_init_lock, &node); /* * Check again. @@ -138,8 +137,7 @@ pthread_mutex_destroy (pthread_mutex_t * mutex) */ result = EBUSY; } - - LeaveCriticalSection (&ptw32_mutex_test_init_lock); + ptw32_mcs_lock_release(&node); } return (result); diff --git a/pthread_rwlock_destroy.c b/pthread_rwlock_destroy.c index d14b447..245a892 100644 --- a/pthread_rwlock_destroy.c +++ b/pthread_rwlock_destroy.c @@ -108,10 +108,11 @@ pthread_rwlock_destroy (pthread_rwlock_t * rwlock) } else { + ptw32_mcs_local_node_t node; /* * See notes in ptw32_rwlock_check_need_init() above also. */ - EnterCriticalSection (&ptw32_rwlock_test_init_lock); + ptw32_mcs_lock_acquire(&ptw32_rwlock_test_init_lock, &node); /* * Check again. @@ -135,7 +136,7 @@ pthread_rwlock_destroy (pthread_rwlock_t * rwlock) result = EBUSY; } - LeaveCriticalSection (&ptw32_rwlock_test_init_lock); + ptw32_mcs_lock_release(&node); } return ((result != 0) ? result : ((result1 != 0) ? result1 : result2)); diff --git a/pthread_spin_destroy.c b/pthread_spin_destroy.c index c9317d2..2ff1425 100644 --- a/pthread_spin_destroy.c +++ b/pthread_spin_destroy.c @@ -81,7 +81,9 @@ pthread_spin_destroy (pthread_spinlock_t * lock) /* * See notes in ptw32_spinlock_check_need_init() above also. */ - EnterCriticalSection (&ptw32_spinlock_test_init_lock); + ptw32_mcs_local_node_t node; + + ptw32_mcs_lock_acquire(&ptw32_spinlock_test_init_lock, &node); /* * Check again. @@ -105,7 +107,7 @@ pthread_spin_destroy (pthread_spinlock_t * lock) result = EBUSY; } - LeaveCriticalSection (&ptw32_spinlock_test_init_lock); + ptw32_mcs_lock_release(&node); } return (result); diff --git a/pthread_timechange_handler_np.c b/pthread_timechange_handler_np.c index 12bd135..0f97e74 100644 --- a/pthread_timechange_handler_np.c +++ b/pthread_timechange_handler_np.c @@ -90,8 +90,9 @@ pthread_timechange_handler_np (void *arg) { int result = 0; pthread_cond_t cv; + ptw32_mcs_local_node_t node; - EnterCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_lock_acquire(&ptw32_cond_list_lock, &node); cv = ptw32_cond_list_head; @@ -101,7 +102,7 @@ pthread_timechange_handler_np (void *arg) cv = cv->next; } - LeaveCriticalSection (&ptw32_cond_list_lock); + ptw32_mcs_lock_release(&node); return (void *) (size_t) (result != 0 ? EAGAIN : 0); } diff --git a/ptw32_cond_check_need_init.c b/ptw32_cond_check_need_init.c index 31359ad..ec3e8bb 100644 --- a/ptw32_cond_check_need_init.c +++ b/ptw32_cond_check_need_init.c @@ -43,29 +43,13 @@ INLINE int ptw32_cond_check_need_init (pthread_cond_t * cond) { int result = 0; + ptw32_mcs_local_node_t node; /* * The following guarded test is specifically for statically * initialised condition variables (via PTHREAD_OBJECT_INITIALIZER). - * - * Note that by not providing this synchronisation we risk - * introducing race conditions into applications which are - * correctly written. - * - * Approach - * -------- - * We know that static condition variables will not be PROCESS_SHARED - * so we can serialise access to internal state using - * Win32 Critical Sections rather than Win32 Mutexes. - * - * If using a single global lock slows applications down too much, - * multiple global locks could be created and hashed on some random - * value associated with each mutex, the pointer perhaps. At a guess, - * a good value for the optimal number of global locks might be - * the number of processors + 1. - * */ - EnterCriticalSection (&ptw32_cond_test_init_lock); + ptw32_mcs_lock_acquire(&ptw32_cond_test_init_lock, &node); /* * We got here possibly under race @@ -88,7 +72,7 @@ ptw32_cond_check_need_init (pthread_cond_t * cond) result = EINVAL; } - LeaveCriticalSection (&ptw32_cond_test_init_lock); + ptw32_mcs_lock_release(&node); return result; } diff --git a/ptw32_mutex_check_need_init.c b/ptw32_mutex_check_need_init.c index 35ec366..897db3c 100644 --- a/ptw32_mutex_check_need_init.c +++ b/ptw32_mutex_check_need_init.c @@ -50,29 +50,9 @@ ptw32_mutex_check_need_init (pthread_mutex_t * mutex) { register int result = 0; register pthread_mutex_t mtx; + ptw32_mcs_local_node_t node; - /* - * The following guarded test is specifically for statically - * initialised mutexes (via PTHREAD_MUTEX_INITIALIZER). - * - * Note that by not providing this synchronisation we risk - * introducing race conditions into applications which are - * correctly written. - * - * Approach - * -------- - * We know that static mutexes will not be PROCESS_SHARED - * so we can serialise access to internal state using - * Win32 Critical Sections rather than Win32 Mutexes. - * - * If using a single global lock slows applications down too much, - * multiple global locks could be created and hashed on some random - * value associated with each mutex, the pointer perhaps. At a guess, - * a good value for the optimal number of global locks might be - * the number of processors + 1. - * - */ - EnterCriticalSection (&ptw32_mutex_test_init_lock); + ptw32_mcs_lock_acquire(&ptw32_mutex_test_init_lock, &node); /* * We got here possibly under race @@ -106,7 +86,7 @@ ptw32_mutex_check_need_init (pthread_mutex_t * mutex) result = EINVAL; } - LeaveCriticalSection (&ptw32_mutex_test_init_lock); + ptw32_mcs_lock_release(&node); return (result); } diff --git a/ptw32_processInitialize.c b/ptw32_processInitialize.c index d13b022..8da3e41 100644 --- a/ptw32_processInitialize.c +++ b/ptw32_processInitialize.c @@ -87,16 +87,6 @@ ptw32_processInitialize (void) ptw32_processTerminate (); } - /* - * Set up the global locks. - */ - InitializeCriticalSection (&ptw32_thread_reuse_lock); - InitializeCriticalSection (&ptw32_mutex_test_init_lock); - InitializeCriticalSection (&ptw32_cond_list_lock); - InitializeCriticalSection (&ptw32_cond_test_init_lock); - InitializeCriticalSection (&ptw32_rwlock_test_init_lock); - InitializeCriticalSection (&ptw32_spinlock_test_init_lock); - return (ptw32_processInitialized); } /* processInitialize */ diff --git a/ptw32_processTerminate.c b/ptw32_processTerminate.c index d2dfa7a..83f0f23 100644 --- a/ptw32_processTerminate.c +++ b/ptw32_processTerminate.c @@ -65,6 +65,7 @@ ptw32_processTerminate (void) if (ptw32_processInitialized) { ptw32_thread_t * tp, * tpNext; + ptw32_mcs_local_node_t node; if (ptw32_selfThreadKey != NULL) { @@ -86,7 +87,7 @@ ptw32_processTerminate (void) ptw32_cleanupKey = NULL; } - EnterCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_acquire(&ptw32_thread_reuse_lock, &node); tp = ptw32_threadReuseTop; while (tp != PTW32_THREAD_REUSE_EMPTY) @@ -96,17 +97,7 @@ ptw32_processTerminate (void) tp = tpNext; } - LeaveCriticalSection (&ptw32_thread_reuse_lock); - - /* - * Destroy the global locks and other objects. - */ - DeleteCriticalSection (&ptw32_spinlock_test_init_lock); - DeleteCriticalSection (&ptw32_rwlock_test_init_lock); - DeleteCriticalSection (&ptw32_cond_test_init_lock); - DeleteCriticalSection (&ptw32_cond_list_lock); - DeleteCriticalSection (&ptw32_mutex_test_init_lock); - DeleteCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_release(&node); ptw32_processInitialized = PTW32_FALSE; } diff --git a/ptw32_reuse.c b/ptw32_reuse.c index 0e86984..79e4dee 100644 --- a/ptw32_reuse.c +++ b/ptw32_reuse.c @@ -76,8 +76,9 @@ pthread_t ptw32_threadReusePop (void) { pthread_t t = {NULL, 0}; + ptw32_mcs_local_node_t node; - EnterCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_acquire(&ptw32_thread_reuse_lock, &node); if (PTW32_THREAD_REUSE_EMPTY != ptw32_threadReuseTop) { @@ -97,7 +98,7 @@ ptw32_threadReusePop (void) t = tp->ptHandle; } - LeaveCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_release(&node); return t; @@ -114,8 +115,9 @@ ptw32_threadReusePush (pthread_t thread) { ptw32_thread_t * tp = (ptw32_thread_t *) thread.p; pthread_t t; + ptw32_mcs_local_node_t node; - EnterCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_acquire(&ptw32_thread_reuse_lock, &node); t = tp->ptHandle; memset(tp, 0, sizeof(ptw32_thread_t)); @@ -143,5 +145,5 @@ ptw32_threadReusePush (pthread_t thread) ptw32_threadReuseBottom = tp; - LeaveCriticalSection (&ptw32_thread_reuse_lock); + ptw32_mcs_lock_release(&node); } diff --git a/ptw32_rwlock_check_need_init.c b/ptw32_rwlock_check_need_init.c index ea2561e..858ee27 100644 --- a/ptw32_rwlock_check_need_init.c +++ b/ptw32_rwlock_check_need_init.c @@ -41,29 +41,13 @@ INLINE int ptw32_rwlock_check_need_init (pthread_rwlock_t * rwlock) { int result = 0; + ptw32_mcs_local_node_t node; /* * The following guarded test is specifically for statically * initialised rwlocks (via PTHREAD_RWLOCK_INITIALIZER). - * - * Note that by not providing this synchronisation we risk - * introducing race conditions into applications which are - * correctly written. - * - * Approach - * -------- - * We know that static rwlocks will not be PROCESS_SHARED - * so we can serialise access to internal state using - * Win32 Critical Sections rather than Win32 Mutexes. - * - * If using a single global lock slows applications down too much, - * multiple global locks could be created and hashed on some random - * value associated with each mutex, the pointer perhaps. At a guess, - * a good value for the optimal number of global locks might be - * the number of processors + 1. - * */ - EnterCriticalSection (&ptw32_rwlock_test_init_lock); + ptw32_mcs_lock_acquire(&ptw32_rwlock_test_init_lock, &node); /* * We got here possibly under race @@ -87,7 +71,7 @@ ptw32_rwlock_check_need_init (pthread_rwlock_t * rwlock) result = EINVAL; } - LeaveCriticalSection (&ptw32_rwlock_test_init_lock); + ptw32_mcs_lock_release(&node); return result; } diff --git a/ptw32_spinlock_check_need_init.c b/ptw32_spinlock_check_need_init.c index bf45bc3..8808454 100644 --- a/ptw32_spinlock_check_need_init.c +++ b/ptw32_spinlock_check_need_init.c @@ -42,16 +42,13 @@ INLINE int ptw32_spinlock_check_need_init (pthread_spinlock_t * lock) { int result = 0; + ptw32_mcs_local_node_t node; /* * The following guarded test is specifically for statically * initialised spinlocks (via PTHREAD_SPINLOCK_INITIALIZER). - * - * Note that by not providing this synchronisation we risk - * introducing race conditions into applications which are - * correctly written. */ - EnterCriticalSection (&ptw32_spinlock_test_init_lock); + ptw32_mcs_lock_acquire(&ptw32_spinlock_test_init_lock, &node); /* * We got here possibly under race @@ -75,7 +72,7 @@ ptw32_spinlock_check_need_init (pthread_spinlock_t * lock) result = EINVAL; } - LeaveCriticalSection (&ptw32_spinlock_test_init_lock); + ptw32_mcs_lock_release(&node); return (result); } diff --git a/tests/SIZES.GC b/tests/SIZES.GC index 603521b..eddaad7 100755 --- a/tests/SIZES.GC +++ b/tests/SIZES.GC @@ -1,7 +1,7 @@ Sizes of pthreads-win32 structs ------------------------------- pthread_t 8 - ptw32_thread_t 140 + ptw32_thread_t 152 pthread_attr_t_ 28 sem_t_ 12 pthread_mutex_t_ 24 diff --git a/tests/SIZES.VC b/tests/SIZES.VC index 603521b..eddaad7 100755 --- a/tests/SIZES.VC +++ b/tests/SIZES.VC @@ -1,7 +1,7 @@ Sizes of pthreads-win32 structs ------------------------------- pthread_t 8 - ptw32_thread_t 140 + ptw32_thread_t 152 pthread_attr_t_ 28 sem_t_ 12 pthread_mutex_t_ 24 -- cgit v1.2.3