popen()/ fgets()间歇性地返回不完整的输出

时间:2014-09-15 16:25:00

标签: c linux pthreads signals posix

Linux系统上的popenfgets库函数遇到了一个奇怪的问题。

证明问题的简短程序如下:

  1. SIGUSR1安装信号处理程序。
  2. 创建辅助线程以重复发送SIGUSR1到主线程。
  3. 在主线程中,通过popen()重复执行一个非常简单的shell命令,通过fgets()获取输出,并检查输出是否为预期长度。
  4. 间歇性地意外截断输出。为什么?

    命令行调用示例:

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

    我的机器的详细信息(程序也将编译并运行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);
        }
    }
    

1 个答案:

答案 0 :(得分:1)

如果在使用FILE读取时fgets流上发生错误,则在fgets返回NULL之前,是否将某些读取的字节传输到缓冲区是未定义的不是(C99规范的7.19.7.2)。因此,如果在fgets调用中发生SIGUSR1信号并导致EINTR,则可能会从流中丢失某些字符。

结果是,如果底层系统调用可能具有可恢复的错误返回(例如FILEEINTR,则您无法使用stdio函数来读/写EAGAIN个对象),因为不能保证标准库在发生这种情况时不会从缓冲区中丢失一些数据。你可以声称这是一个&#34; bug&#34;在标准库实现中,但它是C标准允许的错误。