【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有许多高档用法,但是本文只是对其根底用法进行了介绍,希望能对读者有所启示。