2

我最近写了一个小诅咒游戏,因为它所需要的只是一些计时器机制和一个诅咒实现,尝试为 DOS 构建它的想法很自然。诅咒是由pdcursesDOS 提供的。

POSIX和Win32的时序已经不同了,所以我定义了这个接口:

#ifndef CSNAKE_TICKER_H
#define CSNAKE_TICKER_H

void ticker_init(void);
void ticker_done(void);

void ticker_start(int msec);
void ticker_stop(void);
void ticker_wait(void);

#endif

游戏调用ticker_init()andticker_done()一次,ticker_start()在它需要滴答声时以毫秒为间隔,并ticker_wait()在其主循环中等待下一个滴答声。

在 DOS 上使用与 POSIX 平台相同的实现, usingsetitimer()不起作用。一个原因是 djgpp 附带的 C lib 没有实现waitsig(). 所以我为 DOS 创建了一个新的接口实现:

#undef __STRICT_ANSI__
#include <time.h>

uclock_t tick;
uclock_t nextTick;
uclock_t tickTime;

void
ticker_init(void)
{
}

void
ticker_done(void)
{
}

void
ticker_start(int msec)
{
    tickTime = msec * UCLOCKS_PER_SEC / 1000;
    tick = uclock();
    nextTick = tick + tickTime;
}

void
ticker_stop()
{
}

void
ticker_wait(void)
{
    while ((tick = uclock()) < nextTick);
    nextTick = tick + tickTime;
}

这就像一个魅力dosbox(我现在没有真正的 DOS 系统)。但我担心的是:忙着等待真的是我在这个平台上能做的最好的事情吗?我想要一个解决方案,让 CPU 至少可以节省一些能源。

作为参考,这里是整个来源

4

1 回答 1

3

好的,我想我终于可以回答我自己的问题了(感谢Wyzard的有用评论!)

由于似乎没有任何库调用这样做,显而易见的解决方案是放置一个hlt内联汇编。不幸的是,这使我的程序崩溃了。寻找原因,是因为使用的默认dpmi服务器运行的程序在ring 3...hlt被保留到ring 0. 因此,要使用它,您必须修改加载程序存根以加载dpmi运行您的程序的服务器ring 0。稍后见。

浏览文档时,我遇到了__dpmi_yield()。如果我们在多任务环境(Win 3.x 或 9x ...)中运行,dpmi那么操作系统已经提供了一个服务器,当然,在这种情况下,我们希望在等待时放弃我们的时间片尝试特权hlt

所以,把它们放在一起,DOS 的源代码现在看起来像这样:

#undef __STRICT_ANSI__
#include <time.h>
#include <dpmi.h>
#include <errno.h>

static uclock_t nextTick;
static uclock_t tickTime;
static int haveYield;

void
ticker_init(void)
{
    errno = 0;
    __dpmi_yield();
    haveYield = errno ? 0 : 1;
}

void
ticker_done(void)
{
}

void
ticker_start(int msec)
{
    tickTime = msec * UCLOCKS_PER_SEC / 1000;
    nextTick = uclock() + tickTime;
}

void
ticker_stop()
{
}

void
ticker_wait(void)
{
    if (haveYield)
    {
        while (uclock() < nextTick) __dpmi_yield();
    }
    else
    {
        while (uclock() < nextTick) __asm__ volatile ("hlt");
    }
    nextTick += tickTime;
}

为了让它在普通DOS 上工作,编译的可执行文件中的加载程序存根必须像这样修改:

<path to>/stubedit bin/csnake.exe dpmi=CWSDPR0.EXE

CWSDPR0.EXEdpmi运行所有代码的服务器ring 0

还有待测试的是,在 win 3.x / 9x 下运行时,yield 是否会影响时间。也许时间片太长了,必须检查一下。更新:使用上面的代码,它在 Windows 95 中运行良好。

hlt指令的使用以一种奇怪的方式破坏了兼容性..当试图通过 PDcursesdosbox 0.74进行阻塞时,程序似乎永远挂起。getch()然而,这不会发生在真正的 MS-DOS 6.22 in virtualbox. 更新:这是dosbox 0.74在当前SVN树中修复的错误。

鉴于这些发现,我认为这是在 DOS 程序中“很好地”等待的最佳方式。

更新:通过检查所有可用的方法并选择最好的方法,可以做得更好。我发现了一个应该考虑的DOS 空闲调用。策略:

  1. 如果支持yield,请使用它(我们在多任务环境中运行)

  2. 如果支持空闲,请使用它。可选地,如果我们在 ring-0 中,hlt每次调用 idle 之前都执行一次,因为 idle 被记录为在没有其他程序准备好运行时立即返回。

  3. 否则,在 ring-0 中只需使用简单的hlt指令。

  4. 忙于等待作为最后的手段。

这是一个测试所有可能性的小示例程序(DJGPP):

#include <stdio.h>
#include <dpmi.h>
#include <errno.h>

static unsigned int ring;

static int
haveDosidle(void)
{
    __dpmi_regs regs;
    regs.x.ax = 0x1680;
    __dpmi_int(0x28, &regs);
    return regs.h.al ? 0 : 1;
}

int main()
{
    puts("checking idle methods:");

    fputs("yield (int 0x2f 0x1680): ", stdout);
    errno = 0;
    __dpmi_yield();

    if (errno)
    {
        puts("not supported.");
    }
    else
    {
        puts("supported.");
    }

    fputs("idle (int 0x28 0x1680): ", stdout);

    if (!haveDosidle())
    {
        puts("not supported.");
    }
    else
    {
        puts("supported.");
    }

    fputs("ring-0 HLT instruction: ", stdout);
    __asm__ ("mov %%cs, %0\n\t"
             "and $3, %0" : "=r" (ring));

    if (ring)
    {
        printf("not supported. (running in ring-%u)\n", ring);
    }
    else
    {
        puts("supported. (running in ring-0)");
    }
}

我的 github 存储库中的代码反映了这些更改。

于 2015-07-25T19:17:18.863 回答