Linux系统上的popen
和fgets
库函数遇到了一个奇怪的问题。
证明问题的简短程序如下:
SIGUSR1
安装信号处理程序。SIGUSR1
到主线程。popen()
重复执行一个非常简单的shell命令,通过fgets()
获取输出,并检查输出是否为预期长度。间歇性地意外截断输出。为什么?
命令行调用示例:
$ 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);
}
}
答案 0 :(得分:1)
如果在使用FILE
读取时fgets
流上发生错误,则在fgets
返回NULL之前,是否将某些读取的字节传输到缓冲区是未定义的不是(C99规范的7.19.7.2)。因此,如果在fgets
调用中发生SIGUSR1信号并导致EINTR
,则可能会从流中丢失某些字符。
结果是,如果底层系统调用可能具有可恢复的错误返回(例如FILE
或EINTR
,则您无法使用stdio函数来读/写EAGAIN
个对象),因为不能保证标准库在发生这种情况时不会从缓冲区中丢失一些数据。你可以声称这是一个&#34; bug&#34;在标准库实现中,但它是C标准允许的错误。