为什么从数据集的 /dev 源成功读取 errno?

Why does successfully reading from a /dev source of data set errno?

C 中的简单测试程序调用 get1:

#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?

errno 不是特定操作的结果,而是构建高级 FILE *stdin stdio 结构时支持操作的结果。

Libc 通常调用 stat(),少数其他调用和 lseek() 系统调用失败,因为 /dev/ 中的特定文件不是常规文件。它可能取决于特定的 Libc 实现,因此您在各种系统上看到的差异。

但这并不意味着您的操作失败,您应该仅在返回错误代码时检查 errno。

在 Linux 上,您可以使用命令 strace 获取您的程序正在调用的所有系统调用的列表以及导致错误号的确切调用:

strace ./my program

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

我不会说这是 预期的 行为(事实上,我无法在手边的系统上重现它),但 C 标准都允许这样做Posix.

C标准和Posix一般都禁止库函数将errno设置为0,所以无法清除errno表示成功。但是,C 允许 errno 被库函数修改“无论是否有错误,前提是 errno 的使用没有记录在 函数的描述”(§7.5 第 3 段)。C 标准没有记录 fgetcerrno 的使用。

Posix 更加灵活:它指定

The setting of errno after a successful call to a function is unspecified unless the description of that function specifies that errno shall not be modified.

它确实记录了 fgetcerrno 的使用,但该文档并未表明 errno 不应被成功调用修改,只是表明它应该被修改由于读取错误。

具体来说,Posix说的是

If a read error occurs, the error indicator for the stream shall be set, fgetc() shall return EOF, and shall set errno to indicate the error.

换句话说,EOF 可能表示 调用成功 (因为文件结尾不是错误)或者它可能表示错误。只有在后一种情况下 errno 才设置为有意义的值。因此,在检查 errno 之前,您需要验证 ferror(stdin) 是否报告错误。

因此,正如我所说,该行为是标准允许的。为什么 fgetc 实现会那样做?我的猜测是您系统上的 libc 实现在第一次调用时进行了某种惰性初始化。