我无法查看 <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;
}
下面是我的问题。
read
之前的__libc_
是什么?
为什么需要它?还有当用户调用read(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
更复杂,因为 errno
是 thread-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;
我现在试图通过查看实际代码实现来了解 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;
}
下面是我的问题。
read
之前的__libc_
是什么? 为什么需要它?还有当用户调用read(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
更复杂,因为 errno
是 thread-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;