【Valgrind入门指南】从内存问题到线程安全

1,712 阅读4分钟

【Valgrind入门指南】从内存问题到线程安全

Valgrind是一个非常强大的程序动态分析工具,可以帮助我们探测程序中的各种问题,包括内存管理问题、线程不安全问题等等。本文将介绍Valgrind的基本用法,让你可以快速上手使用。

内存检查

Valgrind有很多功能,其中最著名的就是memcheck,它可以用来探测常见的内存问题,例如:

  1. 越界访问
  2. 使用未初始化的变量
  3. 不正确释放内存,例如double-freeing
  4. 内存泄漏

下面,我们来看一个例子:

#include <stdlib.h>

void f(void) {
   int* x = malloc(10 * sizeof(int));
   x[10] = 0;        
}

int main(void) {
   f();
   return 0;
}

我们可以用Valgrind来分析这段代码:

  1. gcc -g membase.c -o membase,使用-g选项来携带debug信息
  2. valgrind --leak-check=full ./membase

执行后会得到以下输出:

==7069== Memcheck, a memory error detector
==7069== ... // 省略部分输出
==7069== Invalid write of size 4
==7069==    at 0x401144: f (membase.c:5)
==7069==    by 0x401155: main (membase.c:9)
==7069==  Address 0x4a47068 is 0 bytes after a block of size 40 alloc'd
==7069==    at 0x484386F: malloc (vg_replace_malloc.c:393)
==7069==    by 0x401137: f (membase.c:4)
==7069==    by 0x401155: main (membase.c:9)
==7069== ... // 省略部分输出
==7069== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==7069==    at 0x484386F: malloc (vg_replace_malloc.c:393)
==7069==    by 0x401137: f (membase.c:4)
==7069==    by 0x401155: main (membase.c:9)
==7069==    by 0x401155: main (membase.c:9)
==7069== ... // 省略部分输出

我们可以看到,Valgrind给出了一些警告信息,提示我们代码中的问题。其中:

==7069== Invalid write of size 4
==7069==    at 0x401144: f (membase.c:5)
==7069==    by 0x401155: main (membase.c:9)

这部分告诉我们,在不允许的内存区域写入了数据。

==7069== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1

这部分告诉我们,动态分配的空间没有被释放,导致内存泄漏。

下面,我们再看一个例子:

#include <stdlib.h>

int main() {

        int p, t;

        if (p == 5)
                t = p + 1;
        return 0;
}

我们可以用Valgrind来分析这段代码:

bashCopy code
valgrind --leak-check=full ./memuninitvar

执行后会得到以下输出:

......
==7274== Conditional jump or move depends on uninitialised value(s)
==7274== at 0x40110E: main (memuninitvar.c:7)
==7274==
......

这部分告诉我们,使用了未初始化的变量。

竞争条件检查

除了内存问题,Valgrind还可以帮助我们检测线程安全问题。其中,最常用的工具是Helgrind,它可以用来检测线程竞争,例如:

  1. 错误使用POSIX pthreads API
  2. 潜在的死锁问题
  3. 数据竞争:在没有获得锁的情况下访问数据

下面,我们来看一个例子:

#include <pthread.h>

int var = 0;

void* child_fn ( void* arg ) {
   var++;
   return NULL;
}

int main ( void ) {
   pthread_t child;
   pthread_create(&child, NULL, child_fn, NULL);
   var++;
   pthread_join(child, NULL);
   return 0;
}

我们可以用Valgrind来分析这段代码:

  1. gcc -pthread -g helbase.c -o helbase,使用-pthread选项来编译多线程程序
  2. valgrind --tool=helgrind ./helbase

执行后会得到以下输出:

......
==7507==
==7507== Possible data race during read of size 4 at 0x404030 by thread #1
==7507== Locks held: none
==7507==    at 0x401177: main (helbase.c:13)
==7507==
==7507== This conflicts with a previous write of size 4 by thread #2

==7507== ----------------------------------------------------------------
==7507==
==7507== Possible data race during write of size 4 at 0x404030 by thread #1
==7507== Locks held: none
==7507==    at 0x401180: main (helbase.c:13)
==7507==
==7507== This conflicts with a previous write of size 4 by thread #2
......

我们可以看到,Valgrind给出了一些警告信息,提示我们代码中的问题。其中:

==7507== Possible data race during read of size 4 at 0x404030 by thread #1
==7507== Locks held: none
==7507==    at 0x401177: main (helbase.c:13)
==7507==
==7507== This conflicts with a previous write of size 4 by thread #2

这部分告诉我们,在没有加锁的情况下,两个线程同时读写了同一变量。

为了解决这个问题,我们可以在代码中添加锁,例如

#include <pthread.h>

pthread_mutex_t mutex; // 声明一个互斥锁
int var = 0;

void* child_fn ( void* arg ) {
  pthread_mutex_lock(&mutex);
  var++;
  pthread_mutex_unlock(&mutex);
   return NULL;
}

int main ( void ) {
   pthread_t child;
   pthread_create(&child, NULL, child_fn, NULL);
   pthread_mutex_lock(&mutex);
   var++;
   pthread_mutex_unlock(&mutex);
   pthread_join(child, NULL);
   return 0;
}
  

在子线程和主线程访问共享变量var之前,先加锁。这样,就可以确保线程安全了。

总结

本文介绍了Valgrind工具的基本用法,包括内存检查和竞争条件检查。Valgrind是一个非常实用的工具,可以帮助我们发现程序中的一些难以察觉的问题,从而提高代码的质量和可靠性。虽然Valgrind有许多高级用法,但是本文只是对其基础用法进行了介绍,希望能对读者有所启发。