我在掌握如何正确处理从多线程程序中以多线程方式使用Boost Asio创建子进程时遇到了一些麻烦。
如果我理解正确,在Unix世界中启动子进程的方法是调用fork()
,然后调用exec*()
。另外,如果我理解正确,调用fork()
将复制所有文件描述符等等,这些需要在子进程中关闭,除非标记为FD_CLOEXEC
(从而在调用exec*()
时原子地关闭。
Boost Asio要求在调用fork()
时收到通知,以便通过调用notify_fork()
来正常运行。但是,在多线程程序中,这会产生几个问题:
如果我理解正确,套接字默认由子进程继承。它们可以设置为SOCK_CLOEXEC
- 但不能直接创建 *,因此如果从另一个线程创建子进程,则会导致计时窗口。
notify_fork()
要求没有其他线程调用任何其他io_service
函数,也不要求与{关联的任何其他I / O对象上的任何函数{1}} 的。这似乎并不可行 - 毕竟程序是多线程的。
如果我理解正确,io_service
和fork()
之间进行的任何函数调用都需要异步信号安全(请参阅fork()
documentation)。没有exec*()
调用的文档是异步信号安全的。事实上,如果我查看Boost Asio的源代码(至少在版本1.54中),可能会调用pthread_mutex_lock,如果我理解正确,则不异步信号是安全的(请参阅Signal Concepts,还有其他调用不在白名单上。)
问题#1我可以通过分离子进程和套接字+文件的创建来解决这个问题,这样我就可以确保在创建的套接字和设置notify_fork()
之间的窗口中没有创建子进程。问题#2比较棘手,我可能需要确保停止所有所有 asio处理程序线程,执行fork然后再次重新创建它们,这最多是潮流,而且最坏的情况真的很糟糕(我的待定时间器怎么样?)。问题#3似乎完全无法正确使用它。
如何在多线程程序中与SOCK_CLOEXEC
+ fork()
一起正确使用Boost Asio?
......还是我“分叉”?
如果我误解了任何基本概念(请在Windows编程中提出,而不是* nix ......),请告诉我。
编辑:
* - 实际上可以在Linux上创建直接设置exec*()
的套接字,自2.6.27起可用(参见socket()
documentation)。在Windows上,自Windows 7 SP 1 / Windows Server 2008 R2 SP 1起可以使用相应的标志SOCK_CLOEXEC
(请参阅WSASocket()
documentation)。 OS X似乎并不支持这一点。
答案 0 :(得分:9)
在多线程程序中,io_service::notify_fork()
在子进程中调用是不安全的。然而,Boost.Asio期望它基于fork()
support来调用,因为这是孩子关闭父母以前的内部文件描述符并创建新的描述符。虽然Boost.Asio明确列出了调用io_service::notify_fork()
的前提条件,并在fork()
期间保证其内部组件的状态,但对implementation的简要说明表明std::vector::push_back()
可以从免费商店分配内存,并且不保证分配是异步信号安全的。
话虽如此,一个可能值得考虑的解决方案是fork()
当它仍处于单线程时的过程。子进程将保持单线程,并在父进程通过进程间通信告知它时执行fork()
和exec()
。这种分离通过在执行fork()
和exec()
时无需管理多个线程的状态来简化问题。
以下是演示此方法的完整示例,其中多线程服务器将通过UDP接收文件名,子进程将执行fork()
和exec()
以对文件名运行/usr/bin/touch
。为了让示例更具可读性,我选择使用stackful coroutines。
#include <unistd.h> // execl, fork
#include <iostream>
#include <string>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/make_shared.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
/// @brief launcher receives a command from inter-process communication,
/// and will then fork, allowing the child process to return to
/// the caller.
class launcher
{
public:
launcher(boost::asio::io_service& io_service,
boost::asio::local::datagram_protocol::socket& socket,
std::string& command)
: io_service_(io_service),
socket_(socket),
command_(command)
{}
void operator()(boost::asio::yield_context yield)
{
std::vector<char> buffer;
while (command_.empty())
{
// Wait for server to write data.
std::cout << "launcher is waiting for data" << std::endl;
socket_.async_receive(boost::asio::null_buffers(), yield);
// Resize buffer and read all data.
buffer.resize(socket_.available());
socket_.receive(boost::asio::buffer(buffer));
io_service_.notify_fork(boost::asio::io_service::fork_prepare);
if (fork() == 0) // child
{
io_service_.notify_fork(boost::asio::io_service::fork_child);
command_.assign(buffer.begin(), buffer.end());
}
else // parent
{
io_service_.notify_fork(boost::asio::io_service::fork_parent);
}
}
}
private:
boost::asio::io_service& io_service_;
boost::asio::local::datagram_protocol::socket& socket_;
std::string& command_;
};
using boost::asio::ip::udp;
/// @brief server reads filenames from UDP and then uses
/// inter-process communication to delegate forking and exec
/// to the child launcher process.
class server
{
public:
server(boost::asio::io_service& io_service,
boost::asio::local::datagram_protocol::socket& socket,
short port)
: io_service_(io_service),
launcher_socket_(socket),
socket_(boost::make_shared<udp::socket>(
boost::ref(io_service), udp::endpoint(udp::v4(), port)))
{}
void operator()(boost::asio::yield_context yield)
{
udp::endpoint sender_endpoint;
std::vector<char> buffer;
for (;;)
{
std::cout << "server is waiting for data" << std::endl;
// Wait for data to become available.
socket_->async_receive_from(boost::asio::null_buffers(),
sender_endpoint, yield);
// Resize buffer and read all data.
buffer.resize(socket_->available());
socket_->receive_from(boost::asio::buffer(buffer), sender_endpoint);
std::cout << "server got data: ";
std::cout.write(&buffer[0], buffer.size());
std::cout << std::endl;
// Write filename to launcher.
launcher_socket_.async_send(boost::asio::buffer(buffer), yield);
}
}
private:
boost::asio::io_service& io_service_;
boost::asio::local::datagram_protocol::socket& launcher_socket_;
// To be used as a coroutine, server must be copyable, so make socket_
// copyable.
boost::shared_ptr<udp::socket> socket_;
};
int main(int argc, char* argv[])
{
std::string filename;
// Try/catch provides exception handling, but also allows for the lifetime
// of the io_service and its IO objects to be controlled.
try
{
if (argc != 2)
{
std::cerr << "Usage: <port>\n";
return 1;
}
boost::thread_group threads;
boost::asio::io_service io_service;
// Create two connected sockets for inter-process communication.
boost::asio::local::datagram_protocol::socket parent_socket(io_service);
boost::asio::local::datagram_protocol::socket child_socket(io_service);
boost::asio::local::connect_pair(parent_socket, child_socket);
io_service.notify_fork(boost::asio::io_service::fork_prepare);
if (fork() == 0) // child
{
io_service.notify_fork(boost::asio::io_service::fork_child);
parent_socket.close();
boost::asio::spawn(io_service,
launcher(io_service, child_socket, filename));
}
else // parent
{
io_service.notify_fork(boost::asio::io_service::fork_parent);
child_socket.close();
boost::asio::spawn(io_service,
server(io_service, parent_socket, std::atoi(argv[1])));
// Spawn additional threads.
for (std::size_t i = 0; i < 3; ++i)
{
threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service));
}
}
io_service.run();
threads.join_all();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
// Now that the io_service and IO objects have been destroyed, all internal
// Boost.Asio file descriptors have been closed, so the execl should be
// in a clean state. If the filename has been set, then exec touch.
if (!filename.empty())
{
std::cout << "creating file: " << filename << std::endl;
execl("/usr/bin/touch", "touch", filename.c_str(), static_cast<char*>(0));
}
}
1号航站楼:
$ ls a.out example.cpp $ ./a.out 12345 server is waiting for data launcher is waiting for data server got data: a server is waiting for data launcher is waiting for data creating file: a server got data: b server is waiting for data launcher is waiting for data creating file: b server got data: c server is waiting for data launcher is waiting for data creating file: c ctrl + c $ ls a a.out b c example.cpp
2号航站楼:
$ nc -u 127.0.0.1 12345 actrl + dbctrl + dcctrl + d
答案 1 :(得分:3)
请考虑以下事项:
fork()
在子进程中只创建一个线程。您需要重新创建其他线程。fork()
。在pthread_atfork()
注册的回调可以释放互斥锁,但大多数库从不打扰使用pthread_atfork()
。换句话说,当调用malloc()
或new
时,您的子进程可能会永远挂起,因为标准堆分配器确实使用了互斥锁。鉴于上述情况,多线程流程中唯一可靠的选项是调用fork()
然后调用exec()
。
请注意,只要fork()
处理程序未被使用,您的父进程就不会受pthread_atfork()
影响。
关于分叉和boost::asio
,有一个io_service::notify_fork()
函数需要在父级分叉之前和父级和子级分叉之后调用。它的作用最终取决于所用的反应器。对于Linux / UNIX反应器select_reactor
,epoll_reactor
,dev_poll_reactor
,kqueue_reactor
此函数在fork之后对父进程没有任何作用,但是在子进程中它重新创建了反应器陈述并重新注册文件描述符。不过,我不确定它在Windows上的作用。
可以在process_per_connection.cpp中找到其用法示例,您只需复制它即可:
void handle_accept(const boost::system::error_code& ec)
{
if (!ec)
{
// Inform the io_service that we are about to fork. The io_service cleans
// up any internal resources, such as threads, that may interfere with
// forking.
io_service_.notify_fork(boost::asio::io_service::fork_prepare);
if (fork() == 0)
{
// Inform the io_service that the fork is finished and that this is the
// child process. The io_service uses this opportunity to create any
// internal file descriptors that must be private to the new process.
io_service_.notify_fork(boost::asio::io_service::fork_child);
// The child won't be accepting new connections, so we can close the
// acceptor. It remains open in the parent.
acceptor_.close();
// The child process is not interested in processing the SIGCHLD signal.
signal_.cancel();
start_read();
}
else
{
// Inform the io_service that the fork is finished (or failed) and that
// this is the parent process. The io_service uses this opportunity to
// recreate any internal resources that were cleaned up during
// preparation for the fork.
io_service_.notify_fork(boost::asio::io_service::fork_parent);
socket_.close();
start_accept();
}
}
else
{
std::cerr << "Accept error: " << ec.message() << std::endl;
start_accept();
}
}