Linux 编程提示
如果您不记得或者没有阅读上个月的专栏,VT ioctl() 允许您从用户程序指定内核应该如何处理虚拟终端或虚拟控制台。(它们本质上是相同的。在本专栏的其余部分,我将称它们为虚拟控制台,而不是虚拟终端,没有任何特别的原因)。
程序可以请求内核提供原始扫描码而不是完整的击键,可以告诉内核您将在该终端上进入图形模式,并执行许多其他底层操作。XFree86 大量使用了这些 ioctl(),svgalib 也是如此。Linux DOS 模拟器(实际上是一个 BIOS 模拟器)和可加载的键盘映射程序 (kbd) 也使用了它们。
如果您没有阅读上个月的专栏,该专栏的主要内容将包含在 Linux 手册页项目未来发布的手册页中。
我编写了一个名为 vlock 的程序,它是一个可以锁定虚拟控制台的屏幕锁定器。我没有足够的空间在这里重现完整的源代码,但我将提供足够的细节,以便您轻松构建自己的类似程序。在本专栏的代码之后,提供了通过互联网匿名 ftp 获取副本的说明。
我编写 vlock 的最初目的是演示 VT ioctl() 的一种用途,它们实际上并非为此而设计,以展示它们的灵活性。
如果您像许多 Linux 用户一样,您可能运行了一个或两个 X 会话,并且同时激活了几个控制台登录,并在它们之间来回切换。也许您一直在编辑一个您正在处理的程序,并且不希望您的室友或孩子在您离开电脑时开始玩您的文件,但您真的不想注销并重新启动所有会话。
如果您只想锁定一个 X 会话,xlock 可以解决您的问题,但即使 xlock 正在运行,任何人仍然可以切换到控制台。您需要一个可以一次锁定所有会话的程序。好吧,也许您需要一个可以一次锁定所有会话的程序...
我锁定控制台的第一个想法是从键盘读取原始扫描码而不是读取普通字符,并且忽略除字母数字键、Shift 键、Caps Lock 键和 Control 键的扫描码之外的任何内容,并编写一个状态机来从中获取按键。这将自动忽略通常用于从虚拟控制台切换到虚拟控制台的 ALT-Fn 键,因此这些按键不会导致 VC 切换。当然,某些国家键盘可能会存在一些问题,但它在很大程度上适用于大多数人。
然而,这将涉及大量的工作和大量的测试,如果有一种简单的方法可以做到这一点,我就懒得做那么多工作。(我后来意识到这种方法也存在严重的安全问题。我稍后会让您尝试找出缺陷是什么,我将在本专栏的结尾解释。)
然后我注意到有一些 ioctl() 专门用于在切换虚拟控制台之前先询问。程序可以明确拒绝让内核切换虚拟控制台。这些 ioctl() 仅适用于虚拟控制台,因此首先我们需要打开一个虚拟控制台来执行 ioctl()。最简单的方法是
if (vfd = open("/dev/console", O_RDWR) < 0) { perror("vlock: could not open /dev/console"); exit (1); }
/dev/console 代表当前屏幕。假设 vlock 运行时,它将在当前的虚拟控制台上运行。(事实证明,这个假设不会造成安全漏洞,尽管在您看来它应该会。)
也可以切换到未分配的虚拟终端,就像 X 所做的那样,这在某些情况下可能是更可取的。为此,我们可以使用 ioctl VT_OPENQRY 来查找第一个可用虚拟控制台的编号,打开相应的设备 (/dev/ttynn,其中 nn 是 VT_OPENQRY 返回的编号),并使用 VT_ACTIVATE 切换到该虚拟控制台。
仅仅打开 /dev/console 要容易得多。
c = ioctl(vfd, VT_GETMODE, &vtm); if (c < 0) { fprintf(stderr, "This tty is not a virtual console.\n"); is_vt = 0; } else { is_vt = 1; }
我们将像 termios 接口一样处理 VT_GETMODE 和 VT_SETMODE ioctl():首先我们获取当前设置,然后我们更改本地副本,然后我们将内核的副本设置为看起来像更改后的本地副本。
VT_GETMODE 使用当前的 VT 设置填充 vt_mode 结构。如果它返回错误,则程序一定没有在虚拟控制台上运行。vlock 不会在此错误时退出,但它会将 is_vt 变量设置为 0,并且如果 is_vt 变量设置为 0,它不会尝试使用任何更多的 VT ioctl()。
/* we set SIGUSR{1,2} to point to *_vt() */ sigemptyset(&(sa.sa_mask)); sa.sa_flags = 0; sa.sa_handler = release_vt; sigaction(SIGUSR1, &sa, NULL); sa.sa_handler = acquire_vt; sigaction(SIGUSR2, &sa, NULL);
我们稍后将安排在内核请求从程序正在运行的虚拟控制台更改时将 SIGUSR1 发送到进程,并在内核请求更改为程序正在运行的虚拟控制台时将 SIGUSR2 发送到进程。这些请求可能是由用户按下 ALT-Fn 键或其他程序发出 VT_ACTIVATE ioctl 引起的。
当收到 SIGUSR1 时,将调用 release_vt()
void release_vt(int signo) { if (!o_lock_all) /* kernel is allowed to switch */ ioctl(vfd, VT_RELDISP, 1); else /* kernel is not allowed to switch */ ioctl(vfd, VT_RELDISP, 0); }
如果用户想要一次锁定所有虚拟控制台,则设置变量 o_lock_all。如果用户只想锁定当前的虚拟控制台,则不设置该变量。VT_RELDISP 用于告诉内核程序确认它已收到要求其放弃虚拟控制台的信号,并告诉内核它是否同意这样做。第三个参数设置为 1 以允许内核切换到另一个虚拟控制台,或设置为 0 以阻止内核切换到另一个虚拟控制台。
当收到 SIGUSR2 时,将调用 acquire_vt()
void acquire_vt(int signo) { /* This call is not currently required under Linux, but it won't hurt, either... */ ioctl(vfd, VT_RELDISP, VT_ACKACQ); }
Linux 实际上并不要求这样做; 包含它是为了与 SYSV 兼容,SYSV 要求调用它。我将其包含在 vlock 中主要是为了,如果有人想将 vlock 移植到 SYSV 的某个版本,那么他或她就会少一个绊脚石。
现在我们已经设置了这些信号处理程序,我们将告诉虚拟控制台管理器关于它们。
我们不想告诉虚拟控制台管理器通过这些信号路由更改虚拟控制台的请求,直到信号处理程序已安装,因为这样做可能会在速度非常慢的机器上运行太多进程时导致一个小错误。
if (is_vt) { /* Keep a copy around to restore at appropriate times */ ovtm = vtm; vtm.mode = VT_PROCESS; /* handled by release_vt(): */ vtm.relsig = SIGUSR1; /* handled by acquire_vt(): */ vtm.acqsig = SIGUSR2; ioctl(vfd, VT_SETMODE, &vtm); }
ovtm 是另一个 vt_mode 结构,就像 vtm 一样。将 vtm.mode 设置为 VT_PROCESS 会导致内核请求更改虚拟控制台的权限。将 vtm.relsig 设置为 SIGUSR1,vtm.acqsig 设置为 SIGUSR2 会告诉内核如何请求权限。
此时,所有需要做的就是处理所有合理的信号,以便人们无法通过键入 control-c 或 control-\ 或 control-break 来闯入,然后要求用户键入密码并对照真实密码进行检查。有一个库函数 getpass(),它可以从用户那里获取密码,而不会将其回显到屏幕上。
不幸的是,此函数在至少一个影子密码实现下被破坏,因为信号处理程序未正确安装,因此要制作一个适用于影子密码的屏幕锁定程序,您要么必须修复影子密码库,要么编写您自己的 getpass() 版本。对于 vlock,我选择告诉人们 vlock 在不修复他们的影子密码库的情况下无法与影子密码正确配合使用,而不是编写我自己的函数版本。
一旦输入了正确的密码,程序就可以退出。这在 Linux 下至少是可以接受的。但是,以防万一这不适用于 VT ioctl() 的某些其他 SYSV 实现,我在 vlock 中包含了代码来恢复所有内容,包括 VT 状态,到原始设置。这就是为什么我在几个代码片段之前制作了 vtm 的副本 ovtm。
当然,除非你想这样做。当您阅读本文时,我可能已经多次升级了 vlock。最新版本的 vlock 将始终可以从 ftp 站点 tsx-11.mit.edu 的 /pub/linux/sources/usr.bin 目录中名为 vlock-m.n.tar.gz 的文件中获得,其中 m 和 n 分别是发行版的主版本号和次版本号。
截至本文撰写时,vlock 的当前版本为 0.6。如果您无法使用 ftp,但有 Internet 电子邮件,您可以发送电子邮件至 johnsonm@redhat.com 并请求副本,我可以向您发送包含 vlock 的源代码和二进制文件的 uuencoded gzipped tar 文件。此外,Linux 的 Debian 发行版也包含 vlock。
在本文的开头附近,我说过我会解释通过简单地捕获所有按键来锁定虚拟控制台的根本缺陷。问题在于,有人可以很容易地从网络或调制解调器或串行终端登录并运行一个程序(他们可能必须先编写它),该程序将发出更改虚拟控制台的请求。这个程序会比乍看起来更棘手一些,但它是可以编写的。内核将遵守请求更改的 ioctl(),并且屏幕锁定程序将被击败。
我发现许多 Unix 程序员对信号有点困惑。这是可以理解的,因为至少有三个使用信号的标准。[纯粹主义者,请不要告诉我实际上有更多; 我试图在这里保持相对简单。更详细的解释,据我所知,在历史上是正确的,可以在 W. Richard Stevens 的《Unix 环境高级编程》的第 10 章“信号”中找到。] 虽然我之前在第一期中提到过信号的差异,但我将在此处更明确地解释。
最初的信号是不可靠的。signal() 函数用于安装一个信号处理程序,该处理程序适用于信号的一次调用,一旦信号处理程序被调用一次,信号处理程序就会卸载。所以你会像这样安装你的信号处理程序
signal(SIGUSR1, signal_handling_function);
然后你会像这样实现你的信号处理函数
void signal_handling_function(int signo) { signal(SIGUSR1, signal_handling_function); /* Do whatever the signal handling function is supposed to do... */ }
采用这种方法的问题是,有时会在内核卸载信号处理程序和信号处理程序重新安装自身之间到达第二个信号。
不采用这种方法的问题是信号处理程序需要是可重入的。
不幸的是,随着可靠信号的引入,BSD 修改了 signal(),使其在调用时不会被卸载,而 SYSV 则保持 signal() 的原样。故事还有更多,但只会变得更加混乱。
幸运的是,绝对没有必要感到困惑。完全没有必要使用 signal()。不要使用它:这样做(在不了解所有不同版本的 Unix 上的 signal() 函数的所有细节的情况下)是编写不可移植的代码。
POSIX 定义了一个备用接口,该接口在所有符合 POSIX 标准的平台上都是相同的。此接口称为 sigaction,比任何版本的 signal() 都更强大和灵活。[sigaction 源自可靠信号的第一个 BSD 实现,因此使用 sigaction 的代码不仅可以移植到所有 POSIX 平台,还可以移植到 POSIX 之前的 BSD 系统。] 不幸的是,它有点复杂,但您可以编写自己的信号管理包装函数来获得您需要的信号类型。这是一个例子
typedef void signal_handler(int); signal_handler * my_signal(int signo, signal_handler *func, int oneshot) { struct sigaction sact, osact; sigemptyset(&sact.sa_mask); sact.sa_handler = func; if (oneshot) { sact.sa_flags = SA_ONESHOT; } else { sact.sa_flags = 0; } if (sigaction(signo, &sact, &osact) < 0) { return (SIG_ERR); } else { return (oact.sa_handler); } }
这并不完美,但它创建了一个与 sigaction 的接口,该接口与 signal() 一样方便,但无论在什么系统上编译,都将具有相同的语义,这与 signal() 不同。
它的工作方式类似于 signal(),只是它接受第三个参数。第三个参数确定信号处理程序在调用时是否保持安装状态,或者是否在调用后立即卸载。
信号处理程序自动卸载有两个正常原因。第一个是信号处理程序不可重入——如果在已经运行时再次运行信号处理程序不安全。第二个是对于那些您确实只想捕获信号的一个实例的情况,例如 SIGALRM。
您可能已经注意到上面代码中对 sigemptyset() 的调用。它在那里很重要,但我还没有提到它。事实证明,sigaction 信号处理程序有可能在运行时屏蔽掉某些信号。这最常见的发生情况可能是在不可重入的信号处理程序中。这些信号处理程序可以设置它们的 sa_mask 以防止在它们被调用时再次被调用,方法是使用类似这样的代码
sigemptyset(&sact.sa_mask); sigaddset(&sact.sa_mask, SIGFOO); sact.sa_handler = signal_handler; sact.sa_flags = 0; if (sigaction(SIGFOO, &sact, &osact) < 0) { do_signal_error(SIGFOO); }
这将允许您为 SIGFOO 使用不可重入的信号处理程序。当然,这段代码必须稍作修改才能适应您的应用程序。您至少必须使用真实的信号名称而不是 SIGFOO...
如果您有兴趣对信号进行更多操作,请在现代 Unix 编程书籍或手册中查找 sigaction() 函数,并阅读“信号集”,可以在以下函数下找到;sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()、sigprocmask()、sigpending()、sigsetjmp()、siglongjmp() 和 sigsuspend()。这些为各种花哨的信号工作提供了非常精细的可调支持,我本月将不尝试介绍。
如果您对本专栏有任何建议或意见,请发送电子邮件至 johnsonm@redhat.com 或将纸质邮件发送至 Programming Tips, Linux Journal, P.O. Box 85867, Seattle, WA 98145-1867。我想知道到目前为止您发现哪些内容有用。
如果有任何您希望看到的未记录的 Linux 功能,我会查看它们。如果有足够的兴趣,我可以写一个专栏。我也希望有客座专栏作家为 Linux 编程提示撰写文章。