Hello, I rewrote the document so that the need of crossrelease feature can be described logically. I think it's more important than to post specific implementation. Could you let me know your opinions about this? Thanks, Byungchul ----->8----- Crossrelease ============ Started by Byungchul Park <byungchul.park@xxxxxxx> Contents: (*) Background. - What causes deadlock. - What lockdep detects. - How lockdep works. (*) Limitation. - Limit to typical lock. - Pros from the limitation. - Cons from the limitation. (*) Generalization. - Relax the limitation. (*) Crossrelease. - Introduce crossrelease. - Introduce commit. (*) Implementation. - Data structures. - How crossrelease works. (*) Optimizations. - Avoid duplication. - Avoid lock contention. ========== Background ========== What causes deadlock -------------------- A deadlock occurs when a context is waiting for an event to be issued which cannot be issued because the context or another context who can issue the event is also waiting for an event to be issued which cannot be issued. Single context or more than one context both waiting for an event and issuing an event may paricipate in a deadlock. For example, A context who can issue event D is waiting for event A to be issued. A context who can issue event A is waiting for event B to be issued. A context who can issue event B is waiting for event C to be issued. A context who can issue event C is waiting for event D to be issued. A deadlock occurs when these four operations are run at a time because event D cannot be issued if event A isn't issued which in turn cannot be issued if event B isn't issued which in turn cannot be issued if event C isn't issued which in turn cannot be issued if event D isn't issued. No event can be issued since any of them never meets its precondition. We can easily recognize that each wait operation creates a dependency between two issuings e.g. between issuing D and issuing A like, 'event D cannot be issued if event A isn't issued', in other words, 'issuing event D depends on issuing event A'. So the whole example can be rewritten in terms of dependency, Do an operation making 'event D cannot be issued if event A isn't issued'. Do an operation making 'event A cannot be issued if event B isn't issued'. Do an operation making 'event B cannot be issued if event C isn't issued'. Do an operation making 'event C cannot be issued if event D isn't issued'. or, Do an operation making 'issuing event D depends on issuing event A'. Do an operation making 'issuing event A depends on issuing event B'. Do an operation making 'issuing event B depends on issuing event C'. Do an operation making 'issuing event C depends on issuing event D'. What causes a deadlock is a set of dependencies a chain of which forms a cycle, which means that issuing event D depending on issuing event A depending on issuing event B depending on issuing event C depending on issuing event D, finally depends on issuing event D itself, which means no event can be issued. Any set of operations creating dependencies causes a deadlock. The set of lock operations e.g. acquire and release is an example. Waiting for a lock to be released corresponds to waiting for an event and releasing a lock corresponds to issuing an event. So the description of dependency above can be altered to one in terms of lock. In terms of event, issuing event A depends on issuing event B if, Event A cannot be issued if event B isn't issued. In terms of lock, releasing lock A depends on releasing lock B if, Lock A cannot be released if lock B isn't released. CONCLUSION A set of dependencies a chain of which forms a cycle, causes a deadlock, no matter what creates the dependencies. What lockdep detects -------------------- A deadlock actually occurs only when all operations creating problematic dependencies are run at a time. However, even if it has not happend, the deadlock potentially can occur if the problematic dependencies obviously exist. Thus it's meaningful to detect not only an actual deadlock but also its possibility. Lockdep does the both. Whether a deadlock actually occurs or not depends on several factors, which means a deadlock may not occur even though problematic dependencies exist. For example, what order contexts are switched in is a factor. A deadlock will occur when contexts are switched so that all operations causing a deadlock become run simultaneously. Lockdep tries to detect a deadlock or its possibility aggressively, though it also tries to avoid false positive detections. So lockdep is designed to consider all possible combinations of dependencies so that it can detect all potential possibilities of deadlock in advance. What lockdep tries in order to consider all possibilities are, 1. Use a global dependency graph including all dependencies. What lockdep checks is based on dependencies instead of what actually happened. So no matter which context or call path a new dependency is detected in, it's just referred to as a global factor. 2. Use lock classes than lock instances when checking dependencies. What actually causes a deadlock is lock instances. However, lockdep uses lock classes than its instances when checking dependencies since any instance of a same lock class can be altered anytime. So lockdep detects both an actual deadlock and its possibility. But the latter is more valuable than the former. When a deadlock actually occures, we can identify what happens in the system by some means or other even without lockdep. However, there's no way to detect possiblity without lockdep unless the whole code is parsed in head. It's terrible. CONCLUSION Lockdep does, the fisrt one is more valuable, 1. Detecting and reporting deadlock possibility. 2. Detecting and reporting a deadlock actually occured. How lockdep works ----------------- What lockdep should do, to detect a deadlock or its possibility are, 1. Detect a new dependency created. 2. Keep the dependency in a global data structure esp. graph. 3. Check if any of all possible chains of dependencies forms a cycle. 4. Report a deadlock or its possibility if a cycle is detected. A graph used by lockdep to keep all dependencies looks like, A -> B - -> F -> G \ / -> E - -> L / \ / C -> D - -> H - \ -> I -> K / J - where A, B,..., L are different lock classes. Lockdep adds a dependency into graph when a new dependency is detected. For example, it adds a dependency 'A -> B' when a dependency between releasing lock A and releasing lock B, which has not been added yet, is detected. It does same thing on other dependencies, too. See 'What causes deadlock' section. NOTE: Precisely speaking, a dependency is one between releasing a lock and releasing another lock as described in 'What causes deadlock' section. However from now on, we will describe a dependency as if it's one between a lock and another lock for simplicity. Then 'A -> B' can be described as a dependency between lock A and lock B. We already checked how a problematic set of dependencies causes a deadlock in 'What causes deadlock' section. This time let's check if a deadlock or its possibility can be detected using a problematic set of dependencies. Assume that 'A -> B', 'B -> E' and 'E -> A' were added in the sequence into graph. Then the graph finally will be, -> A -> B -> E - / \ \ / ---------------- where A, B and E are different lock classes. >From adding three dependencies, a cycle was created which means, by definition of dependency, the situation 'lock E must be released to release lock B which in turn must be released to release lock A which in turn must be released to release lock E which in turn must be released to release B and so on infinitely' can happen. Once the situation happens, no lock can be released since any of them can never meet each precondition. It's a deadlock. Lockdep can detect a deadlock or its possibility with checking if a cycle was created after adding each dependency into graph. This is how lockdep detects a deadlock or its possibility. CONCLUSION Lockdep detects a deadlock or its possibility with checking if a cycle was created after adding each dependency into graph. ========== Limitation ========== Limit to typical lock --------------------- Limiting what lockdep has to consider to only ones satisfying the following condition, the implementation of adding dependencies becomes simple while its capacity for detection becomes limited. Typical lock e.g. spin lock and mutex is the case. Let's check what pros and cons of it are, in next section. A lock should be released within the context holding the lock. CONCLUSION Limiting what lockdep has to consider to typical lock e.g. spin lock and mutex, the implmentation becomes simple while it has a limited capacity. Pros from the limitation ------------------------ Given the limitation, when acquiring a lock, any lock being in held_locks of the acquire context cannot be released if the lock to acquire was not released yet. Yes. It's the exact case to add a new dependency 'A -> B' into graph, where lock A represents each lock being in held_locks and lock B represents the lock to acquire. For example, only considering typical lock, PROCESS X -------------- acquire A acquire B -> add a dependency 'A -> B' acquire C -> add a dependency 'B -> C' release C release B release A where A, B and C are different lock classes. When acquiring lock A, there's nothing in held_locks of PROCESS X thus no dependency is added. When acquiring lock B, lockdep detects and adds a new dependency 'A -> B' between lock A being in held_locks and lock B. And when acquiring lock C, lockdep also adds another dependency 'B -> C' for same reason. They are added when acquiring each lock, simply. NOTE: Even though every lock being in held_locks depends on the lock to acquire, lockdep does not add all dependencies between them because all of them can be covered by other dependencies except one dependency between the lock on top of held_locks and the lock to acquire, which must be added. Besides, we can expect several advantages from the limitation. 1. Any lock being in held_locks cannot be released unconditionally if the context is stuck, thus we can easily identify a dependency when acquiring a lock. 2. Considering only locks being in local held_locks of a single context makes some races avoidable, even though it fails of course when modifying its global dependency graph. 3. To build a dependency graph, lockdep only needs to keep locks not released yet. However relaxing the limitation, it might need to keep even locks already released, additionally. See 'Crossrelease' section. CONCLUSION Given the limitation, the implementation becomes simple and efficient. Cons from the limitation ------------------------ Given the limitation, lockdep is applicable only to typical lock. For example, page lock for page access or completion for synchronization cannot play with lockdep having the limitation. However since page lock or completion also causes a deadlock, it would be better to detect a deadlock or its possibility even for them. Can we detect deadlocks below with lockdep having the limitation? Example 1: PROCESS X PROCESS Y -------------- -------------- mutext_lock A lock_page B lock_page B mutext_lock A // DEADLOCK unlock_page B mutext_unlock A mutex_unlock A unlock_page B where A and B are different lock classes. No, we cannot. Example 2: PROCESS X PROCESS Y PROCESS Z -------------- -------------- -------------- mutex_lock A lock_page B lock_page B mutext_lock A // DEADLOCK mutext_unlock A unlock_page B held by X unlock_page B mutex_unlock A where A and B are different lock classes. No, we cannot. Example 3: PROCESS X PROCESS Y -------------- -------------- mutex_lock A mutex_lock A mutex_unlock A wait_for_complete B // DEADLOCK complete B mutex_unlock A where A is a lock class and B is a completion variable. No, we cannot. CONCLUSION Given the limitation, lockdep cannot detect a deadlock or its possibility caused by page lock or completion. ============== Generalization ============== Relax the limitation -------------------- Detecting and adding new dependencies into graph is very important for lockdep to work because adding a dependency means adding a chance to check if it causes a deadlock. More dependencies lockdep adds, more throughly it can work. Therefore Lockdep has to do its best to add as many true dependencies as possible. Relaxing the limitation, lockdep can add additional dependencies since it makes lockdep deal with additional ones creating the dependencies e.g. page lock or completion, which might be released in any context. Even so, it needs to be noted that behaviors adding dependencies created by typical lock don't need to be changed at all. For example, only considering typical lock, lockdep builds a graph like, A -> B - -> F -> G \ / -> E - -> L / \ / C -> D - -> H - \ -> I -> K / J - where A, B,..., L are different lock classes, and upper case letters represent typical lock. After the relaxing, the graph will have additional dependencies like, A -> B - -> F -> G \ / -> E - -> L -> c / \ / C -> D - -> H - / \ a - -> I -> K / b -> J - where a, b, c, A, B,..., L are different lock classes, and upper case letters represent typical lock while lower case letters represent non-typical lock e.g. page lock and completion. However, it might suffer performance degradation since relaxing the limitation with which design and implementation of lockdep become efficient might introduce inefficiency inevitably. Each option, that is, strong detection or efficient detection has its pros and cons, thus the right of choice between two options should be given to users. Choosing efficient detection, lockdep only deals with locks satisfying, A lock should be released within the context holding the lock. Choosing strong detection, lockdep deals with any locks satisfying, A lock can be released in any context. In the latter, of course, some contexts are not allowed if they themselves cause a deadlock. For example, acquiring a lock in irq-safe context before releasing the lock in irq-unsafe context is not allowed, which after all ends in a cycle of a dependency chain, meaning a deadlock. Otherwise, any contexts are allowed to release it. CONCLUSION Relaxing the limitation, lockdep adds additional dependencies and gets additional chances to check if they cause a deadlock. It makes lockdep additionally deal with what might be released in any context. ============ Crossrelease ============ Introduce crossrelease ---------------------- To allow lockdep to add additional dependencies created by what might be released in any context, which we call 'crosslock', it's necessary to introduce a new feature which makes it possible to identify and add the dependencies. We call the feature 'crossrelease'. Crossrelease feature has to do, 1. Identify a new dependency created by crosslock. 2. Add the dependency into graph when identifying it. That's all. Once a meaningful dependency is added into graph, lockdep will work with the graph as it did. So the most important thing to do is to identify a dependency created by crosslock. Remind what a dependency is. For example, Lock A depends on lock B if 'lock A cannot be released if lock B isn't released'. See 'What causes deadlock' section. By definition, a lock depends on every lock having been added into held_locks in the lock's release context since the lock was acquired, because the lock cannot be released if the release context is stuck by any of dependent locks, not released. So lockdep should technically consider release contexts of locks to identify dependencies. It's no matter of course to typical lock because acquire context is same as release context for typical lock, which means lockdep would work with considering only acquire contexts for typical lock. However, for crosslock, lockdep cannot identify release context and any dependency until the crosslock will be actually released. Regarding crosslock, lockdep has to record all history by queueing all locks potentially creating dependencies so that real dependencies can be added using the history recorded when identifying release context. We call it 'commit', that is, to add dependencies in batches. See 'Introduce commit' section. Of course, some actual deadlocks caused by crosslock cannot be detected at the time it happened, because the deadlocks cannot be indentified and detected until the crosslock will be actually released. But this way deadlock possibility can be detected and it's worth just possibility detection of deadlock. See 'What lockdep does' section. CONCLUSION With crossrelease feature, lockdep can works with what might be released in any context, namely, crosslock. Introduce commit ---------------- Crossrelease feature names it 'commit' to identify and add dependencies into graph in batches. Lockdep is already doing what commit does when acquiring a lock, for typical lock. However, that way must be changed for crosslock so that it identifies the crosslock's release context first and then does commit. The main reason why lockdep performs additional step, namely commit, for crosslock is that some dependencies by crosslock cannot be identified until the crosslock's release context is eventually identified, though some other dependencies by crosslock can. There are four kinds of dependencies to consider. 1. 'typical lock A -> typical lock B' dependency Just when acquiring lock B, lockdep can identify the dependency between lock A and lock B as it did. Commit is unnecessary. 2. 'typical lock A -> crosslock b' dependency Just when acquiring crosslock b, lockdep can identify the dependency between lock A and crosslock B as well. Commit is unnecessary, too. 3. 'crosslock a -> typical lock B' dependency When acquiring lock B, lockdep cannot identify the dependency. It can be identified only when crosslock a is released. Commit is necessary. 4. 'crosslock a -> crosslock b' dependency Creating this kind of dependency directly is unnecessary since it can be covered by other kinds of dependencies. Lockdep works without commit during dealing with only typical locks. However, it needs to perform commit step, once at least one crosslock is acquired, until all crosslocks in progress are released. Introducing commit, lockdep performs three steps i.e. acquire, commit and release. What lockdep should do in each step is like, 1. Acquire 1) For typical lock Lockdep does what it originally does and queues the lock so that lockdep can check dependencies using it at commit step. 2) For crosslock The crosslock is added to a global linked list so that lockdep can check dependencies using it at commit step. 2. Commit 1) For typical lock N/A. 2) For crosslock Lockdep checks and adds dependencies using data saved at acquire step, as if the dependencies were added without commit step. 3. Release 1) For typical lock No change. 2) For crosslock Lockdep just remove the crosslock from the global linked list, to which it was added at acquire step. CONCLUSION Lockdep can detect a deadlock or its possibility caused by what might be released in any context, using commit step, where it checks and adds dependencies in batches. ============== Implementation ============== Data structures --------------- Crossrelease feature introduces two main data structures. 1. pend_lock (or plock) This is an array embedded in task_struct, for keeping locks queued so that real dependencies can be added using them at commit step. So this data can be accessed locklessly within the owner context. The array is filled when acquiring a typical lock and consumed when doing commit. And it's managed in circular manner. 2. cross_lock (or xlock) This is a global linked list, for keeping all crosslocks in progress. The list grows when acquiring a crosslock and is shrunk when releasing the crosslock. lockdep_init_map_crosslock() should be used to initialize a crosslock instance instead of lockdep_init_map() so that lockdep can recognize it as crosslock. CONCLUSION Crossrelease feature uses two main data structures. 1. A pend_lock array for queueing typical locks in circular manner. 2. A cross_lock linked list for managing crosslocks in progress. How crossrelease works ---------------------- Let's take look at how crossrelease feature works step by step, starting from how lockdep works without crossrelease feaure. For example, the below is how lockdep works for typical lock. RELEASE CONTEXT of A (= ACQUIRE CONTEXT of A) -------------------- acquire A acquire B -> add a dependency 'A -> B' acquire C -> add a dependency 'B -> C' release C release B release A where A, B and C are different lock classes, and upper case letters represent typical lock. After adding 'A -> B', the dependency graph will be, A -> B where A and B are different lock classes, and upper case letters represent typical lock. And after adding 'B -> C', the graph will be, A -> B -> C where A, B and C are different lock classes, and upper case letters represent typical lock. What if applying commit on typical locks? It's not necessary for typical lock. Just for showing what commit does. RELEASE CONTEXT of A (= ACQUIRE CONTEXT of A) -------------------- acquire A -> mark A as started (nothing before, no queueing) acquire B -> mark B as started and queue B acquire C -> mark C as started and queue C release C -> commit C (nothing queued since C started) release B -> commit B -> add a dependency 'B -> C' release A -> commit A -> add dependencies 'A -> B' and 'A -> C' where A, B and C are different lock classes, and upper case letters represent typical lock. After doing commit A, B and C, the dependency graph becomes like, A -> B -> C where A, B and C are different lock classes, and upper case letters represent typical lock. NOTE: A dependency 'A -> C' is optimized out. Here we can see the final graph is same as the graph built without commit. Of course the former way leads to finish building the graph earlier than the latter way, which means we can detect a deadlock or its possibility sooner. So the former way would be prefered if possible. But we cannot avoid using the latter way using commit, for crosslock. Let's look at how commit works for crosslock. RELEASE CONTEXT of a ACQUIRE CONTEXT of a -------------------- -------------------- acquire a -> mark a as started (serialized by some means e.g. barrier) acquire D -> queue D acquire B -> queue B release D acquire C -> add 'B -> C' and queue C acquire E -> queue E acquire D -> add 'C -> D' and queue D release E release D release a -> commit a -> add 'a -> D' and 'a -> E' release C release B where a, B,..., E are different lock classes, and upper case letters represent typical lock while lower case letters represent crosslock. When acquiring crosslock a, no dependency can be added since there's nothing in the held_locks. However, crossrelease feature marks the crosslock as started, which means all locks to acquire from now are candidates which might create new dependencies later when identifying release context. When acquiring lock B, lockdep does what it originally does for typical lock and additionally queues the lock for later commit to refer to because it might be a dependent lock of the crosslock. It does same thing on lock C, D and E. And then two dependencies 'a -> D' and 'a -> E' are added when identifying the release context, at commit step. The final graph is, with crossrelease feature using commit, B -> C - \ -> D / a - \ -> E where a, B,..., E are different lock classes, and upper case letters represent typical lock while lower case letters represent crosslock. However, without crossrelease feature, the final graph will be, B -> C -> D where B and C are different lock classes, and upper case letters represent typical lock. The former graph has two more dependencies 'a -> D' and 'a -> E' giving additional chances to check if they cause a deadlock. This way lockdep can detect a deadlock or its possibility caused by crosslock. Again, behaviors adding dependencies created by only typical locks are not changed at all. CONCLUSION Crossrelease works using commit for crosslock, leaving behaviors adding dependencies between only typical locks unchanged. ============= Optimizations ============= Avoid duplication ----------------- Crossrelease feature uses a cache like what lockdep already uses for dependency chains, but this time it's for caching one dependency like 'crosslock -> typical lock' crossing between two different context. Once that dependency is cached, same dependency will never be added any more. Even queueing unnecessary locks is also prevented based on the cache. CONCLUSION Crossrelease does not add any duplicate dependency. Avoid lock contention --------------------- To keep all typical locks for later use, crossrelease feature adopts a local array embedded in task_struct, which makes accesses to arrays lockless by forcing each array to be accessed only within each own context. It's like how held_locks is accessed. Lockless implmentation is important since typical locks are very frequently accessed. CONCLUSION Crossrelease avoids lock contection as far as possible. -- To unsubscribe, send a message with 'unsubscribe linux-mm' in the body to majordomo@xxxxxxxxx. For more info on Linux MM, see: http://www.linux-mm.org/ . Don't email: <a href=mailto:"dont@xxxxxxxxx"> email@xxxxxxxxx </a>