理解输出时的块缓冲和行缓冲

使用print和printf输出数据时,如果输出的内容不带换行符,则默认会等待一段时间后才输出:等待换行符的出现,或者等待文件句柄被关闭,或者其他情况。

# 两秒后print输出的内容才会显示出来
print "hello world 1";
sleep 2;

# print输出的内容带有换行符,直接输出而不等待
print "hello world 2\n";
sleep 2;

之所以会有这样的现象,是因为print/printf/say等函数输出的内容会先被缓冲在perl为文件句柄维护的缓冲空间(io buffer)内,当达到一定条件后,缓冲空间中准备输出的内容才会交给操作系统,由操作系统负责最终的写入操作。

Perl文件句柄的IO缓冲方式分为三种:

  • 按行缓冲:输出的数据先写入缓冲空间,直到写入换行符,才将缓冲空间的内容刷出交给操作系统
  • 按块缓冲:输出的数据先写入缓冲空间,直到写入缓冲空间的数据达到一定字节数量(perl默认缓冲空间大小是8K),才将缓冲空间的内容刷出交给操作系统
  • 无缓冲:输出的数据不经过缓冲空间,每次写入操作都直接交给操作系统

按行缓冲和按块缓冲时,除了满足以上列出的换行符条件和字节数量条件,在Perl种还有其他情况会立即输出:

  • 缓冲空间已满(即达到8K)时,会立即输出
  • 关闭文件句柄时(包括退出perl程序时自动关闭文件句柄的情况),会立即输出
  • 手动执行flush操作时,会立即刷出给操作系统

默认情况下:

  • 输出到标准输出的终端数据是按行缓冲的,即写入STDOUT文件句柄的数据在遇到换行符之前不会输出
  • 输出到标准错误的数据是无缓冲的,这样可以立即输出错误信息,即写入STDERR文件句柄的数据会立即输出
  • 输出到管道、文件、套接字的数据是块缓冲的,直到缓冲一定数量的数据之后才会输出。但输出到管道和套接字的数据往往不应该按块缓冲

回到上面的示例,print输出不带换行符的数据时会缓冲等待一段时间,输出带换行符的数据时会立即输出。原因就在于这两个print操作都是向STDOUT写入输出,而写入STDOUT的数据默认是按行缓冲的,这意味着遇到换行符之前、缓冲空间满之前、关闭STDOUT之前(包括退出程序时的自动关闭)、手动flush之前,数据都会先缓冲在buffer中。

再例如,下面向某个文件中写入少量数据,由于写入文件的操作默认是按块缓冲,因此数据会一直缓冲在该文件句柄的io buffer中,直到程序退出(程序退出时会自动关闭所有打开的文件句柄):

# 写入文件的操作,10秒后退出程序前才会写入到文件中
open my $fh, ">>", "/tmp/a.log";
say $fh "hello world";
sleep 10;

在Perl中,可通过修改特殊变量$|来设置文件句柄的缓冲模式:

  • 该变量设置为非0数值,表示无缓冲
  • 设置为数值0(默认值),表示按块缓冲

设置$|时要求先使用select选择该文件句柄作为默认输出文件句柄。因此,通常的操作步骤如下:

my $oldfh = select($fh); $| = 1; select $oldfh;

或者,使用IO::Handle提供的面向对象语法来设置缓冲模式:

# Perl v5.14版及之后的版本可不用use导入IO::Handle
use IO::Handle;

$fh->autoflush(1);

例如,下面的两个print操作将会立即输出,而不会等待10秒后才输出:

$| = 1;  # 或:STDOUT->autoflush(1);
print "hello world";
print "WORLD";
sleep 10;