유닉스 시스템에서 대부분의 장치를 쉽게 다룰 수 있는 이유는 커널이 많은 장치 I/O 인터페이스를 사용자 프로세스에 파일 형태로 제공하기 때문이다. 이러한 장치 파일을 장치 노드(device nodes)라고 부르기도 한다.
일부 장치는 cat 같은 표준 프로그램을 통해 접근할 수 있다. 하지만 파일 인터페이스로 할 수 있는 작업에는 한계가 있기 때문에 모든 장치나 장치의 기능을 linux 표준 파일 I/O만으로 접근할 수 있는 것은 아니다.
Linux는 다른 유닉스 계열 운영체제들과 동일한 방식으로 장치 파일을 설계한다. 장치 파일은 /dev
디렉터리에 있으며, ls /dev
명령을 실행하면 /dev
내에 많은 파일이 존재함을 확인할 수 있다.
시작하기 위해서 다음의 명령어를 생각해보자.
echo blah blah > /dev/null
다른 명령어들과 마찬가지로 출력 리디렉션을 사용하면 표준 출력에서 나온 데이터를 파일로 보낼 수 있다. 하지만 이 경우, 파일은 /dev/null
이라는 장치이며, 커널은 일반적인 파일 조작을 우회하고, 이 장치에 기록된 데이터에 대해 장치 드라이버를 사용한다. /dev/null
의 경우, 커널은 입력된 데이터를 단순히 받아들이고 버린다.
장치를 식별하고 해당 장치의 권한을 확인하려면 ls -l
명령을 사용한다. 다음은 몇 가지 예시이다.
$ ls -l
brw-rw---- 1 root disk 8, 1 Sep 6 08:37 sda1
crw-rw-rw- 1 root root 1, 3 Sep 6 08:37 null
prw-r--r-- 1 root root 0 Mar 3 19:17 fdata
srw-rw-rw- 1 root root 0 Dec 18 07:43 log
file의 첫번째 character를 보면 b
, c
, p
, s
file이 있다. 이는 device라는 것을 말하는 것이다. 각각은 block
, character
, pipe
, socket
이다.
블록 장치(Block Device): 프로그램은 블록 장치에서 데이터를 고정된 크기의 청크(chunk) 단위로 접근한다. 앞서 예제로 사용된 sda1
은 디스크 장치이며, 이는 블록 장치의 한 종류이다. 디스크는 데이터를 블록 단위로 쉽게 나눌 수 있다. 블록 장치는 전체 크기가 고정되어 있고 인덱싱이 용이하기 때문에, 커널의 도움을 받아 장치 내 어느 블록이든 빠르게 랜덤 액세스할 수 있다.
문자 장치(Character Device): 문자 장치는 데이터 스트림을 처리한다. /dev/null
에서 살펴본 것처럼, 문자 장치는 문자 데이터를 읽거나 쓸 수만 있다. 문자 장치는 크기가 존재하지 않으며, 해당 장치를 읽거나 쓸 때 커널이 보통 직접 읽기 또는 쓰기 작업을 수행한다. 컴퓨터에 직접 연결된 프린터는 문자 장치로 표현된다. 문자 장치와 상호작용할 때 중요한 점은, 커널이 데이터를 장치나 프로세스로 전달한 후에는 다시 되돌아가 데이터를 재검토할 수 없다는 것이다.
파이프 장치(Pipe Device): 이름 있는 파이프(named pipe)는 문자 장치와 유사하지만, 커널 드라이버 대신 I/O 스트림의 반대쪽에 다른 프로세스가 존재한다.
소켓 장치(Socket Device): 소켓은 주로 프로세스 간 통신(IPC, Interprocess Communication)에 사용되는 특수한 인터페이스이다. 이러한 소켓은 /dev
디렉터리 외부에서 발견되는 경우가 많다. 소켓 파일은 유닉스 도메인 소켓을 나타내며, 이에 대한 자세한 내용은 추후에 다루도록 하자.
ls -l 명령으로 블록 및 문자 장치를 나열하면, 날짜 앞에 나타나는 숫자들은 해당 장치를 식별하는 주 번호(major number)와 부 번호(minor number)를 의미한다. 일반적으로 유사한 장치들은 동일한 주 번호(major number)를 가진다. 예를 들어,sda3
과 sdb1
은 둘 다 하드 디스크 파티션이며, 동일한 주 번호를 공유할 가능성이 높다.
단, 모든 device들이 device 파일들을 가지는 것은 아니다. 가령, network interface의 경우는 이론적으로 하나의 character device로 구현이 가능하나, 이는 매우 어렵고 비효율적이다. 따라서, kernel이 다른 I/O interface를 제공하고 따로 device file을 가지지 않는다.
dd
프로그램은 블록 장치(block device)와 문자 장치(character device)를 다룰 때 매우 유용하다. 이 프로그램의 유일한 기능은 입력 파일 또는 스트림에서 데이터를 읽고 출력 파일 또는 스트림으로 쓰는 것이며, 필요하면 변환 작업도 수행할 수 있다.
특히 dd
는 블록 장치를 다룰 때 유용한 기능을 제공하는데, 파일의 중간에 있는 특정 데이터 블록만 처리하고 앞뒤의 데이터를 무시할 수 있다.
dd
는 매우 강력한 도구이므로 실행할 때 반드시 신중해야 한다. 실수로 잘못된 명령을 실행하면 장치나 파일의 데이터를 쉽게 손상시킬 수 있다. 만약dd
가 정확히 어떤 동작을 수행할지 확신이 없다면, 먼저 새로운 파일에 출력을 저장하는 것이 도움이 된다.
dd
는 고정된 크기의 블록 단위로 데이터를 복사한다. 문자 장치와 함께 dd
를 사용하는 예제는 다음과 같다.
dd if=/dev/zero of=new_file bs=1024 count=1
위에서 볼 수 있듯이, dd
의 옵션 형식은 대부분의 유닉스 명령과 다르다. 이는 오래된 IBM Job Control Language (JCL) 스타일을 기반으로 한다. 대부분의 유닉스 명령이 옵션 앞에 대시(-)를 붙이는 것과 달리, dd는 옵션 이름과 값을 = 기호로 구분한다. 위의 명령은 /dev/zero
(연속적인 0 바이트 스트림)에서 1024바이트 크기의 블록 하나를 new_file
로 복사하는 작업을 수행한다.
dd의 주요 옵션
1. if=: 입력 파일을 지정한다. 기본값은 표준 입력이다.
of=: 출력 파일을 지정한다. 기본값은 표준 출력이다.
bs=: 블록 크기를 지정한다. dd는 한 번에 이 크기만큼 데이터를 읽고 쓴다.
큰 블록 크기를 간단하게 표기하기 위해 다음 단위를 사용할 수 있다.
b → 512바이트
k → 1,024바이트
따라서 위의 예제에서 bs=1024 대신 bs=1k를 사용할 수도 있다.
입력과 출력의 블록 크기가 동일하면 bs 옵션을 사용하는 것이 좋다.
그러나 서로 다른 블록 크기를 사용해야 한다면 ibs와 obs를 각각 지정해야 한다.
매우 큰 파일을 다룰 때나 /dev/zero
처럼 무한한 데이터 스트림을 제공하는 장치에서 작업할 때 유용하다.
이 옵션을 사용하지 않으면 dd
는 입력이 끝날 때까지 계속 실행되므로, 디스크 공간이나 CPU 시간을 낭비할 수 있다.
count
는 skip
옵션과 함께 사용하여 큰 파일이나 장치에서 일부 데이터만 복사하는 데 활용할 수 있다.
건너뛴 블록은 출력 파일로 복사되지 않는다.
dd
는 실제로 특정 app이 대규모 트래픽을 감당할 수 있는 지 없는 지 테스트할 때 유용하게 사용된다. 굳이 시뮬레이터를 사용해서 traffic을 발생시키지 않고, socke에 직접 dd
로 대량의 input stream을 주도록 해서 app이 견고하게 이를 받아들일 수 있는 지 없는 지 확인할 수 있다.
디스크를 파티셔닝할 때처럼 장치의 이름을 찾는 것이 어려울 수 있다. 다음은 장치 이름을 확인하는 몇 가지 방법이다. 단, 현재는 그냥 이런게 있구나만 알아두도록 하자. 아래에 더 자세하게 서술할 것이다.
udevadm
을 사용하여 udevd
에 질의한다. /sys
디렉터리에서 장치를 찾는다.journalctl -k
명령(커널 메시지를 출력함)이나 커널 시스템 로그에서 장치 이름을 추측한다. mount
명령의 출력을 확인한다.cat /proc/devices
를 실행하여 현재 시스템에서 드라이버가 로드된 블록 및 문자 장치 목록을 확인한다. 각 줄은 번호와 이름으로 구성되어 있으며, 이 번호는 주(major) 번호를 의미한다./dev
디렉터리에서 해당 주 번호와 일치하는 문자 또는 블록 장치 파일을 찾으면 된다.udevadm
사용)이 가장 신뢰할 수 있는 방법이지만, 이를 사용하려면 udev
가 활성화되어 있어야 한다.udev
를 사용할 수 없는 상황이라면 다른 방법을 시도할 수 있지만, 커널이 특정 하드웨어에 대해 장치 파일을 생성하지 않았을 수도 있음을 유의해야 한다.아래는 가장 일반적인 linux device의 이름과 명명 규칙을 설명한다.
대부분의 하드 디스크는 현재 Linux 시스템에서 /dev/sda
, /dev/sdb
등의 sd
접두사가 붙은 장치 이름을 갖는다. 이러한 장치는 전체 디스크를 나타내며, 커널은 디스크에 대한 파티션을 나타내기 위해 /dev/sda1
, /dev/sda2
와 같은 별도의 장치 파일을 생성한다.
이 명명 규칙에는 약간의 설명이 필요하다. 이름의 sd
부분은 SCSI
디스크를 나타낸다. SCSI(Small Computer System Interface)는 원래 디스크와 다른 주변 장치 간의 통신을 위한 하드웨어 및 프로토콜 표준으로 개발되었다. 전통적인 SCSI
하드웨어는 대부분의 현대 시스템에서는 사용되지 않지만, SCSI
프로토콜은 매우 적응력이 뛰어나기 때문에 여전히 많이 사용된다. 예를 들어, USB
저장 장치도 SCSI
프로토콜을 사용하여 통신한다. SATA
(Serial ATA) 디스크에 대한 이야기는 좀 더 복잡하지만, Linux 커널은 여전히 이러한 디스크와 통신할 때 SCSI
명령을 사용한다.
시스템에서 SCSI
장치를 나열하려면, sysfs
에서 제공하는 장치 경로를 순회하는 유틸리티를 사용할 수 있다. 가장 간결한 도구 중 하나는 lsscsi
이다. 이를 실행하면 다음과 같은 결과를 얻을 수 있다:
lsscsi
[0:0:0:0] 1 disk ATA WDC WD3200AAJS-2 01.0 /dev/sda3
[2:0:0:0] disk FLASH Drive UT_USB20 0.00 /dev/sdb
첫 번째 열은 장치의 주소를 나타내고, 두 번째 열은 장치 종류를, 마지막 열은 장치 파일을 찾을 위치를 나타낸다. 나머지는 벤더 정보이다.
Linux는 장치 드라이버가 장치를 만나는 순서대로 장치 파일에 장치를 할당한다. 예를 들어, 위의 예에서 커널은 먼저 디스크를 찾고, 그다음 플래시 드라이브를 찾았다.
이러한 장치 할당 방식은 하드웨어를 재구성할 때 문제를 일으킬 수 있다. 예를 들어, /dev/sda
, /dev/sdb
, /dev/sdc
라는 세 개의 디스크가 있는 시스템에서 /dev/sdb
가 고장 나서 제거해야 한다면, 기존의 /dev/sdc
는 /dev/sdb
로 이동하고 더 이상 /dev/sdc
는 존재하지 않게 된다. 이 경우, fstab
파일에서 장치 이름을 직접 참조하고 있었다면, 시스템을 정상적으로 되돌리기 위해 해당 파일을 수정해야 한다. 이러한 문제를 해결하기 위해 많은 Linux 시스템은 UUID
또는 LVM(Logical Volume Manager)
을 사용하여 안정적인 디스크 장치 매핑을 제공한다.
이에 대해서는 추후에 더 자세히 알아보도록 하자.
일부 디스크 장치는 AWS 인스턴스나 VirtualBox와 같은 가상 머신에 최적화되어 있다. Xen 가상화 시스템은 /dev/xvd
접두사를 사용하고, /dev/vd
는 유사한 유형이다.
일부 시스템은 이제 Non-Volatile Memory Devices Express(NVMe) 인터페이스를 사용하여 특정 종류의 solid-state 저장 장치와 통신한다. Linux에서는 이러한 장치가 /dev/nvme*
에 나타난다. nvme list
명령어를 사용하면 시스템에서 이러한 장치의 목록을 확인할 수 있다.
일부 시스템에서는 디스크와 기타 직접 블록 저장 장치보다 한 단계 더 높은 수준에서 LVM(Logical Volume Manager)
을 사용하며, 이 시스템은 device mapper라는 커널 시스템을 사용한다. /dev/dm-
로 시작하는 블록 장치와 /dev/mapper
에서 심볼릭 링크를 보면, 시스템에서 이를 사용하고 있다는 것을 알 수 있다. 이에 대한 자세한 내용은 다음에 다루기로 하자.
Linux는 대부분의 광학 저장 장치를 SCSI
장치로 인식하며, 이는 /dev/sr0
, /dev/sr1
와 같은 장치 이름을 갖는다. 그러나 드라이브가 오래된 인터페이스를 사용하는 경우, 이후에 설명할 PATA
장치로 나타날 수도 있다. /dev/sr*
장치는 읽기 전용이며, 디스크에서 데이터를 읽는 데만 사용된다. 광학 장치의 쓰기 및 재작성 기능은 /dev/sg0
와 같은 "generic" SCSI
장치를 사용하여 처리한다.
PATA(병렬 ATA)는 오래된 종류의 저장 버스이다. Linux 블록 장치인 /dev/hda
, /dev/hdb
, /dev/hdc
, /dev/hdd
는 오래된 Linux 커널 버전과 오래된 하드웨어에서 흔히 사용된다. 이러한 장치들은 인터페이스 0과 1의 장치 쌍에 기반한 고정 할당이다. 때때로 SATA
드라이브가 이러한 디스크 중 하나로 인식될 수 있는데, 이는 SATA 드라이브가 호환 모드에서 실행 중이라는 것을 의미하며 성능에 영향을 줄 수 있다. BIOS 설정에서 SATA 컨트롤러를 본래의 모드로 전환할 수 있는지 확인해보자.
터미널은 사용자 프로세스와 I/O 장치 간에 문자를 이동시키는 장치로, 보통 터미널 화면에 텍스트 출력을 위해 사용된다. 터미널 장치 인터페이스는 오래된 역사로 거슬러 올라가며, 예전에는 터미널이 타자기 기반 장치였고 많은 터미널이 하나의 머신에 연결되어 있었다.
대부분의 터미널은 가상 터미널 장치로, 실제 터미널의 I/O 기능을 이해하는 에뮬레이션된 터미널이다. 실제 하드웨어와 통신하는 대신, 커널은 I/O 인터페이스를 셸 터미널 창처럼 명령을 입력하는 소프트웨어에 제공한다.
두 가지 일반적인 터미널 장치는 /dev/tty1
(the first virtual console)과 /dev/pts/0
(he first pseudoterminal device))이다. /dev/pts
디렉터리는 전용 파일 시스템이다.
/dev/tty
장치는 현재 프로세스의 제어 터미널이다. 프로그램이 현재 터미널에서 읽고 쓰고 있다면, 이 장치는 해당 터미널을 나타내는 동의어가 된다. 프로세스는 반드시 터미널에 연결될 필요는 없다.
최근의 Linux 시스템에서는 장치 파일을 직접 생성할 필요가 없다. 장치 파일은 devtmpfs
와 udev
에 의해 자동으로 생성된다. 하지만 어떻게 장치 파일을 생성하는지 보는 것은 유익하며, 드물게 명명된 파이프나 소켓 파일을 생성해야 할 경우가 있을 수 있다.
mknod
명령을 사용하여 하나의 장치 파일을 생성할 수 있다. 장치의 이름과 주요 번호, 부차적 번호를 알아야 한다. 예를 들어, /dev/sda1
을 생성하려면 다음 명령을 사용한다:
mknod /dev/sda1 b 8 1
여기서 b 8 1은 major 번호 8과 minor 번호 1을 가진 블록 장치를 의미한다. 문자 장치나 명명된 파이프 장치의 경우, b 대신 c 또는 p를 사용하며, 명명된 파이프에는 주요 번호와 부차적 번호를 생략한다.
예전의 Unix
와 Linux
버전에서는 /dev
디렉터리의 관리를 하는 것이 어려운 일이었다. 커널을 크게 업그레이드하거나 드라이버를 추가하면, 커널이 지원하는 장치 종류가 더 많아지기 때문에 장치 파일명에 할당해야 할 새로운 주요 번호와 부차적 번호가 생겼다. 이 문제를 해결하기 위해, 각 시스템에는 MAKEDEV
프로그램이 /dev
디렉터리에 있어 장치 그룹을 생성하는 역할을 했다. 시스템을 업그레이드할 때는 MAKEDEV
업데이트 버전을 찾아 실행하여 새로운 장치들을 생성했다.
이러한 정적 시스템은 점점 비효율적으로 되어갔고, 그로 인해 새로운 해결책이 필요해졌다. 첫 번째 시도는 devfs
였으며, 이는 커널 공간에서 /dev
를 구현하여 현재 커널이 지원하는 모든 장치를 포함했다. 그러나 devfs
에는 여러 가지 제한 사항이 있었고, 이는 결국 udev
와 devtmpfs
의 개발로 이어졌다.
불필요한 복잡성은 linux시스템을 불안정하게 만든다. 이는 device file manager에서도 그러한 예가 있었다. 사용자 공간에서 장치 파일을 만들 수 있는데, 왜 커널에서 이를 해야 할까? Linux 커널은 시스템에서 새로운 장치를 감지하면 udevd
라는 사용자 공간 프로세스에 알림을 보낼 수 있다(예: USB 플래시 드라이브가 연결될 때). 이 udevd
프로세스는 새로운 장치의 특성을 검사하고, 장치 파일을 생성한 후 장치 초기화를 수행할 수 있다.
대부분의 시스템에서
udevd
프로세스는systemd-udevd
라는 이름으로 실행될 것이다. 이는 linux 시스템 시작 메커니즘의 일부이기 때문이다.
이론적으로는 그랬다. 그러나 이 접근 방식에는 문제가 있다. 장치 파일은 부팅 절차 초기에 필요하기 때문에, udevd
도 초기 단계에서 실행되어야 한다. 그러나 장치 파일을 생성하려면 udevd
가 생성해야 할 장치에 의존할 수 없으며, 시스템이 udevd
의 시작을 기다리지 않도록 초기 시작을 매우 빠르게 수행해야 한다는 문제점이 있다.
즉 정리하면 kernel이 부팅되는 경우 디스크와 같은 /dev/sda1
장치 파일이 먼저 초기화되어야한다. 그리고 이러한 장치 파일 초기화를 udevd
가 하는 것이다.
(1) 커널 부팅
|
V
(2) 커널이 장치 감지
|
V
(3) 커널이 장치 초기화 (장치 파일 필요)
|
V
(4) udevd 실행 (장치 파일 생성)
|
V
(5) udevd가 장치 감지하고 파일 생성 (이때 장치 파일이 필요함)
udevd
는 이미 있는 장치들을 감지하고 장치 파일을 만드는데, 문제는 udevd
의 경우 장치들이 먼저 kernel에 연결되어야만 한다는 것이다. 즉, 부팅과정에 있어서 모든 장치들이 udevd
가 실행되기 전에 먼저 연결되어야하는데, 장치들 간의 의존성이 있는 경우, 가령 디스크(/dev/sda
)가 켜져야 다른 장치들이 연결될 수 있는 경우에, udevd
가 실행되기 전에 먼저 장치가 준비되지 못하는 문제가 발생한다. 따라서, 의존성 문제를 해결할 수 없다는 단점이 있는 것이고, 이는 kernel의 부팅 속도를 매우 느리게한 원인이 된다.
따라서, 부팅 중에 일부 장치들은 장치 파일이 만들어져 초기화가 되어야 한다. 이를 해결하기 위해 다양한 기법들이 도입되었다.
devtmpfs
파일 시스템은 부팅 중 장치 사용 가능성 문제를 해결하기 위해 개발되었다. 이 파일 시스템은 이전의 devfs
지원과 유사하지만 단순화되었다. 커널은 필요에 따라 장치 파일을 생성하지만, 동시에 새로운 장치가 사용 가능하다는 신호를 udevd
에 알린다. udevd
는 이 신호를 받으면 장치 파일을 생성하지는 않지만, 장치 초기화와 권한 설정을 수행하고 다른 프로세스에 새로운 장치가 사용 가능하다고 알린다. 또한, 장치를 더 잘 식별할 수 있도록 /dev
에 여러 심볼릭 링크를 생성한다. 예를 들어, /dev/disk/by-id
디렉토리에서 각 연결된 디스크는 하나 이상의 항목을 갖는다.
예를 들어, 일반적인 디스크(/dev/sda
)와 그 파티션에 대한 링크를 /dev/disk/by-id
에서 살펴보자
ls -l /dev/disk/by-id
lrwxrwxrwx 1 root root 9 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671 -> ../../sda
lrwxrwxrwx 1 root root 10 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671-part1 -> ../../sda1
lrwxrwxrwx 1 root root 10 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671-part2 -> ../../sda2
lrwxrwxrwx 1 root root 10 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671-part5 -> ../../sda5
udevd
프로세스는 링크 이름을 인터페이스 유형에 따라 지정하고, 이후 제조업체와 모델 정보, 시리얼 번호, 파티션(해당되는 경우)을 추가한다.
devtmpfs
에서"tmp"
는 파일 시스템이 메인 메모리에 위치하고 사용자 공간 프로세스가 읽기/쓰기가 가능하다는 것을 의미한다. 이 특성 덕분에 디스크 장치가 셋업되지 않을 때에도,udevd
가 심볼릭 링크를 생성해 빠르게 부팅할 수 있는 것이다.
그렇다면 udevd
는 어떻게 어떤 심볼릭 링크를 생성할지 알고, 이를 어떻게 생성할까?
udevd
daemon은 다음과 같이 작동한다:
udevd
에게 uevent
라는 알림 이벤트를 내부 네트워크 링크를 통해 보낸다.udevd
는 uevent
에 포함된 모든 속성을 로드한다.udevd
는 규칙을 파싱하고, 해당 규칙을 기반으로 uevent
를 필터링하고 업데이트한 후, 이에 맞는 작업을 수행하거나 추가적인 속성을 설정한다.커널에서 udevd
가 받는 uevent
는 다음과 같이 생겼을 수 있다 (이 출력은 udevadm monitor --property
명령어로 얻을 수 있다.)
ACTION=change
DEVNAME=sde
DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb1/1-1/1-1.2/1-1.2:1.0/host4/target4:0:0/4:0:0:3/block/sde
DEVTYPE=disk
DISK_MEDIA_CHANGE=1
MAJOR=8
MINOR=64
SEQNUM=2752
SUBSYSTEM=block
UDEV_LOG=3
위 event는 장치의 변화에 해당한다. udevd
는 uevent
를 받은 후, 장치 이름, sysfs
장치 경로 및 여러 속성을 알고 있으며, 이제 규칙을 처리할 준비가 된다.
규칙 파일은 /lib/udev/rules.d
와 /etc/udev/rules.d
디렉터리에 있다. /lib
의 규칙은 기본 규칙이며, /etc
의 규칙은 이를 덮어쓴다. 규칙에 대한 기본적인 정보는 다음과 같다.
udevd
는 규칙 파일을 처음부터 끝까지 읽는다.udevd
는 규칙 파일에서 계속해서 적용 가능한 규칙을 읽는다.이제, /dev/sda
예시에서 본 심볼릭 링크를 살펴보자. 그 링크들은 /lib/udev/rules.d/60-persistent-storage.rules
에 정의된 규칙에 의해 생성된다. 내부적으로는 다음과 같은 줄을 볼 수 있다.
# ATA
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{vendor}=="ATA", IMPORT{program}="ata_id --export $devnode"
# ATAPI devices (SPC-3 or later)
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{type}=="5",ATTRS{scsi_level}=="[6-9]*", IMPORT{program}="ata_id --export $devnode"
이 규칙들은 커널의 SCSI 서브시스템을 통해 제공되는 ATA 디스크와 광학 미디어를 처리한다. 장치가 어떻게 나타날지에 대한 몇 가지 방법을 잡기 위한 규칙들이 있지만, 기본적인 아이디어는 udevd
가 장치를 찾을 때 sd
나 sr
로 시작하는 장치(번호가 없는)를 찾고, 그 후에 SCSI 서브시스템을 확인한 후, 추가 속성을 확인하는 것이다. 이 규칙들이 모두 참이면, udevd
는 다음과 같은 규칙으로 진행된다.
IMPORT{program}="ata_id --export $devnode"
이 규칙은 조건부가 아니라, /lib/udev/ata_id
명령에서 변수들을 가져오는 지시문이다. 예를 들어, 디스크에 대해 ata_id
명령을 실행하면 다음과 같은 결과를 얻을 수 있다.
# /lib/udev/ata_id --export /dev/sda
ID_ATA=1
ID_TYPE=disk
ID_BUS=ata
ID_MODEL=WDC_WD3200AAJS-22L7A0
ID_MODEL_ENC=WDC\x20WD3200AAJS22L7A0\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20
ID_REVISION=01.03E10
ID_SERIAL=WDC_WD3200AAJS-22L7A0_WD-WMAV2FU80671
이제 임포트된 값들은 uevent
의 환경으로 설정된다. 예를 들어, 이후의 규칙들은 ENV{ID_TYPE}
을 disk로 인식할 수 있다.
앞서 본 두 규칙에서 중요한 점은 ID_SERIAL
이다. 각 규칙에서 이 조건이 두 번째로 등장한다.
ENV{ID_SERIAL}!="?*"
이 표현은 ID_SERIAL
이 설정되지 않았을 때 참이 된다. 따라서 ID_SERIAL
이 설정되면 조건이 거짓이 되어 현재 규칙이 적용되지 않고, udevd
는 다음 규칙으로 이동한다.
이 규칙이 있는 이유는 무엇일까? 이 규칙들은 ata_id
를 실행하여 디스크 장치의 일련 번호를 찾고, 이를 현재 uevent
에 추가하는 것이다. 이 일반적인 패턴은 많은 udev
규칙에서 사용된다.
ENV{ID_SERIAL}
이 설정되면, udevd
는 규칙 파일에서 이후에 나타나는 다음 규칙을 평가할 수 있다. 이 규칙은 다음과 같다.
KERNEL=="sd*|sr*|cciss*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}"
이 규칙은 ENV{ID_SERIAL}이 설정되었음을 요구하며, 한 가지 지시문이 있다.
SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}"
이 지시문은 udevd
에게 들어오는 장치에 대해 심볼릭 링크를 추가하라고 지시한다. 이제 장치 심볼릭 링크가 어디에서 왔는지 알게 되었다.
조건부 표현과 지시문을 구분하는 방법이 궁금할 수 있다. 조건부는 == 또는 !=로 표시되고, 지시문은 = 또는 += 또는 :=로 표시된다.
정리하자면, uevent를 받고 udevd
가 정해진 규칙들을 파싱하여 그 규칙대로 장치 파일에 대한 symbolic link를 만들 지 아닐 지 결정한다는 것이다.
udevadm
프로그램은 udevd
를 관리하는 도구이다. 이 프로그램을 사용하면 udevd
의 규칙을 다시 로드하고 이벤트를 트리거할 수 있다. 그러나 udevadm
의 가장 강력한 기능은 시스템 장치를 검색하고 탐색하는 기능과, 커널에서 udevd
로 전달되는 uevent
를 실시간으로 모니터링하는 기능이다. 명령어 구문은 다소 복잡할 수 있으며, 대부분의 옵션에는 긴 형식과 짧은 형식이 존재한다. 여기서는 긴 형식을 사용한다.
/dev/sda
와 같은 장치에 대해 udevd
규칙에서 사용되고 생성된 모든 속성을 확인하려면 다음 명령을 실행한다.
udevadm info --query=all --name=/dev/sda
결과는 다음과 같다.
P: /devices/vbd-51712/block/xvda
M: xvda
U: block
T: disk
D: b 202:0
N: xvda
L: 0
S: disk/by-path/xen-vbd-51712
S: disk/by-diskseq/1
S: sda
Q: 1
E: DEVPATH=/devices/vbd-51712/block/xvda
E: DEVNAME=/dev/xvda
E: DEVTYPE=disk
E: DISKSEQ=1
E: MAJOR=202
...
각 줄의 접두사는 해당 장치의 속성 또는 특성을 나타낸다.
위 예제에서는 전체 출력을 생략했으며, 직접 명령어를 실행하여 더 많은 정보를 확인할 수 있다.
udevadm
에서 monitor
명령을 사용하면 uevent
를 실시간으로 모니터링할 수 있다.
udevadm monitor
USB flash driver를 삽입하면 다음과 같은 메시지가 출력된다.
KERNEL[658299.569485] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2 (usb)
KERNEL[658299.569667] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0 (usb)
KERNEL[658299.570614] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/host15 (scsi)
KERNEL[658299.570645] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/host15/scsi_host/host15 (scsi_host)
UDEV [658299.622579] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2 (usb)
UDEV [658299.623014] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0 (usb)
UDEV [658299.623673] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/host15 (scsi)
UDEV [658299.623690] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/host15/scsi_host/host15 (scsi_host)
...
출력에는 각 메시지에 대해 두 개의 로그가 존재한다.
KERNEL
로 시작하는 메시지는 커널에서 직접 보낸 이벤트이다.UDEV
로 시작하는 메시지는 udevd가 해당 이벤트를 처리한 결과이다.옵션 설명
출력되는 정보를 필터링할 수 있다.
udevadm monitor --kernel
KERNEL[...] 메시지만 출력된다.
udevadm monitor --udev
UDEV[...] 메시지만 출력된다.
udevadm monitor --property
uevent
의 속성 정보를 포함하여 전체 내용을 출력한다.
--udev
와 함께 사용하면 udevd
가 처리한 후의 속성을 확인할 수 있다.
예를 들어, SCSI 서브시스템 관련 이벤트만 보고 싶다면 다음과 같이 실행한다.
udevadm monitor --kernel --subsystem-match=scsi
먼저 SCSI
(Small Computer System Interface)의 기본적인 개념을 살펴보자.
전통적인 SCSI
하드웨어 구성은 호스트 어댑터(host adapter)가 `SCSI 버스를 통해 여러 장치들과 연결된 형태이다. 이는 그림 3-1에서 볼 수 있다.
SCSI 명령 세트를 사용하면 장치 간 피어 투 피어(peer-to-peer) 방식으로 통신할 수 있다. 컴퓨터는 장치 체인에 직접 연결되지 않으며, 반드시 호스트 어댑터(host adapter)를 통해 디스크 및 기타 장치와 통신한다. 일반적으로 컴퓨터는 SCSI 명령을 호스트 어댑터로 보내고, 어댑터는 이를 장치로 전달하며, 장치는 응답을 다시 어댑터를 거쳐 컴퓨터로 반환한다.
SAS(Serial Attached SCSI)와 같은 최신 SCSI 버전은 높은 성능을 제공하지만, 실제 SCSI 장치는 대부분의 컴퓨터에서 찾아보기 어렵다. 대신, USB 저장 장치는 내부적으로 SCSI 명령을 사용하며, ATAPI(Advanced Technology Attachment Packet Interface)를 지원하는 CD/DVD-ROM 드라이브도 SCSI 명령 세트를 변형하여 사용한다.
SATA 디스크는 시스템에서 SCSI 장치처럼 인식되지만, 대부분 libata
라이브러리의 변환 계층을 통해 통신한다. 일부 고성능 RAID
컨트롤러는 이 변환을 하드웨어에서 수행한다.
다음은 lsscsi 명령을 실행하여 SCSI
목록을 확인한 것이다.
lsscsi
[0:0:0:0] disk ATA WDC WD3200AAJS-2 01.0 /dev/sda
[1:0:0:0] cd/dvd Slimtype DVD A DS8A5SH XA15 /dev/sr0
[2:0:0:0] disk USB2.0 CardReader CF 0100 /dev/sdb
[2:0:0:1] disk USB2.0 CardReader SM XD 0100 /dev/sdc
[2:0:0:2] disk USB2.0 CardReader MS 0100 /dev/sdd
[2:0:0:3] disk USB2.0 CardReader SD 0100 /dev/sde
[3:0:0:0] disk FLASH Drive UT_USB20 0.00 /dev/sdf
대괄호 안의 숫자는 다음을 의미한다.
위 예시에서 네 개의 호스트 어댑터(scsi0, scsi1, scsi2, scsi3)가 있으며, 각 어댑터는 하나의 버스(bus 0)를 가지고 있다. 각 버스에는 하나의 장치(target 0)가 연결되어 있으며, 예외적으로 USB 카드 리더(2:0:0)는 네 개의 LUN을 가진다. 이는 서로 다른 플래시 카드 종류를 지원하기 때문이다. 커널은 각 논리 유닛(LUN)에 대해 별도의 장치 파일(device file)을 할당한다.
SCSI 장치는 아니지만, NVMe 장치는 lsscsi 출력에서 어댑터 번호가 "N"으로 표시될 수도 있다.
위 그림은 특정 시스템 구성에서 개별 장치 드라이버부터 블록 드라이버까지, 커널 내부의 드라이버 및 인터페이스 계층 구조를 나타낸다. 단, SCSI generic(sg) 드라이버는 포함되지 않는다.
이 구조는 처음 보면 복잡해 보일 수 있지만, 데이터 흐름은 매우 직선적이다. 이를 더 쉽게 이해하기 위해 SCSI 서브시스템의 세 가지 드라이버 계층을 살펴보자.
최상위 계층 (Top Layer): 이 계층은 특정 장치 클래스(class)의 동작을 처리한다. 예를 들어, sd(SCSI disk) 드라이버는 커널 블록 장치 인터페이스(block device interface)에서 오는 요청을 받아 SCSI 프로토콜의 디스크 명령으로 변환하며, 반대로 SCSI 응답을 블록 장치 요청으로 변환하는 역할을 한다.
중간 계층 (Middle Layer): 이 계층은 SCSI 메시지를 중재 및 라우팅하며, 최상위 계층과 최하위 계층 간의 연결을 관리한다. 또한, 시스템에 연결된 모든 SCSI 버스와 장치 목록을 유지하고, 적절한 경로로 요청을 전달한다.
최하위 계층 (Bottom Layer): 이 계층은 하드웨어별 동작을 담당한다. 여기서 드라이버는 호스트 어댑터(host adapter) 또는 특정 하드웨어에 맞게 SCSI 프로토콜 메시지를 전송하고, 하드웨어로부터 수신된 메시지를 처리한다.
SCSI 명령은 장치 클래스(예: 디스크 클래스)별로 동일하지만, 호스트 어댑터마다 전송 방식이 다를 수 있기 때문에 이 계층을 분리하여 관리한다.
이러한 구조를 통해, SCSI 시스템은 장치 독립적인 표준화된 방식으로 동작하면서도 다양한 하드웨어에 유연하게 대응할 수 있다.
상위 계층과 하위 계층에는 많은 다양한 드라이버들이 있지만, 중요한 점은 시스템의 특정 장치 파일에 대해 커널이 거의 항상 하나의 상위 계층 드라이버와 하나의 하위 계층 드라이버를 사용한다는 것이다. 예를 들어, /dev/sda
에 있는 디스크의 경우, 커널은 sd
상위 계층 드라이버와 ATA
브릿지 하위 계층 드라이버를 사용한다.
때로는 하나의 하드웨어 장치에 대해 여러 상위 계층 드라이버를 사용할 수도 있다.
SCSI 서브시스템이 일반 USB 저장 장치와 통신하기 위해서는 하위 계층 SCSI 드라이버만으로는 충분하지 않다. 예를 들어, /dev/sdf
로 나타나는 USB 플래시 드라이브는 SCSI 명령을 이해하지만, 드라이브와 실제로 통신하려면 커널이 USB 시스템을 통해 어떻게 통신해야 할지 알아야 한다.
추상적으로 보면, USB는 SCSI와 매우 유사하다. USB에도 장치 클래스, 버스, 호스트 컨트롤러가 있다. 따라서 Linux 커널은 SCSI 서브시스템처럼 세 계층의 USB 서브시스템을 포함하고 있으며, 상위 계층에는 장치 클래스 드라이버, 중간에는 버스 관리 핵심, 하위 계층에는 호스트 컨트롤러 드라이버가 있다. SCSI 서브시스템이 SCSI 명령을 전달하는 것처럼 USB 서브시스템도 USB 메시지를 전달한다. lsusb
명령은 lsscsi
와 비슷하다.
우리가 관심 있는 부분은 USB Storage Driver이다. 이 드라이버는 번역기 역할을 한다. 한쪽 끝에서 드라이버는 SCSI 명령을 사용하고, 다른 쪽 끝에서는 USB 명령을 사용한다. 저장 장치 하드웨어가 USB 메시지 내에 SCSI 명령을 포함하고 있기 때문에, 드라이버는 데이터를 재포장하는 역할을 한다.
SCSI와 USB 서브시스템이 모두 구현되면 플래시 드라이브와 통신할 수 있다. 마지막으로 부족한 부분은 하위 계층 SCSI 드라이버이다. USB 저장 드라이버는 USB 서브시스템의 일부이므로 SCSI 서브시스템과 드라이버를 공유하지 않는다. 두 서브시스템이 서로 통신할 수 있도록 하기 위해, 간단한 하위 계층 SCSI 브리지 드라이버가 USB 서브시스템의 저장 드라이버에 연결된다.
SATA 하드 디스크와 광학 드라이브는 모두 동일한 SATA 인터페이스를 사용한다. SATA 전용 커널 드라이버를 SCSI 서브시스템에 연결하기 위해 커널은 USB 드라이브와 마찬가지로 브리지 드라이버를 사용하지만, 다른 메커니즘과 추가적인 복잡성이 있다. 광학 드라이브는 ATAPI
를 사용하며, ATAPI
는 ATA
프로토콜로 인코딩된 SCSI 명령의 버전이다. 하지만 하드 디스크는 ATAPI
를 사용하지 않으며, SCSI 명령을 인코딩하지 않는다.
Linux 커널은 libata
라는 라이브러리의 일부를 사용하여 SATA(및 ATA) 드라이브와 SCSI 서브시스템을 연결한다. ATAPI를 사용하는 광학 드라이브의 경우, SCSI 명령을 ATA 프로토콜로 패키징하고 추출하는 일이 비교적 간단하다. 그러나 하드 디스크의 경우에는 명령 번역을 전면적으로 해야 하므로 더 복잡하다.
이 작업은 마치 독일어 책을 읽고 영어로 번역하는 것과 같다. 두 언어와 책의 내용을 이해해야 한다. 그럼에도 불구하고 libata는 이 작업을 수행하여 ATA/SATA 인터페이스와 장치를 SCSI 서브시스템에 연결할 수 있게 한다.
사용자 공간 프로세스가 SCSI 서브시스템과 통신할 때, 보통은 블록 장치 계층과/또는 다른 커널 서비스(예: sd, sr와 같은 SCSI 장치 클래스 드라이버)를 통해 이루어진다. 즉, 대부분의 사용자 프로세스는 SCSI 장치나 명령에 대해 알 필요가 없다.
그러나 사용자 프로세스는 장치 클래스 드라이버를 우회하여 직접 SCSI 프로토콜 명령을 장치에 전달할 수 있다. 예를 들어, lsscsi
명령에 -g
옵션을 추가하여 일반 장치(generic devices)를 표시하면 다음과 같은 결과를 얻을 수 있다.
lsscsi -g
[0:0:0:0] disk ATA WDC WD3200AAJS-2 01.0 /dev/sda 1/dev/sg0
[1:0:0:0] cd/dvd Slimtype DVD A DS8A5SH XA15 /dev/sr0 /dev/sg1
[2:0:0:0] disk USB2.0 CardReader CF 0100 /dev/sdb /dev/sg2
[2:0:0:1] disk USB2.0 CardReader SM XD 0100 /dev/sdc /dev/sg3
[2:0:0:2] disk USB2.0 CardReader MS 0100 /dev/sdd /dev/sg4
[2:0:0:3] disk USB2.0 CardReader SD 0100 /dev/sde /dev/sg5
[3:0:0:0] disk FLASH Drive UT_USB20 0.00 /dev/sdf /dev/sg6
각 항목은 마지막 열에 SCSI generic 장치 파일을 나열한다. 예를 들어, /dev/sr0
에 해당하는 광학 드라이브의 일반 장치는 /dev/sg1
이다.
SCSI에서 generic devices
는 특정 장치 클래스 드라이버를 사용하지 않고, SCSI 프로토콜 명령을 직접 장치에 전달할 수 있게 해주는 장치 파일이다. 일반적으로, 사용자 프로세스는 SCSI 장치와 통신하기 위해 sd
, sr
같은 장치 클래스 드라이버를 사용하지만, generic device를 사용하면 이러한 클래스 드라이버를 우회하여 SCSI 명령을 직접 장치에 보낼 수 있다.
왜 generic devices
를 사용하고 싶을까? 그 이유는 커널 내 코드의 복잡성과 관련이 있다. 작업이 복잡해지면 커널에서 이를 처리하기보다는 사용자 공간으로 빼는 것이 더 좋다. 예를 들어, CD/DVD 디스크를 읽는 것은 비교적 간단한 작업이고, 이를 위한 전문 커널 드라이버가 있다.
하지만 CD/DVD 디스크에 데이터를 쓰는 것은 읽는 것보다 훨씬 어려운 작업이다. 시스템의 중요한 서비스가 디스크 쓰기 작업에 의존하지 않으므로, 커널 공간에서 이를 처리할 이유가 없다. 따라서 Linux에서 광학 디스크에 데이터를 쓰려면 사용자 공간 프로그램을 실행하여 /dev/sg1
과 같은 일반 SCSI 장치에 접근한다. 이 프로그램은 커널 드라이버보다 비효율적일 수 있지만, 훨씬 더 쉽게 구축하고 유지할 수 있다.
위의 그림은 Process A가 sr
driver를 통해서 데이터를 읽고, Process B가 sg
driver를 통해서 데이터를 쓰고 있다. 단, 이들은 서로 동시에 같은 devices에 대해서 동작하지 않는다.
process A는 block device로부터 데이터를 읽는다. 그러나 user process가 정말로 이러한 방식으로 data를 읽을까?? 일반적으로 그 답은 '아니오'이다. 블록 장치 위에는 더 많은 계층이 존재하며, 하드 디스크에 대한 접근 지점도 더 많다. 이에 대해서는 다음 챕터에서 배우도록 하자.