//===- llvm/Support/Unix/Jobserver.inc - Unix Jobserver Impl ----*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // This file implements the UNIX-specific parts of the JobserverClient class. // //===----------------------------------------------------------------------===// #include #include #include #include #include #include #include namespace { /// Returns true if the given file descriptor is a FIFO (named pipe). bool isFifo(int FD) { struct stat StatBuf; if (::fstat(FD, &StatBuf) != 0) return false; return S_ISFIFO(StatBuf.st_mode); } /// Returns true if the given file descriptors are valid. bool areFdsValid(int ReadFD, int WriteFD) { if (ReadFD == -1 || WriteFD == -1) return false; // Check if the file descriptors are actually valid by checking their flags. return ::fcntl(ReadFD, F_GETFD) != -1 && ::fcntl(WriteFD, F_GETFD) != -1; } } // namespace /// The constructor sets up the client based on the provided configuration. /// For pipe-based jobservers, it duplicates the inherited file descriptors, /// sets them to close-on-exec, and makes the read descriptor non-blocking. /// For FIFO-based jobservers, it opens the named pipe. After setup, it drains /// all available tokens from the jobserver to determine the total number of /// available jobs (`NumJobs`), then immediately releases them back. JobserverClientImpl::JobserverClientImpl(const JobserverConfig &Config) { switch (Config.TheMode) { case JobserverConfig::PosixPipe: { // Duplicate the read and write file descriptors. int NewReadFD = ::dup(Config.PipeFDs.Read); if (NewReadFD < 0) return; int NewWriteFD = ::dup(Config.PipeFDs.Write); if (NewWriteFD < 0) { ::close(NewReadFD); return; } // Set the new descriptors to be closed automatically on exec(). if (::fcntl(NewReadFD, F_SETFD, FD_CLOEXEC) == -1 || ::fcntl(NewWriteFD, F_SETFD, FD_CLOEXEC) == -1) { ::close(NewReadFD); ::close(NewWriteFD); return; } // Set the read descriptor to non-blocking. int flags = ::fcntl(NewReadFD, F_GETFL, 0); if (flags == -1 || ::fcntl(NewReadFD, F_SETFL, flags | O_NONBLOCK) == -1) { ::close(NewReadFD); ::close(NewWriteFD); return; } ReadFD = NewReadFD; WriteFD = NewWriteFD; break; } case JobserverConfig::PosixFifo: // Open the FIFO for reading. It must be non-blocking and close-on-exec. ReadFD = ::open(Config.Path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (ReadFD < 0 || !isFifo(ReadFD)) { if (ReadFD >= 0) ::close(ReadFD); ReadFD = -1; return; } FifoPath = Config.Path; // The write FD is opened on-demand in release(). WriteFD = -1; break; default: return; } IsInitialized = true; // Determine the total number of jobs by acquiring all available slots and // then immediately releasing them. SmallVector Slots; while (true) { auto S = tryAcquire(); if (!S.isValid()) break; Slots.push_back(std::move(S)); } NumJobs = Slots.size(); assert(NumJobs >= 1 && "Invalid number of jobs"); for (auto &S : Slots) release(std::move(S)); } /// The destructor closes any open file descriptors. JobserverClientImpl::~JobserverClientImpl() { if (ReadFD >= 0) ::close(ReadFD); if (WriteFD >= 0) ::close(WriteFD); } /// Tries to acquire a job slot. The first call to this function will always /// successfully acquire the single "implicit" slot that is granted to every /// process started by `make`. Subsequent calls attempt to read a one-byte /// token from the jobserver's read pipe. A successful read grants one /// explicit job slot. The read is non-blocking; if no token is available, /// it fails and returns an invalid JobSlot. JobSlot JobserverClientImpl::tryAcquire() { if (!IsInitialized) return JobSlot(); // The first acquisition is always for the implicit slot. if (HasImplicitSlot.exchange(false, std::memory_order_acquire)) { LLVM_DEBUG(dbgs() << "Acquired implicit job slot.\n"); return JobSlot::createImplicit(); } char Token; ssize_t Ret; LLVM_DEBUG(dbgs() << "Attempting to read token from FD " << ReadFD << ".\n"); // Loop to retry on EINTR (interrupted system call). do { Ret = ::read(ReadFD, &Token, 1); } while (Ret < 0 && errno == EINTR); if (Ret == 1) { LLVM_DEBUG(dbgs() << "Acquired explicit token '" << Token << "'.\n"); return JobSlot::createExplicit(static_cast(Token)); } LLVM_DEBUG(dbgs() << "Failed to acquire job slot, read returned " << Ret << ".\n"); return JobSlot(); } /// Releases a job slot back to the pool. If the slot is implicit, it simply /// resets a flag. If the slot is explicit, it writes the character token /// associated with the slot back into the jobserver's write pipe. For FIFO /// jobservers, this may require opening the FIFO for writing if it hasn't /// been already. void JobserverClientImpl::release(JobSlot Slot) { if (!Slot.isValid()) return; // Releasing the implicit slot just makes it available for the next acquire. if (Slot.isImplicit()) { LLVM_DEBUG(dbgs() << "Released implicit job slot.\n"); [[maybe_unused]] bool was_already_released = HasImplicitSlot.exchange(true, std::memory_order_release); assert(!was_already_released && "Implicit slot released twice"); return; } uint8_t Token = Slot.getExplicitValue(); LLVM_DEBUG(dbgs() << "Releasing explicit token '" << (char)Token << "' to FD " << WriteFD << ".\n"); // For FIFO-based jobservers, the write FD might not be open yet. // Open it on the first release. if (WriteFD < 0) { LLVM_DEBUG(dbgs() << "WriteFD is invalid, opening FIFO: " << FifoPath << "\n"); WriteFD = ::open(FifoPath.c_str(), O_WRONLY | O_CLOEXEC); if (WriteFD < 0) { LLVM_DEBUG(dbgs() << "Failed to open FIFO for writing.\n"); return; } LLVM_DEBUG(dbgs() << "Opened FIFO as new WriteFD: " << WriteFD << "\n"); } ssize_t Written; // Loop to retry on EINTR (interrupted system call). do { Written = ::write(WriteFD, &Token, 1); } while (Written < 0 && errno == EINTR); if (Written <= 0) { LLVM_DEBUG(dbgs() << "Failed to write token to pipe, write returned " << Written << "\n"); } }