Linux 编程提示
由于几个充分的理由,Linux 标准库以一种有些奇怪的方式实现了标准 I/O (stdio)。不幸的是,许多程序对 stdio 的实现方式做出了不必要的假设,导致这些程序无法在 Linux 下正确编译。我之前在本专栏中提到过这个问题;本月我将解释如何修复此类源代码,使其可以在包括 Linux 在内的任何操作系统下编译。
作者:Michael K. Johnson
Linux stdio 并不完全是非标准的;那样就意味着对于标准 I/O 应该如何实现存在一个真正的标准。理论上,所有使用 stdio 库的 I/O 操作都应该只使用“已发布的” FILE 机制,这些机制是抽象的,并且不应该关注细节。不幸的是,许多 stdio 实现相当慢,并且不提供程序所需的功能。
许多程序员没有编写可用的 stdio 替代品,而是选择滥用 stdio 接口,直接访问 FILE 结构的“私有”成员,这些成员不能保证在不同系统之间相同。实际上,这在不同系统之间运行良好,因为几乎所有系统都来自相同的来源,并且使用相同变量名的原型广泛可用。
例如,程序员了解到 FILE 结构的 _cnt 成员包含库已读取但应用程序尚未读取的字节数,而 _ptr 成员包含指向缓冲区的指针,库预取的字符存储在该缓冲区中。众所周知,在幕后,调用 _filbuf() (有时称为 _ _filbuf() ) 宏来使 stdio 库读取更多字符。
只要每个人都使用类似的 stdio 实现,这种方法就有效。许多备受推崇的应用程序使用这些方法来绕过 stdio;GNU emacs 和 Rand MH 邮件处理程序就是其中之一。
Linux 是不同的。
Linux stdio 基于 GNU libg++ iostream I/O。FILE 结构部分看起来像这样(来自 libio.h)
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */
这完全不同。它经过了更好的优化:它没有一个 _ptr 元素,而是有一个用于读取的指针和一个用于写入的指针,以及每个指针的缓冲区。它不是跟踪缓冲区中的字符数,而是保留指向每个缓冲区末尾的指针以及当前指针。这使得将各种事物用作文件变得更容易,包括共享内存、SYSV IPC 以及任何其他符合范例的事物;它是动态可扩展的。它还在 C++ 和 C 之间共享,并且由于作为标准 io 包获得的额外测试,使得 C++ iostream 实现更加健壮。
如果您过去使用过 Linux 或 GNU C 库,您会注意到名称已更改。它们以前是较短的名称,如 _pbase 和 _pptr,看起来与旧的 stdio 名称有关。在 1993 年 11 月,名称更改为您在上面看到的名称。预计在可预见的将来,这些名称不会再次更改。请参阅侧边栏“旧名称到新名称”,以获取名称更改方式的列表。
将直接访问 FILE 结构的成员替换为抽象宏,可以使在任何系统上编译有问题的源代码成为可能。由于 Linux stdio 区分读取和写入,因此要确定的第一件事是每个代码片段是读取还是写入。然后,将直接使用 FILE 结构的成员替换为宏;一些宏特定于读取和写入。最后,您编写宏;一组用于 Linux,一组用于“标准” stdio。以下是一些我的宏
在 Linux 或其他类似的 stdio 实现下
#ifdef _STDIO_USES_IOSTREAM /* defined in libio.h */ #define FWptr(f) ((f)->_IO_write_ptr) #define FRptr(f) ((f)->_IO_read_ptr) #define Fptr(f) (((f)->_IO_file_flags && \ _IO_CURRENTLY_PUTTING) ? \ FWptr(f) : \ FRptr(f)) #define FWcnt(f) (((f)->_IO_write_end - \ (f)->_IO-write_ptr) > 0 ? 0 : \ (f)->_IO_write_end - (f)-> \ _IO_write_ctr) #define FRcnt(f) (((f)->_IO_read_end - \ (f)->_IO_read_ptr) > 0 ? 0 : \ (f)->_IO_read_end - (f)-> \ _IO_read_ctr) #define Fcnt(f) (((f)->_IO_file_flags && \ _IO_CURRENTLY_PUTTING) ? \ FWcnt(f) : \ FRcnt(f)) #define Ffill(f) __underflow(f) #define Fflsh(f) __overflow(f)
在“标准” stdio 下
#else /* standard stdio */ #define Fptr(f) ((f)->_ptr) #define FWptr(f) Fptr(f) #define FRptr(f) Fptr(f) #define Fcnt(f) ((f)->_cnt) #define FWcnt(f) Fcnt(f) #define FRcnt(f) Fcnt(f) #define Ffill(f) _filbuf(f) #define Fflsh(f) _flsbuf(f) #endif
请注意,某些代码可能会使用 f->_cnt 作为左值(一个被赋值的变量)。在这些情况下,f->_ptr 也总是会被赋值;两者都需要在标准 stdio 库中同时更新。“计数”值在这些抽象宏中是基于 iostream 的 stdio 的计算,因此它们不能是左值。但是,由于它们依赖于“指针”值和“结束”值,并且“指针”值已更新,而“结束”值没有更改,因此它们不需要分配更新后的值。因此,
f->_ptr++; f->_cnt; ;
变为(假设代码正在使用此指针进行读取)
FRptr(f)++; #ifndef _STDIO_USES_IOSTREAM FRcnt(f); ; #endif
或简单地
Fptr(f)++; #ifndef _STDIO_USES_IOSTREAM Fcnt(f); ; #endif
如果您不确定代码是正在读取还是写入。
我要警告您:尝试在不理解您正在处理的代码的情况下将这些说明和宏应用于您正在移植的代码可能会是灾难性的。如果放入错误的宏,您的应用程序可能会编译,但会悄悄丢失数据。最重要的是,当库正在写入时,不要使用 FR*() 宏,而当库正在读取时,不要使用 FW*() 宏。如果您无法分辨正在执行哪个操作,那么使用通用版本 Fptr() 和 Fcnt() 远比猜测要好。
还有其他错误可能会发生,我无法涵盖所有错误,因为我不知道它们都是什么。Linux C 库的源代码可通过 ftp 从 tsx-11.mit.edu 和 sunsite.unc.edu 获取,并与许多 Linux 发行版一起分发。阅读 libc 源代码(通常在 /usr/src/libc-linux/libio/ 中找到)并了解其作用,是了解在移植对 stdio 进行假设的代码时该怎么做的最安全途径。仅凭这篇文章只能帮助您入门;您仍然需要了解您正在移植的程序和 Linux stdio 才能取得成功。
Michael K. Johnson 可以通过电子邮件联系:(johnsonm@merengue.oit.unc.edu)