popen()/fgets()间歇地返回不完整的输出 [英] popen()/fgets() intermittently returns incomplete output

查看:876
本文介绍了popen()/fgets()间歇地返回不完整的输出的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在Linux系统上,popenfgets库函数遇到一个奇怪的问题.

I am experiencing a strange problem with the the popen and fgets library functions on a Linux system.

一个简短的演示问题的程序如下:

A short program demonstrating the problem is below that:

  1. SIGUSR1安装信号处理程序.
  2. 创建一个辅助线程以将SIGUSR1重复发送到主线程.
  3. 在主线程中,通过popen()重复执行一个非常简单的shell命令,通过fgets()获取输出,并检查输出是否具有预期的长度.
  1. Installs a signal handler for SIGUSR1.
  2. Creates a secondary thread to repeatedly send SIGUSR1 to the main thread.
  3. In the main thread, repeatedly executes a very simple shell command via popen(), gets the output via fgets(), and checks to see if the output is of the expected length.

输出意外地被间歇性地截断.为什么?

命令行调用示例:

$ gcc -Wall test.c -lpthread && ./a.out 
iteration 0
iteration 1
iteration 2
iteration 3
iteration 4
iteration 5
unexpected length: 0

我的机器的详细信息(该程序还将使用此在线C编译器进行编译和运行):

Details of my machine (the program will also compile and run with this online C compiler):

$ cat /etc/redhat-release
CentOS release 6.5 (Final)

$ uname -a
Linux localhost.localdomain 2.6.32-431.17.1.el6.x86_64 #1 SMP Wed May 7 23:32:49 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

# gcc 4.4.7
$ gcc --version
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4)
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

# glibc 2.12
$ ldd --version
ldd (GNU libc) 2.12
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

程序:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <pthread.h>
#include <errno.h>

void dummy_signal_handler(int signal);
void* signal_spam_task(void* arg);
void echo_and_verify_output();
char* fgets_with_retry(char *buffer, int size, FILE *stream);

static pthread_t main_thread;

/**
 * Prints an error message and exits if the output is truncated, which happens
 * about 5% of the time.
 *
 * Installing the signal handler with the SA_RESTART flag, blocking SIGUSR1
 * during the call to fgets(), or sleeping for a few milliseconds after the
 * call to popen() will completely prevent truncation.
 */
int main(int argc, char **argv) {

    // install signal handler for SIGUSR1
    struct sigaction sa, osa;
    sa.sa_handler = dummy_signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, &osa);

    // create a secondary thread to repeatedly send SIGUSR1 to main thread
    main_thread = pthread_self();
    pthread_t spam_thread;
    pthread_create(&spam_thread, NULL, signal_spam_task, NULL);

    // repeatedly execute simple shell command until output is unexpected
    unsigned int i = 0;
    for (;;) {
        printf("iteration %u\n", i++);
        echo_and_verify_output();
    }

    return 0;
}

void dummy_signal_handler(int signal) {}

void* signal_spam_task(void* arg) {
    for (;;)
        pthread_kill(main_thread, SIGUSR1);
    return NULL;
}

void echo_and_verify_output() {

    // run simple command
    FILE* stream = popen("echo -n hello", "r");
    if (!stream)
        exit(1);

    // count the number of characters in the output
    unsigned int length = 0;
    char buffer[BUFSIZ];
       while (fgets_with_retry(buffer, BUFSIZ, stream) != NULL)
        length += strlen(buffer);

    if (ferror(stream) || pclose(stream))
        exit(1);

    // double-check the output
    if (length != strlen("hello")) {
        printf("unexpected length: %i\n", length);
        exit(2);
    }
}

// version of fgets() that retries on EINTR
char* fgets_with_retry(char *buffer, int size, FILE *stream) {
    for (;;) {
        if (fgets(buffer, size, stream))
            return buffer;
        if (feof(stream))
            return NULL;
        if (errno != EINTR)
            exit(1);
        clearerr(stream);
    }
}

推荐答案

如果使用fgets进行读取时FILE流上发生错误,则尚不确定在是否返回NULL(C99规范的7.19.7.2).因此,如果在fgets调用期间发生SIGUSR1信号并导致EINTR,则可能会从流中丢失某些字符.

If an error occurs on a FILE stream while reading with fgets, it's undefined as to whether some bytes read are transferred to the buffer before fgets returns NULL or not (7.19.7.2 of the C99 spec). So if the SIGUSR1 signal occurs while in the fgets call and causes an EINTR, its possible that some characters may be lost from the stream.

结果是,如果基础系统调用可能具有可恢复的错误返回(例如EINTREAGAIN),则不能使用stdio函数读取/写入FILE对象,因为无法保证标准发生这种情况时,库不会从缓冲区中丢失一些数据.您可以声称这是标准库实现中的错误",但这是C标准允许的错误.

The upshot is that you can't use stdio functions to read/write FILE objects if the underlying system calls might have recoverable error returns (such as EINTR or EAGAIN), as there's no guarantee the standard library won't lose some data from the buffer when that happens. You can claim that this is a "bug" in the standard library implementation, but it is a bug that the C standard allows.

这篇关于popen()/fgets()间歇地返回不完整的输出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆