6장 : 장치접근

프로세스 대신해서 커널이 장치에 접근합니다. 구체적으로는 다음과 같은 인터페이스를 사용합니다.

  • 디바이스 파일이라는 특수한 파일을 조작합니다.

  • 블록 장치에 구축한 파일 시스템을 조작합니다.

  • 파일 시스템은 7장을 참조합니다.

  • 네트워크 인터페이스 카드(NIC)는 속도 등의 문제로 디바이스 파일을 사용하는 대신에 소켓 구조를 사용합니다.

1. 디바이스 파일 : "파일처럼 생긴 장치 조작법"

Device Driver 접근 구조

여기서 중요한 것은 Block Device는 시스템 버퍼 캐시를 사용하고, Character Device는 사용하지 않는다는 점입니다.

프로그램이 파일 입출력을 사용하는 것은 단순하게 느껴지지만 내부적으로는 복잡한 과정이 이루어지는 것을 볼 수 있습니다.

바탕 지식

  • major number : 디바이스를 처리하기 위한 Device Driver 식별 번호, 최대 255

  • minor number : Device Driver가 처리하는 특정 Device를 식별하기 위한 번호

출처 : https://tribal1012.tistory.com/154#:~:text=Device%EC%9D%98%20%EC%A2%85%EB%A5%98%20*%20Block%20Device%20:%20Block,Device%2C%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EA%B4%80%EB%A6%AC%20%EA%B8%B0%EB%8A%A5%EC%9D%84%20%EA%B0%80%EC%A7%84%20%EC%9D%91%EC%9A%A9%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8

📂 디바이스 파일이란?

  • 디바이스 파일은 장치와 커널의 연결점입니다.

  • 예를 들어 /dev/sda, /dev/sdb는 각각 다른 저장 장치(또는 파티션)에 해당합니다.

    • TCP 소켓이나 UDP 소켓을 사용해서 다른 기기와 프로세스 통신할 때 사용

    • 자세히 말하면 저장 장치를 파티션으로 나눴다면 /dev/sdal. /dev/sda2처럼 파티션마다 디바이스 파일이 존재

      • sda : 하나의 저장 장치 전체 (예: SSD 또는 HDD 하나)

      • sda1, sda2, sda3 등 : 그 저장 장치 안의 파티션(=구획, 방)

      /                   <- 루트 디렉토리
      ├── dev             <- 디바이스 파일이 위치한 곳
      │   ├── sda         <- 전체 저장 장치
      │   ├── sda1        <- 첫 번째 파티션
      │   └── sda2        <- 두 번째 파티션

      📦 예시

      상상해보세요! 하나의 외장 하드가 있고, 그 안을 나눠서:

      • C: 드라이브 → /dev/sda1

      • D: 드라이브 → /dev/sda2

      /dev/sda는 하드디스크 전체, /dev/sda1, /dev/sda2는 그 하드디스크를 나눈 각각의 공간이에요!

  • 프로세스는 이 디바이스 파일을 일반 파일처럼 조작해서 장치를 다룹니다.

예시: 저장 장치 /dev/sda  
→ 내부적으로는 HDD/SSD 같은 실제 장치와 연결됨

🔧 디바이스 파일로 장치 조작하기

리눅스는 프로세스가 디바이스 파일을 조작하면 커널 내부의 디바이스 드라이버 device driver라고 부르는 소프트웨어가 사용자 대신에 장치에 접근합니다.

✅ 구조 요약

프로세스 → 디바이스 파일(/dev/XXX) → 디바이스 드라이버 → 실제 장치
  • 사용자는 read(), write() 등의 시스템 콜을 호출해 디바이스 파일을 읽고 씁니다.

    • 일반 파일과 똑같은 방식으로 디바이스 파일 조작

  • 커널 내부에서는 디바이스 드라이버가 이 요청을 해석하고 실제 장치를 조작합니다.

  • 특수한 조작은 ioctl() 시스템 콜로 수행합니다.

🛠️ ioctl()이란?

I/O control의 약자로, 장치(디바이스)에 대해 입출력 외의 특수한 조작을 수행할 수 있도록 해주는 시스템 콜이에요.

read() → 읽기 write() → 쓰기 ioctl() → 그 외 특수한 기능들 제어 (장치 설정, 동작 제어 등)

📦 예시로 이해해봐요!

- "하드 디스크의 온도를 알고 싶다"

- "카메라 장치의 해상도를 바꾸고 싶다"

- "터미널에서 에코 모드를 끄고 싶다"

예1) CD-ROM 꺼내기

CD-ROM 장치는 읽고 쓰는 것도 중요하지만, “열기/닫기” 같은 특수 기능도 있어야 하잖아요?

이럴 때 ioctl()“CD-ROM 트레이 열기” 명령을 보냅니다!

ioctl(cd_fd, CDROMEJECT);

예2) 터미널 설정 바꾸기

터미널 창에서 입력 방식(에코 모드, 키 입력 버퍼 설정 등)을 변경하고 싶을 때도 ioctl()을 사용해 제어할 수 있어요.

struct termios t;
ioctl(STDIN_FILENO, TCGETS, &t);  // 현재 터미널 설정 가져오기
t.c_lflag &= ~ECHO;               // 입력된 키를 화면에 표시하지 않도록 설정
ioctl(STDIN_FILENO, TCSETS, &t);  // 변경된 설정 적용

💡 그럼 왜 read()write()로 안 될까요?

  • read()는 데이터를 읽는 것

  • write()는 데이터를 쓰는 것

하지만 장치는 읽고 쓰기만 하는 게 아니라, 동작 제어나 상태 변경 같은 작업도 필요하거든요.

ioctl()장치별로 정의된 명령어(명령 코드)를 통해 특수한 조작을 가능하게 해줍니다.

ioctl()은 장치와 대화하는 비밀 통로예요! 단순히 읽고 쓰는 걸 넘어, 특수한 명령이나 설정을 조정할 때 사용됩니다.

🔢 디바이스 파일의 중요한 속성

디바이스 파일은 단순한 파일이 아닌, 다음과 같은 고유 정보를 가집니다:

항목
설명

종류

캐릭터 장치(Character Device) 또는 블록 장치(Block Device)

메이저 번호

어떤 드라이버를 사용할지 결정하는 번호

마이너 번호

드라이버 내부에서 어떤 장치인지 구별하는 번호

예를 들어 /dev/sda1, /dev/sda2는 같은 저장장치(sda)의 서로 다른 파티션입니다.

📍 캐릭터 장치 vs 블록 장치

구분
캐릭터 장치
블록 장치

예시

키보드, 마우스, 터미널

하드디스크, SSD, USB

특징

1바이트씩 연속적으로 접근

블록 단위(보통 512B, 4KB)로 접근

전송 방식

스트리밍

랜덤 접근 가능

🚨 접근 권한은 루트만?

  • 대부분의 디바이스 파일은 보안상의 이유로 루트만 접근 가능합니다.

  • 예: /dev/mem(물리 메모리 전체에 접근)은 일반 사용자에겐 매우 위험하므로 제한됨.

🧪 실습: /dev 디렉토리 확인하기

$ ls -l /dev

출력 예시:

brw-rw---- 1 root disk 8, 0 Apr 21 14:12 sda
brw-rw---- 1 root disk 8, 1 Apr 21 14:12 sda1
crw------- 1 root root 10, 61 Apr 21 14:12 cpu_dma_latency

b로 시작: 블록 장치 c로 시작: 캐릭터 장치 8, 0 등은 메이저 번호와 마이너 번호

"디바이스 파일은 하드웨어를 추상화한 문, 커널 드라이버는 그 문의 비밀번호다."


2. 🎛️ 캐릭터 장치(Character Device)의 조작

캐릭터 장치는 연속적인 데이터 흐름을 처리하는 장치로, 대표적인 예는 다음과 같습니다:

  • 단말 (예: 터미널)

  • 키보드

  • 마우스

이 장치들은 읽기(read)와 쓰기(write)는 가능하지만, seek(탐색) 기능은 지원하지 않아요. 즉, 파일처럼 임의 위치로 이동해서 데이터를 읽는 기능은 없다는 뜻입니다.

🖥️ 단말 장치 실습 예시

단말 장치는 디바이스 파일 /dev/pts/X에 매핑되어 있고, 해당 파일을 통해 장치를 직접 조작할 수 있습니다.

✅ 1. 현재 단말 확인

$ ps ax | grep bash
6417 pts/9 Ss
6432 pts/9 S+

여기서 pts/9는 현재 터미널이 사용하는 단말 장치입니다.

/dev/ 아래에 있 는 pts/9 파일이 단말에 대응하는 디바이스 파일

✅ 2. 현재 단말에 문자열 쓰기

$ sudo su
# echo hello > /dev/pts/9

→ 해당 단말에 hello 문자열이 출력됩니다.

  • 디바이스 파일에 write() 시스템 콜을 호출

  • echo hello 명령어를 실행했을 때와 동일한 결과인데 이유가 뭘까요?

echo 명령어는 표준 출력에 hello를 쓰고, 리눅스에서 표준 출력은 단말과 연결되어 있기 때문입니다.

✅ 3. 다른 단말에도 쓰기 가능

“한 터미널에서 다른 터미널 화면에 글씨를 강제로 출력해보기” 실험이에요.

📌 먼저 기본 개념부터 정리

개념
설명

터미널(단말)

우리가 bash 명령어를 입력하는 창

/dev/pts/N

각 터미널에 연결된 디바이스 파일 경로

echo hello > /dev/pts/10

"10번 터미널에 ‘hello’ 라고 써줘" 라는 명령

🧪 실험 과정 요약

1. 여러 터미널을 띄운다

  • 첫 번째 터미널: 내가 명령어를 입력할 곳

  • 두 번째 터미널: 메시지를 받아볼 곳 (아무것도 안 해도 됨)

2. ps 명령어로 각 터미널 번호 확인

$ ps ax | grep bash
6417 pts/9 Ss 0:00 -bash      ← 첫 번째 터미널
6648 pts/10 Ss 0:00 -bash     ← 두 번째 터미널

이걸 보면,

  • 첫 번째 터미널/dev/pts/9

  • 두 번째 터미널/dev/pts/10 이란 뜻이에요.

3. 첫 번째 터미널에서 sudo로 전환한 뒤, 다른 터미널에 메시지 전송

$ sudo su
# echo hello > /dev/pts/10
  • echo hello: "hello"를 출력해라!

  • > /dev/pts/10: 그 출력을 10번 터미널 화면에 보내라!

4. 아무것도 안 하던 두 번째 터미널(pts/10)에 뜨는 화면

$ hello
  • 이건 내가 입력한 게 아니고,

  • 첫 번째 터미널에서 강제로 출력한 hello가 내 터미널에 나타난 것이에요!

🎯 핵심 이해 포인트

  • 리눅스에서는 터미널 하나하나가 다 파일처럼 존재해요 (/dev/pts/번호)

  • 그래서 다른 터미널에다가도 write() 하듯 출력할 수 있어요

  • echo hello > /dev/pts/1010번 터미널에 hello를 강제로 써주는 것이에요.

💬 추가 설명이 필요한 경우

  • sudo가 필요한 이유는? → 다른 사용자 터미널은 루트만 접근 가능해서!

  • pts/10 같은 경로는 어떻게 생기냐면 → pseudo terminal slave라 불리는 가상 터미널 장치에요.

💡 핵심 개념 요약

항목
설명

캐릭터 장치

탐색 불가 / 스트림 기반 데이터 처리

단말 장치 예시

/dev/pts/9, /dev/pts/10

시스템 콜

read(), write() 를 통해 조작

리눅스에서는 '모든 것은 파일'이라는 철학 하에, 장치도 파일처럼 다룰 수 있어요

🖥️ 리눅스에서의 "표준 출력"은?

리눅스나 유닉스 계열 시스템에서 모든 입출력은 "파일"처럼 다룰 수 있어요.

  • stdin (표준 입력, 키보드) → 파일 번호 0

  • stdout (표준 출력, 화면) → 파일 번호 1

  • stderr (표준 에러, 에러 메시지 출력용) → 파일 번호 2

즉, 터미널에서 echo hello를 입력하면, 💡 "hello"라는 문자열을 표준 출력(stdout)에 쓰는 것이에요.

🧵 표준 출력이 "단말 장치"에 연결돼 있다는 건?

지금 내가 명령어를 입력하고 있는 터미널이 바로 단말 장치(device)예요. 리눅스는 이 터미널을 /dev/pts/9 같은 디바이스 파일로 만들어놔요. 즉, 표준 출력은 이 디바이스 파일에 연결돼 있는 상태인 거예요.

🔁 그래서 어떻게 동작하냐면?

echo hello

이 명령은 실제로는 다음과 같이 동작하는 거예요:

  1. echo 프로그램이 실행된다.

  2. 내부적으로 "hello\n" 문자열을 stdout(표준 출력)으로 보낸다.

  3. 리눅스는 이 stdout을 /dev/pts/9 같은 단말 디바이스 파일에 연결해두었다.

  4. 결과적으로 이 문자열은 내가 보고 있는 터미널 화면에 출력된다!

🧪 실험적으로 확인해볼 수도 있어요!

$ echo hello > /dev/pts/10

➡ 이 명령은 echo hello다른 터미널에 출력하라는 의미예요. 왜냐하면 /dev/pts/10은 다른 단말(터미널)을 나타내니까요!

🔚 한줄 요약

echo hello는 사실 stdout에 "hello"를 출력하는 것이고, 이 stdout은 리눅스에서 내 터미널 디바이스 파일(/dev/pts/9)에 연결되어 있기 때문에, 화면에 보이게 되는 거예요!


3. 📦 블록 장치와 루프 장치, 그리고 직접 조작 실습!

1️⃣ 블록 장치란?

  • 블록 단위로 읽고 쓰기가 가능하고, 탐색도 가능한 장치

  • 대표 장치: 하드디스크, SSD 등 저장 장치

  • 보통은 파일 시스템을 통해 접근하지만, 실습에서는 파일 시스템 없이 직접 조작도 가능

2️⃣ 블록 디바이스 파일 직접 조작 예시

✅ 준비 과정

  1. 비어 있는 파티션 /dev/sdc7을 사용한다고 가정

  • 비어 있는 파티션이 없다면 나중에 설명하는 루프 장치 컬럼을 참조해서 루프 장치를 사용

    • 비어 있는 파티션이 없다는 말은?

      • 실습이나 테스트를 위해 직접 블록 장치(/dev/sdc7 같은)에 접근하려면,

      • 다른 데이터가 없는 빈 파티션이 필요해요.

        • 그런데 대부분의 시스템은 이미 모든 디스크 공간이 OS나 사용자 데이터로 꽉 차 있어요.

      ⚠️ 이럴 때 잘못된 파티션을 실습에 사용하면 데이터가 날아가는 대참사가 발생할 수 있습니다

  1. ext4 파일 시스템을 생성:

# mkfs.ext4 /dev/sdc7
  1. 마운트:

# mount /dev/sdc7 /mnt
# echo "hello world" > /mnt/testfile
# umount /mnt

1) mount /dev/sdc7 /mnt

💡 "디바이스 파일 /dev/sdc7에 있는 파일 시스템을 /mnt 디렉토리에 연결해줘!"

  • /dev/sdc7: 디스크나 파티션(예: 외장하드, USB 등)의 블록 디바이스 파일

  • /mnt: 리눅스에서 임시로 디스크를 붙이는 디렉토리

📌 이걸 마운트하면:

  • /mnt 아래에서 /dev/sdc7의 내용을 일반 디렉토리처럼 접근할 수 있어요.

  • 예: /mnt/testfile은 실제 /dev/sdc7에 저장되는 것!

2) echo "hello world" > /mnt/testfile

💡 "/mnt/testfile이라는 새 파일을 만들고, 그 안에 'hello world' 문자열을 써!"

  • 이때 /mnt는 위에서 마운트한 /dev/sdc7이기 때문에,

  • 결과적으로는 /dev/sdc7 디스크에 "hello world"라는 내용을 가진 파일이 생깁니다!

3) umount /mnt

💡 "이제 디스크 사용 끝났으니 /mnt에서 연결 해제할게!"

  • umount는 "unmount"의 줄임말.

  • 연결을 해제하면 /mnt는 다시 빈 디렉토리가 되고,

  • /dev/sdc7은 더 이상 접근할 수 없게 됩니다.

📝 요약 흐름 정리

단계
명령어
의미

1

mount

디바이스를 디렉토리에 붙임

2

echo >

붙인 디스크에 파일 생성 및 쓰기

3

umount

디바이스와 디렉토리 연결 해제

🧠 실전 응용 팁

  • 실수로 umount 안 하고 디스크 뽑으면? → 데이터 손상 가능 😱

  • umount로 안전하게 연결 해제하고 빼야 해요!

✅ 파일 내용 직접 확인

# strings -tx /dev/sdc7
...
f35020 lost+found
f35034 testfile
...

출력 예:

803d000 hello world
10008020 lost+found
10008034 testfile

이건 /mnt/testfile에 쓴 "hello world"가 실제 블록 장치 /dev/sdc7 안의 803d000 위치에 저장되었음을 보여줍니다.

출력 결과에서 /dev/sdc7에는

  • lost+found 디렉터리 및 testfile 파일명

  • 파일 내부에 있는 hello world 문자열

정보가 들어 있습니다.

각각의 문자열이 두 번 출력된 건 ext4의 저널링 기능 때문인데 저널링은 데이터를 쓰기 전에 저널 영역이라고 부르는 장소에 함께 기록합니다

✅ 블록 장치 직접 조작 (위험하니 주의!)

$ echo "HELLO WORLD" > testfile-overwrite
$ sudo dd if=testfile-overwrite of=/dev/sdc7 bs=1 seek=$((0x803d000))

✅ 결과 확인

# mount /dev/sdc7 /mnt
# cat /mnt/testfile
HELLO WORLD

🧙‍♂️ 파일 시스템을 거치지 않고 직접 디바이스 파일을 조작했어요!

📕 저널링이란?

데이터를 디스크에 기록하기 전에, “이 작업을 할 거야!”라는 계획을 따로 미리 적어두는 것입니다.

이 메모장을 “저널(Journal)”이라고 부릅니다. 마치 일기장이죠! 어떤 일이 일어날지 적어놓고, 나중에 진짜로 실행하는 방식이에요.

📦 예시: 파일에 문자열 쓰기

echo "hello world" > /mnt/testfile

이 명령어를 실행하면:

  1. 먼저 ext4 파일 시스템은 "나는 testfile이라는 파일에 'hello world'를 쓸 거야!" 라는 내용을 저널 영역에 먼저 기록합니다.

  2. 그 다음에야 실제 데이터 블록에 진짜로 저장을 시작해요.

💡 왜 이렇게 번거롭게 할까요?

바로, 데이터 손상 방지 때문입니다!

😱 만약 저널링이 없다면?

  • 저장 중 갑자기 정전이 되거나, 디스크가 중간에 제거되면…

  • 파일의 일부만 저장되거나, 파일 시스템이 손상될 수 있어요.

  • 특히 디렉토리 구조나 메타데이터가 꼬이면 부팅조차 안 될 수도 있어요!

🧯 저널링의 효과

  • 시스템이 재부팅되면 저널을 보고 "아, 이 작업은 끝나지 않았구나" 를 감지해서

  • 중단된 작업을 롤백하거나 다시 실행해서 일관성을 복구할 수 있어요.

👉 저널링 덕분에 ext4는 갑작스런 장애가 있어도 파일 시스템을 안전하게 유지할 수 있어요.

📌 그래서 두 번 보이는 이유?

앞서 strings -tx /dev/sdc7 명령어를 썼을 때

803d000 hello world
10008034 hello world

이렇게 두 번 나왔던 이유는:

  • 하나는 실제 데이터 블록

  • 하나는 저널 영역에 미리 써둔 로그

이라는 의미예요!

저널링은 파일 시스템의 "일기장"! 데이터를 쓰기 전에 먼저 기록해서, 문제가 생겨도 복구할 수 있게 해줍니다.

3️⃣ 루프 장치 (Loop Device)란?

파일을 디바이스처럼 사용할 수 있게 해주는 기능 하드웨어 파티션이 없는 경우에 실험 용도로 딱!

✅ 루프 장치 생성 및 연결

$ fallocate -l 16M loopdevice.img
$ sudo losetup -f loopdevice.img
$ losetup -l

출력 예:

/dev/loop0  ... /home/user/loopdevice.img

✅ ext4 파일 시스템 만들고 마운트

$ sudo mkfs.ext4 /dev/loop0
$ mkdir mnt
$ sudo mount /dev/loop0 mnt
$ echo "test" > mnt/example.txt

✅ 사용이 끝났다면 정리하기

$ sudo umount mnt
$ sudo losetup -d /dev/loop0
$ rm loopdevice.img

✅ 요약 표

개념
설명

블록 장치

하드디스크, SSD 등. 블록 단위로 탐색 가능

디바이스 파일

/dev/sda, /dev/sdc7 등. 커널이 장치를 매핑한 가상 경로

루프 장치

일반 파일을 블록 장치처럼 다루는 기능

strings -tx

바이너리 파일 내 문자열 검색 (파일 오프셋과 함께)

dd

블록 단위 복사 도구. 디바이스 조작 가능

블록 장치는 일반 파일보다 더 직접적이고 강력한 방식으로 저장소를 다룹니다. 루프 장치는 실험과 테스트 환경에서 매우 유용한 가상 장치입니다.



🎛️ 디바이스 드라이버와 메모리 맵 입출력(MMIO)

✅ 디바이스 드라이버란?

디바이스 드라이버는 커널이 장치를 제어하기 위해 사용하는 소프트웨어입니다.

"프로세스 → 디바이스 파일을 통해 → 드라이버 → 장치"

라는 식으로 연결되며, 사용자(프로세스)는 장치의 복잡한 동작을 몰라도 일반 파일처럼 다룰 수 있게 됩니다.


🧱 디바이스 드라이버의 동작 흐름

장치에는 레지스터(register) 라는 내부 설정/데이터 영역이 있고, 이것을 통해 장치를 조작합니다. 예를 들어 저장 장치에 데이터를 읽어오려면 다음과 같은 순서로 진행됩니다:

📦 동작 절차 (읽기 기준)

  • 장치를 직접 조작하려면 각 장치에 내장된 레지스터 영역을 읽고 써야 합니다.

  • 구체적인 레지스 터 종류와 조작법 같은 정보는 각 장치 사양에 따라 달라집니다.

단계
동작

1️⃣

프로세스가 디바이스 파일을 통해 read() 호출

2️⃣

CPU가 커널 모드로 전환됨

3️⃣

디바이스 드라이버가 레지스터에 "읽기 작업" 요청 (주소/크기/명령 입력)

4️⃣

장치가 요청을 받아서 처리

5️⃣

완료되면 장치가 레지스터 플래그로 알려줌

6️⃣

CPU가 사용자 모드로 돌아오고, 결과를 프로세스에게 전달


🧠 MMIO (Memory-Mapped I/O)란?

장치의 레지스터를 메모리처럼 접근하는 방식

x86_64 아키텍처는 리눅스 커널이 자신의 가상 주소 공간에 물리 메모리를 모두 매핑합니다. 커널의 가상 주소 공간 범위가 0~1000바이트라고 하면, 예를 들어 [그림 06-03]처럼 가상 주 소 공간의 0~500에 물리 메모리를 매핑합니다.

MMIO를 사용하면 레지스터 접근이 마치 메모리에 쓰는 것처럼 됩니다. 예를 들어 아래와 같이 커널 메모리 공간에 장치 0, 1, 2의 레지스터가 매핑됩니다:

주소         설명
0 ~ 500     일반 메모리
500 ~ 600   장치 0 레지스터
600 ~ 700   장치 1 레지스터
700 ~ 800   장치 2 레지스터

이 주소 범위에 값을 쓰거나 읽는 걸로 장치를 조작하는 것이죠!


💾 저장 장치 예시: 데이터 읽기 흐름

🎯 목표

메모리 100 ~ 200 영역에, 저장장치의 300 ~ 400 주소 데이터를 읽어오자!

📌 사용되는 레지스터 (오프셋 기준)

레지스터 오프셋
역할

0

메모리로 데이터를 저장할 시작 주소

10

저장장치 내부에서 읽을 시작 주소

20

읽을 데이터 크기

30

요청 명령 (0: read, 1: write)

40

처리 완료 플래그 (0: 진행 중, 1: 완료)

🧭 단계별 흐름

  1. 레지스터 설정:

    • 0x500 = 100 (메모리 주소)

    • 0x510 = 300 (저장장치 주소)

    • 0x520 = 100 (읽을 크기)

  2. 0x530 = 0 (읽기 명령 실행)

  3. 저장장치가 작업 시작 → 0x540 = 0 (처리 중)

그 이후

  1. 작업 완료 후 → 0x540 = 1

  2. 디바이스 드라이버가 완료 확인


🔄 처리 완료 확인 방법 2가지

방법
설명

폴링(Polling)

계속 레지스터를 읽어서 완료 여부를 확인

인터럽트(Interrupt)

장치가 직접 CPU에 알려주는 방식 (효율적)


📌 핵심 요약

개념
설명

디바이스 드라이버

커널이 장치와 통신할 때 사용하는 프로그램

MMIO

장치 레지스터를 메모리에 매핑해서 접근하는 방식

레지스터

장치에 명령을 내리거나 상태를 확인하는 용도

폴링/인터럽트

장치 처리 완료 여부를 확인하는 방법

좋아요! 주신 내용을 기반으로 폴링(Polling) 방식이 무엇인지, 왜 비효율적인지, 그리고 이를 개선하기 위한 방법까지 차근차근 설명드릴게요 😊


🔁 폴링(Polling)이란?

“끝났어?”를 계속 묻는 방식


✅ 폴링이란?

디바이스 드라이버가 장치에게 계속 “끝났나요?” 하고 물어보는 방식입니다.

  • 프로세스가 장치에게 어떤 작업(예: 저장, 출력 등)을 요청합니다.

  • 이 요청은 커널을 통해 디바이스 드라이버로 전달됩니다.

  • 드라이버는 장치의 "처리 완료 레지스터" 값을 계속 읽어서, 작업이 끝났는지 감시합니다.

이런 동작은 마치 이런 상황과 같습니다:

👤 “답장 왔나?” 📱 (앱 열어봄) “아직...” 👤 “답장 왔나?” 📱 (앱 열어봄) “아직...”

이런 식으로 계속 확인하는 거예요!


❗ 폴링의 문제점

1. CPU 낭비

  • CPU는 레지스터 값을 확인하는 동안 다른 일을 못 합니다.

  • 예를 들어 프로세스 p0이 장치에 요청을 하고 기다리는 동안,

  • 프로세스 p1이 아무 일도 하지 못하고 같이 기다리게 되는 일이 벌어집니다.

CPU는 초당 수십억 개 명령을 실행할 수 있는데, 몇 밀리초를 기다리느라 놀고 있다면 너무 아깝겠죠?

🔄 개선된 폴링

"계속 확인" 대신, 주기적으로 확인하는 방법!

⏰ 일정 시간마다만 장치에 “끝났니?”라고 물어보는 것.

이 방법은 CPU 낭비를 줄여주지만, 여전히 고민거리가 있어요:

이렇게 정교하게 만들어도 폴링은 디바이스 드라이버가 복잡해진다는 단점이 있습니다.

예를 들어 [그림 06-08]에서 장치에 처리를 요청하고 완료할 때까지 pl이 동작한다면, P1에서 하는 처리 중간 중간에 레지스터 값을 읽는 코드를 삽입해야 합니다. 또한

  • 간격이 너무 길면: 처리 완료 통지가 늦어짐

  • 간격이 너무 짧으면: CPU 낭비는 여전

🧩 그림으로 요약해 볼게요

📊 단순 폴링

[CPU]
 → 확인 → 확인 → 확인 → 확인 → ...
 → 다른 일 못함 😢

장치가 아직 처리를 끝내지 않았는데, 계속 CPU가 ‘확인만’ 반복하는 구조.

📊 주기적 폴링

[CPU]
 → 다른 일 → 확인 → 다른 일 → 확인 → ...
  • CPU가 다른 일도 하면서, 중간중간 확인

  • 그래도 "얼마나 자주?"라는 간격 조절이 난이도 높음

📌 핵심 요약

구분
설명

폴링(Polling)

디바이스 상태를 드라이버가 직접 주기적으로 확인

장점

구조가 단순하다

단점

CPU 낭비 심함, 응답 지연 가능

개선 방법

주기적 폴링 (하지만 간격 조절이 어려움)

🧠 그래서 등장한 해결책이 바로 “인터럽트(Interrupt)” 방식입니다. → 장치가 “끝났어!” 하고 알려주는 방식이랍니다


🧠 인터럽트란? – 장치가 먼저 말 걸어오는 효율적인 소통 방식

🛎️ 장치의 "처리 완료 알림" 방법

1) 폴링이란? (비효율적인 방식)

먼저 폴링을 떠올려 봅시다. 이 방식은 마치 친구한테 문자를 보내놓고, 계속 휴대폰을 켜서 “답장 왔나?”를 확인하는 것과 비슷합니다. CPU가 계속 디바이스 상태를 확인하는 것이죠.

하지만 단점이 큽니다:

  • 답장이 올 때까지 CPU가 다른 일을 못함

  • 확인하는 시간 동안 자원 낭비 심함

  • 예) p0이 장치 요청을 하고, 처리 끝날 때까지 p1조차 못 돌리는 구조

⚡ 그렇다면 인터럽트는?

인터럽트(interrupt)는 장치가 “나 다 끝냈어!” 하고 먼저 CPU를 부르는 방식입니다.

📌 동작 흐름은 이렇습니다:

  1. 디바이스 드라이버가 장치에 요청을 보냅니다.

  2. CPU는 요청을 보낸 뒤 다른 일(예: p1 실행)을 합니다.

  3. 장치가 처리를 끝내면 인터럽트 신호를 CPU에 보냅니다.

  4. CPU는 그 신호를 받아서 미리 등록된 **인터럽트 핸들러(interrupt handler)**를 실행합니다.

  5. 드라이버는 이 핸들러를 통해 결과를 확인합니다.

📱 일상 속 비유: 채팅 앱 알림

  • 폴링: 앱 열어보며 계속 “답 왔나?” 확인

  • 인터럽트: 답 오면 앱이 알림을 보내줌

✅ 인터럽트의 장점

항목
내용

CPU 자원 낭비 없음

기다리는 동안 다른 일 가능

즉시 반응 가능

장치가 바로 알려주니 지연 적음

프로세스 독립성 보장

장치 처리와 무관한 프로세스는 영향 없음

📌 요즘 대부분의 하드웨어 I/O는 인터럽트 기반입니다.

/proc/interrupts 파일이란?

리눅스에서는 현재까지 발생한 인터럽트 개수를 확인할 수 있는 파일이 있어요. 바로 /proc/interrupts입니다.

이 파일을 보면 다음과 같은 형식의 데이터가 나옵니다:

CPU0        CPU1        CPU2       ...
0:     10000       8000       5000     IO-APIC-edge   timer
1:       200        150        120     IO-APIC-edge   keyboard
LOC: 21864665 18393529 28227980 84045773   Local timer interrupts

📌 각 필드 설명

항목
의미

첫 번째 필드

인터럽트 번호 또는 구분자 (0, 1, LOC, 등)

두 번째~여러 번째 필드

논리 CPU에서 발생한 인터럽트 횟수

마지막 필드

해당 인터럽트가 어떤 장치 또는 기능에 대응되는지 (예: timer, keyboard, Local timer)

예시에서 LOC:Local Timer Interrupt를 의미합니다.

🧪 실습: 타이머 인터럽트 실시간 확인

$ while true; do grep Local /proc/interrupts; sleep 1; done

이 명령어는 매초마다 /proc/interrupts에서 타이머 인터럽트 수를 확인하는 거예요.

출력은 이렇게 나와요:

LOC: 21864665 18393529 28227980 84045773 ...
LOC: 21864669 18393529 28227983 84045788 ...
LOC: 21864735 18393584 28228116 84046062 ...

→ 매초 인터럽트 횟수가 늘어나는 것을 확인할 수 있어요!

⏱️ 타이머 인터럽트란?

타이머 인터럽트는 운영체제가 정기적으로 시간을 체크하거나 작업을 스케줄링하기 위해 발생시키는 신호입니다.

  • 예전에는 1초에 1000번씩 모든 CPU에서 발생했지만,

  • 지금은 필요할 때만 발생하도록 최적화되어 있습니다!

⚡ 왜 타이머 인터럽트를 줄였을까?

이유
설명

💡 성능 향상

CPU가 너무 자주 모드 전환(커널 ↔ 사용자)을 하지 않게 됨

🔋 전력 절감

대기 상태의 CPU에 쓸데없는 깨움을 줄일 수 있음

즉, 스마트하게 시간을 관리해서 자원을 아끼는 방식으로 진화한 것이죠.

🧠 요약 정리

개념
설명

/proc/interrupts

인터럽트 발생 횟수를 확인하는 파일

LOC:

Local timer interrupt를 나타냄

타이머 인터럽트

CPU에게 "정해진 시간 됐어!"를 알리는 신호

변화

과거: 매초 1000회 → 현재: 필요할 때만

🌀 그럼에도 불구하고 폴링을 쓰는 경우는?

🙃 예외적인 상황

  • 장치 속도가 너무 빠르거나

  • 처리 빈도가 너무 높아서

  • 인터럽트 오버헤드(호출 자체의 시간)가 더 손해일 때

이럴 땐 폴링으로 전환하는 디바이스 드라이버도 있습니다.

🧪 보너스: 사용자 공간 입출력 (UIO)

리눅스는 디바이스 레지스터를 직접 매핑해서, 사용자 공간(예: Python)에서도 디바이스 드라이버를 만들 수 있게 해줘요.

이걸 UIO (Userspace I/O) 라고 합니다.

  • 인터럽트 없이도 장치에 빠르게 접근 가능

  • 커널 모드 전환을 피하니 속도 향상

  • 고속 처리 필요할 때 사용

대표 예: DPDK, SPDK 같은 고속 네트워크/스토리지 프레임워크

🔚 요약

개념
설명
예시

폴링

CPU가 직접 장치 상태를 계속 확인

문자 확인 계속 눌러보는 친구

인터럽트

장치가 먼저 "끝났어!" 알려줌

채팅 앱 알림

UIO

사용자 공간에서 디바이스 접근

파이썬으로 드라이버 만들기 가능


📦 디바이스 파일명, 왜 조심해야 할까?

1) 🧩 문제 상황: 장치 이름이 바뀐다?

리눅스에서는 저장 장치가 연결될 때, 자동으로 디바이스 파일 이름(/dev/sda, /dev/sdb 등)이 정해집니다.

하지만 이 이름은 '고정된 것'이 아니며, 매번 부팅할 때마다 달라질 수 있어요!

📉 이름이 바뀌는 이유

상황
바뀌는 이유

새 장치 추가

A → B 순서였는데, 중간에 C가 추가되면 이름이 밀릴 수 있음

장치 연결 순서 변경

하드웨어 연결 순서 바뀌면 인식 순서도 바뀜

고장 발생

예: A가 고장 → B가 /dev/sda가 되어버림

⚠️ 실수로 /dev/sdc에 파일 시스템을 만들려 했는데, 장치 순서 바뀌어서 /dev/sdb를 지우는 상황도 발생할 수 있습니다.

2) ✅ 해결책: "영구 장치 이름(Persistent Name)" 사용하기

이런 문제를 막기 위해 리눅스는 udev라는 프로그램을 통해, **"바뀌지 않는 이름"**을 자동으로 만들어줍니다!

🗂️ udev가 만들어주는 이름 종류

경로
의미

/dev/disk/by-path/

장치의 물리적 연결 경로 기반 이름

/dev/disk/by-uuid/

파일 시스템의 UUID(고유 ID) 기반 이름

/dev/disk/by-label/

사용자가 설정한 레이블 기반 이름

🔍 예시: 같은 장치의 다양한 이름

$ ls -l /dev/sda
brw-rw---- 1 root disk 8, 0 /dev/sda

$ ls -l /dev/disk/by-path/acpi-VMBUS:00-scsi-0:0:0:0
-> ../../sda

/dev/sda/dev/disk/by-path/...같은 장치를 가리키고 있지만, by-path는 다음 부팅에도 바뀌지 않음!

3) 🧾 /etc/fstab에 적용하는 예시

시스템 부팅 시 자동으로 디스크를 마운트할 때는, 장치명 대신 UUID를 쓰는 게 안정적입니다.

$ cat /etc/fstab
UUID=077f5c8f-a2f3-4b7f-be96-b7f2d31d07fe /       ext4 defaults 0 0
UUID=C922-4DDC                          /boot/efi vfat defaults 0 0

이렇게 하면 /dev/sda/dev/sdbUUID로 정확한 디스크를 구분하니, 실수로 잘못 마운트하거나 포맷하는 일을 방지할 수 있어요!

📌 요약 정리

개념
설명

디바이스 파일명은 부팅할 때마다 바뀔 수 있음

ex. /dev/sda/dev/sdb

원인

인식 순서, 연결 순서, 고장 등

해결책

udev가 만든 영구 장치명 사용 (UUID, by-path 등)

실수 방지

/etc/fstab에 UUID로 마운트 설정하면 안전!

🔒 한 줄 요약

장치 이름은 믿지 말고, UUID를 믿자!

Last updated