Способ заставить файловый дескриптор закрыться, чтобы pclose() не блокировался?
Я создаю канал, используя popen(), и процесс вызывает сторонний инструмент, который в некоторых редких случаях мне нужно завершить.
::popen(thirdPartyCommand.c_str(), "w");
Если я просто выбрасываю исключение и раскручиваю стек, мой метод раскрутки пытается вызвать pclose() для стороннего процесса, результаты которого мне больше не нужны. Однако pclose() никогда не возвращается, так как блокируется следующей трассировкой стека в Centos 4:
#0 0xffffe410 in __kernel_vsyscall ()
#1 0x00807dc3 in __waitpid_nocancel () from /lib/libc.so.6
#2 0x007d0abe in _IO_proc_close@@GLIBC_2.1 () from /lib/libc.so.6
#3 0x007daf38 in _IO_new_file_close_it () from /lib/libc.so.6
#4 0x007cec6e in fclose@@GLIBC_2.1 () from /lib/libc.so.6
#5 0x007d6cfd in pclose@@GLIBC_2.1 () from /lib/libc.so.6
Есть ли способ заставить вызов pclose() быть успешным перед вызовом, чтобы я мог программно избежать этой ситуации, когда мой процесс зависает, ожидая успешного завершения pclose(), когда это никогда не произойдет, потому что я прекратил вводить данные для процесс popen() и хотите выбросить его работу?
Должен ли я как-то записать конец файла в дескриптор файла ed popen(), прежде чем пытаться закрыть его?
Обратите внимание, что стороннее программное обеспечение разветвляется. На момент зависания pclose() существует четыре процесса, один из которых не функционирует:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
abc 6870 0.0 0.0 8696 972 ? S 04:39 0:00 sh -c /usr/local/bin/third_party /home/arg1 /home/arg2 2>&1
abc 6871 0.0 0.0 10172 4296 ? S 04:39 0:00 /usr/local/bin/third_party /home/arg1 /home/arg2
abc 6874 99.8 0.0 10180 1604 ? R 04:39 141:44 /usr/local/bin/third_party /home/arg1 /home/arg2
abc 6875 0.0 0.0 0 0 ? Z 04:39 0:00 [third_party] <defunct>
2 ответа
Я вижу два решения здесь:
- Аккуратный: ты
fork()
,pipe()
а такжеexecve()
(или что-нибудь вexec
семья, конечно...) "вручную", тогда вам решать, хотите ли вы, чтобы ваши дети стали зомби или нет. (т.е.wait()
для них или нет) - Уродливый: если вы уверены, что в данный момент запущен только один из этих дочерних процессов, вы можете использовать
sysctl()
чтобы проверить, есть ли какой-либо процесс с таким именемpclose()
... юк.
Я настоятельно рекомендую здесь аккуратный способ, или вы можете просто попросить ответственного за исправление этого бесконечного цикла в вашем стороннем инструменте, ха-ха.
Удачи!
РЕДАКТИРОВАТЬ:
Для вас первый вопрос: я не знаю. Проведение некоторых исследований о том, как найти процессы по имени, используяsysctl()
должен сказать тебе то, что тебе нужно знать, я сам никогда не заходил так далеко.
На ваш второй и третий вопрос: popen()
в основном это обертка для fork()
+ pipe()
+ dup2()
+ execl()
,
fork()
дублирует процесс, execl()
заменяет изображение дублированного процесса новым, pipe()
обрабатывает межпроцессное взаимодействие и dup2()
используется для перенаправления вывода... А потом pclose()
будут wait()
чтобы умер дублированный процесс, вот почему мы здесь.
Если вы хотите узнать больше, вы должны проверить этот ответ, где я недавно объяснил, как выполнить простой форк со стандартным IPC. В этом случае это немного сложнее, так как вы должны использовать dup2()
перенаправить стандартный вывод на вашу трубу.
Вы также должны взглянуть на popen()/pclose()
исходники, так как они конечно с открытым исходным кодом.
Наконец, вот краткий пример, я не могу сделать это более ясным:
int pipefd[2];
pipe(pipefd);
if (fork() == 0) // I'm the child
{
close(pipefd[0]); // I'm not going to read from this pipe
dup2(pipefd[1], 1); // redirect standard output to the pipe
close(pipefd[1]); // it has been duplicated, close it as we don't need it anymore
execve()/execl()/execsomething()... // execute the program you want
}
else // I'm the parent
{
close(pipefd[1]); // I'm not going to write to this pipe
while (read(pipefd[0], &buf, 1) > 0) // read while EOF
write(1, &buf, 1);
close(pipefd[1]); // cleaning
}
И, как всегда, не забудьте прочитать справочные страницы и проверить все ваши возвращаемые значения.
Снова удачи!
Другое решение - убить всех своих детей. Если вы знаете, что ваши дочерние процессы - это процессы, которые запускаются, когда вы popen()
тогда это достаточно просто. В противном случае вам может потребоваться дополнительная работа или использовать fork()
+ execve()
комбо, в этом случае вы будете знать PID первого ребенка.
Всякий раз, когда вы запускаете дочерний процесс, его PPID (идентификатор родительского процесса) является вашим собственным PID. Достаточно просто прочитать список запущенных процессов и собрать те, которые имеют свои PPID = getpid()
, Повторите цикл, ища процессы, у которых PPID равен одному из PID ваших детей. В конце вы создаете целое дерево дочерних процессов.
Поскольку ваши дочерние процессы могут в конечном итоге создавать другие дочерние процессы, чтобы сделать их безопасными, вы захотите заблокировать эти процессы, отправив SIGSTOP
, Таким образом они перестанут создавать новых детей. Насколько я знаю, вы не можете предотвратить SIGSTOP
делать свое дело.
Процесс поэтому:
function kill_all_children()
{
std::vector<pid_t> me_and_children;
me_and_children.push_back(getpid());
bool found_child = false;
do
{
found_child = false;
std::vector<process> processes(get_processes());
for(auto p : processes)
{
// i.e. if I'm the child of any one of those processes
if(std::find(me_and_children.begin(),
me_and_children.end(),
p.ppid()))
{
kill(p.pid(), SIGSTOP);
me_and_children.push_back(p.pid());
found_child = true;
}
}
}
while(found_child);
for(auto c : me_and_children)
{
// ignore ourselves
if(c == getpid())
{
continue;
}
kill(c, SIGTERM);
kill(c, SIGCONT); // make sure it continues now
}
}
Это, вероятно, не лучший способ закрыть ваш канал, так как вам, вероятно, нужно дать командному времени обработать ваши данные. Итак, вы хотите выполнить этот код только после тайм-аута. Так что ваш обычный код может выглядеть примерно так:
void send_data(...)
{
signal(SIGALRM, handle_alarm);
f = popen("command", "w");
// do some work...
alarm(60); // give it a minute
pclose(f);
alarm(0); // remove alarm
}
void handle_alarm()
{
kill_all_children();
}
- о alarm(60);
, местоположение зависит от вас, оно также может быть размещено до popen()
если ты боишься что popen()
или работа после того, как это могло также потерпеть неудачу (то есть у меня были проблемы, когда труба заполняется, и я даже не достигаю pclose()
потому что тогда дочерний процесс зацикливается навсегда.)
Обратите внимание, что alarm()
может быть не самая лучшая идея в мире. Вы можете предпочесть использовать нить со сном из poll()
или же select()
на FD, который вы можете проснуться по мере необходимости. Таким образом, поток будет вызывать kill_all_children()
после сна, но вы можете отправить ему сообщение, чтобы он проснулся рано и сообщил, что pclose()
случилось, как и ожидалось.
Примечание: я оставил реализацию get_processes()
из этого ответа. Вы можете прочитать это из /proc
или с libprocps
библиотека. У меня есть такая реализация в моем проекте snapwebsites. Это называется process_list
, Вы можете просто пожинать этот класс.
Я использую popen() для вызова дочернего процесса, которому не нужны stdin или stdout, он просто запускается на короткое время для выполнения своей работы, а затем останавливается сам. Возможно, вызов этого типа дочернего процесса должен выполняться с помощью system()? В любом случае, впоследствии используется pclose(), чтобы убедиться, что дочерний процесс завершился правильно.
При определенных условиях этот дочерний процесс продолжает работать бесконечно. pclose() блокируется навсегда, поэтому мой родительский процесс также зависает. Загрузка ЦП достигает 100%, другие исполняемые файлы не работают, а вся моя встроенная система рушится. Я пришел сюда в поисках решений.
Решение 1 от @cmc: разложение popen() на fork(), pipe(), dup2() и execl(). Возможно, это просто вопрос личного вкуса, но я не хочу сам переписывать идеальные системные вызовы. Я бы просто ввел новые ошибки.
Решение 2 от @cmc: проверка фактического существования дочернего процесса с помощью sysctl(), чтобы убедиться, что pclose() вернется успешно. Я считаю, что это каким-то образом обходит проблему с OP @WilliamKF - определенно есть дочерний процесс, он просто перестал отвечать. Отказ от вызова pclose() этого не решит. [Кстати, за 7 лет, прошедших с тех пор, как @cmc написал этот ответ, похоже, что sysctl () устарела.]
Решение 3 от @Alexis Wilke: убийство дочернего процесса. Мне больше всего нравится такой подход. Он в основном автоматизирует то, что я делал, когда вручную вмешивался, чтобы реанимировать умирающую встроенную систему. Проблема с моим упорным соблюдением popen() заключается в том, что я не получаю PID от дочернего процесса. Я тщетно пытался с
waitid(P_PGID, getpgrp(), &child_info, WNOHANG);
но все, что я получаю в моей системе Debian Linux 4.19, - это EINVAL.
Итак, вот что я сколотил. Я ищу дочерний процесс по имени; Я могу позволить себе использовать несколько сокращений, так как уверен, что будет только один процесс с таким именем. По иронии судьбы, утилита командной строки ps вызывается еще одним popen(). Это не принесет никаких призов за элегантность, но, по крайней мере, моя встроенная система сейчас остается на плаву.
FILE* child = popen("child", "r");
if (child)
{
int nr_loops;
int child_pid;
for (nr_loops=10; nr_loops; nr_loops--)
{
FILE* ps = popen("ps | grep child | grep -v grep | grep -v \"sh -c \" | sed \'s/^ *//\' | sed \'s/ .*$//\'", "r");
child_pid = 0;
int found = fscanf(ps, "%d", &child_pid);
pclose(ps);
if (found != 1)
// The child process is no longer running, no risk of blocking pclose()
break;
syslog(LOG_WARNING, "child running PID %d", child_pid);
usleep(1000000); // 1 second
}
if (!nr_loops)
{
// Time to kill this runaway child
syslog(LOG_ERR, "killing PID %d", child_pid);
kill(child_pid, SIGTERM);
}
pclose(child); // Even after it had to be killed
} /* if (child) */
Я на собственном горьком опыте узнал, что мне нужно связать каждый popen() с pclose(), иначе я накапливаю зомби-процессы. Я считаю примечательным, что это необходимо после прямого убийства; Я полагаю, это потому, что, согласно справочной странице, popen() фактически запускает sh -c с дочерним процессом в нем, и это окружающее sh становится зомби.