https://www.yes24.com/Product/Goods/122109062
그림으로 배우는 리눅스 구조 - 예스24
선배가 옆에서 하나하나 알려주듯 친절히 설명해주는실습과 그림으로 배우는 리눅스 지식의 모든 것 * Go 언어와 Python, Bash 스크립트 실습 코드 제공* 이 도서는 『실습과 그림으로 배우는 리눅
www.yes24.com
이번 두 번째 포스트에서는 디바이스 드라이버와, 그 밖에 장치 접근 시 주의 사항에 대해 알아보겠다.
1. 디바이스 드라이버
디바이스 드라이버 커널 기능은 프로세스가 디바이스 파일에 접근할 때 동작하는데, 이때 장치를 직접 조작하려면 각 장치에 내장된 레지스터 영역(CPU 레지스터가 아님에 유의하자)을 읽고 써야 한다. 프로세스 입장에서 보는 장치 조작은 다음과 같다.
- 프로세스가 디바이스 파일을 사용해서 디바이스 드라이버에 장치를 조작하고 싶다고 요청
- CPU가 커널 모드로 전환되고 디바이스 드라이버가 레지스터를 사용해 장치에 요청을 전달한다.
- 장치가 요청에 따라 처리한다.
- 디바이스 드라이버가 장치의 처리 완료를 확인하고 결과를 받는다.
- CPU가 사용자 모드로 전환되고 프로세스가 디바이스 드라이버 처리 완료를 확인해서 결과를 받는다.
즉 디바이스 파일이란 user mode의 프로세스와, kernel mode의 디바이스 드라이버를 연결해주는 역할을 한다.
2. 메모리 맵 입출력
현대적인 장치는 메모리 맵 입출력(memory-mapped IO - MMIO) 구조를 사용해서 디바이스 레지스터에 접근한다. 그러면 레지스터에 대한 정보를 누가 가지고 있을까? 당연히 커널이다. 커널은 자신의 가상 주소에 디바이스 레지스터의 물리 메모리를 매핑하고, MMIO를 채용했다면 레지스터도 매핑한다.
예를 들어, 다음과 같은 레지스터 오프셋이 존재한다고 생각하자. 커널의 레지스터 매핑 시작 가상 주소 + 오프셋에 접근해서 커널이 어떤 명령을 내릴 지 알 수 있다.
레지스터 오프셋 | 역 |
0 | 읽고 쓰기에 사용하는 메모리 영역 시작 주소 |
10 | 저장 장치 내부의 읽고 쓰기에 사용하는 데이터 영역 시작 주소 |
20 | 읽고 쓰는 크기 |
30 | 처리 요청에 사용. 0이면 읽기 요청, 1이면 쓰기 요청 |
40 | 요청한 처리가 끝났는지 여부를 나타내는 플래그 처리를 요청한 시점에 0이 되고 처리가 끝나면 1이 됨 |
커널의 메모리 영역 100~200으로 저장 장치 내부 주소 300~400 영역에 있는 데이터를 읽어 온다고 가정하자. 레지스터 메모리 주소가 500부터 시작이라면 읽기 요청은 다음과 같다.
- 500 : 100(메모리 100부터 저장할것임)
- 510 : 300(저장 장치 주소 300부터 읽을 거임)
- 520 : 100(100만큼 읽을거임 -> 300 ~ 400 읽을 거임)
- 530 : 0(읽기 요청)
- 540 : 0(요청한 시점 -> 처리가 끝나면 1이 됨)
읽기 요청 이후에는, 디바이스에서 300~400 데이터를 메모리 100부터 200까지 전송하고, 요청 끝났다는 표시로 540을 1로 바꾸고, 디바이스 드라이버(커널)이 처리 완료를 확인할 것이다.
이때 처리 완료를 확인하는 방법으로 폴링과 인터럽트 중 하나를 채용한다.
3. 폴링
폴링은 "디바이스 드라이버(커널)" 이 능동적으로 장치에서 처리를 완료했는지 확인한다. 장치는 디바이스 드라이버가 요청한 처리를 완료하면, 처리 완료 통지용 레지스터의 값을 변화시킨다. 디바이스 드라이버는 이 값을 주기적으로 읽기 때문에 값이 변경되면 처리 완료를 확인한다. 그렇다면 폴링을 CPU에서 어떤 방식으로 스케줄링할까? 우선 다음 상황을 가정하자.
- p0 : device에 요청을 보낸 프로세스
- p1 : 또 다른 프로세스
(1) 단순한 폴링
장치가 요청된 처리를 끝낼 때까지 계속해서 레지스터에 요청을 보내는 것이고, 이 과정에서 모든 프로세스의 동작이 멈춘다. -> 큰 낭비
(2) 복잡한 폴링
p0는 디바이스 응답을 받기까지 다음 동작을 진행할 수 없지만, p1은 관계없기 때문에 p1을 동작시키면서, 일정 간격을 두고 레지스터를 확인한다.(p1 - 확인 - p1 - 확인 -...) 동작 완료를 확인했다면, 다시 p0 - p1 - p0 - p1 순으로 프로세스 스케줄링한다.
따라서 폴링을 설계하려면 응답 확인 간격을 어느 정도로 할지를 trade-off로 정해야 한다.
4. 인터럽트
인터럽트(interrupt)는 폴링과 다르게 수동적인 방식으로, 이번에는 장치가 인터럽트 신호를 보내서 완료되었음을 보내준다. 동작 절차는 다음과 같다.
- 디바이스 드라이버가 장치에 처리를 요청한다. 이후 CPU는 다른 처리를 실행한다.
- 장치가 처리를 완료하면 인터럽트 방식으로 CPU에 알린다.
- CPU가 인터럽트 신호를 받으면, 디바이스 드라이버가 인터럽트 컨트롤러 하드웨어에 등록해 둔 인터럽트 핸들러 처리를 호출한다.
- 인터럽트 핸들러가 장치의 처리 결과를 받는다.
이 방식은 폴링에 비해 다루기 쉽다. 왜냐하면 장치 처리 완료를 즉시 확인할 수 있고, 프로세스 스케줄링도 간단하게 p0를 배제만 하면 되기 때문이다. 이러한 장점 때문에 보통 인터럽트를 장치 처리 완료 확인 방법으로 사용한다.
실습으로 확인해보면, /proc/interrupts 파일에서 현재까지 발생한 인터럽트 개수를 알 수 있다.
인터럽트 컨트롤러는 여러 인터럽트 요청(Interrupt ReQuest, IRQ)를 다루는데, 요청마다 서로 다른 인터럽트 핸들러를 등록할 수 있다. 각각의 요청에 IRQ 번호를 할당하는데 각각의 기기마다 번호를 가진다고 보면 되고, 숫자가 아닌 영어는 일단 무시하자. 두 번째 필드부터는 각 논리 CPU에서 발생한 횟수가 나타난다.
5. 디바이스 파일명에 대해
커널은 같은 종류의 장치가 여러 개 연결되어 있을 때 어떻게 디바이스 파일 이름을 만들까? 우선 일정한 규칙이 있다.
- SATA / SAS : /dev/sda, /dev/sdb, /dev/sdc, ...
- NVMe : /dev/nvme0n1, /dev/nvme1n1, ...
그러나 중요한 것은, 무엇이 sda가 되고, 무엇이 sdb가 되는지는 그냥 커널이 장치를 인식하는 순서에만 의존한다. 따라서, 예를 들어 재시작했을 때 어떤 이유로 저장 장치 인식 순서가 바뀌게 된다면 장치명이 바뀌게 된다. 보통 이러한 이슈의 원인은 다음과 같다.
- 다른 저장 장치 추가 : 저장 장치 C를 추가해서 A->C->B 순으로 인식될 경우 sdb는 sdc로 변경된다.
- 저장 장치 위치 변경
- 저장 장치 고장으로 인식 불가능 A->B->C 에서 B가 고장되면 C가 sdb가 된다.
이러한 불상사가 발생한다면, 부팅이 실패할 수도 있고(저장장치에 윈도우가 없음) , 데이터가 손상될 수도 있다. 이러한 문제는 systemd의 udev 프로그램이 만드는 영구 장치명(persistent device name)을 이용하면 해결할 수 있다. 영구 장치명은 /dev/disk/by-path/ 디렉터리 아래에 존재하는, 디스크가 설치된 경로 위치 같은 정보를 바탕으로 만들어진 디바이스 파일이 있다. udev는 기동 또는 장치를 인식할 때마다 설치된 장치 구성이 변화해도 잘 안바뀌는 영구 장치명을 /dev/disk 아래에 자동으로 작성한다.
'Linux > 그림으로 배우는 리눅스 구조' 카테고리의 다른 글
[그림으로 배우는 리눅스 구조] 8. 메모리 계층 (0) | 2024.08.31 |
---|---|
[그림으로 배우는 리눅스 구조] 7. 파일 시스템 (0) | 2024.08.30 |
[그림으로 배우는 리눅스 구조] 6. 장치 접근(1) (0) | 2024.08.30 |
[그림으로 배우는 리눅스 구조] 5. 프로세스 관리(응용편) (0) | 2024.08.29 |
[그림으로 배우는 리눅스 구조] 4. 메모리 관리 시스템(2) (0) | 2024.08.29 |