В Unix/Linux, что происходит с моими активными объектами RAII при форкировании? Будут ли двойные удаления? Что такое построение копии и назначение? Как убедиться, что ничего плохого не происходит?
Что происходит с объектами RAII после вилки процесса?
Ответ 1
fork(2)
создает полную копию процесса, включая всю его память. Да, деструкторы автоматических объектов будут выполняться дважды - в родительском процессе и в дочернем процессе, в отдельных пространствах виртуальной памяти. Ничего "плохого" не происходит (если, конечно, вы не вычитаете деньги из учетной записи в деструкторе), вам просто нужно знать об этом факте.
Ответ 2
В принципе, не стоит использовать эти функции в С++, но вы должны знать, какие данные используются совместно и как.
Учтите, что при fork()
новый процесс получает полную копию родительской памяти (используя copy-on-write). Память - это состояние, поэтому
у вас есть два независимых процесса, которые должны оставить чистое состояние.
Теперь, пока вы остаетесь в пределах предоставленной вам памяти, у вас не должно быть никаких проблем:
#include <iostream>
#include <unistd.h>
class Foo {
public:
Foo () { std::cout << "Foo():" << this << std::endl; }
~Foo() { std::cout << "~Foo():" << this << std::endl; }
Foo (Foo const &) {
std::cout << "Foo::Foo():" << this << std::endl;
}
Foo& operator= (Foo const &) {
std::cout << "Foo::operator=():" << this<< std::endl;
return *this;
}
};
int main () {
Foo foo;
int pid = fork();
if (pid > 0) {
// We are parent.
int childExitStatus;
waitpid(pid, &childExitStatus, 0); // wait until child exits
} else if (pid == 0) {
// We are the new process.
} else {
// fork() failed.
}
}
Выше программа будет печатать грубо:
Foo():0xbfb8b26f
~Foo():0xbfb8b26f
~Foo():0xbfb8b26f
Никакая копирование или копирование не происходит, ОС будет делать побитовые копии. Адреса одинаковы, поскольку они не являются физическими адресами, а указателями в пространство виртуальной памяти каждого процесса.
Сложнее, когда два экземпляра обмениваются информацией, например. открытый файл, который должен быть сброшен и закрыт перед выходом:
#include <iostream>
#include <fstream>
int main () {
std::ofstream of ("meh");
srand(clock());
int pid = fork();
if (pid > 0) {
// We are parent.
sleep(rand()%3);
of << "parent" << std::endl;
int childExitStatus;
waitpid(pid, &childExitStatus, 0); // wait until child exits
} else if (pid == 0) {
// We are the new process.
sleep(rand()%3);
of << "child" << std::endl;
} else {
// fork() failed.
}
}
Это может печатать
parent
или
child
parent
или что-то еще.
Проблема в том, что двум экземплярам недостаточно, чтобы координировать их доступ к одному файлу, и вы не знаете детали реализации std::ofstream
.
(Возможные) решения можно найти в терминах "Interprocess Communication" или "IPC", наиболее близким будет waitpid()
:
#include <unistd.h>
#include <sys/wait.h>
int main () {
pid_t pid = fork();
if (pid > 0) {
int childExitStatus;
waitpid(pid, &childExitStatus, 0); // wait until child exits
} else if (pid == 0) {
...
} else {
// fork() failed.
}
}
Самое простое решение - обеспечить, чтобы каждый процесс использовал только свою собственную виртуальную память и ничего больше.
Другое решение представляет собой конкретный Linux: убедитесь, что подпроцесс не очищается. Операционная система сделает необработанную, не RAII-очистку всей приобретенной памяти и закроет все открытые файлы без их промывки.
Это может быть полезно, если вы используете fork()
с exec()
для запуска другого процесса:
#include <unistd.h>
#include <sys/wait.h>
int main () {
pid_t pid = fork();
if (pid > 0) {
// We are parent.
int childExitStatus;
waitpid(pid, &childExitStatus, 0);
} else if (pid == 0) {
// We are the new process.
execlp("echo", "echo", "hello, exec", (char*)0);
// only here if exec failed
} else {
// fork() failed.
}
}
Другой способ просто выйти без запуска каких-либо деструкторов - это функция exit()
. Я вообще совет, чтобы не использовать в С++, но при разветвлении он имеет свое место.
Литература:
- http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html
- справочные страницы
Ответ 3
В принятом в настоящее время ответе показана проблема синхронизации, которая, откровенно говоря, не имеет никакого отношения к тем, какие проблемы может вызвать RAII. То есть, используете ли вы RAII или нет, у вас будут проблемы с синхронизацией между родителем-потомком. Если вы выполняете один и тот же процесс в двух разных консолях, у вас есть такая же проблема синхронизация! (т.е. нет fork()
, участвующих в вашей программе, просто ваша программа работает дважды параллельно.)
Чтобы решить проблемы синхронизации, вы можете использовать семафор. См. sema_open(3)
и связанные функции. Обратите внимание, что поток будет генерировать те же самые проблемы синхронизации. Только вы можете использовать мьютексы для синхронизации нескольких потоков.
Итак, когда у вас проблема с RAII, вы используете ее для хранения того, что я называю внешним ресурсом, хотя все внешние ресурсы не затрагиваются одинаково. У меня была проблема в двух обстоятельствах, и я покажу их здесь.
Не завершать работу() сокет
Скажите, что у вас есть собственный класс сокетов. В деструкторе вы завершаете работу. В конце концов, как только вы закончите, вы также можете отправить сообщение на другой конец сокета, в котором говорится, что вы закончили соединение:
class my_socket
{
public:
my_socket(char * addr)
{
socket_ = socket(s)
...bind, connect...
}
~my_socket()
{
shutdown(socket_, SHUT_RDWR);
close(socket_);
}
private:
int socket_ = -1;
};
Когда вы используете этот класс RAII, функция shutdown()
влияет на сокет в родительском и дочернем. Это означает, что и родитель, и ребенок не могут читать и писать в этот сокет. Здесь я полагаю, что ребенок вообще не использует сокет (и, следовательно, у меня нет абсолютно проблем с синхронизацией), но когда ребенок умирает, класс RAII просыпается и деструктор получает вызов. В этот момент он отключает сокет, который становится непригодным.
{
my_socket soc("127.0.0.1:1234");
// do something with soc in parent
...
pid_t const pid(fork());
if(pid == 0)
{
int status(0);
waitpid(pid, &status, 0);
}
else if(pid > 0)
{
// the fork() "duplicated" all memory (with copy-on-write for most)
// and duplicated all descriptors (see dup(2)) which is why
// calling 'close(s)' is perfectly safe in the child process.
// child does some work
...
// here 'soc' calls my_socket::~my_socket()
return;
}
else
{
// fork did not work
...
}
// here my_socket::~my_socket() was called in child and
// the socket was shutdown -- therefore it cannot be used
// anymore!
// do more work in parent, but cannot use 'soc'
// (which is probably not the wanted behavior!)
...
}
Избегайте использования сокета в родительском и дочернем
Другая возможность, все еще с сокетом (хотя вы могли бы иметь такой же эффект с трубой или каким-либо другим механизмом для внешней связи), состоит в том, чтобы в конечном итоге передать команду "BYE" дважды. Это на самом деле очень близко к проблеме синхронизации, но в этом случае эта синхронизация происходит в объекте RAII при его уничтожении.
Скажите, например, что вы создаете сокет и управляете им в объекте. Всякий раз, когда объект уничтожается, вы хотите сообщить другой стороне, отправив команду "BYE":
class communicator
{
public:
communicator()
{
socket_ = socket();
...bind, connect...
}
~communicator()
{
write(socket_, "BYE\n", 4);
// shutdown(socket_); -- now we know not to do that!
close(socket_);
}
private
int socket_ = -1;
};
В этом случае другой конец получает команду "BYE" и закрывает соединение. Теперь родитель не может связаться с этим сокетом, так как он закрыт!
Это очень похоже на то, о чем говорит Фреснель с его примером. Только синхронизировать нелегко. Порядок, в котором вы пишете "BYE\n" или другую команду в сокете, не изменит того факта, что в конце сокет будет закрыт с другой стороны (т.е. синхронизация может быть достигнута с использованием межоперационная блокировка, тогда как команда "BYE" похожа на команду shutdown()
, она останавливает связь на своем пути!)
Решение
Для shutdown()
это было достаточно просто, мы просто не вызываем функцию. При этом, возможно, вы все еще хотели иметь shutdown()
в родительском, а не в дочернем.
Есть несколько способов исправить проблему, одна из них - запомнить pid и использовать его, чтобы узнать, должны ли вызываться вызовы с деструктивными функциями или нет. Существует предложение, которое я предлагаю:
class communicator
{
communicator()
: pid_(getpid())
{
socket_ = socket();
...bind, connect...
}
~communicator()
{
if(pid_ == getpid())
{
write(socket_, "BYE\n", 4);
shutdown(socket_, SHUT_RDWR);
}
close(socket_);
}
private:
pid_t pid_;
int socket_;
};
Здесь мы делаем write()
и shutdown()
, только если мы находимся в родительском.
Обратите внимание, что дочерний элемент может (и, как ожидается,) сделать close()
в дескрипторе сокета с fork()
, называемым dup()
, во всех дескрипторах, чтобы у ребенка был другой файловый дескриптор для каждого файла, который он хранит.
Другой охранник
Теперь могут быть более сложные случаи, когда объект RAII создается в родительском роде, и ребенок в любом случае вызовет деструктор этого объекта RAII. Как упоминалось roemcke, вызов exit()
, вероятно, является самым безопасным делом. Другими словами, вместо использования return
вызовите exit()
.
pid_t r(fork());
if(r == 0)
{
try
{
...child do work here...
}
catch(...)
{
// you probably want to log a message here...
}
exit(0); // prevent stack unfolding
/* NOT REACHED */
}
Это в любом случае намного безопаснее только потому, что вы, вероятно, не хотите, чтобы ребенок возвращался в "родительский код", где могло случиться много других вещей. Не только разворачивание стека. (т.е. продолжение цикла for(), в котором ребенок не должен продолжать...)
Функция exit()
не возвращается, поэтому деструкторы объектов, определенных в стеке, не вызываются. "Try/catch" очень важенздесь, поскольку exit()
не будет вызываться, если ребенок вызывает исключение, хотя он должен вызывать функцию terminate()
, которая также не будет уничтожать все выделенные кучей объекты, она вызывает функцию terminate()
после ее развертывания стек и, следовательно, вероятно, вызвали все ваши деструкторы RAII... и снова не то, что вы ожидаете.
Ответ 4
Если вы не знаете, что вы делаете, дочерний процесс должен всегда вызывать _exit() после того, как он сделал свой материал:
pid_t pid = fork()
if (pid == 0)
{
do_some_stuff(); // Make sure this doesn't throw anything
_exit(0);
}
Подчеркивание важно. Не вызывайте exit() в дочернем процессе, он очищает потоковые буферы на диск (или там, где указывает указатель filedescriptor), и вы получите написанное дважды.