为什么成功从/dev数据源读取数据会设置errno?

4

一个名为 get1 的 C 语言简单测试程序:

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

int main(void) {
    errno = 0;
    int ch = fgetc(stdin);
    printf("ch = %d\n", ch);
    if (errno)
        printf("errno = %d: %s\n", errno, strerror(errno));
    return 0;
}

它只打印第一个字节的十进制数,如果errno不为零,则显示errno和相关的错误消息。

一些结果(foo是一个文本文件,empty是长度为零的文件):

% ./get1 < foo
ch = 104
% ./get1 < empty
ch = -1

好的,如预期所料。然而:

% ./get1 < /dev/zero 
ch = 0
errno = 25: Inappropriate ioctl for device
% ./get1 < /dev/null 
ch = -1
errno = 25: Inappropriate ioctl for device
% ./get1 < /dev/random 
ch = 196
errno = 25: Inappropriate ioctl for device

读取操作正常,但是当我从这些设备中的任何一个读取时,它会设置errno。为什么?

这是在macOS(Darwin)上的情况。在Linux(Debian)和NetBSD上也遇到了相同的问题,只是对于/dev/random有一个不同的错误(其他设备上的错误与macOS上的相同):

% ./get1 < /dev/random 
ch = 170
errno = 22: Invalid argument

据我所知,如果没有收到 EOF,那么就不会出现错误。如果收到了 EOF,那么就需要检查 errno 是否存在错误。在所有这些情况下,从设备获取字节时似乎没有出现错误,也不应该有错误。但是 errno 被设置了。
(去掉 printf 不会改变任何东西,以防有人想问。)
事实证明,在 AIX、SunOS 或 OpenSUSE 上从这样的设备读取时,errno 并不会被设置。
那么这里出了什么问题?这是一个错误吗,尽管很普遍?这种行为可以接受吗?这是预期的行为吗?为什么会设置 errno
2个回答

4
errno 不是特定操作的结果,而是构建高级FILE *stdin stdio结构时支持操作的结果。
Libc通常调用stat()、其他一些函数和lseek()系统调用,因为/dev/中的特定文件不是常规文件,所以它会失败。这可能取决于特定的Libc实现,因此您在各种系统上看到了差异。
这并不意味着您的操作失败了,只有在返回错误代码时才应检查errno
在Linux上,您可以使用命令strace获取程序调用的所有系统调用列表和导致errno的确切调用。
strace ./my program

其他系统上也有类似的工具。


1
只有在返回错误代码的情况下才应检查errno。这个规则有一些例外:在调用strtol()和相关函数之前将errno设置为0,并在调用后将其与0进行比较,才能检测到溢出。 - chqrlie
1
@Stargateur:一个数字可以在没有太多位数的情况下溢出。 - rici
@rici 不用担心,strtol()即使出现溢出也会解析所有数字,因为没有无效字符。 - Stargateur
如果我不执行 getchar(),那么 errno 就不会被设置。因此,无论导致这种情况的不幸事件的细节如何,最终都是 getchar() 导致了 errno 被设置。 - Mark Adler
@MarkAdler:当然,FILE结构体的初始化是在第一次从stdin读取数据时完成的。在这种情况下,就是通过getchar函数实现的。 - Zbynek Vyskovsky - kvr000
显示剩余2条评论

2
我不认为这是“预期”的行为(事实上,在我手头的系统上无法重现),但它被C标准和Posix允许。
通常,C标准和Posix禁止库函数将errno设置为0,因此它们不能清除errno以表示成功。然而,C允许库函数修改errno,“无论是否存在错误,只要在函数的描述中未记录使用errno的情况”(§7.5 para 3)。C标准未记录fgetc使用errno的情况。
Posix更加灵活:它指定
“调用函数成功后errno的设置是未指定的,除非该函数的描述指定不得修改errno。”
它确实记录了fgetc使用errno的情况,但该文档没有指示成功调用不得修改errno,仅指示读取错误时应修改errno。
具体来说,Posix指出
“如果发生读取错误,则流的错误指示器将被设置,fgetc()将返回EOF,并且将设置errno以指示错误。”
换句话说,EOF可能表示一个成功的调用(因为文件结束不是错误),也可能表示一个错误。只有在后一种情况下,errno才会设置为有意义的值。因此,在检查errno之前,您需要验证ferror(stdin)是否报告了错误。
所以,正如我所说,这种行为是符合标准的。为什么fgetc实现会这样做?我猜测你们系统上的libc实现在第一次调用时进行了某种惰性初始化。

不正确。Posix标准指出getchar()等同于fgetc(),而fgetc()确实记录了使用errno的情况,特别是在读取错误时应该设置errno。只是为了严谨起见,我将其更改为fgetc()并获得了相同的行为。 - Mark Adler
实际上,由于它的行为相同,我将问题中的源代码更改为使用fgetc() - Mark Adler
@MarkAdler:是的,它将文档推迟到了fgetc,但仍然记录了它。但是文档并没有说在成功时不会修改errno。它所说的是,如果发生错误,流的错误指示器将被设置,正是这个事实——而不是EOF的返回——应该促使您检查errno - rici
啊,好的,那就是我问题的答案。我应该先检查ferror(stdin)。如果我这样做,我会发现没有错误。 - Mark Adler
顺便提一下,关于你删除的答案,C标准规定errno在执行开始时应该被设置为零。因此我的errno = 0是不必要的。 - Mark Adler
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接