1、问题
套按字绑定失败导致的loop退出异常
2、环境
libuv版本:1.42.0及以下
OS版本:win10 x64
3、现象
uv_async_send的回调方法中,使用uv_udp_bind(或者uv_tcp_bind,后文以udp举例),将一个本机不存在的IP,绑定到uv_udp_t(或者uv_tcp_t)代表的handle(new创建),绑定返回失败后,对handle进行释放(delete);在退出loop的过程中,应用发生异常。
备注:由于主机IP的人为修改等业务性操作流程的存在,意外导致应用配置文件未同步变化,此类问题在实际环境中确实存在。
4、分析
1、在绑定本机上的已有IP时,退出过程中,并未发生异常; 2、退出loop前,使用uv_walk迭代关闭libuv内部维护的handle,发现无论是正常还是异常情况的绑定,均存在一个handle(尽管可能还有别的handle,比如复现demo中的uv_async_t,但我们可以聚焦于问题相关的那个),怀疑此handle是对应的udp类型的handle。在绑定失败的情况下,进入uv_walk后,handle的type是个随机的野值; 3、查看libuv源码中的src/win/udp.c文件,在uv_udp_maybe_bind方法中,当bind失败后,直接返回了WSAGetLastError()方法返回的错误值; 4、第3步导致的问题是,在bind操作前,已经使用了uv_udp_init将handle与loop进行了关联,而bind失败后,并没有关闭(closesocket)创建的handle->socket,也没有撤销handle与loop的关联(QUEUE_REMOVE),导致loop中已经存在了一个udp handle,而bind失败后,又接着对handle占用的内存进行了释放; 5、退出loop时,uv_walk依然发现queue里有个handle,然而,我们在上一步中,已经释放了对应地址的handle(甚至此地址已经又包含着另外的对象),从而导致了连锁的异常;
5、复现
我们的复现demo,其主要流程是:应用进程的主线程内,首先创建一个工程线程,在后者内,初始化、运行libuv的loop(即uv_run位于工作线程中);然后,主线程内,通过uv_async_send的异步方式,传递一个请求,使用libuv的loop创建并绑定udp handle;最后,主线程等待用户输入字符q,以退出整个应用。
在demo中,当绑定使用的是ipNormal的值(即本机存在的IP时),无异常;当绑定使用的是ipAbnormal的值(即非本机存在的IP)时,退出发生异常;
6、解决
可能的解决方案有以下: 方案一 1、没有libuv源代码(无法完成源码修改)的情况下,我们只能从应用的实现、释放流程上,避免发生对bind失败的handle,立刻进行内存释放的操作,而应该在loop彻底退出后,再释放handle占用的内存(尽管这样,让人感觉些许的难受,此外,loop全程也维护了一个无法行使职能的handle);
方案二 1、将libuv源代码src/uv-common.c中的第341行:
return uv__udp_bind(handle, addr, addrlen, flags);
修改为:
int ret = uv__udp_bind(handle, addr, addrlen, flags);
if (ret != 0) {
closesocket(handle->socket); ? ?// need?
QUEUE_REMOVE(&handle->handle_queue);
}
return ret;
2、修改完后,重新编译生成libuv库;
7、附代码
工程结构
?
?
CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
project(UvTests)
set(CMAKE_CXX_STANDARD 11)
include_directories("${PROJECT_SOURCE_DIR}/lib/libuv1.42.0/include")
add_executable(${PROJECT_NAME}
main.cpp
test/DMainTests.cpp
test/DMainTests.h)
link_directories(${PROJECT_SOURCE_DIR}/lib/libuv1.42.0/win/Debug)
target_link_libraries(${PROJECT_NAME} ws2_32)
target_link_libraries(${PROJECT_NAME} "${PROJECT_SOURCE_DIR}/lib/libuv1.42.0/win/Debug/libuv.dll")
test/DMainTests.h
#ifndef UVTESTS_DMAINTESTS_H
#define UVTESTS_DMAINTESTS_H
#include "uv.h"
#include <atomic>
class DMainTests {
protected:
uv_loop_t* objLoop;
uv_async_t *uvAsyncHandle;
uv_udp_t* uvUdpHandle;
volatile std::atomic<bool> flagOfThreadExit; // uv loop thread exit flag
volatile std::atomic<bool> flagOfLoopOn; // uv loop is online?
volatile std::atomic<bool> flagOfExit; // app exit flag
public:
DMainTests();
virtual ~DMainTests();
public:
virtual void init();
protected:
virtual void initBiz();
virtual void closeBiz();
static void threadPrc(void* pVoid);
virtual void threadProcObj();
static void cbAsync(uv_async_t *handle);
virtual void cbAsyncObj();
static void cbWalkClose(uv_handle_t* handle, void *arg);
};
#endif //UVTESTS_DMAINTESTS_H
test/DMainTests.cpp
#include "DMainTests.h"
#include <iostream>
#include <string>
#include <thread>
DMainTests::DMainTests() {
}
DMainTests::~DMainTests() {
}
void DMainTests::init() {
initBiz();
}
void DMainTests::initBiz() {
flagOfLoopOn = false;
flagOfExit = false;
std::thread obj(DMainTests::threadPrc, this);
obj.detach();
while (!flagOfLoopOn) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
::uv_async_send(uvAsyncHandle);
// wait or exit when input 'q'
std::string strInput;
std::getline(std::cin, strInput);
if (strInput.compare("q") == 0) {
closeBiz();
}
}
void DMainTests::closeBiz() {
std::cout << "DMainWindowTests::closeBiz enter" << std::endl;
flagOfExit = true;
::uv_async_send(uvAsyncHandle);
while (!flagOfThreadExit) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::this_thread::yield();
}
std::cout << "DMainWindowTests::closeBiz exit" << std::endl;
}
void DMainTests::threadPrc(void *pVoid) {
DMainTests* obj =(DMainTests*)pVoid;
obj->threadProcObj();
}
void DMainTests::threadProcObj() {
flagOfThreadExit = false;
objLoop = new uv_loop_t;
uv_loop_init(objLoop);
uvUdpHandle = new uv_udp_t;
uvAsyncHandle = new uv_async_t;
uvAsyncHandle->data = this;
::uv_async_init(objLoop, uvAsyncHandle, &DMainTests::cbAsync);
flagOfLoopOn = true;
::uv_run(objLoop, UV_RUN_DEFAULT);
delete uvAsyncHandle;
uvAsyncHandle = nullptr;
if (uvUdpHandle) {
delete uvUdpHandle;
uvUdpHandle = nullptr;
}
delete objLoop;
objLoop = nullptr;
flagOfThreadExit = true;
}
void DMainTests::cbAsync(uv_async_t *handle) {
DMainTests* obj = (DMainTests*)handle->data;
obj->cbAsyncObj();
}
void DMainTests::cbAsyncObj() {
std::cout << "DMainWindowTests::cbAsyncObj enter" << std::endl;
if (flagOfExit) {
::uv_walk(objLoop, &DMainTests::cbWalkClose, nullptr);
std::cout << "DMainWindowTests::cbAsyncObj exit[app exit]" << std::endl;
return;
}
const char* ipNormal = "192.168.159.1";
const char* ipAbnormal = "123.6.2.18";
sockaddr_in addr;
int iRet = -1;
do {
iRet = ::uv_ip4_addr(ipAbnormal, 36218, &addr);
iRet = ::uv_udp_init_ex(objLoop, uvUdpHandle, 2);
if (iRet != 0) {
break;
}
iRet = ::uv_udp_bind(uvUdpHandle, (const sockaddr*)&addr, UV_UDP_REUSEADDR);
if (iRet != 0) {
break;
}
} while(false);
if (iRet != 0) {
if (uvUdpHandle) {
delete uvUdpHandle;
uvUdpHandle = nullptr;
}
}
std::cout << "DMainWindowTests::cbAsyncObj exit[udp bind]" << std::endl;
}
void DMainTests::cbWalkClose(uv_handle_t *handle, void *arg) {
if (handle && uv_is_closing(handle) == 0 && handle->type != 0) {
std::cout << "DMainWindowTests::cbWalkClose run, type : "
<< handle->type
<< std::endl;
uv_close(handle, nullptr);
}
}
main.cpp
#include "test/DMainTests.h"
int main(int argc, char *argv[])
{
DMainTests windowTests;
windowTests.init();
return 0;
}
|