Linux 编程提示
在 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 接口是一组可以在任何控制台设备上执行的 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 编写的部分。
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 将启用对指定端口的访问。端口必须在 0x3b4 到 0x3df 范围内(涵盖常见的图形端口)。要访问此范围之外的端口,请使用 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 设置键盘上的转换模式。选项包括
从一种模式切换到另一种模式也会刷新输入队列,以避免混淆。无论当前模式如何,内核都会维护 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; };
在 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 设置为请求的键码。KDGKBENT 在 kb_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_TEXT 和 VT_AUTO 模式。以这种方式,在正常使用期间会进行一定量的自动重置。当然,如果进程在 KD_GRAPHICS 模式下进行图形更改,则这些更改将不会被内核撤消。
此时切换完成。“切换到”进程可以调用 VT_RELDISP VT_ACKACQ,但内核不需要这样做。如果任何进程正在等待此新 VT 变为活动状态,则此时会唤醒它们。
示例
对于大多数人来说,X 源代码过于庞大,难以轻松下载,也过于庞大,难以轻松研究。但是,还有其他示例可用。svgalib 为这些函数提供了一个易于使用的接口,并为 VGA 和某些 SVGA 视频板提供了一致的接口。它还可以作为那些想要编写自己的代码的人的示例代码,因为它包括使用 mmap() 和 ioperm() 直接访问视频内存的示例代码,一旦它使用了上面描述的 ioctl(),它就被允许这样做。以下代码片段描述了一种访问端口和内存的方法,而无需使用 svgalib。PAGE_SIZE 在 <linux/page.h> 中定义,GRAPH_SIZE 和 GRAPH_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 编写一个屏幕锁定包,因为本月的空间和时间已用完。