SQLite OS 인터페이스 또는 "VFS"

SQLite OS 인터페이스 또는 "VFS" (The SQLite OS Interface)

SQLite 구현 스택의 맨 아래에서 운영체제 간 이식성을 제공하는 OS 포터빌리티 계층 "VFS"에 대해 설명하는 문서예요.

출처: The SQLite OS Interface or "VFS" (sqlite.org)

본문

1. 소개 (Introduction)

이 문서는 SQLite OS 포터빌리티 계층, 즉 "VFS"에 대해 설명해요. VFS는 SQLite 구현 스택의 맨 아래에 있는 모듈로, 운영체제 간 이식성을 제공해요.

2. SQLite의 나머지 부분과의 관계에서 본 VFS

SQLite 라이브러리의 내부 구조는 오른쪽에 보이는 모듈 스택으로 볼 수 있어요. Tokenizer, Parser, Code Generator 구성 요소는 SQL 문을 처리해 가상 머신 언어 또는 바이트 코드의 실행 가능한 프로그램으로 변환하는 데 사용돼요. 대략적으로 말하면 이 위쪽 세 계층이 sqlite3_prepare_v2()를 구현해요. 위쪽 세 계층이 생성한 바이트 코드가 준비된 문이에요. Virtual Machine 모듈은 SQL 문 바이트 코드를 실행하는 역할을 해요. B-Tree 모듈은 데이터베이스 파일을 정렬된 키와 로그 성능을 가진 여러 key/value 저장소로 구성해요. Pager 모듈은 데이터베이스 파일의 페이지를 메모리로 로드하고, 트랜잭션을 구현·제어하고, 충돌이나 정전 후 데이터베이스 손상을 방지하는 저널 파일을 만들고 유지하는 역할을 해요. OS Interface는 SQLite가 다른 운영체제에서 실행되도록 적응시키는 공통 루틴 집합을 제공하는 얇은 추상화 계층이에요. 대략적으로 말하면 아래쪽 네 계층이 sqlite3_step()을 구현해요.

이 문서는 맨 아래 계층에 관한 것이에요.

OS Interface - "VFS"라고도 불리는 - 는 SQLite를 운영체제 간에 이식 가능하게 만드는 요소예요. SQLite의 다른 모듈이 운영체제와 통신해야 할 때마다 VFS의 메서드를 호출해요. 그러면 VFS가 요청을 충족하는 데 필요한 운영체제 특정 코드를 호출해요. 따라서 SQLite를 새 운영체제로 포팅하는 것은 새로운 OS 인터페이스 계층, 즉 "VFS"를 작성하는 문제일 뿐이에요.

3. 여러 개의 VFS (Multiple VFSes)

표준 SQLite 소스 트리에는 unix용과 windows용 내장 VFS가 들어 있어요. 대체 VFS는 sqlite3_vfs_register() 인터페이스를 사용해 시작 시점이나 런타임에 추가할 수 있어요.

여러 VFS를 동시에 등록할 수 있어요. 각 VFS는 고유한 이름을 가져요. 같은 프로세스 안의 서로 다른 데이터베이스 연결이 동시에 서로 다른 VFS를 사용할 수 있어요. 게다가 단일 데이터베이스 연결이 ATTACH 명령으로 여러 데이터베이스 파일을 열고 있다면, 각 연결된(attached) 데이터베이스가 서로 다른 VFS를 사용할 수도 있어요.

3.1. 표준 Unix VFS

Unix 빌드에는 여러 내장 VFS가 들어 있어요. unix의 기본 VFS는 "unix"라고 불리며 대부분의 응용 프로그램에서 사용돼요. (컴파일 타임 옵션에 따라) unix에서 찾을 수 있는 다른 VFS는 다음과 같아요:

  1. unix-dotfile - POSIX 권고 잠금(advisory lock) 대신 dot-file 잠금을 사용해요.

  2. unix-excl - 데이터베이스 파일에 배타적 잠금을 얻고 유지해서 다른 프로세스가 데이터베이스에 접근하지 못하게 해요. 또한 wal-index를 공유 메모리가 아닌 힙(heap)에 유지해요.

  3. unix-none - 모든 파일 잠금 연산이 no-op이에요.

  4. unix-namedsem - 파일 잠금에 이름 있는 세마포어(named semaphore)를 사용해요. VXWorks 전용이에요.

다양한 unix VFS는 파일 잠금을 처리하는 방식에서만 차이가 있어요. 구현 대부분을 서로 공유하며 모두 같은 SQLite 소스 파일(os_unix.c)에 있어요. "unix"와 "unix-excl"을 제외한 다양한 unix VFS는 모두 호환되지 않는 잠금 구현을 사용한다는 점에 주의하세요. 두 프로세스가 서로 다른 unix VFS를 사용해 같은 SQLite 데이터베이스에 접근하면, 서로의 잠금을 보지 못해 서로 간섭하게 되어 데이터베이스 손상이 발생할 수 있어요. 특히 "unix-none" VFS는 전혀 잠금을 하지 않으므로, 두 개 이상의 데이터베이스 연결이 동시에 사용하면 쉽게 데이터베이스 손상이 생겨요. 강력한 이유가 없는 한 프로그래머는 "unix"나 "unix-excl"만 사용하도록 권장해요.

3.2. 표준 Windows VFS

Windows 빌드에도 여러 내장 VFS가 들어 있어요. 기본 Windows VFS는 "win32"라고 불리며 대부분의 응용 프로그램에서 사용돼요. Windows 빌드에서 찾을 수 있는 다른 VFS는 다음과 같아요:

  1. win32-longpath - "win32"와 같지만 경로 이름이 최대 65534바이트까지 가능해요. ("win32"에서는 경로 이름이 1040바이트에서 최대예요.)

  2. win32-none - 모든 파일 잠금 연산이 no-op이에요.

  3. win32-longpath-none - "win32-longpath"와 "win32-none"의 조합이에요. 긴 경로 이름을 지원하고 모든 잠금 연산이 no-op이에요.

unix와 마찬가지로 다양한 Windows VFS의 코드 대부분은 공유돼요.

3.3. 어떤 VFS를 사용할지 지정하기 (Specifying Which VFS To Use)

항상 기본(default) VFS인 하나의 VFS가 있어요. unix 시스템에서는 "unix" VFS가 기본이 되고, windows에서는 "win32"가 기본이 돼요. 다른 조치를 취하지 않으면 새 데이터베이스 연결은 기본 VFS를 사용해요.

기본 VFS는 sqlite3_vfs_register() 인터페이스에 두 번째 매개변수 1을 주어 등록 또는 재등록함으로써 바꿀 수 있어요. 따라서 (unix) 프로세스가 "unix" 대신 항상 "unix-nolock" VFS를 사용하려면 다음 코드가 동작할 거예요:

sqlite3_vfs_register(sqlite3_vfs_find("unix-nolock"), 1);

대체 VFS는 sqlite3_open_v2() 함수의 4번째 매개변수로도 지정할 수 있어요. 예를 들어:

int rc = sqlite3_open_v2("demo.db", &db, SQLITE_OPEN_READWRITE, "unix-nolock");

마지막으로, URI 파일 이름이 활성화되었다면 URI의 "vfs=" 매개변수를 사용해 대체 VFS를 지정할 수 있어요. 이 기법은 sqlite3_open(), sqlite3_open16(), sqlite3_open_v2()에서, 그리고 새 데이터베이스가 기존 데이터베이스 연결에 ATTACH될 때 동작해요. 예를 들어:

ATTACH 'file:demo2.db?vfs=unix-none' AS demo2;

URI로 지정된 VFS가 가장 높은 우선순위를 가져요. 그다음이 sqlite3_open_v2()의 네 번째 인자로 지정된 VFS예요. 달리 지정된 VFS가 없으면 기본 VFS가 사용돼요.

3.4. VFS Shim

SQLite 스택의 상위 계층 관점에서 각 열린 데이터베이스 파일은 정확히 하나의 VFS를 사용해요. 하지만 실제로 특정 VFS는 실제 작업을 하는 다른 VFS 위의 얇은 래퍼일 수도 있어요. 우리는 래퍼 VFS를 "shim"이라고 불러요.

shim의 간단한 예는 "vfstrace" VFS예요. 이것은 (vfstrace.c 소스 파일에 구현된) VFS로, 각 VFS 메서드 호출과 관련된 메시지를 로그 파일에 쓴 다음, 실제 작업을 하도록 다른 VFS로 제어권을 넘겨요.

3.5. 기타 예제 VFS (Other Example VFSes)

공개 SQLite 소스 트리에서 사용할 수 있는 다른 VFS 구현은 다음과 같아요:

  • appendvfs.c - 이 VFS는 SQLite 데이터베이스를 다른 파일의 끝에 덧붙일 수 있게 해줘요. 예를 들어 실행 파일 끝에 SQLite 데이터베이스를 덧붙여, 실행될 때 덧붙여진 데이터베이스를 쉽게 찾을 수 있게 하는 데 사용할 수 있어요. 명령줄 셸은 --append 옵션으로 시작하면 이 VFS를 사용하고, .archive 명령은 --append 플래그가 주어지면 이 VFS를 사용해요.

  • test_demovfs.c - 이 파일은 open(), read(), write(), fsync(), close(), sleep(), time() 같은 POSIX 함수를 사용하는 "demo"라는 매우 단순한 VFS를 구현해요. 이 VFS는 unix 시스템에서만 동작해요. 하지만 unix 플랫폼에서 기본으로 사용되는 표준 "unix" VFS를 대체하기 위한 것은 아니에요. "demo" VFS는 의도적으로 매우 단순하게 유지되어, 학습 도구이자 다른 VFS를 만들거나 SQLite를 새 운영체제로 포팅하기 위한 템플릿으로 사용될 수 있어요.

  • test_quota.c - 이 파일은 데이터베이스 파일 모음에 누적 파일 크기 제한을 적용하는 "quota"라는 shim을 구현해요. 보조 인터페이스가 "쿼터 그룹(quota group)"을 정의하는 데 사용돼요. 쿼터 그룹은 이름이 모두 GLOB 패턴과 일치하는 파일 집합(데이터베이스 파일, 저널, 임시 파일)이에요. 각 쿼터 그룹에 있는 모든 파일 크기의 합이 추적되고, 그 합이 쿼터 그룹에 정의된 임계값을 초과하면 콜백 함수가 호출돼요. 그 콜백은 임계값을 올리거나, 쿼터를 초과했을 작업을 SQLITE_FULL 오류로 실패시킬 수 있어요. 이 shim의 용도 중 하나는 Firefox에서 응용 프로그램 데이터베이스에 리소스 제한을 적용하는 것이에요.

  • test_multiplex.c - 이 파일은 데이터베이스 파일이 기본 파일 시스템의 최대 파일 크기를 초과할 수 있게 해주는 shim을 구현해요. 이 shim은 SQLite의 위쪽 여섯 계층에 인터페이스를 제공하는데, 실제로는 각각의 큰 파일이 기본 시스템에서 많은 작은 파일로 나뉘어 있음에도 매우 큰 파일을 사용하는 것처럼 보이게 해요. 예를 들어 이 shim은 FAT16 파일 시스템에서 데이터베이스가 2기가바이트보다 커질 수 있게 하는 데 사용된 적이 있어요.

  • test_onefile.c - 이 파일은 파일 시스템이 없는 임베디드 장치에서 SQLite를 어떻게 사용할 수 있는지 보여주는 "fs"라는 데모 VFS를 구현해요. 내용이 기본 매체에 직접 쓰여져요. 이 데모 코드에서 파생된 VFS는 제한된 플래시 메모리를 가진 가젯이 SQLite가 그 장치의 플래시 메모리의 파일 시스템처럼 동작하게 하는 데 사용될 수 있어요.

  • test_journal.c - 이 파일은 SQLite 테스트 중에 사용되는 shim을 구현하며, 데이터베이스와 롤백 저널이 올바른 순서로 기록되고 적절한 시점에 "sync"되어 데이터베이스가 언제든지 정전이나 하드 리셋에서 복구될 수 있음을 보장함을 검증해요. 이 shim은 데이터베이스와 롤백 저널의 동작에 대해 여러 불변식(invariant)을 검사하고, 그중 하나라도 위반되면 예외를 발생시켜요. 이러한 불변식은 차례로 데이터베이스가 항상 복구 가능함을 보장해요. 이 shim으로 대규모 테스트 사례 모음을 실행하면 SQLite 데이터베이스가 예상치 못한 정전이나 장치 리셋으로 손상되지 않는다는 추가 확신을 제공해요.

  • test_vfs.c - 이 파일은 파일 시스템 오류를 시뮬레이션하는 데 사용할 수 있는 shim을 구현해요. 이 shim은 테스트 중에 실제 시스템에서 테스트하기 어려운 하드웨어 오작동이나 파일 시스템 공간 고갈 같은 기타 오류 조건에 SQLite가 합리적으로 응답하는지 검증하는 데 사용돼요.

핵심 SQLite 소스 코드 라이브러리와 사용 가능한 확장에 다른 VFS 구현도 있어요. 위 목록은 완전한 것이 아니라 VFS 인터페이스로 실현할 수 있는 기능의 종류를 대표적으로 보여주기 위한 것이에요.

4. VFS 구현 (VFS Implementations)

새 VFS는 세 객체를 하위 클래스화(subclassing)하여 구현돼요:

sqlite3_vfs 객체는 VFS의 이름과 운영체제에 대한 인터페이스를 구현하는 핵심 메서드(파일 존재 확인, 파일 삭제, 파일 생성, 읽기 및/또는 쓰기용 열기, 파일 이름을 정규 형식으로 변환)를 정의해요. sqlite3_vfs 객체에는 운영체제에서 난수를 얻고, 프로세스를 일시 중지(잠들기)하며, 현재 날짜와 시간을 찾는 메서드도 들어 있어요.

sqlite3_file 객체는 열린 파일을 나타내요. sqlite3_vfs의 xOpen 메서드는 파일이 열릴 때 sqlite3_file 객체를 만들어요. sqlite3_file은 파일이 열려 있는 동안 파일의 상태를 추적해요.

sqlite3_io_methods 객체는 열린 파일과 상호작용하는 데 사용되는 메서드를 담아요. 각 sqlite3_file은 자신이 나타내는 파일에 적합한 sqlite3_io_methods 객체에 대한 포인터를 담아요. sqlite3_io_methods 객체에는 파일에서 읽고 쓰고, 파일을 잘라내고(truncate), 변경 내용을 영구 저장소로 플러시하고, 파일 크기를 찾고, 파일을 잠그고 잠금을 해제하고, 파일을 닫고 sqlite3_file 객체를 파괴하는 것 같은 작업을 하는 메서드가 들어 있어요.

새 VFS의 코드를 작성하는 것은 sqlite3_vfs 객체에 대한 하위 클래스를 만들고, 그 VFS 객체를 sqlite3_vfs_register() 호출로 등록하는 작업을 포함해요. VFS 구현은 sqlite3_filesqlite3_io_methods에 대한 하위 클래스도 제공하지만, 그 객체들은 SQLite에 직접 등록되지 않아요. 대신 sqlite3_file 객체는 sqlite3_vfs의 xOpen 메서드에서 반환되고, sqlite3_file 객체는 sqlite3_io_methods 객체의 인스턴스를 가리키게 돼요.

더 알아보기 (Learn more)