我无法查看 <unistd.h> 中定义的 read() 函数代码

I have a trouble with looking into the read() function code defined in <unistd.h>

我现在试图通过查看实际代码实现来了解 read(2) 函数的工作原理,首先,我尝试查看它是如何在#include 头文件中定义的。

在那个文件中,我找到了这个:

ssize_t  read(int, void *, size_t) __DARWIN_ALIAS_C(read);

然后,我用谷歌搜索找到实际的 read() 函数声明。

而且,

https://github.com/lattera/glibc/blob/master/io/read.c

我找到了这个。在这段代码中,

/* Read NBYTES into BUF from FD.  Return the number read or -1.  */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
  if (nbytes == 0)
    return 0;
  if (fd < 0)
    {
      __set_errno (EBADF);
      return -1;
    }
  if (buf == NULL)
    {
      __set_errno (EINVAL);
      return -1;
    }

  __set_errno (ENOSYS);
  return -1;
}

下面是我的问题。

  1. read之前的__libc_是什么? 为什么需要它?还有当用户调用read(2)时,这个函数怎么调用?

  2. 在我看来,这段代码与从文件描述符读取缓冲区无关,而只是处理可能出现的错误的代码:fd < 0 或 buff 为 NULL,等等 那么,代码到底在哪里实现了read(2)函数的实际功能呢?

我是否以错误的方式或来源查找和发现?

您遗漏了已发布代码的重要部分。

weak_alias (__libc_read, __read)
weak_alias (__libc_read, read)

使用什么前缀并不重要。此函数 __libc_read 用作系统调用 read 的存根函数。如果链接器没有找到系统调用 read 而不是使用存根,那将 return 错误代码 ENOSYS.

因为read是系统调用,你应该在OS源文件中搜索它的实现。实现取决于使用的文件描述符。例如,如果在 Linux 中为文件系统调用 read,则 read 的代码在这里:http://lxr.linux.no/linux+v4.15.14/fs/read_write.c#L566

read(传统上,Unix 手册 "section 2" 中定义的所有函数——这就是 (2) 的意思)是一个 系统打电话。这意味着大部分工作是由操作系统内核完成的,而不是由您自己进程中的代码完成的。 C 库仅包含一个 system-call 包装器,它执行将控制转移到内核的特殊指令。

您找到的代码是占位符,而不是 system-call 包装器。正如您猜测的那样,它实际上并没有实现 read。它只会被临时使用,在不完整的操作系统端口中使用,该操作系统没有名为 read 的系统调用。 None 您正在查看的 C 库中的完整端口实际上使用了该代码。他们改为使用真正的 system-call 包装器。这个 C 库在构建时自动生成 system-call 包装器,所以我不能 link 实际代码,但我可以向您展示一个示例,说明为 system-call 包装器生成的代码可能看起来像。 (注意:这不是我熟悉的任何操作系统上使用的实际代码。我故意删除了一些复杂的部分。)

    .text
    .globl read
    .type read, @function
read:
    movl $SYS_read, %eax
    syscall
    testq %rax
    js .error
    ret
.error:
    negl %eax
    movq errno@gottpoff(%rip), %rdx
    movl %eax, %fs:(%rdx)
    movq $-1, %rax
    ret

我特意用 x86 汇编语言写了这个例子,因为没有办法从纯 C 中得到特殊的 syscall 指令。一些 C 库使用 "assembly insert" 扩展 syscall 指令并用 C 编写包装器的其余部分,但对于您想要理解的内容,您应该考虑汇编语言。

在内核中,有一个特殊的 "trap handler" 接受来自 syscall 指令的控制。它查看 %eax 中的值,看到它是 系统调用号 SYS_read (实际数值可能从 OS 到 OS),并调用实际实现read操作的代码。

在系统调用returns之后,包装器测试它是否返回负数。如果是这样,则表明有错误。 (注意:这是我删除了一些并发症的地方之一。)它翻转该数字的符号,将其复制到 errno (这比 mov %eax, errno 更复杂,因为 errnothread-local variable),并且 returns -1。否则返回的值是读取的字节数,直接 returns 。

另一个答案 link 是 read 的实现,但不幸的是它来自 OS 内核,该内核很流行但复杂且难以理解。遗憾的是,我没有更好的教学示例可供参考。


read 占位符实现上的 __libc_ 前缀在那里,因为在这个 C 库中 read 实际上有三个不同的名称:read__read,以及 __libc_read。正如另一个答案所指出的,在您引用的代码下方有一些特殊的宏,这些宏将它们全部安排为同一函数的名称。 read 的 auto-generated 真实 system-call 包装器也将具有所有这些名称。

这是实现 "namespace cleanliness" 的 hack,只有在您打算实现 full-fledged 和完全符合标准的 C 库时才需要担心。简而言之就是C库中有很多函数需要调用read,但是不能用nameread来调用,因为a技术上允许 C 程序定义一个名为 read 的函数。

顺便说一句,您需要注意查看属于相同 C 库的headers 和实现代码。您的计算机上似乎有来自 MacOS 的 unistd.h,但您找到的 read 代码属于 GNU C 库,这是一个完全不同的实现。 read

的基本声明
ssize_t read(int, void *, size_t);

由 POSIX 标准指定,因此两者相同,但之后的 __DARWIN 是 MacOS C 库的一个怪癖. GNU 库有一个具有不同怪癖的声明:

extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;