Serial / TTY 서브시스템
Serial/TTY 서브시스템을 콘솔, 제어 채널, 산업 통신까지 포함한 입출력 관점에서 심층 분석합니다. UART 하드웨어와 tty core 계층 분리, tty_driver/tty_port 구조, line discipline과 termios 파라미터 적용, RX/TX 버퍼링과 플로우 제어, console/earlycon 부팅 로그 경로, DMA 기반 고속 UART 처리, hangup·재연결·오류 플래그 복구, setserial/stty/debugfs 기반 디버깅까지 현장 장비 운용에 필요한 실전 포인트를 다룹니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.
단계별 이해
- 장치 수명주기 확인
probe부터 remove까지 흐름을 점검합니다. - 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리합니다. - 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인합니다. - 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현합니다.
Serial / TTY 서브시스템
TTY(Teletypewriter) 서브시스템은 리눅스 커널에서 가장 오래되고 복잡한 계층 중 하나입니다. 원래 물리적 텔레타이프 단말기를 위해 설계되었지만, 현대 리눅스에서는 시리얼 포트, 가상 콘솔, 의사 터미널(PTY), USB 시리얼 등 다양한 문자 기반 I/O 인터페이스를 통합 관리합니다.
TTY 코어 데이터 구조
TTY 서브시스템의 핵심 구조체들은 include/linux/tty.h와 include/linux/tty_driver.h에 정의되어 있습니다. 각 구조체의 역할과 관계를 이해하는 것이 TTY/Serial 드라이버 개발의 출발점입니다.
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>
/* === tty_struct: 열린 TTY 디바이스 인스턴스 ===
* 프로세스가 /dev/ttyS0 등을 open()하면 생성됩니다.
* 하나의 물리 포트에 대해 최대 하나의 tty_struct가 존재합니다.
*/
struct tty_struct {
int magic; /* TTY_MAGIC — 유효성 검증 */
struct kref kref; /* 참조 카운터 */
struct device *dev; /* sysfs 디바이스 */
struct tty_driver *driver; /* 소속 드라이버 */
const struct tty_operations *ops; /* 드라이버 오퍼레이션 */
int index; /* 드라이버 내 포트 인덱스 */
struct tty_ldisc *ldisc; /* 현재 line discipline */
struct tty_port *port; /* 하드웨어 포트 정보 */
struct tty_struct *link; /* PTY master ↔ slave 연결 */
struct tty_bufhead buf; /* flip buffer 헤드 */
struct winsize winsize; /* 터미널 윈도우 크기 */
struct ktermios termios; /* 현재 termios 설정 */
unsigned long flags; /* TTY_THROTTLED 등 상태 플래그 */
struct work_struct hangup_work; /* hangup 지연 처리 */
struct work_struct SAK_work; /* Secure Attention Key */
/* ... */
};
/* === tty_driver: TTY 드라이버 등록 정보 ===
* 동일 유형의 여러 포트를 관리하는 드라이버 단위 구조체입니다.
* 예: 8250 드라이버가 4개 시리얼 포트를 관리할 때 하나의 tty_driver를 등록합니다.
*/
struct tty_driver {
struct cdev *cdevs; /* 문자 디바이스 배열 */
const char *driver_name; /* 예: "serial" */
const char *name; /* 디바이스 이름 prefix: "ttyS" */
int name_base; /* 번호 시작값 (보통 0) */
int major; /* major 번호 (4=ttyS) */
int minor_start; /* minor 시작 (64=ttyS0) */
unsigned int num; /* 관리 포트 수 */
short type; /* TTY_DRIVER_TYPE_SERIAL 등 */
short subtype; /* SERIAL_TYPE_NORMAL 등 */
struct ktermios init_termios; /* 초기 termios */
const struct tty_operations *ops; /* 드라이버 콜백 */
struct tty_port **ports; /* 포트 배열 */
/* ... */
};
/* === tty_port: 물리/가상 포트 상태 관리 ===
* 하드웨어 포트(또는 가상 포트)의 라이프사이클과 상태를 추적합니다.
* open/close, hangup, carrier detect 등의 동기화를 담당합니다.
*/
struct tty_port {
struct tty_bufhead buf; /* flip buffer */
struct tty_struct *tty; /* 현재 열린 tty */
const struct tty_port_operations *ops; /* 포트 콜백 */
struct mutex mutex; /* open/close 직렬화 */
struct mutex buf_mutex; /* 버퍼 접근 보호 */
unsigned long flags; /* ASYNC_* 플래그 */
int count; /* open 참조 카운터 */
struct wait_queue_head open_wait; /* carrier detect 대기 */
struct wait_queue_head close_wait; /* 닫기 완료 대기 */
struct wait_queue_head delta_msr_wait; /* 모뎀 상태 변화 대기 */
unsigned char console:1; /* 콘솔 포트 여부 */
/* ... */
};
TTY 오퍼레이션 — tty_operations
tty_operations는 TTY 드라이버가 TTY 코어에 제공하는 콜백 함수 테이블입니다. VFS의 file_operations와 유사한 패턴으로, user space의 open()/write()/ioctl() 호출이 이 콜백으로 전달됩니다.
struct tty_operations {
/* 포트 열기/닫기 — 리소스 할당/해제 */
int (*open)(struct tty_struct *tty, struct file *filp);
void (*close)(struct tty_struct *tty, struct file *filp);
/* 데이터 송신 */
ssize_t (*write)(struct tty_struct *tty,
const u8 *buf, size_t count);
unsigned int (*write_room)(struct tty_struct *tty); /* 쓰기 가능 바이트 */
unsigned int (*chars_in_buffer)(struct tty_struct *tty);
/* termios 설정 변경 (보레이트, 데이터 비트, 패리티 등) */
void (*set_termios)(struct tty_struct *tty,
const struct ktermios *old);
/* 흐름 제어 */
void (*throttle)(struct tty_struct *tty); /* 수신 일시 정지 */
void (*unthrottle)(struct tty_struct *tty); /* 수신 재개 */
void (*stop)(struct tty_struct *tty); /* 송신 정지 (^S) */
void (*start)(struct tty_struct *tty); /* 송신 재개 (^Q) */
/* ioctl 핸들러 */
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
/* 모뎀 제어 신호 (DTR, RTS, CTS, DCD, DSR, RI) */
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
/* break 신호 송신 */
int (*break_ctl)(struct tty_struct *tty, int state);
/* 하드웨어 hangup 감지 */
void (*hangup)(struct tty_struct *tty);
/* RS485 설정 (half-duplex 산업용 통신) */
int (*get_serial)(struct tty_struct *tty,
struct serial_struct *p);
int (*set_serial)(struct tty_struct *tty,
struct serial_struct *p);
/* ... */
};
Flip Buffer — 수신 데이터 경로
인터럽트 핸들러에서 수신된 데이터를 user space로 전달하는 메커니즘입니다. ISR(Interrupt Service Routine)에서 직접 user space 버퍼에 복사할 수 없으므로, 커널 내부의 flip buffer를 거칩니다. ISR은 flip buffer에 데이터를 채우고, workqueue를 통해 line discipline으로 전달됩니다.
#include <linux/tty_flip.h>
/* 인터럽트 핸들러에서 수신 데이터 처리 */
static irqreturn_t my_uart_irq(int irq, void *dev_id)
{
struct uart_port *port = dev_id;
struct tty_port *tport = &port->state->port;
unsigned int status, ch;
status = readl(port->membase + REG_STATUS);
/* 수신 데이터 처리 */
while (status & RX_DATA_READY) {
ch = readl(port->membase + REG_DATA);
unsigned int flag = TTY_NORMAL;
port->icount.rx++;
/* 에러 검출 */
if (status & PARITY_ERR) {
port->icount.parity++;
flag = TTY_PARITY;
} else if (status & FRAME_ERR) {
port->icount.frame++;
flag = TTY_FRAME;
} else if (status & OVERRUN_ERR) {
port->icount.overrun++;
flag = TTY_OVERRUN;
} else if (status & BREAK_DETECT) {
port->icount.brk++;
flag = TTY_BREAK;
if (uart_handle_break(port))
continue;
}
/* sysrq / null char 처리 */
if (uart_handle_sysrq_char(port, ch))
continue;
/* flip buffer에 한 바이트 삽입 */
tty_insert_flip_char(tport, ch, flag);
status = readl(port->membase + REG_STATUS);
}
/* flip buffer의 데이터를 line discipline으로 push
* 내부적으로 work를 스케줄하여 ldisc->receive_buf() 호출 */
tty_flip_buffer_push(tport);
/* 송신 처리 */
if (status & TX_EMPTY)
my_handle_tx(port);
return IRQ_HANDLED;
}
/* flip buffer API 요약:
* tty_insert_flip_char(port, ch, flag) — 1바이트 삽입
* tty_insert_flip_string(port, str, len) — 문자열 bulk 삽입 (빠름)
* tty_prepare_flip_string(port, &p, len) — 직접 포인터 획득 (DMA용)
* tty_flip_buffer_push(port) — ldisc로 데이터 전달
*/
Line Discipline (회선 규율)
Line discipline은 TTY 코어와 하위 드라이버 사이에서 데이터를 가공하는 중간 계층입니다. 기본 N_TTY는 canonical/non-canonical 모드 처리, 에코, 시그널 문자(^C, ^Z) 등을 담당합니다. 사용자 정의 line discipline으로 교체하면 시리얼 라인 위에 전용 프로토콜을 구현할 수 있습니다.
| Line Discipline | 번호 | 용도 | 커널 소스 |
|---|---|---|---|
| N_TTY | 0 | 기본 터미널 I/O (canonical/raw 모드) | drivers/tty/n_tty.c |
| N_SLIP | 1 | Serial Line IP — 시리얼 위 IP 통신 | drivers/net/slip/ |
| N_PPP | 3 | Point-to-Point Protocol | drivers/net/ppp/ |
| N_GSM0710 | 21 | GSM 멀티플렉싱 (모뎀) | drivers/tty/n_gsm.c |
| N_NULL | 27 | 모든 데이터를 버림 (테스트용) | drivers/tty/n_null.c |
| N_TRACESINK | 23 | 디버그 트레이스 데이터 싱크 | drivers/tty/n_tracesink.c |
#include <linux/tty_ldisc.h>
/* 사용자 정의 Line Discipline 예제 — 간단한 패킷 프로토콜 */
static struct tty_ldisc_ops my_ldisc_ops = {
.owner = THIS_MODULE,
.num = N_MY_LDISC, /* 고유 번호 (29 이상 사용) */
.name = "my_proto",
/* TTY가 이 ldisc로 전환될 때 */
.open = my_ldisc_open,
.close = my_ldisc_close,
/* user space → driver 방향: write() 시스템콜에서 호출 */
.write = my_ldisc_write,
/* driver → user space 방향: ISR → flip buffer → 이 콜백 */
.receive_buf = my_ldisc_receive,
/* user space read()에서 호출 — 가공된 데이터 전달 */
.read = my_ldisc_read,
.ioctl = my_ldisc_ioctl,
};
/* receive_buf 콜백 — 하드웨어에서 수신된 데이터 처리 */
static void my_ldisc_receive(struct tty_struct *tty,
const u8 *data, const u8 *flags,
size_t count)
{
struct my_proto *proto = tty->disc_data;
size_t i;
for (i = 0; i < count; i++) {
if (flags && flags[i] != TTY_NORMAL)
continue; /* 에러 바이트 건너뛰기 */
if (data[i] == MY_FRAME_DELIM) {
my_process_frame(proto); /* 프레임 완성 → 처리 */
} else {
proto->buf[proto->len++] = data[i];
}
}
}
/* 모듈 초기화 시 ldisc 등록 */
static int __init my_ldisc_init(void)
{
return tty_register_ldisc(&my_ldisc_ops);
}
/* user space에서 ldisc 전환:
* int ldisc = N_MY_LDISC;
* ioctl(fd, TIOCSETD, &ldisc); // line discipline 변경
*/
serial_core 프레임워크 — UART 드라이버
serial_core(drivers/tty/serial/serial_core.c)는 UART 하드웨어 드라이버를 위한 표준 프레임워크입니다. 드라이버 개발자는 uart_driver를 등록하고, 각 포트에 대해 uart_port와 uart_ops를 제공하면 됩니다. TTY 코어와의 연동, line discipline 관리, sysfs 노출 등은 serial_core가 자동 처리합니다.
#include <linux/serial_core.h>
#include <linux/platform_device.h>
#define MY_UART_NR 4 /* 지원 포트 수 */
#define MY_UART_FIFO_SZ 64 /* TX/RX FIFO 깊이 */
/* === uart_ops: UART 하드웨어 오퍼레이션 === */
static unsigned int my_tx_empty(struct uart_port *port)
{
/* TX FIFO가 완전히 비었으면 TIOCSER_TEMT 반환 */
return (readl(port->membase + REG_STATUS) & TX_FIFO_EMPTY)
? TIOCSER_TEMT : 0;
}
static void my_set_mctrl(struct uart_port *port, unsigned int mctrl)
{
u32 val = readl(port->membase + REG_MCR);
if (mctrl & TIOCM_RTS) val |= MCR_RTS; else val &= ~MCR_RTS;
if (mctrl & TIOCM_DTR) val |= MCR_DTR; else val &= ~MCR_DTR;
writel(val, port->membase + REG_MCR);
}
static unsigned int my_get_mctrl(struct uart_port *port)
{
u32 status = readl(port->membase + REG_MSR);
unsigned int mctrl = 0;
if (status & MSR_CTS) mctrl |= TIOCM_CTS;
if (status & MSR_DCD) mctrl |= TIOCM_CAR; /* Carrier Detect */
if (status & MSR_DSR) mctrl |= TIOCM_DSR;
if (status & MSR_RI) mctrl |= TIOCM_RNG; /* Ring Indicator */
return mctrl;
}
static void my_start_tx(struct uart_port *port)
{
/* TX 인터럽트 활성화 — ISR에서 실제 전송 수행 */
u32 ier = readl(port->membase + REG_IER);
ier |= IER_TX_EMPTY;
writel(ier, port->membase + REG_IER);
}
static void my_stop_tx(struct uart_port *port)
{
u32 ier = readl(port->membase + REG_IER);
ier &= ~IER_TX_EMPTY;
writel(ier, port->membase + REG_IER);
}
static void my_stop_rx(struct uart_port *port)
{
u32 ier = readl(port->membase + REG_IER);
ier &= ~IER_RX_DATA;
writel(ier, port->membase + REG_IER);
}
static int my_startup(struct uart_port *port)
{
int ret;
/* IRQ 등록 */
ret = request_irq(port->irq, my_uart_irq,
IRQF_SHARED, "my-uart", port);
if (ret)
return ret;
/* FIFO 활성화, RX 인터럽트 활성화 */
writel(FCR_FIFO_EN | FCR_RX_TRIG_HALF,
port->membase + REG_FCR);
writel(IER_RX_DATA, port->membase + REG_IER);
return 0;
}
static void my_shutdown(struct uart_port *port)
{
/* 모든 인터럽트 비활성화 */
writel(0, port->membase + REG_IER);
free_irq(port->irq, port);
}
static void my_set_termios(struct uart_port *port,
struct ktermios *termios,
const struct ktermios *old)
{
unsigned int baud, lcr = 0;
/* 보레이트 계산 — 클램핑 포함 */
baud = uart_get_baud_rate(port, termios, old,
9600, 4000000);
unsigned int divisor = uart_get_divisor(port, baud);
/* 데이터 비트 */
switch (termios->c_cflag & CSIZE) {
case CS5: lcr |= LCR_WLEN5; break;
case CS6: lcr |= LCR_WLEN6; break;
case CS7: lcr |= LCR_WLEN7; break;
default: lcr |= LCR_WLEN8; break;
}
/* 정지 비트 */
if (termios->c_cflag & CSTOPB)
lcr |= LCR_STOP_2;
/* 패리티 */
if (termios->c_cflag & PARENB) {
lcr |= LCR_PARITY;
if (!(termios->c_cflag & PARODD))
lcr |= LCR_EVEN_PARITY;
}
/* 하드웨어 레지스터 업데이트 */
spin_lock_irq(&port->lock);
uart_update_timeout(port, termios->c_cflag, baud);
writel(divisor, port->membase + REG_BAUD_DIV);
writel(lcr, port->membase + REG_LCR);
/* 에러 무시 마스크 설정 */
port->read_status_mask = OVERRUN_ERR;
if (termios->c_iflag & INPCK)
port->read_status_mask |= PARITY_ERR | FRAME_ERR;
if (termios->c_iflag & (BRKINT | PARMRK))
port->read_status_mask |= BREAK_DETECT;
port->ignore_status_mask = 0;
if (termios->c_iflag & IGNPAR)
port->ignore_status_mask |= PARITY_ERR | FRAME_ERR;
if (termios->c_iflag & IGNBRK)
port->ignore_status_mask |= BREAK_DETECT;
spin_unlock_irq(&port->lock);
}
static const char *my_type(struct uart_port *port)
{
return "MY-UART";
}
static void my_config_port(struct uart_port *port, int flags)
{
if (flags & UART_CONFIG_TYPE)
port->type = PORT_MY_UART;
}
static const struct uart_ops my_uart_ops = {
.tx_empty = my_tx_empty,
.set_mctrl = my_set_mctrl,
.get_mctrl = my_get_mctrl,
.start_tx = my_start_tx,
.stop_tx = my_stop_tx,
.stop_rx = my_stop_rx,
.startup = my_startup,
.shutdown = my_shutdown,
.set_termios = my_set_termios,
.type = my_type,
.config_port = my_config_port,
};
/* === uart_driver: 드라이버 등록 구조체 === */
static struct uart_driver my_uart_drv = {
.owner = THIS_MODULE,
.driver_name = "my-uart", /* /proc/tty/drivers에 표시 */
.dev_name = "ttyMY", /* 디바이스 이름: /dev/ttyMY0, ttyMY1, ... */
.major = 0, /* 0 = 동적 할당 */
.minor = 0,
.nr = MY_UART_NR, /* 최대 포트 수 */
.cons = &my_console, /* 콘솔 구조체 (NULL 가능) */
};
/* === Platform Driver 통합 === */
static int my_uart_probe(struct platform_device *pdev)
{
struct my_uart_priv *priv;
struct resource *res;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->port.membase = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->port.membase))
return PTR_ERR(priv->port.membase);
priv->port.irq = platform_get_irq(pdev, 0);
priv->port.ops = &my_uart_ops;
priv->port.dev = &pdev->dev;
priv->port.type = PORT_MY_UART;
priv->port.iotype = UPIO_MEM32; /* 32-bit MMIO */
priv->port.fifosize = MY_UART_FIFO_SZ;
priv->port.flags = UPF_BOOT_AUTOCONF;
priv->port.line = pdev->id; /* 포트 번호 (DT: alias) */
/* 클럭 설정 */
priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
if (IS_ERR(priv->clk))
return PTR_ERR(priv->clk);
priv->port.uartclk = clk_get_rate(priv->clk);
platform_set_drvdata(pdev, priv);
/* serial_core에 포트 등록 → /dev/ttyMYn 생성 */
ret = uart_add_one_port(&my_uart_drv, &priv->port);
if (ret)
return ret;
dev_info(&pdev->dev, "MY-UART at 0x%lx, irq %d, %d Hz\\n",
(unsigned long)res->start, priv->port.irq,
priv->port.uartclk);
return 0;
}
static void my_uart_remove(struct platform_device *pdev)
{
struct my_uart_priv *priv = platform_get_drvdata(pdev);
uart_remove_one_port(&my_uart_drv, &priv->port);
}
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "vendor,my-uart" },
{ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);
static struct platform_driver my_uart_platform_drv = {
.probe = my_uart_probe,
.remove = my_uart_remove,
.driver = {
.name = "my-uart",
.of_match_table = my_uart_of_match,
},
};
/* 모듈 초기화: uart_driver 등록 → platform_driver 등록 */
static int __init my_uart_init(void)
{
int ret = uart_register_driver(&my_uart_drv);
if (ret)
return ret;
ret = platform_driver_register(&my_uart_platform_drv);
if (ret)
uart_unregister_driver(&my_uart_drv);
return ret;
}
static void __exit my_uart_exit(void)
{
platform_driver_unregister(&my_uart_platform_drv);
uart_unregister_driver(&my_uart_drv);
}
module_init(my_uart_init);
module_exit(my_uart_exit);
시리얼 콘솔과 earlycon
커널 콘솔은 부팅 메시지(printk)를 출력하는 저수준 인터페이스입니다. TTY 서브시스템이 초기화되기 전에도 동작하므로, earlycon으로 부팅 초기부터 디버그 출력이 가능합니다.
#include <linux/console.h>
#include <linux/serial_core.h>
/* === 일반 시리얼 콘솔 === */
static void my_console_write(struct console *co,
const char *s, unsigned int count)
{
struct uart_port *port = &my_ports[co->index];
unsigned long flags;
int locked;
/* 콘솔은 NMI, panic 등에서도 호출될 수 있음
* trylock 실패 시에도 출력 시도 (디버깅 목적) */
locked = spin_trylock_irqsave(&port->lock, flags);
/* uart_console_write()는 '\n' → '\r\n' 변환 포함 */
uart_console_write(port, s, count, my_console_putchar);
if (locked)
spin_unlock_irqrestore(&port->lock, flags);
}
static void my_console_putchar(struct uart_port *port, unsigned char ch)
{
/* TX FIFO 비어질 때까지 polling (콘솔은 인터럽트 불가) */
while (!(readl(port->membase + REG_STATUS) & TX_FIFO_EMPTY))
cpu_relax();
writel(ch, port->membase + REG_DATA);
}
static int my_console_setup(struct console *co, char *options)
{
struct uart_port *port = &my_ports[co->index];
int baud = 115200, bits = 8, parity = 'n', flow = 'n';
if (options)
uart_parse_options(options, &baud, &parity, &bits, &flow);
return uart_set_options(port, co, baud, parity, bits, flow);
}
static struct console my_console = {
.name = "ttyMY", /* console=ttyMY0,115200 */
.write = my_console_write,
.device = uart_console_device, /* serial_core 제공 헬퍼 */
.setup = my_console_setup,
.flags = CON_PRINTBUFFER, /* 등록 전 버퍼 출력 */
.index = -1, /* -1 = 커널 파라미터로 결정 */
.data = &my_uart_drv,
};
/* === earlycon: 부팅 초기 콘솔 ===
* TTY/serial_core 초기화 전에 printk 출력 가능.
* 커널 파라미터: earlycon=my-uart,0x1c28000,115200
* Device Tree: chosen { stdout-path = "serial0:115200n8"; };
*/
static void my_early_write(struct console *co,
const char *s, unsigned int count)
{
struct earlycon_device *dev = co->data;
struct uart_port *port = &dev->port;
uart_console_write(port, s, count, my_early_putchar);
}
static int __init my_early_console_setup(struct earlycon_device *dev,
const char *options)
{
if (!dev->port.membase)
return -ENODEV;
dev->con->write = my_early_write;
return 0;
}
OF_EARLYCON_DECLARE(my_uart, "vendor,my-uart", my_early_console_setup);
# 커널 부팅 파라미터 예시
console=ttyS0,115200n8 # 표준 시리얼 콘솔
console=ttyAMA0,115200 # ARM PL011
console=tty0 # VGA 콘솔
console=ttyS0 console=tty0 # 다중 콘솔 (마지막이 /dev/console)
earlycon=uart8250,mmio32,0xfe215040,115200 # earlycon 직접 지정
earlycon # DT stdout-path에서 자동 감지
TTY 디바이스 명명 규칙
| 디바이스 | 경로 | 용도 | 드라이버/서브시스템 |
|---|---|---|---|
| ttySN | /dev/ttyS0 | 8250/16550 호환 시리얼 포트 | drivers/tty/serial/8250/ |
| ttyAMAN | /dev/ttyAMA0 | ARM AMBA PL011 UART | drivers/tty/serial/amba-pl011.c |
| ttyUSBN | /dev/ttyUSB0 | USB-Serial 변환기 (FTDI, CP210x 등) | drivers/usb/serial/ |
| ttyACMN | /dev/ttyACM0 | USB CDC ACM (Abstract Control Model) | drivers/usb/class/cdc-acm.c |
| ttyMFDN | /dev/ttyMFD0 | Intel MID (Medfield) UART | drivers/tty/serial/mfd.c |
| ttyON | /dev/ttyO0 | TI OMAP UART | drivers/tty/serial/omap-serial.c |
| ttySACN | /dev/ttySAC0 | Samsung S3C/S5P UART | drivers/tty/serial/samsung_tty.c |
| ttyN | /dev/tty1 | 가상 콘솔 (VT) | drivers/tty/vt/ |
| pts/N | /dev/pts/0 | 의사 터미널 slave (PTY) | drivers/tty/pty.c |
| ptmx | /dev/ptmx | PTY master 멀티플렉서 | drivers/tty/pty.c |
| console | /dev/console | 시스템 콘솔 (마지막 console= 파라미터) | 커널 코어 |
| ttyGSN | /dev/ttyGS0 | USB Gadget 시리얼 (디바이스 모드) | drivers/usb/gadget/function/u_serial.c |
| ttyLPN | /dev/ttyLP0 | Intel LPSS UART (Low Power) | drivers/tty/serial/8250/8250_lpss.c |
PTY (Pseudo-Terminal)
의사 터미널은 물리 하드웨어 없이 TTY 인터페이스를 제공합니다. SSH, 터미널 에뮬레이터(xterm, gnome-terminal), screen/tmux 등이 PTY를 사용합니다. master(제어 프로그램 쪽)와 slave(응용 프로세스 쪽)의 쌍으로 동작하며, master에 쓴 데이터가 slave의 입력으로 나타나고, 그 반대도 마찬가지입니다.
/* master에 write → slave에서 read 가능 (키보드 입력 시뮬레이션)
* slave에 write → master에서 read 가능 (프로그램 출력 캡처)
* slave 쪽에 N_TTY line discipline 적용 (에코, ^C 등 처리)
*/
/* PTY 생성 과정 (user space, POSIX API) */
#include <stdlib.h>
#include <fcntl.h>
int master_fd = posix_openpt(O_RDWR | O_NOCTTY);
grantpt(master_fd); /* slave 소유권/퍼미션 설정 */
unlockpt(master_fd); /* slave 잠금 해제 */
char *slave_name = ptsname(master_fd); /* "/dev/pts/3" 등 */
int slave_fd = open(slave_name, O_RDWR);
/* 이제 master_fd ↔ slave_fd 양방향 통신 가능 */
/* 커널의 PTY 구현 핵심 (drivers/tty/pty.c) */
/* master의 write → slave의 입력 버퍼로 전달 */
static ssize_t pty_write(struct tty_struct *tty,
const u8 *buf, size_t c)
{
struct tty_struct *to = tty->link; /* master→slave 또는 slave→master */
if (!to || tty_io_error(tty))
return -EIO;
/* 상대편의 flip buffer에 데이터 삽입 */
c = tty_insert_flip_string(&to->port, buf, c);
if (c)
tty_flip_buffer_push(&to->port);
return c;
}
termios 설정 상세
termios 구조체는 TTY 디바이스의 동작 모드를 제어합니다. 입력/출력 처리, 제어 문자, 로컬 모드 등 네 가지 플래그 그룹으로 구성됩니다.
struct ktermios {
tcflag_t c_iflag; /* 입력 모드: IGNBRK, ICRNL, IXON, IXOFF ... */
tcflag_t c_oflag; /* 출력 모드: OPOST, ONLCR ... */
tcflag_t c_cflag; /* 제어 모드: CSIZE, CSTOPB, PARENB, CRTSCTS ... */
tcflag_t c_lflag; /* 로컬 모드: ECHO, ICANON, ISIG, IEXTEN ... */
cc_t c_cc[NCCS]; /* 제어 문자: VINTR(^C), VEOF(^D), VMIN, VTIME ... */
speed_t c_ispeed; /* 입력 보레이트 */
speed_t c_ospeed; /* 출력 보레이트 */
};
/* c_cflag 주요 비트:
* CSIZE — CS5/CS6/CS7/CS8 (데이터 비트)
* CSTOPB — 정지 비트 2개 (미설정 시 1개)
* PARENB — 패리티 활성화
* PARODD — 홀수 패리티 (미설정 시 짝수)
* CRTSCTS — 하드웨어 흐름 제어 (RTS/CTS)
* CLOCAL — 모뎀 제어 무시 (DCD 불필요)
* CREAD — 수신 활성화
* CBAUD — 보레이트 마스크 (B9600, B115200 등)
*/
/* c_lflag 주요 비트:
* ICANON — Canonical 모드 (줄 단위 입력, ^D로 EOF)
* ECHO — 입력 에코
* ECHOE — Backspace 에코 (지우기)
* ISIG — 시그널 문자 활성화 (^C→SIGINT, ^Z→SIGTSTP, ^\→SIGQUIT)
* IEXTEN — 확장 입력 처리 (^V → literal next)
*/
# stty로 termios 설정 확인/변경
stty -a -F /dev/ttyS0
# speed 115200 baud; rows 0; columns 0; line = 0;
# intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; ...
# -parenb -parodd cs8 -cstopb cread clocal -crtscts
# -ignbrk -brkint ignpar -ignpar -parmrk -inpck -istrip ...
# opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel ...
# -isig -icanon -iexten -echo -echoe -echok -echonl ...
# 보레이트 변경
stty -F /dev/ttyS0 115200
# 8N1 설정 (8 데이터 비트, 패리티 없음, 1 정지 비트)
stty -F /dev/ttyS0 cs8 -parenb -cstopb
# Raw 모드 (line discipline 가공 없이 바이트 그대로)
stty -F /dev/ttyS0 raw
# 하드웨어 흐름 제어 활성화
stty -F /dev/ttyS0 crtscts
# 소프트웨어 흐름 제어 (XON/XOFF)
stty -F /dev/ttyS0 ixon ixoff
RS-485 모드
RS-485는 산업용 half-duplex 직렬 통신 표준으로, 하나의 버스에 여러 디바이스를 연결합니다. 리눅스 커널은 serial_rs485 구조체와 TIOCSRS485 ioctl을 통해 RS-485 모드를 지원합니다.
#include <linux/serial.h>
/* user space에서 RS-485 모드 활성화 */
struct serial_rs485 rs485conf = {
.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
.delay_rts_before_send = 0, /* TX 시작 전 RTS 지연 (ms) */
.delay_rts_after_send = 0, /* TX 완료 후 RTS 지연 (ms) */
};
ioctl(fd, TIOCSRS485, &rs485conf);
/* 커널 UART 드라이버에서 RS-485 지원:
* uart_port.rs485_config() 콜백 구현 필요.
* RTS 핀을 TX enable로 사용하여 송신 시 RTS 활성화,
* 수신 시 RTS 비활성화하여 트랜시버 방향 제어.
*
* Device Tree 설정 예:
* &uart1 {
* linux,rs485-enabled-at-boot-time;
* rs485-rts-delay = <0 0>;
* rs485-rts-active-low; // RTS 극성 반전
* };
*/
TTY/Serial 디버깅
# ─── 시스템 정보 확인 ───
# 등록된 TTY 드라이버 목록
cat /proc/tty/drivers
# /dev/tty /dev/tty 5 0 system:/dev/tty
# /dev/console /dev/console 5 1 system:console
# /dev/ptmx /dev/ptmx 5 2 system
# serial /dev/ttyS 4 64-67 serial
# pty_slave /dev/pts 136 0-... pty:slave
# pty_master /dev/ptm 128 0-... pty:master
# 활성 TTY 라인 정보
cat /proc/tty/line_disc
# n_tty 0
# 시리얼 포트 하드웨어 정보
cat /proc/tty/driver/serial
# serinfo:1.0 driver revision:
# 0: uart:16550A port:000003F8 irq:4 tx:0 rx:0
# 1: uart:16550A port:000002F8 irq:3 tx:0 rx:0
# setserial로 시리얼 포트 상세 정보
setserial -g /dev/ttyS0
# /dev/ttyS0, UART: 16550A, Port: 0x03f8, IRQ: 4
# ─── 디바이스 테스트 ───
# minicom 또는 picocom으로 시리얼 통신
minicom -D /dev/ttyS0 -b 115200
picocom --baud 115200 /dev/ttyS0
# 간단한 시리얼 송수신 테스트
echo "hello" > /dev/ttyS0 # 데이터 송신
cat /dev/ttyS0 # 데이터 수신 (blocking)
dd if=/dev/ttyS0 bs=1 count=10 # 10바이트만 수신
# ─── 커널 디버깅 ───
# TTY 관련 커널 로그
dmesg | grep -i -E 'tty|serial|uart'
# [ 0.000000] printk: console [tty0] enabled
# [ 0.524130] serial8250: ttyS0 at I/O 0x3f8 (irq = 4, base_baud = 115200)
# [ 1.234567] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0
# dynamic debug로 serial_core 트레이싱
echo 'module serial_core +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module 8250_core +p' > /sys/kernel/debug/dynamic_debug/control
# UART 포트 통계 (인터럽트 카운터)
cat /proc/interrupts | grep -i serial
# 4: 128 IO-APIC 4-edge serial
# sysfs를 통한 UART 정보
ls /sys/class/tty/ttyS0/
# close_delay closing_wait custom_divisor io_type iomem_base
# iomem_reg_shift irq line port type uartclk xmit_fifo_size
cat /sys/class/tty/ttyS0/uartclk # UART 기본 클럭
cat /sys/class/tty/ttyS0/type # UART 타입 (16550A=4)
# ─── PTY 정보 ───
# 현재 열린 PTY 확인
ls /dev/pts/
# 0 1 2 ptmx
# 자신의 터미널 확인
tty
# /dev/pts/0
# 프로세스별 controlling terminal
ps -eo pid,tty,comm | head -20
uart_state->xmit)를 통해 이루어집니다. start_tx()가 TX empty 인터럽트를 활성화하면, ISR에서 uart_circ_chars_pending()으로 남은 데이터를 확인하고 FIFO에 채워넣습니다. 버퍼가 비면 uart_write_wakeup()을 호출하여 대기 중인 write()를 깨우고, 전송 완료 시 stop_tx()로 인터럽트를 끕니다.
/* TX 인터럽트 핸들러 패턴 */
static void my_handle_tx(struct uart_port *port)
{
struct tty_port *tport = &port->state->port;
unsigned int pending;
u8 ch;
/* x_char (XON/XOFF) 우선 송신 */
if (port->x_char) {
writel(port->x_char, port->membase + REG_DATA);
port->icount.tx++;
port->x_char = 0;
return;
}
/* pending 데이터를 FIFO에 채워넣기 */
pending = kfifo_len(&tport->xmit_fifo);
if (pending == 0 || uart_tx_stopped(port)) {
my_stop_tx(port);
return;
}
while (readl(port->membase + REG_STATUS) & TX_FIFO_NOT_FULL) {
if (!kfifo_get(&tport->xmit_fifo, &ch))
break;
writel(ch, port->membase + REG_DATA);
port->icount.tx++;
}
/* 버퍼 여유 생기면 write() 대기 프로세스 깨우기 */
if (kfifo_len(&tport->xmit_fifo) < WAKEUP_CHARS)
uart_write_wakeup(port);
/* 모든 데이터 전송 완료 시 TX 인터럽트 끄기 */
if (kfifo_is_empty(&tport->xmit_fifo))
my_stop_tx(port);
}
- ISR(Interrupt Service Routine) 컨텍스트 — UART 인터럽트 핸들러는 hard IRQ 컨텍스트에서 실행됩니다. sleep, mutex, GFP_KERNEL 할당 불가.
spin_lock(&port->lock)으로start_tx/stop_tx와의 경쟁 보호 - flip buffer 크기 —
TTY_BUFFER_PAGE(4KB) 단위로 할당됩니다. 고속 통신에서 ISR이 지연되면 데이터 손실 발생 가능. DMA 전송 사용 권장 - hangup 경쟁 — USB-Serial 언플러그 시
hangup()과write()가 동시에 호출될 수 있음.tty_port_hangup()사용으로 안전한 처리 보장 - DMA 전송 — 고속 UART에서는 PIO(Programmed I/O) 대신 DMA 사용 권장.
tty_prepare_flip_string()으로 직접 DMA 타겟 버퍼 획득 가능 - 콘솔 write 경로 —
console_write()는 printk에서 호출되므로 NMI, panic 등 어떤 컨텍스트에서든 안전해야 합니다.spin_trylock()사용 필수 - suspend/resume —
uart_suspend_port()/uart_resume_port()사용. 진행 중인 DMA 전송 중지, TX FIFO drain 대기, 클럭 재설정 등 순서 준수 필수
8250/16550 드라이버 — 가장 보편적인 UART
8250/16550 호환 UART는 PC 시리얼 포트의 사실상 표준입니다. 리눅스 커널의 drivers/tty/serial/8250/ 디렉터리에 구현되어 있으며, PCI, ACPI, Device Tree, ISA 등 다양한 열거 방식을 지원합니다.
| 레지스터 | 오프셋 | 읽기 용도 | 쓰기 용도 |
|---|---|---|---|
| RBR/THR | 0x00 | 수신 데이터 (RBR) | 송신 데이터 (THR) |
| IER | 0x01 | 인터럽트 활성화 (RX, TX, Line Status, Modem Status) | |
| IIR/FCR | 0x02 | 인터럽트 식별 (IIR) | FIFO 제어 (FCR) |
| LCR | 0x03 | Line Control (데이터 비트, 정지 비트, 패리티, DLAB) | |
| MCR | 0x04 | Modem Control (DTR, RTS, loopback) | |
| LSR | 0x05 | Line Status (Data Ready, Overrun, Parity Err, TX Empty) | |
| MSR | 0x06 | Modem Status (CTS, DSR, RI, DCD 변화 감지) | |
| SCR | 0x07 | Scratch Register (UART 존재 감지용) | |
| DLL/DLM | 0x00/0x01 | Divisor Latch (DLAB=1일 때, 보레이트 설정) | |
/* 8250 포트를 수동으로 등록하는 예 (레거시/커스텀 보드) */
#include <linux/serial_8250.h>
static struct plat_serial8250_port my_8250_data[] = {
{
.mapbase = 0x3F8, /* COM1 물리 주소 */
.irq = 4,
.uartclk = 1843200, /* 1.8432 MHz 기본 클럭 */
.iotype = UPIO_PORT, /* x86 I/O 포트 접근 */
.flags = UPF_SKIP_TEST | UPF_BOOT_AUTOCONF,
.regshift = 0, /* 레지스터 간격: 1바이트 */
},
{ }, /* 터미네이터 */
};
/* 보레이트 계산:
* divisor = uartclk / (16 × baud_rate)
* 115200 baud: 1843200 / (16 × 115200) = 1
* 9600 baud: 1843200 / (16 × 9600) = 12
*/
| 서브시스템 | 주요 드라이버 | 디버깅 도구 |
|---|---|---|
| Input | gpio-keys, atkbd, hid-* | evtest, libinput debug-events |
| USB | xhci-hcd, ehci-hcd, usb-storage | lsusb -v, usbmon |
| V4L2 | uvcvideo, vivid | v4l2-ctl, media-ctl |
| DRM | i915, amdgpu, nouveau | modetest, drm_info |
| ALSA | snd-hda-intel, snd-usb-audio | aplay -l, alsamixer |
| Serial | 8250, pl011, imx-uart | minicom, stty |
USB Serial 서브시스템
USB-Serial 변환기(FTDI, CP210x, CH341, PL2303 등)는 drivers/usb/serial/에 구현됩니다. USB Serial 서브시스템은 USB 버스 위에 TTY 인터페이스를 제공하는 중간 계층으로, usb_serial_driver 구조체를 통해 각 변환 칩에 맞는 콜백을 등록합니다.
#include <linux/usb.h>
#include <linux/usb/serial.h>
/* USB Serial 드라이버 구조체 — 각 변환 칩 드라이버가 등록 */
static struct usb_serial_driver my_usb_serial_device = {
.driver = {
.owner = THIS_MODULE,
.name = "my-usb-serial",
},
.description = "My USB Serial Adapter",
.id_table = my_id_table, /* USB VID/PID 매칭 테이블 */
.num_ports = 1, /* 포트 수 */
/* 포트 초기화/해제 */
.port_probe = my_port_probe,
.port_remove = my_port_remove,
/* TTY operations 매핑 */
.open = my_open, /* DTR/RTS 활성화, bulk URB 제출 */
.close = my_close, /* URB 취소, DTR/RTS 비활성화 */
.write = my_write, /* 데이터 → bulk OUT URB */
.write_room = my_write_room,
/* termios → 칩 레지스터 변환 */
.set_termios = my_set_termios, /* 보레이트, 데이터비트 등 */
/* 모뎀 제어 */
.tiocmget = my_tiocmget,
.tiocmset = my_tiocmset,
.dtr_rts = my_dtr_rts,
/* break 신호 */
.break_ctl = my_break_ctl,
/* 수신 데이터 처리 (bulk IN 완료 콜백에서 호출) */
.process_read_urb = my_process_read_urb,
/* USB suspend/resume */
.suspend = my_suspend,
.resume = my_resume,
};
static const struct usb_device_id my_id_table[] = {
{ USB_DEVICE(0x1234, 0x5678) }, /* VID, PID */
{ }
};
MODULE_DEVICE_TABLE(usb, my_id_table);
static struct usb_serial_driver * const serial_drivers[] = {
&my_usb_serial_device, NULL
};
module_usb_serial_driver(serial_drivers, my_id_table);
/* === 수신 콜백: bulk IN URB 완료 → flip buffer → ldisc === */
static void my_process_read_urb(struct urb *urb)
{
struct usb_serial_port *port = urb->context;
unsigned char *data = urb->transfer_buffer;
int len = urb->actual_length;
if (len == 0)
return;
/* 칩별 상태 바이트 처리 (FTDI: 2바이트 헤더 스킵) */
/* data += header_size; len -= header_size; */
tty_insert_flip_string(&port->port, data, len);
tty_flip_buffer_push(&port->port);
}
/* === 송신: write() → bulk OUT URB 제출 === */
static int my_write(struct tty_struct *tty,
struct usb_serial_port *port,
const unsigned char *buf, int count)
{
struct urb *urb;
int result;
urb = usb_alloc_urb(0, GFP_ATOMIC);
if (!urb)
return -ENOMEM;
memcpy(port->write_urbs[0]->transfer_buffer, buf, count);
usb_fill_bulk_urb(urb, port->serial->dev,
usb_sndbulkpipe(port->serial->dev,
port->bulk_out_endpointAddress),
port->write_urbs[0]->transfer_buffer, count,
my_write_callback, port);
result = usb_submit_urb(urb, GFP_ATOMIC);
if (result)
dev_err(&port->dev, "URB submit failed: %d\n", result);
return count;
}
| 변환 칩 | 드라이버 | 보레이트 범위 | GPIO | 특이사항 |
|---|---|---|---|---|
| FTDI FT232R | ftdi_sio | 300 ~ 3M | CBUS GPIO | 가장 호환성 좋음, bitbang 모드 지원 |
| FTDI FT2232H | ftdi_sio | 300 ~ 12M | CBUS | 듀얼 포트, MPSSE(SPI/I2C/JTAG) |
| CP2102/CP2104 | cp210x | 300 ~ 2M | GPIO | Silicon Labs, 소형 패키지 |
| CH340/CH341 | ch341 | 50 ~ 2M | - | 가장 저렴, 중국산 보드에 다수 |
| PL2303TA | pl2303 | 75 ~ 6M | - | Prolific, 구형 위조칩 이슈 |
| CDC ACM | cdc-acm | 장치 의존 | - | 표준 USB 클래스, Arduino/STM32 사용 |
| MCP2221A | hid | 300 ~ 460K | GPIO/ADC/DAC | HID 기반 (비표준 경로) |
tty_hangup()이 비동기로 호출됩니다. 진행 중인 URB는 usb_kill_urb()로 취소해야 하며, disconnect() 콜백에서 모든 자원을 안전하게 해제해야 합니다. port->port.count가 0이 아닌 상태에서 분리되면 -EIO가 반환되며, 사용자 프로세스는 SIGHUP을 수신합니다.
# USB Serial 디버깅 명령
lsusb -v -d 0403:6001 # FTDI 디바이스 상세 정보
dmesg | grep -i 'ttyUSB\|usb.*serial\|ftdi\|cp210x\|ch341'
# udev 규칙으로 고정 디바이스 이름 부여
# /etc/udev/rules.d/99-serial.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", \
ATTRS{idProduct}=="6001", ATTRS{serial}=="A12345", \
SYMLINK+="ttyMYDEVICE"
# USB Serial 트레이싱
echo 1 > /sys/kernel/debug/usb/usbmon/1u # bus 1 모니터링
cat /sys/kernel/debug/usb/usbmon/1u # URB 트래픽 확인
serdev — Serial Device Bus
serdev(Serial Device Bus)는 커널 5.x에서 도입된 새로운 시리얼 디바이스 프레임워크입니다. 기존에는 시리얼 장치(Bluetooth HCI, GPS, NFC 등)를 사용하려면 user space에서 TTY를 열고 line discipline을 설정해야 했지만, serdev를 사용하면 커널 내에서 직접 시리얼 장치를 드라이버로 바인딩할 수 있습니다.
#include <linux/serdev.h>
/* serdev 클라이언트 드라이버 예제 — Bluetooth HCI over UART */
/* 수신 콜백: UART에서 데이터가 도착하면 호출 */
static size_t my_serdev_receive(struct serdev_device *serdev,
const u8 *data, size_t count)
{
struct my_device *dev = serdev_device_get_drvdata(serdev);
/* 프로토콜 파싱 — 수신된 바이트 처리 */
return my_protocol_parse(dev, data, count);
}
/* 송신 완료 콜백 */
static void my_serdev_write_wakeup(struct serdev_device *serdev)
{
struct my_device *dev = serdev_device_get_drvdata(serdev);
schedule_work(&dev->tx_work);
}
static const struct serdev_device_ops my_serdev_ops = {
.receive_buf = my_serdev_receive,
.write_wakeup = my_serdev_write_wakeup,
};
static int my_serdev_probe(struct serdev_device *serdev)
{
struct my_device *dev;
int ret;
dev = devm_kzalloc(&serdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->serdev = serdev;
serdev_device_set_drvdata(serdev, dev);
serdev_device_set_client_ops(serdev, &my_serdev_ops);
/* 보레이트, 흐름제어 설정 */
ret = serdev_device_open(serdev);
if (ret)
return ret;
serdev_device_set_baudrate(serdev, 3000000); /* 3Mbps */
serdev_device_set_flow_control(serdev, true); /* RTS/CTS */
serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
/* 동기식 데이터 송신 */
ret = serdev_device_write_buf(serdev, init_cmd, sizeof(init_cmd));
return 0;
}
static void my_serdev_remove(struct serdev_device *serdev)
{
serdev_device_close(serdev);
}
/* Device Tree 바인딩:
* &uart2 {
* bluetooth {
* compatible = "vendor,my-bt";
* max-speed = <3000000>;
* };
* };
*/
static const struct of_device_id my_serdev_of_match[] = {
{ .compatible = "vendor,my-bt" },
{ }
};
static struct serdev_device_driver my_serdev_driver = {
.probe = my_serdev_probe,
.remove = my_serdev_remove,
.driver = {
.name = "my-serdev",
.of_match_table = my_serdev_of_match,
},
};
module_serdev_device_driver(my_serdev_driver);
hci_serdev.c (Bluetooth HCI UART), gnss-serial.c (GNSS/GPS), nxp-nci-uart.c (NFC), qca_uart.c (Qualcomm BT) 등이 serdev를 사용합니다. 새로운 시리얼 연결 장치는 line discipline 대신 serdev로 구현하는 것이 권장됩니다.
TTY의 역사와 진화
TTY 서브시스템은 리눅스 커널에서 가장 긴 역사를 가진 계층입니다. 1960년대 물리적 텔레타이프 기계에서 시작하여, 현대의 컨테이너 터미널과 SSH까지 이어지는 진화 과정을 이해하면 현재 코드의 복잡성이 왜 존재하는지 납득할 수 있습니다.
| 시대 | 기술 | 핵심 변화 | 커널 영향 |
|---|---|---|---|
| 1960s | Teletype ASR-33 | 기계식 타자기 ↔ 컴퓨터 직렬 연결 | "TTY" 용어의 기원, 20mA 전류 루프 |
| 1970s | VT100 / UNIX V7 | CRT 단말기, 커서 제어(ANSI escape), stty 도입 | line discipline 개념 확립, struct termio |
| 1984 | POSIX.1 termios | 터미널 I/O 표준화, tcgetattr/tcsetattr | struct termios → 커널 ktermios |
| 1991 | Linux 0.01 | UNIX TTY 계층을 PC용으로 재구현 | 8250 드라이버, /dev/ttyS0, VT 콘솔 |
| 1998 | UNIX98 PTY | ptmx/pts 동적 할당 → 고정 pty 디바이스 제거 | /dev/ptmx + /dev/pts/N |
| 2004 | devpts filesystem | PTY 네임스페이스 분리 (컨테이너 기반) | newinstance 마운트 옵션 |
| 2017 | serdev | 커널 내 시리얼 디바이스 직접 바인딩 | Bluetooth HCI, GNSS 등 ldisc 불필요 |
| 현재 | TTY 리팩토링 | kfifo TX 버퍼, tty_port 통합, locking 정리 | uart_circ_buf → kfifo 전환 진행 중 |
TTY open/close 수명주기
TTY 디바이스를 open()하면 커널 내부에서 복잡한 초기화 체인이 실행됩니다. tty_open() → tty_init_dev() → driver->ops->open() → tty_port_open() 순서로 진행되며, 첫 open 시에만 하드웨어를 활성화하고, 이후 open은 참조 카운터만 증가시킵니다.
/* tty_port_operations — 포트 수명주기 콜백 */
static const struct tty_port_operations my_port_ops = {
/* 첫 open 시 호출 — 하드웨어 활성화 */
.activate = my_port_activate,
/* 마지막 close 시 호출 — 하드웨어 비활성화 */
.shutdown = my_port_shutdown,
/* carrier detect 상태 반환 (DCD) */
.carrier_raised = my_carrier_raised,
/* DTR/RTS 제어 */
.dtr_rts = my_dtr_rts,
/* 포트 해제 (kref → 0) */
.destruct = my_port_destruct,
};
/* activate: 첫 번째 open()에서만 호출 */
static int my_port_activate(struct tty_port *port,
struct tty_struct *tty)
{
struct uart_port *uport = container_of(...);
/* 클럭 활성화 */
clk_prepare_enable(priv->clk);
/* FIFO 리셋 */
writel(FCR_FIFO_EN | FCR_RX_RESET | FCR_TX_RESET,
uport->membase + REG_FCR);
/* 인터럽트 등록 및 활성화 */
request_irq(uport->irq, my_uart_irq, IRQF_SHARED,
"my-uart", uport);
writel(IER_RX_DATA, uport->membase + REG_IER);
return 0;
}
/* shutdown: 마지막 close()에서만 호출 */
static void my_port_shutdown(struct tty_port *port)
{
struct uart_port *uport = container_of(...);
/* TX FIFO drain 대기 */
while (!(readl(uport->membase + REG_STATUS) & TX_FIFO_EMPTY))
cpu_relax();
/* 인터럽트 비활성화 및 해제 */
writel(0, uport->membase + REG_IER);
free_irq(uport->irq, uport);
/* 클럭 비활성화 */
clk_disable_unprepare(priv->clk);
}
TTY 잠금과 동기화 모델
TTY 서브시스템은 여러 컨텍스트(user process, IRQ, workqueue, hangup work)에서 동시에 접근되므로 정교한 잠금 계층이 필요합니다. 잠금 순서를 위반하면 데드락이 발생하므로, 커널 문서(Documentation/driver-api/serial/driver.rst)에 명시된 순서를 반드시 준수해야 합니다.
| 잠금 | 종류 | 보호 대상 | 사용 컨텍스트 |
|---|---|---|---|
tty_mutex | 전역 mutex | tty_driver 목록, tty 생성/삭제 | 프로세스 |
tty->legacy_mutex | per-tty mutex | read/write/ioctl 직렬화 | 프로세스 |
port->mutex | per-port mutex | open/close count, 상태 전환 | 프로세스 |
tty->ldisc_sem | rw_semaphore | line discipline 교체 vs 사용 | 프로세스 |
port->lock | spinlock | 하드웨어 레지스터, icount, 상태 플래그 | 프로세스 + IRQ |
port->buf_mutex | mutex | flip buffer 할당/해제 | 프로세스 |
xmit_fifo | kfifo | TX circular buffer (단일 생산자/소비자) | lock-free |
/* 잠금 사용 패턴 예시 */
/* 1. ISR에서: spin_lock만 사용 (sleep 불가) */
static irqreturn_t uart_irq(int irq, void *dev_id)
{
struct uart_port *port = dev_id;
unsigned long flags;
spin_lock_irqsave(&port->lock, flags);
/* icount 업데이트, 레지스터 접근 */
port->icount.rx++;
spin_unlock_irqrestore(&port->lock, flags);
return IRQ_HANDLED;
}
/* 2. set_termios에서: port->lock으로 레지스터 보호 */
static void my_set_termios(...)
{
spin_lock_irq(&port->lock);
/* 보레이트 divisor, LCR 레지스터 업데이트 */
writel(divisor, port->membase + REG_BAUD_DIV);
spin_unlock_irq(&port->lock);
}
/* 3. hangup에서: 잠금 순서 준수
* tty->legacy_mutex → port->mutex → port->lock */
static void my_hangup(struct tty_struct *tty)
{
struct uart_port *uport = ...;
mutex_lock(&uport->state->port.mutex);
/* shutdown, 상태 초기화 */
tty_port_hangup(&uport->state->port);
mutex_unlock(&uport->state->port.mutex);
}
데이터 흐름 상세 — Write/Read 경로
TTY 서브시스템의 데이터 흐름은 Write(송신)과 Read(수신) 두 방향으로 나뉘며, 각각 다른 버퍼링과 동기화 메커니즘을 사용합니다. 전체 경로를 이해하면 지연(latency), 처리량(throughput), 데이터 손실의 원인을 정확히 진단할 수 있습니다.
| 버퍼 단계 | 크기 | 위치 | 오버플로 시 |
|---|---|---|---|
| HW RX FIFO | 1~128 바이트 (UART 모델 의존) | UART 하드웨어 | Overrun Error (LSR bit 1) |
| Flip Buffer | 4KB 페이지 단위, 동적 체인 | 커널 메모리 (tty_buffer) | 메모리 부족 시 데이터 손실 |
| N_TTY read_buf | 4096 바이트 (N_TTY_BUF_SIZE) | ldisc 내부 | throttle() → RTS 비활성화 |
| xmit_fifo (TX) | PAGE_SIZE (4096) | 커널 kfifo | write() 블로킹 또는 부분 쓰기 |
| HW TX FIFO | 1~128 바이트 | UART 하드웨어 | TX ISR에서 채움 |
N_TTY 내부 동작 — Canonical/Raw/CBREAK
N_TTY는 기본 line discipline으로, 거의 모든 터미널 세션에서 사용됩니다. 세 가지 주요 모드를 제공하며, 각 모드는 입력 처리 방식을 근본적으로 바꿉니다.
/* VMIN/VTIME 조합 — Non-canonical 모드 read() 동작
*
* VMIN=0, VTIME=0 : 폴링 — 즉시 반환 (데이터 없으면 0 반환)
* VMIN>0, VTIME=0 : 블로킹 — VMIN 바이트 수신될 때까지 대기
* VMIN=0, VTIME>0 : 타이머 — VTIME*0.1초 내 데이터 또는 타임아웃
* VMIN>0, VTIME>0 : 혼합 — 첫 바이트 후 VTIME 내 VMIN 바이트 대기
*/
/* cfmakeraw() 내부 구현 (glibc) */
void cfmakeraw(struct termios *t)
{
t->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
INLCR | IGNCR | ICRNL | IXON);
t->c_oflag &= ~OPOST;
t->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
t->c_cflag &= ~(CSIZE | PARENB);
t->c_cflag |= CS8;
t->c_cc[VMIN] = 1;
t->c_cc[VTIME] = 0;
}
/* N_TTY 핵심 경로: receive_buf → canonical 처리 */
/* drivers/tty/n_tty.c — n_tty_receive_buf_common() */
/*
* 1. 각 바이트에 대해:
* - ISTRIP: 8번째 비트 제거
* - IGNCR: CR 무시 / ICRNL: CR→LF 변환
* - INLCR: LF→CR 변환
* - IUCLC: 대문자→소문자 변환
*
* 2. ISIG가 설정된 경우:
* - VINTR(^C) → SIGINT
* - VQUIT(^\) → SIGQUIT
* - VSUSP(^Z) → SIGTSTP
* - VDSUSP(^Y) → SIGTSTP (delayed)
*
* 3. ICANON이 설정된 경우:
* - VERASE(^?) → 마지막 문자 삭제
* - VKILL(^U) → 줄 전체 삭제
* - VWERASE(^W) → 마지막 단어 삭제
* - VEOF(^D) → read() 반환 (빈 줄이면 EOF)
* - VEOL/VEOL2 → 줄 종료
* - '\n' → 줄 종료 + 버퍼 → read() 전달
*
* 4. ECHO가 설정된 경우:
* - 일반 문자: 그대로 출력
* - 제어 문자: ^X 형식으로 출력
* - ECHOE: 백스페이스 시 "BS SP BS" 시퀀스
* - ECHOCTL: 제어 문자 ^X 표시
* - ECHOKE: kill 문자 시 전체 줄 지우기
*/
세션, 프로세스 그룹, 제어 터미널
TTY 서브시스템은 단순한 I/O를 넘어 프로세스 제어(Job Control)의 핵심 역할을 합니다. 로그인 셸은 세션 리더(session leader)로서 제어 터미널을 획득하고, foreground/background 프로세스 그룹을 관리합니다. 제어 터미널이 끊어지면(hangup) 세션 내 모든 프로세스에 SIGHUP이 전달됩니다.
/* 세션/프로세스 그룹 관련 커널 구조 */
/* task_struct → signal_struct에서 세션/그룹 추적 */
struct signal_struct {
struct pid *leader_pid; /* 세션 리더 PID */
struct pid *tty_old_pgrp; /* orphan pgrp SIGHUP용 */
struct tty_struct *tty; /* 제어 터미널 */
/* ... */
};
/* tty_struct에서 세션/그룹 추적 */
struct tty_struct {
struct pid *session; /* 이 TTY를 소유한 세션 */
struct pid *pgrp; /* foreground 프로세스 그룹 */
/* ... */
};
/* Hangup 시퀀스 (DCD 손실, USB 분리, kill -HUP):
*
* 1. tty_vhangup() 또는 tty_hangup() 호출
* 2. __tty_hangup() 실행:
* a. ldisc->flush_buffer() — 버퍼 비우기
* b. driver->hangup() — 하드웨어 정리
* c. 세션의 모든 프로세스에 SIGHUP 전송
* d. 세션의 모든 프로세스에 SIGCONT 전송 (stopped면)
* e. tty 참조 해제
*
* 3. 세션 리더 종료 시:
* a. foreground pgrp에 SIGHUP + SIGCONT
* b. disassociate_ctty() → 제어 터미널 해제
*/
/* user space에서 세션/터미널 관리 */
/* setsid() — 새 세션 생성 (제어 터미널 없음) */
/* ioctl(fd, TIOCSCTTY, 0) — 제어 터미널 획득 */
/* ioctl(fd, TIOCNOTTY) — 제어 터미널 포기 */
/* tcsetpgrp(fd, pgrp) — foreground 그룹 변경 */
/* tcgetpgrp(fd) — foreground 그룹 조회 */
# 세션/프로세스 그룹 확인
ps -eo pid,pgid,sid,tty,comm | head -20
# PID PGID SID TT COMMAND
# 1 1 1 ? systemd
# 1234 1234 1234 pts/0 bash ← 세션 리더
# 5678 5678 1234 pts/0 vim ← fg 그룹
# 9012 9012 1234 pts/0 make ← bg 그룹
# nohup — 세션 분리 (SIGHUP 무시)
nohup long_running_cmd &
# SIGHUP 수신 시에도 프로세스 유지
# stdout → nohup.out 리디렉션
# disown — bash 내장 (job table에서 제거)
long_running_cmd &
disown %1
# bash 종료 시 SIGHUP 전송 대상에서 제거
RS-232 신호와 커넥터
RS-232(EIA-232)는 DTE(Data Terminal Equipment, 예: PC)와 DCE(Data Communication Equipment, 예: 모뎀) 사이의 직렬 통신 표준입니다. 전압 레벨(-3V~-15V = logic 1, +3V~+15V = logic 0), 커넥터 핀 배치, 제어 신호를 규정합니다.
| 신호 | DB-9 핀 | 방향 | Linux 상수 | 역할 |
|---|---|---|---|---|
| TXD | 3 | DTE→DCE | - | 송신 데이터 |
| RXD | 2 | DCE→DTE | - | 수신 데이터 |
| RTS | 7 | DTE→DCE | TIOCM_RTS | 송신 요청 (HW 흐름 제어 시 "수신 가능" 의미로도 사용) |
| CTS | 8 | DCE→DTE | TIOCM_CTS | 송신 허가 (DCE가 수신 준비 완료) |
| DTR | 4 | DTE→DCE | TIOCM_DTR | 단말기 준비 (open 시 활성화, close 시 비활성화) |
| DSR | 6 | DCE→DTE | TIOCM_DSR | 데이터셋 준비 (모뎀 전원 켜짐) |
| DCD | 1 | DCE→DTE | TIOCM_CAR | 캐리어 감지 (통신 연결 수립됨, CLOCAL 해제 시 의미 있음) |
| RI | 9 | DCE→DTE | TIOCM_RNG | 링 표시 (전화 벨 울림) |
| GND | 5 | - | - | 신호 접지 (공통 기준점) |
CLOCAL 플래그를 설정하면 DCD 없이도 포트를 열 수 있습니다.
흐름 제어 메커니즘
흐름 제어(Flow Control)는 수신측이 처리 속도보다 빠르게 데이터가 도착할 때 데이터 손실을 방지하는 메커니즘입니다. 하드웨어(RTS/CTS)와 소프트웨어(XON/XOFF) 두 가지 방식이 있으며, 환경에 따라 선택합니다.
| 항목 | Hardware (RTS/CTS) | Software (XON/XOFF) |
|---|---|---|
| 설정 | stty crtscts | stty ixon ixoff |
| 필요 배선 | TXD, RXD, RTS, CTS, GND (5선) | TXD, RXD, GND (3선) |
| 바이너리 안전 | 예 (제어 신호가 별도 핀) | 아니오 (0x11, 0x13이 예약됨) |
| 반응 속도 | 매우 빠름 (하드웨어 레벨) | 느림 (데이터 경로로 전달) |
| 커널 처리 | uart_ops->set_mctrl() | port->x_char 메커니즘 |
| N_TTY 관련 | throttle()/unthrottle() | N_TTY가 XON/XOFF 자동 처리 |
| 적합 용도 | 고속 통신, 바이너리 전송 | 텍스트 터미널, 레거시 장비 |
/* 커널에서 throttle/unthrottle 처리 (drivers/tty/tty_ioctl.c) */
/* N_TTY read_buf가 가득 차면 호출 */
void tty_throttle(struct tty_struct *tty)
{
mutex_lock(&tty->throttle_mutex);
if (!tty_throttled(tty)) {
if (tty->ops->throttle)
tty->ops->throttle(tty); /* → UART 드라이버 */
set_bit(TTY_THROTTLED, &tty->flags);
}
mutex_unlock(&tty->throttle_mutex);
}
/* UART 드라이버의 throttle 구현 예 */
static void my_throttle(struct tty_struct *tty)
{
struct uart_port *port = ...;
if (C_CRTSCTS(tty)) {
/* HW 흐름 제어: RTS 비활성화 → DCE가 송신 중지 */
u32 mcr = readl(port->membase + REG_MCR);
mcr &= ~MCR_RTS;
writel(mcr, port->membase + REG_MCR);
}
if (I_IXOFF(tty)) {
/* SW 흐름 제어: XOFF(0x13) 전송 */
port->x_char = STOP_CHAR(tty); /* 다음 TX ISR에서 전송 */
my_start_tx(port);
}
}
DMA 기반 UART 전송
고속 시리얼 통신(1Mbps 이상)에서는 PIO(Programmed I/O, 바이트 단위 ISR 처리)가 CPU 부하의 병목이 됩니다. DMA(Direct Memory Access)를 사용하면 CPU 개입 없이 대량의 데이터를 메모리↔UART 간에 직접 전송할 수 있습니다.
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
/* DMA RX 설정 — circular buffer + 타이머 기반 */
static int my_uart_dma_rx_setup(struct my_uart_priv *priv)
{
struct dma_slave_config cfg = {
.direction = DMA_DEV_TO_MEM,
.src_addr = priv->phys_base + REG_DATA,
.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.src_maxburst = 16, /* FIFO 트리거 레벨과 일치 */
};
priv->rx_chan = dma_request_chan(&priv->pdev->dev, "rx");
if (IS_ERR(priv->rx_chan))
return PTR_ERR(priv->rx_chan);
dmaengine_slave_config(priv->rx_chan, &cfg);
/* DMA 버퍼 할당 (coherent — cache 동기화 불필요) */
priv->rx_buf = dma_alloc_coherent(&priv->pdev->dev,
RX_DMA_BUF_SIZE,
&priv->rx_dma_addr,
GFP_KERNEL);
/* Circular DMA descriptor 준비 */
priv->rx_desc = dmaengine_prep_dma_cyclic(
priv->rx_chan,
priv->rx_dma_addr,
RX_DMA_BUF_SIZE,
RX_DMA_BUF_SIZE / 2, /* period: 반 버퍼마다 콜백 */
DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
priv->rx_desc->callback = my_dma_rx_callback;
priv->rx_desc->callback_param = priv;
dmaengine_submit(priv->rx_desc);
dma_async_issue_pending(priv->rx_chan);
/* idle 타이머: DMA period 미달 시 잔여 데이터 처리 */
timer_setup(&priv->rx_timer, my_dma_rx_timer, 0);
return 0;
}
/* DMA RX 콜백 — period 완료 시 호출 */
static void my_dma_rx_callback(void *param)
{
struct my_uart_priv *priv = param;
struct tty_port *tport = &priv->port.state->port;
size_t count;
/* DMA 위치에서 새로 수신된 데이터량 계산 */
struct dma_tx_state state;
dmaengine_tx_status(priv->rx_chan, priv->rx_cookie, &state);
count = RX_DMA_BUF_SIZE - state.residue - priv->rx_pos;
if (count > 0) {
tty_insert_flip_string(tport,
priv->rx_buf + priv->rx_pos, count);
tty_flip_buffer_push(tport);
priv->rx_pos = (priv->rx_pos + count) % RX_DMA_BUF_SIZE;
}
/* idle 타이머 리셋 */
mod_timer(&priv->rx_timer, jiffies + msecs_to_jiffies(10));
}
/* DMA TX — scatter-gather 전송 */
static void my_dma_start_tx(struct my_uart_priv *priv)
{
struct tty_port *tport = &priv->port.state->port;
struct scatterlist sg[2];
unsigned char *tail;
size_t count;
/* kfifo에서 연속 영역 얻기 (wrap-around 시 2개 SG) */
count = kfifo_dma_out_prepare(&tport->xmit_fifo, sg, 2,
kfifo_len(&tport->xmit_fifo));
if (count == 0)
return;
priv->tx_desc = dmaengine_prep_slave_sg(
priv->tx_chan, sg, count, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT);
priv->tx_desc->callback = my_dma_tx_callback;
priv->tx_desc->callback_param = priv;
dmaengine_submit(priv->tx_desc);
dma_async_issue_pending(priv->tx_chan);
}
- idle 타이머 — DMA는 period 크기만큼 차야 콜백이 옵니다. 마지막 소량의 데이터가 DMA 버퍼에 남아 전달되지 않는 문제가 있으므로, UART idle interrupt 또는 타이머로 잔여 데이터를 처리해야 합니다.
- cache coherency —
dma_alloc_coherent()를 사용하거나,dma_map_single()+dma_sync_*_for_cpu()로 캐시를 명시적으로 동기화해야 합니다. - 에러 처리 — DMA 전송 중 UART 에러(parity, frame, overrun)가 발생하면 DMA를 중지하고 PIO로 폴백하는 경로가 필요합니다.
- suspend — PM suspend 시 진행 중인 DMA를
dmaengine_terminate_sync()로 안전하게 중지해야 합니다.
Virtual Console (VT)
가상 콘솔(VT, Virtual Terminal)은 물리 키보드/디스플레이에 연결된 텍스트 모드 터미널입니다. /dev/tty1~/dev/tty63으로 접근하며, Alt+F1~F6(또는 Ctrl+Alt+F1~F6)으로 전환합니다. VT 콘솔은 프레임버퍼(fbcon) 또는 DRM(drmfb) 위에서 텍스트를 렌더링합니다.
# VT 콘솔 관련 명령
chvt 2 # tty2로 전환 (Alt+F2와 동일)
fgconsole # 현재 foreground VT 번호
deallocvt # 미사용 VT 해제
setfont /usr/share/consolefonts/Lat2-Terminus16.psf # 콘솔 폰트 변경
# VT 개수 제한 (커널 파라미터)
# CONFIG_NR_TTY_DEVICES=63 (기본 최대)
# VT 콘솔에 메시지 출력
echo "Hello VT" > /dev/tty1
# 커널 콘솔 로그 레벨 조정
dmesg -n 4 # WARNING 이상만 콘솔에 출력
echo 8 > /proc/sys/kernel/printk # 모든 레벨 출력
virtio-console과 Serial over LAN
가상화 환경과 원격 서버 관리에서는 물리 시리얼 포트 없이 시리얼 인터페이스를 제공해야 합니다. virtio-console은 KVM/QEMU 게스트에서 호스트와의 고속 시리얼 통신을, Serial over LAN (SOL)은 BMC/IPMI를 통한 원격 콘솔 접근을 제공합니다.
| 기술 | 사용 환경 | 디바이스 | 드라이버 | 특징 |
|---|---|---|---|---|
| virtio-console | KVM/QEMU 게스트 | /dev/hvc0 | virtio_console.c | virtio 링 기반, 멀티포트, 고속 |
| virtio-serial | KVM/QEMU 게스트 | /dev/vport0p1 | virtio_console.c | 범용 데이터 채널 (에이전트 통신용) |
| IPMI SOL | 원격 서버 (BMC) | /dev/ttyS0 (리다이렉트) | BIOS/BMC 펌웨어 | 네트워크 경유, IPMI 2.0 표준 |
| iLO VSP | HPE 서버 | 가상 시리얼 | iLO 펌웨어 | iLO 웹/SSH 접근 |
| DRAC | Dell 서버 | 가상 시리얼 | iDRAC 펌웨어 | racadm 명령 |
| Xen console | Xen 하이퍼바이저 | /dev/hvc0 | hvc_xen.c | Xen 공유 링 |
# QEMU에서 virtio-console 설정
qemu-system-x86_64 \
-device virtio-serial \
-chardev socket,path=/tmp/console.sock,server=on,wait=off,id=char0 \
-device virtconsole,chardev=char0,name=console \
-kernel vmlinuz -append "console=hvc0"
# 호스트에서 접근
socat - UNIX-CONNECT:/tmp/console.sock
# IPMI SOL 접근
ipmitool -I lanplus -H bmc-ip -U admin -P pass sol activate
# 서버 커널 파라미터: console=ttyS0,115200n8
# BIOS에서 COM1 → SOL 리다이렉션 설정 필요
# virtio-console 멀티포트 (QEMU agent 통신용)
qemu-system-x86_64 \
-device virtio-serial \
-chardev socket,path=/tmp/qga.sock,server=on,wait=off,id=qga0 \
-device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0
# 게스트 내: /dev/virtio-ports/org.qemu.guest_agent.0
Device Tree UART 바인딩
임베디드 ARM/RISC-V 시스템에서 UART 포트는 Device Tree를 통해 기술됩니다. DT 바인딩은 포트의 물리 주소, 인터럽트, 클럭, DMA 채널, RS-485 설정 등을 정의합니다.
/* Device Tree UART 바인딩 예시 — 종합 */
/* 1. 기본 UART 노드 */
uart0: serial@1c28000 {
compatible = "allwinner,sun50i-a64-uart",
"snps,dw-apb-uart"; /* fallback */
reg = <0x01c28000 0x400>; /* MMIO 영역 */
reg-shift = <2>; /* 레지스터 간격: 4바이트 */
reg-io-width = <4>; /* 32-bit 접근 */
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_UART0>;
resets = <&ccu RST_BUS_UART0>;
clock-frequency = <24000000>; /* uartclk (없으면 clocks에서 추출) */
status = "okay";
/* 핀 멀티플렉싱 */
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
/* DMA 채널 바인딩 */
dmas = <&dma 6>, <&dma 6>; /* TX, RX */
dma-names = "tx", "rx";
};
/* 2. RS-485 설정 */
uart1: serial@1c28400 {
compatible = "ti,am654-uart";
/* ... 기본 속성 ... */
linux,rs485-enabled-at-boot-time;
rs485-rts-delay = <0 0>; /* before_send, after_send (ms) */
rs485-rts-active-low; /* RTS 극성 반전 */
rs485-rx-during-tx; /* full-duplex RS-485 */
};
/* 3. serdev 자식 노드 (Bluetooth) */
uart2: serial@1c28800 {
compatible = "allwinner,sun50i-a64-uart";
/* ... 기본 속성 ... */
bluetooth {
compatible = "realtek,rtl8723bs-bt";
max-speed = <1500000>; /* 최대 보레이트 */
enable-gpios = <&pio 6 18 GPIO_ACTIVE_HIGH>;
device-wake-gpios = <&pio 6 17 GPIO_ACTIVE_HIGH>;
};
};
/* 4. 시리얼 alias (포트 번호 고정) */
aliases {
serial0 = &uart0; /* /dev/ttyS0 */
serial1 = &uart1; /* /dev/ttyS1 */
serial2 = &uart2; /* /dev/ttyS2 */
};
/* 5. stdout-path (earlycon/콘솔 자동 감지) */
chosen {
stdout-path = "serial0:115200n8";
/* 형식: "serial별명:보레이트{패리티}{데이터비트}{흐름제어}" */
/* 예: "serial0:115200n8r" (r=RTS/CTS) */
};
전원 관리 — Suspend/Resume/Wakeup
UART 드라이버의 전원 관리는 시스템 suspend(S3/S4)와 runtime PM 두 차원에서 이루어집니다. 시리얼 포트가 wakeup 소스로 설정되면 suspend 상태에서도 수신 데이터가 시스템을 깨울 수 있습니다.
/* UART PM 구현 핵심 */
/* System suspend */
static int my_uart_suspend(struct device *dev)
{
struct my_uart_priv *priv = dev_get_drvdata(dev);
/* serial_core가 TX drain, ldisc flush 등을 처리 */
uart_suspend_port(&my_uart_drv, &priv->port);
/* DMA 중지 */
if (priv->rx_chan)
dmaengine_terminate_sync(priv->rx_chan);
if (priv->tx_chan)
dmaengine_terminate_sync(priv->tx_chan);
/* wakeup 소스 설정 */
if (device_may_wakeup(dev))
enable_irq_wake(priv->port.irq);
/* 클럭 비활성화 */
clk_disable_unprepare(priv->clk);
return 0;
}
/* System resume */
static int my_uart_resume(struct device *dev)
{
struct my_uart_priv *priv = dev_get_drvdata(dev);
clk_prepare_enable(priv->clk);
if (device_may_wakeup(dev))
disable_irq_wake(priv->port.irq);
/* 레지스터 복원은 uart_resume_port() 내부에서
* set_termios()를 재호출하여 자동 처리 */
uart_resume_port(&my_uart_drv, &priv->port);
/* DMA 재시작 */
if (priv->rx_chan)
my_uart_dma_rx_setup(priv);
return 0;
}
/* Runtime PM 콜백 */
static int my_uart_runtime_suspend(struct device *dev)
{
struct my_uart_priv *priv = dev_get_drvdata(dev);
clk_disable_unprepare(priv->clk);
return 0;
}
static int my_uart_runtime_resume(struct device *dev)
{
struct my_uart_priv *priv = dev_get_drvdata(dev);
return clk_prepare_enable(priv->clk);
}
static const struct dev_pm_ops my_uart_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(my_uart_suspend, my_uart_resume)
SET_RUNTIME_PM_OPS(my_uart_runtime_suspend,
my_uart_runtime_resume, NULL)
};
TTY 보안 — SAK와 접근 제어
Secure Attention Key (SAK)는 TTY에 연결된 모든 프로세스를 강제 종료하여, 가짜 로그인 프로그램(trojan login)으로부터 보호하는 보안 메커니즘입니다. SAK를 누르면 해당 TTY의 모든 프로세스가 종료되고, 진짜 login 프로그램만 남게 됩니다.
| 보안 메커니즘 | 설명 | 설정 |
|---|---|---|
| SAK | TTY의 모든 프로세스 종료 (Alt+SysRq+K) | echo 1 > /proc/sys/kernel/sak |
| TTY 소유권 | 로그인 시 /dev/ttyN 소유권을 사용자에게 변경 | login / systemd-logind |
| PTY 분리 | devpts 네임스페이스로 컨테이너 간 PTY 격리 | mount -t devpts -o newinstance |
| TIOCSTI 차단 | 다른 TTY에 키 입력 주입 방지 | 커널 5.2+ TIOCSTI 제한 |
| TIOCLINUX 제한 | VT 콘솔 복사/붙여넣기 제어 | CAP_SYS_ADMIN 필요 |
| read/write 권한 | /dev/ttyS* 그룹 dialout | usermod -aG dialout user |
/* SAK 처리 (drivers/tty/tty_io.c) */
void __do_SAK(struct tty_struct *tty)
{
struct task_struct *p;
/* 1. tty의 세션 내 모든 프로세스에 SIGKILL */
do_each_pid_task(tty->session, PIDTYPE_SID, p) {
send_sig(SIGKILL, p, 1);
} while_each_pid_task(tty->session, PIDTYPE_SID, p);
/* 2. 이 tty의 fd를 열고 있는 모든 프로세스에도 SIGKILL */
for_each_process(p) {
if (task_has_tty_fd(p, tty))
send_sig(SIGKILL, p, 1);
}
/* 3. tty를 초기 상태로 리셋 */
tty_reset_termios(tty);
}
/* TIOCSTI: 다른 TTY에 키 입력 주입 (보안 위험)
*
* 커널 6.2+에서 제한:
* - sysctl kernel.tiocsti_restrict = 1 (기본 비활성화)
* - 또는 CONFIG_LEGACY_TIOCSTI=n
* - CAP_SYS_ADMIN 없이는 TIOCSTI 사용 불가
*
* 공격 시나리오:
* 1. 공격자가 victim의 /dev/pts/N에 접근
* 2. ioctl(fd, TIOCSTI, 'r') — 'r' 키 입력 주입
* 3. 연쇄 호출로 "rm -rf /" 등의 명령 실행 가능
*/
성능 최적화 — 고속 UART 튜닝
시리얼 통신의 성능 병목은 보레이트만이 아닙니다. 인터럽트 빈도, FIFO 트리거 레벨, DMA 활용, N_TTY 오버헤드, 프로세스 스케줄링 지연 등 여러 계층에서 최적화할 수 있습니다.
| 최적화 포인트 | 기본값 | 튜닝 | 효과 |
|---|---|---|---|
| RX FIFO 트리거 | 1바이트 (트리거 즉시) | FIFO 절반 (8~64) | 인터럽트 횟수 대폭 감소 |
| DMA 사용 | PIO | DMA cyclic RX + SG TX | CPU 부하 95%+ 감소 (고속 시) |
| low_latency | 0 | setserial /dev/ttyS0 low_latency | flip buffer → 즉시 ldisc 전달 (workqueue 우회) |
| N_TTY bypass | N_TTY 사용 | 커스텀 ldisc 또는 serdev | N_TTY 오버헤드 제거 |
| 커스텀 보레이트 | 표준 (115200 등) | BOTHER + c_ospeed | 비표준 보레이트 (예: 250000, 500000) |
| RT 스케줄링 | CFS | 시리얼 처리 스레드 RT 우선순위 | worst-case 지연 개선 |
| IRQ affinity | 자동 | echo N > /proc/irq/IRQ/smp_affinity | UART IRQ를 전용 코어에 고정 |
/* 비표준 보레이트 설정 (user space) */
#include <asm/termbits.h>
#include <sys/ioctl.h>
struct termios2 tio;
ioctl(fd, TCGETS2, &tio);
tio.c_cflag &= ~CBAUD;
tio.c_cflag |= BOTHER; /* 비표준 보레이트 사용 */
tio.c_ispeed = 250000; /* 250Kbps */
tio.c_ospeed = 250000;
ioctl(fd, TCSETS2, &tio);
/* latency_timer 조정 (FTDI USB-Serial) */
/* /sys/bus/usb-serial/devices/ttyUSB0/latency_timer
* 기본값: 16ms → 1ms로 줄이면 지연 개선 (USB 마이크로프레임 기반)
*/
echo 1 > /sys/bus/usb-serial/devices/ttyUSB0/latency_timer
# 성능 측정 도구
# 시리얼 처리량 측정
dd if=/dev/urandom bs=1024 count=1000 | pv > /dev/ttyS0
# 수신측: pv < /dev/ttyS0 > /dev/null
# 왕복 지연(latency) 측정
# 간단한 에코 서버로 RTT 측정
stty -F /dev/ttyS0 4000000 raw -echo
# 타임스탬프 기반 RTT 측정 프로그램 사용
# 인터럽트 빈도 모니터링
watch -n1 'cat /proc/interrupts | grep serial'
# UART 오버런 에러 확인
cat /proc/tty/driver/serial
# 0: ... oe:5 (overrun error 5회 발생)
흔한 실수와 트러블슈팅
일부 장비는 DTR이 활성화되어야 통신을 시작합니다. CLOCAL을 설정하지 않으면 open()이 DCD 대기로 블로킹됩니다.
stty -F /dev/ttyS0 clocal # DCD 무시
# 또는 O_NDELAY / O_NONBLOCK으로 open
소프트웨어 흐름 제어가 켜진 상태에서 바이너리 데이터를 전송하면, 0x11(XON)과 0x13(XOFF) 바이트가 제어 문자로 해석되어 데이터가 훼손됩니다.
stty -F /dev/ttyS0 -ixon -ixoff # 소프트웨어 흐름 제어 해제
# 바이너리 전송 시 raw 모드 사용 필수
기본 설정에서 ICRNL(입력 CR→LF)과 ONLCR(출력 LF→CRLF)가 활성화되어 바이너리 데이터가 변형됩니다.
stty -F /dev/ttyS0 raw # 모든 가공 비활성화
# 또는 cfmakeraw()로 프로그래밍적 설정
/dev/ttyUSB0은 연결 순서에 따라 바뀝니다. udev 규칙으로 고정 symlink를 생성하세요.
# /etc/udev/rules.d/99-serial.rules
SUBSYSTEM=="tty", ATTRS{serial}=="AB0123CD", SYMLINK+="ttyGPS"
# 이후 /dev/ttyGPS로 안정적 접근
UART 인터럽트 핸들러는 hard IRQ 컨텍스트입니다. mutex_lock(), msleep(), kmalloc(GFP_KERNEL) 등 슬립 가능 함수를 호출하면 커널 패닉이 발생합니다. spin_lock()과 GFP_ATOMIC만 사용하세요.
printk()가 UART 콘솔을 통해 출력될 때, UART 드라이버 내부에서 printk()를 호출하면 재귀 → 데드락이 발생합니다. console_write() 경로에서는 dev_dbg()/printk() 사용을 피하세요.
실전 실습
socat으로 가상 시리얼 페어를 생성하여 실습할 수 있습니다.
Lab 1: socat으로 가상 시리얼 페어 생성
# 가상 시리얼 포트 쌍 생성
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1
# 터미널 1: 수신
cat /tmp/ttyV0
# 터미널 2: 송신
echo "Hello Serial!" > /tmp/ttyV1
# minicom으로 양방향 통신
minicom -D /tmp/ttyV0 -b 115200 # 터미널 1
minicom -D /tmp/ttyV1 -b 115200 # 터미널 2
Lab 2: stty로 termios 실험
# 현재 설정 저장
stty -g -F /tmp/ttyV0 > /tmp/stty_backup
# canonical 모드 테스트
stty -F /tmp/ttyV0 icanon echo
# → 줄 단위 입력, 에코 활성, ^C/^Z 동작
# raw 모드 테스트
stty -F /tmp/ttyV0 raw
# → 바이트 즉시 전달, 에코 없음, 시그널 없음
# VMIN/VTIME 실험
stty -F /tmp/ttyV0 -icanon min 5 time 20
# → 5바이트 수신 또는 2초 타임아웃 시 read() 반환
# 설정 복원
stty -F /tmp/ttyV0 $(cat /tmp/stty_backup)
Lab 3: 커스텀 Line Discipline 테스트
# SLIP line discipline으로 시리얼 위 IP 통신
# 호스트 A (ttyS0):
slattach -p slip -s 115200 /dev/ttyS0 &
ifconfig sl0 10.0.0.1 pointopoint 10.0.0.2 up
# 호스트 B (ttyS0):
slattach -p slip -s 115200 /dev/ttyS0 &
ifconfig sl0 10.0.0.2 pointopoint 10.0.0.1 up
# 핑 테스트
ping 10.0.0.2
# 시리얼 라인 위로 IP 패킷이 전송됩니다
Lab 4: RS-485 Half-Duplex 통신
/* RS-485 모드로 Modbus RTU 통신 실습 */
#include <linux/serial.h>
#include <sys/ioctl.h>
int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);
/* RS-485 모드 활성화 */
struct serial_rs485 rs485;
memset(&rs485, 0, sizeof(rs485));
rs485.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND;
rs485.delay_rts_before_send = 1; /* 1ms */
rs485.delay_rts_after_send = 1;
ioctl(fd, TIOCSRS485, &rs485);
/* 9600 8N1 설정 */
struct termios tio;
tcgetattr(fd, &tio);
cfmakeraw(&tio);
cfsetspeed(&tio, B9600);
tio.c_cflag |= CLOCAL | CREAD;
tio.c_cc[VMIN] = 0;
tio.c_cc[VTIME] = 10; /* 1초 타임아웃 */
tcsetattr(fd, TCSANOW, &tio);
/* Modbus RTU 프레임 송신 */
uint8_t req[] = {0x01, 0x03, 0x00, 0x00,
0x00, 0x0A, 0xC5, 0xCD};
write(fd, req, sizeof(req));
tcdrain(fd); /* TX 완료 대기 (RTS 전환 전 필수!) */
/* 응답 수신 */
uint8_t resp[256];
int n = read(fd, resp, sizeof(resp));
Lab 5: /proc/tty 심층 분석
# 등록된 모든 TTY 드라이버 확인
cat /proc/tty/drivers
# driver_name device_name major minor_range type
# 활성 line discipline 목록
cat /proc/tty/ldiscs
# 시리얼 포트 상세 (인터럽트 카운터 포함)
cat /proc/tty/driver/serial
# 0: uart:16550A port:03F8 irq:4 tx:1234 rx:5678 oe:0 pe:0 fe:0 brk:0
# tx: 송신 바이트, rx: 수신 바이트, oe: overrun, pe: parity error
# fe: frame error, brk: break 수신
# sysfs로 UART 타입 확인
for d in /sys/class/tty/ttyS*; do
echo "$(basename $d): type=$(cat $d/type) uartclk=$(cat $d/uartclk)"
done
# debugfs UART 상태 (일부 드라이버)
cat /sys/kernel/debug/serial/ttyS0
# 또는
cat /sys/kernel/debug/8250/ttyS0
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.