본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/x86-64 Linux의 i8042 키보드 입력/IRQ 진입과 디스패치

~ldmsys

키보드 입력 목차

IRQ 번호와 CPU 벡터

이 문서는 Linux v6.12의 x86-64에서 i8042 PS/2 키보드가 이미 초기화된 뒤 입력이 발생하는 경우를 다룬다. IDT를 통해 진입해 인터럽트 문맥에서 IRQ 처리기를 실행하는 경로를 따른다.

키보드가 바이트를 보내면 i8042 컨트롤러는 출력 버퍼 상태와 인터럽트 요청으로 이를 알린다. 여기서 키보드의 IRQ 선, Linux IRQ 번호, CPU 인터럽트 벡터는 서로 다른 식별자다. 전통적인 PC 키보드 경로는 IRQ1이라고 부르지만, 드라이버가 사용하는 번호는 플랫폼에서 정해진 I8042_KBD_IRQ다. x86의 해당 매크로는 변수 i8042_kbd_irq를 참조하며 PNP 자원 정보도 반영한다. 따라서 모든 환경에서 번호를 상수 1로 대입하는 설명은 정확하지 않다.[1]

인터럽트 컨트롤러 설정은 장치의 요청을 수신할 CPU와 벡터에 연결한다. PIC와 I/O APIC은 외부 장치의 인터럽트를 CPU에 전달하는 컨트롤러다. 어떤 컨트롤러와 라우팅을 사용하는지는 실제 부팅 구성에 달려 있다. 여기서는 등록된 키보드 IRQ가 해당 CPU의 유효한 벡터로 전달된 정상 경로를 따라간다. IRQ1이면 항상 벡터 33이라는 관계를 전제하지 않는다. 커널은 CPU별 vector_irq에 벡터와 IRQ 디스크립터의 대응을 유지한다.[2]

IDT에서 C 진입점까지

IDT(Interrupt Descriptor Table)는 인터럽트·예외 벡터별로 진입 주소와 속성을 담은 테이블이다. CPU가 인터럽트를 받아들이면 벡터 번호로 IDT의 엔트리를 골라 제어를 옮긴다. 하드웨어가 복귀에 필요한 상태를 저장하고, 커널의 진입 어셈블리가 일반 레지스터 저장과 실행 환경 정리를 이어서 수행한다. 전체 상태를 하드웨어 혼자 저장하는 것도, 처음부터 C 함수가 저장하는 것도 아니다. idt_setup_apic_and_irq_gates()는 장치 벡터에 irq_entries_start의 대응 스텁을 연결한다.[3]

스텁에서 핵심적인 두 명령은 다음과 같다. 0x6a는 즉시값을 스택에 넣는 명령의 opcode다. 뒤의 vector는 스택에 담을 CPU 인터럽트 벡터 번호이며, 키보드가 보낸 입력 바이트가 아니다.[4]

.byte 0x6a, vector
jmp asm_common_interrupt

asm_common_interrupt는 idtentry_irq와 idtentry 매크로로 만들어진다. 이어지는 idtentry_body와 error_entry는 pt_regs 형태의 저장 상태를 마련하고, 이를 첫 번째 인자로, 벡터를 두 번째 인자로 common_interrupt()에 전달한다. 벡터는 예외의 오류 코드가 놓이는 자리를 재사용하지만, 키보드 IRQ가 CPU 예외 오류 코드를 발생시켰다는 뜻은 아니다.[5]

DEFINE_IDTENTRY_IRQ가 만드는 C 래퍼는 irqentry_enter(), run_irq_on_irqstack_cond(), irqentry_exit() 순서로 본체를 감싼다. 벡터를 8비트로 정리하는 처리도 여기에 있다. 조건부 IRQ 스택 경로는 irq_enter_rcu()와 irq_exit_rcu()로 hardirq 실행을 표시한다. 사용자 모드에서 들어왔거나 IRQ 스택을 이미 사용 중이면 무조건 다른 IRQ 스택으로 바꾸지는 않는다.[4][6]

벡터에서 IRQ 디스크립터로

common_interrupt() 본체는 현재 저장 레지스터를 연결한 뒤 call_irq_handler()를 호출한다. 아래는 정상 디스크립터를 찾는 분기다.[7]

desc = __this_cpu_read(vector_irq[vector]);
if (likely(!IS_ERR_OR_NULL(desc))) {
	handle_irq(desc, regs);
}

이 배열 조회가 CPU 벡터를 Linux의 struct irq_desc로 연결한다. x86-64의 handle_irq()는 generic_handle_irq_desc()를 호출하고, 그 함수는 디스크립터에 설치된 flow handler를 실행한다.[8]

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
	desc->handle_irq(desc);
}

flow handler는 장치 데이터를 해석하는 함수가 아니다. IRQ가 비활성화되었는지, 다른 처리가 진행 중인지, pending 상태가 있는지 등을 관리하며 컨트롤러 연산과 드라이버 호출을 조율한다. 예를 들어 edge로 설정된 I/O APIC 경로는 handle_edge_irq()를 사용한다. 이 함수는 정상 처리에서 irq_ack를 호출한 뒤 handle_irq_event()로 넘어가고, 처리 중 pending이 생기면 조건에 따라 반복한다. 모든 컨트롤러 구성이 이 flow handler를 사용한다는 뜻은 아니다.[9][10]

등록된 i8042 처리기로

앞선 초기화에서 i8042_setup_kbd()는 다음 호출로 키보드 IRQ의 action을 등록했다. IRQF_SHARED는 공유 가능한 등록이며, 같은 디스크립터에 다른 action이 있을 가능성을 허용한다.[11]

error = request_irq(I8042_KBD_IRQ, i8042_interrupt, IRQF_SHARED,
		    "i8042", i8042_platform_device);

handle_irq_event()는 pending을 지우고 진행 중 상태를 세운 다음 디스크립터 잠금을 풀어 action을 실행한다. handle_irq_event_percpu()를 거친 __handle_irq_event_percpu()는 action 목록을 돌며 다음 호출을 수행한다. 여기의 irq는 CPU 벡터가 아니라 desc->irq_data.irq다.[12]

res = action->handler(irq, action->dev_id);

키보드 action에서는 이것이 i8042_interrupt() 호출이다. 이때 CPU는 키보드를 읽으려던 프로세스로 전환된 것이 아니다. 인터럽트 당시 실행 중이던 태스크를 중단한 hardirq 문맥이며, 키보드 독자는 다른 CPU에서 실행 중이거나 잠들어 있을 수도 있다. 이후 독자를 깨우더라도 실제 실행 재개는 스케줄링 문제다. IRQ 복귀 경로도 복귀 모드와 설정에 따라 재스케줄링 조건을 검사한다.[13]

장치 버퍼를 읽는 일과 인터럽트 컨트롤러의 ACK 및 EOI도 구분해야 한다. 앞의 edge 경로에서는 컨트롤러 계층의 irq_ack가 상위 APIC 처리로 이어질 수 있고, apic_ack_irq()가 apic_eoi()를 수행한다. 따라서 EOI를 항상 드라이버 처리 뒤에 보내거나 i8042 드라이버가 직접 보낸다고 설명하면 틀린다. 다음 단계에서 드라이버가 읽는 것은 이 제어 정보가 아니라 컨트롤러에 도착한 실제 입력 바이트다.[10][2]

다음: i8042와 입력 이벤트

참고 문헌