| 1 | //===- llvm/Support/Unix/Jobserver.inc - Unix Jobserver Impl ----*- C++ -*-===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // |
| 9 | // This file implements the UNIX-specific parts of the JobserverClient class. |
| 10 | // |
| 11 | //===----------------------------------------------------------------------===// |
| 12 | |
| 13 | #include <atomic> |
| 14 | #include <cassert> |
| 15 | #include <cerrno> |
| 16 | #include <fcntl.h> |
| 17 | #include <string.h> |
| 18 | #include <sys/stat.h> |
| 19 | #include <unistd.h> |
| 20 | |
| 21 | namespace { |
| 22 | /// Returns true if the given file descriptors are valid. |
| 23 | bool areFdsValid(int ReadFD, int WriteFD) { |
| 24 | if (ReadFD == -1 || WriteFD == -1) |
| 25 | return false; |
| 26 | // Check if the file descriptors are actually valid by checking their flags. |
| 27 | return ::fcntl(fd: ReadFD, F_GETFD) != -1 && ::fcntl(fd: WriteFD, F_GETFD) != -1; |
| 28 | } |
| 29 | } // namespace |
| 30 | |
| 31 | /// The constructor sets up the client based on the provided configuration. |
| 32 | /// For pipe-based jobservers, it duplicates the inherited file descriptors, |
| 33 | /// sets them to close-on-exec, and makes the read descriptor non-blocking. |
| 34 | /// For FIFO-based jobservers, it opens the named pipe. After setup, it drains |
| 35 | /// all available tokens from the jobserver to determine the total number of |
| 36 | /// available jobs (`NumJobs`), then immediately releases them back. |
| 37 | JobserverClientImpl::JobserverClientImpl(const JobserverConfig &Config) { |
| 38 | switch (Config.TheMode) { |
| 39 | case JobserverConfig::PosixPipe: { |
| 40 | // Duplicate the read and write file descriptors. |
| 41 | int NewReadFD = ::dup(fd: Config.PipeFDs.Read); |
| 42 | if (NewReadFD < 0) |
| 43 | return; |
| 44 | int NewWriteFD = ::dup(fd: Config.PipeFDs.Write); |
| 45 | if (NewWriteFD < 0) { |
| 46 | ::close(fd: NewReadFD); |
| 47 | return; |
| 48 | } |
| 49 | // Set the new descriptors to be closed automatically on exec(). |
| 50 | if (::fcntl(fd: NewReadFD, F_SETFD, FD_CLOEXEC) == -1 || |
| 51 | ::fcntl(fd: NewWriteFD, F_SETFD, FD_CLOEXEC) == -1) { |
| 52 | ::close(fd: NewReadFD); |
| 53 | ::close(fd: NewWriteFD); |
| 54 | return; |
| 55 | } |
| 56 | // Set the read descriptor to non-blocking. |
| 57 | int flags = ::fcntl(fd: NewReadFD, F_GETFL, 0); |
| 58 | if (flags == -1 || ::fcntl(fd: NewReadFD, F_SETFL, flags | O_NONBLOCK) == -1) { |
| 59 | ::close(fd: NewReadFD); |
| 60 | ::close(fd: NewWriteFD); |
| 61 | return; |
| 62 | } |
| 63 | ReadFD = NewReadFD; |
| 64 | WriteFD = NewWriteFD; |
| 65 | break; |
| 66 | } |
| 67 | case JobserverConfig::PosixFifo: |
| 68 | // Open the FIFO for reading. It must be non-blocking and close-on-exec. |
| 69 | ReadFD = ::open(file: Config.Path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC); |
| 70 | if (ReadFD < 0) { |
| 71 | if (ReadFD >= 0) |
| 72 | ::close(fd: ReadFD); |
| 73 | ReadFD = -1; |
| 74 | return; |
| 75 | } |
| 76 | FifoPath = Config.Path; |
| 77 | // The write FD is opened on-demand in release(). |
| 78 | WriteFD = -1; |
| 79 | break; |
| 80 | default: |
| 81 | return; |
| 82 | } |
| 83 | |
| 84 | IsInitialized = true; |
| 85 | // Determine the total number of jobs by acquiring all available slots and |
| 86 | // then immediately releasing them. |
| 87 | SmallVector<JobSlot, 8> Slots; |
| 88 | while (true) { |
| 89 | auto S = tryAcquire(); |
| 90 | if (!S.isValid()) |
| 91 | break; |
| 92 | Slots.push_back(Elt: std::move(S)); |
| 93 | } |
| 94 | NumJobs = Slots.size(); |
| 95 | assert(NumJobs >= 1 && "Invalid number of jobs" ); |
| 96 | for (auto &S : Slots) |
| 97 | release(Slot: std::move(S)); |
| 98 | } |
| 99 | |
| 100 | /// The destructor closes any open file descriptors. |
| 101 | JobserverClientImpl::~JobserverClientImpl() { |
| 102 | if (ReadFD >= 0) |
| 103 | ::close(fd: ReadFD); |
| 104 | if (WriteFD >= 0) |
| 105 | ::close(fd: WriteFD); |
| 106 | } |
| 107 | |
| 108 | /// Tries to acquire a job slot. The first call to this function will always |
| 109 | /// successfully acquire the single "implicit" slot that is granted to every |
| 110 | /// process started by `make`. Subsequent calls attempt to read a one-byte |
| 111 | /// token from the jobserver's read pipe. A successful read grants one |
| 112 | /// explicit job slot. The read is non-blocking; if no token is available, |
| 113 | /// it fails and returns an invalid JobSlot. |
| 114 | JobSlot JobserverClientImpl::tryAcquire() { |
| 115 | if (!IsInitialized) |
| 116 | return JobSlot(); |
| 117 | |
| 118 | // The first acquisition is always for the implicit slot. |
| 119 | if (HasImplicitSlot.exchange(i: false, m: std::memory_order_acquire)) { |
| 120 | LLVM_DEBUG(dbgs() << "Acquired implicit job slot.\n" ); |
| 121 | return JobSlot::createImplicit(); |
| 122 | } |
| 123 | |
| 124 | char Token; |
| 125 | ssize_t Ret; |
| 126 | LLVM_DEBUG(dbgs() << "Attempting to read token from FD " << ReadFD << ".\n" ); |
| 127 | // Loop to retry on EINTR (interrupted system call). |
| 128 | do { |
| 129 | Ret = ::read(fd: ReadFD, buf: &Token, nbytes: 1); |
| 130 | } while (Ret < 0 && errno == EINTR); |
| 131 | |
| 132 | if (Ret == 1) { |
| 133 | LLVM_DEBUG(dbgs() << "Acquired explicit token '" << Token << "'.\n" ); |
| 134 | return JobSlot::createExplicit(V: static_cast<uint8_t>(Token)); |
| 135 | } |
| 136 | |
| 137 | LLVM_DEBUG(dbgs() << "Failed to acquire job slot, read returned " << Ret |
| 138 | << ".\n" ); |
| 139 | return JobSlot(); |
| 140 | } |
| 141 | |
| 142 | /// Releases a job slot back to the pool. If the slot is implicit, it simply |
| 143 | /// resets a flag. If the slot is explicit, it writes the character token |
| 144 | /// associated with the slot back into the jobserver's write pipe. For FIFO |
| 145 | /// jobservers, this may require opening the FIFO for writing if it hasn't |
| 146 | /// been already. |
| 147 | void JobserverClientImpl::release(JobSlot Slot) { |
| 148 | if (!Slot.isValid()) |
| 149 | return; |
| 150 | |
| 151 | // Releasing the implicit slot just makes it available for the next acquire. |
| 152 | if (Slot.isImplicit()) { |
| 153 | LLVM_DEBUG(dbgs() << "Released implicit job slot.\n" ); |
| 154 | [[maybe_unused]] bool was_already_released = |
| 155 | HasImplicitSlot.exchange(i: true, m: std::memory_order_release); |
| 156 | assert(!was_already_released && "Implicit slot released twice" ); |
| 157 | return; |
| 158 | } |
| 159 | |
| 160 | uint8_t Token = Slot.getExplicitValue(); |
| 161 | LLVM_DEBUG(dbgs() << "Releasing explicit token '" << (char)Token << "' to FD " |
| 162 | << WriteFD << ".\n" ); |
| 163 | |
| 164 | // For FIFO-based jobservers, the write FD might not be open yet. |
| 165 | // Open it on the first release. |
| 166 | if (WriteFD < 0) { |
| 167 | LLVM_DEBUG(dbgs() << "WriteFD is invalid, opening FIFO: " << FifoPath |
| 168 | << "\n" ); |
| 169 | WriteFD = ::open(file: FifoPath.c_str(), O_WRONLY | O_CLOEXEC); |
| 170 | if (WriteFD < 0) { |
| 171 | LLVM_DEBUG(dbgs() << "Failed to open FIFO for writing.\n" ); |
| 172 | return; |
| 173 | } |
| 174 | LLVM_DEBUG(dbgs() << "Opened FIFO as new WriteFD: " << WriteFD << "\n" ); |
| 175 | } |
| 176 | |
| 177 | ssize_t Written; |
| 178 | // Loop to retry on EINTR (interrupted system call). |
| 179 | do { |
| 180 | Written = ::write(fd: WriteFD, buf: &Token, n: 1); |
| 181 | } while (Written < 0 && errno == EINTR); |
| 182 | |
| 183 | if (Written <= 0) { |
| 184 | LLVM_DEBUG(dbgs() << "Failed to write token to pipe, write returned " |
| 185 | << Written << "\n" ); |
| 186 | } |
| 187 | } |
| 188 | |