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

운영체제 기본/x86-64 Linux의 i8042 키보드 입력/evdev와 프로세스 깨우기

~ldmsys

키보드 입력 목차


Linux v6.12에서 input core까지 도착한 키 입력은 아직 사용자 프로그램이 읽은 데이터가 아니다. 커널 내부의 이벤트를 열린 장치 파일의 버퍼에 넣고, 기다리던 태스크를 실행 가능하게 만들고, 그 태스크가 다시 실행될 때 사용자 메모리로 복사해야 한다. 이 장에서는 프로그램이 권한을 갖고 /dev/input/eventN을 열어 blocking read()를 호출한 경우를 다룬다. 장치 파일을 열 권한과 CPU에서 실행될 차례를 얻는 것은 서로 다른 문제다.

input core에서 evdev 클라이언트로

input_pass_values()는 dev->event_lock을 잡고 로컬 인터럽트가 비활성화된 상태에서 호출된다. 연결된 열린 input handle의 handle_events를 호출하며, 독점 grab이 있으면 그 handle만 선택한다. v6.12의 input_register_handle()은 handler 형태에 따라 이 함수 포인터를 설정한다. evdev처럼 .events를 제공하는 handler는 묶음 이벤트를 직접 받는다.[1]

evdev_events()는 RCU(Read-Copy Update)로 보호되는 클라이언트 목록을 순회하여 evdev_pass_values()를 호출한다. 여기서 RCU는 목록을 읽는 동안 다른 CPU에서 제거한 객체가 너무 일찍 해제되지 않도록 하는 동기화 수단이다. 각각의 열린 파일에는 별도의 evdev_client와 원형 버퍼가 있다. 보통 여러 독자는 각각 이벤트 사본을 받으므로 한 독자의 읽기가 다른 독자의 이벤트를 소비하지 않는다. 다만 evdev의 독점 grab과 클라이언트별 이벤트 필터는 전달 대상을 바꿀 수 있다. 생산자는 buffer_lock 아래에서 시간, type, code, value를 갖는 input_event를 기록한다.[2]

패킷의 완료와 대기 큐

원형 버퍼의 head는 기록 위치이고 tail은 소비 위치다. packet_head는 완성된 패킷의 끝을 나타낸다. 다음 실제 코드는 __pass_event()의 일부다.[2]

if (event->type == EV_SYN && event->code == SYN_REPORT) {
	client->packet_head = client->head;
	kill_fasync(&client->fasync, SIGIO, POLL_IN);
}

읽기 가능성은 단순히 새 슬롯이 채워졌는지가 아니라 packet_head != tail인지로 판정한다. evdev_pass_values()는 비어 있지 않은 SYN_REPORT를 처리하면 버퍼 잠금을 푼 다음 wake_up_interruptible_poll(&client->wait, ...)을 호출한다. 이것이 패킷 경계를 독자에게 공개하는 지점이다. 입력이 소비보다 빨라 버퍼가 넘치면 SYN_DROPPED가 남으므로, 사용자 프로그램은 이벤트가 언제나 손실 없이 보존된다고 가정해서도 안 된다.[2]

잠든 read가 다시 진행되는 과정

evdev_read()는 완성된 이벤트가 없으면 다음 조건으로 기다린다. 장치 제거와 접근 철회 역시 대기 종료 조건에 포함된다.[2]

error = wait_event_interruptible(client->wait,
		client->packet_head != client->tail ||
		!evdev->exist || client->revoked);

정상적인 blocking read에서 태스크는 이 조건을 기다리며 잠들 수 있다. IRQ 쪽의 깨우기는 그 태스크를 실행 가능한 상태로 돌려놓는 것이지, IRQ 핸들러 안에서 사용자 프로그램을 호출하는 것이 아니다. 이후 스케줄러가 태스크를 선택하면 원래의 시스템 콜(System Call) 문맥에서 조건을 다시 확인하고 읽기를 계속한다. 신호가 도착한 경우에는 정상 입력 반환 대신 중단 관련 오류 경로를 밟을 수도 있다.

소비자는 evdev_fetch_next_event()에서 spin_lock_irq()로 같은 버퍼를 보호하고 이벤트 하나를 커널 임시 변수로 꺼낸다. 잠금을 해제한 뒤 input_event_to_user()를 호출한다. 이 helper 내부의 copy_to_user()가 사용자 버퍼에 실제 데이터를 옮긴다. 사용자 메모리 접근을 버퍼 spinlock 안에서 수행하는 구조가 아니다. 반환값은 복사한 바이트 수이며, ABI 호환 처리는 helper가 담당한다.[3]

사용자에게 전달되는 것은 문자 이전의 이벤트

이 read의 결과는 'a' 같은 문자 스트림이 아니라 EV_KEY / KEY_A / 1 같은 구조화된 이벤트다. Shift, 키보드 배열, 입력기 등을 해석하여 문자를 만드는 책임은 별도의 계층에 있다. O_NONBLOCK이면 준비된 패킷이 없을 때 잠들지 않고 -EAGAIN을 반환한다. poll() 계열은 evdev_poll()에서 같은 client->wait에 관심을 등록하고 같은 패킷 조건을 검사한다. 어느 방식이든 이벤트 도착, runnable 전환, 실제 실행, 사용자 버퍼 복사는 구별되는 단계다.[2]

출처

이전: i8042와 입력 이벤트 · 다음: 콘솔과 터미널 입력