Directory Locking¶
Locking scheme used for directory operations is based on twokinds of locks - per-inode (->i_rwsem) and per-filesystem(->s_vfs_rename_mutex).
When taking the i_rwsem on multiple non-directory objects, wealways acquire the locks in order by increasing address. We’ll callthat “inode pointer” order in the following.
Primitives¶
For our purposes all operations fall in 6 classes:
read access. Locking rules:
lock the directory we are accessing (shared)
object creation. Locking rules:
lock the directory we are accessing (exclusive)
object removal. Locking rules:
lock the parent (exclusive)
find the victim
lock the victim (exclusive)
link creation. Locking rules:
lock the parent (exclusive)
check that the source is not a directory
lock the source (exclusive; probably could be weakened to shared)
rename that is _not_ cross-directory. Locking rules:
lock the parent (exclusive)
find the source and target
decide which of the source and target need to be locked.The source needs to be locked if it’s a non-directory, target - if it’sa non-directory or about to be removed.
take the locks that need to be taken (exclusive), in inode pointer orderif need to take both (that can happen only when both source and targetare non-directories - the source because it wouldn’t need to be lockedotherwise and the target because mixing directory and non-directory isallowed only with RENAME_EXCHANGE, and that won’t be removing the target).
cross-directory rename. The trickiest in the whole bunch. Locking rules:
lock the filesystem
if the parents don’t have a common ancestor, fail the operation.
lock the parents in “ancestors first” order (exclusive). If neither is anancestor of the other, lock the parent of source first.
find the source and target.
verify that the source is not a descendent of the target andtarget is not a descendent of source; fail the operation otherwise.
lock the subdirectories involved (exclusive), source before target.
lock the non-directories involved (exclusive), in inode pointer order.
The rules above obviously guarantee that all directories that are goingto be read, modified or removed by method will be locked by the caller.
Splicing¶
There is one more thing to consider - splicing. It’s not an operationin its own right; it may happen as part of lookup. We speak of theoperations on directory trees, but we obviously do not have the fullpicture of those - especially for network filesystems. What we haveis a bunch of subtrees visible in dcache and locking happens on those.Trees grow as we do operations; memory pressure prunes them. Normallythat’s not a problem, but there is a nasty twist - what should we dowhen one growing tree reaches the root of another? That can happen inseveral scenarios, starting from “somebody mounted two nested subtreesfrom the same NFS4 server and doing lookups in one of them has reachedthe root of another”; there’s also open-by-fhandle stuff, and there’s apossibility that directory we see in one place gets moved by the serverto another and we run into it when we do a lookup.
For a lot of reasons we want to have the same directory present in dcacheonly once. Multiple aliases are not allowed. So when lookup runs intoa subdirectory that already has an alias, something needs to be done withdcache trees. Lookup is already holding the parent locked. If alias isa root of separate tree, it gets attached to the directory we are doing alookup in, under the name we’d been looking for. If the alias is alreadya child of the directory we are looking in, it changes name to the onewe’d been looking for. No extra locking is involved in these two cases.However, if it’s a child of some other directory, the things get trickier.First of all, we verify that it isnot an ancestor of our directoryand fail the lookup if it is. Then we try to lock the filesystem and thecurrent parent of the alias. If either trylock fails, we fail the lookup.If trylocks succeed, we detach the alias from its current parent andattach to our directory, under the name we are looking for.
Note that splicing doesnot involve any modification of the filesystem;all we change is the view in dcache. Moreover, holding a directory lockedexclusive prevents such changes involving its children and holding thefilesystem lock prevents any changes of tree topology, other than having aroot of one tree becoming a child of directory in another. In particular,if two dentries have been found to have a common ancestor after takingthe filesystem lock, their relationship will remain unchanged untilthe lock is dropped. So from the directory operations’ point of viewsplicing is almost irrelevant - the only place where it matters is onestep in cross-directory renames; we need to be careful when checking ifparents have a common ancestor.
Multiple-filesystem stuff¶
For some filesystems a method can involve a directory operation onanother filesystem; it may be ecryptfs doing operation in the underlyingfilesystem, overlayfs doing something to the layers, network filesystemusing a local one as a cache, etc. In all such cases the operationson other filesystems must follow the same locking rules. Moreover, “adirectory operation on this filesystem might involve directory operationson that filesystem” should be an asymmetric relation (or, if you will,it should be possible to rank the filesystems so that directory operationon a filesystem could trigger directory operations only on higher-rankedones - in these terms overlayfs ranks lower than its layers, networkfilesystem ranks lower than whatever it caches on, etc.)
Deadlock avoidance¶
If no directory is its own ancestor, the scheme above is deadlock-free.
Proof:
There is a ranking on the locks, such that all primitives takethem in order of non-decreasing rank. Namely,
rank ->i_rwsem of non-directories on given filesystem in inode pointerorder.
put ->i_rwsem of all directories on a filesystem at the same rank,lower than ->i_rwsem of any non-directory on the same filesystem.
put ->s_vfs_rename_mutex at rank lower than that of any ->i_rwsemon the same filesystem.
among the locks on different filesystems use the relativerank of those filesystems.
For example, if we have NFS filesystem caching on a local one, we have
->s_vfs_rename_mutex of NFS filesystem
->i_rwsem of directories on that NFS filesystem, same rank for all
->i_rwsem of non-directories on that filesystem, in order ofincreasing address of inode
->s_vfs_rename_mutex of local filesystem
->i_rwsem of directories on the local filesystem, same rank for all
->i_rwsem of non-directories on local filesystem, in order ofincreasing address of inode.
It’s easy to verify that operations never take a lock with ranklower than that of an already held lock.
Suppose deadlocks are possible. Consider the minimal deadlockedset of threads. It is a cycle of several threads, each blocked on a lockheld by the next thread in the cycle.
Since the locking order is consistent with the ranking, allcontended locks in the minimal deadlock will be of the same rank,i.e. they all will be ->i_rwsem of directories on the same filesystem.Moreover, without loss of generality we can assume that all operationsare done directly to that filesystem and none of them has actuallyreached the method call.
In other words, we have a cycle of threads, T1,..., Tn,and the same number of directories (D1,...,Dn) such that
T1 is blocked on D1 which is held by T2
T2 is blocked on D2 which is held by T3
...
Tn is blocked on Dn which is held by T1.
Each operation in the minimal cycle must have locked at leastone directory and blocked on attempt to lock another. That leavesonly 3 possible operations: directory removal (locks parent, thenchild), same-directory rename killing a subdirectory (ditto) andcross-directory rename of some sort.
There must be a cross-directory rename in the set; indeed,if all operations had been of the “lock parent, then child” sortwe would have Dn a parent of D1, which is a parent of D2, which isa parent of D3, ..., which is a parent of Dn. Relationships couldn’thave changed since the moment directory locks had been acquired,so they would all hold simultaneously at the deadlock time andwe would have a loop.
Since all operations are on the same filesystem, there can’t bemore than one cross-directory rename among them. Without loss ofgenerality we can assume that T1 is the one doing a cross-directoryrename and everything else is of the “lock parent, then child” sort.
In other words, we have a cross-directory rename that lockedDn and blocked on attempt to lock D1, which is a parent of D2, which isa parent of D3, ..., which is a parent of Dn. Relationships betweenD1,...,Dn all hold simultaneously at the deadlock time. Moreover,cross-directory rename does not get to locking any directories until ithas acquired filesystem lock and verified that directories involved havea common ancestor, which guarantees that ancestry relationships betweenall of them had been stable.
Consider the order in which directories are locked by thecross-directory rename; parents first, then possibly their children.Dn and D1 would have to be among those, with Dn locked before D1.Which pair could it be?
It can’t be the parents - indeed, since D1 is an ancestor of Dn,it would be the first parent to be locked. Therefore at least one of thechildren must be involved and thus neither of them could be a descendentof another - otherwise the operation would not have progressed pastlocking the parents.
It can’t be a parent and its child; otherwise we would’ve hada loop, since the parents are locked before the children, so the parentwould have to be a descendent of its child.
It can’t be a parent and a child of another parent either.Otherwise the child of the parent in question would’ve been a descendentof another child.
That leaves only one possibility - namely, both Dn and D1 areamong the children, in some order. But that is also impossible, sinceneither of the children is a descendent of another.
That concludes the proof, since the set of operations with theproperties required for a minimal deadlock can not exist.
Note that the check for having a common ancestor in cross-directoryrename is crucial - without it a deadlock would be possible. Indeed,suppose the parents are initially in different trees; we would lock theparent of source, then try to lock the parent of target, only to havean unrelated lookup splice a distant ancestor of source to some distantdescendent of the parent of target. At that point we have cross-directoryrename holding the lock on parent of source and trying to lock itsdistant ancestor. Add a bunch ofrmdir() attempts on all directoriesin between (all of those would fail with -ENOTEMPTY, had they ever gottenthe locks) and voila - we have a deadlock.
Loop avoidance¶
These operations are guaranteed to avoid loop creation. Indeed,the only operation that could introduce loops is cross-directory rename.Suppose after the operation there is a loop; since there hadn’t been suchloops before the operation, at least on of the nodes in that loop must’vehad its parent changed. In other words, the loop must be passing throughthe source or, in case of exchange, possibly the target.
Since the operation has succeeded, neither source nor target could havebeen ancestors of each other. Therefore the chain of ancestors startingin the parent of source could not have passed through the target andvice versa. On the other hand, the chain of ancestors of any node couldnot have passed through the node itself, or we would’ve had a loop beforethe operation. But everything other than source and target has keptthe parent after the operation, so the operation does not change thechains of ancestors of (ex-)parents of source and target. In particular,those chains must end after a finite number of steps.
Now consider the loop created by the operation. It passes through eithersource or target; the next node in the loop would be the ex-parent oftarget or source resp. After that the loop would follow the chain ofancestors of that parent. But as we have just shown, that chain mustend after a finite number of steps, which means that it can’t be a partof any loop. Q.E.D.
While this locking scheme works for arbitrary DAGs, it relies onability to check that directory is a descendent of another object. Currentimplementation assumes that directory graph is a tree. This assumption isalso preserved by all operations (cross-directory rename on a tree that wouldnot introduce a cycle will leave it a tree andlink() fails for directories).
Notice that “directory” in the above == “anything that might havechildren”, so if we are going to introduce hybrid objects we will needeither to make sure that link(2) doesn’t work for them or to make changesinis_subdir() that would make it work even in presence of such beasts.