gdb 下调试多线程

gdb 下调试多线程

最近接触的工作需要多线程的一些知识才能完成,而以前没有多线程相关知识,现在进行一些简单的学习和研究.本文内容偏向实践,对于想要了解原理的请去看计算机线程方面的资料和书籍.

1.gdb下调试多进程代码的注意事项

最近在调试这样一段代码的时候, 由于我不具备任何多线程的知识,导致我在找错误的时候出现了严重的问题,代码如下:

/***********************************************************************
 * This is a STC that causes the following error on my test machine:
 *   NtCreateEvent(lock): 0xC0000035
 *
 * It tries to use flock() for file locking. It creates a temporary
 * file, the uses fork to spawn a number of children. Each child opens
 * the file, then repeatedly uses flock to lock and unlock it.
 *
 * This test was extracted from the APR test suite.
 *
 * Compile: gcc -Wall -o stc-flock-fork stc-flock-fork.c
 ***********************************************************************/

#include <sys/types.h>
#include <sys/file.h>
#include <sys/wait.h>

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define MAX_ITER 2000
#define CHILDREN 6

/* A temporary file used for flock. */
char tmpfilename[] = "/tmp/flocktstXXXXXX";

/* Fork and use flock to lock and unlock the file repeatedly in the child. */
void make_child(int trylock, pid_t *pid)
{
    if ((*pid = fork()) < 0) {
        perror("fork failed");
        exit(1);
    }
    else if (*pid == 0) {
        int fd2 = open(tmpfilename, O_RDONLY);
        if (fd2 < 0) {
            perror("child open");
            exit(1);
        }

        int rc;
        int i;
        for (i=0; i<MAX_ITER; ++i) {
            do {
                rc = flock(fd2, LOCK_EX);
            } while (rc < 0 && errno == EINTR);
            if (rc < 0) {
                perror("lock");
                exit(1);
            }
            
            do {
                rc = flock(fd2, LOCK_UN);
            } while (rc < 0 && errno == EINTR);
            if (rc < 0) {
                perror("unlock");
                exit(1);
            }
        }
        exit(0);
    }
}

/* Wait for the child to finish. */
void await_child(pid_t pid)
{
    pid_t pstatus;
    int exit_int;

    do {
        pstatus = waitpid(pid, &exit_int, WUNTRACED);
    } while (pstatus < 0 && errno == EINTR);
}

int main(int argc, const char * const * argv, const char * const *env)
{
    pid_t child[CHILDREN];
    int n;
    int fd;
 
    /* Create the temporary file. */
    fd = mkstemp(tmpfilename);
    if (fd < 0) {
        perror("open failed");
        exit(1);
    }
    close(fd);
    
    /* Create the children. */
    for (n = 0; n < CHILDREN; n++)
        make_child(0, &child[n]);

    /* Wait for them to finish. */
    for (n = 0; n < CHILDREN; n++)
        await_child(child[n]);

    /* Clean up. */
    unlink(tmpfilename);
    return 0;
}

调试环境为在Linux下的Wine下运行的MSYS2(32bit) 在MSYS2内运行的gdb, 这段代码在运行到

if ((*pid = fork()) < 0) 

这一行的时候 就出现了错误,导致我认为是fork出了问题,不过实际上不是这样的, 在我了解fork的具体操作后,才明白了为什么错误不发生在fork这一行但是却在fork上报错. 下面将会解释上面这个问题  首先要从fork的应用来说.

通过查看 man 2 fork我们可以了解到fork的使用方法和返回值的含义,  fork的作用是在某一个程序运行到fork()这里的时候 ,新开一个线程,这个线程与原来的线程独立,不过线程内的变量值等信息都与原来的线程一致, (他们不共用内存地址) , 这个线程就称为子进程, 原来的进程就叫做父进程. fork()的返回值为一个进程号, 返回的进程号为当前父进程的子进程的进程号,子进程的这个返回值为0(注意子进程是相对父进程而言) .

另外 在使用gdb调试的时候 ,如果出现了多线程的代码 ,那么不进行任何设置的情况下,gdb不会阻止(控制)子进程的执行,也就是说,就算你在用step in 或者next 对代码进行调试 ,但是遇到了 fork()之后 ,子进程会自动执行从 fork开始到代码末尾的所有语句,不会受gdb控制, 因而 ,如果是在fork之后的代码出了问题, 那么在用没有配置的gdb进行单步调试的时候,代码会在父进程fork之后报错,这样就不便于定位错误的位置. 下面用一段代码来验证一下我的这个说法:

/*************************************************************************
    > File Name: fork_test.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Thu 27 Aug 2015 05:46:19 PM CST
 ************************************************************************/
#include<stdio.h>
#include <unistd.h>
int main(void)
{
    printf("Now will fork a process from Parentn");
    int a = fork();
    printf("Forked PID is %dn",a);
    return 0;
}

上面这段代码做的事情很简单 ,我就不再多解释 ,将这段代码用 gdb -g -O0 编译之后  在gdb下调试

首先使用 start > tmpfile来启动调试进程, 这样所有的输出都会被重定向到一个文件 ,不会因为gdb的原因导致输出延迟, 然后 使用 next指令 执行完 fork这一行, 然后立即查看tmpfile的内容 你会发现 tmpfile的内容为:

Now will fork a process from Parent
Forked PID is 0

我们明明还没有执行到下一个printf,但是却输出了forked PID is 0 , 通过上文我们知道了 PID为0 意味着打印这个信息的进程为子进程 这也就说明了 gdb没有阻拦子进程的执行 ,让子进程执行完毕了,但是父进程依旧在被gdb控制,因此,如果是fork之后的代码出现了问题,由于未经配置的gdb不能监视子进程,子进程自动执行到出错的地方然后扔出错误信息,但是我们在gdb上看到的现象却是执行fork的时候报错了,这样就导致了判断不出来错误的所在.

2.用gdb调试多线程

那么遇到多线程的代码的时候应该如何调试呢, 官方文档给出了三种方法 https://sourceware.org/gdb/onlinedocs/gdb/Forks.html

相关信息已经有很好的列出,在这里就不多解释了. 不过需要注意一点 ,就是在无法使用 follow-fork-mode 和 follow-exec-mode的时候 (比如kernel版本不对,平台不对之类的问题) 那么就要在代码里插入sleep来进行调试, 这个方法是通用的一个方法

 

3.依旧存在疑惑的地方

上面那段代码 ,如果直接执行(将信息输出到stdout上) 那么结果为:

 

Now will fork a process from Parent
Forked PID is 22873
Forked PID is 0

这样三行内容    而如果重定向到文件或者管道的话 ,内容则变为:

Now will fork a process from Parent
Forked PID is 22882
Now will fork a process from Parent
Forked PID is 0

四行, 具体一为什么会出现这一现象的原因有待深入学习fork和重定向的相关知识

Leave a Reply

Your email address will not be published. Required fields are marked *

12 + seven =

This site uses Akismet to reduce spam. Learn how your comment data is processed.