참고: CSDN

Linux 메모리 관리: DAX(Direct Access) 매커니즘의 역할 및 구현 원리

1. DAX에 대한 간략한 소개

DAX(Direct Access) 메커니즘은 사용자 모드 소프트웨어가 영구 메모리(PM) 에 저장된 파일에 직접 액세스할 수 있도록 지원하는 메커니즘으로, 사용자 모드 소프트웨어는 먼저 파일 데이터를 페이지 캐시(Page Cache) 에 복사할 필요가 없습니다 . 1 .

위 설명은 아래 그림(일반 NVDIMM 소프트웨어 아키텍처 2 )에 해당하며, 이는 IO 경로(파일)와 (메모리) 모두 페이지 캐시를 우회할 수 있음을 의미합니다.

  • 파일 경로(이하 일반 파일 경로)는 사용자 모드 소프트웨어가 표준 파일 인터페이스(표준 파일 API)를 통해 영구 메모리 파일 시스템에 액세스함을 나타냅니다.
  • Memory 경로(이하, 매핑된 파일 경로라 함)는 사용자 모드 소프트웨어가 매핑된 파일(Memory-mapped File)을 통해 PM에 직접 접근하는 것을 의미한다.

일반적인 NVDIMM 소프트웨어 아키텍처

2. DAX의 원리

다음은 Linux v5.8-rc1에서 XFS를 예로 들어 소개합니다.

2.1 일반 파일 경로가 페이지 캐시를 우회하는 방법

xfs_file_write_iter 에서 제공하는 파일 쓰기 경로를 예로 들면 이 함수의 코드 부분은 다음과 같습니다:

STATIC ssize_t
xfs_file_write_iter(
	struct kiocb		*iocb,
	struct iov_iter		*from)
{
	struct file		*file = iocb->ki_filp;
	struct address_space	*mapping = file->f_mapping;
	struct inode		*inode = mapping->host;
	ssize_t			ret;

	if (IS_DAX(inode))
		return xfs_file_dax_write(iocb, from);

	if (iocb->ki_flags & IOCB_DIRECT) {
		ret = xfs_file_dio_aio_write(iocb, from);
		if (ret != -EREMCHG)
			return ret;
	}

	return xfs_file_buffered_aio_write(iocb, from);
}

124567891011121314151617181920이십 일스물 

이 코드는 쓰기 작업을 수행할 때 다음 세 가지 상황을 처리합니다.

  • DAX
    • PM 장치에 파일 시스템을 탑재할 때 DAX 플래그( mount -o dax)가 설정된 경우 영구 메모리 파일 시스템은 모든 쓰기 작업에 이 경로를 사용합니다.
    • 이 경로는 주로 dax_iomap_rw 를 호출합니다 . 이 함수는 dax_direct_access를 통해 대상 물리 메모리의 주소를 얻은 후 dax_copy_from_iter를 통해 NVDIMM 드라이버를 호출하여 데이터를 대상 물리 메모리에 직접 복사하고 해당 캐시 라인(Cache Line)을 플러시합니다.
  • DIO(직접 IO):
    • 파일을 열 때 직접 IO 플래그( O_DIRECT)가 설정된 경우 파일 작업은 이 경로를 사용합니다.
    • 이 경로는 주로 iomap_dio_rw 를 호출합니다 . 이 기능은 여전히 기존 스토리지 스택(Storage Stack)을 통해 장치에 액세스합니다. 즉, bio를 구성하고 요청을 블록 장치 계층(Block Device Layer)에 전달한 다음 블록 장치 계층이 드라이버를 호출하여 장치에 액세스합니다. 장치.
  • 일반 IO:
    • 페이지 캐시를 이용한 기존 IO 방식.
    • 이 경로는 주로 iomap_file_buffered_write 를 호출합니다 . 이 함수는 먼저 pagecache_get_page를 통해 페이지 캐시를 얻은 다음 (페이지 캐시 메커니즘의 최신 설계에서는 3 4 5를 읽을 수 있음 ) iomap_read_page_sync 를 통해 bio를 캡슐화하여 블록 장치 계층에 드라이버를 호출하여 페이지의 데이터를 읽도록 요청합니다. 장치를 페이지 캐시에 넣습니다. 페이지 캐시를 준비한 후 iov_iter_copy_from_user_atomic을 호출하여 사용자 모드 소프트웨어 요청으로 작성된 데이터를 페이지 캐시에 복사합니다. 모든 작업이 완료되면 iomap_set_page_dirty를 통해 페이지 캐시를 더티 페이지로 설정하세요 . 이 반복은 모든 사용자 데이터가 페이지 캐시에 기록될 때까지 계속되고, 마지막으로 백그라운드 프로세스를 사용하여 적절하게 Balance_dirty_pages_ratelimited를 통해 더티 페이지를 블록 장치에 다시 기록합니다 .

2.2 매핑된 파일 경로가 페이지 캐시를 우회하는 방법

mmap이 호출되면 파일 시스템은 프로세스의 mm_struct에 가상 메모리 영역(Virtual Memory Area, VMA)에서 설명하는 가상 주소만 등록합니다. 이후 사용자 모드 소프트웨어가 매핑된 파일에 처음 접근하면 메모리 관리 장치(Memory Managment Unit)가 PTE(Page Table Entry)가 비어 있음을 발견하고 페이지 폴트인 폴트 14번을 발생시킨다. (페이지 오류)로 인해 운영 체제가 요구 페이징(요구 페이징) 실행을 시작하게 됩니다. 이때, 가상 메모리 관리자(Virtual Memory Manager)와 파일 시스템이 페이지 테이블을 공동으로 관리하여 가상 메모리와 물리 메모리 간의 매핑 관계를 구축한다. 페이지 폴트는 소프트웨어/하드웨어 인터럽트(Software/Hardware Interrupt)가 아닌 예외(Exception)이기 때문에 위의 내용은 비동기 프로세스가 아닌 동기 프로세스입니다.

2.2.1 mmap이 호출되면 무슨 일이 일어나는가

mmap 시스템 호출 실행 시 do_mmapmmap_region이 주로 실행되며 사용자 모드 소프트웨어의 요청에 따라 사용 가능한 프로세스 가상 주소 공간의 섹션을 설명하는 데 사용되는 VMA를 반환한 후 파일의 mmap 구현을 실행합니다. call_mmap을

XFS에서 mmap은 xfs_file_mmap 에 의해 구현되며 여기서 기본 명령문은 단 하나입니다: vma->vm_ops = &xfs_file_vm_ops;. xfs_file_vm_ops이는 페이지 오류를 처리하기 위해 해당 함수를 호출해야 함을 예외 처리 루틴(예외 처리기)에 알려줍니다 .

2.2.2 페이징이 요청되면 어떻게 되나요?

요청 페이징은 주로 __xfs_filemap_fault 를 실행하며 , 그 코드는 다음과 같습니다:

static vm_fault_t
__xfs_filemap_fault(
	struct vm_fault		*vmf,
	enum page_entry_size	pe_size,
	bool			write_fault)
{
	struct inode		*inode = file_inode(vmf->vma->vm_file);
	struct xfs_inode	*ip = XFS_I(inode);
	vm_fault_t		ret;

	trace_xfs_filemap_fault(ip, pe_size, write_fault);

	if (write_fault) {
		sb_start_pagefault(inode->i_sb);
		file_update_time(vmf->vma->vm_file);
	}

	xfs_ilock(XFS_I(inode), XFS_MMAPLOCK_SHARED);
	if (IS_DAX(inode)) {
		pfn_t pfn;

		ret = dax_iomap_fault(vmf, pe_size, &pfn, NULL,
				(write_fault && !vmf->cow_page) ?
				 &xfs_direct_write_iomap_ops :
				 &xfs_read_iomap_ops);
		if (ret & VM_FAULT_NEEDDSYNC)
			ret = dax_finish_sync_fault(vmf, pe_size, pfn);
	} else {
		if (write_fault)
			ret = iomap_page_mkwrite(vmf,
					&xfs_buffered_write_iomap_ops);
		else
			ret = filemap_fault(vmf);
	}
	xfs_iunlock(XFS_I(inode), XFS_MMAPLOCK_SHARED);

	if (write_fault)
		sb_end_pagefault(inode->i_sb);
	return ret;
}
12삼4567891011121314151617181920이십 일스물 둘이십 삼스물넷25262728293031323334353637383940

분명히 이 코드에는 두 가지 분기가 있습니다.

  • DAX : 이 경로는 주로 dax_iomap_fault 를 호출합니다 . 이 함수는 먼저 Grab_mapping_entry를 통해 페이지 캐시에서 DAX 예외 항목을 가져온 다음(자세한 내용은 3 참조 ) xfs_bmbt_to_iomap을 통해 struct iomap 이라는 데이터 구조를 준비합니다 .

    struct iomap {
    	u64			addr; /* disk offset of mapping, bytes */
    	loff_t			offset;	/* file offset of mapping, bytes */
    	u64			length;	/* length of mapping, bytes */
    	u16			type;	/* type of mapping */
    	u16			flags;	/* flags for mapping */
    	struct block_device	*bdev;	/* block device for I/O */
    	struct dax_device	*dax_dev; /* dax_dev for dax operations */
    	void			*inline_data;
    	void			*private; /* filesystem private */
    	const struct iomap_page_ops *page_ops;
    };
    12삼456789101112
    

    struct iomap을 준비한 후 dax_iomap_pfn을 사용 하고 이를 struct iomap에서 제공하는 정보와 결합하여 대상 PM 페이지의 물리적 페이지 번호(pfn)를 얻습니다. 페이지와 연결된 DAX 예외 항목은 dax_insert_entry 에 의해 페이지 캐시를 유지 관리하는 데 사용되는 XArray 데이터 구조에 추가됩니다 . 마지막으로 vmf_insert_mixed_mkwrite가 호출되어 DAX 예외 항목에 캐시된 pfn을 해당 가상 페이지의 PTE에 채웁니다.

  • 일반 요청 페이징 이 경로는 주로 filemap_fault 를 호출합니다 . 이 함수는 먼저 do_sync_mmap_readahead 를 통해 동기적으로 파일 데이터를 미리 읽으려고 시도합니다 (미리 읽기 동작은 madvise 시스템 호출에 의해 영향을 받을 수 있으므로 전혀 읽지 못할 수도 있습니다). 그런 다음 pagecache_get_page를 통해 페이지 캐시를 할당한 다음 xfs_vm_readpage 파일 데이터를 페이지 캐시에 복사한 후 매핑된 파일 유형(MAP_SHARED, MAP_PRIVATE)에 따라 서로 다른 분기가 실행됩니다. 마지막으로 반환된 페이지는 vmf->page에 저장됩니다.

부록 1: 용어집

  • 영구 메모리(PM) : 메모리 액세스 명령(시스템 호출과 다름)을 통해 액세스할 수 있고 바이트 주소 지정이 가능한(블록과 다름) 비휘발성 메모리(NVM)를 나타 냅니다 . 그중 Byte-addressable은 각 주소 지정 단위가 (단어나 블록이 아닌) PM 단위에 해당함을 의미합니다.
  • 영구 메모리 시스템(PM 인식 파일 시스템) : DAX 메커니즘을 지원하는 파일 시스템을 나타냅니다. 이 용어의 일반적인 표현에는 “영구 메모리 파일 시스템” 8 9 및 “DAX 지원 파일 시스템” 2 도 포함됩니다 .

  • 메모리 매핑 파일(Memory-mapped File) : 파일이나 클래스 파일을 바이트 단위로 가상 메모리에 대응시켜 애플리케이션이 매핑된 부분을 메인 메모리에 접근하듯이 처리할 수 있도록 하는 리소스이다. 10 프로세스 주소 공간은 매핑 파일과 익명 메모리(Anonymous Memory)로 구성됩니다.

부록 2: DAX 역사

  • 2015년에 Carsten Otte는 Linux v2.611에 XIP(Execute-in-place) 메커니즘을 도입했습니다 . 원래 임베디드 시스템에서 사용되었던 XIP는 스토리지 스택의 공통 블록 계층과 드라이버 계층을 버리고 페이지 캐시를 우회하여 프로세스가 읽기 전용 메모리 또는 플래시 기반 메모리에 직접 액세스할 수 있도록 합니다.
  • 2014년에 Subramanya R Dulloor 등은 PMFS의 XIP 메커니즘을 기반으로 PM 12를 관리했습니다 .
  • 같은 해에 Matthew Wilcox는 XIP를 개선하고 DAX라는 하위 시스템을 제안했습니다. 그는 XIP를 Ext4 파일 시스템에 통합하려고 시도했을 때 XIP가 경쟁 조건(Race Conditions) 13을 잘 처리할 수 없다는 것을 발견했습니다 . 경쟁 조건은 여러 스레드가 공유 리소스에 동시에 액세스해야 할 때 발생하며 결과는 상대적인 실행 속도에 따라 달라집니다14 . 그가 수행한 가장 중요한 변경 사항은 파일 시스템의 경로 get_block대체 작업을 사용하는 것입니다 15 .struct address_space_operationsget_xip_mem

  • NVMe는 PCIe 기반 SSD를 위한 확장 가능한 고성능 호스트 컨트롤러 인터페이스입니다.
  • NVMe의 특징은 I/O 명령을 처리하기 위한 여러 대기열을 제공한다는 것입니다. 단일 NVMe 장치는 최대 64K I/O 대기열을 지원하고 각 I/O 대기열은 최대 64K 명령을 관리할 수 있습니다.
  • 호스트가 I/O 명령을 내리면 호스트 시스템은 명령을 제출 대기열(SQ)에 넣은 다음 초인종 레지스터(DB)를 사용하여 NVMe 장치에 알립니다.
  • NVMe 장치가 I/O 명령 처리를 완료하면 장치는 처리 결과를 완료 대기열(CQ)에 쓰고 인터럽트를 트리거하여 호스트 시스템에 알립니다.
  • NVMe는 MSI/MSI-X 및 인터럽트 집계를 사용하여 인터럽트 처리 성능을 향상시킵니다.

2. SPDK 개요

Storage Performance Development Kit(SPDK)는 확장 가능한 고성능 사용자 모드 스토리지 응용 프로그램을 작성하기 위한 일련의 도구 및 라이브러리 기능을 제공합니다. SPDK가 고성능을 낼 수 있는 이유는 필요한 모든 드라이버를 사용자 공간으로 옮겨 실행하고, 인터럽트 모드 대신 폴링 모드를 사용함으로써 커널 컨텍스트 전환을 피하고 인터럽트 처리 오버헤드를 제거하기 때문입니다.

SPDK의 기반은 사용자 공간, 폴링 모드, 비동기식, 잠금 없는 NVMe 드라이버입니다. 사용자 공간 애플리케이션은 SSD 디스크에 직접 액세스할 수 있으며 무복사, 고도의 병렬 방식으로 SSD 디스크에 액세스할 수 있습니다. 드라이버는 단일 공용 헤더 파일을 포함하는 C 라이브러리로 구현됩니다. 마찬가지로 SPDK는 NVMe 드라이버의 모든 특성을 가진 많은 Intel Xeon 기반 플랫폼에 있는 I/OAT DMA 엔진용 사용자 공간 드라이버를 제공합니다.

또한 SPDK는 또한 이러한 사용자 공간 드라이버 위에 구축된 NVMe-oFiSCSI 서버를 제공하여 네트워크 디스크를 서비스할 수 있도록 합니다. 클라이언트는 표준 Linux 커널 iSCSI 및 NVMe-oF 이니시에이터(또는 Windows의 iSCSI 이니시에이터)를 사용하여 서버에 연결할 수 있습니다. 이러한 서버는 다른 구현에 비해 CPU 사용률을 훨씬 향상시킬 수 있습니다.

SPDK는 C 라이브러리와 실행 파일을 통합하는 오픈 소스 BSD 라이센스 개발 키트이며 해당 소스 코드는 GitHub에서 호스팅됩니다. 모든 새로운 개발은 마스터 브랜치에 적용되며 안정적인 버전은 분기마다 릴리스됩니다. 코드 기여자와 사용자는 메일링 리스트를 통해 패치를 제출하고, 문제를 보고하고, 질문할 수 있습니다.

3. SPDK/NVMe 드라이버 개요

NVMe 드라이버는 응용 프로그램과 NVMe SSD 간에 복사 없이 직접 데이터 전송을 제공하기 위해 응용 프로그램에 직접 연결할 수 있는 C 함수 라이브러리입니다. 이것은 완전히 수동적입니다. 스레드를 생성하지 않고 애플리케이션 자체의 함수 호출에 대한 응답으로만 작업을 수행합니다. 이 라이브러리는 PCI BAR 레지스터를 로컬 프로세스에 직접 매핑한 다음 MMIO(메모리 매핑된 I/O)를 수행하여 NVMe 장치를 직접 제어합니다. I/O는 대기열 쌍(Queue Pair)을 통해 비동기적으로 제출되며 일반적인 실행 흐름은 Linux의 libaio와 완전히 다르지 않습니다.

자세한 내용은 여기를 참조하십시오 .

4. 기타 NVMe 드라이버 구현

  • Linux 커널 NVMe 드라이버: 공식 웹사이트 www.kernel.org 또는 미러로 이동 하여 커널 소스 코드를 다운로드한 다음 include/linux/nvme.h, drivers/nvme를 참고하십시오.
  • NVMeDirect : Computer Systems Laboratory@SKKU에서 시작한 오픈 소스 프로젝트로 SPDK/NVMe 드라이버와 유사하지만 Linux 커널 NVMe 드라이버를 기반으로 하고 있습니다. NVMeDirect는 거인의 어깨 위에 서 있는 사용자 모드 I/O 프레임워크라고 할 수 있습니다.