libcurl多线程环境下执行curl_seay_perform后发生coredump的原因及解决

本文探讨了在使用libcurl进行多线程操作时出现coredump的问题,详细分析了问题产生的原因在于sigalarm+sigsetjmp/siglongjmp的线程不安全性,并给出了具体的解决方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

libcurl多线程环境下执行curl_seay_perform后发生coredump的原因及解决

问题背景

  • 设备上的某模块已上线稳定运行了两年了,最近突然发现了前方问题.初步定位后发现用户在配置集控服务器地址时,未切换大小写,导致地址配置成了x.x.x.x:8080,此处":"为中文符号.
  • 按常理推断,此时正常现象应为连接服务器失败,一分钟后尝试重连.
  • 但此时现场的现象为设备上集控进程crash并产生了coredump文件.
  • 若正常配置服务器地址,无任何问题.

问题排查

  • 使用gdb查看core文件打印函数调用栈发现已被踩坏,无法获取到有用信息.只能粗略的看出为段错误.
    在这里插入图片描述

  • 在连接服务器的线程中打调试发现,curl句柄初始化无误,且curl_easy_perform返回值为CURLE_COULDNT_RESOLVE_HOST(6),系统日志发送无误,线程正常退出.无法具体确定产生crash的代码

  • 尝试将主要流程注掉发现,当注掉curl_easy_perform并直接返回失败时,进程不再挂了.

  • 查阅libcurl相关资料后发现此问题为已知问题:
    产生此问题的函数调用栈大致如下:
    curl_easy_perform->easy_perform->easy_transfer->curl_multi_perform->multi_runsingle->Curl_connect->create_conn->resolve_server->Curl_resolv_timeout.
    在curl_resolve_timeout中实现了对服务器域名解析和超时后的错误处理,而在linux系统中,其调用了linux的本地域名解析流程,具体使用了sigalarm+sigsetjmp/siglongjmp 来实现逻辑.

  • 问题就出在此处,sigalarm+sigsetjmp/siglongjmp 并不是线程安全的,当我在A线程中域名解析失败,产生了sigalarm信号,可能被其他线程捕获到,它是使用alarm + siglongjmp 实现的。用sigalarm在多线程下做超时,本身就几乎不可能。如果只是使用sigalarm,并不会导致程序崩溃,但是,再加上siglongjmp,就要命了 (程序崩溃的很可怕,core中几乎看不出有用信息),因为其需要一个sigjmp_buf型的全局变量,多线程修改它。(通常情况下,可以每个线程一个 sigjmp_buf 型的变量,这种情况下,多线程中使用 siglongjmp 是没有问题的,但是libcurl只有一个全局变量,所有的线程都会用)。
    具体是类似 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L) 的超时设置,导致alarm的使用(估计发生在域名解析阶段),如前所述,这在多线程中是不行的。解决方式是禁用掉alarm这种超时, curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L)。

解决方案

设置curl_easy_setopt(curl, CURLOPT_NOSIGNAL,1L);屏蔽当前线程产生的所有信号和处理信号的所有函数.带来的副作用即为此线程域名解析的超时设置失效,若无其他超时设置,将会阻塞在域名解析中.
官方说明中有提到可以用libcurl with the c-ares 中提供的新的异步的域名解析方案来处理此问题.
详情参加:
[link][https://curl.se/libcurl/c/threadsafe.html]
[link][https://curl.se/libcurl/c/CURLOPT_NOSIGNAL.html]

<think>我们正在分析libcurl库在`curl_strnequal`函数处挂起的问题。根据提供的调用栈,虽然没有直接显示`curl_strnequal`,但我们可以基于常见原因和引用内容进行推理。 ### 可能原因分析 #### 1. **多线程安全问题** [^2] - **问题描述**:如果多个线程同时调用libcurl函数,且未正确初始化OpenSSL的线程安全机制,可能导致死锁或挂起。 - **解决方案**: ```c #include <openssl/crypto.h> static pthread_mutex_t *mutex_buf; static void locking_function(int mode, int n, const char* file, int line) { if (mode & CRYPTO_LOCK) pthread_mutex_lock(&mutex_buf[n]); else pthread_mutex_unlock(&mutex_buf[n]); } void init_openssl_locks() { mutex_buf = malloc(CRYPTO_num_locks() * sizeof(pthread_mutex_t)); for (int i=0; i<CRYPTO_num_locks(); i++) pthread_mutex_init(&mutex_buf[i], NULL); CRYPTO_set_locking_callback(locking_function); } ``` 在程序启动时调用`init_openssl_locks()`,并确保`curl_global_init()`只调用一次[^2]。 #### 2. **字符串比较时访问非法内存** - **问题描述**:`curl_strnequal`用于比较两个字符串(不区分大小写),若传入的指针无效(如NULL或已释放内存)会导致挂起。 - **调试方法**: ```bash (gdb) frame 0 # 切换到崩溃帧 (gdb) info args # 查看函数参数 ``` 检查参数`str1`, `str2`是否有效: ```bash (gdb) x/s <str1地址> (gdb) x/s <str2地址> ``` #### 3. **全局初始化冲突** - **引用[2]指出**:`curl_global_init()`和`curl_global_cleanup()`的线程不安全调用可能导致异常。 - **正确做法**: ```c // 主线程初始化一次 curl_global_init(CURL_GLOBAL_ALL); // 每个线程创建独立CURL句柄 CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // 避免信号中断 // 程序退出前清理 curl_global_cleanup(); ``` #### 4. **头文件处理异常** - `curl_strnequal`常用于解析HTTP头,若自定义头格式错误(如未以冒号分隔)可能引发死循环。 - **验证**:检查通过`CURLOPT_HTTPHEADER`设置的头列表: ```c struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "Key: Value"); // 正确格式 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); ``` ### 诊断步骤 1. **复现问题**:在调试环境下运行程序,捕获挂起时的调用栈。 2. **检查参数**:在`curl_strnequal`入口设置断点,观察传入的字符串指针和长度。 3. **启用libcurl调试**: ```c curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); // 输出调试信息 curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback); // 自定义回调 ``` ### 示例:调试回调函数 ```c static int debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr) { if (type == CURLINFO_HEADER_IN || type == CURLINFO_HEADER_OUT) { fwrite(data, size, 1, stderr); // 打印HTTP头 } return 0; } ``` ### 总结 挂起原因优先级排序: 1. 多线程竞争(尤其涉及HTTPS时) 2. 无效字符串指针传入`curl_strnequal` 3. 全局初始化重复或缺失 §§相关问题§§ 1. 如何设置OpenSSL的线程安全回调以兼容libcurl多线程? 2. 使用libcurl时如何避免自定义HTTP头导致的解析错误? 3. 如何通过GDB调试复现libcurl函数挂起问题?</think>### 分析libcurl在`curl_strnequal`函数挂起的可能原因 `curl_strnequal`是libcurl内部用于字符串比较的函数(不区分大小写)。当在此函数处挂起时,可能的原因包括: #### 1. **无效内存访问** [^1][^2] - **空指针解引用**:传入的字符串指针为NULL ```c // 错误示例:可能传入NULL指针 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL); ``` - **野指针/已释放内存**:头字符串已被释放但仍被使用 - **缓存区溢出**:传入的长度参数超出实际字符串长度 #### 2. **多线程竞争** [^2] - **线程不安全操作**:多个线程同时操作同一个CURL句柄 ```c // 错误:多线程共享同一个CURL句柄 void* thread_func(void* arg) { curl_easy_perform(shared_curl_handle); // 危险! } ``` - **OpenSSL线程安全问题**:未初始化OpenSSL线程安全回调 ```c // 必须设置的OpenSSL线程回调 CRYPTO_set_locking_callback(openssl_lock_cb); ``` #### 3. **头处理异常** - **自定义头格式错误**:不符合`Header: Value`格式 ```c // 错误示例:缺少冒号分隔符 struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "InvalidHeaderFormat"); ``` - **特殊字符处理**:包含未转义的回车/换行符`\r\n` #### 4. **内存损坏** - **堆溢出**:其他操作导致内存结构损坏 - **版本不匹配**:debug/release库混用导致内存布局不一致[^2] #### 5. **信号处理问题** - **未禁用信号**:未设置`CURLOPT_NOSIGNAL` ```c // 必须设置(尤其多线程环境curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); ``` ### 诊断步骤 1. **检查参数有效性**(GDB调试) ```bash (gdb) frame 0 (gdb) info args # 检查str1/str2指针和len值 (gdb) x/s <str1地址> # 查看字符串内容 (gdb) x/s <str2地址> ``` 2. **验证线程安全性** - 确保每个线程使用独立CURL句柄 - 检查OpenSSL线程回调初始化 3. **启用libcurl调试** ```c curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_cb); ``` 4. **内存检测工具** ```bash valgrind --tool=memcheck ./your_program ``` ### 解决方案 ```c // 正确初始化示例 curl_global_init(CURL_GLOBAL_ALL); // 设置OpenSSL线程安全(多线程HTTPS必需) init_openssl_threading(); CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // 关键! // 添加头需确保格式正确 struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "User-Agent: MyApp/1.0"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值