github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/lockedfile/internal/filelock/filelock_fcntl.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build aix || (solaris && !illumos) 6 7 // This code implements the filelock API using POSIX 'fcntl' locks, which attach 8 // to an (inode, process) pair rather than a file descriptor. To avoid unlocking 9 // files prematurely when the same file is opened through different descriptors, 10 // we allow only one read-lock at a time. 11 // 12 // Most platforms provide some alternative API, such as an 'flock' system call 13 // or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and 14 // does not require per-inode bookkeeping in the application. 15 16 package filelock 17 18 import ( 19 "errors" 20 "io" 21 "io/fs" 22 "math/rand" 23 "sync" 24 "syscall" 25 "time" 26 ) 27 28 type lockType int16 29 30 const ( 31 readLock lockType = syscall.F_RDLCK 32 writeLock lockType = syscall.F_WRLCK 33 ) 34 35 type inode = uint64 // type of syscall.Stat_t.Ino 36 37 type inodeLock struct { 38 owner File 39 queue []<-chan File 40 } 41 42 var ( 43 mu sync.Mutex 44 inodes = map[File]inode{} 45 locks = map[inode]inodeLock{} 46 ) 47 48 func lock(f File, lt lockType) (err error) { 49 // POSIX locks apply per inode and process, and the lock for an inode is 50 // released when *any* descriptor for that inode is closed. So we need to 51 // synchronize access to each inode internally, and must serialize lock and 52 // unlock calls that refer to the same inode through different descriptors. 53 fi, err := f.Stat() 54 if err != nil { 55 return err 56 } 57 ino := fi.Sys().(*syscall.Stat_t).Ino 58 59 mu.Lock() 60 if i, dup := inodes[f]; dup && i != ino { 61 mu.Unlock() 62 return &fs.PathError{ 63 Op: lt.String(), 64 Path: f.Name(), 65 Err: errors.New("inode for file changed since last Lock or RLock"), 66 } 67 } 68 inodes[f] = ino 69 70 var wait chan File 71 l := locks[ino] 72 if l.owner == f { 73 // This file already owns the lock, but the call may change its lock type. 74 } else if l.owner == nil { 75 // No owner: it's ours now. 76 l.owner = f 77 } else { 78 // Already owned: add a channel to wait on. 79 wait = make(chan File) 80 l.queue = append(l.queue, wait) 81 } 82 locks[ino] = l 83 mu.Unlock() 84 85 if wait != nil { 86 wait <- f 87 } 88 89 // Spurious EDEADLK errors arise on platforms that compute deadlock graphs at 90 // the process, rather than thread, level. Consider processes P and Q, with 91 // threads P.1, P.2, and Q.3. The following trace is NOT a deadlock, but will be 92 // reported as a deadlock on systems that consider only process granularity: 93 // 94 // P.1 locks file A. 95 // Q.3 locks file B. 96 // Q.3 blocks on file A. 97 // P.2 blocks on file B. (This is erroneously reported as a deadlock.) 98 // P.1 unlocks file A. 99 // Q.3 unblocks and locks file A. 100 // Q.3 unlocks files A and B. 101 // P.2 unblocks and locks file B. 102 // P.2 unlocks file B. 103 // 104 // These spurious errors were observed in practice on AIX and Solaris in 105 // cmd/go: see https://golang.org/issue/32817. 106 // 107 // We work around this bug by treating EDEADLK as always spurious. If there 108 // really is a lock-ordering bug between the interacting processes, it will 109 // become a livelock instead, but that's not appreciably worse than if we had 110 // a proper flock implementation (which generally does not even attempt to 111 // diagnose deadlocks). 112 // 113 // In the above example, that changes the trace to: 114 // 115 // P.1 locks file A. 116 // Q.3 locks file B. 117 // Q.3 blocks on file A. 118 // P.2 spuriously fails to lock file B and goes to sleep. 119 // P.1 unlocks file A. 120 // Q.3 unblocks and locks file A. 121 // Q.3 unlocks files A and B. 122 // P.2 wakes up and locks file B. 123 // P.2 unlocks file B. 124 // 125 // We know that the retry loop will not introduce a *spurious* livelock 126 // because, according to the POSIX specification, EDEADLK is only to be 127 // returned when “the lock is blocked by a lock from another process”. 128 // If that process is blocked on some lock that we are holding, then the 129 // resulting livelock is due to a real deadlock (and would manifest as such 130 // when using, for example, the flock implementation of this package). 131 // If the other process is *not* blocked on some other lock that we are 132 // holding, then it will eventually release the requested lock. 133 134 nextSleep := 1 * time.Millisecond 135 const maxSleep = 500 * time.Millisecond 136 for { 137 err = setlkw(f.Fd(), lt) 138 if err != syscall.EDEADLK { 139 break 140 } 141 time.Sleep(nextSleep) 142 143 nextSleep += nextSleep 144 if nextSleep > maxSleep { 145 nextSleep = maxSleep 146 } 147 // Apply 10% jitter to avoid synchronizing collisions when we finally unblock. 148 nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep)) 149 } 150 151 if err != nil { 152 unlock(f) 153 return &fs.PathError{ 154 Op: lt.String(), 155 Path: f.Name(), 156 Err: err, 157 } 158 } 159 160 return nil 161 } 162 163 func unlock(f File) error { 164 var owner File 165 166 mu.Lock() 167 ino, ok := inodes[f] 168 if ok { 169 owner = locks[ino].owner 170 } 171 mu.Unlock() 172 173 if owner != f { 174 panic("unlock called on a file that is not locked") 175 } 176 177 err := setlkw(f.Fd(), syscall.F_UNLCK) 178 179 mu.Lock() 180 l := locks[ino] 181 if len(l.queue) == 0 { 182 // No waiters: remove the map entry. 183 delete(locks, ino) 184 } else { 185 // The first waiter is sending us their file now. 186 // Receive it and update the queue. 187 l.owner = <-l.queue[0] 188 l.queue = l.queue[1:] 189 locks[ino] = l 190 } 191 delete(inodes, f) 192 mu.Unlock() 193 194 return err 195 } 196 197 // setlkw calls FcntlFlock with F_SETLKW for the entire file indicated by fd. 198 func setlkw(fd uintptr, lt lockType) error { 199 for { 200 err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{ 201 Type: int16(lt), 202 Whence: io.SeekStart, 203 Start: 0, 204 Len: 0, // All bytes. 205 }) 206 if err != syscall.EINTR { 207 return err 208 } 209 } 210 }