xv6如何写入终端?

How does xv6 write to the terminal?

printf 函数调用 write(关于 forktest.c):

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

1 作为 fd 写入控制台(因为 1 映射到标准输出)。但是 write 是在哪里定义的呢?我只在 user.h.

中看到它的声明
int write ( int, void*, int );

我假设它以某种方式被重定向到 file.c 中的 filewrite

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" );
}

并且 filewrite 调用 writei ,它在 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;
}

这一切如何导致终端显示字符?终端怎么知道读取fd 1进行显示,到哪里去找fd 1呢? fd 1 的格式是什么?是标准吗?

fd==1 指的是 stdout 或标准输出。这是类 Unix 操作系统的一个共同特征。内核知道它不是真正的文件。写入 stdout 映射到终端输出。

以下是从 printf 到终端的完整路径。要点是最终,xv6 将字符写入 CPU 的串口。

QEMU 使用标志 -nographic-serial mon:stdio 进行初始化,这些标志告诉它使用终端向 CPU 的串行端口发送数据或从中接收数据。

步骤 1) printfforktest.c

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

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

步骤 2) writeusys.S

.globl write
write:

    movl $SYS_write, %eax
    int  $T_SYSCALL
    ret

步骤 3) sys_writesysfile.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 ]
    ...
}

之前在系统初始化期间,init.c 中的 main 被调用,其中 stdin (0),stdout (1) 和 stderr (2) 创建文件描述符。这是 argfd 在查找 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
    ...
}

stdin|out|err 是 T_DEV 类型的索引节点,因为它们是使用 sysfile.c

中的 mknod 创建的
int sys_mknod ( void )
{
    ...
    ip = create( path, T_DEV, major, minor )
    ...
}

用于创建它们的 1 的主设备号映射到控制台。参见 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

步骤 4) filewritefile.c

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

步骤 5) writeifs.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 );
    }
    ...
}

调用devsw[ ip->major ].write( ip, src, n )
变成 devsw[ CONSOLE ].write( ip, src, n ).

之前在系统初始化期间,consoleinit 将此映射到函数 consolewrite(参见 console.c

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

步骤 6) consolewriteconsole.c

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

步骤 7) consoleputcconsole.c

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

步骤 8) uartputcuart.c.
out 汇编指令用于写入 CPU 的 serial port.

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

void uartputc ( int c )
{
    ...

    outb( COM1 + 0, c );
}

步骤 9) QEMU 通过 -nographic-serial mon:stdio 标志配置为使用串口在 Makefile 中进行通信。 QEMU使用终端向串口发送数据,并从串口显示数据。

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

qemu-nox: fs.img xv6.img
    $(QEMU) -nographic $(QEMUOPTS)