Я создаю дочерний процесс, используя fork и exec. Использование двух каналов для ввода и получения вывода из этого процесса.
В большинстве случаев он работает нормально, но когда я использую что-то вроде openmp, чтобы проверить, как он работает в параллельных средах, он зависает в системном вызове read
или иногда waitpid
.
Когда я strace
проверил дочерний процесс, я обнаружил, что он также заблокирован на системном вызове read
. Что странно, потому что я жду чтения в родительском процессе только после того, как я предоставил все свои входные данные и закрыто конец записи канала.
Я пытался создать MVCE, но это довольно долго. Я не знаю, как сделать его короче. Я удалил большую часть кода проверки ошибок для простоты.
Обратите внимание, что в моем коде нет глобальных переменных. И я не пытаюсь читать/записывать одни и те же файловые дескрипторы в нескольких потоках.
Я не могу думать о том, что может пойти не так. Так что, надеюсь, вы, ребята, сможете определить, что я делаю неправильно.
Там идет:
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <limits.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
size_t
min(size_t first, size_t second)
{
if (first < second)
{
return first;
}
return second;
}
struct RDI_Buffer
{
char* data;
size_t size;
};
typedef struct RDI_Buffer RDI_Buffer;
RDI_Buffer
rdi_buffer_init()
{
RDI_Buffer b = {0};
return b;
}
RDI_Buffer
rdi_buffer_new(size_t size)
{
RDI_Buffer b;
b.data = malloc(size);
b.size = size;
return b;
}
void
rdi_buffer_free(RDI_Buffer b)
{
if (!b.data)
{
return;
}
free(b.data);
}
RDI_Buffer
rdi_buffer_resize(RDI_Buffer b, size_t new_size)
{
if (!b.data)
{
return rdi_buffer_new(new_size);
}
char* new_data = realloc(b.data, new_size);
if (new_data)
{
b.size = new_size;
b.data = new_data;
return b;
}
RDI_Buffer output = rdi_buffer_new(new_size);
memcpy(output.data, b.data, output.size);
rdi_buffer_free(b);
return output;
}
RDI_Buffer
rdi_buffer_null_terminate(RDI_Buffer b)
{
b = rdi_buffer_resize(b, b.size + 1);
b.data[b.size - 1] = '\0';
return b;
}
static RDI_Buffer
rw_from_fd(int w_fd, int r_fd, RDI_Buffer input)
{
const size_t CHUNK_SIZE = 4096;
assert(input.size <= CHUNK_SIZE);
write(w_fd, input.data, input.size);
close(w_fd);
RDI_Buffer output = rdi_buffer_new(CHUNK_SIZE);
read(r_fd, output.data, CHUNK_SIZE);
close(r_fd);
return output;
}
int main()
{
#pragma omp parallel for
for(size_t i = 0; i < 100; i++)
{
char* thing =
"Hello this is a sort of long text so that we can test how "
"well this works. It should go with cat and be printed.";
RDI_Buffer input_buffer;
input_buffer.data = thing;
input_buffer.size = strlen(thing);
int main_to_sub[2];
int sub_to_main[2];
pipe(main_to_sub);
pipe(sub_to_main);
int pid = fork();
if (pid == 0)
{
dup2(main_to_sub[0], STDIN_FILENO);
dup2(sub_to_main[1], STDOUT_FILENO);
close(main_to_sub[1]);
close(main_to_sub[0]);
close(sub_to_main[1]);
close(sub_to_main[0]);
char* argv[] = {"cat", NULL};
execvp("cat", argv);
exit(1);
}
close(main_to_sub[0]);
close(sub_to_main[1]);
RDI_Buffer output =
rw_from_fd(main_to_sub[1], sub_to_main[0], input_buffer);
int *status = NULL;
waitpid(pid, status, 0);
if (status)
{
printf("%d\n", *status);
}
output = rdi_buffer_null_terminate(output);
if (strcmp(output.data, thing) == 0)
{
printf("good\n");
}
else
{
printf("bad\n");
}
rdi_buffer_free(output);
}
}
Убедитесь, что вы скомпилировали и связали с -fopenmp
. Вот так: gcc main.c -fopenmp
Ну, я действительно упростил программу и удалил проверку ошибок, чтобы уменьшить количество строк и сделать ее более читаемой.
Насколько я понимаю, openmp заключается в том, что он входит в тело цикла n раз за раз, где n — количество потоков (я ошибаюсь?). Таким образом, в любой момент у меня никогда не должно быть более n * 2 файловых дескрипторов, которых на моей машине должно быть около 24.
Вероятно, это n*4 файловых дескриптора, но могут быть ограничения на параллелизм. С форками и исполнителями, а также с потоками жизнь становится сложной. Файловые дескрипторы не могут быть закрыты, потому что файловые дескрипторы являются ресурсом уровня процесса, поэтому поток 1 может создавать файловые дескрипторы, о которых поток 2 не знает, но которые он разделяет. И тогда файловые дескрипторы не закрываются, что мешает cat
правильно определять EOF и т. д.
«Я не могу думать о том, что может пойти не так»: даже не взглянув на код, моя первая мысль была «многопоточность и fork
— вот это хитрая комбинация».
Вся эта передача структур по значению кажется рецептом неприятностей, особенно если учесть, что структуры содержат указатели на динамически выделяемую память. Я не думаю, что это причина вашей проблемы, но это кажется неразумным.
Я запускал размещенный код много раз (в Ubuntu Linux 18.04), и он всегда выполнялся успешно (в результате столбец текста: good
В будущем, если вы удалите проверку ошибок, чтобы следовать части «минимизации» рекомендаций по созданию MCVE (Минимальный, полный, проверяемый пример), об этом также следует сказать в вопросе. Это законно, пока ваш «настоящий» код усердно проверяет все системные вызовы (которые могут дать сбой — такие системные вызовы, как getpid()
, не могут быть ошибочными) на наличие ошибок.
Почему вы игнорируете SIGPIPE в rw_from_fd()? Этого никогда не должно происходить, так как вы контролируете выход кота. У него есть побочные эффекты: первый дочерний элемент (кошка), которого вы запускаете, имеет включенный SIGPIPE; остальные наследуют свой игнорируемый статус от родителя. Я не знаю, имеет ли это значение, но __kmp_install_signals обрабатывает SIGPIPE как особый...
Сколько процессоров у вас есть в наличии?
@mevets игнорирование sigpipe является частью исходного кода. Я должен был удалить его. В исходном коде я игнорирую sigpipes и обрабатываю ошибки локально в функции с помощью errno()
моя машина имеет 12 процессоров
@user3629249 user3629249 Вы уверены, что компилируете и связываетесь с -fopenmp
?
Я добавил -fopenmp
в параметры компиляции. Это не имело никакого значения. Я компилирую, как вы предложили, с gcc
, а не mpicc
@ Адам, когда я перешел на машину с 16 процессорами, она воспроизводится. Вы не сумасшедший, по крайней мере, в одном отношении. Я нахожу, что дети все ждут записи....
Converting comments into an answer.
Возможно, у вас закончились файловые дескрипторы. При параллелизме 100 итераций цикла, создающего 4 файловых дескриптора на каждой итерации, могут привести к проблемам, если ограничение составляет около 256 дескрипторов. Да, вы закрываете некоторые из них быстро, но достаточно быстро? Это не ясно. И неопределенность планирования легко объясняет различное поведение.
The way I understand openmp is that it goes into the loop body n times at a time where n is the number of threads (am I wrong?). So at any single time I should never have more than n*2 file descriptors which on my machine should be around 24.
Вероятно, это n*4 файловых дескриптора, но могут быть ограничения на параллелизм. Я недостаточно знаком с OpenMP, чтобы авторитетно комментировать это. Существуют ли прагмы, отличные от цикла for, которые следует установить? Мне не ясно, что выполнение показанного кода приводит к параллелизму на Mac, когда код скомпилирован с помощью Clang, который не жалуется на #pragma
, в отличие от GCC 9.1.0, который предупреждает о неизвестной прагме при моей компиляции по умолчанию. опции.
Однако с форками и исполнителями, а также с потоками жизнь становится сложной. Файловые дескрипторы не могут быть закрыты, потому что файловые дескрипторы являются ресурсом уровня процесса, поэтому поток 1 может создавать файловые дескрипторы, о которых поток 2 не знает, но которые он разделяет. А затем, когда поток 2 разветвляется, файловые дескрипторы, созданные потоком 1, не закрываются, что не позволяет cat
правильно определять EOF и т. д.
Один из способов проверить это — использовать такую функцию:
#include <sys/stat.h>
static void dump_descriptors(int max_fd)
{
struct stat sb;
for (int fd = 0; fd <= max_fd; fd++)
putchar((fstat(fd, &sb) == 0) ? 'o' : '-');
putchar('\n');
fflush(stdout);
}
и в дочернем коде назовите его подходящим номером (возможно, 64 — может быть случай использования числа до 404). Хотя заманчиво попробовать использовать flockfile(stdout)
и funlockfile(stdout)
в функции, это бессмысленно, если она вызывается только в дочернем процессе, потому что дочерний процесс является однопоточным, и, следовательно, другие потоки не будут вмешиваться в процесс. Однако вполне возможно, что разные процессы могут мешать выходу друг друга.
Если вы собираетесь использовать dump_descriptor()
из потоков родительского процесса, добавьте flockfile(stdout);
перед циклом и funlockfile(stdout);
после вызова fflush()
. Я не уверен, насколько это повлияет на проблему; он обеспечивает однопоточность через эту функцию, потому что ни один из других потоков не может писать в stdout
, пока один поток заблокирован.
Однако, когда я тестировал его с немного измененной версией кода, который выводит PID перед «хорошей» и «плохой» строками и перед выводом dump_descriptors()
, я никогда не видел чередования операций. Я получил вывод, как:
14128: ooooooo----------------------------------------------------------
14128: good
14129: ooooooo----------------------------------------------------------
14129: good
14130: ooooooo----------------------------------------------------------
14130: good
…
14225: ooooooo----------------------------------------------------------
14225: good
14226: ooooooo----------------------------------------------------------
14226: good
14227: ooooooo----------------------------------------------------------
14227: good
что убедительно свидетельствует о том, что в коде не было параллелизма. А когда нет параллелизма, то и беды не увидишь. Каждый раз для пайпов есть 4 дескриптора, и код их аккуратно закрывает.
Рассмотрите возможность перенаправления карты дескриптора в файл (или один файл для каждого дочернего элемента) в вашем сценарии, где вы действительно можете получить серьезный параллелизм.
Обратите внимание, что смешивание потоков с fork()
по своей сути сложно (как Джон Боллинджеротмеченный) — обычно вы используете один или другой механизм, а не оба.
Я думаю, что ваш GCC жалуется, потому что вы должны предоставить -fopenmp при компиляции/ссылке
Этот код работает как часть веб-сервера, где каждый запрос представляет собой поток. Каждый запрос должен порождать дочерний элемент. Поэтому я вынужден использовать как многопоточность, так и fork/exec. Может быть, есть другой способ, о котором я не знаю?
@AdamZahran: Часть проблемы заключалась не в компиляции -fopenmp
; Я нахожу удивительным, что в коде не было #include <omp.h>
, учитывая, что он использует OMP. Я не уверен, насколько это важно. На моем Mac я сталкиваюсь с неопределенным символом __emutls_get_address()
; в некоторых обстоятельствах это кажется известной проблемой, но разочаровывает то, что она затрагивает GCC 9.1.0, когда она была известна еще во времена GCC 4.7 или около того (4.x для умеренно большого x). И есть переменная env OMP_NUM_THREADS
, которая тоже может быть важна. Это то, что люди, которые использовали это, знают.
@AdamZahran: Вы нашли какие-либо доказательства неожиданных открытых файловых дескрипторов? Я вполне уверен, что вы бы. Возможно, вам придется подумать о циклическом просмотре доступных файловых дескрипторов (по крайней мере, до некоторого числа, например 64) и простом закрытии их, поскольку вы не знаете, для чего они нужны, но вашему дочернему процессу они не нужны. Это боль, но если нет центрального реестра дескрипторов файлов и их назначения, это может быть тем, что требуется. Конечно, вы можете сделать больше, чем просто напечатать o
или -
; вы можете определить тип файла и, возможно, имеет ли он набор O_CLOEXEC и т. д.
Я закончил тем, что сделал это, и это сработало. Я просмотрел все открытые файловые дескрипторы и закрыл их сразу после форка. Однако я не понимаю, почему наличие открытых файлов приводит к зависанию ребенка. Я сделал dup2 для файлов каналов на стандартный ввод / стандартный вывод, зачем ему какие-либо другие открытые файлы? Хотел бы я понять, как это работает.
Когда ваш main завис, введите lsof в отдельном сеансе. Я думаю, вы увидите что-то вроде:
....
cat 5323 steve txt REG 252,0 52080 6553613 /bin/cat
cat 5323 steve mem REG 252,0 1868984 17302005 /lib/x86_64-linux-gnu/libc-2.23.so
cat 5323 steve mem REG 252,0 162632 17301981 /lib/x86_64-linux-gnu/ld-2.23.so
cat 5323 steve mem REG 252,0 1668976 12849924 /usr/lib/locale/locale-archive
cat 5323 steve 0r FIFO 0,10 0t0 32079 pipe
cat 5323 steve 1w FIFO 0,10 0t0 32080 pipe
cat 5323 steve 2u CHR 136,0 0t0 3 /dev/pts/0
cat 5323 steve 3r FIFO 0,10 0t0 32889 pipe
cat 5323 steve 4w FIFO 0,10 0t0 32889 pipe
cat 5323 steve 6r FIFO 0,10 0t0 32890 pipe
cat 5323 steve 7r FIFO 0,10 0t0 34359 pipe
cat 5323 steve 8w FIFO 0,10 0t0 32890 pipe
cat 5323 steve 10r FIFO 0,10 0t0 22504 pipe
cat 5323 steve 15w FIFO 0,10 0t0 22504 pipe
cat 5323 steve 16r FIFO 0,10 0t0 22505 pipe
cat 5323 steve 31w FIFO 0,10 0t0 22505 pipe
cat 5323 steve 35r FIFO 0,10 0t0 17257 pipe
cat 5323 steve 47r FIFO 0,10 0t0 31304 pipe
cat 5323 steve 49r FIFO 0,10 0t0 30264 pipe
что вызывает вопрос, откуда все эти трубы? Ваш основной цикл больше не является одним циклом, это набор несинхронизированных параллельных циклов. Посмотрите на шаблон ниже:
void *tdispatch(void *p) {
int to[2], from[2];
pipe(to);
pipe(from);
if (fork() == 0) {
...
} else {
...
pthread_exit(0);
}
}
...
for (int i = 0; i < NCPU; i++) {
pthread_create(..., tdispatch, ...);
}
for (int i = 0; i < NCPU; i++) {
pthread_join(...);
}
Несколько экземпляров tdispatch могут чередовать вызовы pipe(to), pipe(from) и fork(); таким образом, fd просачиваются в эти разветвленные процессы. Я говорю «утечка», потому что раздвоенный процесс не знает, что они там есть.
Канал продолжает отвечать на системные вызовы read(), пока он либо буферизовал данные, либо для него открыт хотя бы один файловый дескриптор записи.
Предположим, что у процесса 5 открыты два обычных конца двух каналов, указывающие на канал № 10 и канал № 11; а процесс 6 имеет канал №12 и канал №13. Но из-за утечки выше процесс 5 также имеет конец записи канала № 12, а процесс 6 имеет конец записи канала № 10. Процессы 5 и 6 никогда не завершатся, потому что они держат открытыми каналы чтения друг друга.
Решение в значительной степени то, о чем говорили люди в предыдущем фрагменте: потоки и вилки — сложная комбинация. Вам нужно будет сериализовать свои каналы, вилки, начальные закрытые биты, чтобы заставить их работать.
Почему ребенок зависает, потому что он наследует открытый канал (в который они не будут читать/записывать)? Почему бы им просто не читать/записывать в свой стандартный ввод/вывод, а затем умирать, оставляя незакрытые каналы для обработки ядра?
Кстати, я решил проблему, закрыв все файловые дескрипторы сразу после разветвления. Я просто не понимаю, почему это было проблемой и почему мое решение работает.
Я не был уведомлен о вашем редактировании, но я рад, что обновил страницу: D Хорошо, теперь я наконец понял. EOF не будет отправлен, или конвейер не будет разорван, если все концы записи не будут закрыты, будь то из основного процесса или одного из дочерних процессов, которые унаследованы по незнанию. а поскольку дети даже не знают об этих открытых файлах, у них нет причин их закрывать, и поэтому происходит взаимоблокировка. Большое спасибо.
Причиной проблемы оказались открытые файлы, которые унаследованы дочерними процессами, как объяснили Джонатан Леффлер и Мевет в своих ответах. Пожалуйста, прочитайте их ответы, если у вас возникла эта проблема, а затем вернитесь к моему ответу, если вы все еще не понимаете или не знаете, что делать.
Я поделюсь своим объяснением так, как я бы понял сразу. Также поделитесь моим кодовым решением проблемы.
Рассмотрим следующий сценарий: Процесс А открывает канал (это два файла).
Процесс A порождает процесс B для связи с ним с помощью канала. Однако он также создает процесс C, который наследует канал (два файла).
Теперь процесс B будет постоянно вызывать read(2)
в канале, что является блокирующим системным вызовом. (будет ждать, пока кто-нибудь не напишет в трубу)
Процесс А завершает запись и закрывает свой конец трубы. Обычно это приводит к сбою системного вызова read(2)
в процессе B и выходу программы (это то, что мы хотим).
Однако в нашем случае, поскольку процесс C имеет открытый конец канала для записи, системный вызов read(2)
в процессе B не завершится ошибкой и заблокирует ожидание записи из открытого конца записи в процессе C.
Все будет хорошо, когда процесс C просто завершится.
Настоящий тупик возник бы в другом сценарии, когда и B, и C держат трубы друг для друга (как объяснено в ответе Мевета). Каждый из них будет ждать, пока другой закроет свои концы труб. Что никогда не произойдет, вызывая тупик.
Мое решение состояло в том, чтобы закрыть все открытые файлы, которые мне не нужны, сразу после fork(2)
int pid = fork();
if (pid == 0)
{
int exceptions[2] = {main_to_sub[0], sub_to_main[1]};
close_all_descriptors(exceptions);
dup2(main_to_sub[0], STDIN_FILENO);
dup2(sub_to_main[1], STDOUT_FILENO);
close(main_to_sub[0]);
close(sub_to_main[1]);
char* argv[] = {"cat", NULL};
execvp("cat", argv);
exit(1);
}
Вот реализация close_all_descriptors
#include <fcntl.h>
#include <errno.h>
static int
is_within(int fd, int arr[2])
{
for(int i = 0; i < 2; i++)
{
if (fd == arr[i])
{
return 1;
}
}
return 0;
}
static int
fd_is_valid(int fd)
{
return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
}
static void
close_all_descriptors(int exceptions[2])
{
// getdtablesize returns the max number of files that can be open. It's 1024 on my system
const int max_fd = getdtablesize();
// starting at 3 because I don't want to close stdin/out/err
// let dup2(2) do that
for (int fd = 3; fd <= max_fd; fd++)
{
if (fd_is_valid(fd) && !is_within(fd, exceptions))
{
close(fd);
}
}
}
Вы не проверяете наличие ошибок в системных вызовах. Вам следует. Возможно, у вас закончились файловые дескрипторы. При параллелизме 100 итераций цикла, создающего 4 файловых дескриптора на каждой итерации, могут привести к проблемам, если ограничение составляет около 256 дескрипторов. Да, вы закрываете некоторые из них быстро, но достаточно быстро? Не ясно. И неопределенность планирования легко объясняет различное поведение.