Hi Collin,
> Here is a test program:
>
> $ cat main.c
> #include <stdio.h>
> #include <unistd.h>
> int
> main (void)
> {
> printf ("%s %s\n", "hello", "1");
> sleep (1);
> printf ("%s %s\n", "hello", "2");
> return 0;
> }
After debugging it under gdb, here's an explanation of the different
behaviours.
> So both use isatty (implemented through differing, but reasonable
> ioctls) and see that standard output isn't a terminal.
I agree; the problem is elsewhere in musl libc.
Recall that a FILE stream can be in one of three buffering modes:
- _IONBF — unbuffered
- _IOLBF — line-buffered
- _IOFBF — fully buffered
According to the POSIX paragraph that you cited, for "./a.out > /dev/full",
where /dev/full is not interactive (as determined by isatty or equivalent),
stdout should be fully buffered.
What happens in glibc: stdout is fully buffered. So the program works as
expected.
What happens in musl libc: stdout is initially line-buffered. The first
line is output immediately (that happens at the end of the first printf
call); then stdout is switched to fully buffered. The second printf
call outputs nothing directly. The final writev() call that you see comes
when stdout is getting closed, during exit().
Details for glibc
=================
> And on glibc:
>
> $ gcc main.c && strace ./a.out > /dev/full
> [...]
> fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x7), ...}) = 0
> ioctl(1, TCGETS2, 0x7ffdad7df420) = -1 ENOTTY (Inappropriate ioctl
> for device)
> brk(NULL) = 0x1c9d9000
> brk(0x1c9fb000) = 0x1c9fb000
> clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffdad7df7f0)
> = 0
> write(1, "hello 1\nhello 2\n", 16) = -1 ENOSPC (No space left on
> device)
> exit_group(0) = ?
> +++ exited with 0 +++
How is the buffering mode encoded? See gnulib/lib/fbufmode.c:
if (fp_->_flags & _IO_LINE_BUF)
return _IOLBF;
if (fp_->_flags & _IO_UNBUFFERED)
return _IONBF;
return _IOFBF;
glibc/libio/stdfiles.c:53 initializes stdout without _IO_LINE_BUF or
_IO_UNBUFFERED, that is, in fully buffered mode.
During the first output operation, glibc/libio/filedoalloc.c gets invoked.
It does an fstat() call, that succeeds and shows that the file is a
character device, then both DEV_TTY_P (&st) and __isatty_nostatus (fp->_fileno))
return false:
glibc/sysdeps/unix/sysv/linux/device-nrs.h:
/* Test whether given device is a tty. */
#define DEV_TTY_P(statp) \
({ int __dev_major = __gnu_dev_major ((statp)->st_rdev); \
__dev_major >= DEV_TTY_LOW_MAJOR && __dev_major <= DEV_TTY_HIGH_MAJOR; })
#define DEV_TTY_LOW_MAJOR 136
#define DEV_TTY_HIGH_MAJOR 143
Thus the buffering mode remains unchanged.
Details for musl libc
=====================
> Here is it running on musl (trimming some obviously irrelevant calls
> like brk and mmap):
>
> $ gcc main.c && strace ./a.out > /dev/full
> [...]
> ioctl(1, TIOCGWINSZ, 0xbf9d08e8) = -1 ENOTTY (Not a tty)
> writev(1, [{iov_base="hello 1", iov_len=7}, {iov_base="\n", iov_len=1}],
> 2) = -1 ENOSPC (No space left on device)
> nanosleep({tv_sec=1, tv_nsec=0}, 0xbf9d0ae0) = 0
> writev(1, [{iov_base="hello 2\n", iov_len=8}, {iov_base=NULL,
> iov_len=0}], 2) = -1 ENOSPC (No space left on device)
> exit_group(0) = ?
> +++ exited with 0 +++
How is the buffering mode encoded? See gnulib/lib/fbufmode.c:
if (__flbf (fp))
return _IOLBF;
return (__fbufsize (fp) > 0 ? _IOFBF : _IONBF);
And see musl/src/stdio/ext.c:
int __flbf(FILE *f)
{
return f->lbf >= 0;
}
size_t __fbufsize(FILE *f)
{
return f->buf_size;
}
So, it's a combination of the 'lbf' and 'buf_size' fields.
The relevant source files are
musl/src/stdio/stdout.c
musl/src/stdio/fwrite.c
musl/src/stdio/__stdout_write.c
musl/src/stdio/__stdio_write.c
To understand what happens, set these breakpoints in gdb:
(gdb) break __stdio_write
(gdb) watch stdout->lbf
In musl/src/stdio/stdout.c:9 stdout is initialized to be initially
line-buffered.
Then, during the first printf call, the pieces of the format string
are stored in the buffer (via the memcpy() in __fwritex). But for the
last piece, the newline character, this code in musl/src/stdio/fwrite.c
is executed:
if (f->lbf >= 0) {
/* Match /^(.*\n|)/ */
for (i=l; i && s[i-1] != '\n'; i--);
if (i) {
size_t n = f->write(f, s, i);
This invokes the __stdout_write function, which executes
if (!(f->flags & F_SVB) && __syscall(SYS_ioctl, f->fd, TIOCGWINSZ,
&wsz))
f->lbf = -1;
The F_SVB flag is unset, since setvbuf() has not been executed on this FILE
stream. The ioctl syscall returns non-zero, and thus the 'lbf' field gets set
to -1, switching the stream from line-buffered to fully buffered.
But at this point the caller (__fwritex) has already decided to output the
first line, and this happens through the subsequent __stdio_write call.
I believe this is a musl libc bug. __fwritex should trigger switching
the stream from line-buffered to fully buffered *before* deciding whether
to output a line, not after doing this decision. In other words, the
assignment
f->lbf = -1;
ought to be performed *before* the test
if (f->lbf >= 0)
Bruno