中断(Interrupt)nCurses的getch在接收到信号时。

15
我的一个程序使用 ncurses 对小型 tui 进行绘制。我的目标之一是使它能够在其他 curses 实现中更具可移植性。这意味着我希望自己捕获终端模拟器发出的 SIGWINCH 信号,以便在调整大小操作时更新我的 tui 以适应已更改的几何形状(而不依赖于 ncurses 的调整大小设施)。由于 POSIX(据我所知)仅允许在信号处理程序中访问 sig_atomic_t 变量,因此我将一个变量设置为不同的状态。在主循环中,我的程序检查状态是否已更改,并在必要时更新 tui。

但是,现在我遇到了一个问题,当信号到达时,我的程序在 getch 中挂起。ncurses 文档说明处理过的信号永远不会中断 getch。这意味着 tui 的大小直到按下输入键才会更新。

有没有可移植的方法来打断 getch?我目前的方法是在信号处理程序中使用 ungetch 来插入一个虚拟按键,但我不确定这是否被允许。实际上,我找不到任何关于 curses 函数是否可以在信号处理程序中使用的文档。有没有想法如何正确处理这个问题?

问候


可能是ncurses - resizing glitch的重复问题。 - Emilien
@Emilien:问题的被接受答案建议调用endwinrefresh,这似乎是一种有效的方法,但我在文档中找不到任何相关内容,而且我不喜欢使用未定义的行为(特别是在C语言中)。 @fearless_fool:就我理解代码而言,emacs并没有使用(n)curses来检索字符或处理调整大小事件,而是自己完成了大部分工作。但也许我可以找到另一个应用程序……但我仍然可能不知道该应用程序使用的解决方案是已定义的行为还是仅仅是凑巧可行的。 - user1678062
您还可以在信号处理程序中调用许多POSIX“安全函数”。请参阅man 7 signal - Jite
我的ncurses副本(5.9)包括resizeterm(3X)手册,其中包含以下文本:“如果ncurses被配置为提供自己的SIGWINCH处理程序,则resizeterm函数ungetch一个KEY_RESIZE,该键将在下一次调用getch时读取。这用于向应用程序发出警报,屏幕大小已更改,并且应重新绘制无法自动完成的特殊功能,例如垫片。”也许您可以检查您的ncurses安装的详细信息,或者(如果容易)尝试在处理程序中使用KEY_RESIZE的ungetch而不是虚拟键。 - sjnarv
@sjnarv:感谢您的回复。但这仍然依赖于库。而这个问题的整个意图是找到一种可移植和指定的方法来实现这种行为。即使curs_getch(3X)也没有说明是否可以在信号处理程序中调用ungetch - user1678062
显示剩余2条评论
4个回答

6

唯一的指导原则是尽可能少地在中断例程中执行操作。如果您正在执行比设置标志更多的操作,那么您应该考虑重新思考解决方案。

curses系统有一种处理这个问题的方法,但需要开发人员做一些工作。

您可以使用适当的延迟设置半延迟模式,以便getch()在此期间没有按键可用时返回ERR。这有效地使您退出了getch()调用,因此您可以进行所需的任何其他curses操纵。

因此,这是我的建议。首先,将SIGWINCH处理程序更改为仅设置一个名为resized的标志,您的“主”程序可以检测到它。

其次,为您的应用程序提供一种特殊形式的getch(),类似于(伪代码,显然):

def getch_10th():
    set half delay mode for (for example) 1/10th second
    do:
        if resized:
            do whatever it takes to resize window
        set ch to result of real getch() (with timeout, of course)
    while timed out
    return ch

半延迟模式是在效率方面的一种妥协,它介于永久等待(不处理调整大小事件)和立即返回(消耗CPU资源)之间。
明智地使用它可以使您的窗口响应相当快,而无需担心可移植性。
以下是一个C程序的示例,用于将其付诸实践。首先是信号和拦截函数:
#include <curses.h>
#include <signal.h>

// Flag and signal handler.

static volatile int resized = 1;

static void handle_resize (int sig) {
    resized = 1;
}

// Get a character, handling resize events.

int getch10th (void) {
    int ch;
    do {
        if (resized) {
            resized = 0;
            endwin();
            refresh();
            mvprintw (1, 0, "Size = %dx%d.     \n", COLS, LINES);
            refresh();
        }
        halfdelay (1);
        ch = getch();
    } while (ch == ERR || ch == KEY_RESIZE);
    return ch;
}

然后编写一个简单的main函数来测试它:
// Simplified main capturing keystrokes.

int main (void) {
    WINDOW * w = initscr();
    noecho();
    signal (SIGWINCH, handle_resize);
    for (;;) {
        int ch = getch10th();
        mvprintw(0, 0, "Got character 0x%02x.     \n\n", ch);
    }
    endwin();
    return 0;
}

机敏的读者会注意到,在getch10th()函数中还存在KEY_RESIZE。这是因为一些实现实际上会排队一个特殊的键来处理这种情况(在引发SIGWINCH后强制getch()返回)。
如果您使用上述代码以允许那些不执行此操作的系统,则必须记住为那些执行此操作的系统处理该虚假键,因此我们也要捕获它。

resized变量不应该按照原始帖子中所建议的是类型为sig_atomic_t吗?而且它也不应该声明为volatile吗? - Roland Illig
@Roland,“sig_atomic_t”并不一定需要这样。因为它只有零和非零两种状态,原子性并不是一个问题。至于“volatile”,我不是完全确定。该变量无法在编译单元之外进行修改,因此编译器可能会假设,由于从未显式调用“handle_resize”,只有一段代码可以更改该变量。因此,即使编译器足够智能以意识到它可能会被信号处理程序更改,最安全的选择也是将其标记为易失性。好的发现,我会更改的。 - paxdiablo
何不直接在 select() 上等待,只有当输入就绪时才调用 getch()select() 显然会可靠地在接收到信号时中断。 - Crowman
@Paul,这可能应该是一个答案而不是一个评论。 - paxdiablo
我仍然建议你养成使用 volatilesig_atomic_t 的习惯,即使没有其他原因。@Crowman 我能想到的一个原因是与 select(2) 的可移植性问题有关,而将其与 curses 混合使用似乎更加棘手。现在虽然那些熟练掌握 select() 的人会知道要检查哪些事情,但还是有很多微妙的问题和需要考虑的事情——我猜你已经意识到了。 - Pryftan

1
同意@Emilien的观点,这很可能是ncurses - resizing glitch的重复。

然而,OP从未展示工作代码来演示这种情况。

除了OpenBSD,在那里该功能被禁用(直到2011年),getch应该在SIGWINCH上返回KEY_RESIZE。禁用它的原因是对sig_atomic_t的担忧,这在2007中得到了解决(例如,请参见OpenBSD的ncurses没有USE_SIGWINCH)。

除此之外,无法获取 KEY_RESIZE通常原因是建立了一个 SIGWINCH 处理程序(这会阻止 ncurses 中的处理程序运行)。这就是ncurses - resizing glitch中的实际问题(没有一个建议的答案解决了这个问题)。
endwin/refresh 解决方案是众所周知的(请参见自 1997 年以来的 ncurses FAQ 中的 Handling SIGWINCH (resize events))。在其可移植性部分,resizeterm 手册页面总结了这一点。如果您阅读了这篇文章,您就会意识到。
  • 如果定义了KEY_RESIZE,则实现支持与ncurses更/少兼容的SIGWINCH处理程序,
  • 如果没有定义,则只能使用endwin/refresh解决方案。

顺便说一句,从ncurses的curs_getch(3x)手册页中引用的评论并不是指阻止SIGWINCH,而是库中的一种解决方法,以防止它在信号中断读取时向应用程序返回错误。如果您阅读该手册页面的整个可移植性部分,这一点应该是显而易见的。

当然,Emacs仅使用ncurses的termcap接口,而不是curses应用程序。因此,关于它如何处理SIGWINCH的评论是无关紧要的。


1
哦,来吧,Thomas。如果你认为这是ncurses中一个问题的复制品,那么它很可能确实是。我的意思是你就是这个项目的幕后推手(我非常尊重你以及你所推动的其他事情 - 包括你在这里花费的时间)!有趣的是,你提到了emacs;我讨厌emacs,但我使用vim(以前使用vi),不久之前我还认为它也是一个curses程序,但似乎它只是使用termcap(还是terminfo?无论如何,它链接到libtinfo)。无论如何,感谢你详细的回答。 - Pryftan

1

根据FreeBSD文档getch,中断getch取决于您使用的系统:

关心可移植性的程序员应为以下两种情况做好准备:(a) 信号接收不会中断getch; (b) 信号接收会中断getch并导致其返回errno设置为EINTR的ERR。在ncurses实现下,被处理的信号永远不会中断getch。

我认为您应该考虑使用线程,一个线程负责使用getch()并将数据传递给主线程进行处理。当发送SIGWINCH信号时,您将能够终止使用getch()的线程,并在需要时重新启动它。但是,终止使用getch()的线程可能是无用的,因为主线程不会被getch()阻塞。

您可以在此页面中了解如何获得非阻塞输入,而无需使用ncurses。但是,您可以使用read(STDIN_FILENO, &c, sizeof(char))来读取输入,而不是在示例中使用fgetc(),因为如果失败,read将返回一个值<= 0。

1
如果可以避免,将线程与信号混合使用似乎不是正确的方法。此外,read(2)在出现错误时返回-1(并适当设置errno)。实际上,0并不是一个错误。您说“失败”,但我不会认为读取零字节是一种失败。虽然这可能只是语义问题。 - Pryftan

0

虽然已经有一段时间了,但我相信你可以从处理程序中使用longjmp。不确定它在ncurses中的工作方式。

http://www.csl.mtu.edu/cs4411.ck/www/NOTES/non-local-goto/sig-1.html

编辑:我有一个新的答案******************************

我认为非阻塞读取是你的救星,以下代码对我来说完美地工作。此外,在我的实现中,当发生调整大小事件时,我会得到整数值“410”,但此版本未使用该调整大小标志。试试这个?

我将超时设置为10毫秒,当没有任何操作时,它将每秒返回ERR而不是字符100次,并立即捕获调整大小。每秒100次什么都不是...在我的低端机器上,8分钟的时间甚至不会在cput“time”命令(0.000用户和sys使用)中注册。

#include <curses.h>
#include <signal.h>

int resized = 0;

void handle_resize(int sig)
{
  resized = 1;
}

int main(int argc, char * argv[])
{
   WINDOW * w;
   w = initscr();
   timeout(10); // getch returns ERR if no data within X ms
   cbreak();
   noecho();

   signal(SIGWINCH, handle_resize);

   int c, i;

   while (1) {
      c = getch();

      if(resized) {
         resized = 0;
         endwin();
         refresh();
         clear();

         mvprintw(0, 0, "COLS = %d, LINES = %d", COLS, LINES);
         for (i = 0; i < COLS; i++)
            mvaddch(1, i, '*');

         refresh();
      } 

      if (c != ERR) {
         mvprintw(2, 0, "Got character %c (%d)\n", c, c);
      }
  }//while 1

   endwin();
   return 0;
}

正如您所暗示的那样,这似乎高度依赖于实现,而我的实际目标是以完全符合标准的方式实现它。此外,我将不得不在每个其他信号处理程序中阻止SIGWINCH,以避免在嵌套信号处理程序中调用longjmp时发生的未定义行为。 - user1678062
@user1678062 我看了一下man手册,它几乎认为这是一个特性:)。我所能想到的唯一方法就是将stdin设置为非阻塞描述符并对其进行轮询...即使在嵌入式系统上每秒轮询几十次也应该可以忽略不计。除此之外,也许您可以向stdin注入一个不可打印的虚拟字符以触发从getch退出,丢弃该字节并继续执行。 - RobC
@user1678062 在考虑注入时,我想知道是否可以打开本地回显并发送一个字符。我还发现了这个网址http://unix.stackexchange.com/questions/103885/piping-data-to-a-processs-stdin-without-causing-eof-afterward。看起来你可以设置一个管道,在运行时按需发送数据到stdin...不确定正常的stdin在这种情况下是否有效。 - RobC
看起来很有趣。如果我找不到其他解决方案,我想我必须坚持下去。尽管如此,我还是要指出一些事情。首先:当调整终端窗口大小时,getch在超时发生时不会返回ERR,而是返回~Z(410)。其次:如果可以以可移植的方式向stdin注入字符,那么是否也可以在它们返回后使用非阻塞的getch来检索键,并在select/poll上选择stdin?但我仍然缺少关于当直接操作stdin时curses的反应的规范。 - user1678062
在Linux中,“410”是“KEY_RESIZE”,但并非所有实现都支持,尽管它非常方便。 - paxdiablo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接