Preface: I recently read the release notes of Linux 5.15. This version incorporates the real-time lock mechanism. When the configuration macro config is enabled_ PREEMPT_ In RT, these locks are replaced by variants based on real-time mutex: mutex, ww_mutex,rw_semaphore, spinlock, and rwlock. First heard ww_mutex, when searching on Baidu, found that there were few introduction documents, so he studied by himself and wrote notes.
In some cases, multiple locks must be held at the same time, and the order of obtaining locks may be different. In order to avoid deadlock, Wound/Wait Mutexes should be used. Obtaining a lock set is called a transaction. Each transaction is associated with a ticket, which is also called a serial number. Which transaction is young is judged according to the ticket. There are two ways to handle deadlocks, as follows.
(1) Wait die algorithm: when a transaction applies for a lock that has been obtained by another transaction, if the transaction holding the lock is young, the transaction applying for the lock will wait; If the transaction holding the lock is old, the transaction requesting the lock retreats and dies (die).
(2) Version 4.19 supports the wound wait algorithm: when a transaction applies for a lock that has been acquired by another transaction, if the transaction holding the lock is young, the transaction applying for the lock will wound the transaction holding the lock and request it to die; If the transaction holding the lock is old, the transaction requesting the lock waits.
Suppose that process 1 and process 2 run on two processors respectively, process 1 obtains lock A, process 2 obtains lock B, then process 1 applies for lock B and process 2 applies for lock A. Suppose that the ticket number of process 1 is smaller than that of process 2, that is, process 1 is old and process 2 is young.
Both algorithms are fair because one of the transactions will eventually succeed. Compared with the waiting death algorithm, the injury waiting algorithm generates less backoff, but more work needs to be done to recover from a backoff. Injury waiting algorithm is a preemptive algorithm (because the transaction is injured by other transactions). A reliable method is needed to select the injury state and preempt the running transaction. In the injury waiting algorithm, if a transaction dies after injury (return "- EDEADLK"), it is considered that the transaction is preempted.
If there are few processes competing for locks and you want to reduce the number of rollback, you should choose the injury waiting algorithm.
Compared with ordinary mutexes, injury / wait mutexes add the following two concepts.
(1) Acquire context: an acquire context represents a transaction and is associated with a ticket. The ticket is also called a serial number. A small ticket number indicates old age, and a large ticket number indicates young age. Get the context trace debugging status and catch the wrong use of the injury / wait mutex interface.
(2) Injury / waiting class: when initializing the acquisition context, you need to specify the lock class, which will assign tickets to the acquisition context. The lock class also specifies algorithms: wait die or wound wait. When multiple processes compete for the same lock set, they must use the same lock class.
There are three functions to obtain injury / wait mutex, as follows.
(1) Ordinary lock acquisition function ww_mutex_lock(), with get context.
(2) After the process rolls back (that is, releases all acquired locks), it uses the slow path to acquire locks function ww_mutex_lock_slow() gets the lock being contested. A function with a "_slow" suffix is not required because the function WW can be called_ mutex_ Lock() gets the lock being contested. The advantage of functions with "_slow" suffix is interface security, as follows.
- Function ww_mutex_lock() has an integer return value, and the function ww_mutex_lock_slow() has no return value.
- When debugging is turned on, the function ww_mutex_lock_slow() checks that all acquired locks have been released and ensures that the process is blocking on the competing locks.
(3) Only one injury / wait mutex is obtained, which is exactly the same as obtaining an ordinary mutex. Call function ww_mutex_lock() specifies the fetch context as a null pointer.
The use of injury / wait mutex is as follows.
(1) Define a lock class, which is required when initializing the acquisition context. The lock class also specifies the algorithm: wait die or wound wait.
/* Specify wait death algorithm */ static DEFINE_WD_CLASS(my_class); /* Specified damage waiting algorithm */ static DEFINE_WW_CLASS(my_class);
(2) Initialize an acquisition context, and the lock class will assign a ticket to the acquisition context.
void ww_acquire_init(struct ww_acquire_ctx *ctx, struct ww_class *ww_class);
(3) To obtain a lock, a return of 0 indicates that the acquisition is successful, and a return of "- EDEADLK" indicates that a deadlock is detected.
int ww_mutex_lock(struct ww_mutex *lock, struct ww_acquire_ctx *ctx);
(4) After acquiring all the required locks, mark the end of the acquisition phase. At present, this function does not perform any operation, but it may change in the future.
void ww_acquire_done(struct ww_acquire_ctx *ctx);
(5) Release the lock.
void ww_mutex_unlock(struct ww_mutex *lock);
(6) After all locks are released, the fetch context is released.
void ww_acquire_fini(struct ww_acquire_ctx *ctx);
Here is an example. Note: call the function ww_ mutex_ When lock () fails to apply for a lock, it should first release the lock that has been acquired and then call the slow path function ww_. mutex_ lock_ Slow () gets the lock that is competing, and finally gets the other locks. When applying for locks again, you must change the application order, because if you apply for locks in the original order, you will get back the newly released locks.
/* Step 1: define the lock class and specify the injury waiting algorithm.*/ static DEFINE_WW_CLASS(ww_class); struct obj { struct ww_mutex lock; /* obj data */ }; struct obj_entry { struct list_head head; struct obj *obj; }; int lock_objs(struct list_head *list, struct ww_acquire_ctx *ctx) { struct obj *res_obj = NULL; struct obj_entry *contended_entry = NULL; struct obj_entry *entry; int ret; /* Step 2: initialize the fetch context.*/ ww_acquire_init(ctx, &ww_class); /* Step 3: acquire the lock.*/ retry: list_for_each_entry(entry, list, head) { if (entry->obj == res_obj) { res_obj = NULL; continue; } ret = ww_mutex_lock(&entry->obj->lock, ctx); if (ret < 0) { contended_entry = entry; goto err; } } /* Step 4: mark the end of the acquisition phase.*/ ww_acquire_done(ctx); return 0; err: /* Roll back and release the acquired lock.*/ list_for_each_entry_continue_reverse(entry, list, head) { ww_mutex_unlock(&entry->obj->lock); } if (res_obj) { ww_mutex_unlock(&res_obj->lock); } if (ret == -EDEADLK) { /* Use the slow path lock acquisition function to obtain the competing lock.*/ ww_mutex_lock_slow(&contended_entry->obj->lock, ctx); res_obj = contended_entry->obj; /* Obtain other locks.*/ goto retry; } ww_acquire_fini(ctx); return ret; } void unlock_objs(struct list_head *list, struct ww_acquire_ctx *ctx) { struct obj_entry *entry; /* Step 5: release the lock.*/ list_for_each_entry (entry, list, head) { ww_mutex_unlock(&entry->obj->lock); } /* Step 6: release the fetch context.*/ ww_acquire_fini(ctx); }