当前位置: 首页 > 文档资料 > FreeBSD 开发手册 >

11.8 缓存 I/O

优质
小牛编辑
132浏览
2023-12-01

通过使用输入输出缓存, 我们可以提高代码的效率。 我们可以建立一个输入缓存, 并一次读入一系列的字节。 然后, 我们再一个接一个地从缓存中提取它们。

同样, 我们可以建立一个输出缓存。 把我们的输出存在里面,直到添满。 同时, 我们将让内核将缓存的内容写到标准输出 stdout 上。

程序将在没有输入的时候结束。 但是我们仍然需要让内核再向标准输出 stdout 进行最后一次写操作, 否则一些内容将留在缓存中, 永不输出。 别忘记这个操作, 否则你将会困惑为什么你的程序丢失了一些应有的输出。

%include   'system.inc'

%define BUFSIZE 2048

section .data
hex db  '0123456789ABCDEF'

section .bss
ibuffer resb    BUFSIZE
obuffer resb    BUFSIZE

section .text
global  _start
_start:
    sub eax, eax
    sub ebx, ebx
    sub ecx, ecx
    mov edi, obuffer

.loop:
    ; read a byte from stdin
    call    getchar

    ; convert it to hex
    mov dl, al
    shr al, 4
    mov al, [hex+eax]
    call    putchar

    mov al, dl
    and al, 0Fh
    mov al, [hex+eax]
    call    putchar

    mov al, ' '
    cmp dl, 0Ah
    jne .put
    mov al, dl

.put:
    call    putchar
    jmp short .loop

align 4
getchar:
    or  ebx, ebx
    jne .fetch

    call    read

.fetch:
    lodsb
    dec ebx
    ret

read:
    push    dword BUFSIZE
    mov esi, ibuffer
    push    esi
    push    dword stdin
    sys.read
    add esp, byte 12
    mov ebx, eax
    or  eax, eax
    je  .done
    sub eax, eax
    ret

align 4
.done:
    call    write       ; flush output buffer
    push    dword 0
    sys.exit

align 4
putchar:
    stosb
    inc ecx
    cmp ecx, BUFSIZE
    je  write
    ret

align 4
write:
    sub edi, ecx    ; start of buffer
    push    ecx
    push    edi
    push    dword stdout
    sys.write
    add esp, byte 12
    sub eax, eax
    sub ecx, ecx    ; buffer is empty now
    ret

现在我们的程序有了第三个部分,名字叫 .bss。 这个部分不会包含在我们可执行文件里, 因此不会被初始化。 我们需要用 resb 代替 db。 它仅仅为我们保留了指定大小的未初始化内存。

我们将充分利用系统不会修改寄存器这个优点:我们为那些可能存储在 .data 数据段里的全局变量使用寄存器。 和 Microsoft 将参数直接传递进寄存器的方式相比, UNIX® 规范中将参数存放在堆栈里的方式更加高级。 因为我们将更自由得使用寄存器。

我们将 EDIESI 当作指针使用, 以次来对下一个字节进行读写操作。同时,我们使用 EBXECX 来保存两个缓存里字节的数量。 因此,我们知道什么时候该对系统停止读写操作。

我们来看看它是怎样工作的?

% nasm -f elf hex.asm
% ld -s -o hex hex.o
% ./hex
Hello, World!
Here I come!
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
^D %

在我们输入 ^D 前, 程序没有输出,这个和我们的预计不符。 不过修复很简单, 插入三行代码, 让程序在我们换行的时候, 每次都有输出就可以了。 我用 > 标记了这三行 ( 请不要将 > 加入你的代码中)。

%include   'system.inc'

%define BUFSIZE 2048

section .data
hex db  '0123456789ABCDEF'

section .bss
ibuffer resb    BUFSIZE
obuffer resb    BUFSIZE

section .text
global  _start
_start:
    sub eax, eax
    sub ebx, ebx
    sub ecx, ecx
    mov edi, obuffer

.loop:
    ; read a byte from stdin
    call    getchar

    ; convert it to hex
    mov dl, al
    shr al, 4
    mov al, [hex+eax]
    call    putchar

    mov al, dl
    and al, 0Fh
    mov al, [hex+eax]
    call    putchar

    mov al, ' '
    cmp dl, 0Ah
    jne .put
    mov al, dl

.put:
    call    putchar
>   cmp al, 0Ah
>   jne .loop
>   call    write
    jmp short .loop

align 4
getchar:
    or  ebx, ebx
    jne .fetch

    call    read

.fetch:
    lodsb
    dec ebx
    ret

read:
    push    dword BUFSIZE
    mov esi, ibuffer
    push    esi
    push    dword stdin
    sys.read
    add esp, byte 12
    mov ebx, eax
    or  eax, eax
    je  .done
    sub eax, eax
    ret

align 4
.done:
    call    write       ; flush output buffer
    push    dword 0
    sys.exit

align 4
putchar:
    stosb
    inc ecx
    cmp ecx, BUFSIZE
    je  write
    ret

align 4
write:
    sub edi, ecx    ; start of buffer
    push    ecx
    push    edi
    push    dword stdout
    sys.write
    add esp, byte 12
    sub eax, eax
    sub ecx, ecx    ; buffer is empty now
    ret

现在看看它运行的结果:

% nasm -f elf hex.asm
% ld -s -o hex hex.o
% ./hex
Hello, World!
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
Here I come!
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
^D %

看来程序运行得不错!

注意: 这样对缓存 I/O 的操作存在着潜在危险。 我会在说明完 缓存的缺陷 后,讨论并修改它。

11.8.1 如何将字符放回输入流

警告: 这可能是一个较为进阶的话题, 熟悉编译理论的程序员可能会对此有兴趣。 如果愿意的话, 您可以 直接跳转到下一节, 以后再阅读这一部分。

尽管我们的示范程序并不需要它, 但更复杂一些的过滤器可能会用到。 换言之, 这些过滤器可能需要知道下一个字符 (甚至接下来的几个字符) 是什么。 如果下一个字符是某个特定的值, 则它是当前短语 (token) 的一部分, 反之则可能不是。

例如, 您可能正对输入流进行语义串的词法分析 (例如, 实现某种语言的编译器): 如果一个字符跟着另一个字符或数字, 则后面这个字符是正在处理的短语的一部分。 如果这个字符后面是空格, 或某个其它的值, 则它就不是当前短语的一部分了。

这带来了一个很有意思的问题: 如何将接下来的这个字符送回输入流, 使之能够在稍后被重新读取?

一种可行的方法是将其保存到一个字符变量中, 并设置一个标志。 我们可以修改 getchar 使之检查这一标志, 如果这个标志被置位, 则从变量而非输入缓冲区中获取字符, 并复位标志。 不过, 很显然, 这会拖慢我们的脚步。

C 语言中有一个 ungetc() 函数, 它正是用于实现这个目的的。 那么, 有没有办法在我们的代码中迅速实现这一函数呢? 在阅读下一节之前, 希望您能回到前面, 并查看 getchar 过程, 并思考您是否能找到一个漂亮且快捷的解决方案。 接着回到这里来看看我的解法。

将字符放回流的关键是, 如何获取开始的字符:

首先, 我们会通过检查 EBX 的值来检测输入缓冲区是否为空。 如果它是零, 则调用 read 过程。

如果有可用的字符, 则使用 lodsb, 接着对 EBX 的值做递减。 lodsb 指令实际上相当于:

   mov al, [esi]
    inc esi

我们拿到的这个字节会保留在缓冲区中, 直到下次调用 read 为止。 尽管我们不知道这会在何时发生, 但我们知道它不会在下次 getchar 之前。 因此, 要将读取的最后一个字节 “还给” 流, 我们需要做的就是将 ESI 的值递减, 并使 EBX 的值递增:

ungetc:
    dec esi
    inc ebx
    ret

但是, 一定要小心! 如果我们每次只向前读取一个字符的话, 这样做没有任何问题。 但是如果我们会读取多个字符, 并多次调用 ungetc 的话, 有时就会出问题了 (而且会很难调试)。 为什么呢?

这是因为 getchar 并不必然调用 read, 所有预先读入的字符仍会出于缓冲区, 而我们的 ungetc 也会毫无问题地运行。 但是如果 getchar 调用了 read, 则缓冲区的内容就会随之变化。

我们可以认为 ungetc 一定能在使用 getchar 读入的最后一个字符上正确工作, 但在这之前的则没有任何保证。

如果您的程序需要一次读取多个字节, 则有两个选择:

如果可能的话, 修改程序使其一次只向前读最多一个字符。 这是最简单的办法。

如果没办法这样做, 首先要确定程序最多会向流交还多少各字符。 保险起见, 适当地增大这个数字, 最好是 16 的整数倍 ── 使其能够对齐。 接着, 修改您的代码中的 .bss 节, 并在输入缓冲区之前建立一个小的 “预留” 缓冲, 类似下面这样:

section    .bss
    resb    16  ; or whatever the value you came up with
ibuffer resb    BUFSIZE
obuffer resb    BUFSIZE

您还需要修改 ungetc 以便将还回的字符放入 AL

ungetc:
    dec esi
    inc ebx
    mov [esi], al
    ret

经过这样的修改, 您就可以安全地调用最多 17 次 ungetc 了 (第一次调用仍会使用输入缓冲, 而余下的 16 次则有可能在输入缓冲, 也可能在 “预留缓冲” 中)。