콘솔 출력이 어느 TTY로 가는지 커널 디버깅 명령으로 추적하기

Linux 콘솔 출력이 tty0, tty1, ttyS0, pts/N 중 어디로 흘러가는지 /proc/consoles, /sys/class/tty, tracefs, ftrace, dynamic_debug로 추적하는 운영 절차입니다.

콘솔에 글자가 찍혔다는 말은 생각보다 범위가 넓습니다. SSH 터미널의 pts/0로 간 출력일 수도 있고, 물리 화면의 tty1로 간 출력일 수도 있고, 커널 printkttyS0 같은 serial console로 뿌린 메시지일 수도 있습니다. 그래서 콘솔 출력 문제를 볼 때는 먼저 "이 출력이 사용자 프로세스의 stdout/stderr인지, 커널 printk인지, getty/login이 붙은 TTY인지"를 나눠야 합니다.

1. 먼저 용어를 끊어야 한다

이름의미확인 포인트
/dev/tty현재 프로세스의 controlling terminalSSH면 보통 /dev/pts/N, 로컬 콘솔이면 /dev/ttyN
/dev/console커널과 init이 보는 시스템 콘솔 장치console=, /proc/consoles 기준으로 확인
tty0현재 active virtual console을 가리키는 별칭/sys/class/tty/tty0/active, fgconsole
tty1~ttyNLinux virtual terminalgetty@tty1.service, ps -t tty1
ttyS0, ttyAMA0serial console 계열serial-getty@..., hypervisor serial console
pts/NSSH, terminal emulator, tmux 같은 pseudo terminalwho, w, ps -t pts/N

2. 지금 보고 있는 화면이 어느 TTY인지 확인한다

사용자 프로세스 출력은 커널 콘솔과 다릅니다. 먼저 내 shell의 stdin/stdout/stderr가 어느 장치에 붙어 있는지 확인합니다.

tty

readlink -f /proc/$$/fd/0
readlink -f /proc/$$/fd/1
readlink -f /proc/$$/fd/2

ps -o pid,ppid,tty,stat,comm,args -p $$
ps -t "$(tty | sed 's#^/dev/##')" -o pid,ppid,tty,stat,comm,args

w
who
loginctl list-sessions
loginctl session-status "$XDG_SESSION_ID" 2>/dev/null || true

tty/dev/pts/0이면 지금 보는 화면은 SSH나 terminal emulator의 pseudo terminal입니다. 이 경우 echo hello는 kernel console로 가는 것이 아니라 현재 shell의 controlling terminal로 갑니다. 반대로 물리 콘솔에서 로그인했다면 /dev/tty1처럼 보일 수 있습니다.

3. 커널 콘솔 후보를 확인한다

커널 메시지, 부팅 로그, panic 로그, /dev/kmsg로 들어간 출력은 console= 설정과 등록된 console driver를 기준으로 흐릅니다. 이 부분은 /proc/cmdline/proc/consoles를 같이 봐야 합니다.

cat /proc/cmdline | tr ' ' '\n' | grep '^console=' || true
cat /proc/consoles

cat /sys/class/tty/tty0/active 2>/dev/null || true
fgconsole 2>/dev/null || true

systemctl status getty@tty1.service --no-pager
systemctl status serial-getty@ttyS0.service --no-pager 2>/dev/null || true
journalctl -u getty@tty1.service -b --no-pager | tail -n 80
journalctl -u serial-getty@ttyS0.service -b --no-pager 2>/dev/null | tail -n 80

console=tty0 console=ttyS0,115200처럼 여러 개가 있으면 커널 출력은 여러 console driver로 복제될 수 있습니다. 어떤 장치가 실제로 등록됐는지는 추측하지 말고 /proc/consoles로 확인합니다. VM 환경에서는 hypervisor 콘솔이 ttyS0에 붙어 있는 경우가 많고, bare metal에서는 tty0나 BMC serial redirection이 같이 보일 수 있습니다.

4. marker를 만들어 경로를 분리한다

추적할 때는 평소 로그를 그대로 보면 섞입니다. 짧은 marker를 만들어 사용자 TTY 출력과 커널 printk 출력을 따로 발생시킵니다.

# 사용자 프로세스가 자기 controlling terminal로 쓰는 경로
printf 'userspace-tty-marker pid=%s tty=%s\n' "$$" "$(tty)" > /dev/tty

# 커널 printk 경로를 강제로 만들 때. root 필요.
sudo sh -c 'echo "kernel-console-marker from /dev/kmsg" > /dev/kmsg'

# printk가 실제로 들어왔는지 확인
dmesg -T | tail -n 40
journalctl -k -b --no-pager | tail -n 40

여기서 /dev/tty로 보낸 marker는 현재 세션의 TTY 경로를 탑니다. /dev/kmsg로 보낸 marker는 printk 경로를 타고 등록된 console driver로 전달됩니다. 두 흐름을 구분하지 않으면 "콘솔에 찍혔다"는 사실만 남고 어디서 어디로 갔는지 판단하기 어렵습니다.

5. tracefs에서 이 커널이 제공하는 지점을 먼저 본다

커널 버전과 설정에 따라 함수 이름과 tracepoint가 조금씩 다릅니다. 바로 enable하지 말고 이 서버에 있는 지점부터 확인합니다.

sudo mount -t tracefs nodev /sys/kernel/tracing 2>/dev/null || true
cd /sys/kernel/tracing

sudo sh -c '
echo 0 > tracing_on
echo nop > current_tracer
: > trace
'

# 이 서버 커널에 실제로 있는 TTY/console 함수와 tracepoint부터 확인한다.
sudo grep -E '^(tty_write|n_tty_write|do_tty_write|vt_console_print|serial.*console.*write|uart_console_write|call_console_drivers|console_unlock)$' available_filter_functions | sort -u
sudo find events -maxdepth 2 -type d | grep -E '/(tty|console|printk)(/|$)|/(tty_|console|printk_)' | sort

운영 서버에서 안전하게 보려면 tracepoint가 있으면 tracepoint를 먼저 쓰고, 없거나 부족할 때 ftrace function filter로 내려가는 순서가 좋습니다. function graph를 무턱대고 전체 켜면 로그가 너무 커집니다.

6. tracepoint로 TTY write와 console 출력을 본다

tracepoint가 있으면 가장 깔끔합니다. 이벤트 이름은 커널별로 다를 수 있으니 존재하는 것만 enable하도록 작성합니다.

sudo sh -c '
cd /sys/kernel/tracing
echo 0 > tracing_on
: > trace

for event in   events/tty/tty_write   events/tty/tty_open   events/tty/tty_close   events/console/console   events/printk/console
do
  [ -e "${event}/enable" ] && echo 1 > "${event}/enable"
done

echo 1 > tracing_on
'

printf 'tracepoint-tty-marker pid=%s tty=%s\n' "$$" "$(tty)" > /dev/tty
sudo sh -c 'echo "tracepoint-kmsg-marker" > /dev/kmsg'

sudo sh -c '
cd /sys/kernel/tracing
echo 0 > tracing_on
tail -n 200 trace

for event in   events/tty/tty_write   events/tty/tty_open   events/tty/tty_close   events/console/console   events/printk/console
do
  [ -e "${event}/enable" ] && echo 0 > "${event}/enable"
done
'

출력에서 tty_write 계열이 보이면 사용자 프로세스가 TTY device에 write한 경로입니다. console 또는 printk 계열이 보이면 printk가 console driver로 flush되는 경로입니다. 둘이 같이 보인다고 같은 출력이라는 뜻은 아닙니다. marker 문자열과 PID, command 이름을 같이 봐야 합니다.

7. ftrace로 함수 호출 경로를 좁힌다

tracepoint만으로 부족하면 ftrace의 function tracer를 씁니다. 핵심은 전체 커널 함수를 켜지 않고 TTY/console 관련 함수만 filter에 넣는 것입니다.

sudo sh -c '
cd /sys/kernel/tracing
echo 0 > tracing_on
echo nop > current_tracer
: > set_ftrace_filter
: > set_ftrace_pid
: > trace

for func in   tty_write   n_tty_write   do_tty_write   vt_console_print   uart_console_write   call_console_drivers   console_unlock
do
  grep -qx "${func}" available_filter_functions && echo "${func}" >> set_ftrace_filter
done

echo function > current_tracer
echo 1 > tracing_on
'

printf 'ftrace-tty-marker pid=%s tty=%s\n' "$$" "$(tty)" > /dev/tty
sudo sh -c 'echo "ftrace-kmsg-marker" > /dev/kmsg'

sudo sh -c '
cd /sys/kernel/tracing
echo 0 > tracing_on
cat trace | tail -n 200
echo nop > current_tracer
: > set_ftrace_filter
'

tty_write, n_tty_write 쪽은 사용자 프로세스가 TTY에 쓰는 흐름을 보여줍니다. call_console_drivers, console_unlock, vt_console_print, serial console write 계열은 printk가 실제 console driver로 나가는 쪽을 볼 때 의미가 있습니다. 함수가 안 보이면 그 커널에서 inline됐거나 이름이 다르거나 trace 대상 심볼이 아닐 수 있습니다.

8. 특정 PID의 TTY write만 보고 싶을 때

현재 shell이나 특정 프로세스가 /dev/tty에 쓰는 경로만 좁혀 보려면 set_ftrace_pid를 같이 씁니다.

# 현재 shell이 /dev/tty에 쓰는 사용자 프로세스 경로만 좁혀 볼 때.
WRITER_PID=$$

sudo sh -c "
cd /sys/kernel/tracing
echo 0 > tracing_on
echo nop > current_tracer
: > trace
: > set_ftrace_filter
echo tty_write >> set_ftrace_filter
grep -qx n_tty_write available_filter_functions && echo n_tty_write >> set_ftrace_filter
echo ${WRITER_PID} > set_ftrace_pid
echo function > current_tracer
echo 1 > tracing_on
"

printf 'pid-filter-marker pid=%s tty=%s\n' "$$" "$(tty)" > /dev/tty

sudo sh -c '
cd /sys/kernel/tracing
echo 0 > tracing_on
cat trace
echo nop > current_tracer
: > set_ftrace_pid
: > set_ftrace_filter
'

PID filter는 사용자 프로세스가 system call을 타고 TTY에 쓰는 흐름에는 유용합니다. 하지만 printk flush는 다른 kernel context에서 발생할 수 있으므로 /dev/kmsg marker까지 같은 PID로만 보려고 하면 놓칠 수 있습니다.

9. dynamic_debug는 드라이버 내부 로그가 필요할 때만 켠다

ftrace가 호출 여부를 보여준다면 dynamic debug는 커널 소스의 pr_debug 지점이 살아 있을 때 내부 로그를 늘려 줍니다. 운영 장비에서는 반드시 파일 단위로 좁게 켜고 끝나면 끕니다.

sudo mount -t debugfs nodev /sys/kernel/debug 2>/dev/null || true

sudo grep -Ei 'drivers/tty/(tty_io|n_tty|vt|serial)'   /sys/kernel/debug/dynamic_debug/control | head -n 80

# 커널에 pr_debug 지점이 있고 CONFIG_DYNAMIC_DEBUG가 켜져 있을 때만 출력이 늘어난다.
sudo sh -c 'echo "file drivers/tty/tty_io.c +p" > /sys/kernel/debug/dynamic_debug/control'
sudo sh -c 'echo "file drivers/tty/n_tty.c +p" > /sys/kernel/debug/dynamic_debug/control'

dmesg -wT

# 다른 터미널에서 marker를 만든 뒤, 끝나면 반드시 끈다.
sudo sh -c 'echo "file drivers/tty/tty_io.c -p" > /sys/kernel/debug/dynamic_debug/control'
sudo sh -c 'echo "file drivers/tty/n_tty.c -p" > /sys/kernel/debug/dynamic_debug/control'

dynamic_debug/control에 항목이 없거나 CONFIG_DYNAMIC_DEBUG가 꺼진 커널이면 이 방식은 쓸 수 없습니다. 이때는 tracefs/ftrace나 bpftrace로 보는 편이 낫습니다.

10. bpftrace로 빠르게 PID와 comm만 찍기

서버에 bpftrace가 있고 kprobe 접근이 가능하면, 짧게 누가 tty_write를 타는지만 볼 수 있습니다.

# 함수가 커널 심볼로 노출되는지 먼저 확인한다.
sudo bpftrace -l 'kprobe:tty_write'
sudo bpftrace -l 'kprobe:n_tty_write'

# 단순히 어떤 프로세스가 tty_write 경로를 타는지 볼 때.
sudo bpftrace -e 'kprobe:tty_write { printf("pid=%d comm=%s\n", pid, comm); }'

# 별도 터미널에서:
printf 'bpftrace-tty-marker pid=%s tty=%s\n' "$$" "$(tty)" > /dev/tty

이 방식은 빠르지만 운영 커널 보안 설정, lockdown, BPF 권한, 심볼 노출 상태에 영향을 받습니다. 안 되면 tracefs로 돌아가면 됩니다.

11. 흔한 판정 기준

  • SSH 화면에만 보이면 대부분 pts/N 경로입니다. /dev/console 문제가 아닙니다.
  • VM 콘솔에는 보이는데 SSH에는 안 보이면 console=ttyS0, tty0, getty, hypervisor console mapping을 봅니다.
  • 부팅 초기 메시지만 보이고 로그인 프롬프트가 없으면 kernel console은 살아 있지만 getty가 안 떠 있거나 target 전환이 막힌 상태일 수 있습니다.
  • systemd service 로그는 기본적으로 journal로 갑니다. TTY에 직접 쓰려면 unit의 StandardOutput=tty, TTYPath= 같은 설정을 따로 봐야 합니다.
  • panic, oops, printk는 프로세스 stdout이 아닙니다. dmesg, journalctl -k, console driver 경로로 봐야 합니다.

12. 끝나면 반드시 원복한다

trace와 dynamic debug는 잠깐 켜는 도구입니다. 특히 function tracer나 dynamic debug를 켜 둔 채 운영 서버를 방치하면 로그와 오버헤드가 커질 수 있습니다.

sudo sh -c '
cd /sys/kernel/tracing
echo 0 > tracing_on
echo nop > current_tracer
: > set_ftrace_filter
: > set_ftrace_pid
: > trace
find events -name enable -exec sh -c "echo 0 > \"$1\"" sh {} \;
'

sudo sh -c 'echo "file drivers/tty/tty_io.c -p" > /sys/kernel/debug/dynamic_debug/control' 2>/dev/null || true
sudo sh -c 'echo "file drivers/tty/n_tty.c -p" > /sys/kernel/debug/dynamic_debug/control' 2>/dev/null || true

정리

콘솔 출력 추적은 tty 하나만 보고 끝내면 안 됩니다. 먼저 현재 프로세스의 fd가 pts/N인지 ttyN인지 확인하고, 커널 console 후보는 /proc/cmdline/proc/consoles로 확인합니다. 그 다음 marker를 나눠 만든 뒤 tracepoint, ftrace, dynamic_debug 순서로 좁히면 "이 출력이 어느 TTY를 타고 어디까지 갔는지"를 재현 가능한 절차로 설명할 수 있습니다.

BGM EVER