В Linux всякий раз, когда процесс разветвляется, отображения памяти родительского процесса клонируются в дочерний процесс. В действительности, по соображениям производительности, для страниц установлено значение copy-on-write - изначально они являются общими и, в случае, если один из двух процессов записывает один из них, они затем будут клонировано (MAP_PRIVATE
).
Это очень распространенный механизм получения снимка состояния работающей программы - вы делаете разветвление, и это дает вам (непротиворечивое) представление о памяти процесса в тот момент времени.
Я сделал простой тест, в котором у меня есть два компонента:
- A родительский процесс , имеющий пул потоков , записывающий в массив
- дочерний процесс , который имеет пул потоков, создающих снимок массива и его отображение
При некоторых обстоятельствах (машина / архитектура / размещение в памяти / количество потоков / ...) я могу завершить копирование намного раньше, чем потоки записывают в массив.
Однако, когда дочерний процесс завершается, в htop
я все еще вижу большую часть процессорного времени, затрачиваемого на ядро, что соответствует тому, что он используется для обработки копирование при записи всякий раз, когда родительский процесс пишет на страницу.
В моем понимании, если анонимная страница, помеченная как copy-on-write , отображается одним процессом, она не должна копироваться и вместо этого должна использоваться напрямую.
Как я могу быть уверен, что это действительно время, затрачиваемое на копирование памяти?
Если я прав, как мне избежать этих издержек?
Суть теста ниже, в modern C ++.
Определите WITH_FORK
, чтобы включить снимок; оставьте неопределенным, чтобы отключить дочерний процесс.
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <numaif.h>
#include <numa.h>
#include <algorithm>
#include <cassert>
#include <condition_variable>
#include <mutex>
#include <iomanip>
#include <iostream>
#include <cmath>
#include <numeric>
#include <thread>
#include <vector>
#define ARRAY_SIZE 1073741824 // 1GB
#define NUM_WORKERS 28
#define NUM_CHECKPOINTERS 4
#define BATCH_SIZE 2097152 // 2MB
using inttype = uint64_t;
using timepoint = std::chrono::time_point<std::chrono::high_resolution_clock>;
constexpr uint64_t NUM_ELEMS() {
return ARRAY_SIZE / sizeof(inttype);
}
int main() {
// allocate array
std::array<inttype, NUM_ELEMS()> *arrayptr = new std::array<inttype, NUM_ELEMS()>();
std::array<inttype, NUM_ELEMS()> & array = *arrayptr;
// allocate checkpoint space
std::array<inttype, NUM_ELEMS()> *cpptr = new std::array<inttype, NUM_ELEMS()>();
std::array<inttype, NUM_ELEMS()> & cp = *cpptr;
// initialize array
std::fill(array.begin(), array.end(), 123);
#ifdef WITH_FORK
// spawn checkpointer threads
int pid = fork();
if (pid == -1) {
perror("fork");
exit(-1);
}
// child process -- do checkpoint
if (pid == 0) {
std::array<std::thread, NUM_CHECKPOINTERS> cpthreads;
for (size_t tid = 0; tid < NUM_CHECKPOINTERS; tid++) {
cpthreads[tid] = std::thread([&, tid] {
// copy array
const size_t numBatches = ARRAY_SIZE / BATCH_SIZE;
for (size_t i = tid; i < numBatches; i += NUM_CHECKPOINTERS) {
void *src = reinterpret_cast<void*>(
reinterpret_cast<intptr_t>(array.data()) + i * BATCH_SIZE);
void *dst = reinterpret_cast<void*>(
reinterpret_cast<intptr_t>(cp.data()) + i * BATCH_SIZE);
memcpy(dst, src, BATCH_SIZE);
munmap(src, BATCH_SIZE);
}
});
}
for (std::thread& thread : cpthreads) {
thread.join();
}
printf("CP finished successfully! Child exiting.\n");
exit(0);
}
#endif // #ifdef WITH_FORK
// spawn worker threads
std::array<std::thread, NUM_WORKERS> threads;
for (size_t tid = 0; tid < NUM_WORKERS; tid++) {
threads[tid] = std::thread([&, tid] {
// write to array
std::array<inttype, NUM_ELEMS()>::iterator it;
for (it = array.begin() + tid; it < array.end(); it += NUM_WORKERS) {
*it = tid;
}
});
}
timepoint tStart = std::chrono::high_resolution_clock::now();
#ifdef WITH_FORK
// allow reaping child process while workers work
std::thread childWaitThread = std::thread([&] {
if (waitpid(pid, nullptr, 0)) {
perror("waitpid");
}
timepoint tChild = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> durationChild = tChild - tStart;
printf("reunited with child after (s): %lf\n", durationChild.count());
});
#endif
// wait for workers to finish
for (std::thread& thread : threads) {
thread.join();
}
timepoint tEnd = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = tEnd - tStart;
printf("duration (s): %lf\n", duration.count());
#ifdef WITH_FORK
childWaitThread.join();
#endif
}