blamie/EntropyVerify. 저장장치(SSD·USB·SD카드)에 하드웨어 가속 AES-CTR 난수를 디스크 끝까지 써넣은 뒤 전부 다시 읽어, 한 블록이라도 깨졌으면 BLAKE3 체크섬으로 잡아내는 스토리지 검증 도구다. 낡고 느린 H2testw(윈도우 전용·단일 스레드)의 현대적 대안으로, 실제 용량 사기(가짜 용량 USB) 탐지와 PCIe 4.0/5.0 NVMe의 한계 속도 측정을 동시에 한다. 비결은 셋이다 — ① 하드웨어 AES-NI로 7GB/s 난수 생성, ② Direct I/O + 비동기 엔진(리눅스 io_uring · 윈도우 IOCP)으로 OS 캐시를 건너뛴 실측, ③ 코어 피닝 멀티스레드 + ratatui TUI 대시보드로 실시간 처리량 표시. (저장소: blamie/EntropyVerify · 언어 Rust 100% · 약 4,150줄 · 26개 모듈 · v1.0.0 / 2026-06-23 릴리스 · 라이선스 AGPL-3.0 · ★16 신생 — TrendShift 라이브 멘션, r/rust 화제)
"드라이브를 난수로 가득 채웠다가 다시 읽어 본다 — 다 똑같이 돌아오면 진짜, 하나라도 다르면 가짜이거나 고장."
인터넷에는 "1TB"라고 적혀 있지만 실제로는 64GB짜리 칩에 펌웨어만 1TB로 속여 놓은 가짜 USB·SD카드가 넘칩니다. 64GB를 넘겨 쓴 데이터는 조용히 버려지거나 앞 데이터를 덮어쓰죠. 이걸 잡는 고전적 방법이 "디스크를 가득 채웠다가 되읽어 보는" 것인데, 단순히 0으로 채우면 SSD 컨트롤러가 "어차피 0이니까"라며 압축·중복제거로 속여 통과시켜 버립니다. EntropyVerify는 그래서 AES 암호로 만든 진짜 난수를 씁니다 — 압축이 안 되니 컨트롤러가 꼼수를 못 부리고, 실제로 매 바이트가 물리 셀에 기록됩니다. 그리고 되읽을 때 BLAKE3 해시로 한 블록 한 블록 대조해, 깨진 위치를 정확히 짚습니다. 이 모든 걸 Rust 단일 바이너리가 NVMe의 한계 속도(7GB/s+)로, 터미널 대시보드를 띄운 채 해냅니다.
좀 더 구체적으로, EntropyVerify는 Rust로 만든 명령줄(CLI) 도구이자 터미널 UI 앱입니다. entropy_verify --target-dir E:\test처럼 검사할 드라이브를 지정하면, 디스크의 빈 공간을 1GiB짜리 파일 여러 개로 나눠 각 파일을 2MiB 블록 단위로 AES-CTR 난수로 채워 씁니다(쓰기 단계). 그다음 그 파일들을 전부 다시 읽어, 각 블록의 난수를 같은 공식으로 재생성해 BLAKE3 해시가 일치하는지 대조합니다(검증 단계). 끝나면 마크다운 리포트(쓰기/읽기 속도, 피크 처리량, 깨진 블록 위치)를 드라이브에 남깁니다. 인자 없이 더블클릭하면 화살표로 드라이브를 고르는 대화형 TUI가 뜹니다.
이 문서가 파고드는 건 사용법만이 아니라 그 안의 시스템 프로그래밍입니다 — "데이터를 저장도 안 하는데 어떻게 검증하나(결정론적 난수)", "왜 버퍼를 4096바이트에 맞춰야 하나(Direct I/O)", "io_uring과 IOCP는 무엇이고 왜 7GB/s가 나오나". 암호·해시·비동기 I/O·무잠금 동시성·Rust unsafe를 한 레포에서 실물로 배우고 싶은 사람에게 드물게 좋은 교본입니다.
"20년 묵은 H2testw를, 현대 하드웨어(AES-NI·io_uring·NVMe)를 제대로 쓰는 Rust로 다시 썼다."
저장장치 검사에는 두 가지 고질적 문제가 있습니다. 하나는 가짜 용량(fake flash) — 싸구려 USB·SD카드가 펌웨어로 용량을 부풀려 파는 사기입니다. 다른 하나는 검사 도구가 너무 느린 것 — 표준 도구 H2testw는 단일 스레드 동기 I/O라, 7GB/s를 내는 PCIe 5.0 SSD를 검사하는 데 답답할 만큼 오래 걸립니다.
드라이브를 0이나 단순 패턴으로 채워 검사하면, 똑똑한 SSD 컨트롤러가 압축·중복제거로 통과시켜 버립니다 — 실제로 셀에 안 쓰고도 "다 썼다"고 보고하죠. 그러면 가짜 용량도, 약한 셀도 못 잡습니다. 또 OS의 파일 캐시 때문에 "방금 쓴 걸 메모리에서 되읽어" 디스크를 건드리지도 않고 통과하는 일도 흔합니다.
EntropyVerify는 AES-CTR로 만든 통계적 완전 난수를 써서 압축·중복제거를 무력화하고, Direct I/O(리눅스 O_DIRECT · 윈도우 FILE_FLAG_NO_BUFFERING)로 OS 캐시를 통째로 건너뛰어 진짜 디스크 속도를 잽니다. 게다가 난수를 (파일번호, 블록번호)에서 결정론적으로 재생성하므로, 기댓값을 따로 저장할 필요 없이 되읽으며 즉석 검증합니다.
이 프로젝트가 r/rust에서 화제가 된 이유는 단순한 재구현이 아니라 하드웨어의 모든 가속 경로를 동원했기 때문입니다.
| 가속 포인트 | 무엇을 | 효과 |
|---|---|---|
| AES-NI | CPU 내장 AES 명령으로 난수 생성 | 단일 코어 >7GB/s — 디스크가 병목이 되게 함 |
| BLAKE3 | AVX2/SSE 벡터화 해시 | ~2바이트/사이클, 검증이 쓰기 속도를 못 따라가지 않게 |
| io_uring / IOCP | 커널 비동기 I/O | 큐를 가득 채워(QD32) NVMe 병렬성 활용 |
| Direct I/O | OS 페이지 캐시 우회 | 캐시 착시 없는 실측 속도·진짜 셀 검증 |
| 코어 피닝 | 워커 스레드를 전용 코어에 고정 | 캐시 지역성↑·컨텍스트 스위치↓, 코어 0은 UI 전용 |
H2testw = 양동이 하나로 수영장을 채우는 사람. 방법은 옳지만 양동이가 하나(단일 스레드)라, 소방 호스(NVMe)가 있어도 그 속도를 못 냅니다.
EntropyVerify = 코어마다 전용 소방 호스를 잡은 팀. 각 호스(워커 스레드)가 자기 수도꼭지(CPU 코어)에 고정돼, 압축 안 되는 물(AES 난수)을 캐시 우회 직배관(Direct I/O)으로 동시에 들이붓습니다. 그래서 디스크의 한계 속도가 그대로 드러납니다.
| 관점 | EntropyVerify | H2testw | dd / badblocks |
|---|---|---|---|
| 검사 데이터 | AES-CTR 난수(압축 불가) | 의사난수(느림) | 0/패턴 또는 난수 |
| 병렬성 | 멀티스레드 + 비동기 QD | 단일 스레드 동기 | 대체로 단일 스트림 |
| 캐시 우회 | Direct I/O 기본 | 제한적 | 옵션(oflag=direct) |
| 무결성 확인 | 블록별 BLAKE3 | 전체 비교 | badblocks만 부분 지원 |
| 플랫폼 | 윈도우·리눅스 | 윈도우 전용 | 리눅스 위주 |
| UI | 실시간 TUI 대시보드 | 단순 진행바 | 거의 없음 |
| 안전장치 | 시스템 파티션 쓰기 차단 | 수동 주의 | 매우 위험(오타 한 번에 파괴) |
냉정히 보면 EntropyVerify는 ★16, 커밋 2개, v1.0.0(2026-06-23)의 갓 태어난 레포입니다. 공개 릴리스 바이너리는 윈도우용만 올라와 있고(리눅스는 직접 빌드), 라이선스는 상용 통합이 까다로운 AGPL-3.0입니다. 무엇보다 이 도구는 대상 드라이브를 가득 채워 쓰는 파괴적 작업이므로, 빈 드라이브에만, 안전장치를 믿되 두 번 확인하고 써야 합니다. "성능 수치(7GB/s 등)"는 개발자가 README에 제시한 값으로, 하드웨어에 따라 편차가 큽니다.
"전부 Rust. 외부 서비스·런타임 0. 의존성 목록만 봐도 '하드웨어를 직접 만진다'는 설계 의도가 읽힌다."
EntropyVerify는 데이터베이스도, 네트워크도, 외부 라이브러리 서버도 없는 순수 단일 바이너리입니다. 대신 의존성이 암호·비동기 I/O·동시성·OS 시스템콜에 또렷이 몰려 있어, Cargo.toml이 곧 아키텍처 설명서입니다.
| 크레이트 | 역할 | 한 줄 설명 |
|---|---|---|
| aes 0.8 · ctr 0.9 · cipher 0.4 | 난수 생성 | AES-128을 CTR(카운터) 모드로 돌려 압축 불가능한 키스트림을 생성. .cargo/config.toml의 +aes 플래그로 AES-NI 하드웨어 가속. |
| blake3 1.5 | 무결성 검증 | 각 블록 페이로드의 32바이트 BLAKE3 해시. AVX2/SSE로 자동 벡터화되어 매우 빠름. |
| rand 0.8 | 마스터 키 | 시드 미지정 시 16바이트 암호학적 난수 키를 생성(--seed로 고정 가능 → 재현성). |
[파일번호 4B | 블록번호 4B | 0×8]로 만들어 블록마다 절대 겹치지 않는 고유 난수를 보장합니다.aesenc 등). 소프트웨어로 수십 사이클 걸릴 AES 한 라운드를 한 명령에 처리해, 단일 코어로도 7GB/s가 넘는 키스트림이 나옵니다. EntropyVerify는 이걸 컴파일 시점에 강제(target-feature=+aes)하기 때문에, 역설적으로 AES-NI 없는 옛 CPU에선 바이너리가 아예 실행되지 않습니다.| 크레이트 | 플랫폼 | 설명 |
|---|---|---|
| io-uring 0.6 · libc | Linux | io_uring으로 커널 비동기 I/O. 실패하면(커널 <5.6 등) pread/pwrite 동기 폴백. O_DIRECT·posix_memalign·fdatasync도 libc로 직접 호출. |
| windows-sys 0.52 | Windows | IOCP(I/O Completion Ports) + FILE_FLAG_NO_BUFFERING/WRITE_THROUGH. 페이지 정렬 버퍼는 VirtualAlloc로 확보. |
| 크레이트 | 역할 | 설명 |
|---|---|---|
| crossbeam-channel 0.5 | 스레드 간 통신 | 워커→메인 이벤트 채널(에러·손상 보고)과 메인→워커 작업 큐(파일 번호 분배). |
| core_affinity 0.8 · num_cpus | 코어 피닝 | 물리 코어 수를 감지해 워커를 전용 코어에 고정, 코어 0은 TUI에 양보. |
| ratatui 0.29 · crossterm | 터미널 UI | 실시간 처리량 스파크라인·진행바·드라이브 선택 메뉴를 그리는 대시보드. |
| bytemuck 1 | 제로카피 직렬화 | 64바이트 블록 헤더 구조체를 복사·변환 없이 바이트로 보고/되돌림(Pod/Zeroable). |
| clap 4 (derive) | CLI 파싱 | --target-dir·--block-size·--threads 등 인자를 구조체로 선언. |
| sysinfo · ctrlc · chrono · anyhow | 유틸 | 드라이브 정보 조회 · Ctrl-C 우아한 종료 · 타임스탬프 · 에러 처리. |
릴리스 프로파일과 빌드 스크립트도 성능 철학을 드러냅니다.
# Cargo.toml — release 프로파일 [profile.release] opt-level = 3 # 최고 최적화 lto = "thin" # 링크 시점 최적화(크레이트 경계 너머 인라인) codegen-units = 1 # 병렬 컴파일 포기하고 최적화 품질↑ strip = true # 심볼 제거 → 작은 바이너리 panic = "abort" # 패닉 시 되감기 없이 즉시 종료(빠르고 작음)
.cargo/config.toml은 x86_64에 target-feature=+aes,+sse4.1을 박아 AES-NI·SSE4.1을 컴파일 시점에 켜고, build.rs는 타깃 아키텍처/OS를 감지해 cfg 플래그(has_aes_ni 등)를 내보냅니다. 즉 "이 바이너리는 현대 x86_64 + AES-NI를 가정하고 빌드된다"는 선언입니다.
"두 개의 막 — 쓰기(난수를 들이붓는다)와 검증(되읽어 BLAKE3로 대조한다). 그 사이를 원자적 카운터와 채널이 잇는다."
핵심은 상태기계 + 워커 풀 + 무잠금 통신입니다. 앱 전체가 Idle → Writing → Verifying → Complete/Failed 다섯 단계를 오가고, 무거운 일은 워커 스레드들이, 화면은 메인 스레드가 맡습니다. 아래에서 설계 포인트를 하나씩 봅니다.
가장 영리한 절약입니다. 보통 "쓴 걸 검증"하려면 기댓값을 어딘가 저장해 둬야 하는데, 그러면 검사 데이터만큼의 저장공간이 또 필요합니다. EntropyVerify는 난수를 (파일번호, 블록번호)에서 결정론적으로 생성하므로, 검증 때 같은 좌표로 같은 난수를 다시 만들어 비교하면 됩니다 — 기댓값 저장이 0입니다.
// crypto/datagen.rs — 논스를 좌표에서 유도 fn derive_nonce(file_index: u32, block_index: u32) -> [u8; 16] { let mut nonce = [0u8; 16]; nonce[0..4].copy_from_slice(&file_index.to_le_bytes()); nonce[4..8].copy_from_slice(&block_index.to_le_bytes()); // 8..16 바이트는 0 — CTR 카운터 공간 nonce } // 0으로 채운 버퍼에 키스트림을 XOR → 결과 = 순수 난수 buf.fill(0); cipher.apply_keystream(buf);
구구단을 통째로 외워 적어 두는(=기댓값 저장) 대신, "몇 단 몇 번?"이라는 좌표만 받으면 즉석에서 답을 계산하는 것과 같습니다. 답을 보관할 공책이 필요 없죠. (파일번호, 블록번호)가 좌표, AES-CTR이 계산 규칙입니다.
각 2MiB 블록은 앞 64바이트가 구조화된 헤더(매직 EVFY·버전·스레드ID·파일/블록번호·AES 논스·BLAKE3 해시)이고 나머지가 난수 페이로드입니다. 헤더 구조체는 #[repr(C)] + bytemuck::Pod라, 복사·직렬화 라이브러리 없이 메모리 표현을 그대로 바이트로 보고 되돌립니다(제로카피).
// engine/block.rs #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] pub struct BlockHeader { pub magic: [u8; 4], // "EVFY" pub version: u8, pub _pad: u8, pub thread_id: u16, pub file_index: u32, pub block_index: u32, pub aes_nonce: [u8; 16], // 이 블록을 만든 논스 pub blake3_hash: [u8; 32],// 페이로드의 BLAKE3 } // 컴파일 시점에 크기를 못박는다 — 64B가 아니면 빌드 실패 const _: () = assert!(std::mem::size_of::<BlockHeader>() == 64);
저 컴파일 타임 단언(const _: () = assert!(…))이 백미입니다 — 구조체 필드를 잘못 건드려 크기가 틀어지면 실행이 아니라 컴파일에서 잡힙니다. 디스크 포맷처럼 "바이트 한 칸도 틀리면 안 되는" 곳에 딱 맞는 안전장치입니다.
Direct I/O를 쓰려면 버퍼가 4096바이트 경계에 정렬돼야 합니다. 평범한 Vec<u8>은 그 보장이 없어, EntropyVerify는 OS별로 정렬 메모리를 직접 할당하는 AlignedBuffer를 만들었습니다 — 윈도우는 VirtualAlloc, 리눅스는 posix_memalign. 그리고 Deref<Target=[u8]>를 구현해, 쓸 때는 보통 슬라이스처럼 다루게 했습니다.
AlignedBuffer는 이 raw 포인터를 소유·해제(Drop)까지 책임지는 작은 RAII 래퍼로, Rust에서 unsafe를 안전한 추상 뒤에 가두는 전형적 패턴입니다(unsafe impl Send/Sync로 스레드 이동 허용).리눅스 엔진은 io_uring으로 "쓰기/읽기 요청을 제출(submit)하고, 완료(completion)를 나중에 회수"하는 구조입니다. 슬롯(버퍼+상태) 풀을 만들어 큐 깊이(QD)만큼 동시에 띄웁니다. io_uring 초기화가 실패하면 조용히 pread/pwrite 동기 모드로 폴백합니다 — 구형 커널 호환.
// engine/io/linux.rs — io_uring 초기화와 폴백
let inner = match io_uring::IoUring::builder().build(queue_depth) {
Ok(ring) => EngineInner::IoUring(ring),
Err(e) => { eprintln!("io_uring 실패({}), 동기 I/O로 폴백", e);
EngineInner::Synchronous }
};
검증 단계의 파이프라이닝이 특히 교과서적입니다 — 먼저 QD개 읽기를 미리 제출해 두고, 완료가 하나 돌아올 때마다 그 블록을 검증하고 즉시 다음 읽기를 제출합니다. 그래서 디스크는 항상 "할 일이 가득"이라 쉬지 않습니다.
워커들이 1초에 수만 블록을 쓰는데, 진행률 표시를 위해 매번 락을 잡으면 그 자체가 병목입니다. EntropyVerify는 락을 거의 안 씁니다:
| 진행 카운터 | AtomicU64에 fetch_add(Relaxed) — 워커는 더하고, TUI는 읽기만. 락 0. |
| 이벤트 보고 | crossbeam-channel로 에러·손상 블록을 메인에 비동기 전송. |
| 일시정지 | (Mutex<bool>, Condvar) — [P] 키로 워커를 재웠다 깨움. |
| 중단 | Arc<AtomicBool> 플래그 — 워커들이 루프마다 확인. |
// 워커는 그냥 더한다 — 잠금 없음 cfg.metrics.bytes_written.fetch_add(block_size, Ordering::Relaxed); // TUI는 그냥 읽는다 — 50ms마다 처리량 환산 let delta = current.saturating_sub(last); self.current_throughput = delta as f64 / elapsed;
파괴적 도구이므로 다중 가드레일을 둡니다. 대상이 OS 루트 파티션(C:\·/)이면 거부, 리눅스의 /boot·/etc·/usr·/var·스왑 같은 임계 마운트도 차단, 그리고 실제로 쓰기 전 센티넬 파일(.entropy_verify_sentinel)을 만들었다 지워 쓰기 권한을 능동 확인합니다. 디스크는 100MiB 안전 여유를 남겨 100%까지 채우지 않습니다.
이 셋이 EntropyVerify 전체를 관통합니다 — 결정론적 난수(기댓값 저장 0), Direct I/O + 비동기 큐(캐시 착시 0·디스크 포화), 원자적 카운터 + 채널(잠금 경합 0). 거기에 제로카피 헤더와 코어 피닝이 더해져, "단일 바이너리가 NVMe를 끝까지 밀어붙이는" 그림이 완성됩니다.
"src/ 하나에 26개 모듈. 역할별로 폴더가 깔끔히 갈려 있어, 폴더 이름만 봐도 '암호·엔진·I/O·동시성·안전·UI'의 부품 목록이 보인다."
engine/io/mod.rs가 흥미로운데, #[cfg(target_os = "linux")]로 OS에 따라 다른 엔진 타입을 골라 PlatformEngine이라는 같은 이름에 묶습니다. 그래서 상위 코드(writer/verifier)는 OS를 신경 쓰지 않고 PlatformEngine::new(...)만 부르면 됩니다 — 런타임 분기 0, 전부 컴파일 시점에 결정되어 그 플랫폼 코드만 바이너리에 들어갑니다.| 묶음 | 모듈 | 하는 일 |
|---|---|---|
| 조율·상태 | main.rs, app.rs, config.rs, units.rs | 진입점·단계 전이(상태기계)·CLI/계획·단위 변환. 무게중심. |
| 암호·해시 | crypto/datagen.rs, crypto/hasher.rs | AES-CTR 난수 + BLAKE3 — 검사 데이터의 생성·검증 핵심. |
| 엔진(블록) | engine/block.rs, writer.rs, verifier.rs | 64B 헤더 포맷 + 쓰기/검증 워커 루프(파이프라인). |
| 엔진(I/O) | engine/io/{aligned,linux,windows,mod}.rs | 정렬 버퍼 + io_uring/IOCP 비동기 엔진 + 플랫폼 디스패치. |
| 동시성 | threading/affinity.rs | 물리 코어 감지 + 워커 코어 피닝(코어 0은 TUI 양보). |
| 안전 | safety/guardrails.rs, platform.rs | 시스템 파티션·임계 마운트 차단 + 쓰기권한 센티넬 검사. |
| UI·리포트 | tui/*.rs, report/markdown.rs | 실시간 대시보드·드라이브 선택 + 마크다운 결과 리포트. |
코어 로직에서 engine/io/windows.rs(369)·linux.rs(303)가 가장 큰 건, "이 프로젝트의 진짜 난이도는 암호가 아니라 OS별 비동기 I/O에 있다"는 뜻입니다(암호는 라이브러리가 해 주지만, IOCP·io_uring·정렬·DMA는 직접 다뤄야 함). main.rs(412)·app.rs(368)가 큰 건 단계 조율과 상태/메트릭 관리가 그만큼 촘촘하다는 뜻이고, tui/(660여 줄)가 큰 건 "보기 좋은 실시간 대시보드"에 생각보다 코드가 많이 든다는 현실을 보여 줍니다.
"한 레포 안에 대칭암호·해시·Direct I/O·비동기 커널 API·무잠금 동시성·제로카피·Rust unsafe가 전부 실물로 들어 있다."
배울 것: 블록 암호(AES)를 CTR 모드로 돌려 스트림 암호(=난수 생성기)로 쓰는 핵심 발상, 논스/카운터 설계, "0에 XOR하면 키스트림이 그대로 나온다"는 트릭. 그리고 +aes 타깃 피처로 AES-NI를 켜는 법과 그 대가(구형 CPU 비호환).
실습: 파이썬 cryptography나 Rust aes/ctr로 "카운터를 1씩 올리며 16바이트씩 암호화"해 난수 1MB를 만들고, gzip으로 압축이 거의 안 되는지 확인 = "압축 불가능 데이터"를 직접 체감.
배울 것: 데이터 손상을 잡는 체크섬/해시의 역할, BLAKE3가 왜 빠른지(트리 구조 + SIMD 벡터화), "보안 비교가 아니니 상수시간 비교가 필요 없다"는 실용적 판단(hasher.rs 주석).
실습: 1MB 데이터의 BLAKE3 해시를 구한 뒤 한 바이트만 뒤집어 다시 해시 → 완전히 달라지는 걸 확인(쇄도 효과). verify_block이 손상을 잡는 원리와 동일.
배울 것: OS 페이지 캐시가 벤치마크를 어떻게 속이는지, O_DIRECT/FILE_FLAG_NO_BUFFERING으로 그걸 우회하는 법, 그리고 그 대가인 정렬 요구를 posix_memalign/VirtualAlloc로 푸는 법. RAII로 raw 포인터를 안전하게 감싸는 AlignedBuffer 패턴.
실습: 리눅스에서 같은 파일을 일반 쓰기 vs O_DIRECT 쓰기로 각각 측정해 속도 차이를 비교. 정렬 안 된 버퍼로 O_DIRECT 쓰기를 시도해 EINVAL 에러를 직접 만나 보기.
배울 것: "제출하고 나중에 완료를 회수"하는 비동기 모델, SQ/CQ 링과 user_data로 완료를 슬롯에 되짚는 법, 큐를 항상 가득 채우는 파이프라이닝(검증 루프가 본보기), 그리고 우아한 폴백(구형 커널엔 동기 pread/pwrite).
실습: io-uring 크레이트로 파일 하나를 QD4로 읽어 보기 — read 4개 제출 → 완료 회수 → 또 제출. "큐 깊이를 1로 줄이면 느려지는가"를 측정해 NVMe 병렬성을 체감.
배울 것: 핫패스에서 락을 피하는 법 — AtomicU64::fetch_add(Relaxed)로 카운터를 공유하고, crossbeam 채널로 이벤트를 흘리고, Condvar로 일시정지, AtomicBool로 중단. 그리고 core_affinity로 스레드를 코어에 고정해 캐시 지역성을 살리는 법.
실습: 스레드 4개가 공유 카운터를 1억 번 증가시킬 때 Mutex vs AtomicU64의 속도를 비교. core_for_worker처럼 "코어 0은 비우고 1..N에 배정"하는 라운드로빈을 짜 보기.
배울 것: 구조체를 복사 없이 바이트로 보고 되돌리는 bytemuck::Pod, 메모리 레이아웃을 고정하는 #[repr(C)], 그리고 const _: () = assert!(size_of::<T>() == N)로 디스크 포맷 크기를 컴파일에 못박는 기법.
실습: #[repr(C)] 구조체를 만들고 bytemuck::bytes_of로 바이트를 출력해 필드가 어디에 놓이는지(패딩 포함) 확인. 필드 하나를 키워 컴파일타임 단언이 깨지는 걸 직접 보기.
배울 것: libc·windows-sys로 OS 시스템콜 직접 호출(FFI), raw 포인터를 Drop으로 안전하게 해제, unsafe impl Send/Sync의 의미와 책임, 그리고 lto·codegen-units=1·panic="abort" 같은 릴리스 최적화 다이얼.
실습: libc::posix_memalign으로 정렬 메모리를 할당했다가 Drop에서 free하는 작은 래퍼를 만들어 보기. panic="abort" 유무로 바이너리 크기가 어떻게 변하는지 비교.
배울 것: ratatui로 실시간 대시보드를 그리는 법 — 처리량 스파크라인, 진행바, ETA, 그리고 UI 스레드와 워커 스레드의 분리(코어 0 전용). 50ms 샘플링으로 떨림(jitter)을 줄이는 처리량 계산도.
실습: ratatui로 "1초에 한 번 난수 값을 스파크라인에 밀어넣는" 미니 대시보드를 만들어 보기. app.rs의 update_metrics처럼 "최근 120 샘플만 유지"하는 링버퍼를 구현.
"클라우드·GPU·DB·네트워크 전부 불필요. 대신 'AES-NI 있는 x86_64 CPU'와 '검사할 빈 드라이브'가 필수다."
| 항목 | 요구사항 / 메모 |
|---|---|
| CPU | AES-NI 지원 x86_64가 사실상 필수 — .cargo/config.toml이 +aes를 컴파일에 박아, AES-NI 없는 2010년 이전 CPU에선 바이너리가 실행되지 않습니다. 물리 코어가 많을수록 워커 병렬성↑. |
| 빌드 | Rust 1.75+ 툴체인으로 cargo build --release. 결과는 단일 실행파일. |
| OS | 윈도우(IOCP) / 리눅스(io_uring, 커널 5.6+ 권장 — 구형은 pread/pwrite 폴백). 그 외 OS는 표준 정렬 할당 폴백 경로. |
| 외부 서비스 | 없음. DB·네트워크·클라우드·GPU 전부 불필요. 100% 로컬·오프라인. |
| 대상 저장소 | 검사할 빈 드라이브(USB·SD·SSD·HDD). 시스템 파티션은 안전장치가 거부. 100MiB 안전 여유를 남김. |
| 메모리(RAM) | 가볍다 — 워커 수 × 큐깊이 × 블록크기의 정렬 버퍼만 상주(기본 8워커 × QD32 × 2MiB ≈ 수백 MB 수준, 설정으로 조절). |
| 주요 옵션 | --block-size(기본 2MiB)·--file-size(기본 1GiB)·--queue-depth(기본 32)·--threads·--write-only/--verify-only·--seed(재현용 고정 키). |
| 산출물 | 대상 볼륨에 ev_chunk_*.bin 청크 파일 + entropy_verify_report_YYYYMMDD_HHMMSS.md 리포트. 100% 통과 시 청크 자동 삭제, 손상 시 분석용으로 보존. |
| 속도(개발자 제시) | AES-CTR 생성 단일코어 >7GB/s, BLAKE3 >6GB/s — 디스크가 병목이 되도록 설계. 실제 처리량은 드라이브 성능에 좌우. |
대시보드 실행 중 [Tab] 단위 전환(GB↔GiB), [P] 일시정지/재개(Condvar로 워커 재움), [Q] 우아한 중단(진행 중 블록 마무리 → 버퍼 flush → 핸들 닫기 → 중단 리포트 저장)을 지원합니다. 인자 없이 더블클릭하면 화살표로 드라이브를 고르는 대화형 선택 메뉴가 먼저 뜹니다.
EntropyVerify는 대상 드라이브의 빈 공간을 가득 채워 씁니다. 중요한 데이터가 있는 드라이브에 쓰면 공간을 잠식하고, 잘못된 대상을 고르면 위험합니다. 안전장치(시스템 파티션·임계 마운트 차단, 센티넬 권한 검사)가 있지만 맹신하지 말고, 반드시 비어 있고 백업된 드라이브에만, 대상 경로를 두 번 확인하고 실행하세요. 특히 새로 산 USB·SD카드의 '가짜 용량'을 검사할 때 가장 유용합니다(어차피 비어 있으니).
"빌드해서 USB 검사 → CTR 난수 직접 만들기 → BLAKE3로 손상 잡기 → 비동기 I/O 맛보기 → 미니 H2testw 만들기."
여분의 USB/SD카드를 꽂고 entropy_verify --target-dir /경로(또는 더블클릭 후 화살표로 선택)로 검사. 끝나면 드라이브에 생긴 entropy_verify_report_*.md를 열어 쓰기/읽기 속도, 피크 처리량, 손상 블록을 확인.
목표: "표기 용량과 검사 용량이 같은가", "읽기가 쓰기보다 빠른가"를 눈으로 확인. 가능하면 의심스러운 싸구려 USB로 가짜 용량을 잡아 보기.
좋아하는 언어로 datagen.rs를 축소 재현 — 논스 = (파일번호, 블록번호)로 만들고, AES-128-CTR 키스트림을 4KB 버퍼에 채운다. 같은 좌표면 항상 같은 난수가 나오는지, 다른 좌표면 다른지 검증.
목표: "기댓값을 저장하지 않고 좌표만으로 재생성"의 핵심을 체득. 생성한 4KB를 gzip해 거의 안 줄어드는 걸로 '압축 불가능성'을 확인.
과제 2의 난수 블록에 64바이트 헤더(매직·번호·BLAKE3 해시)를 붙여 파일로 쓴다. 되읽어 헤더의 해시와 재계산 해시를 대조하는 verify_block을 구현하고, 일부러 한 바이트를 뒤집어 손상이 잡히는지 확인.
목표: engine/block.rs의 prepare/verify 왕복을 그대로 재현. "어느 파일 몇 번 블록이 깨졌다"를 정확히 출력하게 만들어 보기.
리눅스라면 io-uring 크레이트로 큰 파일을 QD개 read를 미리 제출 → 완료마다 처리하고 다음 read 제출 패턴으로 읽어 본다. verifier.rs의 파이프라인이 본보기. 큐 깊이를 1·4·32로 바꿔 처리량을 측정.
목표: "큐를 깊게 채우면 NVMe가 빨라진다"를 수치로 확인. 동기 pread 루프와 비교해 비동기의 이득을 체감.
과제 2~4를 합쳐 작은 검사기를 만든다 — 정렬 버퍼 + Direct I/O로 난수 파일 쓰기 → 멀티스레드로 병렬화 → 되읽어 BLAKE3 검증 → 처리량 출력. 코어 피닝과 AtomicU64 진행 카운터까지 넣으면 구조가 거의 완성된다.
목표: "단일 바이너리가 디스크를 포화시키는" 전체 그림을 손으로 완성. Direct I/O on/off로 측정해 캐시 착시가 얼마나 큰지 직접 증명.
"Rust 시스템 기초에서 출발해 암호·해시·Direct I/O·비동기 커널 API·무잠금 동시성까지, EntropyVerify를 길잡이 삼아."
| 주차 | 주제 | 핵심 학습 + EntropyVerify 연결점 |
|---|---|---|
| 1주 | Rust 시스템 기초 | 소유권·unsafe·Drop·FFI(libc)·cargo 프로파일(lto·panic=abort) → Cargo.toml·aligned.rs |
| 2주 | 대칭암호 + CTR 모드 | 블록암호 vs 스트림암호, CTR 카운터·논스, AES-NI 하드웨어 가속 → crypto/datagen.rs·.cargo/config.toml |
| 3주 | 해시·무결성 | 체크섬·암호학적 해시, BLAKE3 트리/SIMD, 쇄도 효과 → crypto/hasher.rs·engine/block.rs |
| 4주 | 파일 I/O와 Direct I/O | 페이지 캐시, O_DIRECT/NO_BUFFERING, 섹터 정렬, DMA → engine/io/aligned.rs·linux.rs |
| 5주 | 비동기 I/O 커널 API | io_uring(SQ/CQ)·IOCP, 큐 깊이 파이프라인, 우아한 폴백 → engine/io/linux.rs·verifier.rs |
| 6주 | 무잠금 동시성 + TUI | Atomics·채널·Condvar·코어 affinity, ratatui 대시보드 → app.rs·threading/affinity.rs·tui/ |
이 로드맵은 "암호학 + 해시 + 운영체제 I/O + 동시성 프로그래밍"을 한 줄로 꿴 저수준 시스템 코스입니다. 웹·앱 개발이 가려 주던 "디스크·메모리·CPU 명령·커널 시스템콜"의 실제를 EntropyVerify가 작고 읽을 만한 4,150줄로 펼쳐 보여 주기 때문에, 시스템 프로그래밍·Rust를 진지하게 파려는 사람에게 이상적인 교재입니다.
"이 문서와 저장소에서 반복되는 시스템·암호·I/O 용어를 한곳에 모았다."
| 용어 | 의미 |
|---|---|
| 스토리지 검증 | 저장장치가 표기 용량만큼 실제로 담는지 + 쓴 데이터가 손상 없이 읽히는지 확인하는 작업 |
| H2testw | 이 검사의 고전 표준 도구(윈도우·단일 스레드). EntropyVerify가 현대적으로 대체하려는 대상 |
| 가짜 용량 (fake flash) | 펌웨어로 용량을 부풀려 파는 사기 USB/SD. 넘쳐 쓴 데이터는 버려지거나 덮어써짐 |
| 압축 불가능 데이터 | 패턴이 없어 더 못 줄이는 데이터. SSD 컨트롤러의 압축·중복제거 꼼수를 막아 진짜 셀을 검증 |
| AES-128-CTR | AES 블록암호를 카운터(CTR) 모드로 돌려 무한 난수열(키스트림)을 만드는 방식 |
| 키스트림 / 논스(nonce) | 키스트림=암호가 뱉는 난수열. 논스=카운터 시작점. 여기선 (파일번호·블록번호)로 유도해 블록마다 고유 |
| AES-NI | CPU 내장 AES 하드웨어 명령. 단일코어 >7GB/s. 컴파일 시점에 강제되어 구형 CPU 비호환 |
| BLAKE3 | 트리 구조 + SIMD로 매우 빠른 암호학적 해시. 블록 페이로드의 32바이트 무결성 지문 |
| Direct I/O | OS 페이지 캐시를 우회해 버퍼↔디스크 직결. O_DIRECT(리눅스)/FILE_FLAG_NO_BUFFERING(윈도우) |
| 페이지 정렬 버퍼 | 주소가 4096의 배수인 버퍼. Direct I/O의 필수 조건. posix_memalign/VirtualAlloc로 확보 |
| io_uring | 리눅스의 고성능 비동기 I/O. 제출(SQ)·완료(CQ) 링으로 시스템콜·대기 비용을 줄임(커널 5.6+) |
| IOCP | 윈도우의 I/O Completion Ports. io_uring과 같은 "제출 후 완료 회수" 비동기 모델 |
| 큐 깊이 (queue depth) | 동시에 진행 중인 I/O 개수. NVMe는 깊은 큐(예: 32)를 채워야 최대 속도가 남 |
| 파이프라이닝 | 먼저 QD개 I/O를 제출해 두고, 완료마다 처리 + 즉시 새 I/O 제출 → 디스크가 쉬지 않게 |
| 제로카피 직렬화 | 구조체를 복사 없이 바이트로 보고 되돌림. bytemuck::Pod + #[repr(C)] |
| 컴파일타임 단언 | const _: () = assert!(size_of::<T>()==N) — 디스크 포맷 크기를 빌드에서 못박음 |
| 코어 피닝 (affinity) | 스레드를 특정 CPU 코어에 고정해 캐시 지역성↑·이주↓. 코어 0은 TUI에 양보 |
| AtomicU64 / Relaxed | 락 없이 여러 스레드가 공유하는 정수. fetch_add(Relaxed)로 진행률을 싸게 누적 |
| crossbeam-channel | 스레드 간 메시지 통로. 워커→메인 이벤트(에러·손상), 메인→워커 작업 큐 |
| Condvar / AtomicBool | 일시정지(조건변수로 재움·깨움) / 중단(불리언 플래그) 등 워커 제어 신호 |
| ratatui | Rust 터미널 UI 라이브러리. 처리량 스파크라인·진행바·드라이브 선택 대시보드 |
| 센티넬 검사 | 실제 쓰기 전 임시 파일을 만들었다 지워 쓰기 권한을 능동 확인하는 안전장치 |
| 상태기계 (state machine) | Idle→Writing→Verifying→Complete/Failed 단계 전이로 앱 흐름을 관리 |
| panic=abort / LTO | 릴리스 최적화 다이얼 — 패닉 시 즉시 종료(작고 빠름) / 링크 시점 최적화로 인라인 확대 |