I/O 시스템 03 - I/O 처리
I/O 시스템 03 - I/O 처리
I/O의 유형
동기적 I/O와 비동기적 I/O
동기적 I/O
- 호출자에게 제어를 반환하기 전에 I/O 명령을 완료함
- I/O 요청이 가장 단순한 형태로 사용되는 케이스
- ReadFile과 WriteFile을 동기적으로 사용하는 것
비동기 I/O(Asynchronous I/O)
- 애플리케이션이 다수의 I/O 요청을 발생하고 장치가 I/O 동작을 수행하는 동안에도 계속 실행할 수 있게 함
- I/O 명령이 진행 중인 동안에도 다른 작업을 할 수 있음
- 처리량 향상
- 비동기 I/O를 사용하려면
- FILE_FLAG_OVERLAPPED 플래그를 지정해야 함
- 물론 스레드는 비동기적 I/O 동작을 발생시킨 후 디바이스 드라이버가 데이터 전송을 완료할 때까지 I/O 동작에 관련된 어떤 데이터도 접근하지 않게 주의해야 함
- 스레드는 I/O가 완료될 때 시그널되는 동기화 객체(이벤트 객체 혹은 I/O 완료 포트)의 핸들을 감시해 I/O 요청의 완료와 스레드의 실행을 동기화시켜야 함
I/O 수행
- 요청한 I/O 동작이 드라이버로 전달되면 I/O 명령은 내부적으로 I/O 요청의 유형과 관계없이 비동기적으로 수행
- 즉, I/O 요청이 시작되면 디바이스 드라이버는 I/O 시스템으로 가능한 빨리 복귀해야 함
- I/O 시스템이 즉시 호출자에게 복귀할 것인지는 핸들이 동기, 비동기로 열렸는지에 따라 결정됨
읽기 동작이 시작될 때의 제어 흐름
- 파일 객체의 operlapped 플래그에 따라 대기가 이뤄진다면 이 대기는 NtReadFile 함수에 의해 커널 모드에서 이루어짐
펜딩된(Pending) 비동기 I/O의 상태를 테스트 하는 방법
- HasOverlappedIoCompleted 매크로 사용
- GetOverlappedResult(Ex) 함수
- I/O 완료 포트 사용시 GetQueuedCompletionStatus(Ex) 함수
패스트 I/O
- I/O 시스템 I/O 요청을 완료하기 위해 IRP를 생성하는 것을 건너뜀
- 대신 드라이버 스택으로 직접 접근하게 하는 특별한 메커니즘
- IRP의 느린 특정 I/O 경로를 최적화하기 위해 사용
- 드라이버는 자신의 패스트 I/O 진입점을 드라이버 객체의 PFAST_IO_DISPATCH 포인터가 가리키는 구조체에 등록해야 함
맵 파일 I/O와 파일 캐싱
맵 파일 I/O(Mapped File I/O)
- 디스크에 존재하는 파일을 프로세스 가상 메모리의 일부분 처럼 볼 수 있는 것을 말함
- 데이터를 버퍼링하거나 디스크 I/O를 수행하지 않고 커다란 배열처럼 파일에 접근 가능
- 프로그램은 메모리에 접근하고 메모리 관리자는 자신의 페이징 메커니즘으로 디스크 파일로부터 정확한 페이지를 로드
- 가상 주소 공간에 쓰기 작업을 할 때 메모리 관리자가 일반 페이징의 일부분 처럼 파일에 변화된 내용을 기록
맵 파일 I/O 사용
- CreateFileMapping, MapViewOfFile 등의 함수로 유저 모드에서 사용 가능
- 운영체제 내부에서 파일 캐싱과 이미지 활성화와 같은 중요한 동작에서 사용함
- 주 사용처는 캐시 관리자
- 캐시 관리자를 이용해 I/O 바운드 프로그램에 좀 더 빠른 응답 시간을 제공하기 위해 파일 데이터를 가상 메모리에 매핑함
- 호출자가 파일을 사용할 때 메모리 관리자가 접근된 페이지를 메모리로 가져옴
캐싱 시스템
- 윈도우 캐시는 메모리의 사용 가능 여부에 따라 늘어나거나 줄어듬
- 메모리 관리자의 일반 워킹셋 메커니즘을 사용하기 때문
- 메모리 관리자의 페이징 시스템 장점을 활용하여 캐시 관리자는 메모리 관리자가 이미 실행한 작업을 반복하지 않게 함
- 2권의 14장에서 자세히 설명함
스캐터/게더 I/O
- ReadFileScatter와 WriteFileGather를 통해 사용할 수 있는 고성능 I/O
- 디스크에서 파일의 연속된 영역에 대응하는 가상 메모리의 여러 버퍼에 개별 I/O 요청을 하는 대신 하나의 읽기 요청이나 쓰기 요청을 할 수 있음
- 파일은 넌캐시드(noncached) I/O로 열려있어야 함
- 유저 버프는 page-aligned 여야 하고, 비동기 I/O 여야 함
- 대용량 저장 장치의 I/O 라면 장치 섹터 경계에 정렬되어야 하고 섹터 크기의 배수 길이를 가져야 함
I/O 요청 패킷
- I/O 시스템이 I/O 요청을 처리할 때 필요한 공간을 저장하는 곳
- 스레드가 I/O API를 호출할 때 I/O 관리자는 I/O 동작이 I/O 시스템을 통한 진행 정도에 따라 그 동작을 나타내는 용도로 IRP를 만듬
I/O 관리자는 프로세서 당 존재하는 세 개의 IRP 넌페이지드 룩 어사이드 리스트 중 하나에서 IRP를 할당
- 작은 IRP 룩 어사이드 리스트
- 하나의 스택 로케이션을 갖는 IRP를 저장
- 중간 크기의 IRP 룩 어사이드 리스트
- 최대 4개의 스택 로케이션을 갖는 IRP를 포함
- 큰 IRP 룩 어사이드 리스트
- 4개보다 더 많은 스택 로케이션을 갖는 IRP를 포함
- 기본적으로 14개의 스택 로케이션을 갖는 IRP를 저장
- 최대 20개까지 늘릴 수 있음
이들 리스트는 전역 룩 어사이드 리스트의 지원을 받음
- CPU 간 효율적인 IRP 흐름이 가능케 하기 위해
- 큰 IRP 룩 어사이드 리스트보다 더 많은 로케이션을 필요로 하면 넌페이지드 풀에서 할당
- I/O 관리자는 IoAllocateIrp 함수로 IRP를 할당
- 드라이버 개발자가 이용 가능
- I/O 관리자는 IRP를 할당하고 초기화한 후 호출자의 파일 객체를 가리키는 포인터를 IRP 내부에 저장
IRP 구조체
- IoStatus
- 실제 코드 자체인 Status와 경우에 따라 달라지는 다형성의 값의 Information으로 이뤄진 IRP의 상태
- 예를들어 읽기 쓰기라면 읽기 쓰기 바이트 수
- MdlAddress
- 메모리 디스크립터 리스트에 대한 옵션 포인터 - 요청되지 않는다면 NULL
- 물리 메모리 내 버퍼 정보를 나타내는 구조체
- I/O 스택 로케이션 카운트와 현재 스택 로케이션 카운트
- 스택 로케이션의 총 개수와 현재 스택 로케이션
- 사용자 버퍼
- 클라이언트가 제공한 버퍼의 포인터
- 사용자 이벤트
- 비동기 I/O 동작에 사용됐던 커널 이벤트 객체
- I/O 동작이 완료될 때 일방적으로 통지되는 이벤트
- 취소 루틴
- IRP가 취소되는 경우에 I/O 관리자가 호출하는 함수
- AssociatedIrp
- 세 필드 중 하나를 나타내는 유니온 값
- 버퍼드 I/O 기법을 사용하면 SystemBuffer 멤버가 사용됨
- MasterIrp 멤버는 원래의 작업을 서브 Irp로 나누는 방법을 제공
- 마스터 IRP는 자신의 모든 서브 IRP가 완료된 이후에야 완료되었다고 간주
I/O 스택 로케이션
IRP는 항상 하나 이상의 I/O 스택 로케이션을 동반
- 스택 로케이션 개수는 IRP가 향하는 디바이스 노드 내의 디바이스 계층의 수와 동일
- I/O 동작 정보는 IRP 바디와 현재 I/O 스택 로케이션으로 나뉘어 있음
- 현재의 의미 특정 디바이스 계층에 대해 설정된 값
IRP가 생성될 때
- 요청 I/O 스택 로케이션의 개수가 IoAllocateIrp로 전달
- I/O 관리자는 디바이스 노드에서 최상위 디바이스로 향하는 IRP의 바디와 첫 번째 I/O 스택 로케이션만 초기화
- 디바이스 노드에서 각 계층은 IRP를 다음 디바이스로 내려보내기로 결정하면 다음 I/O 스택 로케이션의 초기화를 책임
IO_STACK_LOCATION 구조체
- 메이저 함수
- 디스패치 루틴 코드로 알려진 주 코드
- 마이너 함수
- 일부 함수에서 메이저 함수를 확장하는 데 사용
- 매개변수
- 복잡한 유니온 유형의 구조체
- 각각 특정 메이저 함수 코드나 메이저/마이너 코드의 조합으로 유효한 의미를 지님
- 파일 객체와 디바이스 객체
- FILE_OBJECT와 DEVICE_OBJECT
- 완료 루틴
- IoSetCompletionRoutine(Ex) DDI를 사용해 드라이버가 등록할 수 있는 함수
- 아래 계층의 드라이버에 의해 IRP가 완료될 때 호출됨
- 이 시점에 드라이버는 IRP의 완료 상태를 살펴보고 필요한 후처리 작업 가능
- 컨텍스트
- 완료 루틴에 전달되는 값
- IoSetCompletionRoutine(Ex)에서 설정
IRP 바디와 I/O 스택 로케이션으로 정보를 분리하면
- 원본 요청의 인자는 유지하며 디바이스 스택에서 다음 디바이스에 대한 I/O 스택 로케이션 인자의 변경이 가능해짐
처리 중인 IRP
- 각 IRP는 보통 I/O를 요청했던 스레드와 관련된 IRP 리스트에 저장됨
- 이렇게 하면 스레드가 미처리된 I/O 요청이 있는 채로 종료될 때 취소를 할 수 있게 함
- 부가적으로 페이징 I/O IRP(취소 불가능하지만) 또한 폴트를 유발한 스레드와 연관됨
현재 스레드가 I/O를 시작한 그 스레드라서 I/O를 완료하는 데 APC를 사용하지 않는 경우
- 윈도우는 비종속적 I/O 최적화를 사용할 수 있음
- APC 전달이 요구되는 대신 페이지 폴트가 내부적으로 발생함을 의미
IRP 흐름
하드웨어 기반 장치 드라이버에서 일반적인 IRP 흐름
- 디바이스 스택 내의 아래 계층의 네임드 디바이스에 대한 핸들이 오픈됐더라도 IRP는 항상 최상위 계층의 디바이스로 전달됨
I/O 관리자만이 IRP를 생성하는 것은 아님
- 플러그앤플레이 관리자와 전원 관리자 또한 IRP를 생성할 책임을 가짐
- IRP_MJ_PNP, IRP_MJ_POWER 메이저 함수 코드에 대한 IRP
IRP를 받은 드라이버가 수행하는 작업
- IoCompleteRequest를 호출해 IRP를 완료 가능
- IRP가 유효하지 않은 매개변수를 갖는 경우
- 레지스트리의 값을 읽는 경우처럼 요청된 동작이 빠르게 즉시 완료될 수 있는 경우
- 드라이버는 IoGetCurrentIrpStackLocation을 호출해 자신이 참조해야 할 스택 로케이션의 포인터를 구함
- 드라이버는 선택적으로 일부 처리를 하고 다음 계층으로 IRP를 보낼 수 있음
- 요청을 아래로 보내기 전 드라이버는 다음 드라이버가 살펴보게 될 다음 I/O 스택 로케이션을 준비시켜야 함
- 드라이버 스택 로케이션의 변경을 원치 않으면 IoSkipCurrentIrpStackLocation 사용 가능
- IoCopyIrpStackLocationToNext를 사용해 스택 로케이션의 복사본을 만들고 IoGetNextIrpStackLocation을 사용해 복사된 스택 로케이션의 포인터를 구해 스택 로케이션 수정 가능
- 다음 I/O 스택 로케이션의 준비가 되면 드라이버는 IoCallDriver를 호출하여 IRP 전달을 함
- 드라이버는 IRP를 아래로 전달하기 전 IoSetCompletionRoutine(Ex)를 호출하여 완료 루틴 전달 가능
- 최하단을 제외한 모든 계층은 완료 루틴을 등록할 수 있음
- 아래 계층의 드라이버가 IoCompleteRequest를 호출한 이후부터 IRP는 상단으로 거슬러 올라오며 등록된 역순으로 완료 루틴을 호출함
- 실제로 IRP의 원주인(I/O관리자 혹은 PnP관리자, 전원 관리자)은 이 메커니즘을 사용해 필요한 후처리를 하고 마지막으로 IRP를 해제
단일 계층 하드웨어 기반 드라이버로의 I/O 요청
단일 계층의 커널 모드 디바이스 드라이버에서 일반적인 IRP 처리 상황
일반적인 사항을 순서대로 살펴봄
- 두 유형의 수평 분할 선이 존재함
- 첫 번째(실선)는 통상적인 유저 모드/커널 모드 분할선
- 두 번째(점선)는 요청 스레드 컨텍스트와 임의의 스레드 컨텍스트에서 실행하는 코드를 분할
이런 컨텍스트는 다음과 같이 정의됨
- 요청 스레드 컨텍스트 영역
- 실행 스레드가 I/O 동작을 요청한 원래의 스레드임을 나타냄
- 스레드가 원래의 호출을 한 스레드 - 프로세스 컨텍스트가 원래의 프로세스임
- 따라서 I/O 동작에 제공된 유저 버퍼를 포함하는 유저 모드 주소 공간을 직접 접근할 수 있기 때문에 매우 중요함
- 임의의 스레드 컨텍스트 영역은 이들 함수를 실행하는 스레드가 임의의 스레드가 될 수 있음을 나타냄
- 즉, 요청 스레드가 아닐 수 있고, 보이는 유저 모드 프로세스 주소 공간은 원래의 프로세스의 것이 아닐 수 있음
- 이 컨텍스트에서 유저 모드 주소로 유저 버퍼에 근하면 손상이 발생할 수 있음
- 4개의 블록으로 구성된 커다란 사각형(디스패치 루틴, Start I/O 루틴, ISR, DPC 루틴)은 드라이버가 제공한 코드임을 나타냄
- 그 외는 모두 시스템이 제공
- 많은 유형의 장치에서 그렇듯 여기에 하드웨어 장치는 한 번에 하나의 요청을 처리할 수 있다 가정
- 여러 요청을 처리할 수 있어도 기본 흐름은 동일
위 그림의 소개된 사건들의 순서
- 클라이언트 애플리케이션은 ReadFile과 같은 윈도우 API 호출
- ReadFile은 네이티브 NtReadFile(Ntdll.dll)을 호출
- 이 함수는 커널 모드 익스큐티브 NtReadFile로 스레드 전환
- I/O 관리자는 자신의 NtReadFile 구현에서 클라이언트가 제공한 버퍼가 올바르게 접근 가능한지 무결성 검사 수행
- I/O 관리자는 관련된 드라이버를 찾고 IRP를 할당해 초기화하고 IoCallDriver를 사용하여 적절한 디스패치 루틴을 호출
- 드라이버는 처음으로 이 IRP를 봄
- 일반적으로 요청 스레드에서 이뤄짐
- 상위 필터가 IRP를 보관하다 다른 스레드에서 IoCallDriver를 호출한 경우 예외
- 예외 경우가 아니라고 가정
- 드라이버 디스패치 Read 콜백은 두 가지 작업을 함
- 요청이 실제로 의미하는 것을 몰라 I/O 관리자가 할 수 없던 추가적 검사(버퍼 크기 등)를 수행
- DeviceIoControl 동작의 경우 드라이버는 제공된 I/O 컨트롤 코드가 지원되는 것인지 검사
- 만약 이런 검사가 실패하면 IRP를 실패 상태로 완료시키고 즉시 복귀
- 검사가 성공하면 드라이버는 busy 비트를 설정하고 Start I/O 루틴(DriverEntry에 존재)을 호출 (IoStartPacket 함수)
- 하드웨어 장치가 바쁜 상태(busy 비트 확인)라면 IRP는 드라이버가 관리하는 큐에 넣어짐
- 이때 IRP는 PENDING 상태를 반환
- IoStartPacket 함수를 사용하지 않더라도 비슷한 흐름에서 동작
- 일반적으로 요청 스레드에서 이뤄짐
- 장치가 바쁘지 않다면 Start I/O 루틴이 디스패치 루틴으로부터 바로 호출
- 여전히 호출을 한 스레드 컨텍스트
- 그러나 그림에서는 Start I/O 루틴이 임의의 스레드 컨텍스트에서 호출되고 있음
- DPC 루틴에서 보통 이런 상황이 발생
- Start I/O의 목적은 IRP 관련 인자를 가져와 하드웨어 장치를 프로그래밍 하는 것
- 완료 이후 호출은 복귀하고 드라이버에서는 특별한 코드가 실행됮 ㅣ않음
- 하드웨어는 지시된 작업을 수행
- 하드웨어가 동작하는 동안 같은 스레드나(비동기 동작) 동일한 장치에 다른 스레드에 의해 추가적인 요청이 올 수 있음
- 이 경우 디스패치 루틴은 장치가 바쁜 상태임을 알고 IRP를 IRP 큐에 넣음
- IoStartPacket을 호출하는 것도 한 방법
- 이 경우 디스패치 루틴은 장치가 바쁜 상태임을 알고 IRP를 IRP 큐에 넣음
- 장치에서 현재 동작이 완료되면 인터럽트를 발생
- 커널 트랩 핸들러는 인터럽트를 처리하기 위해 선정된 CPU에서 현재 CPU의 컨텍스트를 저장하고 인터럽트와 연관된 IRQL까지 IRQL을 상승시키고 등록된 ISR로 점프
- DIRQL에서 실행되는 ISR은 가능한 작은 일만 수행
- ISR의 마지막 작업으로 DPC를 큐에 넣음
- DPC의 이점
- DIRQL과 DPC/디스패치 IRQL(2) 사이의 블록된 인터럽트가 더 낮은 우선순위를 갖는 DPC 처리가 일어나기 전 진행할 수 있다는 것
- 따라서 중간 레벨의 인터럽트는 좀 더 빠르게 수행될 수 있고 시스템 지연을 줄여줌
- 인터럽트가 해제된 이후 커널은 DPC 큐가 비어있지 않음을 감지하고 소프트웨어 인터럽트를 통해 DPC 처리 루프로 점프
- DPC는 큐에서 꺼내지고 IRQL 2에서 실행 - DPC에서 하는 일
- 큐에서 다음 IRP를 가져와 장치에 새로운 동작을 시킴
- 장치가 유휴 상태로 지속되는 시간을 줄이기 위해
- 디스패치 루틴이 IoStartPacket을 사용했다면 DPC 루틴은 IoStartNextPacket을 호출
- IRP가 큐에 있다면 DPC에서 Start I/O 루틴이 호출됨
- 임의의 스레드에서 Start I/O 루틴이 수행되는 이유
- 큐에 IRP가 없다면 장치는 바쁘지 않음으로 표시됨
- 다음 요청을 대기
- 드라이버에 의해 동작이 막 끝난 IRP에 대해 IoCompleteRequest 호출하여 완료시킴
- 이 시점부터 드라이버는 IRP에 대한 책임이 없음
- 어느 순간에 메모리 해제가 될지 모르므로 IRP에 접근하면 안됨
- IoCompleteRequest는 등록된 완료 루틴이 있다면 호출함
- 최종적으로 I/O 관리자는 IRP를 해제함
- 자신의 완료 루틴을 사용하여
- 큐에서 다음 IRP를 가져와 장치에 새로운 동작을 시킴
- 원래 요청 스레드는 완료 통지를 받을 필요가 있음
- DPC에서 실행하는 현재 스레드는 원래의 스레드가 아님(프로세스 주소 공간이 다를 수 있음)
- 요청 스레드의 컨텍스트에서 코드를 실행하기 위해 커널 APC가 스레드에 발행됨
- APC는 특정 스레드의 컨텍스트에서 실행되게 강제된 함수
- 요청 스레드가 CPU 시간을 받을 때 특수한 커널 APC가 먼저 실행(IRQL APC_LEVEL=1)
- APC는 대기로부터 스레드를 해제하고 비동기 동작에서 등록된 이벤트를 시그널하는 등 필요한 작업을 함
비동기 I/O 함수는 콜백 함수를 인자로 받음
- I/O 관리자가 I/O 완료의 마지막 단계에서 유저 모드 APC를 호출자의 스레드 APC 큐에 넣음
- 이 기능으로 완료 혹은 취소의 서브루틴을 호출자가 지정 가능
APC 완료 루틴
- 요청 스레드의 컨텍스트에서 실행
- 스레드가 Alertable wait 상태일 때만 전달됨
유저 주소 공간 버퍼 접근
유저 버퍼에 직접 접근하는 것
- 요청 스레드의 컨텍스트에서 IRQL 0일 때만 가능
- 정상적으로 페이징이 처리될 수 있는 레벨
디스패치 루틴만이 요청 스레드의 컨텍스트와 IRQL 0에서 실행하는 기준을 충족
- 그러나 항상 그런 것도 아님
- 상위 필터가 IRP를 보관해 아래로 즉시 전달하지 않고 다른 스레드를 사용하여 전달 가능
- CPU IRQL이 2 이상일 때 가능
Start I/O, ISR, DPC 루틴은 IRQL 2로 임의의 스레드에서 실행
- 이런 루틴에서 사용자 버퍼에 직접 접근하는 것은 매우 위험함
위험한 이유
- IRQL이 2 이상이므로 페이징이 허용되지 않음
- 사용자 버퍼는 페이지 아웃될 수 있으므로 메모리에 상주하지 않는 페이지에 접근하면 시스템 크래시
- 임의의 스레드가 될 수 있으므로 다른 프로세스 주소 공간을 보고 있을 수 있음
- 원래의 사용자 주소는 의미가 없어지고, 접근 위반 혹은 좋지 않은 상황 발생
이런 루틴에서 사용자 버퍼에 안접하게 접근할 수 있는 방법이 필요
- 버퍼드 I/O와 다이렉트 I/O : I/O 관리자가 처리
- Neither I/O : 드라이버에게 문제의 처리를 맡김
드라이버에서 방법을 선택하는 방법
- 읽기/쓰기 요청의 경우, 디바이스 객체의 Flags 멤버를 DO_BUFFERD_IO, DO_DIRECT_IO로 설정
- 아무것도 설정되지 않으면 Neither I/O를 나타냄
- 디바이스 I/O 컨트롤 요청의 경우 각 컨트롤 코드는 CTL_CODE 매크로로 만들어짐
- 이 때 일부 비트가 버퍼링 방법을 나타냄
버퍼드 I/O
- I/O 관리자는 호출자의 버퍼 크기와 동일한 버퍼를 넌페이지드 풀에 할당
- 이 포인터를 IRP 바디의 AssociatedIrp.SystemBuffer에 저장
드라이버는 임의의 스레드 임의의 IRQL에서 시스템 버퍼에 접근 가능
- 시스템 공간 내에 이 주소가 있음
- 어떤 프로세스 컨텍스트에서도 유효하지
- 넌페이지드 풀이므로 페이지 폴트가 발생하지 않음
쓰기 동작의 경우
- IRP를 생성할 때 호출자의 버퍼 데이터를 버퍼로 복사
읽기 동작의 경우
- IRP가 완료될 때 할당된 버퍼로부터 유저 버퍼로 데이터를 복사(커널 APC를 이용)하고 할당된 버퍼를 해제
버퍼드 I/O 장점
- I/O 관리자가 모든 일을 처리해주므로 간단함
버퍼드 I/O 단점
- 항상 복사 작업이 필요함
- 버퍼드 I/O는 버퍼 크기가 한 페이지를 넘지 않을 때 혹은 장치가 DMA를 지원하지 않을 때 사용
DMA(Direct Memory Access)
- CPU의 간섭 없이 장치에서 RAM으로 혹은 반대로 데이터를 전송하기 위해 사용
다이렉트 I/O
- 복사 없이 드라이버가 사용자 버퍼에 직접 접근할 수 있게 함
다이렉트 I/O의 주요 단계
- I/O 관리자가 IRP를 생성할 때 MmProbeAndLockPages 함수를 호출하여 유저 버퍼를 페이지락 함
- 메모리 디스크립터 리스트(MDL)의 형태로 메모리의 디스크립션을 저장
- 이 주소는 IRP 바디의 MdlAddress에 저장
- DMA를 수행하는 장치는 버퍼의 물리적 디스크립션만 필요로 하기 때문에 MDL은 이런 장치의 동작에 적합
- 그러나 드라이버가 버퍼의 내용에 접근해야 한다면?
- MDL에 주소를 전달해서 MmGetSystemAddressForMdlSafe 함수를 사용해 버퍼를 시스템 주소 공간에 매핑 가능
- 그 결과의 포인터는 어떤 스레드 컨텍스트, 아무런 IRQL에서 접근해서 사용해도 안전
유저 버퍼는 효율적으로 이중 매핑된 상태
- 원래의 프로세스 컨텍스트에서 사용 가능 - 원래 프로세스 가상 주소
- 시스템 주소 공간에 매핑되어 사용 가능 - 어떤 컨텍스트에서든 사용 가능
IRP가 완료되면
- MmUnlockPages를 호출하여 페이징 가능하게 만듬
다이렉트 I/O는 복사가 이뤄지지 않음으로 큰 버퍼에 유용
- 특히 DMA 전송의 경우 더욱 유용
Neither I/O
- I/O 관리자는 어떤 관리도 수행하지 않음 - 드라이버가 직접 관할
- I/O 관리자가 수행하던 일을 직접하도록 할 수 있음
- 일부 경우 디스패치 루틴에서 버퍼에 접근하는 것으로 충분할 수 있음
- 이런 경우 Neither I/O 방식만으로도 가능
- 가장 큰 장점 - 오버헤드가 전혀 없음
Neither I/O 주의점
- 접근하는 버퍼 주소가 유효하고 커널 모드 메모리를 참조하지 않음을 보장해야 함
- 스칼라 값은 이런 용도로 전달하기에 매우 안전한 값(이를 전달하는 드라이버는 극소수)
- 이렇게 하지 않으면 크래시 혹은 커널 코드 인젝션 등 보안 취약점 발생
- ProbeForRead, ProbeForWrite 함수로 버퍼가 유저 모드 영역에 존재한다는 것을 검증 가능
- 유효하지 않은 유저 모드 주소 참조로 발생하는 크래시를 피하기 위해 SEH 사용 가능
- 유효하지 않은 메모리 폴트를 캐치하여 오류 코드를 애플리케이션으로 반환
- 유저 모드 주소가 여전히 유효하더라도 커널 버퍼로 모든 입력 데이터를 캡처해야 함
- 호출자가 언제라도 데이터를 수정할 수 있으므로
동기화
드라이버는 다음 두 가지 이유로 전역 드라이버 데이터와 하드웨어 레지스터를 동기화해야 함
- 드라이버의 실행은 좀 더 높은 우선순위 스레드나 퀀텀 만료에 의해 선점될 수 있음
- 또한 더 높은 IRQL 인터럽트에 의해 인터럽트될 수 있음
- 멀티프로세서 시스템에서 윈도우는 드라이버 코드를 하나 이상의 프로세서에서 동시 실행시킬 수 있음
동기화가 없다면 손상 발생 가능
- 호출자가 I/O 동작을 시작할 때 패시브 IRQL에서 실행되는 드라이버 코드는 장치 인터럽트에 의해 인터럽트 될 수 있기 때문
- ISR이 수정하는 장치 레지스터, 힙 저장소, 정적 데이터와 같은 데이터를 디바이스 드라이버도 수정한다면 ISR이 실행될 때 데이터 손상 가능성이 있음
따라서 윈도우용 드라이버는 하나 이상의 IRQL에서 접근 가능한 데이터에 접근할 때는 반드시 동기화 해야 함
- 공유 데이터의 갱신을 시도하기 전 드라이버는 모든 스레드를 lock out 시켜야 함
단일 CPU 시스템에서는 간단함
- 이들 함수가 실행하는 가장 높은 IRQL로 상승시키면 됨
- 예시로 디스패치 루틴(0)과 DPC 루틴(2)의 동기화
- 디스패치 루틴을 2로 상승시킴
- 예시로 디스패치 루틴(0)과 DPC 루틴(2)의 동기화
멀티프로세서 시스템의 경우 IRQL 상승만으로는 불충분
- 다른 루틴이 다른 CPU에서 서비스될 수 있음
윈도우 커널은 Spinlock을 사용하여 동기화를 수행
뮤텍스와 스핀락의 차이
스핀락
- 원자적으로 접근되는 메모리의 한 비트일 뿐
- CPU에 의해 소유되거나 소유되지 않은 상태일 수 있음
- 높은 IRQL에서 동기화가 필요할 때 필수적
- 바쁜 대기 상태 busy-waiting
- 스핀락 획득을 시도중인 스레드는 대기 상태로 들어가지 못함
뮤텍스를 사용하지 못하는 이유
- 스케줄러가 필요함
- 그러나 IRQL 2 이상에서는 스케줄러가 깨어나지 못함
스핀락 획득의 동작
- 동기화가 일어나는 관련 IRQL로 상승
- 원자적 테스트와 스핀락 비트 설정을 통한 스핀락 획득 시도
스핀락 커널 함수
- KeAcquireSpinLock, KeReleaseSpinLock으로 획득, 해제
- KeInitializeSpinLock으로 초기화
- 먼저 스핀락을 할당해야 함(32비트에선 4바이트, 64비트에선 8바이트)
- 보통 디바이스 익스텐션에 할당
어떤 함수(DPC, 디스패치 루틴)과 ISR 간의 동기화는 다른 함수를 사용
- 모든 인터럽트 객체는 내부에 스핀락을 가짐
- 이는 ISR이 실행하기 전에 획득됨
- 같은 ISR이 다른 CPU에서 동시에 실행될 수 없음
- KeAcquireInterruptSpinLock, KeReleaseInterruptSpinLock 사용
- 또 다른 함수 KeSynchronizeExecution
- 콜백 함수를 인자로 받음
- 인터럽트 스핀락 획득 후 콜백 함수를 호출하고 완료한 뒤 스핀락을 해제
계층적 드라이버에 대한 I/O 요청
비동기적 I/O 요청이 어떻게 주대상으로 비하드웨어를 기반으로 한 장치에 대한 계층화된 드라이버를 통과하는지
- I/O 관리자는 요청을 받고 IRP 생성
- 하지만 이번에는 이것을 파일 시스템 드라이버로 전달
- 파일 시스템 드라이버가 하는 일
- 호출자가 요청한 I/O 유형에 따라 파일 시스템은 I/O 관리자가 보낸 IRP를 디스크 드라이버로 보내거나 추가적인 IRP를 생성해 개별로 디스크 드라이버에 보낼 수 있음
- 파일 시스템은 전달받은 요청이 간단한 단일 요청으로 변환될 경우 IRP를 재사용할 가능성이 높음
디스크 컨트롤러의 DMA 어댑터가 데이터 전송을 마친 후
- 디스크 컨트롤러는 호스트를 인터럽트 하면 디스크 컨트롤러의 ISR이 실행되고
- ISR에서는 IRP를 완료하는 DPC 콜백이 요청됨
연계 IRP(associated IRPs)의 그룹
- 단일 IRP를 재사용하는 것의 대안
- 하나의 I/O 요청에 대해 병렬로 동작함
파일 시스템 드라이버가 연계 IRP를 볼륨 관리자에게 전달
- 볼륨 관리자는 디스크 드라이버로 보냄
- 연계 IRP는 한 번에 하나씩 처리
- 파일 시스템 드라이버는 반환된 데이터를 추적
- 모든 연계 IRP가 완료될 때 I/O 시스템은 원본 IRP를 완료하고 호출자로 복귀
스레드 비종속적 I/O
스레드 특정적인 I/O 처리는 일반적으로 충분
- 그러나 비종속적 I/O 도 지원
비종속적 I/O 메커니즘
- I/O 완료 포트
- I/O의 완료 검사 시점을 결정하는 메커니즘
- I/O를 요청한 이외의 어떤 다른 스레드도 완료 요청을 수행 가능
- IRP를 특정 스레드의 컨텍스트 안에서 완료시키는 것이 아닌 어디든 가능
- 유저 버퍼를 메모리에 락시키고 이를 시스템 주소 공간에 매핑
- 메모리 락을 하고 커널에 매핑시킨 유저 버퍼는 임의의 스레드에서 접근 가능
I/O 취소
많은 I/O는 완료전 취소 될 수 있음
- IoSetCancelRoutine을 통해 취소 루틴을 등록 가능함
- 이 루틴은 I/O 관리자가 I/O를 취소하고자 할 때 동작
사용자 발생 I/O 취소
동기적 I/O 취소
- 스레드는 CancelSychronousIo 호출 가능
- 디바이스 드라이버가 지원하는 경우 create(open) 명령도 취소 가능
비동기 I/O 취소
- CancelIo 호출을 통해 자신의 미처리된 비동기 I/O를 취소시킬 수 있음
- CancelIoEx는 같은 프로세스 내의 특정 파일에 요청된 모든 비동기 I/O를 취소할 수 있음
- 어떤 스레드가 요청했는지는 상관 X
- 완료 포트와 연계하여 동작
- 이는 I/O 시스템이 미처리된 I/O와 완료 포트를 연결해 I/O 완료 포트의 미처리된 I/O를 추적하고 있기 때문
스레드 종료 시의 I/O 취소
모든 스레드는 자신과 관계된 IRP의 리스트를 가짐
- I/O 관리자는 이 리스트를 열거해 취소 가능한 IRP를 찾고 취소
- 프로세스 관리자는 모든 I/O가 취소된 후에야 스레드 종료를 진행
- 만약 IRP 취소를 실패하면 해당 프로세스와 스레드 객체는 시스템 종료 시까지 할당된 채로 남을 것