什么会导致互斥体行为不端?
What could cause a mutex to misbehave?
过去几个月我一直忙于调试一个非常大的专有 C++ 图像处理库中某处导致的罕见崩溃,该库是使用 GCC 4.7.2 为 ARM Cortex-A9 Linux 目标编译的。由于一个常见的症状是 glibc 抱怨堆损坏,第一步是使用堆损坏检查器来捕获 oob 内存写入。我使用 中描述的技术将对 free/malloc 的所有调用转移到我自己的函数,用一些已知数据填充每个分配的内存块以捕获越界写入 - 但没有发现任何东西,即使在每个分配块之前和之后填充多达 1 KB(由于大量使用 STL 容器,有数十万个分配块,所以我不能进一步扩大填充,而且我假设任何写更多超过 1KB 的越界最终会触发段错误)。这个边界检查器在过去发现了其他问题,所以我不怀疑它的功能。
(之前有人说'Valgrind',是的,我也试过了,也没有结果。)
现在,我的内存边界检查器还有一个功能,它在每个分配的块前面加上一个数据结构。这些结构都链接在一个长链表中,以允许我偶尔检查所有分配并测试内存完整性。出于某种原因,即使此列表的所有操作都受互斥锁保护,但该列表已损坏。在调查这个问题时,似乎互斥体本身偶尔无法完成它的工作。这是伪代码:
pthread_mutex_t alloc_mutex;
static bool boolmutex; // set to false during init. volatile has no effect.
void malloc_wrapper() {
// ...
pthread_mutex_lock(&alloc_mutex);
if (boolmutex) {
printf("mutex misbehaving\n");
__THROW_ERROR__; // this happens!
}
boolmutex = true;
// manipulate linked list here
boolmutex = false;
pthread_mutex_unlock(&alloc_mutex);
// ...
}
注释为"this happens!"的代码偶尔会出现,尽管这似乎是不可能的。我的第一个理论是互斥锁数据结构被覆盖了。我将互斥锁放在一个结构中,前后都有大数组,但是当这个问题发生时,数组没有被触及,所以似乎没有任何东西被覆盖。
那么.. 什么样的损坏可能会导致这种情况发生,我将如何找到并修复原因?
还有一些注意事项。测试程序使用3-4个线程进行处理。 运行 更少的线程似乎使损坏不那么常见,但并没有消失。测试每次运行大约 20 秒,并在绝大多数情况下成功完成(我可以让 10 个单元重复测试,第一次失败发生在 5 分钟到几个小时后)。当问题出现时,它在测试中已经很晚了(比如,15 秒),所以这不是一个糟糕的初始化问题。内存边界检查器永远不会捕获实际的越界写入,但 glibc 仍然偶尔会因堆损坏错误而失败(这种错误是否是由 oob 写入以外的其他原因引起的?)。每次故障都会生成一个包含大量跟踪信息的核心转储;在这些转储中我看不到任何模式,也没有比其他部分显示得更多的特定代码部分。这个问题似乎非常特定于特定的算法系列,并且不会发生在其他算法中,所以我很确定这不是零星的硬件或内存错误。我已经做了很多测试来检查 oob 堆访问,我不想列出这些访问以防止此 post 变得更长。
在此先感谢您的帮助!
感谢所有评论者。当我最终决定编写一个简单的内存分配压力测试时,我已经尝试了几乎所有的建议但没有结果 - 一个 运行 每个 CPU 内核上的线程(我的单元是飞思卡尔i.MX6 四核 SoC),每个都以随机顺序高速分配和释放内存。测试在几分钟或最多几小时内因 glibc 内存损坏错误而崩溃。
将内核从 3.0.35 更新到 3.0.101 解决了问题;压力测试和图像处理算法现在 运行 一夜之间没有失败。该问题不会在具有相同内核版本的 Intel 机器上重现,因此该问题通常特定于 ARM,或者可能特定于包含内核 3.0.35 的特定 BSP 版本中包含的某些补丁 Freescale。
好奇者附上压力测试源码。将 NUM_THREADS
设置为 CPU 核心数并构建:
<cross-compiler-prefix>g++ -O3 test_heap.cpp -lpthread -o test_heap
我希望这些信息对某人有所帮助。干杯:)
// Multithreaded heap stress test. By Itay Chamiel 20151012.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <sys/time.h>
#define NUM_THREADS 4 // set to number of CPU cores
#define ALIVE_INDICATOR NUM_THREADS
// Each thread constantly allocates and frees memory. In each iteration of the infinite loop, decide at random whether to
// allocate or free a block of memory. A list of 500-1000 allocated blocks is maintained by each thread. When memory is allocated
// it is added to this list; when freeing, a random block is selected from this list, freed and removed from the list.
void* thr(void* arg) {
int* alive_flag = (int*)arg;
int thread_id = *alive_flag; // this is a number between 0 and (NUM_THREADS-1) given by main()
int cnt = 0;
timeval t_pre, t_post;
gettimeofday(&t_pre, NULL);
const int ALLOCATE=1, FREE=0;
const unsigned int MINSIZE=500, MAXSIZE=1000;
const int MAX_ALLOC=10000;
char* membufs[MAXSIZE];
unsigned int membufs_size = 0;
int num_allocs = 0, num_frees = 0;
while(1)
{
int action;
// Decide whether to allocate or free a memory block.
// if we have less than MINSIZE buffers, allocate.
if (membufs_size < MINSIZE) action = ALLOCATE;
// if we have MAXSIZE, free.
else if (membufs_size >= MAXSIZE) action = FREE;
// else, decide randomly.
else {
action = ((rand() & 0x1)? ALLOCATE : FREE);
}
if (action == ALLOCATE) {
// choose size to allocate, from 1 to MAX_ALLOC bytes
size_t size = (rand() % MAX_ALLOC) + 1;
// allocate and fill memory
char* buf = (char*)malloc(size);
memset(buf, 0x77, size);
// add buffer to list
membufs[membufs_size] = buf;
membufs_size++;
assert(membufs_size <= MAXSIZE);
num_allocs++;
}
else { // action == FREE
// choose a random buffer to free
size_t pos = rand() % membufs_size;
assert (pos < membufs_size);
// free and remove from list by replacing entry with last member
free(membufs[pos]);
membufs[pos] = membufs[membufs_size-1];
membufs_size--;
assert(membufs_size >= 0);
num_frees++;
}
// once in 10 seconds print a status update
gettimeofday(&t_post, NULL);
if (t_post.tv_sec - t_pre.tv_sec >= 10) {
printf("Thread %d [%d] - %d allocs %d frees. Alloced blocks %u.\n", thread_id, cnt++, num_allocs, num_frees, membufs_size);
gettimeofday(&t_pre, NULL);
}
// indicate alive to main thread
*alive_flag = ALIVE_INDICATOR;
}
return NULL;
}
int main()
{
int alive_flag[NUM_THREADS];
printf("Memory allocation stress test running on %d threads.\n", NUM_THREADS);
// start a thread for each core
for (int i=0; i<NUM_THREADS; i++) {
alive_flag[i] = i; // tell each thread its ID.
pthread_t th;
int ret = pthread_create(&th, NULL, thr, &alive_flag[i]);
assert(ret == 0);
}
while(1) {
sleep(10);
// check that all threads are alive
bool ok = true;
for (int i=0; i<NUM_THREADS; i++) {
if (alive_flag[i] != ALIVE_INDICATOR)
{
printf("Thread %d is not responding\n", i);
ok = false;
}
}
assert(ok);
for (int i=0; i<NUM_THREADS; i++)
alive_flag[i] = 0;
}
return 0;
}
过去几个月我一直忙于调试一个非常大的专有 C++ 图像处理库中某处导致的罕见崩溃,该库是使用 GCC 4.7.2 为 ARM Cortex-A9 Linux 目标编译的。由于一个常见的症状是 glibc 抱怨堆损坏,第一步是使用堆损坏检查器来捕获 oob 内存写入。我使用 中描述的技术将对 free/malloc 的所有调用转移到我自己的函数,用一些已知数据填充每个分配的内存块以捕获越界写入 - 但没有发现任何东西,即使在每个分配块之前和之后填充多达 1 KB(由于大量使用 STL 容器,有数十万个分配块,所以我不能进一步扩大填充,而且我假设任何写更多超过 1KB 的越界最终会触发段错误)。这个边界检查器在过去发现了其他问题,所以我不怀疑它的功能。
(之前有人说'Valgrind',是的,我也试过了,也没有结果。)
现在,我的内存边界检查器还有一个功能,它在每个分配的块前面加上一个数据结构。这些结构都链接在一个长链表中,以允许我偶尔检查所有分配并测试内存完整性。出于某种原因,即使此列表的所有操作都受互斥锁保护,但该列表已损坏。在调查这个问题时,似乎互斥体本身偶尔无法完成它的工作。这是伪代码:
pthread_mutex_t alloc_mutex;
static bool boolmutex; // set to false during init. volatile has no effect.
void malloc_wrapper() {
// ...
pthread_mutex_lock(&alloc_mutex);
if (boolmutex) {
printf("mutex misbehaving\n");
__THROW_ERROR__; // this happens!
}
boolmutex = true;
// manipulate linked list here
boolmutex = false;
pthread_mutex_unlock(&alloc_mutex);
// ...
}
注释为"this happens!"的代码偶尔会出现,尽管这似乎是不可能的。我的第一个理论是互斥锁数据结构被覆盖了。我将互斥锁放在一个结构中,前后都有大数组,但是当这个问题发生时,数组没有被触及,所以似乎没有任何东西被覆盖。
那么.. 什么样的损坏可能会导致这种情况发生,我将如何找到并修复原因?
还有一些注意事项。测试程序使用3-4个线程进行处理。 运行 更少的线程似乎使损坏不那么常见,但并没有消失。测试每次运行大约 20 秒,并在绝大多数情况下成功完成(我可以让 10 个单元重复测试,第一次失败发生在 5 分钟到几个小时后)。当问题出现时,它在测试中已经很晚了(比如,15 秒),所以这不是一个糟糕的初始化问题。内存边界检查器永远不会捕获实际的越界写入,但 glibc 仍然偶尔会因堆损坏错误而失败(这种错误是否是由 oob 写入以外的其他原因引起的?)。每次故障都会生成一个包含大量跟踪信息的核心转储;在这些转储中我看不到任何模式,也没有比其他部分显示得更多的特定代码部分。这个问题似乎非常特定于特定的算法系列,并且不会发生在其他算法中,所以我很确定这不是零星的硬件或内存错误。我已经做了很多测试来检查 oob 堆访问,我不想列出这些访问以防止此 post 变得更长。
在此先感谢您的帮助!
感谢所有评论者。当我最终决定编写一个简单的内存分配压力测试时,我已经尝试了几乎所有的建议但没有结果 - 一个 运行 每个 CPU 内核上的线程(我的单元是飞思卡尔i.MX6 四核 SoC),每个都以随机顺序高速分配和释放内存。测试在几分钟或最多几小时内因 glibc 内存损坏错误而崩溃。
将内核从 3.0.35 更新到 3.0.101 解决了问题;压力测试和图像处理算法现在 运行 一夜之间没有失败。该问题不会在具有相同内核版本的 Intel 机器上重现,因此该问题通常特定于 ARM,或者可能特定于包含内核 3.0.35 的特定 BSP 版本中包含的某些补丁 Freescale。
好奇者附上压力测试源码。将 NUM_THREADS
设置为 CPU 核心数并构建:
<cross-compiler-prefix>g++ -O3 test_heap.cpp -lpthread -o test_heap
我希望这些信息对某人有所帮助。干杯:)
// Multithreaded heap stress test. By Itay Chamiel 20151012.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <sys/time.h>
#define NUM_THREADS 4 // set to number of CPU cores
#define ALIVE_INDICATOR NUM_THREADS
// Each thread constantly allocates and frees memory. In each iteration of the infinite loop, decide at random whether to
// allocate or free a block of memory. A list of 500-1000 allocated blocks is maintained by each thread. When memory is allocated
// it is added to this list; when freeing, a random block is selected from this list, freed and removed from the list.
void* thr(void* arg) {
int* alive_flag = (int*)arg;
int thread_id = *alive_flag; // this is a number between 0 and (NUM_THREADS-1) given by main()
int cnt = 0;
timeval t_pre, t_post;
gettimeofday(&t_pre, NULL);
const int ALLOCATE=1, FREE=0;
const unsigned int MINSIZE=500, MAXSIZE=1000;
const int MAX_ALLOC=10000;
char* membufs[MAXSIZE];
unsigned int membufs_size = 0;
int num_allocs = 0, num_frees = 0;
while(1)
{
int action;
// Decide whether to allocate or free a memory block.
// if we have less than MINSIZE buffers, allocate.
if (membufs_size < MINSIZE) action = ALLOCATE;
// if we have MAXSIZE, free.
else if (membufs_size >= MAXSIZE) action = FREE;
// else, decide randomly.
else {
action = ((rand() & 0x1)? ALLOCATE : FREE);
}
if (action == ALLOCATE) {
// choose size to allocate, from 1 to MAX_ALLOC bytes
size_t size = (rand() % MAX_ALLOC) + 1;
// allocate and fill memory
char* buf = (char*)malloc(size);
memset(buf, 0x77, size);
// add buffer to list
membufs[membufs_size] = buf;
membufs_size++;
assert(membufs_size <= MAXSIZE);
num_allocs++;
}
else { // action == FREE
// choose a random buffer to free
size_t pos = rand() % membufs_size;
assert (pos < membufs_size);
// free and remove from list by replacing entry with last member
free(membufs[pos]);
membufs[pos] = membufs[membufs_size-1];
membufs_size--;
assert(membufs_size >= 0);
num_frees++;
}
// once in 10 seconds print a status update
gettimeofday(&t_post, NULL);
if (t_post.tv_sec - t_pre.tv_sec >= 10) {
printf("Thread %d [%d] - %d allocs %d frees. Alloced blocks %u.\n", thread_id, cnt++, num_allocs, num_frees, membufs_size);
gettimeofday(&t_pre, NULL);
}
// indicate alive to main thread
*alive_flag = ALIVE_INDICATOR;
}
return NULL;
}
int main()
{
int alive_flag[NUM_THREADS];
printf("Memory allocation stress test running on %d threads.\n", NUM_THREADS);
// start a thread for each core
for (int i=0; i<NUM_THREADS; i++) {
alive_flag[i] = i; // tell each thread its ID.
pthread_t th;
int ret = pthread_create(&th, NULL, thr, &alive_flag[i]);
assert(ret == 0);
}
while(1) {
sleep(10);
// check that all threads are alive
bool ok = true;
for (int i=0; i<NUM_THREADS; i++) {
if (alive_flag[i] != ALIVE_INDICATOR)
{
printf("Thread %d is not responding\n", i);
ok = false;
}
}
assert(ok);
for (int i=0; i<NUM_THREADS; i++)
alive_flag[i] = 0;
}
return 0;
}