SATA / AHCI — Serial ATA & Advanced Host Controller Interface
SATA(Serial ATA)는 병렬 ATA(PATA)를 대체한 직렬 스토리지 인터페이스이며,
AHCI(Advanced Host Controller Interface)는 SATA HBA(Host Bus Adapter)의 표준화된 레지스터 인터페이스입니다.
이 문서에서는 SATA 프로토콜의 변천(SATA I/II/III), AHCI 레지스터 아키텍처와 커맨드 처리 메커니즘,
FIS(Frame Information Structure) 구조, 리눅스 커널 libata 서브시스템의 핵심 구조체와 드라이버 콜백,
AHCI 드라이버(ahci.c) 구현 상세, NCQ(Native Command Queuing) 심화,
M.2 폼 팩터와 SATA vs NVMe 비교, SATA 전원 관리(ALPM/DIPM/DevSlp),
핫 플러그, Port Multiplier, 에러 처리와 복구, ATA 보안 기능,
sysfs 인터페이스, S.M.A.R.T. 모니터링, 커널 빌드 설정, 디버깅, 성능 튜닝,
OOB 링크 초기화까지 SATA/AHCI의 모든 것을 심층 분석합니다.
핵심 요약
libata 서브시스템이 담당합니다.
libata는 ATA 명령어를 SCSI 명령어로 변환(SAT: SCSI-ATA Translation)하여
SCSI 미드 레이어와 블록 레이어에 통합됩니다.
따라서 SATA 디스크도 /dev/sda, /dev/sdb 등 SCSI 디바이스 이름을 사용합니다.
| 사양 | SATA I (Gen1) | SATA II (Gen2) | SATA III (Gen3) |
|---|---|---|---|
| 출시 연도 | 2003 | 2004 | 2009 |
| 인터페이스 속도 | 1.5 Gbps | 3.0 Gbps | 6.0 Gbps |
| 유효 대역폭 | 150 MB/s | 300 MB/s | 600 MB/s |
| 인코딩 | 8b/10b | 8b/10b | 8b/10b |
| NCQ 지원 | 선택적 | 필수 | 필수 |
| 핫 플러그 | 선택적 | 필수 | 필수 |
| 전원 관리 | Partial/Slumber | +ALPM | +DIPM, DevSlp |
| 하위 호환 | - | SATA I | SATA I/II |
SATA vs NVMe vs SAS 한눈 비교
| 항목 | SATA III | NVMe (PCIe 4.0 x4) | SAS-4 (24G) |
|---|---|---|---|
| 최대 대역폭 | 600 MB/s | ~7,000 MB/s | 2,400 MB/s (풀 듀플렉스) |
| 큐 깊이 | 32 (NCQ) | 65,535 큐 × 65,536 엔트리 | 256 (태그 큐) |
| 평균 레이턴시 | ~100 μs (SSD 기준) | ~10-20 μs | ~70-100 μs |
| 주요 용도 | 클라이언트 SSD/HDD, NAS | 고성능 워크스테이션, 서버, 게이밍 | 엔터프라이즈 스토리지, 데이터센터 |
| 상대 가격대 | 저가 (GB당 최저) | 중-고가 | 고가 (엔터프라이즈 전용) |
8b/10b 인코딩으로 인해 실제 유효 대역폭은 인터페이스 속도의 80%입니다. 예를 들어, SATA III 6 Gbps의 유효 대역폭은 6 × 0.8 = 4.8 Gbps = 600 MB/s입니다.
단계별 이해
SATA/AHCI를 체계적으로 학습하기 위한 4단계 로드맵입니다.
SATA의 물리 계층(PHY), 링크 계층, 전송 계층의 3계층 구조를 이해합니다. PATA에서 SATA로의 전환 배경, 점대점 직렬 링크의 장점, FIS(Frame Information Structure)의 개념을 학습합니다. 이 단계에서는 SATA 개요와 프로토콜 변천과 FIS 구조 섹션을 참고하세요.
AHCI 레지스터 맵(GHC, 포트 레지스터), Command List/FIS Receive 영역, PRDT(Physical Region Descriptor Table)의 메모리 레이아웃을 이해합니다. HBA가 커맨드를 가져와 FIS로 변환하고 디바이스에 전송하는 흐름을 파악합니다. AHCI 스펙과 레지스터 아키텍처와 AHCI 드라이버 구현 섹션을 보세요.
ata_host, ata_port, ata_device, ata_queued_cmd 핵심 구조체와
ata_port_operations 드라이버 콜백의 역할을 학습합니다.
SCSI-ATA Translation(SAT)이 어떻게 SCSI CDB를 ATA 명령어로 변환하는지 이해합니다.
libata 서브시스템과 커맨드 실행 경로를 참고하세요.
NCQ(Native Command Queuing)의 32-태그 병렬 처리, 전원 관리(ALPM/DIPM/DevSlp), 핫 플러그 메커니즘, Port Multiplier, 에러 처리와 복구 전략 등 고급 주제를 다룹니다. 성능 튜닝, S.M.A.R.T. 모니터링, 디버깅 기법까지 실무 지식을 쌓습니다. NCQ 심화부터 마지막 섹션까지 순서대로 학습하세요.
단계별 실습 명령어
각 학습 단계에서 실행해 볼 수 있는 실습 명령어입니다. 시스템 환경에 따라 root 권한이 필요할 수 있습니다.
# ── 1단계: SATA 프로토콜 기초 확인 ──
# SATA 링크 초기화/속도 확인 (dmesg에서 OOB 시퀀스 결과)
dmesg | grep ata
# 출력 예: ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
# 연결된 ATA 디바이스 정보
dmesg | grep "ata[0-9]"
# ── 2단계: AHCI 하드웨어 인터페이스 확인 ──
# AHCI 컨트롤러 PCI 정보
lspci | grep AHCI
# 출력 예: 00:1f.2 SATA controller: Intel Corporation 8 Series SATA Controller [AHCI mode]
# AHCI BAR5(ABAR) 주소 확인
lspci -vv -s $(lspci | grep AHCI | awk '{print $1}') | grep "Region 5"
# AHCI 레지스터 덤프 (setpci 활용)
setpci -s $(lspci | grep AHCI | awk '{print $1}') CAP_LIST_ID+0.l
# ── 3단계: libata 서브시스템 확인 ──
# ATA 디바이스 속성 (sysfs)
ls /sys/class/ata_device/
cat /sys/class/ata_device/dev1.0/id
# IDENTIFY 데이터를 raw hex로 확인
cat /sys/class/ata_device/dev1.0/id | xxd | head -20
# libata 모듈 로드 상태
lsmod | grep "libata\|ahci"
# ── 4단계: 고급 주제 (S.M.A.R.T., 성능) ──
# S.M.A.R.T. 정보 확인
smartctl -a /dev/sda
# NCQ 큐 깊이 확인
cat /sys/block/sda/device/queue_depth
# SATA 링크 전원 관리 정책
cat /sys/class/scsi_host/host0/link_power_management_policy
drivers/ata/ahci.h— AHCI 레지스터 정의, 구조체 선언drivers/ata/ahci.c— PCI AHCI 드라이버 진입점, 포트 초기화drivers/ata/libata-core.c— ATA 명령 발행, IDENTIFY 파싱, 전송 모드 설정drivers/ata/libata-scsi.c— SCSI-ATA Translation(SAT) 변환 테이블drivers/ata/libata-eh.c— 에러 핸들러, 리셋 체인, 복구 로직include/linux/ata.h— ATA 명령어 코드, FIS 타입, IDENTIFY 필드 인덱스include/linux/libata.h— libata 핵심 구조체 (ata_host, ata_port, ata_device)
SATA 개요와 프로토콜 변천
PATA(Parallel ATA, IDE)는 40핀/80핀 리본 케이블로 최대 2개 디바이스(마스터/슬레이브)를 연결하는 방식이었습니다. 최고 속도인 Ultra DMA/133도 133 MB/s에 불과했고, 긴 리본 케이블은 공기 순환을 방해하며 신호 무결성 문제(crosstalk, skew)가 심각했습니다.
SATA는 이 문제를 해결하기 위해 2003년 Serial ATA International Organization(SATA-IO)이 제정했습니다. 핵심 설계 원칙은 다음과 같습니다:
- 점대점(Point-to-Point) 토폴로지 — 각 디바이스가 전용 링크를 가져 대역폭을 독점
- 직렬 전송 — 차동 신호(differential signaling) 2쌍으로 전이중(full-duplex) 통신
- 얇은 케이블 — 7핀 데이터 케이블(최대 1m)로 공기 순환 개선
- 핫 플러그 — 전원 인가 상태에서 디바이스 탈착 가능
- 소프트웨어 호환성 — ATA 명령어 셋 유지, 레거시 드라이버 포팅 용이
PATA vs SATA 비교
| 항목 | PATA (IDE) | SATA |
|---|---|---|
| 신호 방식 | 병렬 (16비트 데이터 버스) | 직렬 (차동 1쌍 x 2) |
| 케이블 | 40/80핀 리본 (최대 46cm) | 7핀 (최대 1m) |
| 토폴로지 | 마스터/슬레이브 (공유 버스) | 점대점 (전용 링크) |
| 최대 속도 | 133 MB/s (UDMA/133) | 600 MB/s (SATA III) |
| 핫 플러그 | 미지원 | 지원 |
| NCQ | TCQ (미흡) | NCQ (32-tag) |
| 전원 커넥터 | 4핀 Molex | 15핀 SATA Power |
| 점퍼 설정 | 필요 (Master/Slave/CS) | 불필요 |
| 공기 순환 | 나쁨 (넓은 리본) | 좋음 (얇은 케이블) |
프로토콜 계층 구조
SATA 프로토콜은 세 계층으로 나뉩니다:
| 계층 | 역할 | 주요 단위 |
|---|---|---|
| 전송 계층 (Transport) | FIS 조립/분해, 명령/데이터 전달 | FIS (Frame Information Structure) |
| 링크 계층 (Link) | 프레임 구성, CRC, 흐름 제어, 스크램블링 | Frame (SOF + FIS + CRC + EOF) |
| 물리 계층 (PHY) | 8b/10b 인코딩, 직렬화, OOB 시그널링 | Dword (32비트 = 40비트 인코딩) |
프레임 구조
SATA 링크 계층의 프레임은 다음과 같이 구성됩니다:
| 필드 | 크기 | 설명 |
|---|---|---|
| SOF (Start of Frame) | 1 Dword | 프레임 시작 프리미티브 |
| FIS 페이로드 | 1~2048 Dword | FIS 타입에 따른 페이로드 |
| CRC | 1 Dword | FIS 페이로드의 32비트 CRC |
| EOF (End of Frame) | 1 Dword | 프레임 종료 프리미티브 |
SATA Express와 mSATA
SATA Express(SATAe)는 SATA 커넥터에 PCIe 레인 2개를 추가해 최대 2 GB/s를 제공하려 했으나, NVMe M.2의 등장으로 시장에서 사라졌습니다. mSATA는 미니 PCIe 슬롯의 물리적 형태를 빌려 SATA 신호를 전달하는 소형 폼 팩터로, M.2에 의해 대체되었습니다.
SATA 신호 전기적 특성
SATA는 LVDS(Low Voltage Differential Signaling) 기반의 차동 신호 쌍을 사용합니다. 송신(Tx)과 수신(Rx) 각각 독립된 차동 쌍을 가져 전이중(full-duplex) 통신이 가능합니다.
- 차동 전압: 400~600 mV (피크-투-피크), 종단 저항 100Ω 차동
- 공통 모드 전압: 0~3.3V 범위, 타겟 중앙값은 명시 안 됨 (AC 커플링)
- AC 커플링 커패시터: 송신측에 10~200 nF 커패시터로 DC 오프셋 제거
- Spread Spectrum Clocking (SSC): EMI 감소를 위해 클럭 주파수를 -0.5% 범위 내에서 변조. SATA III의 경우 6 GHz 기준 5.97~6.00 GHz 사이를 삼각파로 변조합니다. SSC는 다운스프레드 모드만 사용(주파수가 공칭보다 낮은 쪽으로만 변동)합니다.
- 지터 허용 범위: Total Jitter(TJ) < 0.35 UI (Unit Interval), SATA III에서 1 UI ≈ 166.67 ps
8b/10b 인코딩 상세
SATA 물리 계층은 8b/10b 인코딩을 사용하여 8비트 데이터를 10비트 심볼로 변환합니다. 이 인코딩의 핵심 목적은 다음과 같습니다:
- DC 밸런스 유지: 전송되는 0과 1의 개수를 장기적으로 균형 맞춰, AC 커플링 커패시터의 전압 편향 방지
- 전이 밀도 보장: 연속된 동일 비트가 5비트를 넘지 않아 수신측 클럭 복원(CDR) 용이
- 특수 문자(K-코드) 지원: 데이터와 구분되는 제어 심볼로 프레임 경계/동기화 표시
| K-코드 | 심볼 | 10b 인코딩 (RD-) | 용도 |
|---|---|---|---|
| K28.5 | ALIGN | 001111 1010 | 워드 정렬, 클럭 보정용 프리미티브 접두사 |
| K28.3 | SOF/EOF | 001111 0011 | 프레임 시작/종료 프리미티브의 구성 요소 |
| K28.0 | - | 001111 0100 | 제어 프리미티브 구성용 |
| K28.1 | - | 001111 1001 | 제어 프리미티브 구성용 |
| K28.6 | - | 001111 0110 | 제어 프리미티브 구성용 |
Running Disparity (RD)는 현재까지 전송된 1과 0의 누적 불균형을 추적합니다. 각 10비트 심볼은 RD+ 또는 RD- 두 가지 인코딩을 가지며, 현재 RD 상태에 따라 적절한 인코딩을 선택합니다. RD가 양수이면 0이 많은 인코딩을, RD가 음수이면 1이 많은 인코딩을 선택하여 DC 밸런스를 유지합니다. 코드 위반(code violation)은 유효하지 않은 10비트 패턴이 수신될 때 검출되며, 이는 전송 오류의 증거로 링크 계층에서 에러 처리됩니다.
SATA 커넥터 핀 배치 (데이터)
SATA 데이터 커넥터는 7핀으로 구성되며, 3개의 접지 핀이 차동 쌍 사이를 분리하여 신호 무결성을 보장합니다.
| 핀 | 신호명 | 설명 |
|---|---|---|
| 1 | GND | 접지 (차폐) |
| 2 | A+ (Tx+) | 호스트 송신 차동 양극 |
| 3 | A- (Tx-) | 호스트 송신 차동 음극 |
| 4 | GND | 접지 (Tx/Rx 분리) |
| 5 | B- (Rx-) | 호스트 수신 차동 음극 |
| 6 | B+ (Rx+) | 호스트 수신 차동 양극 |
| 7 | GND | 접지 (차폐) |
L자형 키잉 설계로 역삽입이 방지되며, 핫 플러그를 위해 접지 핀(1, 7)이 신호 핀보다 먼저 접촉합니다. 케이블 최대 길이는 1m(외장 eSATA는 2m)이며, 이 제한은 신호 감쇠와 지터 허용 범위에 의해 결정됩니다.
# 현재 시스템의 SATA 디바이스 확인
lsblk -d -o NAME,ROTA,TRAN,SIZE,MODEL
# SATA 링크 속도 확인
dmesg | grep "SATA link up"
# 출력 예: ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
SATA 전원 커넥터
SATA 전원 커넥터(15핀)는 3.3V, 5V, 12V 세 가지 전압을 공급합니다. 3.3V는 초기 SATA SSD에서 사용되었으나, 대부분의 현대 디바이스는 5V와 12V만 사용합니다. HDD는 12V(스핀들 모터)와 5V(전자회로)를 모두 사용하며, SSD는 주로 5V만 사용합니다.
| 핀 | 전압 | 용도 |
|---|---|---|
| 1-3 | 3.3V | 옵션 (대부분 미사용) |
| 4-6 | GND | 접지 |
| 7-9 | 5V | 전자회로, 컨트롤러 |
| 10-12 | GND | 접지 |
| 13-15 | 12V | 모터 구동 (HDD) |
AHCI 스펙과 레지스터 아키텍처
AHCI(Advanced Host Controller Interface)는 Intel이 2004년에 설계한
SATA HBA(Host Bus Adapter)의 표준 레지스터 인터페이스입니다.
AHCI 이전에는 SiI3112, VIA VT6421 등 각 벤더가 독자적인 레지스터 레이아웃을 사용해
벤더별 드라이버가 필요했습니다. AHCI는 PCI BAR5(ABAR, AHCI Base Address Register)를 통해
메모리 매핑된 표준 레지스터 셋을 정의함으로써, 하나의 드라이버(ahci.c)로
모든 AHCI 호환 HBA를 지원할 수 있게 했습니다.
CAP 레지스터 주요 필드
| 비트 | 필드 | 설명 |
|---|---|---|
| 31 | S64A | 64비트 주소 지원 |
| 30 | SNCQ | NCQ(Native Command Queuing) 지원 |
| 29 | SSNTF | SNotification 레지스터 지원 |
| 28 | SMPS | Mechanical Presence Switch 지원 |
| 27 | SSS | Staggered Spin-up 지원 |
| 26 | SALP | Aggressive Link PM 지원 |
| 25 | SAL | Activity LED 지원 |
| 24 | SCLO | Command List Override 지원 |
| 23:20 | ISS | 인터페이스 속도 (1=Gen1, 2=Gen2, 3=Gen3) |
| 17 | SAM | AHCI Only 모드 |
| 16 | SPM | Port Multiplier 지원 |
| 15 | FBSS | FIS-Based Switching 지원 |
| 14 | PMD | PIO Multiple DRQ Block 지원 |
| 13 | SSC | Slumber State Capable |
| 12 | PSC | Partial State Capable |
| 12:8 | NCS | Command Slot 수 (0=1, 31=32) |
| 7 | CCCS | Command Completion Coalescing 지원 |
| 6 | EMS | Enclosure Management 지원 |
| 5 | SXS | External SATA 지원 |
| 4:0 | NP | 포트 수 (0=1, 31=32) |
GHC 레지스터
| 비트 | 필드 | 설명 |
|---|---|---|
| 31 | AE | AHCI Enable — 1이면 AHCI 모드, 0이면 레거시 IDE 호환 모드 |
| 1 | IE | Interrupt Enable — 전역 인터럽트 활성화 |
| 0 | HR | HBA Reset — 1 쓰면 HBA 리셋, 완료 후 자동으로 0 |
/* drivers/ata/ahci.h — AHCI 레지스터 오프셋 정의 */
enum {
HOST_CAP = 0x00, /* host capabilities */
HOST_CTL = 0x04, /* global host control */
HOST_IRQ_STAT = 0x08, /* interrupt status */
HOST_PORTS_IMPL = 0x0c, /* bitmap of implemented ports */
HOST_VERSION = 0x10, /* AHCI spec version */
HOST_CAP2 = 0x24, /* host capabilities, extended */
HOST_BOHC = 0x28, /* BIOS/OS handoff control */
/* port registers (0x100 + port * 0x80) */
PORT_LST_ADDR = 0x00, /* command list DMA addr (PxCLB) */
PORT_LST_ADDR_HI = 0x04, /* command list DMA addr hi (PxCLBU) */
PORT_FIS_ADDR = 0x08, /* FIS rx buf addr (PxFB) */
PORT_FIS_ADDR_HI = 0x0c, /* FIS rx buf addr hi (PxFBU) */
PORT_IRQ_STAT = 0x10, /* interrupt status (PxIS) */
PORT_IRQ_MASK = 0x14, /* interrupt enable/mask (PxIE) */
PORT_CMD = 0x18, /* port command (PxCMD) */
PORT_TFDATA = 0x20, /* taskfile data (PxTFD) */
PORT_SCR_STAT = 0x28, /* SATA phy status (PxSSTS) */
PORT_SCR_CTL = 0x2c, /* SATA phy control (PxSCTL) */
PORT_SCR_ERR = 0x30, /* SATA phy error (PxSERR) */
PORT_SCR_ACT = 0x34, /* SATA phy active (PxSACT for NCQ) */
PORT_CMD_ISSUE = 0x38, /* command issue (PxCI) */
};
포트 인터럽트 상태 (PxIS) 비트
| 비트 | 필드 | 설명 |
|---|---|---|
| 31 | CPDS | Cold Port Detect Status |
| 30 | TFES | Task File Error Status |
| 29 | HBFS | Host Bus Fatal Error Status |
| 28 | HBDS | Host Bus Data Error Status |
| 27 | IFS | Interface Fatal Error Status |
| 26 | INFS | Interface Non-fatal Error Status |
| 24 | OFS | Overflow Status |
| 23 | IPMS | Incorrect Port Multiplier Status |
| 22 | PRCS | PhyRdy Change Status |
| 7 | DMPS | Device Mechanical Presence Status |
| 6 | PCS | Port Connect Change Status |
| 5 | DPS | Descriptor Processed |
| 4 | UFS | Unknown FIS Interrupt |
| 3 | SDBS | Set Device Bits Interrupt (NCQ 완료) |
| 2 | DSS | DMA Setup FIS Interrupt |
| 1 | PSS | PIO Setup FIS Interrupt |
| 0 | DHRS | Device to Host Register FIS Interrupt |
PxCMD 레지스터 주요 필드
| 비트 | 필드 | 설명 |
|---|---|---|
| 31:28 | ICC | Interface Communication Control (전원 상태 전환 명령) |
| 27 | ASP | Aggressive Slumber/Partial — ALPM 자동 전환 전략 |
| 26 | ALPE | Aggressive Link PM Enable — ALPM 활성화 |
| 21 | ATAPI | ATAPI 디바이스 연결 표시 |
| 20 | APSTE | Auto Partial to Slumber Transitions Enabled |
| 19 | FBSCP | FIS-Based Switching Capable Port |
| 18 | ESP | External SATA Port |
| 17 | CPD | Cold Presence Detection |
| 16 | MPSP | Mechanical Presence Switch attached to Port |
| 15 | CR | Command List Running (읽기 전용) |
| 14 | FR | FIS Receive Running (읽기 전용) |
| 4 | FRE | FIS Receive Enable |
| 3:1 | CLO | Command List Override |
| 0 | ST | Start — Command List 처리 시작 |
/* AHCI 포트 시작/정지 절차 */
static int ahci_port_start_engine(void __iomem *port_mmio)
{
u32 cmd;
/* 1. FIS Receive 활성화 */
cmd = readl(port_mmio + PORT_CMD);
cmd |= PORT_CMD_FIS_RX; /* FRE = 1 */
writel(cmd, port_mmio + PORT_CMD);
/* 2. Command List 처리 시작 */
cmd |= PORT_CMD_START; /* ST = 1 */
writel(cmd, port_mmio + PORT_CMD);
/* CR(Command List Running)이 1이 될 때까지 대기 */
return ahci_wait_cmd_start(port_mmio);
}
static int ahci_port_stop_engine(void __iomem *port_mmio)
{
u32 cmd;
/* 1. ST = 0으로 클리어 */
cmd = readl(port_mmio + PORT_CMD);
cmd &= ~PORT_CMD_START;
writel(cmd, port_mmio + PORT_CMD);
/* 2. CR = 0 될 때까지 대기 (최대 500ms) */
return ahci_wait_cmd_stop(port_mmio);
}
PxSSTS (SStatus) 레지스터 상세
PxSSTS는 SATA PHY 링크의 현재 상태를 보고하는 읽기 전용 레지스터입니다.
dmesg에서 볼 수 있는 "SStatus 133"은 이 레지스터의 값입니다.
| 비트 | 필드 | 값 | 의미 |
|---|---|---|---|
| 3:0 | DET (Device Detection) | 0 | 디바이스 미검출, PHY 미확립 |
| 1 | 디바이스 검출, PHY 미확립 | ||
| 3 | 디바이스 검출, PHY 확립 (정상 통신 가능) | ||
| 4 | PHY 오프라인 (COMRESET 후 미응답) | ||
| 7:4 | SPD (Current Speed) | 0 | 속도 미결정 |
| 1 | Gen1 (1.5 Gbps) | ||
| 2 | Gen2 (3.0 Gbps) | ||
| 3 | Gen3 (6.0 Gbps) | ||
| 11:8 | IPM (Interface PM State) | 0 | 디바이스 미존재 |
| 1 | Active 상태 | ||
| 2 | Partial 절전 상태 | ||
| 6 | Slumber 절전 상태 | ||
| 8 | DevSlp 초절전 상태 |
PxSCTL (SControl) 레지스터
PxSCTL은 PHY 링크 동작을 제어하는 레지스터입니다. DET 필드에 값을 쓰면 COMRESET을 발행할 수 있습니다.
| 비트 | 필드 | 값 | 의미 |
|---|---|---|---|
| 3:0 | DET (Device Detection Init) | 0 | 제어 안 함 (정상 동작) |
| 1 | COMRESET 발행 (하드 리셋) | ||
| 4 | PHY 비활성화 | ||
| 7:4 | SPD (Speed Allowed) | 0 | 제한 없음 (최대 속도 협상) |
| 1 | Gen1만 허용 (1.5 Gbps) | ||
| 2 | Gen2까지 허용 (3.0 Gbps) | ||
| 3 | Gen3까지 허용 (6.0 Gbps) | ||
| 11:8 | IPM (Interface PM Transitions) | 0 | 제한 없음 |
| 1 | Partial 전이 비허용 | ||
| 2 | Slumber 전이 비허용 | ||
| 3 | Partial + Slumber 모두 비허용 |
/* COMRESET 발행 예시 (drivers/ata/libata-core.c 참조) */
static int sata_link_hardreset(struct ata_link *link,
const unsigned long *timing,
unsigned long deadline,
bool *online, int (*check_ready)(...))
{
u32 scontrol;
/* 1. SControl.DET = 1 → COMRESET 시작 */
sata_scr_read(link, SCR_CONTROL, &scontrol);
scontrol = (scontrol & 0x0f0) | 0x301; /* DET=1, SPD 유지 */
sata_scr_write(link, SCR_CONTROL, scontrol);
/* 2. 최소 1ms 대기 (SATA spec 요구) */
ata_msleep(link->ap, 1);
/* 3. SControl.DET = 0 → COMRESET 해제, 링크 협상 시작 */
scontrol = (scontrol & 0x0f0) | 0x300;
sata_scr_write(link, SCR_CONTROL, scontrol);
/* 4. SStatus.DET = 3 (PHY 확립) 될 때까지 대기 */
return ata_wait_after_reset(link, deadline, check_ready);
}
AHCI BIOS/OS 핸드오프 (BOHC)
BIOS가 AHCI를 사용 중이면 OS가 직접 레지스터를 건드리면 충돌이 발생합니다. AHCI 스펙은 BOHC(BIOS/OS Handoff Control) 레지스터(오프셋 0x28)를 통해 안전한 소유권 이전 절차를 정의합니다.
/* drivers/ata/ahci.c — BIOS/OS 핸드오프 절차 (간략화) */
static void ahci_bios_os_handoff(void __iomem *mmio)
{
u32 bohc;
bohc = readl(mmio + HOST_BOHC);
if (!(bohc & (1 << 0))) /* BOS (BIOS Owned Semaphore) */
return; /* BIOS가 소유 중이 아니면 핸드오프 불필요 */
/* 1. OOS(OS Owned Semaphore) 비트 설정 */
writel(bohc | (1 << 1), mmio + HOST_BOHC); /* OOS = 1 */
/* 2. BIOS가 BOS를 클리어할 때까지 최대 25ms 대기 */
int timeout = 25;
while (readl(mmio + HOST_BOHC) & (1 << 0)) {
msleep(1);
if (--timeout == 0) {
/* BIOS 응답 없음 → BB(BIOS Busy) 체크 후 강제 전환 */
if (readl(mmio + HOST_BOHC) & (1 << 4)) /* BB */
msleep(2000); /* BIOS 작업 완료 대기 */
break;
}
}
/* 3. OOS 유지, BOS 클리어 확인 → OS가 AHCI 소유 */
}
CAP2 확장 기능
CAP2 레지스터(오프셋 0x24)는 AHCI 1.2 이후 추가된 확장 기능을 보고합니다.
| 비트 | 필드 | 설명 |
|---|---|---|
| 5 | DESO | DevSlp Entrance from Slumber Only |
| 4 | SADM | Supports Aggressive Device Sleep Management |
| 3 | SDS | Supports Device Sleep (DevSlp) |
| 2 | APST | Auto Partial to Slumber Transitions |
| 1 | NVMP | NVMHCI Present (NVMe와 공유 HBA) |
| 0 | BOH | BIOS/OS Handoff 지원 |
# 사용자 공간에서 AHCI 레지스터 읽기 (setpci 활용)
# 먼저 AHCI 컨트롤러의 BDF 주소 확인
BDF=$(lspci | grep AHCI | awk '{print $1}')
# BAR5 (ABAR) 주소 확인
ABAR=$(setpci -s $BDF BASE_ADDRESS_5)
echo "ABAR = 0x$ABAR"
# devmem2로 AHCI CAP 레지스터 읽기 (root 필요)
# CAP = ABAR + 0x00
devmem2 0x${ABAR%0}0 w # CAP 레지스터
devmem2 0x${ABAR%0}4 w # GHC 레지스터
devmem2 0x${ABAR%0}c w # PI (Ports Implemented)
devmem2 0x${ABAR%0}10 w # VS (Version)
# 또는 /sys에서 간접 확인
cat /sys/class/scsi_host/host0/ahci_host_caps
cat /sys/class/scsi_host/host0/ahci_host_version
FIS (Frame Information Structure)
FIS는 SATA 전송 계층에서 호스트와 디바이스 간에 주고받는 데이터 패킷입니다. FIS 타입 바이트(1번째 바이트)로 FIS 종류를 식별하며, 각 FIS 타입마다 고정된 레이아웃을 가집니다.
FIS 타입 일람
| FIS 타입 | 값 | 방향 | 크기 (Dword) | 설명 |
|---|---|---|---|---|
| Register H2D | 0x27 | Host → Device | 5 | ATA 명령/제어 레지스터 전달 |
| Register D2H | 0x34 | Device → Host | 5 | ATA 상태/에러 레지스터 반환 |
| Data | 0x46 | 양방향 | 가변 | PIO/DMA 데이터 전송 |
| DMA Activate | 0x39 | Device → Host | 1 | 디바이스가 DMA 전송 준비 완료 알림 |
| DMA Setup | 0x41 | 양방향 | 7 | First Party DMA(NCQ) 셋업 |
| PIO Setup | 0x5F | Device → Host | 5 | PIO 전송 파라미터 전달 |
| Set Device Bits | 0xA1 | Device → Host | 2 | NCQ 완료 통지 (SActive 비트 갱신) |
/* include/linux/ata.h — FIS 타입 상수 */
enum {
ATA_FIS_TYPE_REG_H2D = 0x27,
ATA_FIS_TYPE_REG_D2H = 0x34,
ATA_FIS_TYPE_DATA = 0x46,
ATA_FIS_TYPE_DMA_ACT = 0x39,
ATA_FIS_TYPE_DMA_SETUP = 0x41,
ATA_FIS_TYPE_PIO_SETUP = 0x5F,
ATA_FIS_TYPE_SDB = 0xA1,
};
/* Register H2D FIS 구조체 (drivers/ata/ahci.c에서 사용) */
struct ahci_cmd_hdr {
__le32 opts; /* PRDTL, flags, CFL */
__le32 status; /* PRDBC (bytes transferred) */
__le32 tbl_addr; /* Command Table base addr low */
__le32 tbl_addr_hi; /* Command Table base addr high */
__le32 reserved[4]; /* 패딩, 총 32바이트 */
};
Register Device-to-Host FIS (0x34)
디바이스가 명령 실행 결과를 호스트에 보고할 때 사용하는 FIS입니다. ATA Status/Error 레지스터 값이 포함되어 있으며, AHCI는 이를 FIS Receive 영역에 자동으로 저장합니다.
| Dword | 바이트 3 | 바이트 2 | 바이트 1 | 바이트 0 |
|---|---|---|---|---|
| 0 | Error | Status | Reserved | I | PMPORT | FIS Type (0x34) |
| 1 | Device | LBA High | LBA Mid | LBA Low |
| 2 | Reserved | LBA High Exp | LBA Mid Exp | LBA Low Exp |
| 3 | Reserved | Reserved | Count [15:8] | Count [7:0] |
| 4 | Reserved | |||
Set Device Bits FIS (0xA1)
NCQ 명령의 완료를 알리는 FIS입니다. 여러 태그를 한 번에 완료 통보할 수 있어, 인터럽트 오버헤드를 줄입니다.
| Dword | 바이트 3 | 바이트 2 | 바이트 1 | 바이트 0 |
|---|---|---|---|---|
| 0 | Error | Status Hi | N | I | Status Lo | Reserved | I | PMPORT | FIS Type (0xA1) |
| 1 | SActive (완료된 태그 비트맵) | |||
AHCI HBA는 SDB FIS를 수신하면 자동으로 PxSACT 레지스터에서 해당 태그 비트를 클리어하고, PxIS.SDBS 인터럽트를 발생시킵니다. 드라이버는 이전 PxSACT 값과 비교하여 어떤 태그가 완료되었는지 식별합니다.
DMA Setup FIS (0x41)
NCQ의 First Party DMA에서 사용됩니다. 디바이스가 호스트에게 DMA 전송 파라미터를 알려주며, 호스트 메모리 주소(DMA Buffer Identifier)와 전송 방향을 포함합니다.
| Dword | 내용 |
|---|---|
| 0 | FIS Type (0x41) | PMPORT | D(방향) | I(인터럽트) | A(Auto-activate) |
| 1-2 | DMA Buffer Identifier Low/High (64비트 주소) |
| 3 | Reserved |
| 4 | DMA Buffer Offset |
| 5 | DMA Transfer Count |
| 6 | Reserved |
PIO Setup FIS (0x5F) 상세
PIO Setup FIS는 디바이스가 PIO 데이터 전송 파라미터를 호스트에 알려주는 FIS입니다. IDENTIFY DEVICE, SMART READ DATA 등 PIO 기반 명령에서 사용됩니다.
| Dword | 바이트 3 | 바이트 2 | 바이트 1 | 바이트 0 |
|---|---|---|---|---|
| 0 | Error | Status | Rsvd | I | D | PMPORT | FIS Type (0x5F) |
| 1 | Device | LBA High | LBA Mid | LBA Low |
| 2 | Reserved | LBA High Exp | LBA Mid Exp | LBA Low Exp |
| 3 | E_Status | Reserved | Count [15:8] | Count [7:0] |
| 4 | Reserved | Transfer Count (바이트 수) | ||
FIS 수신 영역 (FIS Receive Area)
AHCI HBA는 디바이스가 전송한 FIS를 자동으로 PxFB가 가리키는 메모리 영역에 저장합니다. 이 FIS Receive 영역은 포트당 256바이트 크기이며, 각 FIS 타입별 고정 오프셋에 저장됩니다. HBA는 수신한 FIS를 해당 오프셋에 DMA로 복사한 후 PxIS 인터럽트를 발생시킵니다.
| 오프셋 | 크기 | FIS 타입 | 설명 |
|---|---|---|---|
| 0x00 | 28 바이트 | DMA Setup FIS (DSFIS) | NCQ DMA 셋업 파라미터 저장 |
| 0x1C | 4 바이트 | (패딩) | 32바이트 정렬용 |
| 0x20 | 20 바이트 | PIO Setup FIS (PSFIS) | PIO 전송 파라미터 저장 |
| 0x34 | 12 바이트 | (패딩) | 정렬용 |
| 0x40 | 20 바이트 | Register D2H FIS (RFIS) | 명령 완료 상태/에러 저장 |
| 0x54 | 4 바이트 | (패딩) | 정렬용 |
| 0x58 | 8 바이트 | Set Device Bits FIS (SDBFIS) | NCQ 완료 태그 비트맵 저장 |
| 0x60 | 64 바이트 | Unknown FIS (UFIS) | 미분류 FIS 저장 (디버깅용) |
| 0xA0 | 96 바이트 | Reserved | 예약 영역 (총 256바이트 채움) |
/* drivers/ata/ahci.h — FIS Receive 영역 구조체 */
struct ahci_received_fis {
/* 0x00 — DMA Setup FIS */
u8 dsfis[28];
u8 dsfis_pad[4];
/* 0x20 — PIO Setup FIS */
u8 psfis[20];
u8 psfis_pad[12];
/* 0x40 — Register D2H FIS */
u8 rfis[20];
u8 rfis_pad[4];
/* 0x58 — Set Device Bits FIS */
u8 sdbfis[8];
/* 0x60 — Unknown FIS */
u8 ufis[64];
/* 0xA0 — Reserved */
u8 reserved[96];
} __attribute__((packed)); /* 총 256 바이트 */
/* 드라이버에서 D2H FIS 읽기 예시 */
static bool ahci_qc_fill_rtf(struct ata_queued_cmd *qc)
{
struct ahci_port_priv *pp = qc->ap->private_data;
u8 *rx_fis = pp->rx_fis; /* FIS Receive 영역 포인터 */
if (qc->tf.protocol == ATA_PROT_PIO && qc->dma_dir == DMA_FROM_DEVICE) {
/* PIO 명령: PIO Setup FIS(0x20)에서 결과 읽기 */
ata_tf_from_fis(rx_fis + RX_FIS_PIO_SETUP, &qc->result_tf);
} else {
/* DMA/NCQ 명령: Register D2H FIS(0x40)에서 결과 읽기 */
ata_tf_from_fis(rx_fis + RX_FIS_D2H_REG, &qc->result_tf);
}
return true;
}
SATA/AHCI 소프트웨어 스택
각 계층의 역할을 정리하면 다음과 같습니다:
| 계층 | 핵심 소스 파일 | 역할 |
|---|---|---|
| SCSI Mid Layer | drivers/scsi/ | SCSI 명령 디스패치, 디바이스 스캔 |
| libata-scsi | drivers/ata/libata-scsi.c | SCSI CDB → ATA 명령어 변환 (SAT) |
| libata-core | drivers/ata/libata-core.c | ATA 명령 발행, 디바이스 초기화, IDENTIFY |
| libata-sff | drivers/ata/libata-sff.c | SFF(Small Form Factor, 레거시 IDE) 지원 |
| libata-eh | drivers/ata/libata-eh.c | 에러 처리, 리셋, 복구 |
| AHCI LLD | drivers/ata/ahci.c | PCI AHCI 컨트롤러 드라이버 |
| AHCI Platform | drivers/ata/ahci_platform.c | 임베디드/SoC AHCI 컨트롤러 |
SCSI-ATA Translation (SAT) 계층 상세
libata-scsi.c는 SCSI 미드 레이어와 ATA 명령 사이의 브릿지 역할을 합니다.
이 변환 계층이 존재하기 때문에 SATA 디스크도 /dev/sd* 이름을 사용하며,
sg_inq, sg_vpd 등 SCSI 유틸리티가 동작합니다.
/* drivers/ata/libata-scsi.c — SAT 디바이스 탐색 */
static struct ata_device *ata_scsi_find_dev(
struct ata_port *ap, const struct scsi_device *scsidev)
{
struct ata_link *link;
unsigned int devno;
/* SCSI target ID로 ATA 디바이스 매핑:
* target 0 = link.device[0] (master)
* target 1 = link.device[1] (slave, PATA only) */
link = &ap->link;
if (unlikely(scsidev->channel || scsidev->lun))
return NULL;
devno = scsidev->id;
return &link->device[devno];
}
/* SAT 변환 테이블 등록 (SCSI opcode → 변환 함수 매핑) */
static const struct ata_scsi_xlat_handler ata_scsi_xlat_table[] = {
{ INQUIRY, ata_scsi_inquiry_xlat },
{ TEST_UNIT_READY, ata_scsi_tur_xlat },
{ READ_6, ata_scsi_rw_xlat },
{ READ_10, ata_scsi_rw_xlat },
{ READ_16, ata_scsi_rw_xlat },
{ WRITE_6, ata_scsi_rw_xlat },
{ WRITE_10, ata_scsi_rw_xlat },
{ WRITE_16, ata_scsi_rw_xlat },
{ SYNCHRONIZE_CACHE, ata_scsi_flush_xlat },
{ MODE_SENSE, ata_scsi_mode_sense_xlat },
{ START_STOP, ata_scsi_start_stop_xlat },
{ WRITE_SAME_16, ata_scsi_write_same_xlat }, /* → TRIM */
/* ... */
};
libata 모듈 구조와 심볼 의존성
libata는 여러 모듈로 분리되어 있으며, 각 모듈은 EXPORT_SYMBOL_GPL로 심볼을 공유합니다.
| 모듈 | 주요 익스포트 심볼 | 사용처 |
|---|---|---|
| libata-core | ata_qc_issue, ata_dev_classify, ata_dev_read_id | 모든 ATA LLD |
| libata-core | ata_host_alloc, ata_host_register, ata_host_activate | AHCI, SFF LLD |
| libata-scsi | ata_scsi_queuecmd, ata_scsi_slave_config | SCSI 미드 레이어 |
| libata-sff | ata_sff_port_ops, ata_bmdma_port_ops | 레거시 IDE LLD |
| libata-eh | ata_std_error_handler, sata_link_hardreset | 모든 ATA LLD |
요청 흐름 함수 경로 상세
/* 전체 I/O 요청 처리 흐름 (함수 이름 포함) */
/* ── 블록 레이어 ── */
submit_bio()
→ blk_mq_submit_bio()
→ blk_mq_dispatch_rq_list()
→ scsi_queue_rq() /* SCSI 미드 레이어 진입 */
/* ── SCSI 미드 레이어 ── */
scsi_queue_rq()
→ scsi_dispatch_cmd()
→ ata_scsi_queuecmd() /* libata-scsi 진입 */
/* ── libata-scsi (SAT 변환) ── */
ata_scsi_queuecmd()
→ __ata_scsi_queuecmd()
→ ata_scsi_translate()
→ ata_scsi_rw_xlat() /* CDB → taskfile 변환 */
→ ata_qc_issue() /* libata-core 진입 */
/* ── libata-core ── */
ata_qc_issue()
→ ap->ops->qc_prep(qc) /* = ahci_qc_prep() */
→ ap->ops->qc_issue(qc) /* = ahci_qc_issue() */
/* ── AHCI LLD ── */
ahci_qc_prep() /* CFIS + PRDT 채우기 */
ahci_qc_issue() /* PxCI/PxSACT 레지스터 쓰기 */
/* ── 하드웨어 (HBA) ── */
/* Command Header DMA 읽기 → Command Table DMA 읽기 */
/* → H2D FIS 생성 → PHY 전송 → 디바이스 응답 대기 */
ATA 전송 모드 (Transfer Mode)
ATA 디바이스는 다양한 전송 모드를 지원하며, libata는 IDENTIFY 데이터를 기반으로 최적 모드를 협상합니다.
| 모드 | 최대 속도 | 방식 | 비고 |
|---|---|---|---|
| PIO 0 | 3.3 MB/s | CPU 주도 전송 | 모든 ATA 디바이스 기본 지원 |
| PIO 1 | 5.2 MB/s | CPU 주도 전송 | |
| PIO 2 | 8.3 MB/s | CPU 주도 전송 | |
| PIO 3 | 11.1 MB/s | CPU 주도 전송 | IORDY 신호 도입 |
| PIO 4 | 16.7 MB/s | CPU 주도 전송 | IORDY 필수 |
| MDMA 0 | 4.2 MB/s | Multiword DMA | 레거시 DMA |
| MDMA 1 | 13.3 MB/s | Multiword DMA | |
| MDMA 2 | 16.7 MB/s | Multiword DMA | |
| UDMA 0 | 16.7 MB/s | Ultra DMA | 더블 에지 클럭킹 도입 |
| UDMA 1 | 25.0 MB/s | Ultra DMA | |
| UDMA 2 | 33.3 MB/s | Ultra DMA | ATA/33 |
| UDMA 3 | 44.4 MB/s | Ultra DMA | |
| UDMA 4 | 66.7 MB/s | Ultra DMA | ATA/66, 80핀 케이블 필요 |
| UDMA 5 | 100 MB/s | Ultra DMA | ATA/100 |
| UDMA 6 | 133 MB/s | Ultra DMA | ATA/133 (PATA 최종) |
ata_dev_set_xfermode()는
SET FEATURES(0xEF) 명령으로 디바이스에 전송 모드를 통보합니다.
libata 서브시스템
libata는 리눅스 커널에서 ATA/SATA 디바이스를 지원하는 핵심 서브시스템입니다.
SCSI 미드 레이어 위에 구축되어 있으며, ATA 명령어를 SCSI 명령어로 변환(SAT)합니다.
다음은 libata의 핵심 데이터 구조체 계층입니다.
/* include/linux/libata.h — 핵심 구조체 계층 */
/* ata_host: 하나의 AHCI 컨트롤러 (PCI 디바이스) */
struct ata_host {
struct device *dev; /* PCI/platform 디바이스 */
void __iomem *iomap[6]; /* PCI BAR 매핑 */
unsigned int n_ports; /* 포트 수 */
struct ata_port *ports[0]; /* 가변 배열 */
/* ... */
};
/* ata_port: SATA 포트 하나 (물리적 커넥터) */
struct ata_port {
struct ata_host *host; /* 소속 호스트 */
const struct ata_port_operations *ops; /* 드라이버 콜백 */
struct ata_link link; /* 기본 링크 */
struct ata_link *pmp_link; /* PMP 멀티 링크 */
unsigned int port_no; /* 포트 번호 */
struct ata_queued_cmd qcmd[ATA_MAX_QUEUE]; /* 커맨드 슬롯 */
unsigned int qc_active; /* 활성 QC 비트맵 */
/* ... */
};
/* ata_link: 포트와 디바이스 사이의 링크 */
struct ata_link {
struct ata_port *ap; /* 소속 포트 */
struct ata_device device[2]; /* dev0, dev1 (PATA 호환) */
unsigned int pmp; /* PMP 포트 번호 */
struct ata_eh_info eh_info; /* EH 정보 */
/* ... */
};
/* ata_device: 연결된 ATA/ATAPI 디바이스 */
struct ata_device {
struct ata_link *link; /* 소속 링크 */
unsigned int devno; /* 디바이스 번호 (0 or 1) */
u16 id[ATA_ID_WORDS]; /* IDENTIFY 데이터 */
unsigned int class; /* ATA_DEV_ATA, ATA_DEV_ATAPI 등 */
u64 n_sectors; /* 총 섹터 수 */
/* ... */
};
/* ata_queued_cmd: 발행된 하나의 ATA 명령 */
struct ata_queued_cmd {
struct ata_port *ap;
struct ata_device *dev;
struct scsi_cmnd *scsicmd; /* 원본 SCSI 명령 */
struct ata_taskfile tf; /* ATA taskfile 레지스터 */
unsigned int tag; /* 커맨드 슬롯 태그 (0~31) */
unsigned int n_elem; /* scatter-gather 엔트리 수 */
/* ... */
};
ata_port_operations 주요 콜백
| 콜백 | 호출 시점 | 설명 |
|---|---|---|
qc_prep | 커맨드 발행 직전 | Command Table, CFIS, PRDT 채우기 |
qc_issue | 커맨드 발행 | PxCI 비트 쓰기 → HBA에 커맨드 전달 |
qc_fill_rtf | 커맨드 완료 후 | 결과 taskfile(D2H FIS) 읽기 |
freeze | EH 시작 | 포트 인터럽트 비활성화 |
thaw | EH 종료 | 포트 인터럽트 재활성화 |
prereset | 리셋 전 | 리셋 전 조건 검사 (데드라인 설정) |
softreset | 소프트 리셋 | SRST FIS 전송 |
hardreset | 하드 리셋 | COMRESET 발행 (PHY 레벨 리셋) |
postreset | 리셋 후 | SError 클리어, 디바이스 재식별 |
error_handler | 에러 발생 | 에러 분류, 복구 전략 결정 |
port_start | 포트 초기화 | DMA 영역 할당, 포트 활성화 |
port_stop | 포트 종료 | DMA 영역 해제, 포트 비활성화 |
/* drivers/ata/ahci.c — AHCI 드라이버의 port_operations */
static struct ata_port_operations ahci_ops = {
.inherits = &sata_pmp_port_ops,
.qc_defer = ahci_pmp_qc_defer,
.qc_prep = ahci_qc_prep,
.qc_issue = ahci_qc_issue,
.qc_fill_rtf = ahci_qc_fill_rtf,
.freeze = ahci_freeze,
.thaw = ahci_thaw,
.softreset = ahci_softreset,
.hardreset = ahci_hardreset,
.postreset = ahci_postreset,
.pmp_softreset = ahci_softreset,
.pmp_hardreset = ahci_hardreset,
.error_handler = ahci_error_handler,
.post_internal_cmd = ahci_post_internal_cmd,
.port_start = ahci_port_start,
.port_stop = ahci_port_stop,
};
libata 초기화 흐름
/* AHCI 드라이버 로드 시 libata 초기화 경로 */
ahci_pci_probe()
→ ata_host_alloc_pinfo() /* ata_host + ata_port 할당 */
→ ata_port_alloc() /* 포트당 메모리 할당 */
→ ahci_host_activate()
→ ata_host_start() /* port_start 콜백 호출 */
→ ahci_port_start() /* DMA 영역 할당 */
→ devm_request_threaded_irq() /* 인터럽트 핸들러 등록 */
→ ata_host_register() /* 호스트 등록 */
→ async_port_probe() /* 비동기 포트 프로브 */
→ ata_port_probe()
→ ata_eh_recover() /* 초기 리셋 + 디바이스 발견 */
→ ata_dev_read_id() /* IDENTIFY DEVICE */
→ ata_dev_configure() /* 전송 모드, NCQ 설정 */
→ scsi_add_host() /* SCSI 호스트 등록 */
→ scsi_scan_host() /* SCSI 디바이스 스캔 */
IDENTIFY DEVICE 데이터
ATA 디바이스가 IDENTIFY DEVICE (0xEC) 명령에 응답하는 512바이트 데이터에는 디바이스의 모든 기능과 속성이 포함되어 있습니다. libata는 이 데이터를 파싱하여 전송 모드, NCQ 지원 여부, 48비트 LBA 지원 등을 결정합니다.
| Word | 필드 | 설명 |
|---|---|---|
| 0 | General Config | 디바이스 타입 (ATA/ATAPI) |
| 10-19 | Serial Number | 시리얼 번호 (20자 ASCII) |
| 23-26 | Firmware Rev | 펌웨어 버전 (8자 ASCII) |
| 27-46 | Model Number | 모델 번호 (40자 ASCII) |
| 47 | Max Sectors/DRQ | READ/WRITE MULTIPLE 최대 섹터 |
| 49 | Capabilities | DMA/LBA 지원 여부 |
| 60-61 | Total Sectors (28) | 28비트 LBA 총 섹터 수 |
| 75 | Queue Depth | NCQ 큐 깊이 (0=1, 31=32) |
| 76 | SATA Capabilities | NCQ, 속도, PHY 이벤트 카운터 |
| 77 | SATA Features | NCQ Priority, Unload, DevSlp 등 |
| 80 | Major Version | ATA 버전 (ATA-8, ACS-2, ACS-3 등) |
| 83 | Command Set 2 | 48비트 LBA, SMART, Security 등 |
| 100-103 | Total Sectors (48) | 48비트 LBA 총 섹터 수 |
| 106 | Logical Sector | 논리 섹터 크기 (512B or 4KB) |
| 217 | Nominal RPM | 회전 속도 (1=SSD, 5400/7200/10000/15000) |
# IDENTIFY 데이터 전체 확인 (hdparm)
hdparm -I /dev/sda
# 주요 항목 해석 예:
# Model Number: Samsung SSD 870 EVO 500GB
# Serial Number: S5Y1NX0T123456K
# Firmware Revision: SVT02B6Q
# Transport: Serial, ATA8-AST, SATA Rev 3.0
# Queue depth: 32
# SATA Gen3 signaling speed (6.0Gb/s)
# Native Command Queueing (NCQ)
ata_taskfile 구조체
ata_taskfile은 ATA 레지스터의 소프트웨어 표현으로, 모든 ATA 명령 발행과 결과 읽기에 사용됩니다.
48비트 LBA 모드에서는 hob(High Order Byte) 필드가 확장 레지스터를 담당합니다.
/* include/linux/ata.h — ata_taskfile 구조체 */
struct ata_taskfile {
unsigned long flags; /* ATA_TFLAG_* */
u8 protocol; /* ATA_PROT_* */
u8 ctl; /* Control 레지스터 (nIEN, SRST) */
/* 28비트 Taskfile 레지스터 */
u8 command; /* Command 레지스터 (0x25=READ_DMA_EXT 등) */
u8 feature; /* Feature 레지스터 (SET FEATURES 하위 명령) */
u8 lbal; /* LBA Low [7:0] */
u8 lbam; /* LBA Mid [15:8] */
u8 lbah; /* LBA High [23:16] */
u8 device; /* Device/Head 레지스터 */
u8 nsect; /* Sector Count [7:0] */
/* 48비트 확장 (HOB: High Order Byte) */
u8 hob_feature; /* Feature Exp */
u8 hob_lbal; /* LBA Low Exp [31:24] */
u8 hob_lbam; /* LBA Mid Exp [39:32] */
u8 hob_lbah; /* LBA High Exp [47:40] */
u8 hob_nsect; /* Sector Count Exp [15:8] */
/* 결과용 (D2H FIS에서 읽은 값) */
u8 error; /* Error 레지스터 */
u8 status; /* Status 레지스터 (BSY, DRQ, ERR 등) */
u8 auxiliary; /* Auxiliary 레지스터 (NCQ 확장) */
};
/* Taskfile → H2D FIS 변환 (libata-core.c) */
void ata_tf_to_fis(const struct ata_taskfile *tf, u8 pmp, int is_cmd, u8 *fis)
{
fis[0] = ATA_FIS_TYPE_REG_H2D; /* 0x27 */
fis[1] = (pmp & 0x0f) | (is_cmd ? 0x80 : 0); /* C 비트 */
fis[2] = tf->command;
fis[3] = tf->feature;
fis[4] = tf->lbal;
fis[5] = tf->lbam;
fis[6] = tf->lbah;
fis[7] = tf->device;
fis[8] = tf->hob_lbal;
fis[9] = tf->hob_lbam;
fis[10] = tf->hob_lbah;
fis[11] = tf->hob_feature;
fis[12] = tf->nsect;
fis[13] = tf->hob_nsect;
/* fis[14..19] = reserved/0 */
}
libata 명령 프로토콜
ATA 명령은 데이터 전송 방식에 따라 여러 프로토콜로 분류됩니다.
ata_taskfile.protocol 필드가 이를 결정하며, 프로토콜에 따라 AHCI의 처리 방식이 달라집니다.
| 프로토콜 | 상수 | 데이터 전송 | 예시 명령 |
|---|---|---|---|
| ATA_PROT_NODATA | 1 | 없음 | SET FEATURES, FLUSH CACHE, STANDBY |
| ATA_PROT_PIO | 2 | PIO (CPU 주도) | IDENTIFY DEVICE, SMART READ DATA |
| ATA_PROT_DMA | 3 | DMA (HBA 주도) | READ/WRITE DMA EXT |
| ATA_PROT_NCQ | 4 | First Party DMA | READ/WRITE FPDMA QUEUED |
| ATA_PROT_NCQ_NODATA | 5 | NCQ 비데이터 | NCQ Non-Data 서브커맨드 |
| ATAPI_PROT_NODATA | 6 | 없음 (ATAPI) | TEST UNIT READY (ATAPI) |
| ATAPI_PROT_PIO | 7 | PIO (ATAPI) | INQUIRY (ATAPI) |
| ATAPI_PROT_DMA | 8 | DMA (ATAPI) | READ(10) (ATAPI CD/DVD) |
ata_eh_info와 EH 컨텍스트
libata의 에러 핸들러(EH)는 ata_eh_info 구조체에 에러 정보를 축적합니다.
인터럽트 핸들러에서 에러를 감지하면 ata_ehi_push_desc()로 설명을 추가하고,
EH 스레드가 이를 분석하여 복구 전략을 결정합니다.
/* include/linux/libata.h — EH 정보 축적 구조 */
struct ata_eh_info {
struct ata_device *dev; /* 에러 발생 디바이스 */
u32 serror; /* SError 레지스터 스냅샷 */
unsigned int err_mask; /* AC_ERR_* 비트맵 */
unsigned int action; /* ATA_EH_* 복구 액션 */
unsigned int dev_action[ATA_MAX_DEVICES];
char desc[ATA_EH_DESC_LEN]; /* 에러 설명 텍스트 */
int desc_len;
};
/* 에러 마스크 예시 (include/linux/libata.h) */
enum {
AC_ERR_DEV = (1 << 0), /* 디바이스 에러 (Status.ERR) */
AC_ERR_HSM = (1 << 1), /* HSM(Host State Machine) 위반 */
AC_ERR_TIMEOUT = (1 << 2), /* 명령 타임아웃 */
AC_ERR_MEDIA = (1 << 3), /* 미디어 에러 (불량 섹터) */
AC_ERR_ATA_BUS = (1 << 4), /* ATA 버스 에러 (CRC 등) */
AC_ERR_HOST_BUS = (1 << 5), /* 호스트 버스 에러 */
AC_ERR_NCQ = (1 << 7), /* NCQ 관련 에러 */
};
/* 복구 액션 (에러 심각도에 따라 에스컬레이션) */
enum {
ATA_EH_REVALIDATE = (1 << 0), /* 디바이스 재검증 */
ATA_EH_SOFTRESET = (1 << 1), /* SRST FIS 전송 */
ATA_EH_HARDRESET = (1 << 2), /* COMRESET 발행 */
ATA_EH_RESET = ATA_EH_SOFTRESET | ATA_EH_HARDRESET,
ATA_EH_SET_ACTIVE = (1 << 4), /* 전원 상태 Active 강제 */
};
libata 링크 속도 관리
libata는 IDENTIFY 데이터와 SControl 레지스터를 사용하여 전송 모드를 협상합니다.
ata_dev_set_xfermode()는 SET FEATURES(0xEF) 명령으로 디바이스에 선택된 모드를 통보합니다.
/* drivers/ata/libata-core.c — 전송 모드 설정 (간략화) */
static int ata_dev_set_xfermode(struct ata_device *dev)
{
struct ata_taskfile tf;
ata_tf_init(dev, &tf);
tf.command = ATA_CMD_SET_FEATURES; /* 0xEF */
tf.feature = SETFEATURES_XFER; /* 0x03: Set Transfer Mode */
tf.nsect = dev->xfer_mode; /* 예: XFER_UDMA_6 = 0x46 */
tf.protocol = ATA_PROT_NODATA;
/* 내부 명령으로 발행 (동기 대기) */
return ata_exec_internal(dev, &tf, NULL, DMA_NONE, NULL, 0, 0);
}
/* UDMA 모드 선택 로직 (ata_dev_configure에서 호출) */
/* 1. IDENTIFY Word 88: UDMA 모드 지원 비트 확인 */
/* 2. 컨트롤러가 지원하는 최대 UDMA 모드와 교집합 */
/* 3. 80핀 케이블 감지 (PATA만 해당) */
/* 4. 가장 높은 공통 모드 선택 */
ATA 명령어 주요 목록
| 명령 | 코드 | 프로토콜 | 설명 |
|---|---|---|---|
| IDENTIFY DEVICE | 0xEC | PIO | 디바이스 정보 512B 읽기 (모델명, 용량, 기능) |
| IDENTIFY PACKET DEVICE | 0xA1 | PIO | ATAPI 디바이스 정보 읽기 |
| READ DMA EXT | 0x25 | DMA | 48비트 LBA DMA 읽기 |
| WRITE DMA EXT | 0x35 | DMA | 48비트 LBA DMA 쓰기 |
| READ FPDMA QUEUED | 0x60 | NCQ | NCQ 읽기 (최대 32 동시 태그) |
| WRITE FPDMA QUEUED | 0x61 | NCQ | NCQ 쓰기 (최대 32 동시 태그) |
| SET FEATURES | 0xEF | NODATA | 전송 모드, 캐시, 전원 등 설정 변경 |
| FLUSH CACHE EXT | 0xEA | NODATA | 디바이스 캐시 플러시 (fsync 매핑) |
| SMART READ DATA | 0xB0/0xD0 | PIO | S.M.A.R.T. 건강 정보 읽기 |
| SMART RETURN STATUS | 0xB0/0xDA | NODATA | 디바이스 건강 상태 확인 (PASS/FAIL) |
| SECURITY SET PASSWORD | 0xF1 | PIO | ATA 보안 비밀번호 설정 |
| SECURITY ERASE UNIT | 0xF4 | PIO | 보안 삭제 (전체 디스크 소거) |
| DATA SET MANAGEMENT | 0x06 | DMA | TRIM/Unmap (SSD 가비지 컬렉션 힌트) |
| SANITIZE DEVICE | 0xB4 | NODATA | 디바이스 데이터 완전 삭제 |
| CHECK POWER MODE | 0xE5 | NODATA | 현재 전원 상태 확인 (Active/Idle/Standby) |
| STANDBY IMMEDIATE | 0xE0 | NODATA | 즉시 대기 모드 전환 |
ahci_qc_prep()에서 H2D FIS 구성 시 특수 처리가 필요합니다.
NCQ 명령은 PxSACT에 태그 비트를 먼저 설정한 후 PxCI를 써야 합니다.
AHCI 드라이버 구현
AHCI 드라이버는 PCI 디바이스로 프로브(probe)되며, 초기화 과정은 다음과 같습니다:
/* drivers/ata/ahci.c — PCI 프로브 경로 (간략화) */
static int ahci_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct ata_host *host;
struct ahci_host_priv *hpriv;
int n_ports, rc;
/* 1. PCI 리소스 활성화 */
rc = pcim_enable_device(pdev);
/* 2. BAR5 (ABAR) 메모리 매핑 */
rc = pcim_iomap_regions(pdev, 1 << AHCI_PCI_BAR, DRV_NAME);
hpriv->mmio = pcim_iomap_table(pdev)[AHCI_PCI_BAR];
/* 3. HBA 리셋 및 AHCI 모드 활성화 */
ahci_reset_controller(host);
/* 4. 포트 수 확인 (CAP.NP, PI) */
n_ports = ahci_nr_ports(hpriv->cap);
/* 5. ata_host 할당 */
host = ata_host_alloc_pinfo(&pdev->dev, ppi, n_ports);
/* 6. 포트별 DMA 영역 할당 (Command List + FIS Receive) */
/* ahci_port_start()에서 수행 */
/* 7. 인터럽트 등록 및 호스트 활성화 */
rc = ahci_host_activate(host);
return rc;
}
Command Header 필드 상세
| 필드 | 비트 | 설명 |
|---|---|---|
| PRDTL | 31:16 | PRDT 엔트리 수 |
| PMP | 15:12 | Port Multiplier 포트 번호 |
| C | 10 | Clear Busy upon R_OK (Set Device Bits FIS) |
| B | 9 | BIST FIS |
| R | 8 | Reset (SRST 비트) |
| P | 7 | Prefetchable (PRDT 프리페치 가능) |
| W | 6 | Write (1=H2D 데이터, 0=D2H 데이터) |
| A | 5 | ATAPI (ACMD 필드 사용) |
| CFL | 4:0 | Command FIS Length (Dword 단위, 보통 5) |
PRD (Physical Region Descriptor) 엔트리
/* AHCI PRDT 엔트리 (16바이트) */
struct ahci_sg {
__le32 addr; /* DBA: Data Base Address (물리 주소 하위) */
__le32 addr_hi; /* DBAU: Data Base Address Upper */
__le32 reserved;
__le32 flags_size; /* DBC[21:0]: 바이트 카운트 (0-based), 비트31: I (인터럽트) */
};
/* DBC 최대값: 0x3FFFFF = 4MB - 1 (한 PRD 엔트리당 최대 4MB 전송) */
/* 전체 PRDT 크기: 최대 65535 엔트리 × 4MB = ~256TB (실제로는 제한적) */
ahci_port_start() DMA 영역 할당 상세
AHCI는 각 포트에 대해 Command List(1024바이트), FIS Receive(256바이트), 그리고
커맨드 슬롯당 Command Table을 DMA 일관(coherent) 메모리로 할당해야 합니다.
ahci_port_start()는 포트 초기화 시 이 영역들을 할당하고 레지스터에 물리 주소를 기록합니다.
/* drivers/ata/ahci.c — ahci_port_start (간략화) */
static int ahci_port_start(struct ata_port *ap)
{
struct ahci_host_priv *hpriv = ap->host->private_data;
struct ahci_port_priv *pp;
void *mem;
dma_addr_t mem_dma;
size_t dma_sz, rx_fis_sz;
pp = devm_kzalloc(dev, sizeof(*pp), GFP_KERNEL);
/* ── 1. Command List + Command Table 할당 ── */
/* Command Table 크기: 128B(CFIS+ACMD+Reserved) + PRDT */
dma_sz = AHCI_CMD_SLOT_SZ /* 32 * 32 = 1024B (CLB) */
+ AHCI_CMD_TBL_HDR * AHCI_MAX_CMDS /* 128B * 32 = 4096B */
+ AHCI_MAX_SG * sizeof(struct ahci_sg) * AHCI_MAX_CMDS;
mem = dmam_alloc_coherent(dev, dma_sz, &mem_dma, GFP_KERNEL);
memset(mem, 0, dma_sz);
/* Command List Base Address 설정 */
pp->cmd_slot = mem;
pp->cmd_slot_dma = mem_dma;
mem += AHCI_CMD_SLOT_SZ;
mem_dma += AHCI_CMD_SLOT_SZ;
/* 각 슬롯의 Command Table 주소 기록 */
for (int i = 0; i < AHCI_MAX_CMDS; i++) {
pp->cmd_tbl[i] = mem;
pp->cmd_tbl_dma[i] = mem_dma;
/* Command Header에 CTBA/CTBAU 기록 */
pp->cmd_slot[i].tbl_addr = cpu_to_le32(mem_dma & 0xFFFFFFFF);
pp->cmd_slot[i].tbl_addr_hi = cpu_to_le32((mem_dma >> 16) >> 16);
mem += AHCI_CMD_TBL_SZ; /* 128B + PRDT 크기 */
mem_dma += AHCI_CMD_TBL_SZ;
}
/* ── 2. FIS Receive 영역 할당 ── */
rx_fis_sz = AHCI_RX_FIS_SZ; /* 256 바이트 */
pp->rx_fis = dmam_alloc_coherent(dev, rx_fis_sz,
&pp->rx_fis_dma, GFP_KERNEL);
memset(pp->rx_fis, 0, rx_fis_sz);
/* ── 3. 포트 레지스터에 물리 주소 기록 ── */
void __iomem *port_mmio = ahci_port_base(ap);
writel(pp->cmd_slot_dma & 0xFFFFFFFF,
port_mmio + PORT_LST_ADDR);
writel(pp->cmd_slot_dma >> 32,
port_mmio + PORT_LST_ADDR_HI);
writel(pp->rx_fis_dma & 0xFFFFFFFF,
port_mmio + PORT_FIS_ADDR);
writel(pp->rx_fis_dma >> 32,
port_mmio + PORT_FIS_ADDR_HI);
/* 4. FIS Receive 활성화 → Command Engine 시작 */
ahci_start_engine(ap);
return 0;
}
AHCI 인터럽트 처리 상세
AHCI 인터럽트 핸들러는 GHC.IS(전역 인터럽트 상태) 레지스터를 읽어 어느 포트에서 인터럽트가 발생했는지 확인한 후, 각 포트의 PxIS를 읽어 이벤트 종류를 식별합니다.
/* drivers/ata/libahci.c — 인터럽트 핸들러 (간략화) */
static irqreturn_t ahci_single_level_irq_intr(
int irq, void *dev_instance)
{
struct ata_host *host = dev_instance;
struct ahci_host_priv *hpriv = host->private_data;
void __iomem *mmio = hpriv->mmio;
u32 irq_stat, irq_masked;
/* 1. 전역 인터럽트 상태 읽기 */
irq_stat = readl(mmio + HOST_IRQ_STAT);
if (!irq_stat)
return IRQ_NONE;
/* 구현된 포트만 마스킹 */
irq_masked = irq_stat & hpriv->port_map;
/* 2. 각 포트별 인터럽트 처리 */
for (int i = 0; i < host->n_ports; i++) {
if (!(irq_masked & (1 << i)))
continue;
struct ata_port *ap = host->ports[i];
ahci_port_intr(ap);
}
/* 3. 전역 IS 클리어 (W1C: Write 1 to Clear) */
writel(irq_stat, mmio + HOST_IRQ_STAT);
return IRQ_HANDLED;
}
/* 포트 인터럽트 처리 */
static void ahci_port_intr(struct ata_port *ap)
{
void __iomem *port_mmio = ahci_port_base(ap);
u32 status = readl(port_mmio + PORT_IRQ_STAT);
/* PxIS 클리어 */
writel(status, port_mmio + PORT_IRQ_STAT);
if (status & PORT_IRQ_ERROR) {
/* 에러 인터럽트: EH(Error Handler) 트리거 */
ahci_error_intr(ap, status);
} else {
/* 정상 완료: QC(Queued Command) 완료 처리 */
ahci_handle_port_interrupt(ap, status);
/* → PxCI/PxSACT 비교하여 완료된 태그 식별 */
/* → ata_qc_complete() 호출 */
}
}
MSI/MSI-X 설정
AHCI 컨트롤러는 전통적인 INTx 핀 인터럽트 외에 MSI(Message Signaled Interrupt) 또는 MSI-X를 지원할 수 있습니다. MSI-X가 가능한 경우 포트별 독립 인터럽트 벡터를 할당하여 멀티코어 시스템에서 인터럽트 처리를 병렬화할 수 있습니다.
| 모드 | 벡터 수 | 특징 | 성능 |
|---|---|---|---|
| INTx (레거시) | 1 (공유) | PCI 핀 기반, 다른 디바이스와 공유 가능 | 낮음 (공유 인터럽트) |
| MSI | 1 | 메시지 기반, 공유 없음, 에지 트리거 | 중간 |
| MSI-X | 포트당 1개 | 포트별 독립 벡터, CPU 어피니티 설정 가능 | 높음 (멀티코어 최적) |
/* drivers/ata/ahci.c — MSI-X 설정 경로 (간략화) */
static int ahci_init_interrupts(struct pci_dev *pdev,
unsigned int n_ports,
struct ahci_host_priv *hpriv)
{
int nvec;
/* 1. MSI-X 시도 (포트별 벡터) */
nvec = pci_alloc_irq_vectors(pdev, n_ports, n_ports, PCI_IRQ_MSIX);
if (nvec == n_ports) {
hpriv->flags |= AHCI_HFLAG_MULTI_MSI;
return nvec;
}
/* 2. MSI 시도 (단일 벡터) */
nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (nvec == 1)
return nvec;
/* 3. INTx 폴백 */
pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX);
return 1;
}
커맨드 실행 경로
블록 레이어의 읽기 요청이 SATA 디스크에 도달하기까지의 전체 경로를 추적합니다.
1단계: 블록 요청 → SCSI 명령
/* blk-mq가 struct request를 생성하면 SCSI 레이어로 전달 */
scsi_queue_rq()
→ scsi_dispatch_cmd()
→ scsi_cmnd 구성 (CDB: READ_10 / READ_16)
→ host->hostt->queuecommand() /* = ata_scsi_queuecmd */
2단계: SCSI → ATA 변환 (SAT)
/* drivers/ata/libata-scsi.c */
ata_scsi_queuecmd()
→ __ata_scsi_queuecmd()
→ ata_scsi_translate()
→ ata_scsi_rw_xlat() /* READ_10 → taskfile 구성 */
/* SCSI CDB → ATA 명령 변환 예:
* READ_10 → READ_DMA_EXT (non-NCQ) 또는 READ_FPDMA_QUEUED (NCQ)
* WRITE_10 → WRITE_DMA_EXT 또는 WRITE_FPDMA_QUEUED
* INQUIRY → ATA IDENTIFY DEVICE
* TEST_UNIT_READY → ATA CHECK POWER MODE
*/
3단계: ATA 명령 발행
/* drivers/ata/libata-core.c */
ata_qc_issue()
→ ap->ops->qc_prep(qc) /* = ahci_qc_prep */
→ /* Command Table에 CFIS(H2D FIS) 채우기 */
→ /* scatter-gather 리스트 → PRDT 엔트리 채우기 */
→ /* Command Header 구성 (CFL, W, PRDTL 설정) */
→ ap->ops->qc_issue(qc) /* = ahci_qc_issue */
→ writel(1 << tag, port_mmio + PORT_CMD_ISSUE) /* PxCI 쓰기 */
4단계: HBA 하드웨어 동작
PxCI에 비트가 설정되면 HBA는 자동으로:
- Command List에서 해당 Command Header를 DMA로 읽기
- CTBA가 가리키는 Command Table을 DMA로 읽기
- CFIS를 Register H2D FIS로 변환하여 디바이스에 전송
- 디바이스의 응답에 따라 PRDT 기반 데이터 DMA 전송 수행
5단계: 완료 인터럽트
/* drivers/ata/libata-core.c + ahci.c — 완료 경로 */
ahci_interrupt() /* MSI/MSI-X 또는 INTx */
→ ahci_port_intr() /* PxIS 읽기 → 이벤트 식별 */
→ ahci_handle_port_interrupt()
→ ata_qc_complete() /* QC 완료 처리 */
→ scsi_done() /* SCSI 완료 콜백 */
→ blk_mq_end_request() /* 블록 요청 완료 */
SCSI-ATA Translation (SAT) 주요 변환 테이블
| SCSI 명령 (CDB) | ATA 명령 | 비고 |
|---|---|---|
| READ(6) / READ(10) / READ(16) | READ DMA EXT / READ FPDMA QUEUED | NCQ 활성 시 FPDMA |
| WRITE(6) / WRITE(10) / WRITE(16) | WRITE DMA EXT / WRITE FPDMA QUEUED | NCQ 활성 시 FPDMA |
| INQUIRY | IDENTIFY DEVICE | VPD 페이지 에뮬레이션 |
| TEST UNIT READY | CHECK POWER MODE | 디바이스 상태 확인 |
| READ CAPACITY(10/16) | IDENTIFY DEVICE | 섹터 수/크기 추출 |
| MODE SENSE / MODE SELECT | SET FEATURES / IDENTIFY | 캐싱, 전원 관리 등 |
| SYNCHRONIZE CACHE | FLUSH CACHE EXT | Write Cache 플러시 |
| UNMAP (TRIM) | DATA SET MANAGEMENT | SSD TRIM 명령 |
| WRITE SAME | SCT Write Same | 패턴 쓰기 |
| ATA PASS-THROUGH(12/16) | (직접 전달) | hdparm, smartctl 사용 |
| REQUEST SENSE | (에뮬레이션) | libata가 내부적으로 생성 |
| REPORT LUNS | (에뮬레이션) | 단일 LUN 응답 |
/* drivers/ata/libata-scsi.c — SCSI CDB → ATA 변환 디스패치 */
static unsigned int ata_scsi_rw_xlat(
struct ata_queued_cmd *qc)
{
struct scsi_cmnd *scmd = qc->scsicmd;
struct ata_taskfile *tf = &qc->tf;
u64 block;
u32 n_block;
/* SCSI CDB에서 LBA와 섹터 수 추출 */
scsi_10_lba_len(scmd->cmnd, &block, &n_block);
/* NCQ 가능하면 FPDMA, 아니면 DMA EXT */
if (ata_ncq_enabled(qc->dev)) {
tf->protocol = ATA_PROT_NCQ;
tf->command = (rw == READ) ?
ATA_CMD_FPDMA_READ : ATA_CMD_FPDMA_WRITE;
/* NCQ에서는 Sector Count 필드에 태그 번호 저장 */
tf->nsect = qc->tag << 3;
} else {
tf->protocol = ATA_PROT_DMA;
tf->command = (rw == READ) ?
ATA_CMD_READ_EXT : ATA_CMD_WRITE_EXT;
}
/* LBA를 taskfile의 LBA Low/Mid/High + Exp에 채우기 */
ata_tf_from_lba48(tf, block, n_block);
return 0;
}
taskfile 레지스터 상세 매핑
ata_taskfile 구조체의 각 필드가 Register H2D FIS(27h)의 바이트에 매핑됩니다.
| ata_taskfile 필드 | FIS 바이트 | H2D FIS 필드 | 설명 |
|---|---|---|---|
tf->command | Byte 2 | Command | ATA 명령 코드 (0x60=READ FPDMA 등) |
tf->feature | Byte 3 | Features (7:0) | NCQ: 전송 섹터 수 하위 |
tf->lbal | Byte 4 | LBA Low (7:0) | LBA [7:0] |
tf->lbam | Byte 5 | LBA Mid (7:0) | LBA [15:8] |
tf->lbah | Byte 6 | LBA High (7:0) | LBA [23:16] |
tf->device | Byte 7 | Device | 비트6: LBA, 비트4: DEV |
tf->hob_lbal | Byte 8 | LBA Low (15:8) | LBA [31:24] (48-bit) |
tf->hob_lbam | Byte 9 | LBA Mid (15:8) | LBA [39:32] (48-bit) |
tf->hob_lbah | Byte 10 | LBA High (15:8) | LBA [47:40] (48-bit) |
tf->hob_feature | Byte 11 | Features (15:8) | NCQ: 전송 섹터 수 상위 |
tf->nsect | Byte 12 | Count (7:0) | NCQ: 태그(비트 7:3) |
tf->hob_nsect | Byte 13 | Count (15:8) | 확장 섹터 카운트 |
tf->ctl | Byte 15 | Control | nIEN(1), SRST(2) |
ata_scsi_rw_xlat()에서 자동 처리됩니다.
DMA vs PIO 전송 모드 선택
libata는 디바이스 능력과 명령 유형에 따라 전송 모드를 자동 선택합니다.
/* drivers/ata/libata-core.c — PIO IORDY 판단 */
unsigned int ata_pio_need_iordy(const struct ata_device *adev)
{
if (adev->pio_mode > XFER_PIO_2)
return 1; /* PIO 3+: IORDY 필요 */
if (ata_id_is_cfa(adev->id))
return 1; /* CF 카드: 항상 */
return 0;
}
/* 전송 모드 선택: UDMA > MWDMA > PIO */
static int ata_dev_set_mode(struct ata_device *dev)
{
unsigned int udma_mask, dma_mask, pio_mask;
ata_id_to_dma_mode(dev->id, &dma_mask, &udma_mask);
pio_mask = ata_id_pio_modes(dev->id);
if (udma_mask)
dev->xfer_mode = fls(udma_mask) - 1 + XFER_UDMA_0;
else if (dma_mask)
dev->xfer_mode = fls(dma_mask) - 1 + XFER_MW_DMA_0;
else
dev->xfer_mode = fls(pio_mask) - 1 + XFER_PIO_0;
ata_dev_set_feature(dev, SETFEATURES_XFER, dev->xfer_mode);
return 0;
}
scatter-gather → PRDT 변환
/* drivers/ata/ahci.c — SG → PRDT 변환 */
static unsigned int ahci_fill_sg(struct ata_queued_cmd *qc)
{
struct ahci_sg *ahci_sg = pp->cmd_tbl + AHCI_CMD_TBL_HDR_SZ;
struct scatterlist *sg;
unsigned int si;
for_each_sg(qc->sg, sg, qc->n_elem, si) {
dma_addr_t addr = sg_dma_address(sg);
u32 sg_len = sg_dma_len(sg);
ahci_sg[si].addr = cpu_to_le32(addr & 0xFFFFFFFF);
ahci_sg[si].addr_hi = cpu_to_le32((addr >> 32) & 0xFFFFFFFF);
/* DBC: 바이트 카운트 - 1, 최대 4MB per entry */
ahci_sg[si].flags_size = cpu_to_le32(sg_len - 1);
}
return si; /* → Command Header PRDTL */
}
NCQ 심화
NCQ(Native Command Queuing)는 SATA 디바이스가 여러 명령을 동시에 받아 내부적으로 최적 순서로 재배열하여 처리하는 기술입니다. 특히 HDD에서 헤드 탐색 거리를 최소화하고, SSD에서 내부 병렬성을 활용하는 데 효과적입니다.
NCQ 관련 ATA 명령어
| 명령어 | 코드 | 설명 |
|---|---|---|
| READ FPDMA QUEUED | 0x60 | NCQ 읽기 (최대 65536 섹터) |
| WRITE FPDMA QUEUED | 0x61 | NCQ 쓰기 (최대 65536 섹터) |
| NCQ NON DATA | 0x63 | NCQ 비데이터 명령 (SATA 3.1+) |
| SEND FPDMA QUEUED | 0x64 | NCQ 호스트→디바이스 데이터 |
| RECEIVE FPDMA QUEUED | 0x65 | NCQ 디바이스→호스트 데이터 |
NCQ vs Non-NCQ 성능 비교
| 메트릭 | NCQ 비활성 (depth=1) | NCQ 활성 (depth=32) | 향상률 |
|---|---|---|---|
| 랜덤 4K 읽기 IOPS | ~10,000 | ~95,000 | ~9.5배 |
| 랜덤 4K 쓰기 IOPS | ~8,000 | ~85,000 | ~10.6배 |
| 순차 읽기 MB/s | ~540 | ~550 | 미미 |
| 순차 쓰기 MB/s | ~510 | ~520 | 미미 |
| 혼합 70/30 IOPS | ~7,000 | ~70,000 | ~10배 |
NCQ TRIM (Data Set Management)
SATA 3.1부터 TRIM 명령도 NCQ 프로토콜로 발행할 수 있습니다 (Queued TRIM). 기존 non-queued TRIM은 다른 I/O와 병렬 실행이 불가능해 I/O 지연이 발생했지만, NCQ TRIM은 읽기/쓰기 명령과 함께 큐에 넣어 동시에 처리할 수 있습니다.
# Queued TRIM 지원 확인
hdparm -I /dev/sda | grep -i "queued trim"
# * Queued TRIM supported
# 커널의 NCQ TRIM 사용 여부 확인
dmesg | grep "NCQ Send/Recv"
NCQ 구현 핵심
/* libata-core.c — NCQ 태그 할당 */
static unsigned int ata_qc_new(struct ata_port *ap)
{
unsigned int tag;
/* blk-mq 태그를 직접 사용 (qc->tag = scsi_cmnd->request->tag) */
/* NCQ: 태그 0~31, Non-NCQ: 내부 태그 ATA_TAG_INTERNAL */
tag = find_first_zero_bit(&ap->qc_active, ATA_MAX_QUEUE);
return tag;
}
/* ahci.c — NCQ 커맨드 발행 */
static unsigned int ahci_qc_issue(struct ata_queued_cmd *qc)
{
struct ata_port *ap = qc->ap;
void __iomem *port_mmio = ahci_port_base(ap);
if (qc->tf.protocol == ATA_PROT_NCQ) {
/* NCQ: PxSACT에 태그 비트 설정 */
writel(1 << qc->tag, port_mmio + PORT_SCR_ACT);
}
/* PxCI에 태그 비트 설정 → HBA가 커맨드 페치 */
writel(1 << qc->tag, port_mmio + PORT_CMD_ISSUE);
return 0;
}
ata_device의 flags에 ATA_DFLAG_NCQ_PRIO가 설정되면
sysfs를 통해 우선순위를 제어할 수 있습니다.
NCQ 에러 로그 분석
NCQ 명령 실패 시 디바이스는 NCQ Error Log(Log Page 10h)에 실패 정보를 기록합니다. libata는 이 로그를 READ LOG EXT(PIO) 명령으로 읽어 실패한 태그와 에러 원인을 파악합니다.
/* drivers/ata/libata-eh.c — NCQ 에러 로그 분석 */
static void ata_eh_analyze_ncq_error(struct ata_link *link)
{
struct ata_port *ap = link->ap;
u8 *buf = ap->ncq_err_page; /* 512바이트 버퍼 */
u8 tag;
/* READ LOG EXT, Log Page 10h (NCQ Command Error) */
ata_read_log_page(ap->dev, ATA_LOG_NCQ_ERR, 0, buf, 1);
/* Byte 0: NQ 비트(7) + 실패 태그(4:0) */
tag = buf[0] & 0x1F;
/* Byte 2: Status, Byte 3: Error 레지스터 */
link->eh_info.err_mask |= AC_ERR_DEV;
/* 해당 태그의 QC에 에러 설정 → 재시도 대상 */
qc = __ata_qc_from_tag(ap, tag);
qc->err_mask |= AC_ERR_DEV;
}
# smartctl로 NCQ 에러 로그 확인
smartctl -l sataphy /dev/sda # SATA PHY 이벤트 카운터
smartctl -l ncq /dev/sda # NCQ Command Error 로그
smartctl -l devstat /dev/sda # Device Statistics (GP Log 04h)
NCQ vs TCQ (Tagged Command Queuing)
| 항목 | TCQ (SCSI/PATA) | NCQ (SATA) |
|---|---|---|
| 표준 | ATA/ATAPI-4 (PATA) | SATA II (3Gbps+) |
| 최대 큐 깊이 | 32 | 32 |
| FIS 유형 | DMA QUEUED (C8h/CCh) | FPDMA (60h/61h) |
| 완료 통지 | SERVICE 명령 폴링 | SDB FIS (자동) |
| DMA 주도 | 호스트 주도 (Third Party DMA) | 디바이스 주도 (First Party DMA) |
| 채택 | 거의 미사용 (호환성 문제) | 사실상 표준 |
NCQ Non-Data 명령
SATA 3.1부터 NCQ NON DATA(0x63) 명령으로 데이터 전송 없이 NCQ 큐에 제어 명령을 넣을 수 있습니다. 일반 NCQ I/O와 함께 큐잉되어 순서 보장이 가능합니다.
| 서브커맨드 | Feature 값 | 설명 |
|---|---|---|
| Abort NCQ Queue | 0x00 | 지정 태그 또는 전체 NCQ 큐 취소 |
| Deadline Handling | 0x01 | 특정 태그에 완료 기한 설정 |
| Hybrid Demote By Size | 0x02 | 하이브리드 캐시에서 데이터 강등 |
| Hybrid Evict | 0x03 | 하이브리드 캐시에서 데이터 퇴출 |
| Set Features | 0x05 | NCQ 큐 내에서 SET FEATURES 실행 |
| Zero EXT | 0x06 | 지정 LBA 범위를 0으로 초기화 |
NCQ Streaming
NCQ Streaming은 연속 미디어 워크로드(비디오 녹화/재생 등)를 위한 기능으로, 디바이스에게 특정 NCQ 명령이 스트리밍 데이터임을 알려줍니다. 디바이스는 스트리밍 명령의 완료 시간을 보장하기 위해 내부 스케줄링을 조정합니다.
/* NCQ Streaming은 READ/WRITE FPDMA에서
* Auxiliary 필드(ICC: Isochronous Command Completion)를 사용
* Feature(15:8)의 Streaming 비트 설정 */
if (qc->flags & ATA_QCFLAG_STREAMING) {
tf->auxiliary |= ATA_AUX_STREAM;
/* Streaming ID와 완료 기한(deadline) 설정 */
tf->auxiliary |= stream_tag << 16;
}
M.2 폼 팩터
M.2(NGFF, Next Generation Form Factor)는 mSATA를 대체하는 소형 폼 팩터로, SATA와 PCIe(NVMe) 두 가지 인터페이스를 모두 지원합니다. 키(Key) 노치 위치로 지원하는 인터페이스를 구별합니다.
M.2 크기 규격
| 코드명 | 폭 (mm) | 길이 (mm) | 주요 용도 |
|---|---|---|---|
| 2230 | 22 | 30 | WiFi/BT, 소형 SSD |
| 2242 | 22 | 42 | SATA SSD, 일부 NVMe |
| 2260 | 22 | 60 | SATA/NVMe SSD |
| 2280 | 22 | 80 | 가장 일반적인 NVMe/SATA SSD |
| 22110 | 22 | 110 | 엔터프라이즈 NVMe SSD |
# M.2 SATA SSD 확인: TRAN이 'sata'이면 SATA M.2
lsblk -d -o NAME,TRAN,SIZE,MODEL
# NVMe는 TRAN이 비어있고 /dev/nvme* 경로 사용
ls /dev/nvme*
# M.2 SATA는 일반 SATA와 동일하게 /dev/sd* 사용
ls /dev/sd*
M.2 소켓 핀 배치 상세
M.2 커넥터는 총 75핀으로, 소켓 유형(Key)에 따라 각 핀에 할당되는 신호가 다릅니다.
| 핀 범위 | Socket 2 (B-key) | Socket 3 (M-key) | 비고 |
|---|---|---|---|
| 1-3 | GND | GND | 공통 |
| 4-5 | 3.3V | 3.3V | 전원 |
| 12-19 | Key B 노치 | USB 3.0 / HSIC | B-key 삽입 방지 |
| 20-23 | SATA B- / B+ | Reserved | SATA 수신 쌍 |
| 29-32 | PCIe x2 Rx | PCIe x4 Rx[0] | PCIe 수신 |
| 35-38 | PCIe x2 Tx | PCIe x4 Tx[0] | PCIe 송신 |
| 39-40 | I2C SDA/SCL | I2C SDA/SCL | 디바이스 관리 |
| 41-42 | SATA A- / A+ | Reserved | SATA 송신 쌍 |
| 47-50 | PCIe x2 Ref Clock | PCIe x4 Ref Clock | 기준 클럭 100MHz |
| 49-54 | Reserved | PCIe x4 Rx/Tx[2,3] | M-key 추가 레인 |
| 56-58 | Reserved | PCIe x4 Rx/Tx[1] | M-key 레인 1 |
| 59-66 | PCIe Ref Clock | Key M 노치 | M-key 삽입 방지 |
| 67-70 | UIM (SIM) | PCIe x4 Tx[3] | WWAN/PCIe |
| 71-75 | GND / 3.3V | GND / 3.3V | 전원/접지 |
M.2 열 관리
M.2 SSD, 특히 NVMe 모델은 소형 폼팩터에서 높은 발열이 문제됩니다. SATA M.2 SSD는 상대적으로 발열이 적지만(~45°C), NVMe는 80°C 이상까지 올라갈 수 있습니다.
# SMART로 SSD 온도 모니터링
smartctl -A /dev/sda | grep -i temperature
# 194 Temperature_Celsius 0x0022 067 052 000 Old_age Always - 33
# NVMe 온도 확인
smartctl -A /dev/nvme0 | grep -i temperature
# Temperature: 38 Celsius
# Warning Comp. Temp. Threshold: 85 Celsius
# Critical Comp. Temp. Threshold: 90 Celsius
# 실시간 온도 모니터링
watch -n1 "smartctl -A /dev/sda | grep Temperature"
mSATA에서 M.2로의 전환
mSATA(Mini-SATA)는 Mini PCIe와 동일한 물리 커넥터를 사용하지만 SATA 신호를 전달하는 규격이었습니다. M.2로 대체되면서 다음과 같은 차이가 있습니다.
| 항목 | mSATA | M.2 SATA |
|---|---|---|
| 커넥터 | Mini PCIe 52핀 | M.2 75핀 |
| 크기 | 30 x 50.95 mm (고정) | 22mm 폭, 길이 가변 |
| 인터페이스 | SATA 전용 | SATA + PCIe 겸용 |
| 최대 속도 | 600 MB/s (SATA III) | 600 MB/s (SATA) / 32 Gbps (PCIe 4.0 x4) |
| 현재 상태 | 단종 추세 | 현행 표준 |
메인보드 M.2 슬롯 식별 방법
메인보드의 M.2 슬롯이 SATA, NVMe, 또는 둘 다 지원하는지 확인하는 단계별 방법입니다.
# 1단계: M.2 슬롯의 Key 노치 확인 (물리적)
# - M-key만: 대부분 NVMe 전용
# - B+M key: SATA 가능, 일부 NVMe도 가능
# 2단계: lspci로 연결된 컨트롤러 확인
lspci -v | grep -A5 "SATA controller"
# AHCI 컨트롤러에 연결된 M.2 = SATA 지원
lspci -v | grep -A5 "Non-Volatile memory"
# NVMe 컨트롤러 = NVMe 지원
# 3단계: dmidecode로 슬롯 정보 확인 (root 필요)
dmidecode -t slot | grep -A10 "M.2"
# 4단계: SATA M.2 SSD가 인식되는지 확인
dmesg | grep -i "ata.*SATA link"
lsblk -d -o NAME,TRAN,MODEL | grep sata
# 5단계: NVMe M.2 SSD가 인식되는지 확인
nvme list
SATA vs NVMe 비교
| 항목 | SATA (AHCI) | NVMe |
|---|---|---|
| 버스 | SATA 전용 링크 | PCIe 레인 (x1~x4) |
| 최대 대역폭 | 600 MB/s (SATA III) | ~7 GB/s (PCIe 4.0 x4) |
| 큐 모델 | 1 큐 x 32 태그 | 65535 큐 x 65536 태그 |
| 커맨드 오버헤드 | 높음 (SCSI/SAT 변환) | 낮음 (직접 명령) |
| 인터럽트 | MSI/INTx (1~2개) | MSI-X (CPU 코어당) |
| 랜덤 4K IOPS | ~100K | ~1M+ |
| 레이턴시 | ~100 us | ~10 us |
| CPU 효율 | 낮음 | 높음 |
| 핫 플러그 | AHCI 지원 | PCIe Surprise Removal |
| 전원 관리 | ALPM/DIPM/DevSlp | APST (Autonomous PS) |
| 드라이버 소스 | drivers/ata/ | drivers/nvme/ |
| 디바이스 이름 | /dev/sdX | /dev/nvmeNnP |
프로토콜 오버헤드 정량 비교
SATA와 NVMe의 커맨드 처리 오버헤드를 단계별로 분해하면 성능 차이의 원인을 정확히 이해할 수 있습니다.
| 단계 | SATA (AHCI) 지연 | NVMe 지연 | 비고 |
|---|---|---|---|
| 명령 준비 | ~1.5 us (SCSI→SAT 변환) | ~0.5 us (SQE 직접 작성) | SATA는 2회 프로토콜 변환 |
| 명령 발행 | ~1.0 us (MMIO: PxCI 쓰기) | ~0.3 us (Doorbell 쓰기) | 단일 MMIO 쓰기 |
| HBA/컨트롤러 처리 | ~2.0 us (Cmd Header/Table DMA) | ~0.5 us (SQE 직접 읽기) | SATA: 2회 DMA 필요 |
| PHY/버스 전송 | ~0.5 us (SATA FIS) | ~0.2 us (PCIe TLP) | SATA: FIS 오버헤드 |
| 완료 인터럽트 | ~1.0 us (MSI, 단일 벡터) | ~0.5 us (MSI-X, 코어별) | NVMe: 코어 로컬 인터럽트 |
| 총 오버헤드 | ~6 us | ~2 us | SATA는 3배 오버헤드 |
멀티코어 확장성
NVMe가 고성능 워크로드에서 압도적인 이유는 멀티코어 확장성에 있습니다. AHCI는 단일 커맨드 큐를 공유하므로 CPU 코어 간 lock contention이 발생합니다.
| CPU 코어 수 | SATA AHCI IOPS | NVMe IOPS | SATA 병목 |
|---|---|---|---|
| 1 | ~95K | ~200K | 대역폭 |
| 2 | ~95K | ~400K | 큐 Lock |
| 4 | ~95K | ~750K | 큐 Lock |
| 8 | ~90K (감소) | ~1.2M | 큐 Lock + IRQ 집중 |
| 16 | ~85K (감소) | ~1.8M | 큐 Lock + IRQ 집중 |
마이그레이션 가이드: SATA → NVMe
기존 SATA 기반 시스템을 NVMe로 마이그레이션할 때 확인해야 할 실무 체크리스트입니다.
| 항목 | SATA (이전) | NVMe (이후) | 필요 작업 |
|---|---|---|---|
| 디바이스 이름 | /dev/sda, /dev/sdb | /dev/nvme0n1 | fstab, 스크립트 수정 |
| 파티션 이름 | /dev/sda1 | /dev/nvme0n1p1 | grub.cfg, fstab UUID 권장 |
| 커널 드라이버 | ahci (libata) | nvme | initramfs에 nvme 포함 확인 |
| 부트로더 | AHCI/IDE 모드 | NVMe 부팅 지원 필수 | BIOS/UEFI에서 NVMe boot 활성 |
| TRIM | hdparm / fstrim | fstrim (동일) | fstab에 discard 마운트 옵션 |
| 전원 관리 | ALPM/DIPM | APST | nvme_core.default_ps_max_latency_us 조정 |
| 모니터링 | smartctl /dev/sda | smartctl /dev/nvme0 | smartmontools 업데이트 |
# dd 기반 디스크 복제 (SATA → NVMe)
# 주의: 대상 디스크의 모든 데이터가 삭제됩니다!
dd if=/dev/sda of=/dev/nvme0n1 bs=64M status=progress conv=sync,noerror
# 파티션 크기 조정 (NVMe가 더 큰 경우)
parted /dev/nvme0n1 resizepart 2 100%
resize2fs /dev/nvme0n1p2
# fstab을 UUID 기반으로 변경 (디바이스명 변경에 대비)
blkid /dev/nvme0n1p2
# UUID=xxxx-xxxx / ext4 defaults,discard 0 1
# initramfs 재생성 (nvme 드라이버 포함)
update-initramfs -u # Debian/Ubuntu
dracut --force # RHEL/Fedora
# GRUB 업데이트
update-grub
SATA 전원 관리
전원 관리 정책 (sysfs)
# 현재 정책 확인
cat /sys/class/scsi_host/host0/link_power_management_policy
# 정책 변경
echo "med_power_with_dipm" > /sys/class/scsi_host/host0/link_power_management_policy
| 정책 | ALPM | DIPM | 설명 |
|---|---|---|---|
max_performance | 비활성 | 비활성 | 항상 Active 상태 유지 |
medium_power | Partial 허용 | 비활성 | 호스트만 Partial 전환 가능 |
med_power_with_dipm | Partial 허용 | 활성 | 디바이스도 절전 전환 가능 (권장) |
min_power | Slumber 허용 | 활성 | 최대 절전, 레이턴시 증가 가능 |
/* drivers/ata/libata-core.c — ALPM 정책 설정 */
static ssize_t ata_scsi_lpm_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct Scsi_Host *shost = class_to_shost(dev);
struct ata_port *ap = ata_shost_to_port(shost);
/* PxSCTL의 IPM 필드를 설정하여 허용 상태 제어 */
/* PxCMD의 ALPE, ASP 비트로 자동 전환 활성화 */
ata_dev_set_feature(dev, SETFEATURES_SATA_ENABLE,
SATA_DIPM);
return count;
}
Runtime PM 통합
# Runtime PM 상태 확인
cat /sys/class/scsi_host/host0/device/power/runtime_status
# active / suspended / suspending / resuming
# Runtime PM 제어
cat /sys/class/scsi_host/host0/device/power/control
# auto / on
echo "auto" > /sys/class/scsi_host/host0/device/power/control
# Runtime PM 자동 대기 시간 (ms)
cat /sys/class/scsi_host/host0/device/power/autosuspend_delay_ms
# 기본: 15000 (15초)
# AHCI 포트의 Runtime PM
# ALPM 정책과 Runtime PM이 연동됨:
# - max_performance: Runtime PM 비활성
# - med_power_with_dipm: 15초 후 Partial 전환
# - min_power: 즉시 Slumber 전환
PHY 전원 상태 전환 타이밍 상세
| 전환 | 전환 시간 | 트리거 | 비고 |
|---|---|---|---|
| Active → Partial | ~2 us | ALPM 또는 DIPM | PHY 부분 비활성, PLL 유지 |
| Partial → Active | ~10 us | 새 명령 또는 COMWAKE | 빠른 복귀, 사용자 체감 없음 |
| Active → Slumber | ~2 us | ALPM min_power 정책 | PHY + PLL 비활성 |
| Slumber → Active | ~10 ms | 새 명령 또는 COMWAKE | PLL 재잠금 필요, 체감 지연 |
| Slumber → DevSlp | 가변 (~5 ms) | DEVSLP 핀 신호 | HW 지원 필수 |
| DevSlp → Active | ~20 ms | DEVSLP 핀 해제 + COMRESET | 가장 긴 복귀 시간 |
ALPM 구현 상세
ALPM(Aggressive Link Power Management)은 AHCI 포트 레지스터를 통해 제어됩니다.
/* drivers/ata/libata-core.c — ALPM 활성화 */
static int ata_dev_enable_pm(struct ata_device *dev,
enum link_pm policy)
{
struct ata_port *ap = dev->link->ap;
u32 scontrol, cmd;
/* PxSCTL.IPM: 허용할 전원 상태 설정
* 비트 8-11:
* 0x0 = 제한 없음 (모든 상태 허용)
* 0x1 = Partial 진입 금지
* 0x2 = Slumber 진입 금지
* 0x3 = Partial + Slumber 진입 금지 */
scontrol = readl(port_mmio + PORT_SCR_CTL);
scontrol &= ~(0xF << 8);
if (policy == MAX_PERFORMANCE)
scontrol |= (0x3 << 8); /* 절전 금지 */
writel(scontrol, port_mmio + PORT_SCR_CTL);
/* PxCMD.ALPE: Aggressive LPM Enable
* PxCMD.ASP: Aggressive Slumber/Partial
* ASP=0 → Partial 선호, ASP=1 → Slumber 선호 */
cmd = readl(port_mmio + PORT_CMD);
if (policy != MAX_PERFORMANCE) {
cmd |= PORT_CMD_ALPE; /* ALPM 활성화 */
if (policy == MIN_POWER)
cmd |= PORT_CMD_ASP; /* Slumber 선호 */
} else {
cmd &= ~PORT_CMD_ALPE;
}
writel(cmd, port_mmio + PORT_CMD);
/* DIPM: 디바이스 측에서 절전 전환 허용 */
if (policy == MED_POWER_WITH_DIPM || policy == MIN_POWER)
ata_dev_set_feature(dev, SETFEATURES_SATA_ENABLE, SATA_DIPM);
return 0;
}
SATA 전원 관리와 배터리 수명
노트북 환경에서 SATA 전원 관리 정책은 배터리 수명에 상당한 영향을 미칩니다.
| 전원 상태 | HDD 소비 전력 | SSD 소비 전력 | 적합한 상황 |
|---|---|---|---|
| Active (R/W) | ~6W (7200RPM) | ~3W | I/O 진행 중 |
| Active (Idle) | ~4W | ~1.5W | I/O 대기 |
| Partial | ~3W | ~1W | 짧은 유휴 (수 ms) |
| Slumber | ~1.5W | ~0.5W | 긴 유휴 (수 초 이상) |
| DevSlp | 지원 드묾 | ~5mW | 장기 유휴 (분 단위) |
SATA 핫 플러그
AHCI는 SATA 디바이스의 런타임 삽입/제거(핫 플러그)를 지원합니다. 핫 플러그 이벤트는 PHY 상태 변화로 감지되며, 포트 인터럽트를 통해 드라이버에 알려집니다.
핫 플러그 이벤트 감지
| 레지스터/비트 | 설명 |
|---|---|
| PxSERR.DIAG.X | Device Exchanged — PHY 상태가 변경됨 (디바이스 삽입/제거) |
| PxSERR.DIAG.N | PhyRdy Change — PHY가 Ready 상태로 전환됨 |
| PxIS.PCS | Port Connect Status Change — 핫 플러그 이벤트 인터럽트 |
| PxIS.PRCS | PhyRdy Change Status — PHY 상태 변화 인터럽트 |
| PxSSTS.DET | Device Detection — 0=없음, 1=감지중, 3=정상 연결, 4=오프라인 |
핫 플러그 처리 경로
/* 디바이스 삽입 시 */
ahci_port_intr()
→ PxIS.PCS 또는 PxIS.PRCS 감지
→ ata_port_schedule_eh(ap) /* EH 스케줄 */
→ ata_eh_link_autopsy() /* 상태 분석 */
→ ata_eh_recover() /* 포트 리셋 + 디바이스 식별 */
→ ata_eh_reset() /* COMRESET 수행 */
→ ata_eh_revalidate_and_attach()
→ ata_dev_read_id() /* IDENTIFY DEVICE */
→ ata_dev_configure() /* 디바이스 설정 */
→ scsi_add_device() /* SCSI 디바이스 등록 */
/* 디바이스 제거 시 */
ahci_port_intr()
→ PxSSTS.DET == 0 감지
→ ata_port_schedule_eh(ap)
→ ata_eh_link_autopsy()
→ ata_eh_recover()
→ ata_dev_disable() /* 디바이스 비활성화 */
→ scsi_remove_device() /* SCSI 디바이스 제거 */
핫 플러그 dmesg 출력 예
# 디바이스 삽입
[ 123.456789] ata3: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
[ 123.567890] ata3.00: ATA-10: Samsung SSD 870 EVO 500GB, SVT02B6Q, max UDMA/133
[ 123.567891] ata3.00: 976773168 sectors, multi 1: LBA48 NCQ (depth 32)
[ 123.678901] ata3.00: configured for UDMA/133
[ 123.678902] scsi 2:0:0:0: Direct-Access ATA Samsung SSD 870 02B6 PQ: 0 ANSI: 5
[ 123.789012] sd 2:0:0:0: [sdc] 976773168 512-byte logical blocks: (500 GB/466 GiB)
# 디바이스 제거
[ 456.789012] ata3: SATA link down (SStatus 0 SControl 300)
[ 456.789013] ata3.00: disabled
udev 규칙과 핫 플러그 자동화
# /etc/udev/rules.d/99-sata-hotplug.rules
# SATA 디바이스 삽입 시 자동 마운트
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z]1", \
ENV{ID_BUS}=="ata", \
RUN+="/usr/bin/systemd-mount --no-block /dev/%k /mnt/hotplug-%k"
# SATA 디바이스 제거 시 자동 언마운트
ACTION=="remove", SUBSYSTEM=="block", KERNEL=="sd[a-z]1", \
ENV{ID_BUS}=="ata", \
RUN+="/usr/bin/systemd-umount /mnt/hotplug-%k"
# udev 규칙 리로드
udevadm control --reload-rules
udevadm trigger
소프트웨어적 핫 플러그 (안전 제거)
# 방법 1: SCSI 디바이스 제거
echo 1 > /sys/block/sdc/device/delete
# 방법 2: SCSI 호스트 스캔으로 재감지
echo "- - -" > /sys/class/scsi_host/host2/scan
# 방법 3: ATA 포트 리셋으로 재감지
echo 1 > /sys/class/ata_port/ata3/device/rescan
# 안전 제거 순서
# 1. 마운트 해제
umount /mnt/data
# 2. 디바이스 삭제
echo 1 > /sys/block/sdc/device/delete
# 3. 물리적 분리
echo 1 > /sys/block/sdX/device/delete로 먼저 소프트웨어적으로 제거한 후
물리적으로 분리하는 안전한 방법입니다.
Surprise removal은 예고 없이 물리적으로 분리하는 것으로, 미완료 I/O가 있으면 데이터 손실이 발생할 수 있습니다.
AHCI 핫 플러그 레지스터 흐름
디바이스 삽입 시 AHCI 하드웨어 레벨에서 일어나는 레지스터 변화 순서입니다.
/* 1. PHY 상태 변화 감지 */
PxSERR.DIAG.X = 1 /* Device Exchanged 비트 설정 */
PxSERR.DIAG.N = 1 /* PhyRdy Change 비트 설정 */
/* 2. 포트 인터럽트 상태에 반영 */
PxIS.PCS = 1 /* Port Connect Status Change */
PxIS.PRCS = 1 /* PhyRdy Change Status */
/* 3. 글로벌 인터럽트 상태 업데이트 */
GHC.IS |= (1 << port) /* 해당 포트 비트 설정 */
/* 4. MSI/INTx 인터럽트 발생 → 드라이버 핸들러 진입 */
ahci_single_level_irq_intr()
→ ahci_port_intr()
→ irq_stat = readl(port_mmio + PORT_IRQ_STAT)
→ /* PCS/PRCS 비트 확인 */
→ writel(PxSERR, port_mmio + PORT_SCR_ERR) /* SError 클리어 */
→ writel(irq_stat, port_mmio + PORT_IRQ_STAT)
→ ata_port_schedule_eh(ap) /* EH 스케줄링 */
핫 플러그 안정성 개선
핫 플러그 이벤트와 systemd
systemd 환경에서 SATA 핫 플러그 디바이스의 자동 마운트를 설정하는 방법입니다.
# /etc/systemd/system/mnt-hotplug.mount
[Unit]
Description=SATA Hot-plug Auto Mount
After=blockdev@dev-disk-by\x2dlabel-HOTDATA.target
[Mount]
What=/dev/disk/by-label/HOTDATA
Where=/mnt/hotplug
Type=ext4
Options=defaults,noatime,nofail
[Install]
WantedBy=multi-user.target
# systemd-udevd가 처리하는 핫 플러그 이벤트 모니터링
udevadm monitor --subsystem-match=block --property
# 출력 예시 (디바이스 삽입):
# UDEV [1234.567] add /devices/.../block/sdc (block)
# ID_BUS=ata
# ID_ATA_SATA=1
# ID_MODEL=Samsung_SSD_870_EVO
# 커널 notification chain (ATA 디바이스 이벤트)
# drivers/ata/libata-scsi.c:
# scsi_add_device() → kobject_uevent(KOBJ_ADD)
# scsi_remove_device() → kobject_uevent(KOBJ_REMOVE)
Port Multiplier (FBS)
SATA Port Multiplier(PMP)는 하나의 SATA 호스트 포트에 여러 디바이스를 팬아웃(fan-out) 방식으로 연결하는 장치입니다. 최대 15개의 디바이스 포트를 제공하며, 1개의 제어 포트(포트 15)를 가집니다.
PMP 토폴로지
Host Port 0 ─── PMP ─┬── PMP Port 0 → SATA Device 0
├── PMP Port 1 → SATA Device 1
├── PMP Port 2 → SATA Device 2
│ ...
├── PMP Port 14 → SATA Device 14
└── PMP Port 15 (Control Port)
FIS-Based Switching (FBS)
FBS가 없으면 PMP는 Command-Based Switching을 사용하며, 한 번에 하나의 디바이스에만 명령을 보낼 수 있습니다. FBS를 사용하면 여러 디바이스에 동시에 명령을 발행할 수 있어 성능이 크게 향상됩니다.
| 항목 | Command-Based Switching | FIS-Based Switching |
|---|---|---|
| 동시 명령 | 1개 디바이스 | 여러 디바이스 |
| NCQ 지원 | 1개 디바이스만 | 모든 디바이스 |
| HBA 요구 | 기본 | CAP.FBSS 비트 필요 |
| PMP 요구 | 기본 | FBS 지원 PMP 필요 |
| 성능 | 낮음 | 높음 |
GSCR (General Status and Control Registers)
/* PMP GSCR 레지스터 */
enum {
SATA_PMP_GSCR_DEVO = 0, /* Device/Vendor OUI */
SATA_PMP_GSCR_REV = 1, /* Revision */
SATA_PMP_GSCR_PORT_INFO = 2, /* Port Info (포트 수) */
SATA_PMP_GSCR_ERROR = 32, /* Error Information */
SATA_PMP_GSCR_ERROR_EN = 33, /* Error Enable */
SATA_PMP_GSCR_FEAT = 64, /* Features */
SATA_PMP_GSCR_FEAT_EN = 96, /* Feature Enable */
};
/* libata에서 PMP 구조: 포트당 ata_link를 추가 생성 */
/* ap->pmp_link[0..14] 배열로 각 PMP 포트 표현 */
PMP 리눅스 커널 구현
/* drivers/ata/libata-pmp.c — PMP 프로브 경로 */
static int sata_pmp_attach(struct ata_device *dev)
{
struct ata_port *ap = dev->link->ap;
int nr_ports;
/* 1. PMP GSCR 읽기 → 포트 수 확인 */
sata_pmp_read(dev->link, SATA_PMP_GSCR_PORT_INFO, &val);
nr_ports = val & 0xf;
/* 2. PMP 링크 배열 할당 */
ap->pmp_link = kcalloc(SATA_PMP_MAX_PORTS,
sizeof(struct ata_link), GFP_KERNEL);
/* 3. 각 PMP 포트에 대해 ata_link 초기화 */
for (i = 0; i < nr_ports; i++) {
struct ata_link *link = &ap->pmp_link[i];
ata_link_init(ap, link, i);
}
/* 4. FBS(FIS-Based Switching) 활성화 시도 */
if (ap->flags & ATA_FLAG_FPDMA_AUX)
sata_pmp_enable_fbs(ap);
return 0;
}
/* PMP GSCR 레지스터 읽기/쓰기는 Register H2D FIS의
* PMPORT 필드를 15(제어 포트)로 설정하여 수행 */
PMP 커맨드 라우팅
PMP 환경에서 호스트가 특정 디바이스에 명령을 보내려면 FIS의 PMPORT 필드(4비트)에 대상 포트 번호를 설정합니다.
/* Register H2D FIS에서 PMPORT 필드 설정 */
/* Byte 1의 비트 [3:0] = PM Port */
struct ata_taskfile tf;
tf.flags |= ATA_TFLAG_FUA;
/* libata에서 PMPORT는 ata_link->pmp에 저장 */
struct ata_link *link = qc->dev->link;
u8 pmport = link->pmp; /* 0~14: 디바이스, 15: 제어 포트 */
/* ahci_fill_cmd_slot()에서 Command Header에 PMPORT 기록 */
opts = cmd_fis_len | (qc->n_elem << 16);
opts |= (pmport << 12); /* PMP 필드 (비트 15:12) */
PMP 에러 처리
PMP 환경에서의 에러 처리는 개별 링크 단위로 수행되며, 일반 직접 연결보다 복잡합니다.
/* drivers/ata/libata-pmp.c — PMP 에러 복구 */
static int sata_pmp_eh_recover(struct ata_port *ap)
{
struct ata_link *link;
int rc;
/* 1. 모든 PMP 링크의 에러 상태 수집 */
ata_for_each_link(link, ap, EDGE) {
sata_pmp_read(link, SATA_PMP_GSCR_ERROR, &err);
/* 각 PMP 포트별 SError 읽기 */
sata_scr_read(link, SCR_ERROR, &serror);
}
/* 2. 개별 링크 리셋 시도 (호스트 포트 리셋 불필요) */
ata_for_each_link(link, ap, EDGE) {
if (link->eh_info.action) {
rc = ata_eh_reset(link, 0, softreset, hardreset);
/* PMP 포트별 COMRESET: GSCR에 쓰기 */
}
}
/* 3. 전체 실패 시 호스트 포트 리셋 (PMP 포함 전체 재초기화) */
if (rc) {
ata_eh_reset(ap->link, 0, softreset, hardreset);
sata_pmp_attach(ap->link->device);
}
return rc;
}
PMP 실제 사용 시나리오
| 시나리오 | 구성 | PMP 역할 | 주의사항 |
|---|---|---|---|
| 5-bay NAS | AHCI 1포트 + PMP 5포트 | 단일 SATA 포트에서 5개 HDD 연결 | FBS 없으면 성능 저하, RAID 시 병목 |
| PCIe SATA 카드 | PCIe x1 + ASMedia ASM1166 | 내장 PMP로 6포트 제공 | 실제로는 PMP 기반, 대역폭 공유 |
| eSATA 도킹 | 노트북 eSATA + 외장 PMP | 다중 외장 디스크 연결 | 드라이버 호환성 확인 필수 |
| 서버 백플레인 | SAS HBA + SATA PMP | SAS Expander와 유사한 역할 | SAS Expander 사용이 더 안정적 |
SATA 에러 처리와 복구
SError 레지스터 비트 상세
| 카테고리 | 비트 | 필드 | 설명 |
|---|---|---|---|
| ERR | 0 | I | Recovered Data Integrity Error |
| 1 | M | Recovered Communications Error | |
| 8 | T | Transient Data Integrity Error | |
| 9 | C | Persistent Comm/Data Integrity Error | |
| 10 | P | Protocol Error | |
| 11 | E | Internal Error | |
| DIAG | 16 | N | PhyRdy Change |
| 17 | I | PHY Internal Error | |
| 18 | W | Comm Wake | |
| 19 | B | 10B-to-8B Decode Error | |
| 20 | D | Disparity Error | |
| 21 | H | Handshake Error | |
| 23 | T | Transport state transition error | |
| 26 | X | Device Exchanged |
에러 복구 시나리오 예시
# 실제 에러 복구 dmesg 출력 예:
# 1. 에러 감지 → 포트 동결
ata1.00: exception Emask 0x10 SAct 0x7 SErr 0x400000 action 0x6 frozen
ata1.00: irq_stat 0x08000000, interface fatal error
# 2. SError 분석
ata1: SError: { Handshk }
# → DIAG.H: 핸드셰이크 에러 (PHY 레벨 통신 문제)
# 3. 소프트 리셋 시도
ata1: hard resetting link
# → softreset 생략, 바로 hardreset (COMRESET)
# 4. 링크 재성립
ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
# 5. 디바이스 재식별
ata1.00: configured for UDMA/133
# 6. 실패한 NCQ 명령 재시도
ata1.00: EH complete
에러 분류 (libata)
/* include/linux/libata.h — 에러 마스크 */
enum {
AC_ERR_DEV = (1 << 0), /* 디바이스 에러 (Status.ERR) */
AC_ERR_HSM = (1 << 1), /* Host State Machine 위반 */
AC_ERR_TIMEOUT = (1 << 2), /* 커맨드 타임아웃 */
AC_ERR_MEDIA = (1 << 3), /* 미디어 에러 (배드 섹터) */
AC_ERR_ATA_BUS = (1 << 4), /* ATA 버스 에러 (CRC, 인코딩) */
AC_ERR_HOST_BUS = (1 << 5), /* 호스트 버스 에러 (PCI) */
AC_ERR_SYSTEM = (1 << 6), /* 시스템 에러 */
AC_ERR_INVALID = (1 << 7), /* 잘못된 인자 */
AC_ERR_OTHER = (1 << 8), /* 기타 */
AC_ERR_NODEV_HINT = (1 << 9), /* 디바이스 없음 힌트 */
AC_ERR_NCQ = (1 << 10), /* NCQ 에러 */
};
/* 복구 동작 */
enum {
ATA_EH_RESET = (1 << 0), /* 리셋 (soft → hard) */
ATA_EH_REVALIDATE = (1 << 1), /* 디바이스 재식별 */
ATA_EH_SET_MODE = (1 << 4), /* 전송 모드 재설정 */
ATA_EH_LPM = (1 << 5), /* 전원 관리 재설정 */
};
NCQ 에러 복구 상세
NCQ 명령이 실패하면 다음 단계로 복구가 진행됩니다. 실패한 태그만 재시도하고 나머지는 계속 진행할 수 있습니다.
/* NCQ 에러 복구 흐름 */
/* 1. SDB FIS에서 에러 감지 (I 비트 = 1) */
ahci_port_intr()
→ status & PORT_IRQ_SDB_FIS
→ D2H FIS의 Status.ERR 또는 SDB FIS의 Error 비트 확인
/* 2. 포트 동결 — 추가 명령 발행 차단 */
ata_port_freeze(ap);
/* PxIE = 0, PxCMD.ST 클리어, 진행중 QC 모두 freeze */
/* 3. READ LOG EXT (Log Page 10h) — NCQ 에러 로그 읽기 */
ata_eh_read_log_10h(dev, &tag, &tf);
/* PIO 명령: 실패한 태그, Status, Error 레지스터 획득 */
/* 4. 실패한 태그 식별 및 에러 분류 */
qc = __ata_qc_from_tag(ap, tag);
qc->err_mask |= AC_ERR_DEV;
/* 나머지 활성 QC는 AC_ERR_TIMEOUT으로 마킹 */
/* 5. 포트 리셋 (COMRESET) + 디바이스 재초기화 */
ata_eh_reset(link);
/* 6. 실패한 명령 재시도, 나머지 QC 재발행 */
ata_eh_finish(ap);
/* 재시도 카운터 감소, 한계 도달 시 SCSI에 에러 반환 */
libata EH 상태 머신
libata의 에러 핸들러는 명확한 상태 머신으로 동작합니다.
| 상태 | 함수 | 동작 | 다음 상태 |
|---|---|---|---|
| IDLE | - | 정상 동작, EH 비활성 | SCHEDULE (에러 발생 시) |
| SCHEDULE | ata_port_schedule_eh() | EH 워크큐에 작업 등록 | FREEZE |
| FREEZE | ata_port_freeze() | 포트 인터럽트 비활성, 명령 중단 | AUTOPSY |
| AUTOPSY | ata_eh_autopsy() | 에러 원인 분류, 복구 전략 결정 | REPORT |
| REPORT | ata_eh_report() | dmesg에 에러 로그 출력 | RECOVER |
| RECOVER | ata_eh_recover() | softreset/hardreset, 디바이스 재설정 | FINISH 또는 AUTOPSY (실패 시) |
| FINISH | ata_eh_finish() | 실패 QC 완료/재시도, 포트 해동 | IDLE |
SATA 에러 통계 수집
# sysfs를 통한 ATA 포트 에러 통계
cat /sys/class/ata_port/ata1/port_no
cat /sys/class/ata_port/ata1/idle_irq
# SATA PHY 이벤트 카운터 (SMART Log 11h)
smartctl -l sataphy /dev/sda
# ID Description Value
# 0x0001 Command failed 0
# 0x0003 R_ERR response for device-to-host data FIS 2
# 0x0009 Transition from drive PhyRdy to drive PhyNRdy 1
# libata 에러 tracepoint 활성화
echo 1 > /sys/kernel/debug/tracing/events/libata/ata_eh_link_autopsy/enable
echo 1 > /sys/kernel/debug/tracing/events/libata/ata_eh_about_to_do/enable
cat /sys/kernel/debug/tracing/trace_pipe
# dmesg에서 ATA 에러 패턴 추출
dmesg | grep -E "ata[0-9]+\.(00|[0-9]+): (exception|error|hard resetting|COMRESET)"
케이블/커넥터 문제 진단 패턴
| SError 비트 패턴 | dmesg 표시 | 가능한 원인 | 조치 |
|---|---|---|---|
| DIAG.B (10B-to-8B) | { 10B8B } | 케이블 불량, 신호 감쇠 | SATA 케이블 교체 |
| DIAG.D (Disparity) | { Dispar } | EMI 간섭, 케이블 너무 긴 경우 | 케이블 경로 변경/단축 |
| DIAG.H (Handshake) | { Handshk } | 커넥터 접촉 불량 | 커넥터 재삽입, 단자 청소 |
| ERR.C (Persistent) | { CommWake } | 전원 공급 불안정 | PSU 점검, 전원 케이블 교체 |
| ERR.T (Transient) | { TrDataI } | 간헐적 신호 문제 | 케이블 교체 + 재시도 관찰 |
| ERR.P (Protocol) | { Proto } | FIS 프로토콜 위반 | 펌웨어 업데이트, 속도 다운그레이드 |
| DIAG.N (PhyRdy) | { PHYRdyChg } | 핫 플러그 또는 링크 불안정 | 정상(핫플러그) 또는 케이블 점검 |
| 복합 (B+D+H) | { 10B8B Dispar Handshk } | 심각한 케이블/커넥터 손상 | 케이블+커넥터 모두 교체 |
ATA 보안 기능
ATA Security Feature Set은 디스크에 비밀번호를 설정하여 무단 접근을 방지하는 기능입니다. BIOS/UEFI 레벨에서 디스크를 잠그면, 올바른 비밀번호 없이는 디스크를 읽을 수 없습니다.
ATA Security 명령어
| 명령 | ATA 코드 | 설명 |
|---|---|---|
| SECURITY SET PASSWORD | 0xF1 | User/Master 비밀번호 설정 |
| SECURITY UNLOCK | 0xF2 | 비밀번호로 디스크 잠금 해제 |
| SECURITY ERASE PREPARE | 0xF3 | 보안 삭제 준비 |
| SECURITY ERASE UNIT | 0xF4 | 전체 디스크 보안 삭제 |
| SECURITY FREEZE LOCK | 0xF5 | 보안 설정 동결 (재부팅까지) |
| SECURITY DISABLE PASSWORD | 0xF6 | 비밀번호 비활성화 |
# ATA 보안 상태 확인
hdparm -I /dev/sda | grep -A10 "Security:"
# 비밀번호 설정 (주의: 비밀번호 분실 시 디스크 접근 불가)
hdparm --security-set-passwd "mypassword" /dev/sda
# 보안 삭제 (Normal Erase)
hdparm --security-erase "mypassword" /dev/sda
# Enhanced Secure Erase (SSD 권장: TRIM 기반 전체 삭제)
hdparm --security-erase-enhanced "mypassword" /dev/sda
# 보안 동결 (BIOS가 보통 부팅 시 수행)
hdparm --security-freeze /dev/sda
TCG Opal (자체 암호화 드라이브, SED)
TCG Opal은 하드웨어 기반 전체 디스크 암호화(FDE)를 제공합니다. 디스크 컨트롤러가 모든 데이터를 실시간으로 암호화/복호화하며, AES-256 키는 디스크 내부에 저장됩니다.
# sedutil-cli로 Opal SED 관리
sedutil-cli --scan # Opal 지원 디스크 검색
sedutil-cli --query /dev/sda # Opal 상태 확인
sedutil-cli --initialSetup "password" /dev/sda # 초기 설정
# 리눅스 커널 OPAL 지원 (block/sed-opal.c)
# ioctl(fd, IOC_OPAL_SAVE, &opal_lock_unlock) 등
ATA Sanitize
| Sanitize 모드 | 설명 | 속도 |
|---|---|---|
| BLOCK ERASE | 블록 단위 삭제 (SSD에 최적) | 빠름 (수 초) |
| CRYPTO SCRAMBLE | 암호화 키 변경으로 데이터 무효화 | 매우 빠름 |
| OVERWRITE | 지정 패턴으로 전체 덮어쓰기 | 느림 (HDD 크기 비례) |
ATA Sanitize 명령 사용
# Sanitize 지원 확인
hdparm -I /dev/sda | grep -i sanitize
# * BLOCK ERASE
# * CRYPTO SCRAMBLE EXT
# * OVERWRITE EXT
# hdparm으로 Sanitize 실행 (주의: 데이터 완전 삭제!)
# Block Erase (SSD 권장)
hdparm --sanitize-block-erase /dev/sda
# Crypto Scramble (자체 암호화 드라이브)
hdparm --sanitize-crypto-scramble /dev/sda
# Sanitize 상태 확인
hdparm --sanitize-status /dev/sda
TCG Opal 잠금/해제 흐름
TCG Opal SED(Self-Encrypting Drive)의 잠금/해제 흐름은 다음과 같습니다.
/* TCG Opal 초기 설정 및 잠금 흐름 */
/* 1. Initial Setup — Locking SP 활성화 */
sedutil-cli --initialSetup "password" /dev/sda
/* 내부적으로:
* - Admin SP에 SID(Security ID) 비밀번호 설정
* - Locking SP 활성화
* - MBR Shadow 테이블 활성화 (PBA용) */
/* 2. Locking Range 설정 */
sedutil-cli --setLockingRange 0 RW "password" /dev/sda
/* Range 0 = Global Range (전체 디스크)
* RW = ReadWrite 잠금 활성화 */
/* 3. 잠금 */
sedutil-cli --setLockingRange 0 LK "password" /dev/sda
/* LK = Locked, 모든 읽기/쓰기 차단 */
/* 4. 잠금 해제 (부팅 시 PBA에서) */
sedutil-cli --setLockingRange 0 RW "password" /dev/sda
/* 올바른 비밀번호로 Range 잠금 해제 */
/* 5. MBR Shadow — Pre-Boot Authentication */
/* 잠금 상태에서 MBR 읽기 시 PBA 이미지 반환
* → PBA가 비밀번호 입력 받아 잠금 해제
* → 실제 MBR로 전환되어 OS 부팅 */
커널의 OPAL 지원 (sed-opal.c)
리눅스 커널은 block/sed-opal.c에서 TCG Opal 명령을 ioctl 인터페이스로 노출합니다.
/* block/sed-opal.c — 사용자 공간 OPAL ioctl */
switch (cmd) {
case IOC_OPAL_SAVE:
/* Locking Range 잠금 상태 저장 */
return opal_save(bdev, p);
case IOC_OPAL_LOCK_UNLOCK:
/* Locking Range 잠금/해제 */
return opal_lock_unlock(bdev, p);
case IOC_OPAL_ACTIVATE_LSP:
/* Locking SP 활성화 */
return opal_activate_lsp(bdev, p);
case IOC_OPAL_SET_PW:
/* 비밀번호 설정/변경 */
return opal_set_new_pw(bdev, p);
case IOC_OPAL_PSID_REVERT_TPR:
/* PSID를 사용한 공장 초기화 (데이터 삭제) */
return opal_reverttper(bdev, p, 1);
case IOC_OPAL_MBR_DONE:
/* MBR Shadow → 실제 MBR 전환 (PBA 완료) */
return opal_mbr_done(bdev, p);
}
/* 사용 예시 (C 코드):
* int fd = open("/dev/sda", O_RDWR);
* struct opal_lock_unlock lk = {
* .session.opal_key.lr = 0,
* .session.opal_key.key = "password",
* .session.opal_key.key_len = 8,
* .l_state = OPAL_RW,
* };
* ioctl(fd, IOC_OPAL_LOCK_UNLOCK, &lk);
*/
ATA Security Frozen 상태 우회
대부분의 BIOS/UEFI는 부팅 과정에서 ATA SECURITY FREEZE LOCK(0xF5) 명령을 보내 보안 설정 변경을 방지합니다. Frozen 상태에서는 비밀번호 설정, 삭제 등이 차단됩니다.
# Frozen 상태 확인
hdparm -I /dev/sda | grep frozen
# frozen ← BIOS가 동결한 상태
# not frozen ← 보안 명령 사용 가능
# Frozen 해제 방법: Sleep/Resume 트릭
# 시스템을 S3(Suspend to RAM)에 넣었다 깨우면
# BIOS의 freeze 명령이 재실행되지 않아 not frozen 상태가 됨
# 1. 시스템 Suspend
systemctl suspend
# 2. 깨운 직후 즉시 확인
hdparm -I /dev/sda | grep frozen
# not frozen ← 이제 보안 명령 사용 가능
# 3. 이제 Secure Erase 등 실행 가능
hdparm --security-set-passwd "p" /dev/sda
hdparm --security-erase "p" /dev/sda
데이터 보안 삭제 모범 사례
| 방법 | 대상 | 속도 | 보안 수준 | 복구 가능성 |
|---|---|---|---|---|
| ATA Secure Erase | HDD/SSD | HDD: 수 시간, SSD: 수 분 | 중간 | SSD에서 일부 잔존 가능 |
| ATA Enhanced Secure Erase | HDD/SSD | SSD: 수 초~분 | 높음 | 매핑 테이블 포함 삭제 |
| Sanitize BLOCK ERASE | SSD | 수 초 | 매우 높음 | 모든 NAND 블록 삭제 |
| Sanitize CRYPTO SCRAMBLE | SED SSD | 즉시 | 매우 높음 | 암호화 키 파기 = 데이터 무효화 |
| Sanitize OVERWRITE | HDD | 수 시간 | 매우 높음 | 물리적 패턴 덮어쓰기 |
| 물리적 파괴 | 모든 매체 | - | 최고 | 물리적 복구 불가 |
sysfs 인터페이스
리눅스 커널은 SATA/ATA 디바이스 정보를 sysfs를 통해 사용자 공간에 노출합니다.
sysfs 디렉터리 구조
# ATA 호스트/포트/링크/디바이스 계층
/sys/class/ata_port/ata1/ # ATA 포트
/sys/class/ata_port/ata1/nr_pmp_links # PMP 링크 수
/sys/class/ata_port/ata1/idle_irq # 유휴 인터럽트 카운트
/sys/class/ata_link/link1/ # ATA 링크
/sys/class/ata_link/link1/sata_spd # 현재 SATA 속도
/sys/class/ata_link/link1/hw_sata_spd_limit # 하드웨어 최대 속도
/sys/class/ata_device/dev1.0/ # ATA 디바이스
/sys/class/ata_device/dev1.0/class # 디바이스 클래스
/sys/class/ata_device/dev1.0/id # IDENTIFY 데이터 (512B)
/sys/class/ata_device/dev1.0/pio_mode # PIO 모드
/sys/class/ata_device/dev1.0/xfer_mode # 현재 전송 모드
# SCSI 호스트 레벨
/sys/class/scsi_host/host0/link_power_management_policy # ALPM 정책
/sys/class/scsi_host/host0/ahci_host_caps # AHCI CAP 레지스터
# 블록 디바이스 심볼릭 링크
/sys/block/sda/device/ # → SCSI 디바이스
/sys/block/sda/device/vendor # "ATA "
/sys/block/sda/device/model # 모델명
/sys/block/sda/device/rev # 펌웨어 버전
/sys/block/sda/device/queue_depth # 큐 깊이 (NCQ=32)
주요 sysfs 속성 활용
# SATA 링크 속도 확인
cat /sys/class/ata_link/link1/sata_spd
# 출력: 6.0 Gbps
# 큐 깊이 확인 (NCQ 활성화 여부)
cat /sys/block/sda/device/queue_depth
# 출력: 32 (NCQ 활성), 1 (NCQ 비활성)
# IDENTIFY 데이터 원본 읽기 (바이너리)
xxd /sys/class/ata_device/dev1.0/id | head -5
# 전송 모드 확인
cat /sys/class/ata_device/dev1.0/xfer_mode
# 출력: UDMA/133
# ALPM 정책 설정
for host in /sys/class/scsi_host/host*/; do
echo "med_power_with_dipm" > "${host}link_power_management_policy"
done
sysfs ATA 속성 전체 목록
libata는 세 가지 sysfs 클래스를 통해 ATA 서브시스템의 상태를 노출합니다.
| 속성명 | 권한 | 형식 | 설명 |
|---|---|---|---|
nr_pmp_links | R | 정수 | PMP에 연결된 링크 수 |
idle_irq | R | 정수 | 유휴 인터럽트 카운트 |
port_no | R | 정수 | 물리적 포트 번호 |
| 속성명 | 권한 | 형식 | 설명 |
|---|---|---|---|
sata_spd | R | 문자열 | 현재 협상된 SATA 속도 |
hw_sata_spd_limit | R | 문자열 | 하드웨어 최대 SATA 속도 |
sata_spd_limit | R | 문자열 | 소프트웨어 제한 SATA 속도 |
| 속성명 | 권한 | 형식 | 설명 |
|---|---|---|---|
class | R | 문자열 | 디바이스 클래스 (ata, atapi, pmp) |
id | R | 바이너리 | IDENTIFY DEVICE 데이터 (512B) |
gscr | R | 바이너리 | PMP GSCR 레지스터 |
pio_mode | R | 문자열 | PIO 전송 모드 |
dma_mode | R | 문자열 | DMA 전송 모드 |
xfer_mode | R | 문자열 | 현재 활성 전송 모드 |
ering | R | 텍스트 | 에러 링 버퍼 |
spdn_cnt | R | 정수 | 속도 다운그레이드 횟수 |
trim | R | 문자열 | TRIM 지원 상태 |
sysfs를 통한 디바이스 정보 스크립팅
#!/bin/bash — sata_report.sh
echo "===== SATA 디바이스 보고서 ($(date)) ====="
for port in /sys/class/ata_port/ata*; do
pn=$(basename "$port"); pnum=${pn#ata}
lp="/sys/class/ata_link/link${pnum}"
[ -d "$lp" ] || continue
spd=$(cat "${lp}/sata_spd" 2>/dev/null)
[ "$spd" = "<unknown>" ] && continue
echo "--- ${pn} (${spd}) ---"
dp="/sys/class/ata_device/dev${pnum}.0"
[ -d "$dp" ] && echo " 전송 모드: $(cat ${dp}/xfer_mode 2>/dev/null)"
for sd in /sys/block/sd*; do
sdn=$(basename "$sd")
hl=$(readlink -f "${sd}/device")
if echo "$hl" | grep -q "ata${pnum}"; then
echo " /dev/${sdn}: $(cat ${sd}/device/model 2>/dev/null | xargs)"
echo " 펌웨어: $(cat ${sd}/device/rev 2>/dev/null | xargs)"
echo " 큐 깊이: $(cat ${sd}/device/queue_depth 2>/dev/null)"
hn=$(echo "$hl" | grep -oP 'host\K[0-9]+')
echo " ALPM: $(cat /sys/class/scsi_host/host${hn}/link_power_management_policy 2>/dev/null)"
if command -v smartctl &>/dev/null; then
echo " SMART: $(smartctl -H /dev/${sdn} 2>/dev/null | grep result)"
fi
dm=$(cat "${sd}/queue/discard_max_bytes" 2>/dev/null)
[ "$dm" != "0" ] && echo " TRIM: 지원" || echo " TRIM: 미지원"
fi
done
done
커널의 sysfs 속성 등록
/* drivers/ata/libata-transport.c */
/* 전송 모드 속성 show 콜백 */
static ssize_t ata_tdev_xfer_mode_show(
struct device *dev,
struct device_attribute *attr, char *buf)
{
struct ata_device *ata_dev = transport_class_to_dev(dev);
return sysfs_emit(buf, "%s\n",
ata_mode_string(ata_dev->xfer_mode));
}
/* ALPM 정책 store 콜백 */
static ssize_t link_pm_policy_store(
struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct Scsi_Host *shost = class_to_shost(dev);
struct ata_port *ap = ata_shost_to_port(shost);
enum ata_lpm_policy policy = ata_parse_lpm_policy(buf);
if (policy < 0) return -EINVAL;
return ata_set_lpm(ap, policy, ATA_LPM_UNKNOWN);
}
DEVICE_ATTR_RW(link_power_management_policy);
procfs와의 차이점
| 항목 | /proc/scsi/scsi | sysfs (/sys/class/ata_*) |
|---|---|---|
| 구조 | 단일 텍스트 파일 | 디렉터리/파일 계층 |
| SATA 속도 | 미제공 | sata_spd, hw_sata_spd_limit |
| 전송 모드 | 미제공 | xfer_mode, pio_mode |
| IDENTIFY | 미제공 | id (512B 바이너리) |
| 에러 이력 | 미제공 | ering |
| 쓰기 속성 | 제한적 | ALPM, 큐 깊이 등 |
| 지원 상태 | 레거시 | 표준 (장기 지원) |
S.M.A.R.T. 모니터링
S.M.A.R.T.(Self-Monitoring, Analysis and Reporting Technology)는 저장 장치가
자체적으로 건강 상태를 모니터링하고 보고하는 기술입니다.
리눅스에서는 smartmontools 패키지의 smartctl과 smartd를 사용합니다.
# SMART 전체 정보 조회
smartctl -a /dev/sda
# SMART 상태만 빠르게 확인
smartctl -H /dev/sda
# SMART overall-health self-assessment test result: PASSED
# 자체 테스트 실행
smartctl -t short /dev/sda # 짧은 테스트 (~2분)
smartctl -t long /dev/sda # 긴 테스트 (~수 시간)
smartctl -t conveyance /dev/sda # 운송 테스트
# 테스트 결과 확인
smartctl -l selftest /dev/sda
# 에러 로그 확인
smartctl -l error /dev/sda
핵심 SMART 속성
| ID | 속성명 | 설명 | 위험 기준 |
|---|---|---|---|
| 5 | Reallocated Sectors Count | 재할당된 불량 섹터 수 | 1 이상이면 주의 |
| 9 | Power-On Hours | 누적 전원 켜진 시간 | HDD: ~30000h, SSD: ~50000h |
| 12 | Power Cycle Count | 전원 ON/OFF 횟수 | 참고용 |
| 187 | Reported Uncorrectable Errors | 교정 불가 에러 수 | 증가하면 위험 |
| 188 | Command Timeout | 타임아웃된 명령 수 | 증가하면 케이블/전원 확인 |
| 197 | Current Pending Sector Count | 읽기 실패, 재할당 대기 섹터 | 1 이상이면 주의 |
| 198 | Offline Uncorrectable | 오프라인 검사 중 발견된 불량 섹터 | 1 이상이면 주의 |
| 199 | UDMA CRC Error Count | CRC 에러 (케이블/커넥터 문제) | 증가하면 케이블 교체 |
| 241 | Total LBAs Written | 총 쓰기량 (SSD 수명 지표) | TBW 한계 확인 |
smartd 데몬 설정
# /etc/smartd.conf 예시
# 모든 SATA 디스크 모니터링, 이메일 알림
/dev/sda -a -o on -S on -s (S/../.././02|L/../../6/03) \
-m admin@example.com -M exec /usr/share/smartmontools/smartd_warning.sh
# -a: 모든 SMART 속성 모니터링
# -o on: 오프라인 테스트 자동 활성화
# -S on: 속성 자동 저장 활성화
# -s: 자체 테스트 스케줄 (Short: 매일 02시, Long: 토요일 03시)
libata SMART 패스스루
/* SMART 명령은 SCSI-ATA Translation(SAT)을 통해 전달됨 */
/* SCSI 명령: ATA_16 (0x85) 또는 ATA_12 (0xA1) CDB */
/* smartctl은 SG_IO ioctl로 ATA passthrough CDB를 전송 */
/* libata-scsi.c에서 ATA passthrough 처리: */
ata_scsi_simulate()
→ ata_scsi_pass_thru() /* CDB에서 ATA taskfile 추출 */
→ ata_qc_issue() /* ATA 명령 직접 발행 */
SMART 속성 임계값과 경고 해석
# SMART 속성 상세 출력 (원시값 포함)
smartctl -A /dev/sda
# 출력 예:
# ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
# 5 Reallocated_Sector_Ct 0x0033 100 100 010 Pre-fail Always - 0
# 9 Power_On_Hours 0x0032 094 094 000 Old_age Always - 28456
# 197 Current_Pending_Sector 0x0012 100 100 000 Old_age Always - 0
# 199 UDMA_CRC_Error_Count 0x003e 200 200 000 Old_age Always - 3
# VALUE: 정규화된 현재 값 (높을수록 좋음)
# WORST: 기록된 최저 VALUE
# THRESH: 임계값 (VALUE가 이 아래면 FAIL)
# TYPE: Pre-fail (FAIL=즉시 교체) / Old_age (점진적 열화)
# RAW_VALUE: 실제 원시 카운터 값
smartd 대신 systemd timer로 주기적 SMART 체크를 구성할 수도 있습니다.
smartctl --json=o -a /dev/sda로 JSON 출력을 받아
모니터링 시스템(Prometheus, Zabbix 등)에 연동하면 대규모 서버 환경에서 유용합니다.
SSD 전용 SMART 속성
SSD는 HDD와 다른 고유한 SMART 속성을 제공합니다. 제조사마다 ID 해석이 다를 수 있으므로, 정확한 해석을 위해 smartmontools의 디바이스 데이터베이스를 참조하세요.
| ID | 속성명 | 설명 | 위험 기준 |
|---|---|---|---|
| 170 | Available Spare Blocks | 남은 예비 NAND 블록 수 | 임계값 이하면 교체 필요 |
| 171 | Program Fail Count | NAND 프로그램(쓰기) 실패 횟수 | 증가 추세면 주의 |
| 172 | Erase Fail Count | NAND 이레이즈 실패 횟수 | 증가 추세면 주의 |
| 173 | Average Block Erase Count | 평균 블록 이레이즈 횟수 | TBW 한계 참조 |
| 174 | Unexpected Power Loss | 비정상 전원 차단 횟수 | 잦으면 전원 점검 |
| 175 | Power Loss Protection Failure | 전원 보호 회로 실패 | 1 이상이면 위험 |
| 177 | Wear Leveling Count | 웨어 레벨링 카운터 (마모 균등화) | 0에 가까울수록 위험 |
| 180 | Unused Reserve Block | 미사용 예비 블록 수 | 0이면 교체 필요 |
| 232 | Available Reserve Space | 남은 예비 공간 비율 | 10% 이하면 주의 |
| 233 | Media Wearout Indicator | 미디어 마모 지표 (100→0) | 10 이하면 교체 고려 |
| 241 | Total LBAs Written | 총 쓰기량 (LBA 단위) | TBW 한계 대비 확인 |
| 242 | Total LBAs Read | 총 읽기량 (LBA 단위) | 참고용 |
SMART 자체 테스트 (Self-Test) 상세
SMART Self-Test는 디스크가 자체적으로 하드웨어 무결성을 검사하는 기능입니다. 테스트 유형별로 검사 범위와 소요 시간이 다릅니다.
| 테스트 유형 | 검사 범위 | 소요 시간 | 용도 |
|---|---|---|---|
| Short | 전자 회로, 서보, 일부 표면 읽기 | ~2분 | 빠른 건강 진단 |
| Extended (Long) | 전체 디스크 표면 스캔 | HDD: 수 시간, SSD: 수 분~수십 분 | 완전한 표면 검사 |
| Conveyance | 운송 중 손상 감지 | ~5분 | 새 디스크 초기 검사 |
| Selective | 지정 LBA 범위만 검사 | 범위에 비례 | 특정 영역 문제 확인 |
# 각 테스트 유형 실행
smartctl -t short /dev/sda # Short: ~2분
smartctl -t long /dev/sda # Extended: 전체 표면
smartctl -t conveyance /dev/sda # 운송 테스트
smartctl -t select,0-999999 /dev/sda # LBA 0~999999 선택 테스트
# 예상 소요 시간 확인
smartctl -c /dev/sda | grep "Total time"
# 진행 상황 모니터링
smartctl -c /dev/sda | grep "remaining"
# 테스트 결과 확인
smartctl -l selftest /dev/sda
# Num Test_Description Status Remaining LifeTime LBA_of_first_error
# # 1 Extended offline Completed without error 00% 28456 -
smartctl JSON 출력과 자동화
# JSON 형식으로 SMART 전체 정보 조회
smartctl --json=o -a /dev/sda | jq '.ata_smart_attributes.table[] |
select(.id == 5 or .id == 197 or .id == 198) |
{id: .id, name: .name, raw: .raw.value}'
# 온도 추출
smartctl --json=o -a /dev/sda | jq '.temperature.current'
# 전원 켜진 시간 추출
smartctl --json=o -a /dev/sda | jq '.power_on_time.hours'
#!/usr/bin/env python3
# smart_alert.py — SMART 임계값 기반 알림
import json, subprocess, sys
THRESHOLDS = {
5: ("Reallocated_Sector_Ct", 0), # 재할당 섹터: 0 초과 시 경고
197: ("Current_Pending_Sector", 0), # 대기 섹터: 0 초과 시 경고
198: ("Offline_Uncorrectable", 0), # 오프라인 불량: 0 초과 시 경고
199: ("UDMA_CRC_Error_Count", 10), # CRC 에러: 10 초과 시 경고
}
result = subprocess.run(
["smartctl", "--json=o", "-A", sys.argv[1]],
capture_output=True, text=True)
data = json.loads(result.stdout)
for attr in data.get("ata_smart_attributes", {}).get("table", []):
if attr["id"] in THRESHOLDS:
name, threshold = THRESHOLDS[attr["id"]]
raw_val = attr["raw"]["value"]
if raw_val > threshold:
print(f"WARNING: {name} = {raw_val} (threshold: {threshold})")
SMART 패스스루 상세 (ATA_16/ATA_12)
smartctl은 리눅스의 SG_IO ioctl을 통해 SCSI CDB를 전송하며, ATA_16(0x85) 또는 ATA_12(0xA1) CDB를 사용하여 ATA 명령을 패스스루합니다.
/* ATA_16 CDB (16바이트) 구조 — SAT-5 표준 */
CDB[0] = 0x85; /* ATA PASS-THROUGH (16) */
CDB[1] = PROTOCOL; /* [7:4] = 프로토콜: */
/* 3=Non-data, 4=PIO Data-In, */
/* 5=PIO Data-Out, 12=DMA */
CDB[2] = FLAGS; /* BYTE_BLOCK | T_DIR | T_LENGTH */
/* | CK_COND (체크 컨디션) */
CDB[3] = FEATURES_H; /* Features 상위 바이트 (48-bit) */
CDB[4] = FEATURES_L; /* Features 하위 바이트 */
CDB[5] = SECTOR_CNT_H; /* Sector Count 상위 (48-bit) */
CDB[6] = SECTOR_CNT_L; /* Sector Count 하위 */
CDB[7] = LBA_LOW_H; /* LBA Low 상위 (48-bit) */
CDB[8] = LBA_LOW_L; /* LBA Low 하위 */
CDB[9] = LBA_MID_H; /* LBA Mid 상위 */
CDB[10] = LBA_MID_L; /* LBA Mid 하위 */
CDB[11] = LBA_HIGH_H; /* LBA High 상위 */
CDB[12] = LBA_HIGH_L; /* LBA High 하위 */
CDB[13] = DEVICE; /* Device 레지스터 */
CDB[14] = COMMAND; /* ATA 명령 코드 */
/* 0xB0=SMART, 0xEC=IDENTIFY */
CDB[15] = CONTROL; /* Control 바이트 */
/* SMART READ DATA 예시: */
/* COMMAND=0xB0, FEATURES=0xD0, LBA_MID=0x4F, LBA_HIGH=0xC2 */
PHY 이벤트 카운터
SATA PHY Event Counters(Log Page 11h)는 PHY 레벨의 통신 오류를 추적합니다. 케이블 품질 문제나 EMI 간섭을 진단하는 데 핵심적인 도구입니다.
| 카운터 ID | 이름 | 설명 |
|---|---|---|
| 0x001 | Command failed | 명령 실패 횟수 |
| 0x002 | R_ERR response for data FIS | 데이터 FIS의 CRC/인코딩 에러 |
| 0x003 | R_ERR response for device-to-host data FIS | 디바이스→호스트 데이터 에러 |
| 0x004 | R_ERR response for host-to-device data FIS | 호스트→디바이스 데이터 에러 |
| 0x005 | R_ERR response for non-data FIS | 비데이터 FIS 에러 |
| 0x006 | R_ERR response for device-to-host non-data FIS | 디바이스→호스트 비데이터 에러 |
| 0x008 | Device-to-host non-data FIS retries | 비데이터 FIS 재시도 횟수 |
| 0x009 | Transition from drive PhyRdy to drive PhyNRdy | PHY 링크 다운 횟수 |
| 0x00B | CRC errors within host-to-device FIS | 호스트→디바이스 CRC 에러 |
| 0x00D | Non-CRC errors within host-to-device FIS | 비CRC 에러 (인코딩 등) |
# PHY 이벤트 카운터 읽기
smartctl -l sataphy /dev/sda
# 출력 예:
# ID Size Value Description
# 0x0001 2 0 Command failed due to ICRC error
# 0x0002 2 0 R_ERR response for data FIS
# 0x0009 2 3 Transition from drive PhyRdy to PhyNRdy
# → 0x0009가 높으면 케이블/커넥터 접촉 불량 의심
# PHY 이벤트 카운터 리셋
smartctl -l sataphy,reset /dev/sda
커널 빌드 설정
SATA/AHCI 지원을 위한 주요 커널 설정 옵션입니다.
make menuconfig에서 Device Drivers → Serial ATA and Parallel ATA drivers (libata) 아래에 있습니다.
| Kconfig 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_ATA | Y | libata 서브시스템 (최상위 스위치) |
CONFIG_SATA_AHCI | Y | PCI AHCI 컨트롤러 드라이버 |
CONFIG_SATA_AHCI_PLATFORM | M | 임베디드/SoC AHCI (DT 기반) |
CONFIG_ATA_SFF | Y | SFF (Small Form Factor) ATA 지원 |
CONFIG_ATA_BMDMA | Y | BMDMA (Bus Master DMA) 지원 |
CONFIG_ATA_PIIX | M | Intel PIIX/ICH SATA 컨트롤러 |
CONFIG_SATA_MV | M | Marvell SATA 컨트롤러 |
CONFIG_SATA_SIL | M | Silicon Image SATA 컨트롤러 |
CONFIG_SATA_SIS | M | SiS SATA 컨트롤러 |
CONFIG_SATA_VIA | M | VIA SATA 컨트롤러 |
CONFIG_ATA_GENERIC | M | Generic ATA (알 수 없는 컨트롤러) |
CONFIG_PATA_* | M | 각종 PATA(병렬 ATA) 컨트롤러 |
# 현재 커널의 SATA 관련 설정 확인
zgrep "CONFIG_SATA\|CONFIG_ATA" /proc/config.gz
# 또는
grep "CONFIG_SATA\|CONFIG_ATA" /boot/config-$(uname -r)
# menuconfig 경로
# Device Drivers
# → Serial ATA and Parallel ATA drivers (libata)
# → AHCI SATA support [CONFIG_SATA_AHCI]
# → Platform AHCI SATA support [CONFIG_SATA_AHCI_PLATFORM]
# → ATA SFF support (for legacy IDE) [CONFIG_ATA_SFF]
# 로드된 AHCI 모듈 확인
lsmod | grep ahci
# 출력 예: ahci 40960 3
# libahci 36864 1 ahci
CONFIG_SATA_AHCI_PLATFORM과 해당 SoC의 PHY 드라이버를 함께 활성화해야 합니다.
Device Tree에서 compatible = "generic-ahci" 또는 벤더별 compatible 문자열을 사용합니다.
Device Tree AHCI 바인딩 예시
/* AHCI Platform 디바이스 트리 노드 예시 */
sata@f1090000 {
compatible = "marvell,armada-380-ahci";
reg = <0xf1090000 0x2000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&gate_clk 15>;
phys = <&comphy0 1>;
phy-names = "sata-phy";
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
sata-port@0 {
reg = <0>;
/* 포트별 PHY 설정 */
};
sata-port@1 {
reg = <1>;
};
};
커널 모듈 의존성
# AHCI 모듈 의존성 트리 확인
modinfo ahci | grep depends
# depends: libahci,libata
# 모듈 로드 순서
# 1. libata.ko (core)
# 2. libahci.ko (AHCI 공통 함수)
# 3. ahci.ko (PCI AHCI 드라이버)
# 또는 ahci_platform.ko (플랫폼 AHCI)
# 수동 모듈 로드/언로드
modprobe ahci # 의존성 자동 해결
modprobe -r ahci # 언로드 (디바이스 사용 중이면 실패)
커널 설정 최적화 가이드
| 용도 | AHCI | ATA_SFF | ATA_PIIX | PATA | 비고 |
|---|---|---|---|---|---|
| 서버 | =y (내장) | =n | =n | =n | 불필요 코드 제거, 부팅 속도 향상 |
| 데스크톱 | =m (모듈) | =m | =m | =m | 유연성 확보, 다양한 하드웨어 지원 |
| 임베디드 | PLATFORM=y | =n | =n | =n | SoC AHCI만 필요 |
| 가상 머신 | =y | =y | =y | =n | QEMU/KVM 레거시: ATA_PIIX 필요 |
initramfs와 AHCI
루트 파일시스템이 SATA 디스크에 있는 경우, AHCI 드라이버가 initramfs에 포함되거나 커널에 내장(=y)되어야 합니다. 모듈(=m)로 빌드했는데 initramfs에 포함하지 않으면 커널이 루트 파일시스템을 마운트할 수 없어 부팅에 실패합니다.
# 방법 1: 커널에 내장 (권장, 가장 간단)
# .config에서:
CONFIG_SATA_AHCI=y # 모듈(=m)이 아닌 내장(=y)
CONFIG_ATA=y
# 방법 2: initramfs에 모듈 포함 (dracut)
dracut --add-drivers "ahci libahci" -f /boot/initramfs-$(uname -r).img $(uname -r)
# 방법 3: initramfs에 모듈 포함 (mkinitramfs/update-initramfs)
echo "ahci" >> /etc/initramfs-tools/modules
update-initramfs -u
# initramfs 내용 확인
lsinitrd /boot/initramfs-$(uname -r).img | grep ahci
# 또는
lsinitramfs /boot/initrd.img-$(uname -r) | grep ahci
컴파일 타임 디버그 옵션
| 옵션 | 설명 | 영향 |
|---|---|---|
CONFIG_ATA_VERBOSE_ERROR | ATA 에러 메시지 상세 출력 | dmesg에 에러 원인 텍스트 추가 |
CONFIG_ATA_FORCE | libata.force 커널 파라미터 지원 | 런타임 NCQ/속도 강제 설정 가능 |
CONFIG_SATA_PMP | Port Multiplier 지원 | PMP 미사용 시 비활성화 가능 |
CONFIG_ATA_ACPI | ATA ACPI 통합 | ACPI 기반 전원 관리, 핫 플러그 |
# SATA 전용 최소 .config 프래그먼트 (PATA/SFF 제외)
CONFIG_ATA=y
CONFIG_ATA_VERBOSE_ERROR=y
CONFIG_ATA_FORCE=y
CONFIG_ATA_ACPI=y
CONFIG_SATA_AHCI=y
CONFIG_SATA_PMP=y
# CONFIG_ATA_SFF is not set ← SFF/PATA 전부 비활성화
# CONFIG_ATA_BMDMA is not set
# CONFIG_ATA_GENERIC is not set
# CONFIG_PATA_LEGACY is not set
CONFIG_SATA_AHCI=y만으로 충분합니다. 단, 레거시 IDE 모드(-M ide)를 사용하면
CONFIG_ATA_PIIX가 필요합니다. VirtIO 블록(virtio-blk)을 사용하면
SATA 드라이버가 불필요합니다.
디버깅과 트러블슈팅
SATA/AHCI 문제를 진단하기 위한 도구와 기법을 정리합니다.
dmesg 핵심 메시지
# 정상 초기화
ata1: SATA max UDMA/133 abar m2048@0xf7c10000 port 0xf7c10100 irq 30
ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
ata1.00: ATA-10: Samsung SSD 870 EVO 500GB, SVT02B6Q, max UDMA/133
ata1.00: 976773168 sectors, multi 1: LBA48 NCQ (depth 31/32)
ata1.00: configured for UDMA/133
# 속도 다운그레이드 (문제 징후)
ata1: SATA link up 3.0 Gbps (SStatus 123 SControl 300)
# → 6Gbps가 아닌 3Gbps로 연결: 케이블 또는 PHY 문제
# 링크 다운 (디바이스 미감지)
ata1: SATA link down (SStatus 0 SControl 300)
# → 케이블 미연결, 전원 미공급, 또는 디바이스 고장
# 에러 발생
ata1.00: exception Emask 0x10 SAct 0x1 SErr 0x400100 action 0x6 frozen
ata1.00: irq_stat 0x08000000, interface fatal error
# → SErr 분석: 0x400100 = DIAG.X + ERR.I
# COMRESET 반복 (링크 불안정)
ata1: hard resetting link
ata1: COMRESET failed (errno=-16)
ata1: reset failed, giving up
libata.force 커널 파라미터
# 부트 파라미터로 libata 동작 강제 설정
# 형식: libata.force=[port][.device]:feature
# NCQ 비활성화 (문제 디버깅용)
libata.force=noncq
# 특정 포트만 NCQ 비활성화
libata.force=1:noncq
# SATA 속도 제한 (3Gbps)
libata.force=1.5Gbps
libata.force=3.0Gbps
# DMA 모드 강제
libata.force=udma5
# 디버그 메시지 활성화
libata.force=dump_id # IDENTIFY 데이터 덤프
Tracepoints
# ATA tracepoint 목록
ls /sys/kernel/tracing/events/libata/
# ata_qc_issue, ata_qc_complete, ata_eh_link_autopsy, ata_eh_about_to_do ...
# QC 발행/완료 추적
echo 1 > /sys/kernel/tracing/events/libata/ata_qc_issue/enable
echo 1 > /sys/kernel/tracing/events/libata/ata_qc_complete/enable
cat /sys/kernel/tracing/trace_pipe
# AHCI tracepoint (있는 경우)
ls /sys/kernel/tracing/events/ahci/ 2>/dev/null
일반적인 문제와 해결
| 증상 | 원인 | 해결 |
|---|---|---|
| SATA link down | 케이블/전원/디바이스 고장 | 케이블 교체, 다른 포트 시도 |
| 3Gbps로 다운그레이드 | 케이블 품질, PHY 협상 실패 | SATA III 케이블로 교체 |
| NCQ 에러 반복 | 펌웨어 버그, 디바이스 호환성 | libata.force=noncq |
| COMRESET 루프 | PHY 불안정, 전원 부족 | 전원 공급 확인, 속도 제한 |
| Command Timeout | 디바이스 응답 없음 | SMART 확인, 디바이스 교체 고려 |
| CRC 에러 증가 | 케이블/커넥터 접촉 불량 | 케이블 교체, 커넥터 청소 |
SError 디코딩 도구
# dmesg의 SErr 값을 수동 디코딩하는 방법
# 예: SErr 0x00400100
# 비트 8 (ERR.T) = Transient Data Integrity Error
# 비트 22 (DIAG.X) = Device Exchanged (핫 플러그)
# SError 레지스터 직접 확인 (AHCI 포트)
# BAR5 + 0x100 (Port 0) + 0x30 (PxSERR)
setpci -s 00:1f.2 ABAR.l # BAR5 주소 확인
# SError 클리어 (1을 쓰면 해당 비트 클리어)
# 커널이 자동으로 처리하므로 수동 개입은 드묾
# 전체 AHCI 레지스터 덤프
lspci -s 00:1f.2 -vvv | grep -A20 "Memory at"
동적 디버그 활성화
# libata 동적 디버그 메시지 활성화
echo "module libata +p" > /sys/kernel/debug/dynamic_debug/control
echo "module ahci +p" > /sys/kernel/debug/dynamic_debug/control
# 특정 파일의 디버그만 활성화
echo "file libata-eh.c +p" > /sys/kernel/debug/dynamic_debug/control
echo "file libata-scsi.c +p" > /sys/kernel/debug/dynamic_debug/control
# 비활성화
echo "module libata -p" > /sys/kernel/debug/dynamic_debug/control
ftrace를 이용한 SATA I/O 추적
# ftrace 디렉터리로 이동
cd /sys/kernel/tracing
# function_graph 트레이서 설정
echo function_graph > current_tracer
# AHCI 관련 함수만 필터링
echo 'ahci_qc_*' > set_ftrace_filter
echo 'ata_scsi_*' >> set_ftrace_filter
echo 'ahci_interrupt' >> set_ftrace_filter
# 추적 시작
echo 1 > tracing_on
# I/O 워크로드 실행 (예: dd)
dd if=/dev/sda of=/dev/null bs=4k count=100 iflag=direct
# 추적 중지 및 결과 확인
echo 0 > tracing_on
cat trace | head -50
# 출력 예:
# 2) 0.341 us | ahci_qc_prep();
# 2) 0.120 us | ahci_qc_issue();
# 2) +15.234 us | ahci_single_level_irq_intr();
# → 15.234us = 명령 발행부터 인터럽트까지의 레이턴시
# 정리
echo nop > current_tracer
echo > set_ftrace_filter
blktrace/blkparse를 이용한 블록 레벨 분석
# blktrace 수집 (10초간)
blktrace -d /dev/sda -o sda_trace -w 10
# blkparse로 분석
blkparse -i sda_trace -d sda_trace.bin
# 주요 이벤트 단계:
# Q (Queue) — 요청이 블록 레이어에 진입
# G (Get) — 요청 구조체 할당
# I (Insert) — I/O 스케줄러 큐에 삽입
# D (Dispatch) — 드라이버에 전달 (AHCI로 발행)
# C (Complete) — 완료 인터럽트 수신
# btt로 통계 분석
btt -i sda_trace.bin
# Q2Q: 요청 간 간격
# Q2D: 큐 진입부터 디스패치까지 (소프트웨어 오버헤드)
# D2C: 디스패치부터 완료까지 (하드웨어 서비스 시간)
# Q2C: 전체 요청 레이턴시
# D2C가 크면 디바이스 자체 성능 문제
# Q2D가 크면 스케줄러/큐 설정 문제
perf를 이용한 AHCI 인터럽트 분석
# AHCI 인터럽트 빈도 추적
perf record -g -e irq:irq_handler_entry -a -- sleep 10
perf report --stdio | grep ahci
# 블록 I/O 이벤트 통계
perf stat -e block:block_rq_issue,block:block_rq_complete,\
block:block_rq_insert -a -- sleep 10
# AHCI 함수별 CPU 시간 프로파일링
perf top -g -p $(pgrep -d, kworker) 2>/dev/null
# → ahci_handle_port_interrupt, ata_scsi_queuecmd 등 확인
AHCI 레지스터 덤프 분석
# lspci로 AHCI BAR5 주소 확인
lspci -s 00:1f.2 -v | grep "Memory at"
# Memory at f7c10000 (32-bit, non-prefetchable) [size=2K]
# 주요 AHCI Generic Host Control 레지스터
# BAR5 + 0x00: CAP (Host Capabilities)
# BAR5 + 0x04: GHC (Global Host Control)
# BAR5 + 0x08: IS (Interrupt Status)
# BAR5 + 0x0C: PI (Ports Implemented)
# 포트 레지스터 (Port 0 = BAR5 + 0x100)
# +0x00: PxCLB (Command List Base Address)
# +0x08: PxFB (FIS Base Address)
# +0x10: PxIS (Port Interrupt Status)
# +0x18: PxCMD (Port Command and Status)
# +0x28: PxSSTS (SATA Status — DET, SPD, IPM)
# +0x2C: PxSCTL (SATA Control)
# +0x30: PxSERR (SATA Error)
# +0x34: PxSACT (SATA Active — NCQ)
# +0x38: PxCI (Command Issue)
# lspci -xxxx로 PCI 설정 공간 전체 덤프
lspci -s 00:1f.2 -xxxx | head -20
SATA 성능 튜닝
SATA 디바이스의 성능을 최적화하기 위한 커널 및 시스템 설정입니다.
NCQ 활성화 확인
# NCQ 지원 및 활성화 확인
dmesg | grep NCQ
# ata1.00: 976773168 sectors, multi 1: LBA48 NCQ (depth 31/32)
# depth 31/32 = NCQ 활성화, 최대 32 슬롯 중 31 사용
# 큐 깊이 확인
cat /sys/block/sda/device/queue_depth
# 32 = NCQ 활성, 1 = NCQ 비활성
# NCQ 비활성화 (디버깅용)
echo 1 > /sys/block/sda/device/queue_depth
I/O 스케줄러
# 현재 스케줄러 확인
cat /sys/block/sda/queue/scheduler
# [mq-deadline] none kyber bfq
# SATA HDD: mq-deadline 또는 bfq 권장
echo "mq-deadline" > /sys/block/sda/queue/scheduler
# SATA SSD: none (noop) 또는 mq-deadline
echo "none" > /sys/block/sda/queue/scheduler
Read-Ahead 설정
# 현재 read-ahead 값 (섹터 단위, 512B)
cat /sys/block/sda/queue/read_ahead_kb
# 기본: 128 KB
# HDD 순차 읽기 최적화: 증가
echo 2048 > /sys/block/sda/queue/read_ahead_kb
# SSD 랜덤 I/O: 줄이기
echo 64 > /sys/block/sda/queue/read_ahead_kb
Write Cache
# Write Cache 상태 확인
hdparm -W /dev/sda
# write-caching = 1 (on)
# Write Cache 비활성화 (데이터 안전 우선)
hdparm -W0 /dev/sda
# Write Cache 활성화 (성능 우선)
hdparm -W1 /dev/sda
fio 벤치마크 예시
# 순차 읽기 (SATA 대역폭 한계 테스트)
fio --name=seq-read --filename=/dev/sda --rw=read \
--bs=128k --iodepth=32 --numjobs=1 --direct=1 \
--ioengine=libaio --runtime=30 --time_based
# 랜덤 4K 읽기 (NCQ + IOPS 테스트)
fio --name=rand-read --filename=/dev/sda --rw=randread \
--bs=4k --iodepth=32 --numjobs=1 --direct=1 \
--ioengine=libaio --runtime=30 --time_based
# 순차 쓰기
fio --name=seq-write --filename=/dev/sda --rw=write \
--bs=128k --iodepth=32 --numjobs=1 --direct=1 \
--ioengine=libaio --runtime=30 --time_based
# 혼합 랜덤 R/W (70:30)
fio --name=mixed --filename=/dev/sda --rw=randrw --rwmixread=70 \
--bs=4k --iodepth=32 --numjobs=4 --direct=1 \
--ioengine=libaio --runtime=30 --time_based
| 메트릭 | SATA HDD (7200RPM) | SATA SSD |
|---|---|---|
| 순차 읽기 | ~150-200 MB/s | ~550 MB/s |
| 순차 쓰기 | ~130-180 MB/s | ~520 MB/s |
| 랜덤 4K 읽기 IOPS | ~100-200 | ~90,000-100,000 |
| 랜덤 4K 쓰기 IOPS | ~100-200 | ~80,000-90,000 |
| 레이턴시 | ~5-10 ms | ~0.05-0.1 ms |
MSI vs Legacy Interrupt
# AHCI 인터럽트 모드 확인
grep ahci /proc/interrupts
# 출력 예 (MSI):
# 30: 12345 PCI-MSI 512000-edge ahci[0000:00:1f.2]
# 출력 예 (INTx):
# 19: 12345 IO-APIC 19-fasteoi ahci
# MSI가 활성화되지 않은 경우 (일부 구형 칩셋)
# BIOS에서 MSI 활성화 또는 커널 파라미터 확인
lspci -s 00:1f.2 -vv | grep "MSI:"
# MSI: Enable+ Count=1/1 Maskable- 64bit-
TRIM/Discard 설정 (SATA SSD)
# TRIM 지원 확인
hdparm -I /dev/sda | grep -i trim
# * Data Set Management TRIM supported (limit 8 blocks)
# discard 마운트 옵션 (연속 TRIM)
mount -o discard /dev/sda1 /mnt
# 주기적 TRIM (fstrim, 권장)
fstrim -v /mnt
# 또는 systemd timer 사용 (보통 기본 활성화)
systemctl enable --now fstrim.timer
# TRIM 지원 확인 (블록 레벨)
cat /sys/block/sda/queue/discard_max_bytes
# 0이면 TRIM 미지원
cat /sys/block/sda/queue/discard_granularity
mount -o discard (연속 TRIM)은 파일 삭제 시마다 TRIM 명령을 보내므로
삭제 성능이 약간 저하될 수 있습니다. fstrim (주기적 TRIM)은 한꺼번에
모아서 TRIM을 보내므로 일상적인 성능에 영향이 없습니다.
서버 환경에서는 fstrim.timer (주 1회)를 권장합니다.
I/O 스케줄러 상세 비교 (SATA 관점)
| 스케줄러 | SATA HDD 순차 | SATA HDD 랜덤 | SATA SSD | 레이턴시 | 공정성 | 권장 용도 |
|---|---|---|---|---|---|---|
mq-deadline | 우수 | 양호 | 양호 | 보장 (deadline) | 제한적 | 범용, 서버 HDD |
bfq | 양호 | 우수 | 양호 | 양호 | 우수 (cgroup) | 데스크톱, 멀티 사용자 |
kyber | 양호 | 양호 | 우수 | 자동 조절 | 양호 | SSD, 고성능 |
none | 보통 | 보통 | 최적 | 최소 오버헤드 | 없음 | NVMe, SSD 단일 용도 |
mq-deadline은 읽기/쓰기 각각에 deadline을 설정하여
기아(starvation)를 방지합니다. SATA HDD에서는 bfq도 좋은 선택이며,
데스크톱 환경에서 반응성이 더 좋을 수 있습니다.
SATA SSD에서는 none 또는 kyber가 적합합니다.
블록 레이어 큐 설정
# nr_requests: 큐당 최대 요청 수 (기본 256)
cat /sys/block/sda/queue/nr_requests
echo 512 > /sys/block/sda/queue/nr_requests # 서버에서 증가
# max_sectors_kb: 단일 I/O 최대 크기 (기본 512KB)
cat /sys/block/sda/queue/max_sectors_kb
echo 1024 > /sys/block/sda/queue/max_sectors_kb # 순차 I/O 향상
# nomerges: 요청 병합 비활성화 (SSD 랜덤 워크로드)
echo 2 > /sys/block/sda/queue/nomerges # 0=병합, 1=단순, 2=비활성
# rotational: 회전 미디어 여부 (스케줄러 동작 영향)
cat /sys/block/sda/queue/rotational
# 1=HDD, 0=SSD (SATA SSD는 자동 감지되지만 확인 필요)
# io_poll: 폴링 모드 (SATA에서는 보통 효과 없음)
cat /sys/block/sda/queue/io_poll
# SATA/AHCI는 인터럽트 기반이므로 폴링 비권장
NUMA와 SATA 성능
다중 소켓 서버에서 AHCI 컨트롤러가 연결된 NUMA 노드와 I/O를 수행하는 프로세스의 NUMA 노드가 다르면, 메모리 접근 레이턴시 증가로 인해 성능이 저하될 수 있습니다.
# AHCI 컨트롤러의 NUMA 노드 확인
cat /sys/block/sda/device/../../numa_node
# 0 = NUMA 노드 0, -1 = NUMA 정보 없음
# lstopo로 PCI 디바이스의 NUMA 토폴로지 시각화
lstopo --of ascii | grep -A5 PCI
# AHCI 컨트롤러와 같은 NUMA 노드에서 fio 실행
numactl --cpunodebind=0 --membind=0 \
fio --name=test --filename=/dev/sda --rw=randread \
--bs=4k --iodepth=32 --direct=1 --ioengine=libaio --runtime=30
성능 모니터링 도구 모음
| 도구 | 용도 | 사용 예시 |
|---|---|---|
iostat | 디바이스별 I/O 통계 | iostat -xz 1 |
iotop | 프로세스별 I/O 사용량 | iotop -oPa |
dstat | 통합 시스템 통계 | dstat -tdD sda |
pidstat -d | 프로세스별 디스크 I/O | pidstat -d 1 |
bpftrace biolatency | 블록 I/O 레이턴시 히스토그램 | biolatency.bt |
bpftrace biosnoop | 블록 I/O 요청별 추적 | biosnoop.bt |
blktrace | 블록 레이어 상세 추적 | blktrace -d /dev/sda -w 10 |
SATA SSD 성능 저하 원인과 대응
SATA SSD는 사용 시간이 경과하면 성능이 저하될 수 있습니다. 주요 원인과 대응 방법을 정리합니다.
| 원인 | 메커니즘 | 대응 |
|---|---|---|
| Write Amplification (WA) | NAND 특성상 블록 단위 이레이즈 필요, 실제 쓰기량이 호스트 쓰기량보다 큼 | 정렬된 순차 쓰기, 4K 정렬 파티션 |
| Garbage Collection (GC) | 유효 페이지 이동 후 블록 이레이즈, I/O 레이턴시 스파이크 유발 | Over-provisioning 확보, TRIM 활성화 |
| TRIM 미적용 | 삭제된 블록 정보가 SSD에 전달되지 않아 GC 효율 저하 | fstrim.timer 활성화 |
| Over-provisioning 부족 | 예비 공간 부족으로 GC 빈도 증가 | 전체 용량의 10-20% 미사용 유지 |
| Steady-state 진입 | 초기 성능(FOB) 대비 장기 사용 후 성능 하락 | 벤치마크 시 steady-state 기준 사용 |
| 온도 쓰로틀링 | NAND 온도 상승 시 성능 자동 제한 | 방열 확보, SMART 온도 모니터링 |
SATA 링크 초기화
OOB 시그널링 상세
OOB(Out-of-Band) 시그널링은 SATA PHY 레벨에서 링크 초기화를 위해 사용하는 특수 신호입니다. 정상적인 데이터 전송과 구별되는 전기적 패턴으로, PHY가 꺼진 상태에서도 동작합니다.
| 시그널 | 버스트 패턴 | 간격 | 용도 |
|---|---|---|---|
| COMRESET | 175.6ns burst x 6 | 480ns gap | 호스트→디바이스 리셋 요청 |
| COMINIT | 175.6ns burst x 6 | 480ns gap | 디바이스→호스트 리셋 응답 |
| COMWAKE | 106.7ns burst x 6 | 106.7ns gap | 슬립 상태에서 웨이크업 |
/* drivers/ata/libata-core.c — COMRESET 수행 */
int sata_link_hardreset(struct ata_link *link,
const unsigned long *timing, unsigned long deadline,
bool *online, int (*check_ready)(struct ata_link *))
{
u32 scontrol;
/* 1. SControl에 DET=1 쓰기 → COMRESET 시작 */
sata_scr_read(link, SCR_CONTROL, &scontrol);
scontrol = (scontrol & 0x0f0) | 0x01; /* DET = 1 */
sata_scr_write(link, SCR_CONTROL, scontrol);
msleep(timing[0]); /* 최소 COMRESET 기간 대기 */
/* 2. DET=0으로 복원 → COMRESET 종료, 속도 협상 시작 */
scontrol = (scontrol & 0x0f0) | 0x00;
sata_scr_write(link, SCR_CONTROL, scontrol);
/* 3. PxSSTS.DET == 3 (링크 성립) 대기 */
return ata_wait_after_reset(link, deadline, check_ready);
}
PxSSTS.DET 값 해석
| DET 값 | 의미 |
|---|---|
| 0 | 디바이스 미감지, PHY 통신 없음 |
| 1 | 디바이스 감지됨, PHY 통신 미성립 |
| 3 | 디바이스 감지, PHY 통신 성립 (정상) |
| 4 | PHY 오프라인 모드 |
속도 협상 실패 시 동작
속도 협상은 최고 속도(Gen3, 6Gbps)부터 시도하며, PHY 레벨에서 D10.2 패턴을
성공적으로 교환하지 못하면 한 단계 낮은 속도로 재시도합니다.
리눅스 커널에서는 PxSCTL.SPD 필드로 최대 속도를 제한할 수 있습니다.
| PxSCTL.SPD | 의미 |
|---|---|
| 0 | 제한 없음 (최고 속도 시도) |
| 1 | Gen1 (1.5 Gbps)로 제한 |
| 2 | Gen2 (3.0 Gbps)로 제한 |
| 3 | Gen3 (6.0 Gbps)로 제한 |
# 속도 제한 적용 (커널 파라미터)
# 특정 포트를 3Gbps로 제한
libata.force=1:3.0Gbps
# SStatus로 현재 협상된 속도 확인
# SStatus[7:4] = SPD: 1=Gen1, 2=Gen2, 3=Gen3
# SStatus[3:0] = DET: 3=링크 성립
dmesg | grep "SATA link up"
# ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
# SStatus 133: DET=3(링크OK), SPD=3(Gen3), IPM=1(Active)
Staggered Spin-up
다수의 HDD가 동시에 전원이 인가되면 순간 전류가 급등하여 전원 공급이 불안정해질 수 있습니다. AHCI의 Staggered Spin-up 기능은 포트별로 순차적으로 디스크 모터를 시작하여 이 문제를 해결합니다. CAP.SSS 비트로 지원 여부를 확인하며, PxCMD.SUD(Spin-Up Device) 비트를 설정하여 개별 포트의 스핀업을 제어합니다.
/* Staggered Spin-up 절차 */
if (hpriv->cap & HOST_CAP_SSS) {
/* 각 포트를 순차적으로 스핀업 */
for (i = 0; i < host->n_ports; i++) {
u32 cmd = readl(port_mmio + PORT_CMD);
cmd |= PORT_CMD_SPIN_UP; /* SUD = 1 */
writel(cmd, port_mmio + PORT_CMD);
msleep(10); /* 포트 간 지연 */
}
}
PHY 캘리브레이션과 이퀄라이저
SATA PHY는 링크 초기화 과정에서 신호 무결성을 보장하기 위해 자동 캘리브레이션을 수행합니다. 특히 6Gbps(Gen3) 동작 시에는 고주파 신호 감쇠와 ISI(Inter-Symbol Interference)가 심해지므로 PHY 이퀄라이저가 중요합니다.
| 파라미터 | 역할 | Gen3 영향 |
|---|---|---|
| TX Amplitude | 송신 신호 진폭 조절 | 케이블 길이에 따라 증폭 필요 |
| TX Pre-emphasis | 고주파 성분 미리 강조 | 긴 케이블에서 신호 보상 |
| TX De-emphasis | 저주파 성분 감쇠 | 반사파 감소 |
| RX Equalization | 수신측 신호 보정 (CTLE/DFE) | 고주파 감쇠 보상 |
/* 일부 SoC AHCI 드라이버는 PHY 파라미터를 Device Tree로 설정 */
sata-phy@e0003000 {
compatible = "vendor,sata-phy";
reg = <0xe0003000 0x100>;
tx-amplitude-millivolt = <600>; /* TX 진폭: 600mV */
tx-de-emphasis-db = <6>; /* DE-emphasis: -6dB */
};
스프레드 스펙트럼 클럭킹 (SSC)
SSC(Spread Spectrum Clocking)는 클럭 주파수를 의도적으로 미세하게 변동시켜 전자파 방출(EMI)의 피크 에너지를 분산시키는 기술입니다. SATA 스펙은 ±0.5% (5000ppm) 범위의 다운-스프레드를 허용합니다.
| 항목 | 값 |
|---|---|
| 주파수 변동 범위 | -0.5% ~ 0% (다운스프레드) |
| 변조 주파수 | 30~33 kHz |
| 목적 | EMI 방출 피크 저감 (FCC/CE 인증) |
| 링크 마진 영향 | 수신측 CDR(Clock Data Recovery) 추적 범위 내 |
| 호환성 | 호스트/디바이스 모두 SSC 지원 필요 (Gen3 필수) |
SATA 링크 초기화 타임아웃
libata는 다양한 상황에 맞는 타임아웃 값을 정의합니다.
이 값들은 include/linux/libata.h에서 확인할 수 있습니다.
| 상수명 | 기본값 | 용도 |
|---|---|---|
ATA_TMOUT_BOOT | 30초 | 부팅 시 초기 디바이스 감지 대기 |
ATA_TMOUT_BOOT_QUICK | 7초 | 빠른 부팅 모드 디바이스 감지 |
ATA_TMOUT_INTERNAL | 30초 | 내부 명령 타임아웃 |
ATA_TMOUT_INTERNAL_QUICK | 5초 | 빠른 내부 명령 (IDENTIFY 등) |
ATA_TMOUT_FLUSH | 40초 | FLUSH CACHE 명령 |
ATA_TMOUT_PMP_SRST | 10초 | PMP 소프트 리셋 |
SATA_COMRESET_DELAY | 1ms + 대기 | COMRESET 최소 지속 시간 |
# 커널 로그에서 링크 초기화 타이밍 분석
dmesg -T | grep -E "ata[0-9]+:" | head -20
# [Sun Mar 09 10:00:01] ahci 0000:00:1f.2: AHCI 0001.0301
# [Sun Mar 09 10:00:01] ahci 0000:00:1f.2: 6 ports detected
# [Sun Mar 09 10:00:01] ata1: SATA max UDMA/133 ... irq 30
# [Sun Mar 09 10:00:02] ata1: SATA link up 6.0 Gbps ← +1초: OOB 완료
# [Sun Mar 09 10:00:02] ata1.00: ATA-10: Samsung SSD ... ← IDENTIFY
# [Sun Mar 09 10:00:02] ata1.00: configured for UDMA/133 ← SET FEATURES
# [Sun Mar 09 10:00:32] ata6: SATA link down ← +30초: 빈 포트 타임아웃
# → 빈 포트는 ATA_TMOUT_BOOT(30초) 후 포기
일반적인 실수와 올바른 패턴
M.2 SATA SSD를 NVMe 전용 M.2 슬롯에 꽂거나, NVMe SSD를 SATA 전용 슬롯에 꽂으면 물리적으로는 맞아도 전혀 인식되지 않습니다. 메인보드 매뉴얼에서 M.2 슬롯의 지원 인터페이스를 반드시 확인하세요.
lsblk -d -o NAME,TRAN으로 TRAN이 sata이면 SATA M.2,
nvme 디바이스는 /dev/nvmeN으로 나타납니다.
BIOS/UEFI에서 SATA 모드를 IDE/Legacy로 설정하면 AHCI 기능(NCQ, 핫 플러그, ALPM)을 전혀 사용할 수 없습니다. SSD의 NCQ 없이는 성능이 크게 저하됩니다.
min_power 정책을 설정하면 Slumber 상태에서 깨어나는 데 ~10ms가 걸려
간헐적인 I/O 레이턴시 스파이크가 발생합니다. 데이터베이스 서버에서 특히 문제됩니다.
max_performance, 노트북에서는
med_power_with_dipm을 권장합니다. min_power는 극단적인 배터리 절약이
필요한 경우에만 사용하세요.
과거 특정 디바이스의 NCQ 버그 때문에
libata.force=noncq를 설정한 채
잊어버리는 경우가 있습니다. NCQ 없이 SSD를 사용하면 성능이 크게 저하됩니다.
dmesg에서 NCQ 관련 에러를 확인하고,
특정 포트/디바이스에만 NCQ를 비활성화하세요 (libata.force=1:noncq).
문제가 해결되면 반드시 원복하세요.
저가형 SATA 데이터 케이블은 6Gbps에서 신호 무결성 문제를 일으킬 수 있습니다. CRC 에러(SMART ID 199)가 증가하거나, 링크가 3Gbps로 다운그레이드되는 증상이 나타납니다.
Write Cache는 성능을 크게 향상시키지만, 정전 시 캐시의 데이터가 유실됩니다. 파일시스템 저널링만으로는 Write Cache 유실을 완전히 보호하지 못합니다.
hdparm -W0 /dev/sda로 Write Cache를 비활성화하세요.
SATA SSD를 사용하면서 TRIM을 설정하지 않으면 시간이 지남에 따라 쓰기 성능이 저하됩니다. 또한 USB-SATA 브리지(외장 케이스)를 통해 연결된 SSD는 대부분의 USB 브리지 칩이 TRIM(UNMAP) 명령을 전달하지 않아 TRIM이 동작하지 않습니다.
fstrim.timer를 활성화하세요.
USB-SATA 브리지 환경에서는 lsblk -D로 DISC-MAX가 0이 아닌지 확인하세요.
UAS(USB Attached SCSI) 모드를 지원하는 최신 브리지 칩(ASM2362 등)은 TRIM을 전달할 수 있습니다.
BIOS에서 보이는 SATA 포트 번호(Port 0, 1, 2...)와 리눅스의
ata1, ata2 번호,
그리고 /dev/sda, /dev/sdb 이름이 서로 일치하지 않는 경우가 흔합니다.
디바이스 추가/제거 시 sdX 이름이 바뀔 수도 있어, 잘못된 디스크를 포맷하는 사고가 발생할 수 있습니다.
/dev/sdX 대신 안정적인 디바이스 경로를 사용하세요:
/dev/disk/by-id/ata-MODEL_SERIAL (모델+시리얼 기반),
/dev/disk/by-uuid/UUID (파일시스템 UUID),
/dev/disk/by-path/pci-0000:00:1f.2-ata-1 (PCI 경로 기반).
fstab에서도 UUID를 사용하세요.
일부 RAID 컨트롤러(특히 Intel RST)는 BIOS에서 AHCI 모드로 설정해도 내부적으로 RAID 기능이 활성화되어 있을 수 있습니다. 이 경우 리눅스에서 순수 AHCI 드라이버 대신
ahci의 RAID 변형이 로드되거나,
RAID 메타데이터가 디스크에 남아 있어 예기치 않은 동작이 발생할 수 있습니다.
mdadm --zero-superblock /dev/sdX로 제거하세요.
dmraid -r로 가짜 RAID(FakeRAID) 메타데이터도 확인하세요.
자주 묻는 질문 (FAQ)
| 질문 | 답변 |
|---|---|
| SATA SSD에서 NCQ가 필요한가요? | 필요합니다. NCQ는 SSD 내부의 병렬 NAND 채널을 활용하여 랜덤 I/O 성능을 크게 향상시킵니다. queue_depth=32가 정상입니다. |
| SATA 케이블 길이 제한은? | SATA 스펙상 최대 1미터입니다. 가능한 50cm 이하를 권장하며, 6Gbps에서는 케이블 품질이 중요합니다. |
| SATA 핫 플러그는 안전한가요? | AHCI 모드에서 핫 플러그를 지원합니다. 단, 마운트된 파일시스템은 반드시 언마운트 후 제거하세요. echo 1 > /sys/block/sdX/device/delete로 안전 제거합니다. |
| SATA와 NVMe 중 어떤 것을 선택해야 하나요? | 성능이 중요하면 NVMe, 비용 효율과 호환성이 중요하면 SATA입니다. SATA SSD의 순차 성능은 ~550MB/s로 인터페이스 한계에 도달합니다. |
| dmesg에서 "configured for UDMA/133"은 무엇인가요? | SATA 디바이스가 libata의 SAT 레이어를 통해 UDMA mode 6(133MB/s)으로 설정된 것입니다. 실제 SATA 대역폭(600MB/s)과는 다른 레거시 표현입니다. |
| SATA 포트 수가 부족하면? | PCI Express SATA 확장 카드(ASM1166 등)를 사용하거나, SATA Port Multiplier를 사용합니다. 단, PMP는 대역폭을 공유합니다. |
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.
외부 참고 자료
| 자료 | 설명 |
|---|---|
| AHCI 1.3.1 Specification | Intel의 공식 AHCI 스펙 문서 (레지스터 상세, 동작 모델) |
| Serial ATA Revision 3.5a | SATA-IO의 공식 SATA 프로토콜 스펙 |
| ATA/ATAPI Command Set (ACS-4) | T13 위원회의 ATA 명령어 셋 표준 |
| SCSI/ATA Translation (SAT-5) | T10 위원회의 SAT 표준 |
| drivers/ata/ (커널 소스) | libata, ahci 드라이버 소스 코드 |
| Documentation/ABI/testing/sysfs-ata | 커널 sysfs ATA 인터페이스 문서 |
| smartmontools.org | smartctl/smartd 공식 문서와 디바이스 데이터베이스 |
추천 커널 소스 읽기 순서
libata/AHCI 서브시스템을 이해하기 위해 다음 순서로 소스 코드를 읽는 것을 권장합니다.
| 순서 | 파일 | 핵심 내용 |
|---|---|---|
| 1 | include/linux/libata.h | 핵심 구조체 정의: ata_host, ata_port, ata_device, ata_queued_cmd. 전체 아키텍처의 뼈대 |
| 2 | include/linux/ata.h | ATA 레지스터, 명령 코드, 상태/에러 비트 상수. ATA 프로토콜 레퍼런스 |
| 3 | drivers/ata/libata-core.c | libata 코어: 디바이스 감지, IDENTIFY 처리, 전송 모드 설정, 모듈 초기화 |
| 4 | drivers/ata/libata-scsi.c | SCSI-ATA Translation(SAT): SCSI CDB를 ATA 명령으로 변환, scsi_host_template |
| 5 | drivers/ata/ahci.h | AHCI 레지스터 정의, 포트 구조체, CAP/GHC 비트 상수 |
| 6 | drivers/ata/libahci.c | AHCI 공통 함수: 인터럽트 핸들러, 포트 시작/정지, QC 발행/완료 |
| 7 | drivers/ata/ahci.c | PCI AHCI 드라이버: PCI probe, 칩셋별 quirk, 모듈 등록 |
| 8 | drivers/ata/libata-eh.c | 에러 핸들링: EH 상태 머신, autopsy(원인 분석), 리셋 절차, 속도 다운그레이드 |
관련 RFC/표준 문서
| 표준 | 관리 기관 | 최신 버전 | 핵심 추가 사항 |
|---|---|---|---|
| ATA/ATAPI Command Set | T13 (INCITS) | ACS-6 | ZAC(Zoned), Streams, Concurrent Positioning |
| AT Attachment | T13 | ATA-8 | SATA 통합, NCQ, 48-bit LBA 확장 |
| SCSI/ATA Translation | T10 (INCITS) | SAT-5 | ATA passthrough 개선, ZBC/ZAC 매핑 |
| Serial ATA | SATA-IO | Revision 3.5a | DevSleep, MDAT(Multilink), 하이브리드 정보 |
| AHCI | Intel | 1.3.1 | FBS(FIS-Based Switching), Enclosure Management |
| SFF-8xxx | SFF Committee | SFF-8482 등 | 커넥터/케이블 물리 규격 |
sata-io.org에서 회원 가입 후 무료 열람 가능합니다.
T13/T10 표준은 INCITS(incits.org)에서 구매하거나, 공개된 드래프트 버전을 t13.org/t10.org에서 다운로드할 수 있습니다.
Intel AHCI 스펙은 인터넷에서 PDF로 무료 배포됩니다.