【问题标题】:pthread_kill() gives segmentation fault when called from second threadpthread_kill() 从第二个线程调用时会出现分段错误
【发布时间】:2015-01-05 22:31:40
【问题描述】:

当程序的主线程被read() 系统调用阻塞时,我试图手动中断它。我在第二个线程中通过调用pthread_kill() 执行此操作,但是发生了分段错误。但是,如果我在第二个线程中调用read(),即不是主线程并从主线程调用pthread_kill(),那么一切都会按预期工作。

例如,以下代码导致分段错误,我在第二个线程中调用pthread_kill(),大约在它启动后 2 秒。它使用通过(在主线程中)调用pthread_self()获得的主线程的pthread_t:

示例 1

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <unistd.h>
#include <signal.h>


static int fd = 0;
unsigned char buf[255];
static pthread_t s;
void sigHandler(int sig){
    printf("Signal handler called.\n");
}

void * closeFD(void *arg){
    printf("Second thread started.\n");
    sleep(2);
    int r = pthread_kill(s, SIGUSR1);
}

int main(char *argv[], int argc){
    struct termios newtio;
    pthread_t t1;
    unsigned char buf[255];
    void *res;
    struct sigaction int_handler = {.sa_handler=sigHandler};
    sigaction(SIGUSR1,&int_handler,0);
    s = pthread_self();
    printf("Process id is: %d.\n", getpid());
    fd = open("/dev/ttyS0", O_RDONLY | O_NOCTTY);
    if (fd != -1){
        bzero(&newtio, sizeof(newtio));
        newtio.c_cflag = B2400 | CS7 | CLOCAL | CREAD ;
        newtio.c_iflag = ICRNL;
        newtio.c_oflag = 0;
        newtio.c_lflag = ~ICANON;
        newtio.c_cc[VMIN]     = 14;
        tcsetattr(fd,TCSANOW,&newtio);
        pthread_create(&t1, NULL, closeFD, NULL);
        printf("Reading ..\n");
        read(fd,buf,255);
        close(fd);
    }
    return 0;
}

以下代码是相同的,只是我将调用 read() 放在第二个线程(在 closeFD() 中)并按预期工作。第二个线程解除阻塞并终止,而主线程等待它退出然后自行退出。

示例 2:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <unistd.h>
#include <signal.h>


static int fd = 0;
unsigned char buf[255];
static pthread_t s;
void sigHandler(int sig){
    printf("Signal handler called.\n");
}

void * closeFD(void *arg){
    printf("Second thread started.\n");
    read(fd,buf,255);
    printf("Read interrupted.\n");
}

int main(char *argv[], int argc){
    struct termios newtio;
    pthread_t t1;
    unsigned char buf[255];
    void *res;
        struct sigaction int_handler = {.sa_handler=sigHandler};
        sigaction(SIGUSR1,&int_handler,0);
        s = pthread_self();
        printf("Process id is: %d.\n", getpid());
        fd = open("/dev/ttyS0", O_RDONLY | O_NOCTTY);
        if (fd != -1){
            bzero(&newtio, sizeof(newtio));
            newtio.c_cflag = B2400 | CS7 | CLOCAL | CREAD ;
            newtio.c_iflag = ICRNL;
            newtio.c_oflag = 0;
            newtio.c_lflag = ~ICANON;
            newtio.c_cc[VMIN]     = 14;
            tcsetattr(fd,TCSANOW,&newtio);
            pthread_create(&t1, NULL, closeFD, NULL);
            sleep(2);
            int r = pthread_kill(t1, SIGUSR1);
            pthread_join(t1, &res);
            close(fd);
        }
    return 0;
}

到目前为止,我还没有找到具体的参考资料,说明从第二个(在同一进程中)终止主线程是非法操作,那么我做错了什么吗?

更新 #1
感谢所有回复的人,但是我应该澄清几点:

  1. 我知道在信号处理程序中使用printf 是不安全的,但这是一个示例,它不是分段错误的原因,尽管它是一个有效点。将printf() 从信号处理程序中取出仍然会导致分段错误。示例 2 与信号处理程序中的 printf() 一起使用。
  2. 我知道发送 SIGUSR 不会终止程序。但是,通过使用pthread_kill(pthread_t thread, int signal),它会向线程thread 发送一个信号,它会解除阻塞(如果它确实被阻塞了)。这是我想要的操作,这是示例 2 中实际发生的情况,这是我理解 应该 在任一示例中发生的情况,但在示例 1 中不会发生。
  3. 在描述示例 1 时,当我指的是“线程”时,我使用了术语“方法”,其中提到了对 pthread_kill() 的调用。

进一步,引自“使用 POSIX 线程编程”,David R. Butenhof,第 6.6.3 节 p217 'pthread_kill':

在一个进程中,一个线程可以向一个特定线程发送信号 (包括它自己)调用pthread_kill。

话虽如此,以下示例还给出了分段错误:

示例 3

#include <stdio.h>
#include <string.h>
#include <string.h>
#include <signal.h>



static pthread_t s;
int value = 0;

void sigHandler(int sig){
    value = 1;
}


int main(char *argv[], int argc){

    struct sigaction int_handler = {.sa_handler=sigHandler};
    sigaction(SIGUSR1,&int_handler,0);
    s = pthread_self();
    printf("The value of 'value' is %d.\n", value);
    printf("Process id is: %d.\n", getpid());
    int r = pthread_kill(s, SIGUSR1);
    printf("The value of 'value' is %d.\n", value);
    return 0;
}

如果不是对sigaction() 的调用被替换为对signal() 的(不可移植的)调用,这也会失败。考虑到第三个示例,非常很简单,我无法找到任何明确声明这是非法行为的文档。事实上,引用的参考表明它是允许的!

【问题讨论】:

  • AFAIK printf 不是异步信号安全的,因此您的两个程序都有未定义的行为。 (并不是说这会改变您观察到的行为。)
  • 我试图重现这个问题,但我无法访问我正在使用的系统上的 /dev/ttyS0。所以我尝试让主线程执行sleep(10) 而不是从文件中读取,它正确地运行信号处理程序而没有段错误。如果没有从 /dev/ttyS0 读取主线程,是否还会出现段错误?
  • 这一行,在线程中:'int r = pthread_kill(s, SIGUSR1);'将杀死当前线程,而不是主线程。建议使用“pthread_exit(null)”之类的东西来退出当前线程。 main() 可能正在做类似 pthread_join(s);等待线程完成
  • 大家好,请参阅我的原始帖子,其中我添加了更多信息和另一个失败的示例。

标签: c pthreads


【解决方案1】:

你忘了#include &lt;pthread.h&gt;。这在最近的 Linux 系统上为我解决了示例 #3 中的段错误。

--- pthread_kill-self.c.orig    2015-01-06 14:08:54.949000690 -0600
+++ pthread_kill-self.c 2015-01-06 14:08:59.820998965 -0600
@@ -1,6 +1,6 @@
 #include <stdio.h>
 #include <string.h>
-#include <string.h>
+#include <pthread.h>
 #include <signal.h>

然后……

$:- gcc -o pthread_kill-self pthread_kill-self.c -pthread
$:- ./pthread_kill-self 
The value of 'value' is 0.
Process id is: 3152.
The value of 'value' is 1.

【讨论】:

  • 顺便说一句 -Wall 为我抱怨这个。
  • 天哪,我脸红了!我知道它必须是这样简单的!我没有使用 -Wall 命令行开关。这让我意识到我需要阅读更多关于 pthread 的信息。感谢您的帮助!
【解决方案2】:

您正在使用printf(),而不是async-signal safe,并且您没有正确初始化您的struct sigaction(特别是未定义信号掩码)。

第三,发送SIGUSR1 信号并安装处理程序,不会也不应该终止主线程。你只是向它发送一个信号,仅此而已。

正如 Jens Gustedt 在对原始问题的评论中提到的,这两个程序都有未定义的行为。因此,我不会尝试去猜测究竟是未定义行为的哪一部分导致了分段错误(在第一个程序中)。

相反,我将向您展示一个工作示例。

出于调试/测试目的,我喜欢从异步信号安全输出函数开始,基于write(2):

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <termios.h>
#include <pthread.h>
#include <errno.h>
#include <time.h>

#define  MYSIGNAL  SIGUSR1

#define  SECONDS   10

static int wrstr(const int descriptor, const char *p, const char *const q)
{
    while (p < q) {
        ssize_t n;

        n = write(descriptor, p, (size_t)(q - p));
        if (n > (ssize_t)0)
            p += n;
        else
        if (n != (ssize_t)-1)
            return EIO;
        else
        if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)
            return errno;
    }

    return 0;
}

static const char *ends(const char *s)
{
    if (s)
        while (*s != '\0')
            s++;
    return s;
}

static int wrout(const char *const p)
{
    if (p != NULL && *p != '\0') {
        int saved_errno, result;
        saved_errno = errno;
        result = wrstr(STDOUT_FILENO, p, ends(p));
        errno = saved_errno;
        return result;
    } else
        return 0;
}

static int wrouti(const int value)
{
    char          buffer[32];
    char         *p = buffer + sizeof buffer;
    unsigned int  u;

    if (value < 0)
        u = -(long)value;
    else
        u = value;

    do {
        *(--p) = '0' + (u % 10U);
        u /= 10U;
    } while (u > 0U);

    if (value < 0)
        *(--p) = '-';

    return wrstr(STDOUT_FILENO, p, buffer + sizeof buffer);
}

static int wrerr(const char *const p)
{
    if (p != NULL && *p != '\0') {
        int saved_errno, result;
        saved_errno = errno;
        result = wrstr(STDERR_FILENO, p, ends(p));
        errno = saved_errno;
        return result;
    } else
        return 0;
}

上述函数是异步信号安全的,因此可以在信号处理程序中使用。 wrout() 和 wrerr() 也保持 errno 不变,这很有用。顺便说一句,通常会省略在信号处理程序中保存和恢复errno,尽管我确实相信有一些奇怪的极端情况可能很重要。 wrouti() 只是一个粗略的十进制有符号整数打印机,也是异步信号安全的,但它不会保持 errno 不变。

接下来,让我们定义信号处理程序本身,以及它的安装程序函数。 (我喜欢这样做,让main() 更简单。)

static volatile sig_atomic_t handled = 0;

static void handler(int signum)
{
    wrerr("Signal received.\n");
    handled = signum;
}

static int install_handler(const int signum)
{
    struct sigaction act;

    /* memset(&act, 0, sizeof act); */   
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;
    act.sa_flags = 0;

    if (sigaction(signum, &act, NULL))
        return errno;

    return 0;
}

建议使用注释掉的 memset,但不是正确操作所必需的。但是,sigemptyset() 是必需的,以清除阻塞的信号集。

接下来,我们来看看线程函数。你不应该使用 sleep(),因为它会与信号交互;请改用 POSIX.1-2001 nanosleep()。

static void *worker(void *target)
{
    struct timespec duration, left;
    int retval;

    wrout("Worker started. Sleeping ");
    wrouti((int)SECONDS);
    wrout(" seconds...\n");

    duration.tv_sec = SECONDS;
    duration.tv_nsec = 0;
    left.tv_sec = 0;
    left.tv_nsec = 0;

    while (1) {
        retval = nanosleep(&duration, &left);
        if (retval == 0)
            break;

        if (left.tv_sec <= 0 ||
            (left.tv_sec == 0 && left.tv_nsec <= 0))
            break;

        duration = left;
        left.tv_sec = 0;
        left.tv_nsec = 0;
    }

    wrout("Sleep complete.\n");

    if (target) {
        wrout("Sending signal...\n");

        retval = pthread_kill(*(pthread_t *)target, MYSIGNAL);
        if (retval == 0)
            wrout("Signal sent successfully.\n");
        else {
            const char *const errmsg = strerror(retval);
            wrout("Failed to send signal: ");
            wrout(errmsg);
            wrout(".\n");
        }
    }

    wrout("Thread done.\n");
    return NULL;
}

给线程函数的指针应该指向信号指向的线程标识符(pthread_t)。

请注意,在上面,nanosleep() 可以被信号传​​递中断,如果信号被传递到该特定线程或被该特定线程捕获。如果发生这种情况,nanosleep() 会告诉我们还有多少时间可以睡觉。上面的循环显示了如何确保您至少在指定的时间睡眠,即使被信号传递中断。

最后是main()。我没有打开特定设备,而是使用标准输入。要重现 OP 的程序,请在执行时重定向来自/dev/ttyUSB0 的标准输入,即./program &lt; /dev/ttyUSB0。

int main(void)
{
    pthread_t main_thread, worker_thread;
    pthread_attr_t attrs;
    struct termios original, settings;
    int result;

    if (!isatty(STDIN_FILENO)) {
        wrerr("Standard input is not a terminal.\n");
        return EXIT_FAILURE;
    }

    if (tcgetattr(STDIN_FILENO, &original) != 0 ||
        tcgetattr(STDIN_FILENO, &settings) != 0) {
        const char *const errmsg = strerror(errno);
        wrerr("Cannot get terminal settings: ");
        wrerr(errmsg);
        wrerr(".\n");
        return EXIT_FAILURE;
    }

    settings.c_lflag = ~ICANON;
    settings.c_cc[VMIN] = 14;

    if (tcsetattr(STDIN_FILENO, TCSANOW, &settings) != 0) {
        const char *const errmsg = strerror(errno);
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &original);
        wrerr("Cannot set terminal settings: ");
        wrerr(errmsg);
        wrerr(".\n");
        return EXIT_FAILURE;
    }

    wrout("Terminal is now in raw mode.\n");

    if (install_handler(MYSIGNAL)) {
        const char *const errmsg = strerror(errno);
        wrerr("Cannot install signal handler: ");
        wrerr(errmsg);
        wrerr(".\n");
        return EXIT_FAILURE;
    }

    main_thread = pthread_self();

    pthread_attr_init(&attrs);
    pthread_attr_setstacksize(&attrs, 65536);

    result = pthread_create(&worker_thread, &attrs, worker, &main_thread);
    if (result != 0) {
        const char *const errmsg = strerror(errno);
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &original);
        wrerr("Cannot create a worker thread: ");
        wrerr(errmsg);
        wrerr(".\n");
        return EXIT_FAILURE;
    }

    pthread_attr_destroy(&attrs);

    wrout("Waiting for input...\n");

    while (1) {
        char    buffer[256];
        ssize_t n;

        if (handled) {
            wrout("Because signal was received, no more input is read.\n");
            break;
        }

        n = read(STDIN_FILENO, buffer, sizeof buffer);
        if (n > (ssize_t)0) {
            wrout("Read ");
            wrouti((int)n);
            wrout(" bytes.\n");
            continue;

        } else
        if (n == (ssize_t)0) {
            wrout("End of input.\n");
            break;

        } else
        if (n != (ssize_t)-1) {
            wrout("read() returned an invalid value.\n");
            break;

        } else {
            result = errno;

            wrout("read() == -1, errno == ");
            wrouti(result);
            wrout(": ");
            wrout(strerror(result));
            wrout(".\n");
            break;
        }
    }

    wrout("Reaping the worker thread..\n");
    result = pthread_join(worker_thread, NULL);
    if (result != 0) {
        wrout("Failed to reap worker thread: ");
        wrout(strerror(result));
        wrout(".\n");
    } else
        wrout("Worker thread reaped successfully.\n");

    tcsetattr(STDIN_FILENO, TCSAFLUSH, &original);
    wrout("Terminal reverted back to original mode.\n");

    return EXIT_SUCCESS;
}

因为使用终端测试更有趣,所以上面尝试将终端恢复到返回之前的原始状态。

请注意,由于 termios 结构中的 VMIN 字段设置为 14,read() 会阻塞,直到缓冲区中至少有 14 个字节可用。如果传递了一个信号,如果缓冲区中至少有一个字节,则返回一个 short count。因此,您不能指望read() 总是返回 14 个字节,也不能指望它在传递信号时返回 -1 和 errno == EINTR!试验这个程序非常有用,可以在你的脑海中澄清这些。

我不记得 Linux 中的 USB 串行驱动程序是否会产生 EPIPE 或引发 SIGPIPE,但在使用管道时肯定会发生这种情况。使用管道时,最常见的原因是在读取已经返回零(输入结束)后尝试读取。除非被信号处理程序忽略或捕获,否则进程会像分段错误一样终止,只是原因是SIGPIPE 信号而不是SIGSEGV。对于类似终端的字符设备,这取决于驱动程序,我似乎记得。

最后,我在天气(流感)下写了上面的代码,所以在tharrr中可能存在错误。它应该是 POSIX.1 C99 代码,gcc -Wall -pedantic 没有抱怨,但是脑袋塞满了,我在这里不做任何承诺。修复非常受欢迎!

问题?评论?

【讨论】:

  • 嗨标称。感谢您的深入回答。我在原始帖子中澄清了几点,并给出了第三个示例,该示例非常简单,但仍然导致分段错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-12
  • 1970-01-01
相关资源
最近更新 更多