3

The printf function calls write (re. forktest.c):

void printf ( int fd, char *s, ... )
{
    write( fd, s, strlen(s) );
}

Passing 1 as the fd writes to the console (as 1 maps to stdout). But where is write defined? I only see its declaration in user.h.

int write ( int, void*, int );

I'm assuming it somehow gets redirected to filewrite in file.c.

int filewrite (struct file *f, char *addr, int n )
{
    int r;

      if ( f->writable == 0 )
          return -1;

      if ( f->type == FD_PIPE )
          return pipewrite( f->pipe, addr, n );

    if ( f->type == FD_INODE )
    {
        // write a few blocks at a time to avoid exceeding
        // the maximum log transaction size, including
        // i-node, indirect block, allocation blocks,
        // and 2 blocks of slop for non-aligned writes.
        // this really belongs lower down, since writei()
        // might be writing a device like the console.
        int max = ( ( MAXOPBLOCKS - 1 - 1 - 2 ) / 2 ) * 512;
        int i = 0;
        while ( i < n )
        {
            int n1 = n - i;
            if ( n1 > max )
                n1 = max;

            begin_op();
            ilock( f->ip );
            if ( ( r = writei( f->ip, addr + i, f->off, n1 ) ) > 0 )
                f->off += r;
            iunlock( f->ip );
            end_op();

            if ( r < 0 )
                break;
            if ( r != n1 )
                panic( "short filewrite" );
            i += r;
        }
        return i == n ? n : -1;
    }
    panic( "filewrite" );
}

And filewrite calls writei which is defined in fs.c.

int writei ( struct inode *ip, char *src, uint off, uint n )
{
    uint tot, m;
    struct buf *bp;

    if ( ip->type == T_DEV )
    {
        if ( ip->major < 0 || ip->major >= NDEV || !devsw[ ip->major ].write )
            return -1;
        return devsw[ ip->major ].write( ip, src, n );
    }

    if ( off > ip->size || off + n < off )
        return -1;
    if ( off + n > MAXFILE*BSIZE )
        return -1;

    for ( tot = 0; tot < n; tot += m, off += m, src += m )
    {
        bp = bread( ip->dev, bmap( ip, off/BSIZE ) );
        m = min( n - tot, BSIZE - off%BSIZE );
        memmove( bp->data + off%BSIZE, src, m );
        log_write( bp );
        brelse( bp );
    }

    if ( n > 0 && off > ip->size )
    {
        ip->size = off;
        iupdate( ip );
    }
    return n;
}

How does all this result in the terminal displaying the characters? How does the terminal know to read fd 1 for display, and where to find fd 1? What is the format of fd 1? Is it a standard?

Jet Blue
  • 5,109
  • 7
  • 36
  • 48

2 Answers2

4

Below is the full path from printf to the terminal. The gist is that eventually, xv6 writes the character to the CPU's serial port.

QEMU is initialized with the flags -nographic or -serial mon:stdio which tell it to use the terminal to send data to, or receive data from the CPU's serial port.

Step 1) printf in forktest.c

void printf ( int fd, const char *s, ... )
{
    write( fd, s, strlen( s ) );
}

void forktest ( void )
{
    ...
    printf( 1, "fork test\n" );
    ...
}

Step 2) write in usys.S

.globl write
write:

    movl $SYS_write, %eax
    int  $T_SYSCALL
    ret

Step 3) sys_write in sysfile.c

int sys_write ( void )
{
    ...
    argfd( 0, 0, &f )
    ...
    return filewrite( f, p, n );
}

static int argfd ( int n, int *pfd, struct file **pf )
{
    ...
    f = myproc()->ofile[ fd ]
    ...
}

Previously during system initialization, main in init.c was called where the stdin (0), stdout (1), and stderr (2) file descriptors are created. This is what argfd finds when looking up the file descriptor argument to sys_write.

int main ( void )
{
    ...
    if ( open( "console", O_RDWR ) < 0 )
    {
        mknod( "console", 1, 1 );  // stdin

        open( "console", O_RDWR );
    }

    dup( 0 );  // stdout
    dup( 0 );  // stderr
    ...
}

The stdin|out|err are inodes of type T_DEV because they are created using mknod in sysfile.c

int sys_mknod ( void )
{
    ...
    ip = create( path, T_DEV, major, minor )
    ...
}

The major device number of 1 that is used to create them is mapped to the console. See file.h

// Table mapping major device number to device functions
struct devsw
{
    int ( *read  )( struct inode*, char*, int );
    int ( *write )( struct inode*, char*, int );
};

extern struct devsw devsw [];

#define CONSOLE 1

Step 4) filewrite in file.c

int filewrite ( struct file *f, char *addr, int n )
{
    ...
    if ( f->type == FD_INODE )
    {
        ...
        writei( f->ip, addr + i, f->off, n1 )
        ...
    }
    ...
}

Step 5) writei in fs.c

int writei ( struct inode *ip, char *src, uint off, uint n )
{
    ...
    if ( ip->type == T_DEV )
    {
        ...
        return devsw[ ip->major ].write( ip, src, n );
    }
    ...
}

The call to devsw[ ip->major ].write( ip, src, n )
becomes devsw[ CONSOLE ].write( ip, src, n ).

Previously during system initialization, consoleinit mapped this to the function consolewrite (see console.c)

void consoleinit ( void )
{
    ...
    devsw[ CONSOLE ].write = consolewrite;
    devsw[ CONSOLE ].read  = consoleread;
    ...
}

Step 6) consolewrite in console.c

int consolewrite ( struct inode *ip, char *buf, int n )
{
    ...
    for ( i = 0; i < n; i += 1 )
    {
        consputc( buf[ i ] & 0xff );
    }
    ...
}

Step 7) consoleputc in console.c

void consputc ( int c )
{
    ...
    uartputc( c );
    ...
}

Step 8) uartputc in uart.c.
The out assembly instruction is used to write to the CPU's serial port.

#define COM1 0x3f8  // serial port
...

void uartputc ( int c )
{
    ...

    outb( COM1 + 0, c );
}

Step 9) QEMU is configured to use the serial port for communication in the Makefile through the -nographic or -serial mon:stdio flags. QEMU uses the terminal to send data to the serial port, and to display data from the serial port.

qemu: fs.img xv6.img
    $(QEMU) -serial mon:stdio $(QEMUOPTS)

qemu-nox: fs.img xv6.img
    $(QEMU) -nographic $(QEMUOPTS)
Jet Blue
  • 5,109
  • 7
  • 36
  • 48
  • How would it display colour to the console then. – Insane Miner Mar 26 '21 at 03:18
  • The coloring is done by the "terminal" that receives the bytes via UART. What color to use and where is specified by [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#In_C). It is up to the receiving terminal to interpret these codes and render the appropriate colors. If you have a simple terminal (like the one in xv6) these codes will mean nothing to it and it will just print them as text. If you want, you can take it upon yourself to extend the xv6 console (console.c, line147) to recognize these escape sequences and render color accordingly. – Jet Blue Mar 28 '21 at 04:35
1

fd==1 refers to stdout, or Standard Out. It's a common feature of Unix-like Operatin Systems. The kernel knows that it's not a real file. Writes to stdout are mapped to terminal output.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • How? I understand that at a high level stdout is displayed by the terminal but how? – Jet Blue Apr 22 '18 at 23:46
  • What does 2 refer to then. – Insane Miner Mar 26 '21 at 03:18
  • @InsaneMiner On startup, there are no existing file descriptors. init.c creates the [first file descriptor (0)](https://github.com/mit-pdos/xv6-public/blob/master/init.c#L17) to be used as stdin. It then creates [another file descriptor (1)](https://github.com/mit-pdos/xv6-public/blob/master/init.c#L19) to be used as stdout. And then another (2) to be used as stderr. It then creates a child process (using fork) to run the shell. This child process inherits the open file descriptors (0,1,2). If the child process creates a new file, it will get assigned a file descriptor of 4, and so on. – Jet Blue Mar 28 '21 at 05:59