【问题标题】:Why is Valgrind finding errors in this hash table test case?为什么 Valgrind 在这个哈希表测试用例中发现错误?
【发布时间】:2019-11-29 19:26:36
【问题描述】:

我自己承担了用 C 开发并发通用哈希表的任务。

hash_table.h的相关内容:

typedef struct list_node {
    void * data;
    struct list_node * next;
} list_node_t;


typedef struct hash_table {
    int max_size;
    int count;
    list_node_t * * elements;
    pthread_rwlock_t * locks;
    pthread_rwlock_t global_table_lock;
    hash_table_compare_function compare;
    hash_table_hash_function hash;
} hash_table_t;

hash_table.c的相关内容:

#define LOCK_RD(lock)   pthread_rwlock_rdlock(&lock);
#define LOCK_WR(lock)   pthread_rwlock_wrlock(&lock);
#define UNLOCK(lock)    pthread_rwlock_unlock(&lock);

bool
hash_table_remove(hash_table_t * table, void * element)
{
    int hash_value = table->hash(element);
    list_node_t * node, * prev;

    LOCK_WR(table->locks[hash_value]);

    node = table->elements[hash_value];
    prev = NULL;

    while (node) {
        if (!table->compare(node->data, element)) {
            // value is first item in the list
            if (node == table->elements[hash_value]) {
                table->elements[hash_value] = node->next;
                free(node);
                UNLOCK(table->locks[hash_value]);
                LOCK_WR(table->global_table_lock);
                table->count--;
                UNLOCK(table->global_table_lock);
                return true;
            } else {
                // link previous node with one after current
                prev->next = node->next;
                free(node);
                UNLOCK(table->locks[hash_value]);
                LOCK_WR(table->global_table_lock);
                table->count--;
                UNLOCK(table->global_table_lock);
                return true;
            }
        }
        prev = node;
        node = node->next;
    }

    UNLOCK(table->locks[hash_value]);

    return false;
}

我写了一个使用字符串的测试用例,相关代码如下:

#include "hashtable.h"

#define NUM_THREADS 2
#define NUM_STRINGS 154560
#define NUM_LOOKUPS 10000


void *
do_work(void * data)
{
    int thread_id = *(int*)data;

    // write "threadX.txt" to filename, where X is the given thread id
    char filename[64];
    strcpy(filename, "thread");
    char thread_id_str[4];
    sprintf(thread_id_str, "%d", thread_id);
    strcat(filename, thread_id_str);
    strcat(filename, ".txt");

    FILE * file = fopen(filename, "r");
    char buffer[128];
    int i, num_str_per_thread = NUM_STRINGS / NUM_THREADS;
    char * str_array[num_str_per_thread];

    for (i = 0; i < num_str_per_thread; i++) {
        fgets(buffer, 128, file);

        str_array[i] = calloc((strlen(buffer) + 1), sizeof(char));
        strcpy(str_array[i], buffer);
    }

    fclose(file);

    for (i = 0; i < num_str_per_thread; i++)
        hash_table_insert(table, str_array[i]);

    for (i = 0; i < NUM_LOOKUPS; i++)
        hash_table_contains(table, str_array[rand() % num_str_per_thread]);

    for (i = 0; i < num_str_per_thread / 2; i++)
        hash_table_remove(table, str_array[rand() % num_str_per_thread]);

    //sleep(2); NOTE: no read errors reported if I leave this sleep() here.

    for (i = 0; i < num_str_per_thread; i++)
        if (str_array[i])
            free(str_array[i]);

    return NULL;
}


void
create_workers()
{
    pthread_t threads[NUM_THREADS];
    int ids[NUM_THREADS];
    int i;

    for (i = 0; i < NUM_THREADS; i++)
        ids[i] = i + 1;

    for (i = 0; i < NUM_THREADS; i++)
        pthread_create(&threads[i], NULL, do_work, (void*)&ids[i]);

    for (i = 0; i < NUM_THREADS; i++)
        pthread_join(threads[i], NULL);
}

测试用例应该如下工作:有两个文件thread1.txt和thread2.txt,每个文件都包含我事先生成的unique字符串。我创建了两个线程,每个线程将从一个文件中读取并将每个字符串存储在一个名为str_array 的字符串数组中。然后他们会将所有这些字符串插入哈希表并执行随机搜索(hash_table_contains)和删除(hash_table_remove)。然后,每个人都会释放他们各自的字符串数组。但是,当我运行这个测试用例时,Valgrind 会报告以下内容:

请注意没有内存泄漏。我从这些错误中得到的是,一个线程在调用hash_table_remove 时试图释放已经被free(str_array[i]) 释放的内存。然而,这是没有意义的,因为hash_table_removefree(str_array[i] 之前被调用。我不知道是什么给了我这些无效的读数。

提前谢谢你!

【问题讨论】:

  • 你确定你从文件中读取的数据是 '\0' 终止的吗?这可能会损坏您的字符串缓冲区。
  • strcpy 处理 \0 终止,如果我没记错的话。谢谢你的回复!
  • 如果没有 MCVE (Minimal, Complete, Verifiable Example?)(或 MRE 或 SO 现在使用的任何名称;MCVE 已经使用了五年多,而且不需要更改),这将是地狱般的解决方案或 SSCCE (Short, Self-Contained, Correct Example) 让我们进行实验。非常糟糕,我不会费心去尝试它,直到有一个 minimal 示例(最小)数据可以重现问题。
  • 如果源未终止,@saucygote strcpy 无法插入 \0。它将继续复制,直到到达可能超过字符串缓冲区末尾的 \0。但是你也会遇到 strlen 的同样问题。
  • @jo-art: 如果fgets() 成功,它总是返回一个字符串(这意味着它是空终止的)——源文件不需要有一个。但是,问题中的代码不会检查fgets() 是否成功...

标签: c memory pthreads valgrind


【解决方案1】:

在这里,您的线程最多删除它插入的一半字符串:

for (i = 0; i < num_str_per_thread / 2; i++)
    hash_table_remove(table, str_array[rand() % num_str_per_thread]);

(事实上,它最有可能删除它插入的大约 39% 的字符串)。

然后,它继续释放 所有它插入的字符串:

for (i = 0; i < num_str_per_thread; i++)
    if (str_array[i])
        free(str_array[i]);

但是,这些字符串中至少有一半(很可能是 ~61%)仍在哈希表中,其他线程会在扫描链接的哈希桶条目时尝试比较它们。那是你的 use-after-free 错误。

您可以在删除它们时释放它们,而不是释放所有字符串:

for (i = 0; i < num_str_per_thread / 2; i++)
{
    int str_index = rand() % num_str_per_thread;

    if (str_array[str_index])
    {
        hash_table_remove(table, str_array[str_index]);
        free(str_array[str_index]);
        str_array[str_index] = NULL;
    }
}

此时,str_array[] 中的非 NULL 条目是仍然存在于哈希表中的字符串。在将它们从哈希表中删除(或哈希表不再使用)之前,您无法释放它们。

您的测试用例出错这一事实很好地表明您的界面的人体工程学没有达到应有的水平。您可能应该考虑将插入的字符串的所有权转移到哈希表的设计,以便hash_table_remove() 自己负责释放字符串。

【讨论】:

  • 对延迟回复表示歉意。关于您回答的最后一条建议,我认为让数据结构不处理释放插入其中的对象的内存是一种很好的做法。我猜你边走边学。非常感谢!
  • @SaucyGoat:通常你会将键和对象本身分开,键由数据结构管理,而对象由调用代码管理——只是在你的情况下,你已经将两者结合在一起(并且您的测试代码除了密钥本身之外不存储任何内容)。
猜你喜欢
  • 1970-01-01
  • 2020-04-07
  • 1970-01-01
  • 2015-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-20
  • 1970-01-01
相关资源
最近更新 更多