当前接受的答案显示了一个同步问题,坦率地说,这与 RAII 真正引起的问题无关。也就是说,无论你是否使用 RAII,你都会遇到父子之间的同步问题。哎呀,如果您在两个不同的控制台中运行相同的进程,您将遇到完全相同的同步问题! (即没有fork() 参与您的程序,只是您的程序并行运行两次。)
要解决同步问题,您可以使用信号量。请参阅sema_open(3) 和相关功能。请注意,线程会产生完全相同的同步问题。只有您可以使用互斥锁来同步多个线程,并且在大多数情况下,互斥锁比信号量快得多。..
因此,当您使用 RAII 来保留我所说的外部资源时,您确实会遇到问题,尽管所有外部资源都不会受到相同的影响。我在两种情况下都遇到过这个问题,我会在这里展示这两种情况。
不要关闭()套接字
假设您有自己的套接字类。在析构函数中,您执行关闭操作。毕竟,一旦完成,您也可以向套接字的另一端发送一条消息,说明您已完成连接:
class my_socket
{
public:
my_socket(char * addr)
{
socket_ = socket(s)
...bind, connect...
}
~my_socket()
{
if(_socket != -1)
{
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!)
...
}
避免在父子中使用socket
另一种可能性,仍然使用套接字(尽管您可以使用管道或其他用于外部通信的机制具有相同的效果)是最终发送两次“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”命令并关闭连接。现在父级无法使用该套接字进行通信,因为它已被另一端关闭!
这与 phresnel 在他的 ofstream 示例中所说的非常相似。只是,修复同步并不容易。您向套接字写入“BYE\n”或其他命令的顺序不会改变最终套接字从另一端关闭的事实(即 同步 可以使用进程间锁,而"BYE" 命令类似于shutdown() 命令,它会停止通信!)
解决方案
对于shutdown(),这很简单,我们只是不调用该函数。话虽如此,也许您仍然希望shutdown() 发生在父级中,而不是在子级中。
有几种方法可以解决这个问题,其中之一是记住 pid 并使用它来知道是否应该调用这些 破坏性 函数调用。有一个可能的解决方法:
class communicator
{
communicator()
: pid_(getpid())
{
socket_ = socket();
...bind, connect...
}
~communicator()
{
if(socket_ != -1)
{
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() 可能是最安全的做法(exit() 在大多数情况下有效,但它可能会对父母产生不良副作用,同时,孩子可能需要exit()干净地结束——即删除它创建的tmpfile()!)。换句话说,不要使用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 and calls to atexit() functions
/* NOT REACHED */
}
这无论如何都要安全得多,因为您可能不希望孩子返回“父母的代码”,因为那里可能会发生许多其他事情。不只是堆栈展开。 (即继续一个孩子不应该继续的for()循环......)
_exit() 函数不会返回,因此堆栈上定义的对象的析构函数不会被调用。 try/catch 在这里非常重要,因为如果孩子引发异常,_exit() 不会被调用,尽管它应该调用 terminate() 函数,这也不会破坏所有堆分配的对象,它在展开堆栈后调用terminate() 函数,因此可能调用了所有的 RAII 析构函数……这又不是你所期望的。
exit() 和_exit() 的区别在于前者调用你atexit() 函数。您相对很少需要在孩子或父母身上这样做。至少,我从来没有任何奇怪的副作用。但是,一些库确实使用了atexit(),而不考虑fork() 被调用的可能性。在atexit() 函数中保护自己的一种方法是记录需要atexit() 函数的进程的PID。如果函数被调用时 PID 不匹配,那么您只需返回并且不执行任何其他操作。
pid_t cleanup_pid = -1;
void cleanup()
{
if(cleanup_pid != getpid())
{
return;
}
... do your clean up here ...
}
void some_function_requiring_cleanup()
{
if(cleanup_pid != getpid())
{
cleanup_pid = getpid();
atexit(cleanup);
}
... do work requiring cleanup ...
}
显然,使用atexit() 并正确使用的库的数量可能非常接近于 0。所以...您应该避免使用此类库。
请记住,如果您调用 execve() 或 _exit(),则不会进行清理。因此,如果在子进程中调用tmpfile() + _exit(),该临时文件将不会被自动删除...