C 语言 Fileno() 函数

7 Jan 2025 | 7 分钟阅读

引言

Fileno() 函数是 C 语言中的一个标准库函数,可以在 <stdio.h> 头文件中找到。它返回文件流的文件描述符。文件描述符是 Unix 系统中一个已打开文件的低级表示。许多系统 I/O 操作都涉及文件描述符的使用。一个文件流表示程序中的每个打开文件,并且该文件流背后都有一个文件描述符。fileno() 函数允许程序员获取该文件。

当您需要执行标准 I/O 函数不直接支持但可通过使用文件描述符的系统调用(如 read()、write()、close() 或其他直接与文件描述符交互的函数)执行的操作时,此函数特别有用。

语法

它具有以下语法:

参数

  • 流:指向 FILE 对象的指针,它是一个专门用于表示已打开文件流的特殊对象。
  • FILE *stream:这是一个指向已打开文件的指针,如果未打开,它将是指向 FILE 对象的指针。此文件流可能与通过库打开的文件相关联,例如标准输入、输出和错误文件流,如 stdin、stdout 和 stderr。

返回值

  • 成功时:它返回与 FILE 流关联的文件描述符。
  • 失败时:它们返回 -1 并将错误号指定给全局变量 errno 以告知错误。

标准流示例

  • Stdin:标准输入,通常是键盘。
  • Stdout:标准输出,通常输出到控制台/屏幕。
  • Stderr:标准错误(用于显示错误消息)。

示例

以下是一些示例,演示了 fileno() 函数在不同场景下的用法

示例 1:与 fopen() 的基本用法

输出

 
File descriptor: 3   

说明

  • 在此示例中,程序使用 fopen() 打开名为 example.txt 的文件以供读取。
  • 调用 fileno() 函数以检索与文件流关联的文件描述符。
  • 打印文件描述符,然后使用 fclose() 关闭文件。

特性、优点和缺点

特点

C 语言中 **fileno()** 函数的几个特性如下:

  • 无缝集成:它能够轻松地在文件 I/O 系统调用和传统系统级调用之间进行交互。
  • 多功能性:它与所有 FILE 流兼容,并且独立于 stdin、stdout 和 stderr。
  • 兼容性:可用于各种类 Unix 操作系统。

优点

C 语言中 **fileno()** 函数的几个优点如下:

  • 访问系统级操作:fileno() 函数提供了对简单 I/O 操作中未提供的系统调用的访问。
  • 效率:在某些情况下,对于涉及 read() 和 write() 的直接文件操作,使用文件描述符可能效率更高。
  • 错误处理:如果发生错误,则返回 -1 并设置 errno,以便进行适当的错误区分。

缺点

C 语言中 **fileno()** 函数的几个缺点如下:

  • 平台依赖性:在非 POSIX 环境(如 Windows)中很难找到,尤其是在常见的操作系统(如 LINUX 和 Mac)上,而主要在类 Unix 系统上可用。
  • 有限的用例:通常不需要用于大多数上层文件 I/O 操作,尤其是当需要高层函数进行文件操作时。

Unix-like 系统中的低级 I/O

在类 Unix 操作系统中,文件处理是系统编程的基本方面。操作系统为每个打开的文件分配一个唯一的标识符,称为 **文件描述符**。这些描述符是小的、非负整数,通常从 0 开始,随着打开更多文件的增加而递增。例如:

  • 文件描述符 0:对应于标准输入 (stdin)。
  • 文件描述符 1:对应于标准输出 (stdout)。
  • 文件描述符 2:对应于标准错误 (stderr)。

文件描述符提供了一种比标准文件流更低级别的与文件交互的机制。open()、read()、write() 和 close() 等函数依赖于文件描述符来执行文件操作。这与 fopen()、fread()、fprintf() 和 fclose() 等对文件流(FILE 指针)进行操作的更高级文件 I/O 操作形成对比。

当需要从文件流的较高级抽象迁移到文件描述符提供的更精细控制时,**fileno() 函数** 就变得至关重要。

实际用例

  • 混合 I/O 操作:当将文件流 (FILE *) 与需要文件描述符的系统级操作结合使用时,fileno() 提供了必要的链接。
  • 文件锁定和同步:在使用 fcntl() 或 flock() 等系统调用应用文件锁时,会使用 fileno() 函数。
  • 与库接口:它允许使用需要文件描述符的库,同时仍使用标准 C 文件 I/O 管理文件。
  • 标准流重定向:在自定义应用程序中将 stdin、stdout 或 stderr 重定向到/从文件或套接字时,它非常有用。

常见陷阱

  • 缓冲不匹配:根据 Kent C. Engle 关于如何避免糟糕 I/O 的指导,使用低级和高级 I/O 操作可能会出现问题,因为它们使用不同的缓冲技术。
  • 错误处理:这可能导致对无效文件描述符执行各种操作,因此需要检查 fileno() 的返回值。
  • 关闭流和描述符:在程序结束前或返回调用者之前,关闭文件流和文件描述符,以防止资源泄漏。

替代方案和比较

  • Windows 兼容性:在 Visual Studio 环境中,可以考虑使用 **_fileno()** 函数,或使用 #ifdef _WIN32 进行条件编译。
  • 直接描述符管理:在某些情况下,直接使用 open() 和管理文件描述符可能比与 fileno() 混合使用更简单且可移植性更好。

有效使用技巧

  • 隔离低级操作:如果可能,将低级和高级 I/O 操作分开,以最小化复杂性。
  • 小心使用标准流:在使用 fileno() 函数处理 stdin、stdout 和 stderr 时要小心,以避免在控制台应用程序中出现意外行为。

将 fileno() 与其他系统调用结合使用

  • 将 fcntl() 与 fileno() 结合使用:fileno() 函数可以与 fcntl() 一起使用,以更改文件描述符的属性,例如使其成为非阻塞或操作文件锁。
  • 与 select() 或 poll() 接口:当需要对多个文件描述符上的 I/O 操作进行多路复用以执行 select() 或 poll() 时,fileno() 函数是必需的。
  • 重定向输出:例如,如果您使用 **freopen()** 函数指示 stdout 写入文件,则使用该文件的文件描述符上的 fileno()。

实际应用

  • 守护进程:守护进程** 在 Unix 中通常需要关闭标准输入、输出和错误流,并用来自日志文件或套接字的文件描述符替换它们。fileno() 函数有助于管理这些过渡。
  • 自定义 Shell 或命令行工具:开发人员构建自定义 Shell 或命令行实用程序,通常使用 file() 来操作标准 I/O 流、重定向它们或直接使用系统级调用。
  • 网络服务器:网络服务器,特别是那些使用需要文件描述符的库(如 libevent 或 libuv)构建的服务器,通常使用 fileno() 将文件流与网络套接字操作集成。

安全注意事项

  • 描述符传递:如果您的应用程序涉及在进程之间传递文件描述符(例如,通过 Unix 域套接字),请确保安全处理,并使用 fileno() 等函数验证描述符。
  • 避免描述符泄漏:确保由 fileno() 检索的文件描述符已正确关闭,尤其是在多线程环境中,因为描述符泄漏可能导致安全漏洞或资源耗尽。

优化性能

  • 避免双重缓冲:在使用 fileno() 函数访问底层文件描述符时,要小心双重缓冲。在不进行适当处理的情况下在 fread()/fwrite() 和 read()/write() 之间切换可能会降低性能。
  • 与缓存结合使用:在性能关键型应用程序中,如果需要多个低级操作,请考虑缓存 fileno() 的结果,以避免冗余调用和潜在的开销。

调试技巧

  • 打印文件描述符:在调试时,打印由 fileno() 函数检索的文件描述符,以跟踪其使用情况并识别任何管理不当或泄漏。
  • 检查描述符有效性:在执行进一步操作之前,在 fileno() 调用后使用断言或条件检查,以确保文件描述符有效。

高级场景

  • 使用 fork() 进行多进程:在 fork() 的进程中,可以使用 **fileno()** 函数访问继承的文件描述符,以确保在父进程和子进程之间正确管理。
  • 自定义日志系统:fileno() 函数可用于将日志输出从标准流重定向到文件,从而为应用程序中的日志记录机制提供灵活性。

教学和学习用途

  • 教授低级 I/O:在教学环境中使用 fileno() 函数有助于学生理解文件流和文件描述符之间的区别,从而更清晰地了解类 Unix 系统中的系统级 I/O。
  • 概念结合:使用 fileno() 函数演示高级 I/O 抽象与低级系统函数相遇的实际场景,从而巩固 C 编程的多功能性。

结论

总之,C 语言中的 **fileno()** 函数是需要弥合高级文件流操作与低级文件描述符系统调用之间差距的开发人员的重要工具。它通过允许标准 I/O 函数与系统级操作之间无缝交互,提供了独特的优势,这在涉及网络、文件操作和系统编程的复杂应用程序中尤其有用。