最近在做服务器的稳定性的相关测试,服务器的网络底层使用的是boost asio,然后自己做的二次封装以更好的满足需求。
服务器昨天晚上发现crash了一次,之前测试了将近半个多月,有一次是莫名的退出了,不过由于是新的测试服,忘记将ulimit -c进行修改了,所以没有coredump,这次又发生了。
coredump如下:
#0 0x0000000000000091 in ?? () #1 0x0000000000459729 in ClientHandler::HandleConnect(cpnet::IConnection*) () #2 0x00000000004a0bbc in boost::asio::detail::completion_handler<boost::_bi::bind_t<void, boost::_mfi::mf1<void, cpnet::IMsgHandler, cpnet::IConnection*>, boost::_bi::list2<boost::_bi::value<cpnet::IMsgHandler*>, boost::_bi::value<cpnet::Connection*> > > >::do_complete(boost::asio::detail::task_io_service*, boost::asio::detail::task_io_service_operation*, boost::system::error_code const&, unsigned long) () #3 0x0000000000492e25 in boost::asio::detail::strand_service::do_complete(boost::asio::detail::task_io_service*, boost::asio::detail::task_io_service_operation*, boost::system::error_code const&, unsigned long) () #4 0x0000000000493f20 in boost::asio::detail::task_io_service::run(boost::system::error_code&) () #5 0x0000000000495bb5 in boost::asio::io_service::run() () #6 0x00007ff3798153cf in thread_proxy () from /home/slither/slither/depends/libboost_thread.so.1.58.0 #7 0x00007ff3788d7df5 in start_thread () from /lib64/libpthread.so.0 #8 0x00007ff3786051ad in clone () from /lib64/libc.so.6
整个网络库是针对boost asio做的二次封装,程序由于是release版本的,之前也没有特别生成这个版本对应的symbols,我只能看出异常时候的堆栈信息和线程信息,其他的东西我也没有什么好的办法去查看。
从堆栈异常中可以看出来,最后出错的地方是非法的地址方法,不过调用它的frame 1是我们自己的函数,一个当有连接成功时候的回调函数。不过一开始看这个回调函数的堆栈感觉很奇怪,因为是我们自己内部通过strand dispatch的,按理说调用这个函数的上层函数也应该是我们自己写的代码,然而情况不是这样,那么不可能是堆栈显示错了,而应该是自己之前对于dispatch的立即错了。
这个HandleConnect我是使用boost strand做dispatch分发的,这里就涉及到我之前对strand dispatch接口的一个误读了,之前看文档不够细致,以为strand dispath是立即执行的,其实这是不对的,dispath的接口说明文档是这样的:
/** * This function is used to ask the strand to execute the given handler. * * The strand object guarantees that handlers posted or dispatched through * the strand will not be executed concurrently. The handler may be executed * inside this function if the guarantee can be met. If this function is * called from within a handler that was posted or dispatched through the same * strand, then the new handler will be executed immediately. * * The strand's guarantee is in addition to the guarantee provided by the * underlying io_service. The io_service guarantees that the handler will only * be called in a thread in which the io_service's run member function is * currently being invoked. * * @param handler The handler to be called. The strand will make a copy of the * handler object as required. The function signature of the handler must be: * @code void handler(); @endcode */
注意红色标记的那段,如果调用strand dispatch的时候,是持有相同strand调用的,那么当前dispatch的handler会立即执行。也就是说在多线程的时候,如果我们的线程调用strand dispatch的时候,其他线程已经在调用了,那么其实它是不会立即执行的,会放到等待队列里面去的。
asio中的dispatch代码是这样的:
template <typename Handler> void strand_service::dispatch(strand_service::implementation_type& impl, Handler& handler) { // If we are already in the strand then the handler can run immediately.
// 如果我们已经在这个strand中了,那么这个handler立即执行
if (call_stack<strand_impl>::contains(impl)) { fenced_block b(fenced_block::full); boost_asio_handler_invoke_helpers::invoke(handler, handler); return; } // Allocate and construct an operation to wrap the handler. typedef completion_handler<Handler> op; typename op::ptr p = { boost::asio::detail::addressof(handler), boost_asio_handler_alloc_helpers::allocate( sizeof(op), handler), 0 }; p.p = new (p.v) op(handler); BOOST_ASIO_HANDLER_CREATION((p.p, "strand", impl, "dispatch"));
// do_dispatch判断是否能够立即执行 bool dispatch_immediately = do_dispatch(impl, p.p); operation* o = p.p; p.v = p.p = 0; if (dispatch_immediately) { // Indicate that this strand is executing on the current thread. call_stack<strand_impl>::context ctx(impl); // Ensure the next handler, if any, is scheduled on block exit. on_dispatch_exit on_exit = { &io_service_, impl }; (void)on_exit; completion_handler<Handler>::do_complete( &io_service_, o, boost::system::error_code(), 0); } }
再来看下do_dispatch的代码:
bool strand_service::do_dispatch(implementation_type& impl, operation* op) { // If we are running inside the io_service, and no other handler already // holds the strand lock, then the handler can run immediately.
// 如果没有其他handler已经持有strand lock锁,那么这个handler就可以立即执行
bool can_dispatch = io_service_.can_dispatch(); impl->mutex_.lock(); if (can_dispatch && !impl->locked_) { // Immediate invocation is allowed. impl->locked_ = true; impl->mutex_.unlock(); return true; } if (impl->locked_) { // Some other handler already holds the strand lock. Enqueue for later.
// 如果其他handler已经持有strand锁了,那么放到队列中
impl->waiting_queue_.push(op); impl->mutex_.unlock(); } else { // The handler is acquiring the strand lock and so is responsible for // scheduling the strand. impl->locked_ = true; impl->mutex_.unlock(); impl->ready_queue_.push(op); io_service_.post_immediate_completion(impl, false); } return false; }
通过asio strand的dispatch源代码,我们可以看出来,我们dispatch的handler是有可能不会被立即执行的。由于我们自己之前对于dispatch逻辑的认知错误,在dispatch handler之前,我们就开始准备读网络数据,在比较特殊的情况下,也就是客户端刚连上,立即端口,那么我们读网络数据的函数就立即返回错误,由于我自己封装的Connection是使用shared_ptr做的封装,如果没有任何引用,就会析构掉,那么等我们之前dispatch的handler从队列中被执行的时候,之前传递的Connection指针已经是野指针了,就导致程序crash掉了。
这种偶现的bug,是比较难被测试出来的,通常只有我们自己进行多样的压力测试的时候,才比较容易发现。同时也是告诫自己在使用其他第三方库的时候,还是要更加仔细的弄懂api。