Linux 编程提示

作者:Michael K. Johnson

在 Linux 版本 .12 和 .95 左右(对于那些不了解 Linux 历史中一些怪异之处的人来说,这两个版本是连续的...),Orest Zborowski1 承担了让 X Windowing System 在 Linux 上运行的任务。Orest 没有采取目光短浅的方法,花费时间将 X 移植到 Linux,而是将 Linux 移植到 X。为此,他为 Linux 编写了原始的 Unix 域套接字和 VT 接口,它是 SVR4 下 VT 接口的子集。后来,Andries Brouwer2 完成了加载键盘映射的大部分工作,添加了更多的键盘处理功能。

本文将解释如何编程 VT 接口来完成那些在 Linux 控制台上使用“转义序列”不容易完成的事情,并为执行此操作所需的 ioctl() 提供参考。本专栏的大部分内容源自 Orest 撰写的一份文档,因为他有兴趣进一步传播这些信息。

VT 接口

VT 接口是一组可以在任何控制台设备上执行的 ioctl()。VT 与 VC(虚拟控制台)紧密相连。它们之所以被不同地命名,是因为它们在 SVR4 中被称为 VT,也是因为在源代码中 VT 操作和 VC 操作之间存在一些区别。VT 编号与 VC 编号相同:0 是“当前” VT 的同义词,所有真正的 VT 从 1 开始。在下面所有的 ioctl 中,将 VT 0 用作 ioctl 的目标是合法的——它只会影响当前活动的 VT。3

这与 SVR4 不同,在 SVR4 中,0 是第一个 VT,而 /dev/console 是当前的 VT。这种差异是由于 Linux 中的原始 VC 使用 VC 0 作为 /dev/console,而 SVR4 使 /dev/console 成为一个单独的设备。幸运的是,这在实践中不会引起问题。

头文件 sys/vt.h 和 sys/kd.h 几乎是完整的,符合 SVR4 规则,但它们的大部分内容不受 Linux 支持。头文件

linux/keyboard.h 文件维护了更多关于键盘映射的信息,并包含了 Brouwer 编写的部分。

VT 参考

ioctl(int ttyfd, KIOCSOUND, unsigned int count)

KIOCSOUND 将使用以下关系打开声音

                  hz = 1193180
                ---------------
                     count

如果 count = 0,则声音关闭。声音将持续到显式关闭为止。

 ioctl(int ttyfd, KDMKTONE, unsigned int count_ticks)

KDMKTONE 将在特定数量的时钟滴答声内打开声音。count_ticks 由两部分组成:高 16 位保存您希望声音持续的时钟滴答声数量(在 Linux/86 下至少是百分之一秒;请参阅 linux/sched.h 中的 HZ 定义),而低 16 位保存计数,它与 KIOCSOUND 的 count 参数相同。调用立即返回。

ioctl(int ttyfd, KDGKBTYPE, unsigned char *kb)

KDGKBTYPE 在 kb 中返回键盘类型。这可以是

        KB_84           84 key keyboard
        KB_101          101 key keyboard
        KB_OTHER         other keyboard

Bug

目前,总是返回 KB_101

ioctl(int ttyfd, KDADDIO, int port)

KDADDIO 将启用对指定端口的访问。端口必须在 0x3b40x3df 范围内(涵盖常见的图形端口)。要访问此范围之外的端口,请使用 ioperm(2) 系统调用。

ioctl(int ttyfd, KDDISABIO, int port)

KDDISABIO 将禁用对指定端口的访问。有关更多详细信息,请参阅 KDADDIO

ioctl(int ttyfd, KDSETMODE, int mode)

KDSETMODE 将 VT 的模式更改为文本或图形

        KD_GRAPHICS                  graphics mode KD_TEXT
        text mode KD_TEXT0             same as KD_TEXT KD_TEXT1
        same as KD_TEXT

ttyfd 必须是当前控制台。如果指定的模式已就位,则不执行任何操作。当进入文本模式时,屏幕将被取消消隐,并且启用消隐定时器(在正常操作中)。当进入图形模式时,屏幕将被消隐,并将保持消隐状态,直到切换回文本模式。

Bug:没有做出特殊的规定来保存或恢复此调用期间 VT 的内容。应用程序有责任保存任何必要的信息以供以后恢复。这是因为需要芯片组特定的信息才能正确保存或恢复 VT 的内容。

ioctl(int ttyfd, KDGETMODE, int mode)

KDGETMODE 返回指定 VT 的当前模式。有关更多详细信息,请参阅 KDSETMODE

ioctl(int ttyfd, KDSKBMODE, int kbmode)

KDSKBMODE 设置键盘上的转换模式。选项包括

Linux Programming Hints

从一种模式切换到另一种模式也会刷新输入队列,以避免混淆。无论当前模式如何,内核都会维护 shift、lock 等键的正确状态信息。

ioctl(int ttyfd, KDGKBMODE, unsigned long *mode)

KDGKBMODE 返回与特定 tty 关联的键盘模式。

ioctl(int ttyfd, KDGETLED, unsigned char *leds)

KDGETLED 以标志形式返回 LED 的状态

        LED_SCR scroll lock is down
        LED_NUM num lock is down
        LED_CAP caps lock is down
ioctl(int ttyfd, KDSETLED, unsigned char leds)

KDSETLED 根据传入的标志设置 LED。正确的使用方法是使用 KDGETLED,然后对这些标志进行更改,然后使用 KDSETLED 更改标志。

ioctl(int ttyfd, VT_SETMODE, struct vt_mode *vtm)

VT_SETMODE 根据以下结构设置 VT 的控制模式

struct vt_mode {
        char mode;
        char waitv;
        short relsig;
        short acqsig;
        short frsig;
};
Linux Programming Hints

VT_AUTO 模式下,内核负责 VT 切换等。这是默认模式。在 VT_PROCESS 模式下,一个进程接管一个 VT 的控制权。它负责确认切换请求并执行任何需要的任务。例如,图形程序可能希望在 VT_PROCESS 模式下运行,因此如果用户想要切换到另一个 VT 并返回,则图形模式将被正确保存和恢复。

下面的一节将完整描述切换语义。

Bug:不支持写入的 waitv 模式。

ioctl(int ttyfd, VT_GETMODE, struct vt_mode *vtm)

VT_GETMODE 返回 VT 的当前控制状态。有关更多详细信息,请参阅上面的 VT_SETMODE。

ioctl(int ttyfd, VT_GETSTATE, struct vt_stat *vts)

VT_GETSTATE 在结构中返回内核中所有 VT 的状态

struct vt_stat {
        ushort v_active;
        ushort v_signal;
        ushort v_state;
};
v_active        the currently active VT
v_state         mask of all the opened VT's

v_active 保存活动 VT 的编号(从 1 开始),而 v_state 保存一个掩码,其中每个已被某些进程打开的 VT 都有一个 1。请注意,VT 0 在此场景中始终处于打开状态,因为它指的是当前 VT。

Bug

v_signal 成员不受支持。

ioctl(int ttyfd, VT_OPENQRY, long *num)

VT_OPENQRY 返回第一个可用 VT 的编号,即尚未被任何进程打开的 VT。如果没有空闲 VT,则在 num 中返回 -1。

ioctl(int ttyfd, VT_ACTIVATE, int num)

VT_ACTIVATE 将导致切换到 VT 编号 num,就像从键盘引起的一样。特别是,如果 VT 编号 num 处于 VT_PROCESS 模式,则与负责的进程开始协商。调用可能会在切换完成之前返回。使用 VT_WAITACTIVE 等待直到切换完成。

ioctl(int ttyfd, VT_WAITACTIVE, int num)

VT_WAITACTIVE 将等待直到指定的 VT 已被激活(已完成切换到它)。

Bug

此调用实际上并不执行切换,但它可能也需要执行切换,就像 SVR4 所做的那样,以便与某些应用程序兼容。

ioctl(int ttyfd, VT_RELDISP, int val)

VT_RELDISP 用于向内核发出有关正在进行的切换的信号。如果 ttyfd 是当前控制台,则它必须处于 VT_PROCESS 模式。

如果从一个 VT 切换到另一个 VT,“from” VT 会收到关于切换请求的信号。回复是通过带有以下值的 VT_RELDISP ioctl:

0       switch is disallowed, and the kernel aborts the attempt
1       switch is allowed, and the kernel continues with the switch
2       switch has been completed

如果从另一个 VT 切换到 VT,内核将发出关于切换请求的信号。回复是通过带有以下值的 VT_RELDISP ioctl:

VT_ACKACQ               switch-to is allowed

Bug

切换到响应是 SVR4 中的非标准行为。目前,Linux 不需要切换到 VT_RELDISP ioctl,但如果进行了切换,则它必须具有参数 VT_ACKACQ

ioctl(int fd, KDSKBMETA, int flags)

KDSKBMETA 指定按下 meta(alt)键是否生成 ESC (\033) 前缀,后跟 keysym,或者用高位设置标记的 keysym。

K_METABIT       generate an ESC  prefix
K_ESCPREFIX     same as K_METABIT
0               generates a high-bit marked keysym
ioctl(int fd, KDGKBMETA, unsigned long *flags)

KDGKBMETA 返回 META 前缀的状态,如上面的 KDSKBMETA 中所述。

ioctl(int fd, KDGKBENT, struct kbentry *kbe)

KDGKBENT 返回特定键和修饰符的 keysym 映射。

struct kbentry {
        u_char kb_table;
        u_char kb_index;
        u_short kb_value;
        };

用户将 kb_table 设置为请求的修饰符表,并将 kb_index 设置为请求的键码。KDGKBENTkb_value 中返回 keysym。修饰符表由以下值的逻辑“或”生成

        K_NORMTAB       normal table
        K_SHIFTTAB      shift
        K_ALTTAB        alt (meta)
        K_SRQTAB        right alt (altgr)
ioctl(int fd, KDSKBENT, struct kbentry *kbe)

KDSKBENT 设置特定键码和修饰符组合的 keysym 映射。有关更多信息,请参阅上面的 KDGKBENT

ioctl(int fd, KDGKBSENT, struct kbsentry *kbs)

KDGKBSENT 返回绑定到特定功能键的字符串

struct kbsentry {
        u_char kb_func;
        u_char kb_string[512];
        };

kb_func 是功能键的索引(0 - NR_FUNC),KDGKBSENT 将在 kb_string 中返回当前映射的字符串。

ioctl(int fd, KDSKBSENT, struct kbsentry *kbs)

KDSKBSENT 设置映射到功能键的字符串。当按下此功能键时,会发出该字符串。有关 struct kbsentry 的说明,请参阅上面的 KDGDBSENT

ioctl(int fd, KDGKBDIACR, struct kbdiacrs *kbds)

KDGKBDIACR 返回内核变音符号映射表

struct kbdiacr {
        u_char diacr,
        base, result;
        }; struct kbdiacrs {
        unsigned int kb_cnt;
        struct kbdiacr kbdiacr[256];
        };

有关详细信息,请参阅 keymap 包。

ioctl(int fd, KDSKBDIACR, struct kbdiacrs *kbds)

KDSKBDIACR 设置变音符号表。有关详细信息,请参阅上面的 KDGKBDIACR。有关详细信息,请参阅 keymap 包。

ioctl(int fd, PIO_FONT, unsigned char font[8192])

PIO_FONT 设置控制台视频字体。字体长度为 8192 字节,并且特定于正在使用的特定模式。有关详细信息,请参阅 keymap 包。

ioctl(int fd, GIO_FONT, unsigned char font[8192])

GIO_FONT 获取控制台视频字体。返回 8192 字节的字体信息。有关详细信息,请参阅 keymap 包。

ioctl(int fd, PIO_SCRNMAP, unsigned char trans[256])

PIO_SCRNMAP 设置用户控制台转换表。这会将 8 位代码映射到视频字体代码。用户表可以通过向控制台发送 ESC(K 来选择。有关详细信息,请参阅 keymap 包。

ioctl(int fd, GIO_SCRNMAP, unsigned char trans[256])

GIO_SCRNMAP 返回控制台转换表。有关详细信息,请参阅 keymap 包。VT 切换 当用户键入 <Alt>-<Fn>,其中 n 是 VT 的编号时,内核将切换到该 VT。如果某些进程执行 ioctl(fd, VT_ACTIVATE, n);,也会发生相同的序列

首先,如果“切换到”VT 处于 VT_AUTO 模式,则内核将忽略切换请求(如果它也处于 KD_GRAPHICS 模式),否则它将继续切换。

如果“切换到”VT 处于 VT_PROCESS 模式,则 relsig 信号将发送到“切换自”进程,以便它可以释放 VT。如果进程接受该信号,则内核将等待来自它的 VT_RELDISP ioctl。如果进程已死,则 VT 将被强制重置为 KD_TEXT 和 VT_AUTO 模式。这可能会导致极大的混乱和不快,但内核无法做得更好。

“切换自”进程将需要执行任何清理,并发出 VT_RELDISP ioctl,告诉内核可以继续切换。它也可能拒绝切换,在这种情况下,内核将停止切换。

如果“切换自”进程已同意切换,则内核将更改为新的 VT,同时更改键盘模式和 LED。然后,如果新的 VT 处于 VT_PROCESS 控制下,“切换到”进程将收到 acqsig 信号。如果此进程丢失,则新的 VT 将重置为 KD_TEXTVT_AUTO 模式。以这种方式,在正常使用期间会进行一定量的自动重置。当然,如果进程在 KD_GRAPHICS 模式下进行图形更改,则这些更改将不会被内核撤消。

此时切换完成。“切换到”进程可以调用 VT_RELDISP VT_ACKACQ,但内核不需要这样做。如果任何进程正在等待此新 VT 变为活动状态,则此时会唤醒它们。

示例

对于大多数人来说,X 源代码过于庞大,难以轻松下载,也过于庞大,难以轻松研究。但是,还有其他示例可用。svgalib 为这些函数提供了一个易于使用的接口,并为 VGA 和某些 SVGA 视频板提供了一致的接口。它还可以作为那些想要编写自己的代码的人的示例代码,因为它包括使用 mmap()ioperm() 直接访问视频内存的示例代码,一旦它使用了上面描述的 ioctl(),它就被允许这样做。以下代码片段描述了一种访问端口和内存的方法,而无需使用 svgalib。PAGE_SIZE<linux/page.h> 中定义,GRAPH_SIZEGRAPH_BASE 可能因显卡而异。此代码基于 vgalib 版本 1.2 中的代码。

FILE *mem_fd;
char *graph_get, *graph_mem;
if (ioperm(port, 1, 1)) {
   fprintf(stderr, "Can't access port %x\n", port); exit(1);
} if ((mem_fd = open("/dev/mem", O_RDWR)) < 0) {
   fprintf(stderr, "Can't open /dev/mem\n"); exit(1);
} if ((graph_get = malloc(GRAPH_SIZE + (PAGE_SIZE-1))) === NULL) {
fprintf(stderr, "Insufficient memory\n");
   exit(1);
}
graph_mem = graph_get; if ((unsigned long)graph_mem % PAGE_SIZE)
   graph_mem += PAGE_SIZE - ((unsigned long)graph_mem % PAGE_SIZE);
graph_mem = (unsigned char *)
                mmap((caddr_t)graph_mem,
                        GRAPH_SIZE, PROT_READ|PROT_WRITE,
                        MAP_SHARED|MAP_FIXED, mem_fd, GRAPH_BASE);
if ((long)graph_mem < 0) {
   fprintf(stderr, "mmap error\n"); exit(1);
}

此时,写入 graph_mem 实际上是在写入屏幕内存。iopl()ioperm() 也可以用于获得写入端口的权限,KDADDIO ioctl() 也可以,如上所述。《Linux 文档项目》手册页包括关于 iopl()ioperm() 的手册页,因此我不会在此处记录它们,因为这些手册页应该已随您的 Linux 发行版一起提供。如果您没有它们,可以在 sunsite.unc.edu 上以 /pub/Linux/docs/LDP/man-pages/* 的形式访问它们。

DOS 模拟器也使用其中一些调用来提供 DOS 习惯于使用的接口给真正的 DOS 程序,并允许 DOS 会话使用视频卡提供的视频 bios。

大多数 KD*i ioctl() 的权威示例代码是随 Linux 内核分发的 keymap 包。

对于下个月,我计划解释如何使用这些调用为 Linux 编写一个屏幕锁定包,因为本月的空间和时间已用完。

加载 Disqus 评论