linux面试题:Ctrl+Alt+Fx 切换 tty 这个功能是在哪里实现的?
Linux终端切换功能实现分析 文章探讨了Linux系统中Ctrl+Alt+Fx切换tty功能的实现机制。通过分析发现: 终端切换功能主要由systemd-logind服务实现,通过ioctl(fd, VT_ACTIVATE)系统调用来完成 使用chvt命令可以直接切换终端,其核心逻辑是调用VT_ACTIVATE和VT_WAITACTIVE的ioctl操作 在SSH的pts终端中执行chvt可能无
题目
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-logind的main函数处很容易就找到一个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 7的data对应
# 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被暂停时候,屏幕切换终端功能不影响。。。
检索内核,找到了这样一段描述,这个文件可以作为poll的fd检测虚拟终端切换:
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.c和defkeymap.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终端问题。不过知道这些跟踪方法,你看,几个小时就足够找到这一切是在哪、如何实现的了。。。
更多推荐


所有评论(0)