题目

Linux 上 Ctrl+Alt+Fx 切换 tty 这个功能是在哪里实现的?如何修改快捷键? 如果让你写个程序从而 ./changetty 3 切换到 tty3 你如何实现?

文章链接:v2ex gullitintanni: 骑驴找马, Linux 面试凉经分享

一开始我也没看过这一块的地方,以为只是以到简单题,毕竟也只是op面试的第一题,以为考察的是文件dup

kiraskyler      17 小时 25 分钟前
Linux 上 Ctrl+Alt+Fx 切换 tty 这个功能是在哪里实现的?如何修改快捷键? 如果让你写个程序从而 ./changetty 3 切换到 tty3 你如何实现?


ctrl+alt+fx 这一块内核代码没看过

切换 tty
可以直接 open("/dev/tty/3")得到文件描述符,dup 将现在的标准输入输出转发到 tty3 即可。但是否影响其他 tty3 用户未知。可以先遍历/proc/<pid>/fd 看看有没有进程使用 tty3 ,没有了再切换

实际上,这道题考察的是ioctl(chvt)相关知识:

gullitintanni   
OP
   7 小时 31 分钟前
@kiraskyler #16

其实面试官说的切换 tty 想问的是 chvt(1) 的实现,当时我只记得是在 /dev/tty 上面调用某个 ioctl 实现的(具体答案是 VT_ACTIVATE )。

所以我觉得这种八股挺无聊的,记得这些细枝末节又能怎样呢?想知道直接搜一下不就好了。

chvt

首先,ai告诉我,用chvt命令可以直接切换终端,ok,chvt命令的源码非常简单,主要逻辑只有下面这几行:

kbd/BUILD/kbd-2.6.1/src/chvt.c: 102

    if ((fd = getfd(NULL)) < 0)
		kbd_error(EXIT_FAILURE, 0, _("Couldn't get a file descriptor referring to the console."));

kbd/BUILD/kbd-2.6.1/src/chvt.c: 129
	do {
		errno = 0;

		if (ioctl(fd, VT_ACTIVATE, num) < 0 && errno != EINTR)
			kbd_error(EXIT_FAILURE, errno,  _("Couldn't activate vt %d"), num);

		if (ioctl(fd, VT_WAITACTIVE, num) < 0 && errno != EINTR)
			kbd_error(EXIT_FAILURE, errno, "ioctl(%d,VT_WAITACTIVE)", num);

	} while (errno == EINTR);

getfd(NULL)这一段标识获取的是0,即标准输入终端。

可惜,试了下,当前终端和屏幕终端的pts/tty都没有变化,问了ai,说是这个功能只是ACTIVATE激活,不是切换

ps:后来发现,这个命令只有在tty中敲才有用,当时在sshd的pts终端里,所以才无效,具体原理没有细看

追踪谁在切换

一开始还以为是ioctl(fd, VT_WAITEVENT),因为这里面有oldev/newev,看着像切换终端用的

/usr/include/linux/vt.h: 66

struct vt_event {
	unsigned int event;
#define VT_EVENT_SWITCH		0x0001	/* Console switch */
#define VT_EVENT_BLANK		0x0002	/* Screen blank */
#define VT_EVENT_UNBLANK	0x0004	/* Screen unblank */
#define VT_EVENT_RESIZE		0x0008	/* Resize display */
#define VT_MAX_EVENT		0x000F
	unsigned int oldev;		/* Old console */
	unsigned int newev;		/* New console (if changing) */
	unsigned int pad[4];		/* Padding for expansion */
};

#define VT_WAITEVENT	0x560E	/* Wait for an event */

跟踪下,过滤VT_WAITEVENT事件

# bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl /args->cmd == 0x560E/ {printf("%s %d", comm, pid); kstack(); printf("\n");}'

屏幕切换,结果,没输出。。。

把过滤去掉,看看谁在ioctl

# bpftrace -e 'tracepoint:syscalls:sys_enter_ioctl /args->cmd == 0x560E/ {printf("%s %d", comm, pid); kstack(); printf("\n");}'
systemd-logind 840 21505
(agetty) 1919 21505
agetty 1919 21505

嗯,看来是systemd-logind

之前还给欧拉提过一个关于这个服务的简单的issue:src-openEuler/systemd: systemd-logind 启动失败

systemd-logind vt

看一下服务的pid

# systemctl status systemd-logind
● systemd-logind.service - User Login Management
     Loaded: loaded (/usr/lib/systemd/system/systemd-logind.service; static)
     CGroup: /system.slice/systemd-logind.service
             └─840 /usr/lib/systemd/systemd-logind

gdb跟一下,断点ioctl,屏幕切一些终端,然后敲个bt就看到在哪里切换终端了

# gdb -p 840

# b ioctl
Breakpoint 1 at 0x7fd033725de0

# bt
#0  0x00007fd033725de0 in ioctl () from target:/usr/lib64/libc.so.6
#1  0x000055cc457c5a0e in vt_is_busy (vtnr=5) at ../src/login/logind-core.c:516
#2  manager_spawn_autovt (vtnr=5, m=0x55cc804b2b80) at ../src/login/logind-core.c:541
#3  seat_active_vt_changed (vtnr=5, s=<optimized out>) at ../src/login/logind-seat.c:383
#4  seat_read_active_vt.isra.0 (s=<optimized out>) at ../src/login/logind-seat.c:416
#5  0x000055cc457a7b32 in manager_dispatch_console (s=<optimized out>, fd=<optimized out>, revents=<optimized out>, userdata=<optimized out>) at ../src/login/logind.c:594
#6  0x00007fd033a71eb0 in source_dispatch (s=s@entry=0x55cc804b4fb0) at ../src/libsystemd/sd-event/sd-event.c:4261
#7  0x00007fd033a7211d in sd_event_dispatch (e=<optimized out>, e@entry=0x55cc804b3e70) at ../src/libsystemd/sd-event/sd-event.c:4882
#8  0x00007fd033a73998 in sd_event_run (e=<optimized out>, timeout=timeout@entry=18446744073709551615) at ../src/libsystemd/sd-event/sd-event.c:4943
#9  0x000055cc457a4d10 in manager_run (m=0x55cc804b2b80) at ../src/login/logind.c:1157
#10 run (argv=<optimized out>, argc=<optimized out>) at ../src/login/logind.c:1205
#11 main (argc=<optimized out>, argv=<optimized out>) at ../src/login/logind.c:1208

具体这个服务如何切换就不细看了

里面设计一些session管理、状态、systemd服务之间的通讯

不过简单看一下,实际是ioctl(fd, VT_ACTIVATE激活,切换终端貌似也是open("/dev/tty/<num>")vt本身不控制终端。

ctrl alt f

systemd-logindmain函数处很容易就找到一个manager_parse_config_file函数,加载配置文件,可惜指向的配置文件里没看到哪里写了什么按键可以切换终端

strace跟一下服务,看一下屏幕敲ctrl alt f时候发生了什么

# strace -p 8224
strace: Process 8224 attached
gettid()                                = 8224
epoll_wait(4, [{events=EPOLLERR, data={u32=2994139056, u64=94208806809520}}], 32, -1) = 1
lseek(7, 0, SEEK_SET)                   = 0
read(7, "tty4\n", 63)                   = 5
openat(AT_FDCWD, "/dev/tty1", O_RDWR|O_NOCTTY|O_CLOEXEC) = 18

注意到这个事件:epoll_wait(4, [{events=EPOLLERR, data={u32=2994139056, u64=94208806809520}}], 32, -1) = 1

# gdb -p 8224
(gdb) bt
#0  0x00007ff407f2a597 in epoll_wait () from target:/usr/lib64/libc.so.6
#1  0x00007ff4082723fe in epoll_wait_usec (timeout=<optimized out>, maxevents=<optimized out>, events=<optimized out>, 
    fd=<optimized out>) at ../src/libsystemd/sd-event/sd-event.c:4641
#2  process_epoll (ret_min_priority=<synthetic pointer>, threshold=9223372036854775807, timeout=<optimized out>, e=0x55aeb276de70)
    at ../src/libsystemd/sd-event/sd-event.c:4664
#3  sd_event_wait (e=<optimized out>, e@entry=0x55aeb276de70, timeout=<optimized out>, timeout@entry=18446744073709551615)
    at ../src/libsystemd/sd-event/sd-event.c:4787
#4  0x00007ff408273a2b in sd_event_run (e=<optimized out>, timeout=timeout@entry=18446744073709551615)
    at ../src/libsystemd/sd-event/sd-event.c:4936
#5  0x000055aea016bd10 in manager_run (m=0x55aeb276cb80) at ../src/login/logind.c:1157
#6  run (argv=<optimized out>, argc=<optimized out>) at ../src/login/logind.c:1205
#7  main (argc=<optimized out>, argv=<optimized out>) at ../src/login/logind.c:1208

看来是别的进程传递来的信号

可惜,strace没显示出epoll_data_t里关键的fd

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

没关系,gdb看一下

gdb附加后,发现进程停在epoll_wait_usec,在结束的地方设置断点

src/libsystemd/sd-event/sd-event.c: 46643

                r = epoll_wait_usec(
                                e->epoll_fd,
                                e->event_queue,
                                n_event_max,
                                timeout);
                if (r < 0)
                        return r;

(gdb) p e->event_queue.data
ptr:
0x5634e60ecfb0
fd:
-435236944
u32:
3859730352
u64:
94785198018480

虽然fd乱了,可能是大小端问题,但是ptr看到了,正好和epollfd 4中的fd 7data对应

# cat /proc/10058/fdinfo/4 
pos:	0
flags:	02000002
mnt_id:	16
ino:	32
tfd:        7 events:       18 data:     5634e60ecfb0  pos:5 ino:521d sdev:16

fd 7,指向一个内核提供的虚拟文件接口

# ll /proc/10058/fd
lr-x------. 1 root root 64  7月31日 14:02 7 -> /sys/devices/virtual/tty/tty0/active

ps:调试过程中发现,systemd-logind被暂停时候,屏幕切换终端功能不影响。。。

检索内核,找到了这样一段描述,这个文件可以作为pollfd检测虚拟终端切换:

linux-5.10.202/Documentation/ABI/testing/sysfs-tty: 23

What:		/sys/class/tty/tty0/active
Date:		Nov 2010
Contact:	Kay Sievers <kay.sievers@vrfy.org>
Description:
		 Shows the currently active virtual console
		 device, like 'tty1'.
		 The file supports poll() to detect virtual
		 console switches.

看来,ctrl alt f是内核控制的,systemd-logind只是监控变化

kernel

搜索内核找到这个地方看起来可疑:

linux-5.10.202/drivers/tty/vt/vt.c: 236

/*
 * /sys/class/tty/tty0/
 *
 * the attribute 'active' contains the name of the current vc
 * console and it supports poll() to detect vc switches
 */
static struct device *tty0dev;

这个静态成员的使用,只有这一个地方:

linux-5.10.202/drivers/tty/vt/vt.c: 984

void redraw_screen(struct vc_data *vc, int is_switch)
{

			sysfs_notify(&tty0dev->kobj, NULL, "active");

看看这里如何被调用的:

# bpftrace -e 'kprobe:redraw_screen {printf("%s", kstack());}'
Attaching 1 probe...

        redraw_screen+1
        complete_change_console+65
        console_callback+402
        process_one_work+380
        worker_thread+621
        kthread+201
        ret_from_fork+45
        ret_from_fork_asm+27

注意到这里,切换终端:

/root/qemu/linux-5.10.202/drivers/tty/vt/vt.c: 2970

/*
 * This is the console switching callback.
 *
 * Doing console switching in a process context allows
 * us to do the switches asynchronously (needed when we want
 * to switch due to a keyboard interrupt).  Synchronization
 * with other console code and prevention of re-entrancy is
 * ensured with console_lock.
 */
static void console_callback(struct work_struct *ignored)
{
	console_lock();

	if (want_console >= 0) {
		if (want_console != fg_console &&
			change_console(vc_cons[want_console].d);        // <- 这里

注意到want_console变量,然后又找到这里比较可以,设置的want_console

/root/qemu/linux-5.10.202/drivers/tty/vt/vt.c: 3014

int set_console(int nr)
{
	struct vc_data *vc = vc_cons[fg_console].d;

	want_console = nr;

看一下内核如何到这里的:

# bpftrace -e 'kprobe:set_console {printf("%s", kstack());}'
Attaching 1 probe...

        set_console+1
        kbd_keycode+575
        kbd_event+238
        input_to_handler+218
        input_pass_values.part.0+280
        input_event_dispose+197
        input_handle_event+61
        input_event+79
        atkbd_receive_byte+1746
        ps2_interrupt+117
        serio_interrupt+67
        i8042_interrupt+323
        __handle_irq_event_percpu+70
        handle_irq_event+56
        handle_edge_irq+144
        __common_interrupt+56

追踪找到:

这里定义了一些按键的回调:

linux-5.10.202/drivers/tty/vt/keyboard.c: 71

/*
 * Handler Tables.
 */

#define K_HANDLERS\
	k_self,		k_fn,		k_spec,		k_pad,\
	k_dead,		k_cons,		k_cur,		k_shift,\
	k_meta,		k_ascii,	k_lock,		k_lowercase,\
	k_slock,	k_dead2,	k_brl,		k_ignore

static k_handler_fn *k_handler[16] = { K_HANDLERS };

其中一个回调正是设置终端

linux-5.10.202/drivers/tty/vt/keyboard.c: 732

static void k_cons(struct vc_data *vc, unsigned char value, char up_flag)
{
	if (up_flag)
		return;

	set_console(value);
}

vt键盘驱动中这里调用到的设置终端:

linux-5.10.202/drivers/tty/vt/keyboard.c: 1359

static void kbd_keycode(unsigned int keycode, int down, int hw_raw)
{
	if (keycode < NR_KEYS)
		keysym = key_map[keycode];
	else if (keycode >= KEY_BRL_DOT1 && keycode <= KEY_BRL_DOT8)
		keysym = U(K(KT_BRL, keycode - KEY_BRL_DOT1 + 1));
	else
		return;

	type = KTYP(keysym);
	
	type -= 0xf0;
	
	(*k_handler[type])(vc, keysym & 0xff, !down);

这里面对按键的处理,按键映射大多写在keyboard.cdefkeymap.c文件中,貌似没有看到有什么配置文件可以配置修改按键的地方,如果要改,也是要改内核这两个文件的代码。

所以,为什么这个面试题要问,如何修改按键呢?

同时注意到,tty/vt/keyboard.c,这个是专属于tty的按键驱动,如果自己实现一套别的终端,也可以带上自己的键盘驱动,就可以想怎么改就怎么改了。难道这道题考察的是驱动开发???

end

Linux 上 Ctrl+Alt+Fx 切换 tty 这个功能是在哪里实现的?如何修改快捷键? 如果让你写个程序从而 ./changetty 3 切换到 tty3 你如何实现?

这道题目设计到:会话管理、内核键盘驱动、虚拟终端,三个知识点

Ctrl+Alt+Fx 切换 tty 实现方式:

tty提供键盘驱动,键盘硬中断->终端键盘驱动->切换终端->/sys/devices/virtual/tty/tty0/active接口通知->systemd-logind服务收到通知->systemd-logind切换会话

如何修改快捷键?

只能修改内核源代码,在终端键盘驱动中

如果让你写个程序从而 ./changetty 3 切换到 tty3

参考chvt源代码,就几行

以上方法中各个模块实际我都没有细看,因为工作中没有遇到这方面的工作内容,这道题出的并不好,方向太细分了,主要涉及到的还是tty终端问题。不过知道这些跟踪方法,你看,几个小时就足够找到这一切是在哪、如何实现的了。。。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐