mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
The BM_ProcessSwitch benchmark in fork_benchmark measures context switching performance by creating a ring of processes connected by pipes and passing tokens around the ring. When the benchmark completes, the parent process closes its ends of the pipes to initiate shutdown. This causes children to either read EOF (if the previous process in the ring closed its write end) or attempt to write to a pipe with no readers (if the next process in the ring has already exited and closed its read end). In the latter case, the writing process receives a SIGPIPE signal. The benchmark code is designed to handle this by checking for an EPIPE error from the write system call, breaking the loop, and exiting normally. However, the default action for SIGPIPE is to terminate the process immediately. This test typically passes on Linux without modification because the standard test execution environment (such as Bazel or the test runner) often ignores SIGPIPE by default for the test process tree. This allows the write call to return EPIPE as intended. In environments where SIGPIPE is not ignored by default, the child process is killed by the signal, causing the test to fail when the parent verifies that all children exited normally (via WIFEXITED). This change explicitly sets the disposition of SIGPIPE to SIG_IGN in the benchmark. This ensures consistent behavior regardless of the outer execution environment, allowing the write call to return EPIPE and the child processes to exit gracefully. PiperOrigin-RevId: 907729729
359 lines
9.8 KiB
C++
359 lines
9.8 KiB
C++
// Copyright 2020 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <csignal>
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "absl/synchronization/barrier.h"
|
|
#include "benchmark/benchmark.h"
|
|
#include "test/util/cleanup.h"
|
|
#include "test/util/file_descriptor.h"
|
|
#include "test/util/logging.h"
|
|
#include "test/util/test_util.h"
|
|
#include "test/util/thread_util.h"
|
|
|
|
namespace gvisor {
|
|
namespace testing {
|
|
|
|
namespace {
|
|
|
|
constexpr int kBusyMax = 250;
|
|
|
|
// Do some CPU-bound busy-work.
|
|
int busy(int max) {
|
|
// Prevent the compiler from optimizing this work away,
|
|
volatile int count = 0;
|
|
|
|
for (int i = 1; i < max; i++) {
|
|
for (int j = 2; j < i / 2; j++) {
|
|
if (i % j == 0) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void BM_CPUBoundUniprocess(benchmark::State& state) {
|
|
for (auto _ : state) {
|
|
busy(kBusyMax);
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_CPUBoundUniprocess);
|
|
|
|
void BM_CPUBoundAsymmetric(benchmark::State& state) {
|
|
const size_t max = state.max_iterations;
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
for (size_t i = 0; i < max; i++) {
|
|
busy(kBusyMax);
|
|
}
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
ASSERT_TRUE(state.KeepRunningBatch(max));
|
|
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
ASSERT_FALSE(state.KeepRunning());
|
|
}
|
|
|
|
BENCHMARK(BM_CPUBoundAsymmetric)->UseRealTime();
|
|
|
|
void BM_CPUBoundSymmetric(benchmark::State& state) {
|
|
std::vector<pid_t> children;
|
|
auto child_cleanup = Cleanup([&] {
|
|
for (const pid_t child : children) {
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
}
|
|
ASSERT_FALSE(state.KeepRunning());
|
|
});
|
|
|
|
const int processes = state.range(0);
|
|
for (int i = 0; i < processes; i++) {
|
|
size_t cur = (state.max_iterations + (processes - 1)) / processes;
|
|
if ((state.iterations() + cur) >= state.max_iterations) {
|
|
cur = state.max_iterations - state.iterations();
|
|
}
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
for (size_t i = 0; i < cur; i++) {
|
|
busy(kBusyMax);
|
|
}
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
if (cur > 0) {
|
|
// We can have a zero cur here, depending.
|
|
ASSERT_TRUE(state.KeepRunningBatch(cur));
|
|
}
|
|
children.push_back(child);
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_CPUBoundSymmetric)->Range(2, 16)->UseRealTime();
|
|
|
|
// Child routine for ProcessSwitch/ThreadSwitch.
|
|
// Reads from readfd and writes the result to writefd.
|
|
void SwitchChild(int readfd, int writefd) {
|
|
while (1) {
|
|
char buf;
|
|
int ret = ReadFd(readfd, &buf, 1);
|
|
if (ret == 0) {
|
|
break;
|
|
}
|
|
TEST_CHECK_MSG(ret == 1, "read failed");
|
|
|
|
ret = WriteFd(writefd, &buf, 1);
|
|
if (ret == -1) {
|
|
TEST_CHECK_MSG(errno == EPIPE, "unexpected write failure");
|
|
break;
|
|
}
|
|
TEST_CHECK_MSG(ret == 1, "write failed");
|
|
}
|
|
}
|
|
|
|
void SwitchSetup(const benchmark::State& state) {
|
|
// Ignore SIGPIPE so that WriteFd inside SwitchChild generates EPIPE
|
|
// instead of a SIGPIPE that terminates the process.
|
|
TEST_PCHECK(signal(SIGPIPE, SIG_IGN) != SIG_ERR);
|
|
}
|
|
|
|
// Send bytes in a loop through a series of pipes, each passing through a
|
|
// different process.
|
|
//
|
|
// Proc 0 Proc 1
|
|
// * ----------> *
|
|
// ^ Pipe 1 |
|
|
// | |
|
|
// | Pipe 0 | Pipe 2
|
|
// | |
|
|
// | |
|
|
// | Pipe 3 v
|
|
// * <---------- *
|
|
// Proc 3 Proc 2
|
|
//
|
|
// This exercises context switching through multiple processes.
|
|
void BM_ProcessSwitch(benchmark::State& state) {
|
|
// Code below assumes there are at least two processes.
|
|
const int num_processes = state.range(0);
|
|
ASSERT_GE(num_processes, 2);
|
|
|
|
std::vector<pid_t> children;
|
|
auto child_cleanup = Cleanup([&] {
|
|
for (const pid_t child : children) {
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
}
|
|
});
|
|
|
|
// Must come after children, as the FDs must be closed before the children
|
|
// will exit.
|
|
std::vector<FileDescriptor> read_fds;
|
|
std::vector<FileDescriptor> write_fds;
|
|
|
|
for (int i = 0; i < num_processes; i++) {
|
|
int fds[2];
|
|
ASSERT_THAT(pipe(fds), SyscallSucceeds());
|
|
read_fds.emplace_back(fds[0]);
|
|
write_fds.emplace_back(fds[1]);
|
|
}
|
|
|
|
// This process is one of the processes in the loop. It will be considered
|
|
// index 0.
|
|
for (int i = 1; i < num_processes; i++) {
|
|
// Read from current pipe index, write to next.
|
|
const int read_index = i;
|
|
const int read_fd = read_fds[read_index].get();
|
|
|
|
const int write_index = (i + 1) % num_processes;
|
|
const int write_fd = write_fds[write_index].get();
|
|
|
|
// std::vector isn't safe to use from the fork child.
|
|
FileDescriptor* read_array = read_fds.data();
|
|
FileDescriptor* write_array = write_fds.data();
|
|
|
|
pid_t child = fork();
|
|
if (!child) {
|
|
// Close all other FDs.
|
|
for (int j = 0; j < num_processes; j++) {
|
|
if (j != read_index) {
|
|
read_array[j].reset();
|
|
}
|
|
if (j != write_index) {
|
|
write_array[j].reset();
|
|
}
|
|
}
|
|
|
|
SwitchChild(read_fd, write_fd);
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
children.push_back(child);
|
|
}
|
|
|
|
// Read from current pipe index (0), write to next (1).
|
|
const int read_index = 0;
|
|
const int read_fd = read_fds[read_index].get();
|
|
|
|
const int write_index = 1;
|
|
const int write_fd = write_fds[write_index].get();
|
|
|
|
// Kick start the loop.
|
|
char buf = 'a';
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
|
|
for (auto _ : state) {
|
|
ASSERT_THAT(ReadFd(read_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_ProcessSwitch)->Setup(SwitchSetup)->Range(2, 16)->UseRealTime();
|
|
|
|
// Equivalent to BM_ThreadSwitch using threads instead of processes.
|
|
void BM_ThreadSwitch(benchmark::State& state) {
|
|
// Code below assumes there are at least two threads.
|
|
const int num_threads = state.range(0);
|
|
ASSERT_GE(num_threads, 2);
|
|
|
|
// Must come after threads, as the FDs must be closed before the children
|
|
// will exit.
|
|
std::vector<std::unique_ptr<ScopedThread>> threads;
|
|
std::vector<FileDescriptor> read_fds;
|
|
std::vector<FileDescriptor> write_fds;
|
|
|
|
for (int i = 0; i < num_threads; i++) {
|
|
int fds[2];
|
|
ASSERT_THAT(pipe(fds), SyscallSucceeds());
|
|
read_fds.emplace_back(fds[0]);
|
|
write_fds.emplace_back(fds[1]);
|
|
}
|
|
|
|
// This thread is one of the threads in the loop. It will be considered
|
|
// index 0.
|
|
for (int i = 1; i < num_threads; i++) {
|
|
// Read from current pipe index, write to next.
|
|
//
|
|
// Transfer ownership of the FDs to the thread.
|
|
const int read_index = i;
|
|
const int read_fd = read_fds[read_index].release();
|
|
|
|
const int write_index = (i + 1) % num_threads;
|
|
const int write_fd = write_fds[write_index].release();
|
|
|
|
threads.emplace_back(std::make_unique<ScopedThread>([read_fd, write_fd] {
|
|
FileDescriptor read(read_fd);
|
|
FileDescriptor write(write_fd);
|
|
SwitchChild(read.get(), write.get());
|
|
}));
|
|
}
|
|
|
|
// Read from current pipe index (0), write to next (1).
|
|
const int read_index = 0;
|
|
const int read_fd = read_fds[read_index].get();
|
|
|
|
const int write_index = 1;
|
|
const int write_fd = write_fds[write_index].get();
|
|
|
|
// Kick start the loop.
|
|
char buf = 'a';
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
|
|
for (auto _ : state) {
|
|
ASSERT_THAT(ReadFd(read_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
}
|
|
|
|
// The two FDs still owned by this thread are closed, causing the next thread
|
|
// to exit its loop and close its FDs, and so on until all threads exit.
|
|
}
|
|
|
|
BENCHMARK(BM_ThreadSwitch)->Setup(SwitchSetup)->Range(2, 16)->UseRealTime();
|
|
|
|
void BM_ThreadStart(benchmark::State& state) {
|
|
const int num_threads = state.range(0);
|
|
|
|
for (auto _ : state) {
|
|
state.PauseTiming();
|
|
|
|
auto barrier = new absl::Barrier(num_threads + 1);
|
|
std::vector<std::unique_ptr<ScopedThread>> threads;
|
|
|
|
state.ResumeTiming();
|
|
|
|
for (int i = 0; i < num_threads; ++i) {
|
|
threads.emplace_back(std::make_unique<ScopedThread>([barrier] {
|
|
if (barrier->Block()) {
|
|
delete barrier;
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (barrier->Block()) {
|
|
delete barrier;
|
|
}
|
|
|
|
state.PauseTiming();
|
|
|
|
for (const auto& thread : threads) {
|
|
thread->Join();
|
|
}
|
|
|
|
state.ResumeTiming();
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_ThreadStart)->Range(1, 2048)->UseRealTime();
|
|
|
|
// Benchmark the complete fork + exit + wait.
|
|
void BM_ProcessLifecycle(benchmark::State& state) {
|
|
const int num_procs = state.range(0);
|
|
|
|
std::vector<pid_t> pids(num_procs);
|
|
for (auto _ : state) {
|
|
for (int i = 0; i < num_procs; ++i) {
|
|
int pid = fork();
|
|
if (pid == 0) {
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(pid, SyscallSucceeds());
|
|
pids[i] = pid;
|
|
}
|
|
|
|
for (const int pid : pids) {
|
|
ASSERT_THAT(RetryEINTR(waitpid)(pid, nullptr, 0),
|
|
SyscallSucceedsWithValue(pid));
|
|
}
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_ProcessLifecycle)->Range(1, 512)->UseRealTime();
|
|
|
|
} // namespace
|
|
|
|
} // namespace testing
|
|
} // namespace gvisor
|