Skip to content

Commit d6473a1

Browse files
ianlancetaylorgopherbot
authored andcommitted
syscall: avoid serializing forks on ForkLock
Fixes #23558 Fixes #54162 Change-Id: I3cf6efe466080cdb17e171218e9385ccb272c301 Reviewed-on: https://go-review.googlesource.com/c/go/+/421441 Run-TryBot: Ian Lance Taylor <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: David Chase <[email protected]> Reviewed-by: Bryan Mills <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 4e679e2 commit d6473a1

File tree

2 files changed

+110
-18
lines changed

2 files changed

+110
-18
lines changed

src/sync/rwmutex.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ func (rw *RWMutex) Unlock() {
219219
}
220220
}
221221

222+
// syscall_hasWaitingReaders reports whether any goroutine is waiting
223+
// to acquire a read lock on rw. This exists because syscall.ForkLock
224+
// is an RWMutex, and we can't change that without breaking compatibility.
225+
// We don't need or want RWMutex semantics for ForkLock, and we use
226+
// this private API to avoid having to change the type of ForkLock.
227+
// For more details see the syscall package.
228+
//
229+
//go:linkname syscall_hasWaitingReaders syscall.hasWaitingReaders
230+
func syscall_hasWaitingReaders(rw *RWMutex) bool {
231+
r := rw.readerCount.Load()
232+
return r < 0 && r+rwmutexMaxReaders > 0
233+
}
234+
222235
// RLocker returns a Locker interface that implements
223236
// the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
224237
func (rw *RWMutex) RLocker() Locker {

src/syscall/exec_unix.go

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
"unsafe"
1717
)
1818

19-
// Lock synchronizing creation of new file descriptors with fork.
19+
// ForkLock is used to synchronize creation of new file descriptors
20+
// with fork.
2021
//
2122
// We want the child in a fork/exec sequence to inherit only the
2223
// file descriptors we intend. To do that, we mark all file
@@ -53,16 +54,14 @@ import (
5354
// The rules for which file descriptor-creating operations use the
5455
// ForkLock are as follows:
5556
//
56-
// 1) Pipe. Does not block. Use the ForkLock.
57-
// 2) Socket. Does not block. Use the ForkLock.
58-
// 3) Accept. If using non-blocking mode, use the ForkLock.
59-
// Otherwise, live with the race.
60-
// 4) Open. Can block. Use O_CLOEXEC if available (Linux).
61-
// Otherwise, live with the race.
62-
// 5) Dup. Does not block. Use the ForkLock.
63-
// On Linux, could use fcntl F_DUPFD_CLOEXEC
64-
// instead of the ForkLock, but only for dup(fd, -1).
65-
57+
// - Pipe. Use pipe2 if available. Otherwise, does not block,
58+
// so use ForkLock.
59+
// - Socket. Use SOCK_CLOEXEC if available. Otherwise, does not
60+
// block, so use ForkLock.
61+
// - Open. Use O_CLOEXEC if available. Otherwise, may block,
62+
// so live with the race.
63+
// - Dup. Use F_DUPFD_CLOEXEC or dup3 if available. Otherwise,
64+
// does not block, so use ForkLock.
6665
var ForkLock sync.RWMutex
6766

6867
// StringSlicePtr converts a slice of strings to a slice of pointers
@@ -194,14 +193,11 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
194193
return 0, errorspkg.New("Setctty set but Ctty not valid in child")
195194
}
196195

197-
// Acquire the fork lock so that no other threads
198-
// create new fds that are not yet close-on-exec
199-
// before we fork.
200-
ForkLock.Lock()
196+
acquireForkLock()
201197

202198
// Allocate child status pipe close on exec.
203199
if err = forkExecPipe(p[:]); err != nil {
204-
ForkLock.Unlock()
200+
releaseForkLock()
205201
return 0, err
206202
}
207203

@@ -210,10 +206,10 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
210206
if err1 != 0 {
211207
Close(p[0])
212208
Close(p[1])
213-
ForkLock.Unlock()
209+
releaseForkLock()
214210
return 0, Errno(err1)
215211
}
216-
ForkLock.Unlock()
212+
releaseForkLock()
217213

218214
// Read child error status from pipe.
219215
Close(p[1])
@@ -245,6 +241,89 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
245241
return pid, nil
246242
}
247243

244+
var (
245+
// Guard the forking variable.
246+
forkingLock sync.Mutex
247+
// Number of goroutines currently forking, and thus the
248+
// number of goroutines holding a conceptual write lock
249+
// on ForkLock.
250+
forking int
251+
)
252+
253+
// hasWaitingReaders reports whether any goroutine is waiting
254+
// to acquire a read lock on rw. It is defined in the sync package.
255+
func hasWaitingReaders(rw *sync.RWMutex) bool
256+
257+
// acquireForkLock acquires a write lock on ForkLock.
258+
// ForkLock is exported and we've promised that during a fork
259+
// we will call ForkLock.Lock, so that no other threads create
260+
// new fds that are not yet close-on-exec before we fork.
261+
// But that forces all fork calls to be serialized, which is bad.
262+
// But we haven't promised that serialization, and it is essentially
263+
// undetectable by other users of ForkLock, which is good.
264+
// Avoid the serialization by ensuring that ForkLock is locked
265+
// at the first fork and unlocked when there are no more forks.
266+
func acquireForkLock() {
267+
forkingLock.Lock()
268+
defer forkingLock.Unlock()
269+
270+
if forking == 0 {
271+
// There is no current write lock on ForkLock.
272+
ForkLock.Lock()
273+
forking++
274+
return
275+
}
276+
277+
// ForkLock is currently locked for writing.
278+
279+
if hasWaitingReaders(&ForkLock) {
280+
// ForkLock is locked for writing, and at least one
281+
// goroutine is waiting to read from it.
282+
// To avoid lock starvation, allow readers to proceed.
283+
// The simple way to do this is for us to acquire a
284+
// read lock. That will block us until all current
285+
// conceptual write locks are released.
286+
//
287+
// Note that this case is unusual on modern systems
288+
// with O_CLOEXEC and SOCK_CLOEXEC. On those systems
289+
// the standard library should never take a read
290+
// lock on ForkLock.
291+
292+
forkingLock.Unlock()
293+
294+
ForkLock.RLock()
295+
ForkLock.RUnlock()
296+
297+
forkingLock.Lock()
298+
299+
// Readers got a chance, so now take the write lock.
300+
301+
if forking == 0 {
302+
ForkLock.Lock()
303+
}
304+
}
305+
306+
forking++
307+
}
308+
309+
// releaseForkLock releases the conceptual write lock on ForkLock
310+
// acquired by acquireForkLock.
311+
func releaseForkLock() {
312+
forkingLock.Lock()
313+
defer forkingLock.Unlock()
314+
315+
if forking <= 0 {
316+
panic("syscall.releaseForkLock: negative count")
317+
}
318+
319+
forking--
320+
321+
if forking == 0 {
322+
// No more conceptual write locks.
323+
ForkLock.Unlock()
324+
}
325+
}
326+
248327
// Combination of fork and exec, careful to be thread safe.
249328
func ForkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
250329
return forkExec(argv0, argv, attr)

0 commit comments

Comments
 (0)