← Documents Documentation/filesystems/directory-locking.rst GitHub 원문 ↗

Linux 6.18.37 · Filesystems

Directory Locking

VFS directory operation의 lock 순서, dcache splicing, cross-filesystem rank, deadlock·loop 회피 증명을 다룬 전문 번역입니다.

Source pathDocumentation/filesystems/directory-locking.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약·해설

directory-locking.rst:1-286

VFS directory locking은 filesystem rename mutex, directory rwsem, non-directory rwsem을 일정한 rank로 획득합니다. cross-directory rename은 공통 ancestor 확인, ancestor-first parent locking, source-first child locking, inode pointer order를 결합해 deadlock과 directory loop를 방지합니다.

Directory lock 전체 순서
필요하면 filesystem `->s_vfs_rename_mutex` 획득공통 ancestor와 상호 descendant 관계 검증parent directory를 ancestor-first로 잠금subdirectory를 source-first로 잠금non-directory를 inode pointer order로 잠금operation 완료 후 역순 해제

복잡한 rename에서도 감소하지 않는 rank를 지키는 핵심 순서입니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 =================
2 Directory Locking
3 =================
4
5
6 Locking scheme used for directory operations is based on two
7 kinds of locks - per-inode (->i_rwsem) and per-filesystem
8 (->s_vfs_rename_mutex).
9
10 When taking the i_rwsem on multiple non-directory objects, we
11 always acquire the locks in order by increasing address. We'll call
12 that "inode pointer" order in the following.
13
14
15 Primitives
16 ==========
17
18 For our purposes all operations fall in 6 classes:
19
20 1. read access. Locking rules:
21
22 * lock the directory we are accessing (shared)
23
24 2. object creation. Locking rules:
25
26 * lock the directory we are accessing (exclusive)
27
28 3. object removal. Locking rules:
29
30 * lock the parent (exclusive)
31 * find the victim
32 * lock the victim (exclusive)
33
34 4. link creation. Locking rules:
35
36 * lock the parent (exclusive)
37 * check that the source is not a directory
38 * lock the source (exclusive; probably could be weakened to shared)
39
40 5. rename that is _not_ cross-directory. Locking rules:
41
42 * lock the parent (exclusive)
43 * find the source and target
44 * decide which of the source and target need to be locked.
45 The source needs to be locked if it's a non-directory, target - if it's
46 a non-directory or about to be removed.
47 * take the locks that need to be taken (exclusive), in inode pointer order
48 if need to take both (that can happen only when both source and target
49 are non-directories - the source because it wouldn't need to be locked
50 otherwise and the target because mixing directory and non-directory is
51 allowed only with RENAME_EXCHANGE, and that won't be removing the target).
52
53 6. cross-directory rename. The trickiest in the whole bunch. Locking rules:
54
55 * lock the filesystem
56 * if the parents don't have a common ancestor, fail the operation.
57 * lock the parents in "ancestors first" order (exclusive). If neither is an
58 ancestor of the other, lock the parent of source first.
59 * find the source and target.
60 * verify that the source is not a descendent of the target and
61 target is not a descendent of source; fail the operation otherwise.
62 * lock the subdirectories involved (exclusive), source before target.
63 * lock the non-directories involved (exclusive), in inode pointer order.
64
65 The rules above obviously guarantee that all directories that are going
66 to be read, modified or removed by method will be locked by the caller.
67
68
69 Splicing
70 ========
71
72 There is one more thing to consider - splicing. It's not an operation
73 in its own right; it may happen as part of lookup. We speak of the
74 operations on directory trees, but we obviously do not have the full
75 picture of those - especially for network filesystems. What we have
76 is a bunch of subtrees visible in dcache and locking happens on those.
77 Trees grow as we do operations; memory pressure prunes them. Normally
78 that's not a problem, but there is a nasty twist - what should we do
79 when one growing tree reaches the root of another? That can happen in
80 several scenarios, starting from "somebody mounted two nested subtrees
81 from the same NFS4 server and doing lookups in one of them has reached
82 the root of another"; there's also open-by-fhandle stuff, and there's a
83 possibility that directory we see in one place gets moved by the server
84 to another and we run into it when we do a lookup.
85
86 For a lot of reasons we want to have the same directory present in dcache
87 only once. Multiple aliases are not allowed. So when lookup runs into
88 a subdirectory that already has an alias, something needs to be done with
89 dcache trees. Lookup is already holding the parent locked. If alias is
90 a root of separate tree, it gets attached to the directory we are doing a
91 lookup in, under the name we'd been looking for. If the alias is already
92 a child of the directory we are looking in, it changes name to the one
93 we'd been looking for. No extra locking is involved in these two cases.
94 However, if it's a child of some other directory, the things get trickier.
95 First of all, we verify that it is *not* an ancestor of our directory
96 and fail the lookup if it is. Then we try to lock the filesystem and the
97 current parent of the alias. If either trylock fails, we fail the lookup.
98 If trylocks succeed, we detach the alias from its current parent and
99 attach to our directory, under the name we are looking for.
100
101 Note that splicing does *not* involve any modification of the filesystem;
102 all we change is the view in dcache. Moreover, holding a directory locked
103 exclusive prevents such changes involving its children and holding the
104 filesystem lock prevents any changes of tree topology, other than having a
105 root of one tree becoming a child of directory in another. In particular,
106 if two dentries have been found to have a common ancestor after taking
107 the filesystem lock, their relationship will remain unchanged until
108 the lock is dropped. So from the directory operations' point of view
109 splicing is almost irrelevant - the only place where it matters is one
110 step in cross-directory renames; we need to be careful when checking if
111 parents have a common ancestor.
112
113
114 Multiple-filesystem stuff
115 =========================
116
117 For some filesystems a method can involve a directory operation on
118 another filesystem; it may be ecryptfs doing operation in the underlying
119 filesystem, overlayfs doing something to the layers, network filesystem
120 using a local one as a cache, etc. In all such cases the operations
121 on other filesystems must follow the same locking rules. Moreover, "a
122 directory operation on this filesystem might involve directory operations
123 on that filesystem" should be an asymmetric relation (or, if you will,
124 it should be possible to rank the filesystems so that directory operation
125 on a filesystem could trigger directory operations only on higher-ranked
126 ones - in these terms overlayfs ranks lower than its layers, network
127 filesystem ranks lower than whatever it caches on, etc.)
128
129
130 Deadlock avoidance
131 ==================
132
133 If no directory is its own ancestor, the scheme above is deadlock-free.
134
135 Proof:
136
137 There is a ranking on the locks, such that all primitives take
138 them in order of non-decreasing rank. Namely,
139
140 * rank ->i_rwsem of non-directories on given filesystem in inode pointer
141 order.
142 * put ->i_rwsem of all directories on a filesystem at the same rank,
143 lower than ->i_rwsem of any non-directory on the same filesystem.
144 * put ->s_vfs_rename_mutex at rank lower than that of any ->i_rwsem
145 on the same filesystem.
146 * among the locks on different filesystems use the relative
147 rank of those filesystems.
148
149 For example, if we have NFS filesystem caching on a local one, we have
150
151 1. ->s_vfs_rename_mutex of NFS filesystem
152 2. ->i_rwsem of directories on that NFS filesystem, same rank for all
153 3. ->i_rwsem of non-directories on that filesystem, in order of
154 increasing address of inode
155 4. ->s_vfs_rename_mutex of local filesystem
156 5. ->i_rwsem of directories on the local filesystem, same rank for all
157 6. ->i_rwsem of non-directories on local filesystem, in order of
158 increasing address of inode.
159
160 It's easy to verify that operations never take a lock with rank
161 lower than that of an already held lock.
162
163 Suppose deadlocks are possible. Consider the minimal deadlocked
164 set of threads. It is a cycle of several threads, each blocked on a lock
165 held by the next thread in the cycle.
166
167 Since the locking order is consistent with the ranking, all
168 contended locks in the minimal deadlock will be of the same rank,
169 i.e. they all will be ->i_rwsem of directories on the same filesystem.
170 Moreover, without loss of generality we can assume that all operations
171 are done directly to that filesystem and none of them has actually
172 reached the method call.
173
174 In other words, we have a cycle of threads, T1,..., Tn,
175 and the same number of directories (D1,...,Dn) such that
176
177 T1 is blocked on D1 which is held by T2
178
179 T2 is blocked on D2 which is held by T3
180
181 ...
182
183 Tn is blocked on Dn which is held by T1.
184
185 Each operation in the minimal cycle must have locked at least
186 one directory and blocked on attempt to lock another. That leaves
187 only 3 possible operations: directory removal (locks parent, then
188 child), same-directory rename killing a subdirectory (ditto) and
189 cross-directory rename of some sort.
190
191 There must be a cross-directory rename in the set; indeed,
192 if all operations had been of the "lock parent, then child" sort
193 we would have Dn a parent of D1, which is a parent of D2, which is
194 a parent of D3, ..., which is a parent of Dn. Relationships couldn't
195 have changed since the moment directory locks had been acquired,
196 so they would all hold simultaneously at the deadlock time and
197 we would have a loop.
198
199 Since all operations are on the same filesystem, there can't be
200 more than one cross-directory rename among them. Without loss of
201 generality we can assume that T1 is the one doing a cross-directory
202 rename and everything else is of the "lock parent, then child" sort.
203
204 In other words, we have a cross-directory rename that locked
205 Dn and blocked on attempt to lock D1, which is a parent of D2, which is
206 a parent of D3, ..., which is a parent of Dn. Relationships between
207 D1,...,Dn all hold simultaneously at the deadlock time. Moreover,
208 cross-directory rename does not get to locking any directories until it
209 has acquired filesystem lock and verified that directories involved have
210 a common ancestor, which guarantees that ancestry relationships between
211 all of them had been stable.
212
213 Consider the order in which directories are locked by the
214 cross-directory rename; parents first, then possibly their children.
215 Dn and D1 would have to be among those, with Dn locked before D1.
216 Which pair could it be?
217
218 It can't be the parents - indeed, since D1 is an ancestor of Dn,
219 it would be the first parent to be locked. Therefore at least one of the
220 children must be involved and thus neither of them could be a descendent
221 of another - otherwise the operation would not have progressed past
222 locking the parents.
223
224 It can't be a parent and its child; otherwise we would've had
225 a loop, since the parents are locked before the children, so the parent
226 would have to be a descendent of its child.
227
228 It can't be a parent and a child of another parent either.
229 Otherwise the child of the parent in question would've been a descendent
230 of another child.
231
232 That leaves only one possibility - namely, both Dn and D1 are
233 among the children, in some order. But that is also impossible, since
234 neither of the children is a descendent of another.
235
236 That concludes the proof, since the set of operations with the
237 properties required for a minimal deadlock can not exist.
238
239 Note that the check for having a common ancestor in cross-directory
240 rename is crucial - without it a deadlock would be possible. Indeed,
241 suppose the parents are initially in different trees; we would lock the
242 parent of source, then try to lock the parent of target, only to have
243 an unrelated lookup splice a distant ancestor of source to some distant
244 descendent of the parent of target. At that point we have cross-directory
245 rename holding the lock on parent of source and trying to lock its
246 distant ancestor. Add a bunch of rmdir() attempts on all directories
247 in between (all of those would fail with -ENOTEMPTY, had they ever gotten
248 the locks) and voila - we have a deadlock.
249
250 Loop avoidance
251 ==============
252
253 These operations are guaranteed to avoid loop creation. Indeed,
254 the only operation that could introduce loops is cross-directory rename.
255 Suppose after the operation there is a loop; since there hadn't been such
256 loops before the operation, at least on of the nodes in that loop must've
257 had its parent changed. In other words, the loop must be passing through
258 the source or, in case of exchange, possibly the target.
259
260 Since the operation has succeeded, neither source nor target could have
261 been ancestors of each other. Therefore the chain of ancestors starting
262 in the parent of source could not have passed through the target and
263 vice versa. On the other hand, the chain of ancestors of any node could
264 not have passed through the node itself, or we would've had a loop before
265 the operation. But everything other than source and target has kept
266 the parent after the operation, so the operation does not change the
267 chains of ancestors of (ex-)parents of source and target. In particular,
268 those chains must end after a finite number of steps.
269
270 Now consider the loop created by the operation. It passes through either
271 source or target; the next node in the loop would be the ex-parent of
272 target or source resp. After that the loop would follow the chain of
273 ancestors of that parent. But as we have just shown, that chain must
274 end after a finite number of steps, which means that it can't be a part
275 of any loop. Q.E.D.
276
277 While this locking scheme works for arbitrary DAGs, it relies on
278 ability to check that directory is a descendent of another object. Current
279 implementation assumes that directory graph is a tree. This assumption is
280 also preserved by all operations (cross-directory rename on a tree that would
281 not introduce a cycle will leave it a tree and link() fails for directories).
282
283 Notice that "directory" in the above == "anything that might have
284 children", so if we are going to introduce hybrid objects we will need
285 either to make sure that link(2) doesn't work for them or to make changes
286 in is_subdir() that would make it work even in presence of such beasts.
287

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

두 잠금 계층과 inode pointer 순서

1-14

directory operation의 locking scheme은 inode별 `->i_rwsem`과 filesystem별 `->s_vfs_rename_mutex`라는 두 종류의 lock에 기반합니다.

여러 non-directory object의 `i_rwsem`을 함께 잡을 때는 inode 주소가 증가하는 순서로 항상 획득합니다. 이 문서에서는 이를 `inode pointer` 순서라고 부릅니다.

Directory locking 계층
Lock범위순서
`->s_vfs_rename_mutex`filesystem 전체 rename topology같은 filesystem의 모든 `i_rwsem`보다 먼저
directory `->i_rwsem`개별 directoryancestor 우선 또는 operation 규칙
non-directory `->i_rwsem`개별 inodeinode pointer 주소 증가 순서

operation이 사용하는 두 lock 종류와 정렬 규칙입니다.

=================
Directory Locking
=================


Locking scheme used for directory operations is based on two
kinds of locks - per-inode (->i_rwsem) and per-filesystem
(->s_vfs_rename_mutex).

When taking the i_rwsem on multiple non-directory objects, we
always acquire the locks in order by increasing address.  We'll call
that "inode pointer" order in the following.

여섯 operation class의 잠금 규칙

15-68

읽기 접근은 접근할 directory를 shared로 잠급니다. 객체 생성은 접근할 directory를 exclusive로 잠급니다.

객체 제거는 parent를 exclusive로 잠근 뒤 victim을 찾고 victim도 exclusive로 잠급니다. link 생성은 parent를 exclusive로 잠그고 source가 directory가 아님을 확인한 다음 source를 exclusive로 잠급니다. source lock은 shared로 약화할 수 있을 가능성이 있습니다.

같은 directory 안의 rename은 parent를 exclusive로 잠그고 source와 target을 찾은 뒤 필요한 inode를 결정합니다. source가 non-directory이면 source lock이 필요하고, target이 non-directory이거나 제거될 예정이면 target lock이 필요합니다. 둘 다 잠글 때는 inode pointer 순서를 따릅니다.

directory와 non-directory의 혼합 rename은 `RENAME_EXCHANGE`에서만 허용되며, 이 경우 target을 제거하지 않습니다. 따라서 source와 target을 모두 잠그는 경우는 둘 다 non-directory인 때뿐입니다.

cross-directory rename은 가장 복잡합니다. filesystem lock을 잡고 parent들이 공통 ancestor를 갖는지 확인합니다. 없으면 실패합니다. parent는 ancestor 우선으로 exclusive lock하며 서로 ancestor 관계가 아니면 source parent를 먼저 잠급니다.

그 뒤 source와 target을 찾고 서로 descendant 관계가 아닌지 확인합니다. 관련 subdirectory는 source 다음 target 순서로 exclusive lock하고, 관련 non-directory는 inode pointer 순서로 exclusive lock합니다. 이 규칙은 method가 읽거나 수정하거나 제거할 모든 directory를 caller가 잠그도록 보장합니다.

여섯 operation의 lock 순서
Class잠금 순서
Readdirectory shared
Createdirectory exclusive
Removeparent exclusive → victim exclusive
Linkparent exclusive → source non-directory 확인 → source exclusive
Same-directory renameparent → 필요한 source·target을 inode pointer 순서
Cross-directory renamefilesystem → parents ancestor-first → subdirectories source-first → non-directories pointer order

VFS directory primitive별 최소 잠금 순서를 요약합니다.

Primitives
==========

For our purposes all operations fall in 6 classes:

1. read access.  Locking rules:

        * lock the directory we are accessing (shared)

2. object creation.  Locking rules:

        * lock the directory we are accessing (exclusive)

3. object removal.  Locking rules:

        * lock the parent (exclusive)
        * find the victim
        * lock the victim (exclusive)

4. 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)

5. 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's
          a non-directory or about to be removed.
        * take the locks that need to be taken (exclusive), in inode pointer order
          if need to take both (that can happen only when both source and target
          are non-directories - the source because it wouldn't need to be locked
          otherwise and the target because mixing directory and non-directory is
          allowed only with RENAME_EXCHANGE, and that won't be removing the target).

6. 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 an
          ancestor 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 and
          target 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 going
to be read, modified or removed by method will be locked by the caller.

dcache subtree splicing

69-112

splicing은 독립 operation이 아니라 lookup 도중 일어날 수 있는 dcache tree 조정입니다. 특히 network filesystem에서는 전체 directory tree가 아니라 dcache에 보이는 여러 subtree만 알고 있으며, lookup으로 tree가 자라고 memory pressure로 가지가 잘립니다.

한 tree의 성장이 다른 tree의 root에 닿는 경우가 있습니다. 같은 NFS4 server의 중첩 subtree를 각각 mount한 경우, open-by-fhandle, server가 directory를 다른 위치로 옮긴 뒤 lookup에서 다시 만나는 경우가 예입니다.

같은 directory가 dcache에 여러 alias로 존재하는 것은 허용하지 않습니다. lookup에서 이미 alias가 있는 subdirectory를 만나면 tree를 조정합니다. alias가 별도 tree의 root이면 현재 lookup directory 아래에 찾던 이름으로 붙이고, 이미 현재 directory의 child이면 찾던 이름으로 변경합니다. 이 두 경우에는 추가 locking이 없습니다.

alias가 다른 directory의 child이면 먼저 alias가 현재 directory의 ancestor가 아님을 확인하고, 그렇다면 lookup을 실패시킵니다. 다음으로 filesystem과 alias의 현재 parent를 trylock합니다. 하나라도 실패하면 lookup을 실패시키고, 둘 다 성공하면 alias를 이전 parent에서 떼어 현재 directory 아래에 찾던 이름으로 붙입니다.

splicing은 실제 filesystem을 수정하지 않고 dcache view만 바꿉니다. directory exclusive lock은 그 child를 포함하는 변경을 막고, filesystem lock은 한 tree의 root가 다른 tree directory의 child가 되는 경우를 제외한 topology 변경을 막습니다.

filesystem lock을 잡은 뒤 두 dentry의 공통 ancestor를 확인했다면 lock을 놓을 때까지 관계가 유지됩니다. 따라서 directory operation에서 splicing이 중요한 지점은 cross-directory rename에서 parent들의 공통 ancestor를 검사하는 단계뿐입니다.

dcache alias splicing
lookup 중 이미 alias가 있는 subdirectory 발견별도 tree root이면 현재 directory 아래에 attach현재 directory child이면 찾던 이름으로 rename다른 parent child이면 ancestor 여부 검사filesystem과 기존 parent를 trylock성공하면 detach 후 현재 lookup 위치에 attach

lookup이 기존 alias를 만났을 때 위치에 따라 처리하는 분기입니다.

Splicing
========

There is one more thing to consider - splicing.  It's not an operation
in its own right; it may happen as part of lookup.  We speak of the
operations on directory trees, but we obviously do not have the full
picture of those - especially for network filesystems.  What we have
is a bunch of subtrees visible in dcache and locking happens on those.
Trees grow as we do operations; memory pressure prunes them.  Normally
that's not a problem, but there is a nasty twist - what should we do
when one growing tree reaches the root of another?  That can happen in
several scenarios, starting from "somebody mounted two nested subtrees
from the same NFS4 server and doing lookups in one of them has reached
the root of another"; there's also open-by-fhandle stuff, and there's a
possibility that directory we see in one place gets moved by the server
to 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 dcache
only once.  Multiple aliases are not allowed.  So when lookup runs into
a subdirectory that already has an alias, something needs to be done with
dcache trees.  Lookup is already holding the parent locked.  If alias is
a root of separate tree, it gets attached to the directory we are doing a
lookup in, under the name we'd been looking for.  If the alias is already
a child of the directory we are looking in, it changes name to the one
we'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 is *not* an ancestor of our directory
and fail the lookup if it is.  Then we try to lock the filesystem and the
current parent of the alias.  If either trylock fails, we fail the lookup.
If trylocks succeed, we detach the alias from its current parent and
attach to our directory, under the name we are looking for.

Note that splicing does *not* involve any modification of the filesystem;
all we change is the view in dcache.  Moreover, holding a directory locked
exclusive prevents such changes involving its children and holding the
filesystem lock prevents any changes of tree topology, other than having a
root of one tree becoming a child of directory in another.  In particular,
if two dentries have been found to have a common ancestor after taking
the filesystem lock, their relationship will remain unchanged until
the lock is dropped.  So from the directory operations' point of view
splicing is almost irrelevant - the only place where it matters is one
step in cross-directory renames; we need to be careful when checking if
parents have a common ancestor.

여러 filesystem에 걸친 operation

113-128

eCryptfs가 underlying filesystem을 조작하거나 overlayfs가 layer를 조작하고 network filesystem이 local cache를 사용하는 것처럼, 한 filesystem method가 다른 filesystem의 directory operation을 일으킬 수 있습니다.

다른 filesystem에서 수행되는 operation도 같은 locking rule을 따라야 합니다. 또한 `이 filesystem의 directory operation이 저 filesystem의 directory operation을 유발할 수 있다`는 관계는 비대칭이어야 합니다.

동일한 뜻으로 filesystem에 rank를 부여해 한 filesystem의 operation이 더 높은 rank의 filesystem에서만 operation을 유발하도록 해야 합니다. overlayfs는 layer보다 낮고 network filesystem은 cache filesystem보다 낮은 rank를 갖습니다.

Cross-filesystem rank
낮은 rank의 virtual·network filesystem에서 operation 시작자체 filesystem lock과 inode lock 획득필요한 underlying·layer·cache filesystem 호출더 높은 rank의 lock만 추가 획득반대 방향 호출을 금지해 lock cycle 차단

stacked filesystem이 아래 계층으로만 lock을 확장하는 방향입니다.


Multiple-filesystem stuff
=========================

For some filesystems a method can involve a directory operation on
another filesystem; it may be ecryptfs doing operation in the underlying
filesystem, overlayfs doing something to the layers, network filesystem
using a local one as a cache, etc.  In all such cases the operations
on other filesystems must follow the same locking rules.  Moreover, "a
directory operation on this filesystem might involve directory operations
on that filesystem" should be an asymmetric relation (or, if you will,
it should be possible to rank the filesystems so that directory operation
on a filesystem could trigger directory operations only on higher-ranked
ones - in these terms overlayfs ranks lower than its layers, network
filesystem ranks lower than whatever it caches on, etc.)

Lock rank와 교착 증명의 전제

129-172

어떤 directory도 자기 자신의 ancestor가 아니라면 앞의 locking scheme은 deadlock-free입니다. 증명의 핵심은 모든 primitive가 감소하지 않는 rank 순서로 lock을 잡도록 rank를 정의할 수 있다는 점입니다.

같은 filesystem의 non-directory `->i_rwsem`은 inode pointer 순서로 rank를 매깁니다. 모든 directory `->i_rwsem`은 같은 rank이며 non-directory lock보다 낮습니다. `->s_vfs_rename_mutex`는 같은 filesystem의 모든 `->i_rwsem`보다 낮습니다. 서로 다른 filesystem의 lock에는 filesystem 간 상대 rank를 적용합니다.

예를 들어 local filesystem을 cache로 쓰는 NFS에서는 NFS rename mutex, NFS directory rwsem, NFS non-directory rwsem, local rename mutex, local directory rwsem, local non-directory rwsem 순입니다. 각 filesystem 안의 non-directory lock은 inode 주소 증가 순서를 따릅니다.

operation이 이미 잡은 lock보다 낮은 rank의 lock을 다시 잡지 않는다는 것은 쉽게 확인할 수 있습니다.

deadlock이 가능하다고 가정하고 최소 deadlock thread 집합을 고릅니다. 각 thread가 cycle의 다음 thread가 가진 lock을 기다리는 cycle입니다. rank 순서가 일관되므로 이 최소 cycle에서 경합하는 모든 lock은 같은 rank, 즉 같은 filesystem의 directory `->i_rwsem`이어야 합니다. 일반성을 잃지 않고 operation은 그 filesystem에 직접 수행되고 아직 method call에는 도달하지 않았다고 볼 수 있습니다.

Deadlock 방지 lock rank
순위Lock class
1낮은-rank filesystem의 `->s_vfs_rename_mutex`
2그 filesystem의 모든 directory `->i_rwsem`
3그 filesystem의 non-directory `->i_rwsem`을 pointer order로
4더 높은-rank filesystem의 `->s_vfs_rename_mutex`
5그 filesystem의 directory `->i_rwsem`
6그 filesystem의 non-directory `->i_rwsem`을 pointer order로

낮은 rank에서 높은 rank로만 획득하는 전체 순서입니다.


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 take
them in order of non-decreasing rank.  Namely,

  * rank ->i_rwsem of non-directories on given filesystem in inode pointer
    order.
  * 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_rwsem
    on the same filesystem.
  * among the locks on different filesystems use the relative
    rank of those filesystems.

For example, if we have NFS filesystem caching on a local one, we have

  1. ->s_vfs_rename_mutex of NFS filesystem
  2. ->i_rwsem of directories on that NFS filesystem, same rank for all
  3. ->i_rwsem of non-directories on that filesystem, in order of
     increasing address of inode
  4. ->s_vfs_rename_mutex of local filesystem
  5. ->i_rwsem of directories on the local filesystem, same rank for all
  6. ->i_rwsem of non-directories on local filesystem, in order of
     increasing address of inode.

It's easy to verify that operations never take a lock with rank
lower than that of an already held lock.

Suppose deadlocks are possible.  Consider the minimal deadlocked
set of threads.  It is a cycle of several threads, each blocked on a lock
held by the next thread in the cycle.

Since the locking order is consistent with the ranking, all
contended 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 operations
are done directly to that filesystem and none of them has actually
reached the method call.

최소 deadlock cycle의 모순

173-237

최소 cycle에 thread `T1...Tn`과 directory `D1...Dn`이 있어 `T1`은 `T2`가 가진 `D1`을 기다리고, `T2`는 `T3`가 가진 `D2`를 기다리며, 마지막 `Tn`은 `T1`이 가진 `Dn`을 기다린다고 가정합니다.

각 operation은 directory 하나 이상을 잠근 뒤 다른 directory에서 block되어야 합니다. 가능한 operation은 parent 후 child를 잠그는 directory 제거, subdirectory를 제거하는 same-directory rename, 그리고 cross-directory rename뿐입니다.

cycle에는 반드시 cross-directory rename이 하나 있어야 합니다. 모두 parent 다음 child를 잠그는 operation이라면 `Dn`이 `D1`의 parent이고 `D1`이 `D2`의 parent인 관계가 계속 이어져 다시 `Dn`으로 돌아오는 ancestry loop가 됩니다. lock을 잡은 뒤 관계가 변할 수 없으므로 이는 불가능합니다.

모든 operation이 같은 filesystem에 있으므로 cross-directory rename은 둘 이상일 수 없습니다. 일반성을 잃지 않고 `T1`이 그 rename을 하고 나머지는 parent 다음 child를 잠근다고 둡니다. 그러면 rename은 `Dn`을 잡고 `D1`을 기다리며, `D1`부터 `Dn`까지 parent chain이 형성됩니다. rename은 filesystem lock과 공통 ancestor 검사를 마친 뒤에만 directory를 잠그므로 이 ancestry 관계는 안정적입니다.

cross-directory rename은 먼저 parent들을, 다음에 필요한 child들을 잠급니다. `Dn`을 `D1`보다 먼저 잠글 수 있는 조합을 살피면 둘 다 parent일 수 없습니다. `D1`이 `Dn`의 ancestor라면 `D1`이 먼저여야 하기 때문입니다.

parent와 자신의 child 조합도 불가능합니다. parent가 child보다 먼저 잠기므로 반대 순서라면 parent가 child의 descendant인 loop가 필요합니다. 한 parent와 다른 parent의 child 조합도 한 child가 다른 child의 descendant가 되어 불가능합니다.

남는 가능성은 `Dn`과 `D1`이 모두 child인 경우뿐이지만, rename이 parent lock 이후 진행하려면 두 child가 서로 descendant가 아니어야 하므로 이것도 불가능합니다. 따라서 최소 deadlock에 필요한 operation 집합은 존재할 수 없고 증명이 끝납니다.

최소 deadlock 반증
`T1...Tn`과 `D1...Dn`의 최소 wait cycle 가정가능한 operation을 remove·same-dir rename·cross-dir rename으로 제한cross-directory rename이 최소 하나 필요함을 보임같은 filesystem이므로 cross-directory rename은 하나뿐parent·child lock 조합을 ancestry 규칙으로 모두 배제최소 deadlock cycle이 존재하지 않는다는 모순

동일 rank의 directory lock cycle이 존재할 수 없음을 좁혀 가는 과정입니다.


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 least
one directory and blocked on attempt to lock another.  That leaves
only 3 possible operations: directory removal (locks parent, then
child), same-directory rename killing a subdirectory (ditto) and
cross-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" sort
we would have Dn a parent of D1, which is a parent of D2, which is
a parent of D3, ..., which is a parent of Dn.  Relationships couldn't
have changed since the moment directory locks had been acquired,
so they would all hold simultaneously at the deadlock time and
we would have a loop.

Since all operations are on the same filesystem, there can't be
more than one cross-directory rename among them.  Without loss of
generality we can assume that T1 is the one doing a cross-directory
rename and everything else is of the "lock parent, then child" sort.

In other words, we have a cross-directory rename that locked
Dn and blocked on attempt to lock D1, which is a parent of D2, which is
a parent of D3, ..., which is a parent of Dn.  Relationships between
D1,...,Dn all hold simultaneously at the deadlock time.  Moreover,
cross-directory rename does not get to locking any directories until it
has acquired filesystem lock and verified that directories involved have
a common ancestor, which guarantees that ancestry relationships between
all of them had been stable.

Consider the order in which directories are locked by the
cross-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 the
children must be involved and thus neither of them could be a descendent
of another - otherwise the operation would not have progressed past
locking the parents.

It can't be a parent and its child; otherwise we would've had
a loop, since the parents are locked before the children, so the parent
would 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 descendent
of another child.

That leaves only one possibility - namely, both Dn and D1 are
among the children, in some order.  But that is also impossible, since
neither of the children is a descendent of another.

That concludes the proof, since the set of operations with the
properties required for a minimal deadlock can not exist.

공통 ancestor 검사의 중요성

238-248

cross-directory rename에서 parent들이 공통 ancestor를 갖는지 검사하는 단계는 필수입니다. 이 검사가 없으면 deadlock이 실제로 가능해집니다.

처음에는 parent가 서로 다른 tree에 있어 source parent를 잠근 뒤 target parent를 기다릴 수 있습니다. 그 사이 무관한 lookup이 source의 먼 ancestor를 target parent의 먼 descendant 아래로 splice하면, rename은 source parent lock을 가진 채 그 먼 ancestor의 lock을 기다리게 됩니다.

그 사이 모든 중간 directory에서 `rmdir()`가 시도되면 각 호출은 lock을 얻은 뒤라면 `-ENOTEMPTY`로 실패하겠지만, 실제로는 lock wait chain을 완성해 deadlock을 만듭니다.

공통 ancestor 검사 누락 시 cycle
서로 다른 tree의 source·target parent로 rename 시작source parent lock 보유 후 target parent 대기lookup이 source ancestor를 target descendant 아래로 splicerename이 자신이 가진 lock의 먼 ancestor를 기다림중간 `rmdir()` lock wait가 cycle을 완성

서로 다른 tree와 lookup splicing이 rename lock 순서를 뒤집는 위험입니다.


Note that the check for having a common ancestor in cross-directory
rename is crucial - without it a deadlock would be possible.  Indeed,
suppose the parents are initially in different trees; we would lock the
parent of source, then try to lock the parent of target, only to have
an unrelated lookup splice a distant ancestor of source to some distant
descendent of the parent of target.   At that point we have cross-directory
rename holding the lock on parent of source and trying to lock its
distant ancestor.  Add a bunch of rmdir() attempts on all directories
in between (all of those would fail with -ENOTEMPTY, had they ever gotten
the locks) and voila - we have a deadlock.

Rename loop 방지와 tree 가정

249-286

이 operation들은 directory loop 생성을 피하도록 보장됩니다. loop를 새로 만들 수 있는 유일한 operation은 cross-directory rename입니다.

rename 뒤 loop가 생겼다고 가정하면, 이전에는 loop가 없었으므로 loop의 node 중 적어도 하나는 parent가 바뀌어야 합니다. 따라서 loop는 source를 지나며 `RENAME_EXCHANGE`라면 target도 지날 수 있습니다.

operation이 성공했다면 source와 target은 서로 ancestor일 수 없습니다. source parent에서 시작한 ancestor chain은 target을 통과할 수 없고 그 반대도 마찬가지입니다. 어떤 node의 ancestor chain도 자기 자신을 통과할 수 없는데, 그랬다면 operation 전부터 loop가 있었기 때문입니다.

source와 target 이외의 모든 node는 parent가 그대로이므로 rename은 source·target의 이전 parent가 가진 ancestor chain을 바꾸지 않습니다. 이 chain들은 유한한 단계 뒤 끝나야 합니다.

새 loop는 source 또는 target을 지난 뒤 각각 target 또는 source의 이전 parent로 이어지고, 이후에는 그 parent의 기존 ancestor chain을 따라야 합니다. 그러나 그 chain은 유한하게 끝나므로 loop 일부가 될 수 없습니다. 이것으로 loop가 생길 수 없다는 증명이 끝납니다.

locking scheme 자체는 임의의 DAG에서도 동작하지만, 한 directory가 다른 object의 descendant인지 검사할 수 있어야 합니다. 현재 구현은 directory graph가 tree라고 가정하며, cycle을 만들지 않는 tree의 cross-directory rename과 directory에 실패하는 `link()`가 이 가정을 보존합니다.

여기서 directory는 child를 가질 수 있는 모든 object를 뜻합니다. hybrid object를 도입한다면 `link(2)`가 그런 object에 동작하지 않도록 하거나, `is_subdir()`가 해당 graph에서도 올바르게 동작하도록 바꿔야 합니다.

Cross-directory rename의 loop 배제
rename 뒤 새 loop가 생겼다고 가정parent가 바뀐 source 또는 exchange target을 loop에서 식별성공 조건으로 source와 target의 상호 ancestry 배제이전 parent의 ancestor chain은 operation 전과 동일기존 chain은 유한하게 끝나므로 새 loop를 닫을 수 없음현재 구현은 directory graph가 tree라는 가정을 유지

parent가 바뀌는 node와 기존 ancestor chain을 추적하는 증명입니다.


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 such
loops before the operation, at least on of the nodes in that loop must've
had its parent changed.  In other words, the loop must be passing through
the source or, in case of exchange, possibly the target.

Since the operation has succeeded, neither source nor target could have
been ancestors of each other.  Therefore the chain of ancestors starting
in the parent of source could not have passed through the target and
vice versa.  On the other hand, the chain of ancestors of any node could
not have passed through the node itself, or we would've had a loop before
the operation.  But everything other than source and target has kept
the parent after the operation, so the operation does not change the
chains 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 either
source or target; the next node in the loop would be the ex-parent of
target or source resp.  After that the loop would follow the chain of
ancestors of that parent.  But as we have just shown, that chain must
end after a finite number of steps, which means that it can't be a part
of any loop.  Q.E.D.

While this locking scheme works for arbitrary DAGs, it relies on
ability to check that directory is a descendent of another object.  Current
implementation assumes that directory graph is a tree.  This assumption is
also preserved by all operations (cross-directory rename on a tree that would
not introduce a cycle will leave it a tree and link() fails for directories).

Notice that "directory" in the above == "anything that might have
children", so if we are going to introduce hybrid objects we will need
either to make sure that link(2) doesn't work for them or to make changes
in is_subdir() that would make it work even in presence of such beasts.