Colors of Ray+Hue'

분류 전체보기 +69

On Ubuntu or Debian machine You can get related packages doing something like this:

$ apt-cache search gflags

Output on my machine (Ubuntu 14.04):

libgflags-dev - commandline flags module for C++ (development files)

libgflags-doc - documentation of gflags

libgflags2 - commandline flags module for C++ (shared library)

python-gflags - Python implementation of the Google command line flags module

python-google-apputils - Google Application Utilities for Python

So, I presume You could install it using it's full name like this:

$ sudo apt-get install libgflags2 libgflags-dev

original source: https://askubuntu.com/questions/312173/installing-gflags-12-04


'Dababase' 카테고리의 다른 글

rocksdb error  (0) 2016.03.03
postgresql 9.3 on SpecVirt 작업 일지  (0) 2016.02.05
port forwarding  (0) 2016.02.02
examplie: test application for rocksdb compile  (0) 2015.07.18
Rocks DB 설치 on Ubuntu 12.04 LTS  (0) 2015.07.18



PCI Configuration Space

original source: http://egloos.zum.com/nimhaplz/v/5314763

PCI 버스에는 여러가지 장치가 물리는데 그 장치를 사용하려면, 각 장치가 어떤 것이고(identification), 어떻게 장치와 통신해야 하는지(protocol)를 알아야 하는데, PCI 버스에서는 장치를 인식하고, 그 장치의 기본적인 정보를 얻어오기 위해 configuration space를 사용한다.

PCI configuration space 대략 다음과 같은 정보가 담긴 데이터 스트럭쳐 이다.
Device ID,Vendor ID,Status,Class code,.. 등

디바이스의 PCI configuration space 정보를 읽으면 디바이스와 통신을 하기 위한 기본적인 정보를 알 수 있는 것이다. 그러면 예를 들어, 내 PC에 달린 네트웍 카드(NIC)의 configuration space는 어떻게 읽어올 수 있을까? 직접적으로 읽어올 수는 없고, 모든 디바이스를 검색 해야 한다. PCI디바이스는 물리적으로 모든 디바이스에 bus, device, function 이라는 번호가 부여된다. 이 번호는 PCI slot에 따라 부여되는 것이기 때문에, 모든 bus, 모든 device, 모든 function을 스캔 해 보면, 컴퓨터에 달린 모든 디바이스 정보를 알 수 있다.

bus, device, function은 총 16bit이므로, 여걸 다 스캔하면 65536개를 스캔해야 하는 것이다. 실제로 Linux는 부팅과정에서 이걸 다 스캔해서 디바이스를 인식하며, 이 과정을 enumeration이라 한다. 실제 Linux 디바이스 드라이버를 살펴 보자. 살펴 볼 드라이버는 Realtek의 네트웍카드인 r8169이다. Linux source의 drivers/net/r8169.c 파일이다.

(구글에서 r8169.c라고 검색하면 바로 볼 수 있다)
이 파일의 맨 끝부분을 보면, 다음과 같이 디바이스 드라이버를 등록하는 코드가 있다.


static struct pci_driver rtl8169_pci_driver = {
    .name       = MODULENAME,
    .id_table   = rtl8169_pci_tbl,
    .probe      = rtl8169_init_one,
    .remove     = __devexit_p(rtl8169_remove_one),
#ifdef CONFIG_PM
    .suspend    = rtl8169_suspend,
    .resume     = rtl8169_resume,
#endif
};

이 디바이스 드라이버가 처리할 수 있는 ID는 id_table에 저장돼 있다. 그걸 따라가 보면 이렇다.

static struct pci_device_id rtl8169_pci_tbl[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8129), 0, 0, RTL_CFG_0 },
    { PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8136), 0, 0, RTL_CFG_2 },
    { PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8167), 0, 0, RTL_CFG_0 },


여기서 PCI_VENDOR_ID_REALTEK이 realtek의 vendor ID이고,(0x10ec)Device ID가 각각 0x8129, 8136, 8167이다. 이 vendor ID, Device ID가 PCI Configuration space에 적혀 있어서,이 VID(Vendor ID), DID(Device ID)를 가진 하드웨어가 검색되면 이 8169.c를 쓸 수 있는 것이다.디바이스 드라이버를 등록하는 곳으로 다시 돌아가서, 이번에는 probe함수를 살펴보자.이 함수는 하드웨어를 사용하기 위해 실제로 준비를 하는 함수이다.

probe함수의 이름은 rtl8169_init_one 이다.이 함수 중간에 보면, pci_set_master라는 함수를 부르는 곳이 있다.pci set master는 busmaster DMA를 켜주는 함수로, 다음과 같이 정의된다.

void pci_set_master(struct pci_dev *dev){
    u16 cmd;
    pci_read_config_word(dev, PCI_COMMAND, &cmd);
    if (! (cmd & PCI_COMMAND_MASTER)) {
        pr_debug("PCI: Enabling bus mastering for device %s\n", pci_name(dev));
        cmd |= PCI_COMMAND_MASTER;
        pci_write_config_word(dev, PCI_COMMAND, cmd);
    }
    dev->is_busmaster = 1;
    pcibios_set_master(dev);
}

여기서 굵은 글씨로 표시한 두 줄이 바로 PCI configuration space를 읽고 쓰는 부분이다.

* pci_read_config_word  : dev의 PCI configuration space에서 PCI_COMMAND field를 읽어와서, cmd에 저장한다.
* pci_write_config_word  : dev의 PCI configuration space의 PCI_COMMAND field에 cmd값을 쓴다.

따라서 위 코드는, PCI_COMMAND field에다가 PCI_COMMAND_MASTER라는 bit을 켜는 역할을 수행하는 것이다.여기까지 PCI configuration space가 무엇인지, 어떻게 쓰는지를 살펴봤다.그 외 field에 대해 좀 더 자세히 살펴보자면 이렇다.

* Class Code
이 PCI device의 분류를 나타낸다. 예를 들어 이 디바이스가 network card인지, USB controller인지 구별하는 field이다.class code는 include/linux/pci_ids.h 파일에 보면 적혀있다.상위 16 bits가 저 파일에 적혀 있으며, 예를 들면 다음과 같다.

#define PCI_CLASS_STORAGE_IDE       0x0101      // Legacy IDE controller
#define PCI_CLASS_NETWORK_ETHERNET  0x0200     // Ethernet Interface

따라서, class code만 봐도 디바이스의 종류를 알 수 있는것이다.입출력 표준이 정해 져 있는 class가 있는데, 그런 경우 class마다 디바이스 드라이버가 있다.예를 들어, IDE같은 경우(class code 0x0101), PCI IDE controller Specification을 보고디바이스 드라이버를 작성하면, device ID나 vendor ID에 상관없이 동작한다.이러한 표준이 없는 디바이스 드라이버의 경우, device ID와 vendor ID에 따라 디바이스 드라이버가 필요하다.

* Base Address Register (BAR)
PCI configuration space는 장치를 찾을 때만 이용할 수 있는 매우 좁은 공간이다.실제로 디바이스 드라이버를 이용하기 위해서는 좀 더 넓은 영역이 필요할 것이다.예를 들어, 비디오 카드의 경우, Memory-mapped I/O (MMIO)를 하기 위해서는넓은 공간이 필요하다. (2000x1000 pixels에서 32bits color를 쓰면, 8MB정도가 필요하다) 이러한 공간을 따로 잡아서, 디바이스와 OS가 "우리 저길 쓰자"고 합의해야 하는데,이 때, Base Address Register (BAR)에 그러한 내용을 쓰는 것이다.부팅 과정에서 BIOS는 기본적으로 각 디바이스 별로 이용할 address space를 할당 해 주는데,BAR를 읽어보면 그 값을 알 수 있고, BAR를 통해서 그 값을 설정할 수도 있다.

* Decoding PCI data and lspci output on Linux Hosts

$ ls -la /sys/bus/pci/devices

위의 명령을 실행하면 다음과 같은 예제를 얻을 수 있는데, 

lrwxrwxrwx 1 root root 0 2009-08-03 10:38 0000:04:00.0 -> ../../../devices/pci0000:00/0000:00:0b.0/0000:04:00.0

디바이스의 문자열 "0000:04:00.0" 의 의미는 다음과 같다.

0000 : PCI domain (각 도메인은 256개의 PCI 버스를 가질 수 있음) 04 : the bus number the device is attached to 00 : the device number .0 : PCI device function

해당 디바이스에 대한 좀더 구체적인 정보를 얻기 위해서는 0000:04:00.0 디렉토리로 들어가서, 파일을 보면 된다.


참고할 만한 사이트
http://en.wikipedia.org/wiki/PCI_configuration_space
위키 페이지는 항상 자세하고 친절한 설명이 있으니 여기에도 좋은 말씀이 많을 것이다.나는 보통 configuration space 모양을 보는데 참고한다.

http://wiki.osdev.org/PCI
PCI configuration space 사용법에 대해 자세히 나와있다.reference manual용도로 사용하면 되겠다.

http://lwn.net/Kernel/LDD3/
불후의 명저 Linux Device Driver 3rd edition 사이트이다.책으로도 파는데 온라인으로도 공개 돼 있다.Linux Device Driver전반에 대한 자세하고(?) 친절한 설명이 있다.처음 보기엔 좀 어려울 수 있다.

'Linux Kernel' 카테고리의 다른 글

Block Device Open  (0) 2016.09.01
volatile keyword  (0) 2016.08.30
glibc Malloc  (0) 2016.08.27
LD_PRELOAD example  (0) 2016.08.27
MMIO in PCIe  (0) 2016.08.24

Block Device Open

Linux Kernel2016. 9. 1. 06:19


original source: Understanding Linux Kernel 3rd Edition


블록 장치 파일 열기


커널은 fs가 disk 또는 partition에 마운트 될때, 스왑 파티션이 활성화 될때, 유저 프로세스가 블록 장치에 open 시스템 콜을 호출할 때, block device file을 연다. 모든 경우 커널은 같은 작업을 수행하는데, (i)블록 장치 디스크립터를 찾고, (ii)파일 연산 메소드를 설정한다. 장치 파일이 열릴때, dentry_open() 함수가 파일 객체의 메소드를 전용함수로 설정하는데, 파일 객체의 f_op 필드는 def_blk_fops table의 주소로 설정한다. 기본적으로 디스크립터를 찾아 없으면 생성하고, open을 포함하여 이후에 사용될 method에 블록장치 함수에 매핑하는 것을 완료하면 concept 적인 open 과정이 끝난다. 고려해야 할 항목은 inode, filp, bdev, gendisk 등이다. 

 Method

 Function 

 open

 blkdev_open 

 release

 blkdev_close

 llseek

 block_llseek

 read

 generic_file_read

 write

 blkdev_file_write

 mmap

 generic_file_mmap

 fsync

 block_fsync

 ioctl

 block_ioctl

 aio_read

 generic_fio_aio_read

1. blkdev_open() 함수는 매개 변수로 inode 와 filp (파일객체)를 전달 받아 다음 과정을 수행한다. 

1. 아이노드 객체의 inode->i_bdev 필드를 검사 (NULL=nothing, else 해당 블록 디스크럽터의 주소), return desc.

2. if NULL, bdget(inode->i_rdev) 함수를 통해, desc. 를 찾는다. 없으면 새로 할당한다. 

3. 추후 open에 desc가 사용될 수 있으므로, inode->i_bdev에 저장. 

4. inode->i_mapping 필드를 bdev 아이노드에서 해당 필드 값으로 설정

5. inode를 열린 아이노드 리스트에 추가

2. filp->i_mapping 필드를 inode->i_mapping 값으로 설정

3. gendisk dscriptor 주소를 얻음.

4. bdev->bd_openers (0: 블록장치가 안열림, else: 이미 열렸음)

5. bdev->bd_disk를 gendisk 디스크립터의 주소 disk로 초기화함.

6. 블록 장치가 전체 디스크이면 (part 0) 다음 과정을 수행

1. 정의된 disk->fops->open 수행

2. disk->queue 로부터 관련 필드를 셋팅

7. return 0 (기타 몇몇 과정은 생략)


'Linux Kernel' 카테고리의 다른 글

PCI configuration space  (1) 2016.09.03
volatile keyword  (0) 2016.08.30
glibc Malloc  (0) 2016.08.27
LD_PRELOAD example  (0) 2016.08.27
MMIO in PCIe  (0) 2016.08.24


libvmmalloc initialization analysis


INTRO.

libvmmalloc은 애플리케이션의 코드 수정 없이 PM영역 메모리 할당 해제를 가능하게 한다. 훅을 걸기 위해서는 LD_PRELOAD를 사용해야 하며,  모든 메모리 할당/해제는 jemalloc을 바탕으로 동작한다. PM 영역의 파일시스템 path 및 pool(arena)의 크기를 지정해야 하며, 최초 라이브러리가 후킹 될때 path/pool 이 초기화 된다. 해당 풀을 직접 엑세스 하기 위해서는 반드시 page cache가 bypass 되어야 하므로, DAX를 사용하는 것이 바람직하다. 

초기화 과정은 크게 2가지로 나뉘는데, (i)PM 영역에 대한 메모리 매핑 설정, (ii) jemalloc의 풀 생성이다. 기본적으로 jemalloc을 포함한 모든 메모리 관리자는 커널로부터 메모리를 할당 받아 heap영역을 구성하게 되는데, 메모리 할당은 (s)brk/mmap을 통해 buddy의 free page list로부터 할당 받는다. 그러나, pmem.io에서 PM영역은 buddy에서 관리 되지 않고 file mapped IO를 통해 해당 memory address 를 엑세스를 허용하므로,  jemalloc을 비롯한 모든 malloc 의 초기 메모리 영역을 설정할 수 없다. 이를 위해 메모리 풀을 생성하기 위해 요구되는 과정을 init 함수에서 반드시 해줘야 한다. 다시 말해 je_pool_create에 요구되는 파라미터 (address, size)를 모두 셋업하여 jemalloc이 동작할 수 있는 기반을 마련한다. 

편의를 위해 오류체크등 부가적인 모든 소스코드를 생략한다.


최초 LD_PRELOAD에 의해 library가 load 될때 libvmmalloc_init가 자동으로 호출된다. 이하의 함수는 모두 libvmalloc_init의 함수이다.

가장 먼저 할일은 common_init 함수를 통해 log에 대한 메모리 크기를 페이지크기에 맞게 정렬함과 동시에 valgrind 에 관한 옵션을 설정한다. 

common_init(VMMALLOC_LOG_PREFIX, VMMALLOC_LOG_LEVEL_VAR, VMMALLOC_LOG_FILE_VAR, MMALLOC_MAJOR_VERSION, VMMALLOC_MINOR_VERSION); 

다음 jemalloc에 대한 헤더크기를 설정한다. 

Header_size = roundup(sizeof(VMEM), Pagesize); 

이후 PM 영역으로 사용할 디렉토리와 풀의 크기를 확인한다. getenv를 이용하여 환경변수를 통해 디렉토리 string과 크기를 정하고, 만일 풀의 크기가 최소 크기보다 작을 경우, 에러를 리턴한다. 최소 풀의 크기는 14MB 이다. 

if ((env_str = getenv(VMMALLOC_POOL_SIZE_VAR)) == NULL) {

abort();

} else {

long long v = atoll(env_str);

if (v < 0) {

abort();

}

size = (size_t)v;

}

if (size < VMMALLOC_MIN_POOL) {

abort();


#define VMMALLOC_MIN_POOL ((size_t)(1024 * 1024 * 14)) /* min pool size: 14MB */ 

이제 패스설정 및 풀 크기 확인등 초기과정이 완료 되었고, 해당 디렉토리에 파일 생성 및 실제 매핑에 관한 일을 한다. 관련된 모든 일은 libvmmalloc_create 에서 이루어진다. libvmalloc_create 함수는 풀을 생성한 후 유효한 값으로 채워진 vmp 구조체 포인터를 리턴한다. 

Vmp = libvmmalloc_create(Dir, size); 

이제 가장 중요한 libvmmalloc_create함수를 살펴본다. 먼저 풀이 몇페이지가 요구되는지 확인하고 (1), 헤당 디렉토리에 임시파일 (vmem.XXXXXX) 을 생성한다 (2). posix_fallocate를 통해 필요한 페이지에 대해 블록을 미리 할당한다 (3). posix_fallocate의 경우, 실제 블록이 할당 되는 것은 아니고, 파일 시스템에서 bitmap을 검색하여 empty블록을 검색, 해당 파일에 대한 inode에 블록을 할당한후 unwritten 상태로 세팅을 한다. 이후 util_map을 통해 실제 매핑이 이루어진다 (4). 매핑이 매핑 자체는 해당 풀에 대한 VM를 얻어내는 역할에 불과하며, 실제로 해당 메모리에 대한 엑세스는 발생하지 않는다. 이제 virtual memory pool (vmp) 구조체에 header, base addr, size 등을 모두 채워 넣는다 (5). 이제 jemalloc의 pool을 생성할 모든 준비가 끝났다. jemalloc library에서 제공되는 je_pool_create 의 alias 함수인 je_vmem_pool_create를 호출하여  해더를 제외한 메모리로 사용할 수 있는 base address 와 풀의 크기를 넘긴다 (6).  마지막으로 해당 메모리 영역을 다른 프로세스가 엑세스 할 수 없도록 보호한다 (7). 

static VMEM * libvmmalloc_create(const char *dir, size_t size)

{

1. size = roundup(size, Pagesize);

2. Fd = util_tmpfile(dir, "/vmem.XXXXXX");

3. if ((errno = posix_fallocate(Fd, 0, (off_t)size)) != 0)

4. if ((addr = util_map(Fd, size, 0, 4 << 20)) == NULL)

5.1 struct vmem *vmp = addr;

5.2 memset(&vmp->hdr, '\0', sizeof(vmp->hdr));

5.3 memcpy(vmp->hdr.signature, VMEM_HDR_SIG, POOL_HDR_SIG_LEN);

5.4 vmp->addr = addr;

5.5 vmp->size = size;

5.6 vmp->caller_mapped = 0;

6. if (je_vmem_pool_create((void *)((uintptr_t)addr + Header_size),

7. util_range_none(addr, sizeof(struct pool_hdr));

return vmp;


핵심은 jemalloc 뿐만 아니라 모든 메모리 할당 라이브러리가 PM영역을 사용할 수 없으므로, 기존의 라이브러리들이 메모리로써 사용할 수 있도록 기반 작업을 한다. 이후 hooking된 malloc이 해당 풀(arena)로부터 메모리를 할당 받을 수 있도록 한다. 

'NVM' 카테고리의 다른 글

LD_PRELOAD rootkits  (0) 2016.08.26
libvmmalloc  (0) 2016.08.24
libpmem library  (0) 2016.08.09
libnvdimm  (0) 2016.08.05
PCMSIM  (0) 2016.08.05

volatile keyword

Linux Kernel2016. 8. 30. 01:51


volatile keyword

original source:https://ko.wikipedia.org/wiki/Volatile_%EB%B3%80%EC%88%98

C/C++ 프로그래밍 언어에서 이 키워드는 최적화 등 컴파일러의 재량을 제한하는 역할을 한다. 개발자가 설정한 개념을 구현하기 위해 코딩된 프로그램을 온전히 컴파일되도록 한다. 주로 최적화와 관련하여 volatile가 선언된 변수는 최적화에서 제외된다. OS와 연관되어 장치제어를 위한 주소체계에서 지정한 주소를 직접 액세스하는 방식을 지정할 수도 있다. 리눅스 커널 등의 OS에서 메모리 주소는 MMU와 연관 된 주소체계로 논리주소와 물리주소 간의 변환이 이루어진다. 경우에 따라 이런 변환을 제거하는 역할을 한다. 또한 원거리 메모리 점프 기계어 코드 등의 제한을 푼다.

C언어 MMIO에서 적용[편집]

주로 메모리 맵 입출력(MMIO)을 제어할 때, volatile을 선언한 변수를 사용하여 컴파일러의 최적화를 못하게 하는 역할을 한다.

static int foo;
 
void bar(void)
{
    foo = 0;
 
    while (foo != 255);
}

foo의 값의 초기값이 0 이후, while 루프 안에서 foo의 값이 변하지 않기 때문에 while의 조건은 항상 true가 나온다. 따라서 컴파일러는 다음과 같이 최적화한다.

void bar_optimized(void)
{
    foo = 0;

    while (true);
}

이렇게 되면 while의 무한 루프에 빠지게 된다. 이런 최적화를 방지하기 위해 다음과 같이 volatile을 사용한다.

static volatile int foo;

void bar (void)
{
    foo = 0;

    while (foo != 255);
}

이렇게 되면 개발자가 의도한 대로, 그리고 눈에 보이는 대로 기계어 코드가 생성된다. 이 프로그램만으로는 무한루프라고 생각할 수 있지만, 만약 foo가 하드웨어 장치의 레지스터라면 하드웨어에 의해 값이 변할 수 있다. 따라서 하드웨어 값을 폴링(poll)할 때 사용할 수 있다. 컴파일러 최적화를 피해야 하는 변수들, 레지스터 또는 CPU가 아닌 장치에 대해 엑세스 되는 메모리 영역등에 대한 제어문의 최적화의 회피에 volatile 변수가 사용된다. 

'Linux Kernel' 카테고리의 다른 글

PCI configuration space  (1) 2016.09.03
Block Device Open  (0) 2016.09.01
glibc Malloc  (0) 2016.08.27
LD_PRELOAD example  (0) 2016.08.27
MMIO in PCIe  (0) 2016.08.24

glibc Malloc

Linux Kernel2016. 8. 27. 09:32


Glibc Malloc 

original source: http://studyfoss.egloos.com/5206979


malloc() 함수의 서비스 루틴은 public_mALLOc() 함수이며 실제로는 전처리 과정에 의해 __libc_malloc()이라는 이름으로 바뀐다. __malloc과 malloc은 이 함수에 대한 alias이다.) public_mALLOc() 함수는 다음과 같은 작업을 수행한다.


1. __malloc_hook이 정의되어 있다면 해당 hook을 호출한 후 종료한다.

2. 그렇지 않으면 malloc을 처리할 heap 영역(arena)를 찾는데 일반적으로 main_arena가 사용된다.

3. arena에 대한 lock을 건 후에 실제 malloc의 처리 루틴인 _int_malloc() 내부 함수를 호출한다.

4. 만약 _int_malloc() 함수가 NULL을 반환했다면 다른 arena에 대해 _int_malloc()을 다시 한 번 호출한다.

5. arena에 걸린 lock을 해제한다.

6. _int_malloc() 함수가 반환한 값을 반환하고 종료한다.


_int_malloc() 함수는 다음과 같은 작업을 수행한다.

Fast Bin Search

요청한 크기를 chunk 크기에 맞춘다. 즉, 헤더를 위한 4 바이트를 더한 후 8 바이트 단위로 정렬(align)한다. 이 후로는 chunk 크기를 기준으로 계산한다. 주어진 크기가 fast bin에 속한다면 (<= 72) fast bin 내의 free chunk를 찾아본다. 주어진 크기에 맞는 fast bin의 인덱스를 계산한다. 해당 인덱스의 포인터가 가리키는 chunk를 victim 지역 변수에 저장한다. victim이 NULL이 아니라면 fast bin의 해당 인덱스에 victim->fb가 가리키는 chunk를 저장하고 victim의 데이터 영역에 대한 포인터를 반환한다. (종료)

Small Bin Search

주어진 크기가 small bin에 속한다면 (< 512) small bin 내에서 free chunk를 찾아본다. 주어진 크기에 맞는 small bin의 인덱스를 계산하여 idx 지역 변수에 저장한다. 해당 인덱스 내에 가장 오래된 chunk를 victim 지역 변수에 저장한다. victim이 올바른 chunk를 가리킨다면 해당 인덱스 내의 리스트에서 victim을 제거하고, victim 바로 다음에 위치한 chunk의 헤더에 P (PREV_INUSE) 플래그를 설정한 뒤 victim의 데이터 영역에 대한 포인터를 반환한다. (종료) 

Large Bin Search

large bin은 바로 찾아보지 않고 다음과 같은 준비 과정을 거친다. 주어진 크기에 맞는 large bin의 인덱스를 계산하여 idx 지역 변수에 저장한다. 만약 fast bin을 포함하고 있다면 이들을 모두 병합(consolidate)하여 보다 큰 chunk로 만든다. 이는 큰 메모리 요청을 받은 경우에는 더 이상 작은 크기의 요청이 (최소한 당분간은) 없을 것이라고 가정하기 때문이다. (이로 인해 fast bin으로 인한 fragmentation 문제를 줄일 수 있다.) 이제 unsorted bin을 검색하여 일치하는 크기의 free chunk가 있는지 검색한다. unsorted bin 내의 가장 오래된 chunk를 victim 지역 변수에 저장한다. victim을 unsorted bin의 리스트에서 분리한다. victim의 크기와 주어진 크기가 일치한다면 victim을 반환한다. (종료)

Bin is not Found

idx 값을 하나 증가시킨 후 더 큰 크기의 bin 내에 free chunk가 있는지 검사한다. (이는 bitmap을 통해 빨리 확인할 수 있다.) 현재 인덱스에 해당하는 bitmap을 검사하여 free chunk가 있는지 확인한다. 만약 해당 bin이 비어있다면 인덱스를 하나 증가시킨 후 검사를 다시한다. bitmap이 설정된 bin이 있다면 해당 bin 내의 (가장 작은 크기의) 가장 오래된 chunk를 victim 지역 변수에 저장한다. victim을 리스트에서 분리한다. victim의 크기가 요청을 처리하고도 다른 chunk를 구성할 수 있을 정도로 크다면 분할하여 나머지 영역을 chunk로 만들어서 unsorted bin에 추가한다. 나머지 영역의 크기가 small bin에 속한다면 last_remainder 변수가 나머지 영역을 가리키도록 설정한다. victim을 반환한다. (종료)

Heap Increase

그래도 없다면 시스템의 heap 영역을 늘려야 한다. 이는 sYSMALLOc() 함수가 처리하며, 이 함수의 반환값을 반환하고 종료한다. sYSMALLOc() 함수는 다음과 같은 작업을 수행한다.

먼저 (1)요청된 크기가 mmap() 시스템 콜을 이용하도록 설정된 범위에 속하고 (>= 128K) mmap() 사용 횟수 제한을 넘지 않는다면 (< 65536회) mmap()을 호출한다. 호출이 성공하면 chunk에 M (IS_MMAPPED) 플래그를 설정하고 데이터 영역의 포인터를 반환한다. mmap()으로 할당한 chunk는 분할할 수 없으므로 크기에 여유가 있더라도 하나의 chunk로 사용된다.

그 보다 작은 크기거나 mmap() 호출이 실패했다면 heap 영역을 늘려야 한다. 증가시킬 크기는 요청한 크기에서 원래의 top chunk 크기를 빼고 top chunk가 기본적으로 가져야 할 여유 공간의 크기(pad)를 더한 후 할당 후 남은 영역에 chunk를 구성하기 위한 최소 크기(16)를 더한 값이다. 또한 이는 시스템의 페이지 크기에 맞춰 조정된다. (2)위에서 계산한 크기에 대해 sbrk() (MORCORE라는 이름을 사용한다) 시스템 콜을 호출한다. 호출이 성공했다면 __after_morecore_hook이 정의되어 있는지 검사하여 이를 호출한다. (3)호출이 실패했다면 크기와 횟수 제한에 상관없이 mmap() 시스템 콜을 호출하여 메모리 할당을 시도한다. 이것이 성공하면 해당 arena는 더 이상 연속된 주소 공간에 속하지 않으므로 NONCONTIGUOUS_BIT를 설정한다. 실패했다면 errno 변수를 ENOMEM으로 설정하고 NULL을 반환한다. (종료)


'Linux Kernel' 카테고리의 다른 글

Block Device Open  (0) 2016.09.01
volatile keyword  (0) 2016.08.30
LD_PRELOAD example  (0) 2016.08.27
MMIO in PCIe  (0) 2016.08.24
JEMALLOC: A Scalable Concurrent malloc(3) Implementation for FreeBSD  (0) 2016.08.18

LD_PRELOAD example

Linux Kernel2016. 8. 27. 05:53


LD_PRELOAD rootkits Part2

Original Source: http://www.catonmat.net/blog/simple-ld-preload-tutorial-part-2/


이번에는 간단히  fopen: 을 실행하는 프로그램 prog.c를 예로 들어 보자.

#include <stdio.h>

int main(void) {
    printf("Calling the fopen() function...\n");

    FILE *fd = fopen("test.txt", "r");
    if (!fd) {
        printf("fopen() returned NULL\n");
        return 1;
    }

    printf("fopen() succeeded\n");

    return 0;
}

그리고 공유 라이브러리 myfopen.c 를 작성하자. 이 파일은 prog.c 의 fopen 을 override 하고, c standard 라이브러리 원본  fopen 를 호출한다. 

#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>

FILE *fopen(const char *path, const char *mode) {
    printf("In our own fopen, opening %s\n", path);

    FILE *(*original_fopen)(const char*, const char*);
    original_fopen = dlsym(RTLD_NEXT, "fopen");
    return (*original_fopen)(path, mode);
}

이 공유 라이브러리는 fopen 함수를 export 하고, path 를 출력한다. 그리고 RTLD_NEXT pseudo handle을 통한 dlsym 을 사용하여  원본  fopen 를 찾는다. 우리는 반드시 _GNU_SOURCE feature 를 define 해야 하는데, 이것은  RTLD_NEXT 를  <dlfcn.h>. 로부터 사용하기 위해서 이다.  RTLD_NEXT 는 현재 라이브러리에서 검색순위에 따라 순차적으로 함수를 검색한다. 

다음과 같이 이 공유 라이브러리를 컴파일 하고, 

gcc -Wall -fPIC -shared -o myfopen.so myfopen.c -ldl

Now when we preload it and run prog we get the following output that shows that test.txt was successfully opened:

이제 preload 해서 prog를 실행시킨다. test.txt 가 성공적으로 open 된 것을 확인 할 수 있다. 

$ LD_PRELOAD=./myfopen.so ./prog
Calling the fopen() function...
In our own fopen, opening test.txt
fopen() succeeded


'Linux Kernel' 카테고리의 다른 글

volatile keyword  (0) 2016.08.30
glibc Malloc  (0) 2016.08.27
MMIO in PCIe  (0) 2016.08.24
JEMALLOC: A Scalable Concurrent malloc(3) Implementation for FreeBSD  (0) 2016.08.18
malloc 소개  (0) 2016.08.18

LD_PRELOAD rootkits

NVM2016. 8. 26. 08:49

# LD_PRELOAD rootkits

original source: http://hyunmini.tistory.com/55

original source: https://blog.gopheracademy.com/advent-2015/libc-hooking-go-shared-libraries/


How LD_PRELOAD rootkits work

An LD_PRELOAD rootkit works by implementing alternative versions of functions provided by the libc library that many Unix binaries link to dynamically. Using these ‘hooks’, the rootkit can evade detection by modifying the behaviour of the functions and bypassing authentication mechanisms, e.g. PAM, to provide an attacker with a backdoor such as an SSH login using credentials configured by the rootkit.

For example, the Azazel rootkit hooks into the fopen function and conceals evidence of network activity or files related to the rootkit. If there is nothing to hide, Azazel invokes the original libc function so that the application behaves as normal from the user’s perspective.

Using LD_PRELOAD to hook into other libraries is an old trick and can usefully be used for debugging applications, especially when you don’t have access to an application’s source code.

LD_PRELOAD

프로세스 실행 과정 중 라이브러리를 로딩 할때, LD_PRELOAD 변수가 설정되어 있으면 해당 변수에 지정된

라이브러리를 먼저 로딩하고, 이중 libc 함수명과 동일한 함수가 있다면 해당 함수를 먼저 호출해 준다.

즉, 자동으로 후킹을 수행하도록 해준다는 말과 같다.

참고 - OS 별 PRELOAD 환경변수


Linux : LD_PRELOAD 

AIX : LDR_PRELOAD

Solaris : LD_PRELOAD

FreeBSD : LD_PRELOAD

간단히 개념 정리를 위한 예제를 살펴보자. 

$ ls

secuholic  test1  test2  test3

ls 명령 수행 시 현재 디렉토리의 파일 목록이 보인다. 파일 중 secuholic 이 보이지 않도록 해 본다.

$ ltrace ls

         ...

strcpy(0x08058758, "test1")                                               = 0x08058758

readdir64(0x08057720, 0x08057700, 0xbffffb84, 1, 0x0805777c) = 0x08057794

malloc(6)                                                                      = 0x08058768

strcpy(0x08058768, "test2")                                               = 0x08058768

readdir64(0x08057720, 0x08057700, 0xbffffb84, 1, 0x08057794) = 0x080577ac

malloc(6)                                                                       = 0x08058778

strcpy(0x08058778, "test3")                                               = 0x08058778

readdir64(0x08057720, 0x08057700, 0xbffffb84, 1, 0x080577ac) = 0x080577c4

malloc(10)                                                                     = 0x08058788

strcpy(0x08058788, "secuholic")                                       = 0x08058788

readdir64(0x08057720, 0x08057700, 0xbffffb84, 1, 0x080577c4) = 0x080577e0

malloc(7)                                                                      = 0x08058798

         ...

secuholic  test1  test2  test3

중간 문자열 처리 과정에서 strcpy 함수 호출 시 src 가 secuholic 인지 비교하여 참인 경우 변조를 하면 된다.

$ cat test.c

#include <stdio.h>

#include <string.h>


char *strcpy(char *dest, const char *src)

{

int i =0;

while (src[i] != '\0')

{

dest[i] = src[i];

i++;

}

dest[i] = '\0';

printf("[hooked] : strcpy(%x,\"%s\")\n",dest,src);

return &dest[0];

}

$ LD_PRELOAD=./hook.so ls

[hooked] : strcpy(8054a48,"xterm-redhat")

[hooked] : strcpy(8054c18,"xterm-xfree86")

[hooked] : strcpy(bffffa87,"46")

[hooked] : strcpy(8054a4c,"li#46")

[hooked] : strcpy(bffffa87,"98")

[hooked] : strcpy(8054c1c,"co#98")

[hooked] : strcpy(8054fa0,"no=00:fi=00:di=01;34:ln=")

[hooked] : strcpy(80549b8,".")

[hooked] : strcpy(80549c8,"test1")

[hooked] : strcpy(80549d8,"test2")

[hooked] : strcpy(80549e8,"test3")

[hooked] : strcpy(80549f8,"secuholic")   // secuholic 문자열 복사

[hooked] : strcpy(8054b28,"test.c")

[hooked] : strcpy(8054b38,"hook.so")

hook.so  secuholic  test.c  test1  test2  test3


이제 해당 부분을 수정해 보자.


$ cat test.c

#include <stdio.h>

#include <string.h>


char *strcpy(char *dest, const char *src)

{

int i =0;

if(strcmp(src,"secuholic")==0){

dest[i] = '\0';

return &dest[0]; // src가 secuholic 인 경우 바로 리턴

}

while (src[i] != '\0')

{

dest[i] = src[i];

i++;

}

dest[i] = '\0';

// printf("[hooked] : strcpy(%x,\"%s\")\n",dest,src);

return &dest[0];

}

gcc -shared -fPIC -o hook.so test.c


$ ls 

 hook.so secuholic test.c test1 test2 test3   // secuholic 존재


$ LD_PRELOAD=./hook.so ls 

 hook.so test.c test1 test2 test3   //  secuholic 숨김

이렇게 간단히 후킹이 가능함을 확인해 보았다. LD_PRELOAD 는 setuid 가 걸려 있으면 동작하지 않으며, 타인의 프로세스에는 영향을

줄 수 없는 등 몇가지 제한 사항이 있으나, 쉽게 후킹이 가능하다는 점에서 유용하다 볼 수있겠다.  :)

'NVM' 카테고리의 다른 글

libvmmalloc initialization analysis  (0) 2016.08.30
libvmmalloc  (0) 2016.08.24
libpmem library  (0) 2016.08.09
libnvdimm  (0) 2016.08.05
PCMSIM  (0) 2016.08.05

libvmmalloc

NVM2016. 8. 24. 08:11

original source: http://pmem.io/nvml/libvmmalloc/libvmmalloc.3.html

libvmmalloc

INTRO.

libvmmalloc은 프로그램을 수정하지 않고, 해당 라이브러리를 삽입하여 SYNOPSIS에 소개 되어 있는 libmalloc 함수들을 NVM영역에서 메모리 할당이 가능하도록 한다. 기존의 메모리 할당이 힙을 통해 할당되는 반면, 일정 NVM영역을 MMFIO를 통해 메모리 할당을 가능하게 한다. 이를 위해 환경변수 세팅을 통해 NVM 영역의 디렉토리와 메모리풀로 사용될 파일의 크기를 지정해야 한다. 이를 통해 해당 파일 영역에 매핑 되어 있는 주소를 엑세스 하여, 라이브러리 로드시 해당 파일 영역이 초기화 되며, 메모리 할당은 Jemalloc을 베이스로 한다. 

EXAMPLE: LD_PRELOAD=./libvmmalloc.so VMMALLOC_POOL_DIR=/mnt/mem VMMALLOC_POOL_SIZE=1073741824 ./ma

NAME

libvmmalloc − general purpose volatile memory allocation library

SYNOPSIS

$ LD_PRELOAD=libvmmalloc.so command [ args... ]

or

#include <stdlib.h>
#include <malloc.h>
#include <libvmmalloc.h>

$ cc [ flag... ] file... -lvmmalloc [ library... ]

void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t number, size_t size);
void *realloc(void *ptr, size_t size);

int posix_memalign(void **memptr, size_t alignment, size_t size);
void *aligned_alloc(size_t alignment, size_t size);
void *memalign(size_t alignment, size_t size);
void *valloc(size_t size);
void *pvalloc(size_t size);

size_t malloc_usable_size(const void *ptr);
void cfree(void *ptr);

DESCRIPTION

libvmmalloc transparently converts all the dynamic memory allocations into Persistent Memory allocations. The typical usage of libvmmalloc does not require any modification of the target program. It is enough to load libvmmalloc before all other libraries by setting the environment variable LD_PRELOAD. When used in that way,libvmmalloc interposes the standard system memory allocation routines, as defined in malloc(3), posix_memalign(3) and malloc_usable_size(3), and provides that all dynamic memory allocations are made from a memory poolbuilt on memory-mapped file, instead of a system heap. The memory managed by libvmmalloc may have different attributes, depending on the file system containing the memory-mapped file. In particular, libvmmalloc is part of the Non-Volatile Memory Library because it is sometimes useful to use non-volatile memory as a volatile memory pool, leveraging its capacity, cost, or performance characteristics.

libvmmalloc은 동적 메모리 할당을 영속 메모리 할당으로 투명하게 변환한다. libvmalloc 사용은 해당 프로그램의 수정을 요구하지 않는다. 환경변수 LD_PRELOAD를 세팅하기 전에 libvmmaloc을 로드하여 libvmmalloc을 표준 시스템 메모리 할당 루틴에 끼워넣는다. 

malloc(3), posix_memalign(3), malloc_usable_size(3) 등은 시스템 힙 대신 MMFIO를 통해 메모리 풀에서 동적메모리 할당을 지원한다. libvmmaloc의 메모리 관리는 MMFIO를 포함하는 파일 시스템에 의존한다. 특히 NVM을 일반 메모리 풀로 사용이 가능하다.

libvmmalloc may be also linked to the program, by providing the vmmalloc argument to the linker. Then it becomes the default memory allocator for given program.

NOTE: Due to the fact the library operates on a memory-mapped file, it may not work properly with the programs that perform fork(3) not followed by exec(3).

There are two variants of experimental fork() support available in libvmmalloc. The desired library behavior may be selected by setting VMMALLOC_FORK environment variable. By default variant #1 is enabled. See ENVIRONMENT section for more details.

libvmmalloc uses the mmap(2) system call to create a pool of volatile memory. The library is most useful when used with Direct Access storage (DAX), which is memory-addressable persistent storage that supports load/store access without being paged via the system page cache. A Persistent Memory-aware file system is typically used to provide this type of access. Memory-mapping a file from a Persistent Memory-aware file system provides the raw memory pools, and this library supplies the traditional malloc interfaces on top of those pools.

libvmmalloc은 mmap(2) 시스템 콜을 통해 일반 메모리 풀을 생성한다. 라이브러리는 DAX에 가장 효과가 좋다. PMFS는 보통 L/S를 지원한다. PMFS를 통한 MMFIO는 저수준 메모리 풀을 지원하고, 이 라이브러리는 malloc 인터페이스를 해당 풀의 상위에서 지원한다. 

The memory pool acting as a system heap replacement is created automatically at the library initialization time. User may control its location and size by setting the environment variables described in ENVIRONMENT section. The allocated file space is reclaimed when process terminates or in case of system crash.

시스템 힙 대체로서 메모리풀은 라이브러리 초기화 과정에서 자동으로 생성된다. 사용자는 위치와 크기를 ENVIRONMENT 섹션에서 소개되는 환경변수 세팅을 통해 조정된다. 

Under normal usage, libvmmalloc will never print messages or intentionally cause the process to exit. The library uses pthreads(7) to be fully MT-safe, but never creates or destroys threads itself. The library does not make use of any signals, networking, and never calls select() or poll().

ENVIRONMENT

There are two configuration variables that must be set to make libvmmalloc work properly. If any of them is not specified, or if their values are not valid, the library prints the appropriate error message and terminates the process.

다음 2개의 환경 변수는 libvmmalloc에 반드시 필요하다. 아닐 경우, 에러 메시지와 함께 해당 프로세스를 종료 시킨다. 


VMMALLOC_POOL_DIR

Specifies a path to directory where the memory pool file should be created. The directory must exist and be writable.

메모리 풀에 생성되어야 하는 패스를 나타내며, 해당 디렉토리는 쓰기가능하도록 존재해야 한다. 


VMMALLOC_POOL_SIZE

Defines the desired size (in bytes) of the memory pool file. It must be not less than the minimum allowed size VMMALLOC_MIN_POOL as defined in <libvmmalloc.h>. Note that due to the fact the library adds some metadata to the memory pool, the amount of actual usable space is typically less than the size of the memory pool file.

메모리 풀 파일의 크기를 명시한다. VMMALLOC_MIN_POOL 보다는 반드시 커야한다. 




'NVM' 카테고리의 다른 글

libvmmalloc initialization analysis  (0) 2016.08.30
LD_PRELOAD rootkits  (0) 2016.08.26
libpmem library  (0) 2016.08.09
libnvdimm  (0) 2016.08.05
PCMSIM  (0) 2016.08.05

MMIO in PCIe

Linux Kernel2016. 8. 24. 06:11

MMIO in PCIe: Device has CPU accessible memory

Abstract
디바이스 드라이버 모듈의 .init는 먼저 디바이스 드라이버를 등록하는데, name, ID (vendor, device), prove, remove 등을 등록한다. IDtable 에서 vendor ID 와 device ID는 규격에 의해 제품마다 정해져 있기 때문에, 드라이버 작성시 반드시 매칭을 해야 한다 (참고:http://pcidatabase.com/). 또한 lspci를 통해 ID를 확인 할 수 있다. 이후 정상적으로 디바이스가 OS에 의해 인식이 되면, 먼저 .probe가 호출된다. .probe의 콜백이 호출되지 않는 다면 디바이스는 인식되지 않은 것이다. .probe의 콜백의 주요한 역할은 실제 드라이버가 구동하기 전 해당 디바이스의 메모리를 사용할 수 있도록 준비 상태를 만드는 것이며, 이는 IOremap 에 의해 완성된다. 

 static struct pci_device_id test_ids[] = {

{ PCI_DEVICE(0x vendorID, 0x deviceID) },
{ 0 }
};

static struct pci_driver test_driver = {
.name = "test",
.id_table = test_ids,
.probe = test_probe,
.remove = test_remove
};


 static int __init test_init (void){

rc = pci_register_driver(&test_driver);

}

static void __exit test_exit (void){
pci_unregister_driver(&test_driver);
}

.probe에서는 가장 먼저 디바이스를 enable 한다. 정상적으로 디바이스가 구동 가능한 상태가 되면, 매핑을 하고자 하는 영역(bar)을 선택하여, 어떤 타입의 매핑(IORESOURCE_MEM)을 할것인지를 결정한다. 해당 디바이스의 시작 주소 (PFN)와 크기(PFN)을 선택한 후, ioremap을 통해 매핑된 영역의 시작 가상 주소 (VA)을 얻어내어, CPU를 통한 직접 읽기가 가능한 메모리 영역을 엑세스 할 수 있다. nopage method 와는 달리 이의 방법은 연속된 물리적 주소에만 가능하고, 당연히 page fault를 발생시키기 않는다. kernel page table은 해당 VMA에 모든 PFN을 홀드하고 있는 상태이기 때문이다.



 static int test_probe (struct pci_dev *pdev, const struct pci_device_id *id)


{


pci_enable_device_mem(pdev)


pci_select_bars(pdev,IORESOURCE_MEM)


start = pci_resource_start(pdev,0)


size = pci_resource_len(pdev,0)


io_va = ioremap/_wt/_nocache(start,size)


...






Accessing the I/O and Memory Spaces

Original Source: http://www.oreilly.com/openbook/linuxdrive3/book/ch12.pdf

A PCI device implements up to six I/O address regions. Each region consists of either memory or I/O locations. Most devices implement their I/O registers in memory regions, because it’s generally a saner approach (as explained in the section “I/O Ports and I/O Memory,” in Chapter 9). However, unlike normal memory, I/O registers should not be cached by the CPU because each access can have side effects. The PCI device that implements I/O registers as a memory region marks the difference by setting a “memory-is-prefetchable” bit in its configuration register.* If the memory region is marked as prefetchable, the CPU can cache its contents and do all sorts of optimization with it; nonprefetchable memory access, on the other hand, can’t be optimized because each access can have side effects, just as with I/O ports. Peripherals that map their control registers to a memory address range declare that range as nonprefetchable, whereas something like video memory on PCI boards is prefetchable.

In this section, we use the word region to refer to a generic I/O address space that is memory-mapped or port-mapped. An interface board reports the size and current location of its regions using configuration registers—the six 32-bit registers shown in Figure 12-2, whose symbolic names are PCI_BASE_ADDRESS_0 through PCI_BASE_ADDRESS_5. Since the I/O space defined by PCI is a 32-bit address space, it makes sense to use the same configuration interface for memory and I/O. If the device uses a 64-bit address bus, it can declare regions in the 64-bit memory space by using two consecutive PCI_BASE_ADDRESS registers for each region, low bits first. It is possible for one device to offer both 32-bit regions and 64-bit regions.

In the kernel, the I/O regions of PCI devices have been integrated into the generic resource management. For this reason, you don’t need to access the configuration variables in order to know where your device is mapped in memory or I/O space. The preferred interface for getting region information consists of the following functions:

커널에서 PCI 장치의 IO 영역은 범용 자원 관리자로 통합되었다. 따라서, 메모리나 입출력 공간의 어디에 장치가 매핑되었는지를 알기 위해 configuration variable을 엑세스 할 필요가 없다. 그냥 매핑이 일단 이루어지고, 연속된 해당 영역에 대해 시작과 크기 주소공간만 알면, 엑세스가 가능하다.

unsigned long pci_resource_start(struct pci_dev *dev, int bar);

The function returns the first address (memory address or I/O port number) associated with one of the six PCI I/O regions. The region is selected by the integer bar (the base address register), ranging from 0–5 (inclusive).

unsigned long pci_resource_end(struct pci_dev *dev, int bar);

The function returns the last address that is part of the I/O region number bar. Note that this is the last usable address, not the first address after the region.unsigned long pci_resource_flags(struct pci_dev *dev, int bar);

This function returns the flags associated with this resource. Resource flags are used to define some features of the individual resource. For PCI resources associated with PCI I/O regions, the information is extracted from the base address registers, but can come from elsewhere for resources not associated with PCI devices.

All resource flags are defined in <linux/ioport.h>; the most important are:

IORESOURCE_IO

IORESOURCE_MEM

If the associated I/O region exists, one and only one of these flags is set.

IORESOURCE_PREFETCH

IORESOURCE_READONLY

These flags tell whether a memory region is prefetchable and/or write protected. The latter flag is never set for PCI resources. By making use of the pci_resource_ functions, a device driver can completely ignore the underlying PCI registers, since the system already used them to structure resource information.

pci_resource 함수는 디바이스 드라이버가 PIC 레지스터를 무시할 수 있도록 해주는데, 시스템이 리소스 정보를 구조화 하는데 이미 그것들을 사용하고 있기 때문이다.

'Linux Kernel' 카테고리의 다른 글

glibc Malloc  (0) 2016.08.27
LD_PRELOAD example  (0) 2016.08.27
JEMALLOC: A Scalable Concurrent malloc(3) Implementation for FreeBSD  (0) 2016.08.18
malloc 소개  (0) 2016.08.18
posix_fallocate  (0) 2016.08.16


JEMALLOC: A Scalable Concurrent malloc(3) Implementation for FreeBSD

Original Source: https://people.freebsd.org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf


Abstract 

본글은 facebook의 jemalloc에 대해 소개한다. libmalloc의 ptmalloc은 일반적인 성능이 괜찮지만, false file sharing이 발생하는 multi-threaded 환경에서는 극단적으로 메모리 할당 성능이 저하될 수 있다. 특히 enterprise 환경에서는 이와 같은 특징은 치명적이므로, 구글의 tcmalloc 과 함께 jemalloc은 위의 문제를 해결할 수 있는 대안으로 작성되었다. 기본적인 구조는 CPU 당 4개의 아레나를 생성하고 (CPU 2개 이상일 경우), 각 쓰레드가 메모리 할당을 시도할 경우, round-robin 방식으로 아레나들로부터 메모리를 할당 받아 false file sharing을 줄이도록 노력한다. 각 아레나는 3개의 주요한 섹션으로 나뉘며 (small, large, huge), buddy & slab이 메모리를 할당하는 방법과 매우 비슷한 형태로 동작한다.


Problem Issue: False File Sharing

Modern multi-processor systems preserve a coherent view of memory on a per-cache-line basis. If two threads are simultaneously running on separate processors and manipulating separate objects that are in the same cache line, then the processors must arbitrate ownership of the cache line. This false cache line sharing can cause serious performance degradation.

멀티 프로세서 시스템은 메모리뷰의 일관성(coherent)을 캐시 라인을 기준으로 보존한다. 만일 두개의 쓰레드 A, B가 각각 CPU 0, 1에서 동시에 동작하고, 각개의 같은 캐시 라인에 있는 오브젝트를 다룰 때, 프로세서는 해당 캐시라인에 대한 오너쉽을 중재해야 한다. 이 false cache line sharing은 성능에 심각한 영향을 미친다.  각기 다른 쓰레드에 의해 사용되는 2개의  할당이 물리 메모리 캐시의 같은 라인에서 공유된다(false cache sharing). 만일 쓰레드들이 2개의 할당을 동시에 수정하려고 할 경우,프로세서는 캐시라인의 소유권을 중재해야 한다. 결론적으로 스레드의 개수가 증가할 경우, false cache sharing의 확률이 증가하여 cache update 에 대한 rock contention overhead가 증가한다. 


Related works

One of the main goals for this allocator was to reduce lock contention for multi-threaded applications running on multi-processor systems. Larson and Krishnan (1998) did an excellent job of presenting and testing strategies. They tried pushing locks down in their allocator, so that rather than using a single 2 allocator lock, each free list had its own lock. This helped some, but did not scale adequately, despite minimal lock contention. They attributed this to “cache sloshing” – the quick migration of cached data among processors during the manipulation of allocator data structures. Their solution was to use multiple arenas for allocation, and assign threads to arenas via hashing of the thread identifiers (Figure 2). This works quite well, and has since been used by other implementations (Berger et al., 2000; Bonwick and Adams, 2001). jemalloc uses multiple arenas, but uses a more reliable mechanism than hashing for assignment of threads to arenas.

메모리 할당자의 주요한 목표중 하나는 멀티 프로세서 시스템에서 동작하는 멀티 쓰레드 에플리케이션의 락 경쟁을 줄이는 것이다.  Larson and Krishnan (1998)은 그들의 할당자에 락을 줄이기 위해 노력했는데, 각각의 free list가 자신의 락을 갖도록 했다. 그들은 "캐시 출렁거림 (cache sloshing)" 에 공헌했다:- 할당자 자료 구조를 조작하는 동안 프로세서들간의 빠른 캐시 데이터의 통합. 그들의 해법은 복수의 경기장 (arena)을 할당에 사용하는 것이었고, 쓰레드 식별자의 해싱을 통해 스레드를 경기장에 할당한다. jemalloc은 복수의 경기장을 사용하지만, 쓰레드를 경기장에 할당하는데 해싱을 통한 기법보다 좀더 유연한 메커니즘을 사용한다.


Algorithms and Data Structure

Each application is configured at run-time to have a fixed number of arenas. By default, the number of arenas depends on the number of processors: 

- Single processor: Use one arena for all allocations. There is no point in using multiple arenas, since contention within the allocator can only occur if a thread is preempted during allocation. 

- Multiple processors: Use four times as many arenas as there are processors. By assigning threads to a set of arenas, the probability of a single arena being used concurrently decreases.

Larson and Krishnan (1998) 방법과 유사하게 여러개의 경기장을 유지하여, 스레드를 할당하지만, 스레드 식별자를 통한 해싱이 아닌, Round Robin 방식을 통해 순차적으로 스레드 별 메모리를 할당한다.  모든 에플리케이션은 고정된 개수의 경기장을 동작중에 갖도록 구성되어 있다. 기본적으로, 경기장의 개수는 프로세서의 개수에 따른다.

- Reliable Pseudo-Random Hashing: Hash(스레드 식별자) 를 통한 스레드의 아레나 할당: round-robin보다 fairness sk contention 측면에서 나은 점을 찾아 볼수 없다. 

- Dynamic re-balancing: 확실히 경쟁을 줄일 수 있지만, 유지비용이 많이 들고, 오버해드 대비 이득을 보장하는데 힘들다.


All memory that is requested from the kernel via sbrk(2) or mmap(2) is managed in multiples of the “chunk” size, such that the base addresses of the chunks are always multiples of the chunk size. This chunk alignment of chunks allows constant-time calculation of the chunk that is associated with an allocation. Chunks are usually managed by particular arenas, and observing those associations is critical to correct function of the allocator. The chunk size is 2 MB by default. Chunks are always the same size, and start at chunk-aligned addresses. Arenas carve chunks into smaller allocations, but huge allocations are directly backed by one or more contiguous chunks.

모든 메모리는 커널로 부터 sbark/mmap을 통해 요청되는데, 몇개의 청크 크기를 통해 관리 된다. 청크들의 정렬은 할당에 관련된 청크를 계산하는데 상수 시간 계산이 가능하게 한다. 청크들은 보통 특정 아레나에 의해 관리되고, 할당자의 동작을 수정하는데 사용된다. 청크들은 언제나 같은 크기이고, 청크에 정렬된 주소에서 시작한다. 경기장들은 청크들을 더 작은 할당으로 다듬 지만, 큰 할당에 대해서는 하나 이상의 몇개의 청크들을 직접 사용한다. 


Allocation size classes fall into three major categories: small, large, and huge. All allocation requests are rounded up to the nearest size class boundary. Huge allocations are larger than half of a chunk, and are directly backed by dedicated chunks. Metadata about huge allocations are stored in a single red-black tree. Since most applications create few if any huge allocations, using a single tree is not a scalability issue.

기본적으로 아레나는 특정 크기의 연속적인 메모리 주소의 나타내며, 스레드를 아레나에 할당한 다는 것은 특정 스레드의 메모리 할당이 해당 아레나의 주소 공간의 일부를 통해 이루어진다는 뜻이다. 할당의 크기는 3개의 주요한 항목으로 나뉜다: small, large, and huge. 모든 할당 요청은 가까운 사이즈 클래스에 따라 라운드 로빈으로 동작한다. Huge 할당은 청크의 1/2 보다 큰 할당이며, 특정 청크에 직접 할당된다. Huge 할당에 데한 메타데이터는 단일 RB 트리에 저장된다. 대부분의 애플리케이션이 Huge 할당을 거의 생성하지 않기 때문에 단일 트리의 사용은 scalability 문제가 없다. 


For small and large allocations, chunks are carved into page runs using the binary buddy algorithm. Runs can be repeatedly split in half to as small as one page, but can only be coalesced in ways that 4 reverse the splitting process. Information about the states of the runs is stored as a page map at the beginning of each chunk. By storing this information separately from the runs, pages are only ever touched if they are used. This also enables the dedication of runs to large allocations, which are larger than half of a page, but no larger than half of a chunk.

small과 large 할당에 대해서는, 이진 버디 알고리즘을 통해 동작하는 페이지들로 분할된다. 동작은 절반씩 청크를 줄이는 방법을 반복해서 한페이지 크기까지 줄이지만, 오로지 4번의 분할 과정을 역으로 해서 합쳐질 수 있다.  동작의 상태를 나타내는 정보는 각 청크의 시작 주소에 있는 페이지 맾에 저장된다. 각 동작별로 이정보를 분할하여 저장하는 것을 통해, 페이지들은 사용될 때만 오로지 수정된다. 이것은 동작의 특정을 페이지의 절반보다 큰고 청크의 절반보다 작은 large 할당이 가능하도록 한다. 


Small allocations fall into three subcategories: tiny, quantum-spaced, and sub-page. Modern architectures impose alignment constraints on pointers, depending on data type. malloc(3) is required to return memory that is suitably aligned for any purpose. This worst case alignment requirement is referred to as the quantum size here (typically 16 bytes). In practice, power-of-two alignment works for tiny allocations since they are incapable of containing objects that are large enough to require quantum alignment. Figure 4 shows the size classes for all allocation sizes.

small 할당은 3개의 작은 서브 항목으로 나뉜다. tiny, quantum-spaced, and sub-page. 최신 아키텍쳐는 포인터의 정렬 제한을 도입하고, 이것은 데이터의 타입에 따른다. malloc은 어떤 목적에 부합하도록 정렬되어 있는 메모리를 리턴한다. 최악의 정렬요구사항은 퀀텀 크기의 정렬이다.  실제로, 2승의 요구사항은 tiny 할당에 사용되는데 그것들은 퀀텀 정렬 만큼 큰 객체를 수용할 수 있게 한다. 



'Linux Kernel' 카테고리의 다른 글

LD_PRELOAD example  (0) 2016.08.27
MMIO in PCIe  (0) 2016.08.24
malloc 소개  (0) 2016.08.18
posix_fallocate  (0) 2016.08.16
System Memory  (0) 2016.08.13

malloc 소개

Linux Kernel2016. 8. 18. 04:54



malloc 소개

Original Source:http://webtn.tistory.com/entry/Facebook%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9%EC%9E%90-jemalloc


바쁘신 분들을 위한 핵심 요약

Facebook이 소개한 jemalloc은 Google의 tcmalloc과 함께 요즘에 뜨는 메모리 할당자 ( malloc ) 입니다. 둘 다 기존 바이너리를 고치지 않고, 실행 전 한 줄 추가하는 것만으로 수십 퍼센트의 성능향상을 얻을 수가 있습니다. 꼭 테스트하고 사용해보도록 합시다.

소개

연초에 Facebook에서 Jason Evans씨가 쓴 “Facebook은 메모리 할당자를 jemalloc을 써서 속도향상을 얻었다”는 글이 개발자들의 트위터 타임라인을 한창 돌아다녔습니다. 무엇을 하든 화제가 되는 기업이 Google에서 Facebook으로 넘어간 듯한 느낌이었습니다. 이들이 쓰는 jemalloc은 어떤 것일까요?

Malloc

프로그램이 무엇을 하려 하든, 시스템에서 메모리를 받아오는 일이 가장 먼저입니다. 도화지가 있어야 그림을 그릴 수 있으니까요. 때문에 메모리를 할당 받는 malloc은 C, C++ 프로그래머들이 가장 많이 사용하는 call 입니다. 효율적인 malloc을 만들고자 하는 대가들의 도전은 지금도 계속되고 있습니다. 프로그램이 도는 동안 수십, 수백만 번 이상 불리기에, 소스를 고치지 않고 빠른 malloc을 사용하는 것만으로 전체 프로그램의 속도가 올라가기 때문이죠.

Malloc의 중요성에 대한 재조명

malloc은 원래부터 중요했습니다만, 최근의 멀티코어, 멀티스레드 환경에서 동작하는 서버 프로그램에서 다음 측면 때문에 더욱 중요해지고 있습니다.

속도 – 최근에는 프로그램 하나가 많은 수의 스레드를 사용하고, 각 스레드가 여러 CPU에 분산되어 실행되게 되었습니다. 이 상황에서도 메모리를 효율적으로 분배하는 일은 그렇게 쉬운 일이 아닙니다. 많은 스레드를 다루게 되면 기존 malloc library의 성능이 주저 앉기 시작합니다. 예를 들어 리눅스에 기본으로 들어있는 glibc malloc의 경우 스레드 8개 이상을 돌리기 시작하면 최고 성능의 60% 수준으로 떨어져 버립니다. 당연히 그것을 쓰는 프로그램도 성능이 뚝 낮아져버리는 겁니다. 대책이 필요합니다.

공간 효율성 – malloc은 도화지에서 그림을 그리기 위한 구역을 따오는 것과 비슷합니다. 도화지에서 중구난방으로 영역을 가져오면, 여기저기에 구멍이 숭숭 뚫리게 됩니다. 이렇게 오랜 시간 동안 쓰게 되면, 전체 도화지에서 아직 칠할 수 있는 전체 면적은 많지만, 단일 덩어리로서의 큰 여백이 점차 사라져서, 있어도 못쓰는 현상이 발생합니다(fragmentation). 오랜 시간 동작하는 서버 프로그램에서 이 부분은 특히나 치명적입니다. 메모리가 충분히 남아 있는데도, 메모리 공간이 부족하다가 시스템이 죽는 현상을 겪어보셨을 텐데 바로 이 경우에 해당합니다. 따라서 오랜 시간 영역을 할당 받고 해제하더라도 큰 면적을 잘 보존하는 malloc이 더욱 중요해졌습니다.

사실 속도와 공간 효율성은 두 마리의 토끼와 같아 동시에 달성하기 어렵습니다만, 소프트웨어 엔지니어들의 각고의 노력 끝에 둘 다 쫓을 수 있는 malloc이 점차 나오고 있습니다. 오늘 소개드릴 jemalloc이 바로 그 예입니다.

대표적인 malloc 들

jemalloc은 하늘에서 뚝 떨어진 것이 아닙니다. 기존의 malloc개발 역사 계보를 따라 가고 있습니다. 아주 과거의 역사를 제외하면, 최근에 가장 많이 쓰이는 malloc들은 다음과 같은 것들이 있습니다.

dlmalloc – Doug Lea 아저씨가 만들었던 malloc입니다. 빠르지는 않고, 예전에 만들어져서 멀티코어, 멀티스레드 개념도 고려되지 않았습니다. 그러나 이후 많은 malloc의 베이스가 됩니다. 참고로 doug Lea 씨는 Java Concurrency의 대가 입니다. 이분이 2006년에 쓰신 Java Concurrency in Practice 는 자바로 서버 쪽 프로그래밍 하시는 분들에게는 아직도 매우 강력히 추천되는 서적입니다. (우리나라에도 번역서가 나와있습니다.)

ptmalloc – glibc에 포함된 malloc입니다. 리눅스의 사실상 표준이란 이야기죠. dlmalloc기반에 멀티코어와 멀티스레드 개념이 고려되었습니다. 뒤에 설명드릴 jemalloc의 arena 개념도 ptmalloc2에 먼저 도입되어 있습니다. 제일 빠른 malloc은 아니지만, 범용적인 사용에 평균적인 성능을 보여주기에 아직까지 리눅스 glibc 기본으로 채택되어 있습니다. 고지식한 모범생 같은 녀석입니다.

tcmalloc – 구글의 Sanjay Ghemawat 아저씨가 만든 malloc입니다. “구글이 만들면 malloc도 다릅니다.”를 천명하며 많은 이들을 열광시킨 malloc입니다. 이름부터 thread caching malloc으로 thread에 대한 고려가 매우 크게 되었고, ptmalloc대비 굉장히 빠릅니다. 덤으로 이 malloc을 쓰면 구글의 여러가지 프로그램 분석도구, 튜닝도구들이 제공 됩니다. 이들이 매우 훌륭합니다. 참고로 이 아저씨가 Google File System도 만들고 MapReduce, BigTable 도 만들었습니다. 구글의 인프라를 만든 사람입니다.



'Linux Kernel' 카테고리의 다른 글

MMIO in PCIe  (0) 2016.08.24
JEMALLOC: A Scalable Concurrent malloc(3) Implementation for FreeBSD  (0) 2016.08.18
posix_fallocate  (0) 2016.08.16
System Memory  (0) 2016.08.13
Radix Tree  (0) 2016.08.12

posix_fallocate

Linux Kernel2016. 8. 16. 09:55

original Source: http://pubs.opengroup.org/onlinepubs/009695399/functions/posix_fallocate.html


posix_fallocate

NAME

posix_fallocate 함수는 해당 파일에 대해서 요청하는 크기만큼 블록을 미리 할당한다. 주로 filesystem에서의 block fragmentation을 방지하는데 많은 도움이 되며, torrent와 같이 미리 할당이 요구되는 작업에 효율적이다. posix_fallocate를 지원하지 않는 파일 시스템에 대해서는 커널에서 적절히(?) 블록을 미리 할당한다. 

NAME

posix_fallocate - file space control (ADVANCED REALTIME)

SYNOPSIS

[ADV] [Option Start] #include <fcntl.h>

int posix_fallocate(int
 fd, off_t offset, off_t len); [Option End]

DESCRIPTION

The posix_fallocate() function shall ensure that any required storage for regular file data starting at offset and continuing for len bytes is allocated on the file system storage media. If posix_fallocate() returns successfully, subsequent writes to the specified file data shall not fail due to the lack of free space on the file system storage media.

posix_fallocate() 함수는 일반 파일이 요구하는 연속된 공간(off-len)이 스토리지 partition에 연속적으로 할당되어 있는 것을 보장 한다. 만일 posix_fallocate()가 success를 리턴하면, 반드시 요구되는 공간이 있다는 것을 의미한다. 해당 파일에 대한 연속적인 쓰기는 파일시스템 스토리지에 free space가 없는 이유로 실패할 수 없다. 

만일 4KB 연속적인 공간을 해당 fd를 포함하고 있는 파일시스템의 블록 디바이스에 할당할 경우, posix_fallocate(fd,0,4096) 과 같이 사용할 수 있다. 

If the offset+ len is beyond the current file size, then posix_fallocate() shall adjust the file size to offset+ len. Otherwise, the file size shall not be changed.

만일 offset+ len 가 현재 파일 크기보다 크면, posix_fallocate() 는 파일 크기를 offset+ len 로 조정해야 한다. 아닐경우, 파일 크기는 변할 수 없다.

It is implementation-defined whether a previous posix_fadvise() call influences allocation strategy.

Space allocated via posix_fallocate() shall be freed by a successful call to creat() or open() that truncates the size of the file. Space allocated via posix_fallocate() may be freed by a successful call to ftruncate() that reduces the file size to a size smaller than offset+len.

posix_fallocate() 에 의해 할당된 공간은 파일의 크기를 절단하는 create나 open에 해제 될 수 있다. posix_fallocate() 에 의해 할당된 공간은  파일 크기를 offset+len 보다 작은 파일 크기로 줄일 수 있는 ftruncate() 에 의해 해제될 수 있다.  

RETURN VALUE

success: return zero

else return errors below:

[EBADF]      The fd argument is not a valid file descriptor.

[EBADF]      The fd argument references a file that was opened without write permission.

[EFBIG]       The value of offset+ len is greater than the maximum file size.

[EINTR]       A signal was caught during execution.

[EINVAL]     The len argument was zero or the offset argument was less than zero.

[EIO]          An I/O error occurred while reading from or writing to a file system.

[ENODEV]   The fd argument does not refer to a regular file.

[ENOSPC]    There is insufficient free space remaining on the file system storage media.

[ESPIPE]      The fd argument is associated with a pipe or FIFO.


'Linux Kernel' 카테고리의 다른 글

JEMALLOC: A Scalable Concurrent malloc(3) Implementation for FreeBSD  (0) 2016.08.18
malloc 소개  (0) 2016.08.18
System Memory  (0) 2016.08.13
Radix Tree  (0) 2016.08.12
Linear VS Physical Address  (0) 2016.08.10

System Memory

Linux Kernel2016. 8. 13. 10:44

original source: http://egloos.zum.com/studyfoss/v/5020843


[Linux] x86 시스템 메모리 맵 설정


시스템 부팅 시에 수행하는 가장 중요한 일 중 하나는 시스템에서 사용할 수 있는 메모리의 크기와 위치를 파악하여 이를 적절히 설정하는 것이다. ARM이나 MIPS와 같은 임베디드에서 주로 사용되는 코어들은 하드웨어 구성이 표준적으로 정해질 수 없으므로 이러한 작업은 보통 컴파일 시 특정 하드웨어에 정해진 설정을 그대로 사용하거나 부트로더에서 명령행 옵션으로 설정을 넘겨주어야 한다.

하지만 x86/PC 환경에서는 이러한 작업을 위한 표준적인 BIOS 서비스를 제공한다. 그 중 가장 대표적으로 사용되는 것이 이른바 'e820' 방식이라고 하는 BIOS 인터럽트 15번을 이용하는 방법이다. (실행 시 AX 레지스터에 16진수 e820이 들어있어야 하기 때문에 붙여진 이름이다.) 이에 대한 설명은 Ralf Brown's Interrupt List에 다음과 같이 나와있다.

INT 15 - newer BIOSes - GET SYSTEM MEMORY MAP
    AX = E820h
    EAX = 0000E820h
    EDX = 534D4150h ('SMAP')
    EBX = continuation value or 00000000h to start at beginning of map
    ECX = size of buffer for result, in bytes (should be >= 20 bytes)
    ES:DI -> buffer for result (see #00581)
Return: CF clear if successful
        EAX = 534D4150h ('SMAP')
        ES:DI buffer filled
        EBX = next offset from which to copy or 00000000h if all done
        ECX = actual length returned in bytes
    CF set on error
        AH = error code (86h) (see #00496 at INT 15/AH=80h)
Notes:    originally introduced with the Phoenix BIOS v4.0, this function is
      now supported by most newer BIOSes, since various versions of Windows
      call it to find out about the system memory
    a maximum of 20 bytes will be transferred at one time, even if ECX is
      higher; some BIOSes (e.g. Award Modular BIOS v4.50PG) ignore the
      value of ECX on entry, and always copy 20 bytes
    some BIOSes expect the high word of EAX to be clear on entry, i.e.
      EAX=0000E820h
    if this function is not supported, an application should fall back
      to AX=E802h, AX=E801h, and then AH=88h
    the BIOS is permitted to return a nonzero continuation value in EBX
      and indicate that the end of the list has already been reached by
      returning with CF set on the next iteration
    this function will return base memory and ISA/PCI memory contiguous
      with base memory as normal memory ranges; it will indicate
      chipset-defined address holes which are not in use and motherboard
      memory-mapped devices, and all occurrences of the system BIOS as
      reserved; standard PC address ranges will not be reported
SeeAlso: AH=C7h,AX=E801h"Phoenix",AX=E881h,MEM xxxxh:xxx0h"ACPI"
 
Format of Phoenix BIOS system memory map address range descriptor:
Offset    Size    Description    (Table 00580)
 00h    QWORD    base address
 08h    QWORD    length in bytes
 10h    DWORD    type of address range (see #00581)
 
(Table 00581)
Values for System Memory Map address type:
 01h    memory, available to OS
 02h    reserved, not available (e.g. system ROM, memory-mapped device)
 03h    ACPI Reclaim Memory (usable by OS after reading ACPI tables)
 04h    ACPI NVS Memory (OS is required to save this memory between NVS
      sessions)
 other    not defined yet -- treat as Reserved
SeeAlso: #00580


이 방식을 통해 메모리 맵 정보를 구성하는 코드는 (setup 프로그램에 포함되는) arch/x86/boot/memory.c 파일의 detect_memory_e820 함수이다. e820 방식을 통해 얻은 메모리 맵 정보는 부트로더 혹은 setup 프로그램을 통해 boot_params내에 포함되어 커널로 전달된다. 커널은 해당 e820 맵 정보를 모두 검사하여 중복된 정보가 있는지 확인하고 이를 순서대로 정리한다. (sanitize_e820_map) 이 후 이 정보를 통대로 max_pfn, max_low_pfn 등의 변수를 설정하고 init_memory_mapping 함수를 호출하여 커널 영역의 페이지 테이블을 초기화한다. e820 방식으로 얻은 메모리 정보 및 커널이 수정한 메모리 정보는 dmesg 명령을 통해 확인할 수 있으며 /sys/firmware/memmap 디렉토리에서도 확인할 수 있다.

현재 이 글을 작성 중인 머신에서의 dmesg 출력은 다음과 같다.

[    0.000000] BIOS-provided physical RAM map:
[    0.000000]  BIOS-e820: 0000000000000000 - 000000000009f800 (usable)
[    0.000000]  BIOS-e820: 000000000009f800 - 00000000000a0000 (reserved)
[    0.000000]  BIOS-e820: 00000000000dc000 - 0000000000100000 (reserved)
[    0.000000]  BIOS-e820: 0000000000100000 - 000000007f6e0000 (usable)
[    0.000000]  BIOS-e820: 000000007f6e0000 - 000000007f700000 (ACPI NVS)
[    0.000000]  BIOS-e820: 000000007f700000 - 0000000080000000 (reserved)
[    0.000000]  BIOS-e820: 00000000e0000000 - 00000000f0000000 (reserved)
...

sysfs의 정보는 다음과 같다.

namhyung@NHK-XNOTE:/sys/firmware/memmap/0$ cat start end type 
0x0
0x9f7ff
System RAM


'Linux Kernel' 카테고리의 다른 글

malloc 소개  (0) 2016.08.18
posix_fallocate  (0) 2016.08.16
Radix Tree  (0) 2016.08.12
Linear VS Physical Address  (0) 2016.08.10
Block I/O Operation  (0) 2016.08.06

Radix Tree

Linux Kernel2016. 8. 12. 06:35

Original Source: http://timewizhan.tistory.com/entry/%EB%9D%BC%EB%94%95%EC%8A%A4-%ED%8A%B8%EB%A6%ACRadix-Tree


Radix Tree

라딕스 트리(Radix Tree)란 무엇일까? .. 라딕스 트리에 대해 알아 보기 전에.!! 라딕스 트리는 왜 사용하는 것일까?? 간단히 말해서 페이지 캐시를 위해 쓰이는 자료 구조이다.

그러면 자세히 알아볼까?? 페이지를 좀 더 빠르게 이용하기 위해서 보통 캐시 기법을 사용한다. 그래서 디스크에서 페이지 인덱스가 주어지면 커널은 페이지 캐시를 찾기 위해 라딕스 트리를 이용한다. 왜냐하면!! 라딕스 트리에 페이지 캐시가 위치가 나오기 때문에.. 
아무튼. 커널은 라딕스 트리를 이용하여 있다면. 페이지 디스크립터를 가져오게 된다.~~
그리고 이 페이지 디스크립터를 보고 이 페이지가 어떤 페이지구나~ 라는 것을 알게 된다.

그렇기 떄문에.~ 페이지 캐시를 위해서는 라딕스 트리를 사용하는 것이다. 그러면 라딕스 트리의 구조에 대해 알아 볼까?? 잠시 '리눅스 커널의 이해'의 그림을 참조 하자면...

이렇게 일반적인 리스트(?) 와 같은 형태이다. root->node->node ... 형태로..그렇다면 자료 형태를 보도록 해보자.

height : 현재 트리의 높이
gfp_mask : 새로운 노드를 위해 메모리 요청이 있을 경우 사용하는 플래그
rnode : 1 단계에 있는 노드를 가르킴 (한 단계씩 내려감)

height : 현재 높이
count : 노드 내에 NULL이 아닌 포인터의 수
slot : 64개의 포인터 배열
RADIX_TREE_MAP_SIZE = 1UL << RADIX_TREE_MAP_SHIFT(6) ---> 그래서 2의 6승 = 64
tags : 좀 더 뒤에서..간단한 풀이할 개념이..ㅜㅜ

아무튼 각 노드당 64개의 페이지의 포인터를 가지고 있기 때문에 트리가 2개일 경우에는 2^6 * 2^6 = 2^12 -1
즉, 4096 - 1 -> 4095개의 페이지의 포인터를 가질 수 있다.

그렇다면 라딕스 트리를 이용해서 페이지를 어떻게 쉽게 찾을 수 있을까???
바로 리눅스 페이징 시스템 개념을 다시 한번 쓰는 것이다. (페이지 32 비트 , 48 비트를 비트 별로 쪼개는 것.)

페이지 인덱스가 들어오면 비트 별로 쪼개는 것이다.
라딕스 트리가 1이라면 하위 6 비트로 slot 배열 인덱스로.
2라면 하위 12비트에서 상위 6비트는 1단계에서 하위 6비트는 2단계에서 사용된다.

'Linux Kernel' 카테고리의 다른 글

posix_fallocate  (0) 2016.08.16
System Memory  (0) 2016.08.13
Linear VS Physical Address  (0) 2016.08.10
Block I/O Operation  (0) 2016.08.06
What is wmb() in linux driver  (0) 2016.08.05

CRC

Algorithm2016. 8. 11. 07:53

OriginalSource: http://hayanmail.com/jsy/index.html?board=cizblog_zboard3&ln_mode=news_view&upcount=active&id=6&ct_sel=2


CRC-16/32

 

CRC(Cyclic Redundancy Check)는 시리얼 전송에서 데이타의 신뢰성을 검증하기 위한 에러 검출 방법의 일종이다.

간단한 에러 검출방법으로는 parity 비트에 의한 방법과 check-sum에 의한 에러 검출 방법이 있지만 parity 비트에 의한 방법은 데이타 중에 한꺼번에 2비트나 4비트가 변하게 되면 검출을 할 수 없고, check-sum에 의한 방법은 한 바이트에서 +1, 다른 바이트에서는 -1로 에러가 생기는 경우만 해도 에러는 검출 되지 않는다. 즉, 이들 방법으로는 에러를 검출해 낼 수 있는 확률이 대단히 낮다.

CRC에 의한 방법은 높은 신뢰도를 확보하며 에러 검출을 위한 오버헤드가 적고, 랜덤 에러나 버스트 에러를 포함한 에러 검출에 매우 좋은 성능을 갖는 것을 특징으로 한다.

이러한 CRC 방법으로 보통 2가지 종류가 사용 되는데, 원칩 마이크로 프로세서와 같이 간단한 용도에서는 CRC-16 이 사용되고, 이보다 더욱 정확한 에러 검출이 필요한 경우에는 CRC-32를 사용한다.

ZIP,ARJ,RAR 과 같은 압축 프로그램이나 플로피 디스크 등의 데이터 검증 용도에 널리 사용되고 있다.

* CRC 검증의 에러 확률 p = 2-k (여기서 k는 CRC 비트수)
예를들어 CRC16의 경우 에러 확율은 p = 2-16 = 1 / 65536 = 0.0000152587890625 = 0.0015%
반대로 데이터의 신뢰성은 1 - p = 0.9999847412109375 = 99.9984 %

 

기본 원리

n 비트의 주어진 정보가 있을때, 이를 k 비트 만큼 자리를 올리고 (K bit right shift) 미리 약속한 k 비트의 키 값으로 나누면 r 비트의 나머지가 남게 된다. 송신측에서는 원래의 정보 비트를 k 비트 rigth shift 와 r 비트의 나머지를 더해서 n+r 비트의 데이타를 만들어 보낸다.
수신측에서는 수신된 n+r 비트의 데이타를 키 값으로 나누어 보고 나머지가 정확히 0 이 되는지를 검사하면 된다.

이 때 k 가 16 비트이면 CRC-16, 32비트이면 CRC-32 가 되고 키 값으로는 수학자 들에 의해 정해진 값을 주로 사용한다.
CRC-16 에는 0x8005, CRC-32 에는 0x04c11db7 이 많이 사용된다. 그리고 r 비트의 나머지를 Frame Check Sequence(FCS)라고 부른다.

여기서 CRC 계산에 사용되는 modulo-2 연산의 세계를 살펴보자.

CRC 계산시의 사칙연산은 carry를 고려하지 않는다.
1 + 1 = 0 (carry는 생각하지 않음)
0 - 1 = 1
덧셈 연산은 뺄셈 연산과 결과가 같으며 XOR 연산과도 같다. 

다항식 표현 방법을 통해 CRC 계산 방법을 살펴보자.

(1) 2진 다항식으로 표시

예) 비트열 101 --> 다항식 x2 + 1

정보 다항식: 데이터 비트열의 다항식으로 P(x) = pn xn-1 + ... + p3x2 + p2x1 + p1
CRC 다항식: CRC 생성을 위한 다항식으로 G(x) (미리 정해진 키 값)
몫: Q(x)
나머지: R(x)
전송 데이타: T(x)

(2) CRC 계산 방법

P(x)를 k 비트 만큼 자리를 올리고( P(x)에 xk를 곱하는 것과 동일) G(x)로 나누면

xk P(x) = Q(x)*G(x) +/- R(x) 이다.

(k는 CRC 다항식의 최고 차수)

R(x) = dk xk-1 + .. + d2x1 + d1 ( R(x)의 최고 차수는 k-1)

비트열 dk ... d2 d1 을 FCS(Frame Check Sequence) 라 함

위 식에서 xk P(x) + R(x) 는 Q(x)*G(x) 와 같다.

즉, xk P(x) + R(x) 는 G(x)의 배수이므로 G(x) 로 나누면 나머지가 0 임을 알 수 있다.

결과적으로 전송되는 데이터 비트열 : pn ... p3 p2 p1 dk ... d2 d1

즉, 전송 T(x) = xk P(x) + R(x)

 

예) 데이터 비트열 110011 즉 P(x) =x5+x4+x1+x0, CRC 다항식G(x) = x4+x3+1, 즉 11001일 경우 FCS와 전송되는 비트열은?

xkP(x) = x4 (x5 + x4 + x1 + 1) = x9 + x8 + x5 + x4, 비트열로 표시하면 1100110000

                   100001
          ____________
11001 | 1100110000
            11001
          ____________
                     10000
                     11001
          ____________
                       1001    

xkP(x) = Q(x)G(x) - R(x)에서

Q(x) = x5 + x0 이므로,

R(x) = x3 + x0, ---> FCS는1001

따라서 전송되는 비트열 1100111001

 

연산의 최적화

CRC의 계산은 일반 나눗셈 명령을 이용해 구현할 수 없다. 1비씩 shift 하면서 XOR 연산을 통해 나머지를 구해야 한다.
하지만 정보 비트에 대해 하나하나씩 연산을 하는 것에는 분명 속도 개선의 여지가 있다.
실제 계산 시에는 모든 바이트에 대해 CRC 다항식에 대한 CRC값을 계산해 표로 만들어 두고 들어오는 데이타를 인덱스로 삼아 계산값을 바로 얻는 방법을 사용 한다.

CRC-16 C소스 : crc16h.c
CRC-32 C소스 : crc32h.c

Original Source: http://www.on-time.com/rtos-32-docs/rttarget-32/programming-manual/x86-cpu/protected-mode/virtual-linear-and-physical-addresses.htm


Virtual, Linear, and Physical Addresses

The 386 memory management can become quite confusing. Here is a summary of the different types of addresses and how one type is translated to another:

Virtual addresses are used by an application program. They consist of a 16-bit selector and a 32-bit offset. In the flat memory model, the selectors are preloaded into segment registers CS, DS, SS, and ES, which all refer to the same linear address. They need not be considered by the application. Addresses are simply 32-bit near pointers.

Linear addresses are calculated from virtual addresses by segment translation. The base of the segment referred to by the selector is added to the virtual offset, giving a 32-bit linear address. Under RTTarget-32, virtual offsets are equal to linear addresses since the base of all code and data segments is 0.

Physical addresses are calculated from linear addresses through paging. The linear address is used as an index into the Page Table where the CPU locates the corresponding physical address. If paging is not enabled, linear addresses are always equal to physical addresses. Under RTTarget-32, linear addresses are equal to physical addresses except for remapped RAM regions (see section RTLoc: Locating a Program, sections Virtual Command and FillRAM Command) and for memory allocated using the virtual memory manager.

Linear address is generated after page table mapping. Physical address is generated before page table mapping(ie paging).

Linear Adress,created by adding logical address to the base of segment, CS,DS,ES,SS,FSor GS. When Paging is enabled, the page tables are used to translate linear address to physical address.

On the Other Hand, Physical Address is nothing but, the address value that appears on pins of processor during a memory read/memory write operations.

In Short, we can say if paging is disabled linear address = physical address


'Linux Kernel' 카테고리의 다른 글

System Memory  (0) 2016.08.13
Radix Tree  (0) 2016.08.12
Block I/O Operation  (0) 2016.08.06
What is wmb() in linux driver  (0) 2016.08.05
What is the return address of kmalloc() ? Physical or Virtual?  (0) 2016.07.29

libpmem library

NVM2016. 8. 9. 06:41

Original Source: http://pmem.io/nvml/libpmem/


The libpmem library

libpmem provides low level persistent memory support. In particular, support for the persistent memory instructions for flushing changes to pmem is provided.

This library is provided for software which tracks every store to pmem and needs to flush those changes to durability. Most developers will find higher level libraries like libpmemobj to be much more convenient.

The libpmem man page contains a list of the interfaces provided.

libpmem Examples

The Basics

If you’ve decided to handle persistent memory allocation and consistency across program interruption yourself, you will find the functions in libpmem useful. It is important to understand that programming to raw pmem means you must create your own transactions or convince yourself you don’t care if a system or program crash leaves your pmem files in an inconsistent state. Libraries like libpmemobj provide transactional interfaces by building on these libpmem functions, but the interfaces in libpmem are non-transactional.

For this simple example, we’re just going to hard code a pmem file size of 4 kilobytes. The lines above create the file, make sure 4k is allocated, and map the file into memory. This illustrates one of the helper functions in libpmem: pmem_map() which takes a file descriptor and calls mmap(2) to memory map the entire file. Calling mmap() directly will work just fine – the main advantage of pmem_map() is that it tries to find an address where mapping is likely to use large page mappings, for better performance when using large ranges of pmem.

Since the system calls for memory mapping persistent memory are the same as the POSIX calls for memory mapping any file, you may want to write your code to run correctly when given either a pmem file or a file on a traditional file system. For many decades it has been the case that changes written to a memory mapped range of a file may not be persistent until flushed to the media. One common way to do this is using the POSIX call msync(2). If you write your program to memory map a file and use msync() every time you want to flush the changes media, it will work correctly for pmem as well as files on a traditional file system. However, you may find your program performs better if you detect pmem explicitly and use libpmem to flush changes in that case.

The libpmem function pmem_is_pmem() can be used to determine if the memory in the given range is really persistent memory or if it is just a memory mapped file on a traditional file system. Using this call in your program will allow you to decide what to do when given a non-pmem file. Your program could decide to print an error message and exit (for example: “ERROR: This program only works on pmem”). But it seems more likely you will want to save the result of pmem_is_pmem() as shown above, and then use that flag to decide what to do when flushing changes to persistence as later in this example program.

The novel thing about pmem is you can copy to it directly, like any memory. The strcpy()call shown on line 80 above is just the usual libc function that stores a string to memory. If this example program were to be interrupted either during or just after the strcpy() call, you can’t be sure which parts of the string made it all the way to the media. It might be none of the string, all of the string, or somewhere in-between. In addition, there’s no guarantee the string will make it to the media in the order it was stored! For longer ranges, it is just as likely that portions copied later make it to the media before earlier portions. (So don’t write code like the example above and then expect to check for zeros to see how much of the string was written.)

How can a string get stored in seemingly random order? The reason is that until a flush function like msync() has returned successfully, the normal cache pressure that happens on an active system can push changes out to the media at any time in any order. Most processors have barrier instructions (like SFENCE on the Intel platform) but those instructions deal with ordering in the visibility of stores to other threads, not with the order that changes reach persistence. The only barriers for flushing to persistence are functions like msync() or pmem_persist().

this example uses the is_pmem flag it saved from the previous call topmem_is_pmem(). This is the recommended way to use this information rather than callingpmem_is_pmem() each time you want to make changes durable. That’s becausepmem_is_pmem() can have a high overhead, having to search through data structures to ensure the entire range is really persistent memory.

For true pmem, the highlighted line 84 above is the most optimal way to flush changes to persistence. pmem_persist() will, if possible, perform the flush directly from user space, without calling into the OS. This is made possible on the Intel platform using instructions like CLWB and CLFLUSHOPT which are described in Intel’s manuals. Of course you are free to use these instructions directly in your program, but the program will crash with an undefined opcode if you try to use the instructions on a platform that doesn’t support them. This is where libpmem helps you out, by checking the platform capabilities on start-up and choosing the best instructions for each operation it supports.

The above example also uses pmem_msync() for the non-pmem case instead of callingmsync(2) directly. For convenience, the pmem_msync() call is a small wrapper aroundmsync() that ensures the arguments are aligned, as requirement of POSIX.

Buildable source for the libpmem manpage.c example above is available in the NVML repository.

Copying to Persistent Memory

Another feature of libpmem is a set of routines for optimally copying to persistent memory. These functions perform the same functions as the libc functions memcpy()memset(), andmemmove(), but they are optimized for copying to pmem. On the Intel platform, this is done using the non-temporal store instructions which bypass the processor caches (eliminating the need to flush that portion of the data path).

The first copy example, called simple_copy, illustrates how pmem_memcpy() is used.

The highlighted line, line 105 above, shows how pmem_memcpy() is used just like memcpy(3)except that when the destination is pmem, libpmem handles flushing the data to persistence as part of the copy.

Buildable source for the libpmem simple_copy.c example above is available in the NVML repository.

Separating the Flush Steps

There are two steps in flushing to persistence. The first step is to flush the processor caches, or bypass them entirely as explained in the previous example. The second step is to wait for any hardware buffers to drain, to ensure writes have reached the media. These steps are performed together when pmem_persist() is called, or they can be called individually by calling pmem_flush() for the first step and pmem_drain() for the second. Note that either of these steps may be unnecessary on a given platform, and the library knows how to check for that and do the right thing. For example, on Intel platforms, pmem_drain()is an empty function.

When does it make sense to break flushing into steps? This example, called full_copyillustrates one reason you might do this. Since the example copies data using multiple calls to memcpy(), it uses the version of libpmem copy that only performs the flush, postponing the final drain step to the end. This works because unlike the flush step, the drain step does not take an address range – it is a system-wide drain operation so can happen at the end of the loop that copies individual blocks of data.


Original Source: https://software.intel.com/sites/default/files/managed/b4/3a/319433-024.pdf

Efficient cache flushing

CLFLUSHOPT is defined to provide efficient cache flushing. 

CLWB instruction (Cache Line Write Back) writes back modified data of a cacheline similar to CLFLUSHOPT, but avoids invalidating the line from the cache (and instead transitions the line to non-modified state). CLWB attempts to minimize the compulsory cache miss if the same data is accessed temporally after the line is flushed.

Writes back to memory the cache line (if dirty) that contains the linear address specified with the memory operand from any level of the cache hierarchy in the cache coherence domain. The line may be retained in the cache hierarchy in non-modified state. Retaining the line in the cache hierarchy is a performance optimization (treated as a hint by hardware) to reduce the possibility of cache miss on a subsequent access. Hardware may choose to retain the line at any of the levels in the cache hierarchy, and in some cases, may invalidate the line from the cache hierarchy. The source operand is a byte memory location. 

The availability of CLWB instruction is indicated by the presence of the CPUID feature flag CLWB (bit 24 of the EBX register, see “CPUID — CPU Identification” in this chapter). The aligned cache line size affected is also indicated with the CPUID instruction (bits 8 through 15 of the EBX register when the initial value in the EAX register is 1). The memory attribute of the page containing the affected line has no effect on the behavior of this instruction. It should be noted that processors are free to speculatively fetch and cache data from system memory regions that are assigned a memory-type allowing for speculative reads (such as, the WB, WC, and WT memory types). PREFETCHh instructions can be used to provide the processor with hints for this speculative behavior. Because this speculative fetching can occur at any time and is not tied to instruction execution, the CLWB instruction is not ordered with respect to PREFETCHh instructions or any of the speculative fetching mechanisms (that is, data can be speculatively loaded into a cache line just before, during, or after the execution of a CLWB instruction that references the cache line). CLWB instruction is ordered only by store-fencing operations. For example, software can use an SFENCE, MFENCE, XCHG, or LOCK-prefixed instructions to ensure that previous stores are included in the write-back. CLWB instruction need not be ordered by another CLWB or CLFLUSHOPT instruction. CLWB is implicitly ordered with older stores executed by the logical processor to the same address. For usages that require only writing back modified data from cache lines to memory (do not require the line to be invalidated), and expect to subsequently access the data, software is recommended to use CLWB (with appropriate fencing) instead of CLFLUSH or CLFLUSHOPT for improved performance. Executions of CLWB interact with executions of PCOMMIT. The PCOMMIT instruction operates on certain store-to memory operations that have been accepted to memory. CLWB executed for the same cache line as an older store causes the store to become accepted to memory when the CLWB execution becomes globally visible. The CLWB instruction can be used at all privilege levels and is subject to all permission checking and faults associated with a byte load. Like a load, the CLWB instruction sets the A bit but not the D bit in the page tables. In some implementations, the CLWB instruction may always cause transactional abort with Transactional Synchronization Extensions (TSX). CLWB instruction is not expected to be commonly used inside typical transactional regions. However, programmers must not rely on CLWB instruction to force a transactional abort, since whether they cause transactional abort is implementation dependent.



'NVM' 카테고리의 다른 글

LD_PRELOAD rootkits  (0) 2016.08.26
libvmmalloc  (0) 2016.08.24
libnvdimm  (0) 2016.08.05
PCMSIM  (0) 2016.08.05
Persistent memory and page structures  (0) 2016.08.03

Original Source: http://studyfoss.egloos.com/5575220, http://studyfoss.egloos.com/5576850, http://studyfoss.egloos.com/5583458, http://studyfoss.egloos.com/5585801


Block I/O Operation

블록 장치는 개별 바이트 단위가 아닌 일정 크기(block) 단위로 접근하는 장치를 말하는 것으로 간단히 말하면 하드 디스크와 같은 대용량 저장 장치를 말한다. 전통적으로 이러한 블록 장치는 다른 (문자) 장치처럼 직접 다루는 대신 파일 시스템이라고 하는 추상화 계층을 통해 간접적으로 접근하게 되며 따라서 프로그래머는 해당 저장 장치가 어떠한 종류의 장치인지와는 무관하게 (또한 VFS에 의해 어떠한 파일 시스템인지와도 무관하게) 일관된 방식으로 (즉, 파일 및 디렉터리의 형태로) 이용하게 된다.

리눅스의 VFS 계층은 디스크 접근을 최소화하기 위해 페이지 캐시를 이용하여 한 번 접근한 디스크의 내용을 저장해 둔다. 하지만 여기서는 이러한 페이지 캐시의 작용은 건너뛰고 실제로 블록 장치와 I/O 연산을 수행하는 경우에 대해서 살펴보게 될 것이다.

블록 I/O의 시작 지점은 submit_bio() 함수이다. (일반적인 파일 시스템의 경우 buffer_head (bh)라는 구조체를 통해 디스크 버퍼를 관리하는데 이 경우 submit_bh() 함수가 사용되지만 이는 bh의 정보를 통해 bio 구조체를 할당하여 적절히 초기화한 후 다시 submit_bio() 함수를 호출하게 된다.)

이 함수는 I/O 연산의 종류 (간단하게는 READ 혹은 WRITE) 및 해당 연산에 대한 모든 정보를 포함하는 bio 구조체를 인자로 받는다. bio 구조체는 기본적으로 I/O를 수행할 디스크 영역의 정보와
I/O를 수행할 데이터를 저장하기 위한 메모리 영역의 정보를 포함한다. 여기서 몇가지 용어가 함께 사용되는데 혼동의 여지가 있으므로 간략히 정리하고 넘어가기로 한다.

먼저 섹터(sector)라는 것은 장치에 접근할 수 있는 최소의 단위이며 (H/W적인 특성이다) 대부분의 장치에서 512 바이트에 해당하므로, 리눅스 커널에서는 섹터는 항상 512 바이트로 가정하며 sector_t 타입은 (512 바이트의) 섹터 단위 크기를 나타낸다. (만약 해당 장치가 더 큰 크기의 섹터를 사용한다면 이는 장치 드라이버에서 적절히 변환해 주어야 한다)

블록(block)은 장치를 S/W적으로 관리하는 (즉, 접근하는) 크기로 섹터의 배수이다. 일반적으로 파일 시스템 생성 (mkfs) 시 해당 파일 시스템이 사용할 블록 크기를 결정하게 되며 현재 관리의 용이성을 위해 블록 크기는 페이지 크기 보다 크게 설정될 수 없다. 즉, 일반적인 환경에서 블록의 크기는 512B(?), 1KB, 2KB, 4KB 중의 하나가 될 것이다. 하나의 블록은 디스크 상에서 연속된 섹터로 이루어진다.

세그먼트(segment)는 장치와의 I/O 연산을 위한 데이터를 저장하는 "메모리" 영역을 나타내는 것으로 일반적으로는 페이지 캐시 내의 일부 영역에 해당 할 것이다. 하나의 블록은 메모리 상에서 동일한 페이지 내에 저장되지만 하나의 I/O 연산은 여러 블록으로 구성될 수도 있으므로 하나의 세그먼트는 (개념적으로) 여러 페이지에 걸칠 수도 있다.

블록 I/O 연산은 기본적으로 디스크에 저장된 데이터를 메모리로 옮기는 것 (READ) 혹은 메모리에 저장된 데이터를 디스크로 옮기는 것 (WRITE)이다. (장치의 특성에 따라 FLUSH, FUA, DISCARD 등의 추가적인 연산이 발생될 수도 있다.) I/O 연산이 여러 블록을 포함하는 경우 약간 복잡한 문제가 생길 수 있는데 이러한 블록 데이터가 디스크 혹은 메모리 상에서 연속되지 않은 위치에 존재할 수 있기 때문이다.

예를 들어 파일 시스템을 통해 어떠한 파일을 읽어들이는 경우를 생각해보자. 파일을 연속적으로 읽어들인다고 해도 이는 VFS 상에서 연속된 것으로 보이는 것일 뿐 실제 데이터는 디스크 곳곳에 흩어져있을 수도 있다. (많은 파일 시스템은 성능 향상을 위해 되도록 연속된 파일 데이터를 디스크 상에서도 연속된 위치에 저장하려고 시도하지만 시간이 지날 수록 단편화가 발생하므로 결국에는 어쩔 수 없이 이러한 현상이 발생하게 될 것이다.)

또한 디스크에서 읽어들인 데이터는 페이지 캐시 상에 저장되는데 페이지 캐시로 할당되는 메모리는 항상 개별 페이지 단위로 할당이 이루어지므로 메모리 상에서도 연속된 위치에 저장된다고 보장할 수 없다.

따라서 bio 구조체는 이러한 상황을 모두 고려하여 I/O 연산에 필요한 정보를 구성한다. 우선 하나의 bio은 디스크 상에서 연속된 영역 만을 나타낼 수 있다. 즉, 접근하려는 연속된 파일 데이터가 디스크 상에서 3부분으로 나뉘어져 있다면 세 개의 bio가 각각 할당되어 submit_bio() 함수를 통해 각각 전달될 것이다.

블록 I/O 연산 시 실제 데이터 복사는 대부분 DMA를 통해 이루어지게 되는데 이 때 (DMA를 수행하는) 장치는 물리 주소를 통해 메모리에 접근하게 되므로 설사 파일 매핑을 통해 파일 데이터를 저장한 페이지들이 (해당 프로세스의) 가상 메모리 상에서 연속된 위치에 존재한다고 하더라도 떨어진 페이지 프레임에 존재한다면 별도의 세그먼트로 인식할 것이다.

구식 장치의 경우 DMA를 수행할 때 디스크는 물론 메모리 상에서도 연속된 하나의 세그먼트 만을 지원했었다. 따라서 디스크 상에서 연속된 위치에 저장된 데이터라고 하더라도 메모리 상에서 연속되지 않았다면 하나의 I/O 연산을 통해 처리할 수 없는 상황이 발생하므로 여러 연산으로 분리해야 했었다. 하지만 장치가 scatter-gather DMA를 지원하거나 IO-MMU를 포함한 머신이라면 얘기가 달라진다. 현재 bio는 세그먼트를 bio_vec 구조체를 통해 저장하는데 세그먼트는 기본적으로 페이지의 형태로 저장되므로 이에 대한 모든 정보가 포함되며 장치가 한 I/O 당 여러 세그먼트를 지원할 수 있으므로 이를 배열(vector) 형태로 저장한다. 혹은 우연히도 디스크 상에 연속된 데이터가 메모리 상에서도 연속된 페이지에 저장되었을 수도 있다. 이 경우 별도의 페이지로 구성되었어도 물리적으로는 하나의 세그먼트로 처리한다. 또는 IO-MMU를 통해 떨어져있는 페이지들을 하나의 세그먼트 (연속된 주소)로 매핑할 수도 있다.


위 그림은 지금껏 설명한 bio의 구성을 보여준다. (설명을 간단히하기 위해 블록 크기와 페이지 크기가 동일한 환경을 고려하며 장치는 scatter-gather DMA 등을 통해 여러 세그먼트를 동시에 처리할 수 있다고 가정한다) 연속된 파일 주소 공간에 대한 I/O 요청은 디스크 상의 위치를 기준으로 3개의 bio로 나뉘어졌으며 각 bio는 해당 영역의 데이터를 담는 세그먼트를 여러 개 포함할 수 있다.

이번에는 submit_bio() 함수를 통해 bio가 전달되는 과정을 들여다보기로 하자.

submit_bio() 함수는 주어진 I/O 연산의 종류를 bio 구조체에 저장한 뒤 generic_make_request() 함수를 호출한다. I/O 연산의 종류 및 그에 따른 특성을 나타내기 위해 bio와 request 구조체는 REQ_* 형태의 플래그를 공유하며 이는 rq_flag_bits 열거형을 통해 정의되어 있고 위에서 설명한 I/O 연산 매크로들은 이 플래그들을 조합하여 만들어진다.

generic_make_request() 함수는 주어진 bio에 대해 장치 드라이버에 제공하는 방식(make_request_fn 콜백)을 통해 request를 만들어내는 작업을 수행한다.

여기서 bio는 앞서 살펴보았듯이 상위 계층 (VFS)에서 요청한 블록 I/O 연산에 대한 정보를 담고 있는 것이며 request는 실제로 장치 드라이버에서 장치와 실제 I/O 작업을 수행하는 것에 필요한 정보를 담고 있는 구조체이다.

이전 글에서 언급했듯이 블록 장치는 상대적으로 연산 속도가 매우 느리기 때문에 상위 계층에서 요청한 작업을 즉시 수행하지 않고 (I/O 스케줄러를 통해) 순서를 조정하게 되며 이 과정에서 여러 번에 걸쳐 요청된 bio들이 하나의 request로 합쳐지게 되는 경우도 있다.

이러한 작업들을 모두 처리하는 함수가 generic_make_request() 함수로써 장치 드라이버에서 I/O 연산에 필요한 여러 준비 작업들을 수행하게 되는데 몇몇 특별한 장치의 경우 이 과정이 재귀적으로 일어날 수 있기 때문에 이에 대한 대비를 위해 실제 처리는 __generic_make_request() 함수로 분리하였다.

S/W RAID (리눅스 커널에서는 MD (Multple Disks)라고 부른다) 또는 DM (Device Mapper)과 같은 장치는 커널에서 제공하는 특수 장치로 여러 물리적인 디스크 장치를 묶어서 마치 하나의 장치인 것 처럼 관리하는데, 이러한 장치에 대한 I/O 연산은 하위에 존재하는 여러 개의 실제 장치에 대한 I/O 연산으로 변경(clone)되어 수행되기도 하므로 이에 대한 재귀적인 처리 과정에서 커널 스택이 소진되는 문제가 발생할 수 있다. (direct-reclaim 시의 writeback과 같은 경우 이미 많은 양의 커널 스택이 사용된 상황일 것이다)

참고로 블록 계층에서의 메모리 할당은 매우 조심스럽게(?) 이루어지는데 앞서 말했다시피 이미 시스템의 메모리가 부족해진 상황에서 캐시로 사용되던 페이지들을 다른 용도로 재사용하기 위해 기존의 내용을 디스크에 기록해야 하는 경우가 많은데 이 때 디스크 I/O가 처리되기 때문이다. 즉, 메모리가 부족한 상황에서 메모리를 회수해야 하는 태스크가 (I/O 처리 과정에 필요한) 새로운 메모리를 요청하게 되는데 이미 메모리가 부족하므로 할당이 성공할 수 없고 따라서 해당 태스크가 대기 상태로 빠져 deadlock이 발생할 수 있는 문제를 안게 된다.

그래서 블록 I/O 처리 경로에서의 메모리 할당은 일반적으로 사용하는 GFP_KERNEL 매크로가 아닌,(I/O를 발생시키지 않는) GFP_NOIO 매크로를 통해 이루어지며 많은 경우 memory pool과 같은 기법을 이용하여 최악의 상황에서도 사용할 수 있도록 필요한 객체들을 사전에 미리 할당해 두는 방식을 사용한다.

generic_make_request() 함수는 현재 실행되는 태스크가 해당 함수를 재귀적으로 호출했는지 검사하기 위해 먼저 task_struct의 bio_list 필드를 검사한다.이 값이 NULL이 아니라면 재귀적으로 호출된 경우이므로 리스트에 현재 bio를 추가하고 종료한다. 그렇지 않다면 최초 호출이므로 스택에 할당된 bio_list 구조체로 bio_list 필드를 설정하고 실제로 요청을 처리하기 위해 __generic_make_request() 함수를 호출하며 호출이 완료된 후에는 그 사이에 재귀적으로 추가된 bio가 있는지 검사하여 있다면 이를 다시 수행한다. 리스트 내에 더 이상 bio가 존재하지 않는다면 bio_list 필드를 NULL로 설정하고 종료한다.

__generic_make_request() 함수도 또한 하나의 loop로 구현되어 있는데 마찬가지로 MD 혹은 DM과 같은 장치에서 해당 장치에 대한 I/O 요청을 그 하위의 실제 장치에 대한 I/O 요청으로 변경(remap)하는 경우가 있기 때문이다. 장치 드라이버는 주어진 bio를 실제 장치가 처리하기 위한 request로 만들기 위해 make_request_fn 콜백을 제공하는데 정상적인 경우 이 콜백 함수는 0을 리턴하여 loop 내부를 1번 만 수행하고 바로 종료한다. 하지만 위에서 말한 특수한 장치의 경우 0이 아닌 값을 리턴하여 bio가 다른 장치로 remap 되었음을 알려주면 다시 loop 내부를 수행하여 새로운 장치에 대해 필요한 검사를 수행한다.

loop 내부에서는 bio가 요청한 장치가 현재 사용 가능한 상태인지, 요청한 블록이 장치의 범위를 넘어서는지, FLUSH, FUA, DISCARD와 같은 특수 기능을 장치가 제공하는지 등을 검사하며 I/O를 요청한 장치가 디스크 파티션이라면 이를 전체 디스크에 대한 위치로 재조정한다. 또한 fault injection에 의한 I/O 요청 실패 상황을 검사하거나 block throttling 정책에 따라 현재 요청된 I/O를 잠시 대기 시킬 것인지 여부를 결정하게 된다.

이러한 모든 단계가 정상적으로 완료되면 드라이버에서 제공하는 make_request_fn 콜백을 호출한다.
일반적인 디스크 장치는 기본 구현인 __make_request() 함수를 콜백으로 등록하게 되며 이 과정에서 현재 bio를 장치에 전달하기 위해 필요한 request를 찾거나 새로 생성한다.

하지만 위에서 말한 MD 및 DM과 같은 복잡한 장치들은 물론 일반 파일을 디스크처럼 다루는 loop 장치와 메모리를 다루는 RAM 디스크 장치 (brd 모듈) 등은 request를 생성하지 않고 bio 구조체를 직접 이용하여 I/O 연산을 수행한다.

예를 들어 MD 장치의 구성 중에 여러 디스크를 마치 하나의 디스크인 것처럼 연결하는 linear 모드
(MD의 용어로는 personality라고 한다)가 있다. 이 경우 MD 장치로 들어온 요청은 make_request_fn 콜백으로 등록된 md_make_request() 함수에서 처리되는데 이는 다시 해당 장치의 personality에서 제공하는 make_request 콜백을 호출하여 결국 linear_make_request() 함수가 호출되게 된다.

linear_make_request() 함수는 MD 장치의 블록 번호에 해당하는 실제 장치를 찾은 후에 bio의 장치 및 섹터 정보를 적절히 변경하고 1을 리턴한다. 그러면 __generic_make_request() 함수 내의 loop가 새로운 장치에 대해 다시 수행되어 실제 디스크 장치로 I/O 요청이 전달되는 것이다. 만일 MD 장치에 대한 요청이 linear 모드로 연결된 실제 장치의 경계에 걸친 경우 이는 내부적으로 두 개의 bio로 분할되고 (bio_split() 함수 참고), 각각의 장치에 대해 다시 generic_make_request() 함수를 호출하므로 task_struct의 bio_list에 연결된 후 차례로 처리될 것이다.

bio를 통해 전달된 I/O 연산 요청은 각 블록 장치 드라이버에서 제공하는 make_request_fn 콜백을 통해 처리되는데 일반적인 디스크 장치의 경우 __make_request() 함수가 request를 할당하고 buffer bouncing, plugging, merging 등의 공통적인 작업을 처리한 후 이를 (elevator라고도 부르는) I/O 스케줄러에게 넘겨주게 된다. 여기서는 이 __make_request() 함수에 대해서 알아보기로 할 것이다.

가장 먼저 blk_queue_bounce() 함수를 통해 디스크 장치의 특성에 따라 페이지를 더 할당하는데
오래된 ISA 방식의 디스크인 경우 디스크 장치가 DMA를 통해 접근할 수 있는 주소의 범위가
16MB (24bit) 밖에 되지 않기 때문이다. (동일한 문제는 64bit 시스템의 PCI 장치에서도
4GB 이상의 메모리가 존재하는 경우에 발생할 수 있다.)

이 경우 전달된 bio의 세그먼트에 해당하는 페이지를 장치가 접근할 수 없으므로 접근할 수 있는 영역의 페이지 (ZONE_DMA/DMA32)를 새로 할당한 후 (이를 bounce buffer라고 한다) 이를 이용하여 실제 I/O를 대신 처리하는 방법을 사용해야 한다.

이 과정이 완료되면 I/O 요청을 드라이버에게 전달하기 위해 request 구조체를 할당하게 되는데 그 전에 기존의 request에 현재 bio가 merge 될 수 있는지를 먼저 검사하게 된다. 일단 request 구조체에 대해서 먼저 간략히 살펴볼 필요가 있다.

request 구조체는 기본적으로는 (bio와 동일하게) 디스크 상에서 연속된 영역에 해당하는 I/O 연산 요청에 대한 정보를 포함하는데 추가적으로 드라이버에서 사용할 여러 low-level 자료 구조를 포함/참조하고 있다. 특히나 세그먼트 정보는 이미 bio 구조체에 저장되어 있으므로 이를 그대로 이용하며
만약 연속된 디스크 영역에 여러 bio가 전달된 경우 이를 하나의 리스트로 연결하여 관리한다.

아래는 전체 request 구조체 중에서 현재 관심있는 부분 만을 표시한 것이다.

include/linux/blkdev.h:

struct request {
    struct list_head queuelist;

    ...

    struct request_queue *q;

    unsigned int cmd_flags;
    enum rq_cmd_type_bits cmd_type;

    ...

    ⁄* the following two fields are internal, NEVER access directly *⁄
    unsigned int __data_len;    ⁄* total data len *⁄
    sector_t __sector;          ⁄* sector cursor *⁄

    struct bio *bio;
    struct bio *biotail;

    struct hlist_node hash;      ⁄* merge hash *⁄

    ⁄*
     * The rb_node is only used inside the io scheduler, requests
     * are pruned when moved to the dispatch queue. So let the
     * completion_data share space with the rb_node.
     *⁄
    union {
        struct rb_node rb_node;    ⁄* sort/lookup *⁄
        void *completion_data;
    };

    ...

    ⁄* Number of scatter-gather DMA addr+len pairs after
     * physical address coalescing is performed.
     *⁄
    unsigned short nr_phys_segments;

    ...
};


request는 궁극적으로 해당 장치에서 제공하는 request_queue로 전달되어 처리되는데 (실제로 전달되는 순서는 I/O 스케줄러에서 조정한다) q 필드는 이 request가 전달될 큐를 가리키며 queuelist 필드는 request_queue 내의 리스트를 관리하기 위해 필요한 포인터이다. cmd_flags는 앞서 bio에서 살펴보았듯이 해당 I/O 연산의 특성을 알려주는 REQ_* 형태의 플래그이며 cmd_type은 일반적인 경우 REQ_TYPE_FS 값으로 설정된다. (filesystem 연산)

__sector는 해당 request가 접근하는 디스크 상의 위치를 섹터 단위로 저장한 것이며 __data_len은 해당 request가 처리하는 데이터의 길이를 바이트 단위로 저장한 것이다. (이 필드들은 드라이버에서 요청을 처리하는 도중에 갱신될 수 있으므로 외부에서 접근하면 안된다)

bio와 biotail은 해당 request에 포함된 bio의 목록으로 merge 시에 확장될 수 있으며 hash는 merge할 request를 빨리 찾을 수 있도록 해시 테이블을 구성하기 위해 필요하다. (merge 과정에 대해서는 잠시 후에 살펴볼 것이다.)

rb_node 필드는 I/O 스케줄러가 request를 디스크 상의 위치를 통해 정렬하기 위해 사용되며 nr_phys_segments는 해당 request가 포함하는 총 메모리 세그먼트의 수를 저장한다.

이제 merge 과정에 대해서 알아보기로 하자. submit_bio() 함수를 통해 요청된 (최초) bio는 request 형태로 변경될 것이다. 그런데 바로 후에 (아마도 filesystem 계층에서) 디스크 상에서 연속된 영역에 대해 다시 submit_bio()를 호출하여 bio를 요청하는 경우가 있을 수 있다.

이 경우 최초에 생성된 request에 두 번째로 요청된 bio가 포함되게 되며 __sector 및 __data_len 필드는 필요에 따라 적절히 변경될 것이고 bio와 biotail 필드는 각각 첫번째 bio와 두번째 bio를 가리키게 될 것이다. (각각의 bio는 내부의 bi_next 필드를 통해 연결된다)

그럼 문제는 주어진 bio를 merge할 request를 어떻게 찾아내느냐 인데 (위에서 설명한 아주 단순한 경우는 바로 이전에 생성된 request를 찾은 경우였지만 디스크 접근 패턴이 복잡한 경우는 여러 request들을 검색해 보아야 할 것이다.) 이를 위해 기본적으로 각 디스크의 I/O 스케줄러는 (위에서 언급한) 해시 테이블을 유지한다.

해시 테이블은 request가 접근하는 가장 마지막 섹터의 경계를 기준으로 구성하는데 이는 디스크 접근이 보통 섹터 번호가 증가하는 순으로 이루어지는 경우가 많기 때문일 것이다. 이 경우 원래의 request가 접근하는 제일 뒤쪽에 새로운 bio가 연결되므로 이를 back merge라고 부른다. 반대로 원래의 request보다 앞쪽에 위치하는 bio가 요청된 경우를 front merge라고 한다. back merge의 경우는 항상 가능하지만 front merge의 경우는 I/O 스케줄러에 따라 허용하지 않을 수도 있다. 물론 이 외에도 merge가 되려면 해당 request와 bio는 호환가능한 속성을 가져야 한다.

또한 sysfs를 통해 I/O 스케줄러의 merge 시도 여부를 제어할 수가 있는데예를 들어 sda라는 디스크의 경우 /sys/block/sda/queue/nomerges 파일의 값에

  • 0을 쓰면 항상 (해시 테이블을 검색하여) 가능한 경우 merge를 허용하고,
  • 1을 쓰면 바로 이전에 생성 또는 merge된 request와의 merge 만을 허용하며
  • 2를 쓰면 merge를 허용하지 않게 된다.

하지만 이러한 I/O 스케줄러의 해시 테이블은 각 디스크 별로 유지되기 때문에 해당 디스크에 접근하려는 여러 태스크는 동기화를 위해 lock을 필요로하게 된다. 이는 많은 디스크 I/O가 발생하는 시스템에서 성능 상 좋지 않은 효과를 줄 수 있는데 이를 위해 이러한 공유 해시 테이블에 접근하기 전에 먼저 각 태스크 별로 유지하는 plugged list를 검사하여 merge가 가능한 request가 존재하는지 확인하게 된다.

plugged list는 이른바 'block device plugging'이라는 기능을 구현한 것인데 이는 디스크의 동작 효율을 높이기 위한 기법으로, 디스크가 idle 상태였다면 request가 요청된 즉시 처리하지 않고 조금 더 기다림으로써 여러 request를 모아서 한꺼번에 처리하거나 merge될 시간을 벌어주는 효과를 얻게 된다.

즉, 디스크에 대한 접근이 발생하면 plugged 상태로 되어 I/O 스케줄러가 잠시 request를 보관하며
이후 특정 조건이 만족된 경우 (일정 시간이 경과하거나, 충분히 많은 I/O 요청이 발생한 경우) 장치가 (자동으로) unplug되어 주어진 request들을 실제로 처리하기 시작하는 형태였다.

하지만 2.6.39 버전부터 plugging 방식이 태스크가 직접 unplug 하는 식으로 변경되면서 태스크 별로 I/O 스케줄러에 request를 넘기기 전에 자신이 생성한 request를 리스트 형태로 유지하게 되었다. 따라서 이는 공유되지 않으므로 불필요한 lock contention을 줄일 수 있다.

단 이 per-task plugging 방식은 선택적인 것이므로 __make_request() 실행 당시 해당 태스크는 이 기능을 이용하지 않을 수도 있다.

이렇게 plugged list와 I/O 스케줄러 (혹은 엘리베이터)의 request를 검색한 후에도 merge할 마땅한 request를 찾지 못했다면 해당 bio를 위한 request를 새로 생성한다. 마찬가지로 request 구조체를 할당할 때도 GFP_NOIO 플래그를 사용하며 mempool 인터페이스를 사용하여 비상 시를 위한 여분의 구조체를 미리 준비해 둔다.

또한 각 디스크 (request_queue)에는 처리할 수 있는 request의 최대값이 정해져 있어서 그 이상 request를 생성하지 못하도록 제어하는데 기본값으로는 BLKDEV_MAX_RQ (128)이 사용되며 이에 따라 해당 디스크의 congestion 상태를 판단하기 위한 threshold 값이 결정된다. 이 경우 113개 이상의 request가 대기 중이면 디스크가 병목 현상을 겪고 있다고 판단하며 대기 중인 request의 수가 다시 103개 이하로 떨어지면 정상 상태로 회복되었음을 인식한다.

따라서 request 할당 시 이 threshold 값을 보고 적절히 디스크 상태를 설정하여 상위 계층에서 I/O 요청을 생성하는 속도를 조절할 수 있도록 하고 있다.

만약 병목 현상이 일어나고 있는 상황에서도 계속 I/O 요청이 발생하여 결국 할당된 request의 수가 최대값에 다다르면 디스크 (request_queue)가 가득찼음을 나타내는 플래그를 설정하여 더 이상 request를 생성하지 못하도록 하되, 단 현재 태스크는 batcher task로 설정하여 얼마간의 (함께 요청된) request를 더 생성할 수 있도록 배려하고 있다. 또한 request 할당 시 메모리 부족으로 인해 잠시 sleep되었던 경우에도 해당 태스크를 batcher task로 설정한다.

이렇게 request를 할당받고 난 후에는 per-task plugging을 이용하는 경우라면 해당 request를 plugged list에 연결하고 그렇지 않은 경우라면 I/O 스케줄러에 전달한 뒤 바로 디스크 드라이버에게 I/O를 요청한다.

지금까지 상위 (filesystem) 계층에서 요청된 I/O 연산이 bio를 거쳐 request로 만들어지는 과정을 살펴보았다. 이제 이렇게 생성된 request가 I/O 스케줄러 단에서 처리되는 방식을 알아볼 것이다.

앞서 살펴보았듯이 생성된 request는 대부분 (per-task) plugging 기능이 적용된 상태일 것이므로 (직접적인 read/write의 경우는 물론 read-ahead, writeback의 경우도 이에 해당한다) I/O 스케줄러에게 전달되기에 앞서 plugged list에 잠시 보관된다.

plugging 기능을 사용하려면 해당 함수의 스택에 blk_plug 구조체를 할당하고 먼저 blk_start_plug() 함수를 호출한 후에 I/O 연산을 발생시키고 마지막으로 blk_finish_plug() 함수를 호출하면 된다.

blk_start_plug() 함수는 주어진 blk_plug 구조체를 적절히 초기화한 후에 현재 태스크의 plug 필드에 저장하는데, 만약 blk_start_plug() 함수가 중첩된 실행 경로 상에서 여러 번 호출되었다면 제일 첫 번째로 호출된 경우에만 plug 필드를 저장한다. 이는 plugging 로직이 가장 상위 수준에서 처리될 수 있도록 보장해 준다.

blk_finish_plug() 함수는 태스크의 plug 필드와 인자로 주어진 blk_plug 구조체가 일치하는 경우에만 동작하며, 대응하는 start 함수와 현재 finish 함수 사이에서 발생한 I/O 연산 (request)들을 모두
I/O 스케줄러에게 전달하고 (insert) 실제로 드라이버가 I/O를 실행하도록 한다. request를 I/O 스케줄러에게 전달하는 방식은 request의 종류 및 상황에 따라 몇 가지 정책이 적용된다.

만약 plugged list에 request가 존재하는 상황에서 어떠한 이유로 인해 현재 태스크가 더 이상 실행되지 못하고 (자발적으로!) sleep 해야한다면 kblockd 스레드가 대신 plugged list를 넘겨받아 I/O 스케줄러에게 전달한 뒤에 I/O 연산을 실행한다.

plugged list 내의 request들이 I/O 스케줄러에게 전달되는 순간 다시 한번 merge가 가능한지 검사하게 되는데 이는 여러 태스크들이 동시에 디스크 상의 비슷한 위치에 접근하는 경우 각각의 태스크들은 자신의 plugged list에 포함되어 다른 태스크들은 접근하지 못하던 request들이 이제 공유되므로
새로이 merge될 가능성이 있기 때문이다. 이러한 정책은 ELEVATOR_INSERT_SORT_MERGE로 나타내며, plugging 기법을 이용하지 않을 시에는 이러한 merge 시도를 할 필요가 없으므로ELEVATOR_INSERT_SORT 정책이 사용된다.

I/O 스케줄러는 주어진 request들을 디스크 상의 위치에 따라 배열하여 seek time을 최소화하기 위해 노력하는데, 이 때 기본적으로 디스크의 헤드가 한 쪽 방향으로만 일정하게 움직이도록 하므로 이를 엘리베이터 (elevator)라고도 부른다. (물론 세부 동작은 각 I/O 스케줄러마다 다르다)

이를 위해서는 I/O 스케줄러 내부에 request들을 (잘 정렬하여) 보관할 자료구조가 필요한데 여기서는 rb tree (red-black tree)가 사용되며, 앞서 살펴보았듯이 (merge를 위해) 정렬된 rb tree 내의 특정 request를 빨리 찾아내기 위해 별도의 해시 테이블을 가지고 있다. 이렇게 rb tree 내에 보관된 request들은 REQ_SORTED라는 플래그를 추가하여 표시한다.

하지만 FLUSH/FUA request에 대해서는 약간 다른 ELEVATOR_INSERT_FLUSH 정책을 취하게 되는데
이러한 request들은 해당 디스크의 특성에 따라 다르게 처리될 수 있으며 또한 일반적인 merge를 지원하는 대신 중첩된 flush 요청을 한꺼번에 처리하는 기법을 사용하기 때문이다.

앞서 살펴보았듯이 FLUSH는 디스크 내부의 write-back 캐시의 내용을 실제 디스크에 저장하라는 의미이며 FUA는 write-back 캐시가 없는 것처럼 현재 데이터를 디스크에 직접 기록하라는 의미이다.
따라서 디스크가 내부 캐시를 가지지 않는 경우라면 FLUSH/FUA는 아무런 의미가 없다. 또한 캐시를 가진 디스크라고 하더라도 FUA 지원 여부는 선택적이므로 지원하지 않는 디스크의 경우 FUA request가 들어오면 이를 다시 FLUSH로 변경하여 처리하게 된다.

특히 FUA request의 경우 write할 데이터와 함께 요청되므로 최악(?)의 경우 하나의 (FLUSH & FUA) request는 다음과 같이 세 단계로 나누어 처리되어야 한다.

 (pre) FLUSH + WRITE + (post) FLUSH


따라서 FLUSH/FUA request는 REQ_FLUSH_SEQ 플래그를 추가하여 이러한 과정을 거치고 있음을 나타내며 이에 대한 추가적인 정보를 request 구조체 내의 flush (구조체) 필드에 저장하고 있다. 또한 이러한 request를 여러 태스크가 동시에 요청하는 경우 FLUSH 연산이 여러 차례 실행될 수 있으나
그 사이 데이터가 write 되지 않았다면 실질적으로 의미가 없으므로 (캐시 내의 모든 데이터가 이미 저장되었다) 이러한 중첩된 FLUSH 연산을 한 번만 수행해도 동일한 효과를 얻을 수 있게 될 것이다.

따라서 이러한 FLUSH/FUA request를 효율적으로 처리하기 위해 별도의 queue를 유지하며 총 2개의 리스트를 통해 하나의 FLUSH 요청이 실행되는 동안 발생된 FLUSH request들은 다른 리스트에 대기시키는 double buffering 방식을 이용하여 중첩된 request들을 한꺼번에 완료시키게 된다. 이렇게 I/O 스케줄러에게 전달된 request는 최종적으로 dispatch queue로 전달된다. 이렇게 전달된 request는 더 이상 merge될 수 없으므로 해시 테이블에서 제거되며 dispatch queue 내에서 디스크 섹터 번호를 기준으로 정렬된다 (단, 이미 처리 중이거나 REQ_SOFTBARRIER 플래그가 설정된 request들은 더 이상 정렬할 수 없으므로 그 이후의 request들만을 고려하게 된다).

dispatch queue 내의 request들은 순서대로 드라이버에 의해 처리되며 이렇게 request의 처리를 실제로 시작하는 것을 dispatch 혹은 issue라고 부른다. dispatch된 request들은 REQ_STARTED 플래그를 추가로 설정하며 queue에서 제거되며 디스크 오류로 인해 request가 오랫동안 완료되지 못하는 경우를 방지하기 위해 타이머를 설정한다. dispatch queue가 비게되면 드라이버는 I/O 스케줄러에게 새로운 request를 queue에 추가하도록 요청한다. request가 더이상 존재하지 않거나 I/O 스케줄러가 dispatch queue로 전달하지 않으면 처리는 종료된다.

지금껏 블록 장치 I/O 연산이 전달되는 과정을 간략히 살펴보았는데 리눅스 커널의 블록 서브시스템 관리자이기도 한 Jens Axboe님이 만든 blktrace 도구를 이용하면 현재 시스템 내의 디스크 장치의 I/O 과정을 한 눈에 알아볼 수 있는 방법을 제공한다.

만일 기본적인 출력 내용을 터미널 상에서 확인하고 싶다면 단순히 btrace라는 스크립트를 이용할 수 있다. 그 외의 자세한 옵션은 blktrace 및 blkparse의 man 페이지를 참조하기 바란다. 아래는 내 시스템에서의 출력 내용 중 일부이다.

# btrace /dev/sda
  ...
  8,0    0       60    10.168088873   178  A  WS 353925552 + 8 <- (8,5) 46516656
  8,0    0       61    10.168089576   178  Q  WS 353925552 + 8 [jbd2/sda5-8]
  8,0    0       62    10.168097323   178  G  WS 353925552 + 8 [jbd2/sda5-8]
  8,0    0       63    10.168098432   178  P   N [jbd2/sda5-8]
  8,0    0       64    10.168100785   178  A  WS 353925560 + 8 <- (8,5) 46516664
  8,0    0       65    10.168101033   178  Q  WS 353925560 + 8 [jbd2/sda5-8]
  8,0    0       66    10.168102298   178  M  WS 353925560 + 8 [jbd2/sda5-8]
  8,0    0       67    10.168104627   178  A  WS 353925568 + 8 <- (8,5) 46516672
  8,0    0       68    10.168104843   178  Q  WS 353925568 + 8 [jbd2/sda5-8]
  8,0    0       69    10.168105513   178  M  WS 353925568 + 8 [jbd2/sda5-8]
  8,0    0       70    10.168106517   178  A  WS 353925576 + 8 <- (8,5) 46516680
  8,0    0       71    10.168106744   178  Q  WS 353925576 + 8 [jbd2/sda5-8]
  8,0    0       72    10.168107411   178  M  WS 353925576 + 8 [jbd2/sda5-8]
  8,0    0       73    10.168109205   178  A  WS 353925584 + 8 <- (8,5) 46516688
  8,0    0       74    10.168109435   178  Q  WS 353925584 + 8 [jbd2/sda5-8]
  8,0    0       75    10.168110081   178  M  WS 353925584 + 8 [jbd2/sda5-8]
  8,0    0       76    10.168111110   178  A  WS 353925592 + 8 <- (8,5) 46516696
  8,0    0       77    10.168111328   178  Q  WS 353925592 + 8 [jbd2/sda5-8]
  8,0    0       78    10.168111953   178  M  WS 353925592 + 8 [jbd2/sda5-8]
  8,0    0       79    10.168112970   178  A  WS 353925600 + 8 <- (8,5) 46516704
  8,0    0       80    10.168113266   178  Q  WS 353925600 + 8 [jbd2/sda5-8]
  8,0    0       81    10.168113923   178  M  WS 353925600 + 8 [jbd2/sda5-8]
  8,0    0       82    10.168115804   178  A  WS 353925608 + 8 <- (8,5) 46516712
  8,0    0       83    10.168116019   178  Q  WS 353925608 + 8 [jbd2/sda5-8]
  8,0    0       84    10.168116656   178  M  WS 353925608 + 8 [jbd2/sda5-8]
  8,0    0       85    10.168118495   178  A  WS 353925616 + 8 <- (8,5) 46516720
  8,0    0       86    10.168118722   178  Q  WS 353925616 + 8 [jbd2/sda5-8]
  8,0    0       87    10.168119371   178  M  WS 353925616 + 8 [jbd2/sda5-8]
  8,0    0       88    10.168121449   178  A  WS 353925624 + 8 <- (8,5) 46516728
  8,0    0       89    10.168121665   178  Q  WS 353925624 + 8 [jbd2/sda5-8]
  8,0    0       90    10.168122304   178  M  WS 353925624 + 8 [jbd2/sda5-8]
  8,0    0       91    10.168123327   178  A  WS 353925632 + 8 <- (8,5) 46516736
  8,0    0       92    10.168123554   178  Q  WS 353925632 + 8 [jbd2/sda5-8]
  8,0    0       93    10.168124212   178  M  WS 353925632 + 8 [jbd2/sda5-8]
  8,0    0       94    10.168125241   178  A  WS 353925640 + 8 <- (8,5) 46516744
  8,0    0       95    10.168125462   178  Q  WS 353925640 + 8 [jbd2/sda5-8]
  8,0    0       96    10.168126087   178  M  WS 353925640 + 8 [jbd2/sda5-8]
  8,0    0       97    10.168128954   178  I  WS 353925552 + 96 [jbd2/sda5-8]
  8,0    0        0    10.168131125     0  m   N cfq178 insert_request
  8,0    0        0    10.168131926     0  m   N cfq178 add_to_rr
  8,0    0       98    10.168133660   178  U   N [jbd2/sda5-8] 1
  8,0    0        0    10.168135051     0  m   N cfq workload slice:100
  8,0    0        0    10.168136148     0  m   N cfq178 set_active wl_prio:0 wl_type:1
  8,0    0        0    10.168136908     0  m   N cfq178 Not idling. st->count:1
  8,0    0        0    10.168138014     0  m   N cfq178 fifo=          (null)
  8,0    0        0    10.168138615     0  m   N cfq178 dispatch_insert
  8,0    0        0    10.168139739     0  m   N cfq178 dispatched a request
  8,0    0        0    10.168140355     0  m   N cfq178 activate rq, drv=1
  8,0    0       99    10.168140588   178  D  WS 353925552 + 96 [jbd2/sda5-8]
  8,0    0      100    10.168534375     0  C  WS 353925552 + 96 [0]
  8,0    0        0    10.168554570     0  m   N cfq178 complete rqnoidle 1
  8,0    0        0    10.168555455     0  m   N cfq178 set_slice=120
  8,0    0        0    10.168556271     0  m   N cfq178 Not idling. st->count:1
  8,0    0        0    10.168556774     0  m   N cfq schedule dispatch
  ...


여기서 주의깊게 봐야할 부분은 알파벳 약자로 이루어진 6번째와 7번째 열 부분이다. 6번째 열이 나타내는 것은 해당 request가 처리되는 과정을 나타내며 (아래에서 설명) 7번째 열이 나타내는 것은 request의 종류로 여기서 WS는 sync write, N은 none에 해당한다. 6번째 열을 자세히 살펴보면 약간의 규칙성을 발견할 수 있는데 (첫번째 request는 제외) 먼저 A는 remap의 약자로 (8,5) 즉 /dev/sda5 파티션에 대한 I/O가 /dev/sda 디스크 전체에 대한 위치로 변환된 것을 뜻한다. 다음은 Q인데 이것은 queue의 약자로 make_request_fn이 실행되어 bio의 처리가 시작되었음을 뜻한다. 다음은 G인데 이것은 get (request)의 약자로 request 구조체가 하나 할당되었음을 뜻한다. 다음은 P인데 이것은 plug의 약자로 request가 plugged list에 포함되었음을 뜻한다.

이후의 요청들은 모두 A -> Q -> M의 과정을 거치는데, A와 Q는 위와 동일하고 M은 merge의 약자로 요청된 bio가 (앞선) request와 통합되었음을 뜻하는 것이며 8번째 열은 해당 bio의 시작 섹터 번호 및 크기임을 고려하면 연속된 요청이란 것을 쉽게 알 수 있다. 그 아래쪽에 I가 보이는데 이것은 insert의 약자로 앞서 생성(되고 merge)된 request가 I/O 스케줄러에게 전달되었음을 뜻한다. 그 바로 아래는 실제 request가 아닌 message를 의미하는 m이 있으며 (이는 CFQ 스케줄러에서 출력한 메시지이다) 지금은 무시하고 넘어가도 좋다. 다음은 U인데 이것은 unplug의 약자로 plugged list 내의 request들을 I/O 스케줄러에게 모두 전달했음을 뜻한다. 다음은 D인데 이것은 dispatch의 약자로 드라이버에게 I/O 연산의 실행을 시작하라고 요청하였음을 뜻한다. 다음은 C인데 이것은 complete의 약자로 dispatch된 request의 처리가 완료되었음을 뜻하는 것이다.

위의 경우 8섹터 (= 4KB) 크기의 bio 12개가 순서대로 요청되어 96섹터 (= 48KB) 크기의 한 request로
merge된 후 한 번에 처리되는 것을 볼 수 있었다.

지금까지 살펴본 과정을 그림으로 나타내면 다음과 같다.



'Linux Kernel' 카테고리의 다른 글

Radix Tree  (0) 2016.08.12
Linear VS Physical Address  (0) 2016.08.10
What is wmb() in linux driver  (0) 2016.08.05
What is the return address of kmalloc() ? Physical or Virtual?  (0) 2016.07.29
Memory Mapping  (0) 2016.07.28

libnvdimm

NVM2016. 8. 5. 10:46

Original Source: 

https://github.com/torvalds/linux/blob/master/Documentation/nvdimm/nvdimm.txt

 LIBNVDIMM: Non-Volatile Devices

     libnvdimm - kernel / libndctl - userspace helper library

  • Glossary
  • Overview
  •    Supporting Documents
  • LIBNVDIMM PMEM and BLK
  • Why BLK?
  •    PMEM vs BLK
  •        BLK-REGIONs, PMEM-REGIONs, Atomic Sectors, and DAX
  • Example NVDIMM Platform
  • LIBNVDIMM Kernel Device Model and LIBNDCTL Userspace API
  •    LIBNDCTL: Context
  •        libndctl: instantiate a new library context example
  •    LIBNVDIMM/LIBNDCTL: Bus
  •        libnvdimm: control class device in /sys/class
  •        libnvdimm: bus
  •        libndctl: bus enumeration example
  •    LIBNVDIMM/LIBNDCTL: DIMM (NMEM)
  •        libnvdimm: DIMM (NMEM)
  •        libndctl: DIMM enumeration example
  •    LIBNVDIMM/LIBNDCTL: Region
  •        libnvdimm: region
  •        libndctl: region enumeration example
  •        Why Not Encode the Region Type into the Region Name?
  •        How Do I Determine the Major Type of a Region?
  •    LIBNVDIMM/LIBNDCTL: Namespace
  •        libnvdimm: namespace
  •        libndctl: namespace enumeration example
  •        libndctl: namespace creation example
  •        Why the Term "namespace"?
  •    LIBNVDIMM/LIBNDCTL: Block Translation Table "btt"
  •        libnvdimm: btt layout
  •        libndctl: btt creation example
  • Summary LIBNDCTL Diagram


Glossary

  • PMEM: A system-physical-address range where writes are persistent. A block device composed of PMEM is capable of DAX. A PMEM address range may span an interleave of several DIMMs.
  • BLK: A set of one or more programmable memory mapped apertures provided by a DIMM to access its media. This indirection precludes the performance benefit of interleaving, but enables DIMM-bounded failure modes.
  • DPA: DIMM Physical Address, is a DIMM-relative offset.  With one DIMM in the system there would be a 1:1 system-physical-address: DPA association. Once more DIMMs are added a memory controller interleave must be decoded to determine the DPA associated with a given system-physical-address.  BLK capacity always has a 1:1 relationship with a single-DIMM's DPA range.
  • DAX: File system extensions to bypass the page cache and block layer to mmap persistent memory, from a PMEM block device, directly into a process address space.
  • DSM: Device Specific Method: ACPI method to to control specific device - in this case the firmware.
  • DCR: NVDIMM Control Region Structure defined in ACPI 6 Section 5.2.25.5. It defines a vendor-id, device-id, and interface format for a given DIMM.
  • BTT: Block Translation Table: Persistent memory is byte addressable. Existing software may have an expectation that the power-fail-atomicity of writes is at least one sector, 512 bytes.  The BTT is an indirection table with atomic update semantics to front a PMEM/BLK block device driver and present arbitrary atomic sector sizes.
  • LABEL: Metadata stored on a DIMM device that partitions and identifies (persistently names) storage between PMEM and BLK.  It also partitions BLK storage to host BTTs with different parameters per BLK-partition.
  • Note that traditional partition tables, GPT/MBR, are layered on top of a BLK or PMEM device.

Overview

The LIBNVDIMM subsystem provides support for three types of NVDIMMs, namely, PMEM, BLK, and NVDIMM devices that can simultaneously support both PMEM and BLK mode access.  These three modes of operation are described by the "NVDIMM Firmware Interface Table" (NFIT) in ACPI 6.  While the LIBNVDIMM implementation is generic and supports pre-NFIT platforms, it was guided by the superset of capabilities need to support this ACPI 6 definition for NVDIMM resources. The bulk of the kernel implementation is in place to handle the case where DPA accessible via PMEM is aliased with DPA accessible via BLK.  When that occurs a LABEL is needed to reserve DPAfor exclusive access via one mode a time.

Supporting Documents

ACPI 6: http://www.uefi.org/sites/default/files/resources/ACPI_6.0.pdf

NVDIMM Namespace: http://pmem.io/documents/NVDIMM_Namespace_Spec.pdf

DSM Interface Example: http://pmem.io/documents/NVDIMM_DSM_Interface_Example.pdf

Driver Writer's Guide: http://pmem.io/documents/NVDIMM_Driver_Writers_Guide.pdf


Git Trees

LIBNVDIMM: https://git.kernel.org/cgit/linux/kernel/git/djbw/nvdimm.git

LIBNDCTL: https://github.com/pmem/ndctl.git

PMEM: https://github.com/01org/prd


LIBNVDIMM PMEM and BLK

Prior to the arrival of the NFIT, non-volatile memory was described to a system in various ad-hoc ways.  Usually only the bare minimum was provided, namely, a single system-physical-address range where writes are expected to be durable after a system power loss.  Now, the NFIT specification standardizes not only the description of PMEM, but also BLK and platform message-passing entry points for control and configuration.

For each NVDIMM access method (PMEM, BLK), LIBNVDIMM provides a block device driver:

1. PMEM (nd_pmem.ko): Drives a system-physical-address range. This range is contiguous in system memory and may be interleaved (hardware memory controller striped) across multiple DIMMs.  When interleaved the platform may optionally provide details of which DIMMs are participating in the interleave.

Note that while LIBNVDIMM describes system-physical-address ranges that may alias with BLK access as ND_NAMESPACE_PMEM ranges and those without alias as ND_NAMESPACE_IO ranges, to the nd_pmem driver there is no distinction.  The different device-types are an implementation detail that userspace can exploit to implement policies like "only interface with address ranges from certain DIMMs".  It is worth noting that when aliasing is present and a DIMM lacks a label, then no block device can be created by default as userspace needs to do at least one allocation of DPA to the PMEM range. In contrast ND_NAMESPACE_IO ranges, once registered, can be immediately attached to nd_pmem.

2. BLK (nd_blk.ko): This driver performs I/O using a set of platform defined apertures.  A set of apertures will access just one DIMM. Multiple windows (apertures) allow multiple concurrent accesses, much like tagged-command-queuing, and would likely be used by different threads or different CPUs.

The NFIT specification defines a standard format for a BLK-aperture, but the spec also allows for vendor specific layouts, and non-NFIT BLK implementations may have other designs for BLK I/O.  For this reason "nd_blk" calls back into platform-specific code to perform the I/O. One such implementation is defined in the "Driver Writer's Guide" and "DSM Interface Example".

Why BLK?

While PMEM provides direct byte-addressable CPU-load/store access to NVDIMM storage, it does not provide the best system RAS (recovery, availability, and serviceability) model.  An access to a corrupted system-physical-address address causes a CPU exception while an access to a corrupted address through an BLK-aperture causes that block window to raise an error status in a register. The latter is more aligned withthe standard error model that host-bus-adapter attached disks present. Also, if an administrator ever wants to replace a memory it is easier to service a system at DIMM module boundaries.  Compare this to PMEM where data could be interleaved in an opaque hardware specific manner across several DIMMs.

PMEM vs BLK

BLK-apertures solve these RAS problems, but their presence is also the major contributing factor to the complexity of the ND subsystem. They complicate the implementation because PMEM and BLK alias in DPA space. Any given DIMM's DPA-range may contribute to one or more system-physical-address sets of interleaved DIMMs, *and* may also be accessed in its entirety through its BLK-aperture. Accessing a DPA through a system-physical-address while simultaneously accessing the same DPA through a BLK-aperture has undefined results. For this reason, DIMMs with this dual interface configuration include a DSM function to store/retrieve a LABEL.  The LABEL effectively partitions the DPA-space into exclusive system-physical-address and BLK-aperture accessible regions.  For simplicity a DIMM is allowed a PMEM "region" per each interleave set in which it is a member. The remaining DPA space can be carved into an arbitrary number of BLK devices with discontiguous extents.


BLK-REGIONs, PMEM-REGIONs, Atomic Sectors, and DAX

One of the few reasons to allow multiple BLK namespaces per REGION is so that each BLK-namespace can be configured with a BTT with unique atomic sector sizes.  While a PMEM device can host a BTT the LABEL specification does not provide for a sector size to be specified for a PMEM namespace. This is due to the expectation that the primary usage model for PMEM is via DAX, and the BTT is incompatible with DAX.  However, for the cases where an application or filesystem still needs atomic sector updateguarantees it can register a BTT on a PMEM device or partition. See LIBNVDIMM/NDCTL: Block Translation Table "btt"


LIBNVDIMM Kernel Device Model and LIBNDCTL Userspace API

What follows is a description of the LIBNVDIMM sysfs layout and a corresponding object hierarchy diagram as viewed through the LIBNDCTL API.  The example sysfs paths and diagrams are relative to the Example NVDIMM Platform which is also the LIBNVDIMM bus used in the LIBNDCTL unit test.

LIBNDCTL: Context Every API call in the LIBNDCTL library requires a context that holds the logging parameters and other library instance state.  The library is based on the libabc template:

https://git.kernel.org/cgit/linux/kernel/git/kay/libabc.git

LIBNDCTL: instantiate a new library context example

struct ndctl_ctx *ctx;

if (ndctl_new(&ctx) == 0)

return ctx;

else

return NULL;


LIBNVDIMM/LIBNDCTL: Bus

A bus has a 1:1 relationship with an NFIT.  The current expectation for ACPI based systems is that there is only ever one platform-global NFIT. That said, it is trivial to register multiple NFITs, the specification does not preclude it.  The infrastructure supports multiple buses and we we use this capability to test multiple NFIT configurations in the unit test.

LIBNVDIMM: control class device in /sys/class

This character device accepts DSM messages to be passed to DIMM identified by its NFIT handle.

/sys/class/nd/ndctl0

|-- dev

|-- device -> ../../../ndbus0

|-- subsystem -> ../../../../../../../class/nd

LIBNVDIMM: bus

struct nvdimm_bus *nvdimm_bus_register(struct device *parent,

      struct nvdimm_bus_descriptor *nfit_desc);


LIBNDCTL: bus enumeration example. Find the bus handle that describes the bus from Example NVDIMM Platform

static struct ndctl_bus *get_bus_by_provider(struct ndctl_ctx *ctx,

const char *provider)

{

struct ndctl_bus *bus;

ndctl_bus_foreach(ctx, bus)

if (strcmp(provider, ndctl_bus_get_provider(bus)) == 0)

return bus;

return NULL;

}

bus = get_bus_by_provider(ctx, "nfit_test.0");


LIBNVDIMM/LIBNDCTL: DIMM (NMEM)

The DIMM device provides a character device for sending commands to hardware, and it is a container for LABELs.  If the DIMM is defined by NFIT then an optional 'nfit' attribute sub-directory is available to add NFIT-specifics.

Note that the kernel device name for "DIMMs" is "nmemX".  The NFIT describes these devices via "Memory Device to System Physical Address Range Mapping Structure", and there is no requirement that they actually be physical DIMMs, so we use a more generic name.

LIBNVDIMM: DIMM (NMEM)

struct nvdimm *nvdimm_create(struct nvdimm_bus *nvdimm_bus, void *provider_data,

const struct attribute_group **groups, unsigned long flags,

unsigned long *dsm_mask);


LIBNDCTL: DIMM enumeration example

Note, in this example we are assuming NFIT-defined DIMMs which are identified by an "nfit_handle" a 32-bit value where:

  • Bit 3:0 DIMM number within the memory channel
  • Bit 7:4 memory channel number
  • Bit 11:8 memory controller ID
  • Bit 15:12 socket ID (within scope of a Node controller if node controller is present)
  • Bit 27:16 Node Controller ID
  • Bit 31:28 Reserved

static struct ndctl_dimm *get_dimm_by_handle(struct ndctl_bus *bus,

      unsigned int handle)

{

struct ndctl_dimm *dimm;

ndctl_dimm_foreach(bus, dimm)

if (ndctl_dimm_get_handle(dimm) == handle)

return dimm;

return NULL;

}

#define DIMM_HANDLE(n, s, i, c, d) \

(((n & 0xfff) << 16) | ((s & 0xf) << 12) | ((i & 0xf) << 8) \

| ((c & 0xf) << 4) | (d & 0xf))

dimm = get_dimm_by_handle(bus, DIMM_HANDLE(0, 0, 0, 0, 0));


LIBNVDIMM/LIBNDCTL: Region

A generic REGION device is registered for each PMEM range or BLK-apertureset. Per the example there are 6 regions: 2 PMEM and 4 BLK-aperture sets on the "nfit_test.0" bus.  The primary role of regions are to be a container of "mappings".  A mapping is a tuple of <DIMM, DPA-start-offset, length>.

LIBNVDIMM provides a built-in driver for these REGION devices.  This driver is responsible for reconciling the aliased DPA mappings across all regions, parsing the LABEL, if present, and then emitting NAMESPACE devices with the resolved/exclusive DPA-boundaries for the nd_pmem or nd_blk device driver to consume.

In addition to the generic attributes of "mapping"s, "interleave_ways" and "size" the REGION device also exports some convenience attributes."nstype" indicates the integer type of namespace-device this region emits, "devtype" duplicates the DEVTYPE variable stored by udev at the 'add' event, "modalias" duplicates the MODALIAS variable stored by udev at the 'add' event, and finally, the optional "spa_index" is provided in the case where the region is defined by a SPA.

LIBNVDIMM: region

struct nd_region *nvdimm_pmem_region_create(struct nvdimm_bus *nvdimm_bus,

struct nd_region_desc *ndr_desc);

struct nd_region *nvdimm_blk_region_create(struct nvdimm_bus *nvdimm_bus,

struct nd_region_desc *ndr_desc);


LIBNDCTL: region enumeration example

Sample region retrieval routines based on NFIT-unique data like "spa_index" (interleave set id) for PMEM and "nfit_handle" (dimm id) for BLK.


static struct ndctl_region *get_pmem_region_by_spa_index(struct ndctl_bus *bus,

unsigned int spa_index)

{

struct ndctl_region *region;

ndctl_region_foreach(bus, region) {

if (ndctl_region_get_type(region) != ND_DEVICE_REGION_PMEM)

continue;

if (ndctl_region_get_spa_index(region) == spa_index)

return region;

}

return NULL;

}

static struct ndctl_region *get_blk_region_by_dimm_handle(struct ndctl_bus *bus,

unsigned int handle)

{

struct ndctl_region *region;

ndctl_region_foreach(bus, region) {

struct ndctl_mapping *map;


if (ndctl_region_get_type(region) != ND_DEVICE_REGION_BLOCK)

continue;

ndctl_mapping_foreach(region, map) {

struct ndctl_dimm *dimm = ndctl_mapping_get_dimm(map);


if (ndctl_dimm_get_handle(dimm) == handle)

return region;

}

}

return NULL;

}


Why Not Encode the Region Type into the Region Name?

At first glance it seems since NFIT defines just PMEM and BLK interface types that we should simply name REGION devices with something derived from those type names.  However, the ND subsystem explicitly keeps the REGION name generic and expects userspace to always consider the region-attributes for four reasons:

1. There are already more than two REGION and "namespace" types.  For PMEM there are two subtypes.  As mentioned previously we have PMEM where the constituent DIMM devices are known and anonymous PMEM. For BLK regions the NFIT specification already anticipates vendor specific implementations.  The exact distinction of what a region contains is in the region-attributes not the region-name or the region-devtype.

2. A region with zero child-namespaces is a possible configuration.  For example, the NFIT allows for a DCR to be published without a corresponding BLK-aperture.  This equates to a DIMM that can only accept control/configuration messages, but no i/o through a descendant block device.  Again, this "type" is advertised in the attributes ('mappings'== 0) and the name does not tell you much.

3. What if a third major interface type arises in the future?  Outside of vendor specific implementations, it's not difficult to envision a third class of interface type beyond BLK and PMEM.  With a generic name for the REGION level of the device-hierarchy old userspace    implementations can still make sense of new kernel advertised region-types.  Userspace can always rely on the generic region attributes like "mappings", "size", etc and the expected child devices named "namespace".  This generic format of the device-model hierarchy allows the LIBNVDIMM and LIBNDCTL implementations to be more uniform and future-proof.

4. There are more robust mechanisms for determining the major type of a region than a device name.  See the next section, How Do I Determine the Major Type of a Region?


How Do I Determine the Major Type of a Region?

Outside of the blanket recommendation of "use libndctl", or simply looking at the kernel header (/usr/include/linux/ndctl.h) to decode the "nstype" integer attribute, here are some other options.

1. module alias lookup: The whole point of region/namespace device type differentiation is to     decide which block-device driver will attach to a given LIBNVDIMM namespace. One can simply use the modalias to lookup the resulting module.  It's important to note that this method is robust in the presence of a vendor-specific driver down the road.  If a vendor-specific    implementation wants to supplant the standard nd_blk driver it can with minimal impact to the rest of LIBNVDIMM.

In fact, a vendor may also want to have a vendor-specific region-driver (outside of nd_region).  For example, if a vendor defined its own LABEL format it would need its own region driver to parse that LABEL and emit the resulting namespaces.  The output from module resolution is more accurate than a region-name or region-devtype.

2. udev: The kernel "devtype" is registered in the udev database

    # udevadm info --path=/devices/platform/nfit_test.0/ndbus0/region0

    P: /devices/platform/nfit_test.0/ndbus0/region0

    E: DEVPATH=/devices/platform/nfit_test.0/ndbus0/region0

    E: DEVTYPE=nd_pmem

    E: MODALIAS=nd:t2

    E: SUBSYSTEM=nd


    # udevadm info --path=/devices/platform/nfit_test.0/ndbus0/region4

    P: /devices/platform/nfit_test.0/ndbus0/region4

    E: DEVPATH=/devices/platform/nfit_test.0/ndbus0/region4

    E: DEVTYPE=nd_blk

    E: MODALIAS=nd:t3

    E: SUBSYSTEM=nd


...and is available as a region attribute, but keep in mind that the "devtype" does not indicate sub-type variations and scripts should really be understanding the other attributes.

3. type specific attributes: As it currently stands a BLK-aperture region will never have a    "nfit/spa_index" attribute, but neither will a non-NFIT PMEM region.  A BLK region with a "mappings" value of 0 is, as mentioned above, a DIMM that does not allow I/O.  A PMEM region with a "mappings" value of zero is a simple system-physical-address range.


LIBNVDIMM/LIBNDCTL: Namespace

A REGION, after resolving DPA aliasing and LABEL specified boundaries, surfaces one or more "namespace" devices.  The arrival of a "namespace" device currently triggers either the nd_blk or nd_pmem driver to load and register a disk/block device.

LIBNVDIMM: namespace

Here is a sample layout from the three major types of NAMESPACE where namespace0.0 represents DIMM-info-backed PMEM (note that it has a 'uuid' attribute), namespace2.0 represents a BLK namespace (note it has a 'sector_size' attribute) that, and namespace6.0 represents an anonymous PMEM namespace (note that has no 'uuid' attribute due to not support a LABEL).

(See the Original Source)

LIBNDCTL: namespace enumeration example

Namespaces are indexed relative to their parent region, example below. These indexes are mostly static from boot to boot, but subsystem makesno guarantees in this regard.  For a static namespace identifier use its 'uuid' attribute.

static struct ndctl_namespace *get_namespace_by_id(struct ndctl_region *region,

                unsigned int id)

{

        struct ndctl_namespace *ndns;


        ndctl_namespace_foreach(region, ndns)

                if (ndctl_namespace_get_id(ndns) == id)

                        return ndns;

        return NULL;

}


LIBNDCTL: namespace creation example

Idle namespaces are automatically created by the kernel if a given region has enough available capacity to create a new namespace. Namespace instantiation involves finding an idle namespace and configuring it.  For the most part the setting of namespace attributes can occur in any order, the only constraint is that 'uuid' must be set before 'size'.  This enables the kernel to track DPA allocations internally with a static identifier.

static int configure_namespace(struct ndctl_region *region,

                struct ndctl_namespace *ndns,

                struct namespace_parameters *parameters)

{

        char devname[50];

        snprintf(devname, sizeof(devname), "namespace%d.%d",

                        ndctl_region_get_id(region), paramaters->id);

        ndctl_namespace_set_alt_name(ndns, devname);

        /* 'uuid' must be set prior to setting size! */

        ndctl_namespace_set_uuid(ndns, paramaters->uuid);

        ndctl_namespace_set_size(ndns, paramaters->size);

        /* unlike pmem namespaces, blk namespaces have a sector size */

        if (parameters->lbasize)

                ndctl_namespace_set_sector_size(ndns, parameters->lbasize);

        ndctl_namespace_enable(ndns);

}


Why the Term "namespace"?

1. Why not "volume" for instance?  "volume" ran the risk of confusing ND (libnvdimm subsystem) to a volume manager like device-mapper.

2. The term originated to describe the sub-devices that can be created within a NVME controller (see the nvme specification: http://www.nvmexpress.org/specifications/), and NFIT namespaces are meant to parallel the capabilities and configurability of NVME-namespaces.


LIBNVDIMM/LIBNDCTL: Block Translation Table "btt"

A BTT (design document: http://pmem.io/2014/09/23/btt.html) is a stacked block device driver that fronts either the whole block device or a partition of a block device emitted by either a PMEM or BLK NAMESPACE.

LIBNVDIMM: btt layout Every region will start out with at least one BTT device which is the seed device. To activate it set the "namespace", "uuid", and "sector_size" attributes and then bind the device to the nd_pmem or nd_blk driver depending on the region type.

LIBNDCTL: btt creation example

Similar to namespaces an idle BTT device is automatically created per region.  Each time this seed" btt device is configured and enabled a new seed is created.  Creating a BTT configuration involves two steps of finding and idle BTT and assigning it to consume a PMEM or BLK namespace.

static struct ndctl_btt *get_idle_btt(struct ndctl_region *region)

{

struct ndctl_btt *btt;


ndctl_btt_foreach(region, btt)

if (!ndctl_btt_is_enabled(btt)

&& !ndctl_btt_is_configured(btt))

return btt;


return NULL;

}

static int configure_btt(struct ndctl_region *region,

struct btt_parameters *parameters)

{

btt = get_idle_btt(region);


ndctl_btt_set_uuid(btt, parameters->uuid);

ndctl_btt_set_sector_size(btt, parameters->sector_size);

ndctl_btt_set_namespace(btt, parameters->ndns);

/* turn off raw mode device */

ndctl_namespace_disable(parameters->ndns);

/* turn on btt access */

ndctl_btt_enable(btt);

}


Once instantiated a new inactive btt seed device will appear underneath the region. Once a "namespace" is removed from a BTT that instance of the BTT device will be deleted or otherwise reset to default values.  This deletion is only at the device model level.  In order to destroy a BTT the "info block" needs to be destroyed.  Note, that to destroy a BTT the media needs to be written in raw mode.  By default, the kernel will autodetect the presence of a BTT and disable raw mode.  This autodetect behavior can be suppressed by enabling raw mode for the namespace via the ndctl_namespace_set_raw_mode() API.


'NVM' 카테고리의 다른 글

libvmmalloc  (0) 2016.08.24
libpmem library  (0) 2016.08.09
PCMSIM  (0) 2016.08.05
Persistent memory and page structures  (0) 2016.08.03
ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02

http://stackoverflow.com/questions/30236620/what-is-wmb-in-linux-driver

What is wmb() in linux driver


WMB macro: Write Memory Barrior

Let's assume a serial port, where you have to write bytes to a certain address. The serial chip will then send these bytes over the wires. It is then important that you do not mess up the writes - they have to stay in order or everything is garbled.

But the following is not enough:

   *serial = 'h';
   *serial = 'e';
   *serial = 'l';
   *serial = 'l';
   *serial = 'o';

Because the compiler, the processor, the memory subsystems and the buses in between are allowed to reorder your stores as optimization.

so you'll have to add code that will ensure the stores do not get tangled up. That's what, amongst others, the wmb() macro does: prevent reordering of the stores.

Note that just making the serial pointer volatile is not enough: while it ensures the compiler will not reorder, the other mechanisms mentioned can still wreak havoc. This is documented in another kernel doc piece.

'Linux Kernel' 카테고리의 다른 글

Linear VS Physical Address  (0) 2016.08.10
Block I/O Operation  (0) 2016.08.06
What is the return address of kmalloc() ? Physical or Virtual?  (0) 2016.07.29
Memory Mapping  (0) 2016.07.28
고정 크기 ramdisk 만들기 및 swap 영역 사용  (0) 2016.03.22

PCMSIM

NVM2016. 8. 5. 09:57

A simple PCM block device simulator for Linux

PCMSIM

Original Source: https://code.google.com/archive/p/pcmsim/


A block device driver for Linux that simulates the presence of a Phase Change Memory (PCM), a type of a non-volatile (persistent) byte-addressable memory (NVBM), in the system installed in one of the DIMM slots on the motherboard. The simulator is implemented as a kernel module for Linux that creates /dev/pcm0 when it is loaded - a ramdisk-backed block devices with latencies that of PCM.

We have designed pcmsim to have minimal overhead, so that the users can run benchmarks in real time. pcmsim accounts for the differences between read and write latencies, and the effects of CPU caches, but it is by no means a complete system simulation. The current implementation only partially accounts for prefetching in the CPU and for memory-level parallelism.

Please beware that a bunch of configuration is currently hard-coded in memory.c and module.c, separately for 32-bit and 64-bit systems (separated using the LP64 macro). Please refer to README.txt about what you need to change, so that you get meaningful results. I would also recommend that you run your benchmarks with disabled swap, so that your simulated PCM device would not be swapped out while you are using it.

Lastly, the pcmsim kernel module has been tested only on selected 2.6.x kernels; it still needs to be ported to 3.x kernels. If you fix any of these issues, please send me your patches, and I'll be happy to merge them!



'NVM' 카테고리의 다른 글

libpmem library  (0) 2016.08.09
libnvdimm  (0) 2016.08.05
Persistent memory and page structures  (0) 2016.08.03
ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02
Direct Access for files (DAX in EXT4)  (0) 2016.08.02

Persistent memory and page structures


Original Source: http://lwn.net/Articles/644079/

Abstraction

PM을 paging 통해 관리하는 것이 바람직 한가에 대한 논의이다. 메모리 관리자를 통하여 PM을 관리할 경우, 모든 물리 페이지는 page structure에 의해 명시되며, 해당 페이지 프레임은 버디를 통해 관리된다. 이 접근은 PM을 메모리 영역으로 사용하는데 매우 용이하나, 하나의 PFN 에 대해 하나의 page structure가 요구되며, TB단위의 큰 PM에 대해서 메모리 공간을 관리하는 구조체에 많은 메모리를 할당 해야 한다. 이때 PM영역의 page structure의 메모리 할당 위치: RAM or PRAM, 문제에 대해 고려해야 한다. 일반적으로 PM 의 경우, NVDIMM-N 과 같이 일반 DRAM이 사용되는 경우는 관계없지만 PCM 과 같은 소재가 사용되면 Wear Level 을 고려해야 하기 때문에 업데이트가 매우 잦은 page 구조체를 PM영역에 위치하는 것은 옳지 않다고 보는 견해가 많다. 따라서 그 거대한(?) 크기의 page 구조체를 DRAM에 위치하는 것은 상당부분 공간/비용에 대한 overhead가 많을 수 밖에 없다.  또한 버디는 메모리 존 개수 만큼 생성되므로, 커널을 수정하지 않고서는 에플리케이션은 해당 VA가 PM 영역의 PFN을 가리키는지 알수 없고, 또한 특정 PM영역을 엑세스 할 수 없다. 마지막으로 버디 역시 TB 단위의 메모리 관리에 대한 검증이 전혀 없으므로, 효율성 측면에서 의문점이 남는다. 

결론적으로 PM을 paging 하는 것은 그리 바람직하지 않다고 본다. 개인적으론 MMIO + DAX를를 베이스로 하는 PMEM.IO가 좋은 선택이 아닌가 한다. 특히 PM 영역을 특정 목적으로만 사용하고자 할경우, filesystem interface를 사용하는 대신 row device mapping을 하고, 디바이스 자체를 여러개의 풀로 관리할 수 있는 접근도 가능한 선택이 아닐까 한다. intel의 nvdimm 에 대한 접근 (libnvdimm, pmem.io,) 등은 모두 MMFIO를 기반으로 동작하는데, OS call-path overhead가 분명 어느정도 존재할 텐데, 왜 이러한 선택을 했는가는 여전히 의문을 갖고 있다. 


By Jonathan Corbet
May 13, 2015
As is suggested by its name, persistent memory (or non-volatile memory) is characterized by the persistence of the data stored in it. But that term could just as well be applied to the discussions surrounding it; persistent memory raises a number of interesting development problems that will take a while to work out yet. One of the key points of discussion at the moment is whether persistent memory should, like ordinary RAM, be represented by page structures and, if so, how those structures should be managed.

One page structure exists for each page of (non-persistent) physical memory in the system. It tracks how the page is used and, among other things, contains a reference count describing how many users the page has. A pointer to a page structure is an unambiguous way to refer to a specific physical page independent of any address space, so it is perhaps unsurprising that this structure is used with many APIs in the kernel. Should a range of memory exist that lacks corresponding page structures, that memory cannot be used with any API expecting a struct page pointer; among other things, that rules out DMA and direct I/O.

Persistent memory looks like ordinary memory to the CPU in a number of ways. In particular, it is directly addressable at the byte level. It differs, though, in its persistence, its performance characteristics (writes, in particular, can be slow), and its size — persistent memory arrays are expected to be measured in terabytes. At a 4KB page size, billions of page structures would be needed to represent this kind of memory array — too many to manage efficiently. As a result, currently, persistent memory is treated like a device, rather than like memory; among other things, that means that the kernel does not need to maintain page structures for persistent memory. Many things can be made to work without them, but this aspect of persistent memory does bring some limitations; one of those is that it is not currently possible to perform I/O directly between persistent memory and another device. That, in turn, thwarts use cases like using persistent memory as a cache between the system and a large, slow storage array.

Page-frame numbers

One approach to the problem, posted by Dan Williams, is to change the relevant APIs to do away with the need for page structures. This patch set creates a new type called __pfn_t:

    typedef struct {
	union {
	    unsigned long data;
	    struct page *page;
	};
    __pfn_t;

As is suggested by the use of a union type, this structure leads a sort of double life. It can contain a page pointer as usual, but it can also be used to hold an integer page frame number (PFN). The two cases are distinguished by setting one of the low bits in the data field; the alignment requirements for page structures guarantee that those bits will be clear for an actual struct page pointer.

A small set of helper functions has been provided to obtain the information from this structure. A call to __pfn_t_to_pfn() will obtain the associated PFN (regardless of which type of data the structure holds), while __pfn_t_to_page() will return a struct page pointer, but only if a page structure exists. These helpers support the main goal for the __pfn_t type: to allow the lower levels of the I/O stack to be converted to use PFNs as the primary way to describe memory while avoiding massive changes to the upper layers where page structures are used.

With that infrastructure in place, the block layer is changed to use __pfn_t instead of struct page; in particular, the bio_vec structure, which describes a segment of I/O, becomes:

    struct bio_vec {
        __pfn_t         bv_pfn;
        unsigned short  bv_len;
        unsigned short  bv_offset;
    };

The ripple effects from this change end up touching nearly 80 files in the filesystem and block subtrees. At a lower level, there are changes to the scatter/gather DMA API to allow buffers to be specified using PFNs rather than page structures; this change has architecture-specific components to enable the mapping of buffers by PFN.

Finally, there is the problem of enabling kmap_atomic() on PFN-specified pages. kmap_atomic() maps a page into the kernel's address space; it is only really needed on 32-bit systems where there is not room to map all of main memory into that space. On 64-bit systems it is essentially a no-op, turning a page structure into its associated kernel virtual address. That problem gets a little trickier when persistent memory is involved; the only code that really knows where that memory is mapped is the low-level device driver. Dan's patch set adds a function by which the driver can inform the rest of the kernel of the mapping between a range of PFNs and kernel space; kmap_atomic() is then adapted to use that information.

All together, this patch set is enough to enable direct block I/O to persistent memory. Linus's initial response was on the negative side, though; he said "I detest this approach." Instead, he argued in favor of a solution where special page structures are created for ranges of persistent memory when they are needed. As the discussion went on, though, he moderated his position, saying: "So while I (very obviously) have some doubts about this approach, it may be that the most convincing argument is just in the code." That code has since been reposted with some changes, but the discussion is not yet finished.

Back to page structures

Various alternatives have been suggested, but the most attention was probably drawn by Ingo Molnar's "Directly mapped pmem integrated into the page cache" proposal. The core of Ingo's idea is that all persistent memory would have page structures, but those structures would be stored in the persistent memory itself. The kernel would carve out a piece of each persistent memory array for these structures; that memory would be hidden from filesystem code.

Despite being stored in persistent memory, the page structures themselves would not be persistent — a point that a number of commenters seemed to miss. Instead, they would be initialized at boot time, using a lazy technique so that this work would not overly slow the boot process as a whole. All filesystem I/O would be direct I/O; in this picture, the kernel's page cache has little involvement. The potential benefits are huge: vast amounts of memory would be available for fast I/O without many of the memory-management issues that make life difficult for developers today.

It is an interesting vision, and it may yet bear fruit, but various developers were quick to point out that things are not quite as simple as Ingo would like them to be. Matthew Wilcox, who has done much of the work to make filesystems work properly with persistent memory, noted that there is an interesting disconnect between the lifecycle of a page-cache page and that of a block on disk. Filesystems have the ability to reassign blocks independently of any memory that might represent the content of those blocks at any given time. But in this directly mapped view of the world, filesystem blocks and pages of memory are the same thing; synchronizing changes to the two could be an interesting challenge.

Dave Chinner pointed out that the directly mapped approach makes any sort of data transformation by the filesystem (such as compression or encryption) impossible. In Dave's view, the filesystem needs to have a stronger role in how persistent memory is managed in general. The idea of just using existing filesystems (as Ingo had suggested) to get the best performance out of persistent memory is, in his view, not sustainable. Ingo, instead, seems to feel that management of persistent memory could be mostly hidden from filesystems, just like the management of ordinary memory is.

In any case, the proof of this idea would be in the code that implements it, and, currently, no such code exists. About the only thing that can be concluded from this discussion is that the kernel community still has not figured out the best ways of dealing with large persistent-memory arrays. Likely as not, it will take some years of experience with the actual hardware to figure that out. Approaches like Dan's might just be merged as a way to make things work for now. The best way to make use of such memory in the long term remains undetermined, though.




'NVM' 카테고리의 다른 글

libnvdimm  (0) 2016.08.05
PCMSIM  (0) 2016.08.05
ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02
Direct Access for files (DAX in EXT4)  (0) 2016.08.02
How to emulate Persistent Memory  (0) 2016.08.02

ND: NFIT-Defined / NVDIMM Subsystem


Since 2010 Intel has included non-volatile memory support on a few
storage-focused platforms with a feature named ADR (Asynchronous DRAM
Refresh).  These platforms were mostly targeted at custom applications
and never enjoyed standard discovery mechanisms for platform firmware
to advertise non-volatile memory capabilities. This now changes with
the publication of version 6 of the ACPI specification [1] and its
inclusion of a new table for describing platform memory capabilities.
The NVDIMM Firmware Interface Table (NFIT), along with new EFI and E820
memory types, enumerates persistent memory ranges, memory-mapped-I/O
apertures, physical memory devices (DIMMs), and their associated
properties.

The ND-subsystem wraps a Linux device driver model around the objects
and address boundaries defined in the specification and introduces 3 new
drivers.

  nd_pmem: NFIT enabled version of the existing 'pmem' driver [2]
  nd_blk: mmio aperture method for accessing persistent storage
  nd_btt: give persistent memory disk semantics (atomic sector update)

See the documentation in patch2 for more details, and there is
supplemental documentation on pmem.io [4].  Please review, and
patches welcome...

For kicking the tires, this release is accompanied by a userspace
management library 'ndctl' that includes unit tests (make check) for all
of the kernel ABIs.  The nfit_test.ko module can be used to explore a
sample NFIT topology.

[1]: http://www.uefi.org/sites/default/files/resources/ACPI_6....
[2]: https://git.kernel.org/cgit/linux/kernel/git/tip/tip.git/...
[3]: https://github.com/pmem/ndctl
[4]: http://pmem.io/documents/

--
Dan for the NFIT driver development team Andy Rudoff, Matthew Wilcox, Ross
Zwisler, and Vishal Verma


Original Source: https://lwn.net/Articles/640891/

'NVM' 카테고리의 다른 글

PCMSIM  (0) 2016.08.05
Persistent memory and page structures  (0) 2016.08.03
Direct Access for files (DAX in EXT4)  (0) 2016.08.02
How to emulate Persistent Memory  (0) 2016.08.02
Supporting filesystems in persistent memory  (0) 2016.08.02

Original Source http://www.eiric.or.kr/util/pdsFileDownload.php?db=TB_PostConference2&fileName=FN_1606287078993.pdf&seq=4957

최근 Linux 플랫폼에는 PM 저장 장치에 직접적으로 데이터를 입출력 시킬 수 있는 DAX (Direct Access eXciting) 기능이 추가되었다. 이 기술은 POSIX I/O 시스템 콜을 통해 구동되며, 기존 파일 시스템인 Ext4, XFS의 파일 시스템 계층 내부에서 바이트 단위로 넘어온 입출력 데이터를 DAX 인터페이스 (direct_access()) 이용하여 블록 단위로 변환하지 않고, PM 기반 저장 장치에 직접적으로 데이터를 접근시킴으로써 불 필요한 성능 부하의 문제를 해결하였다. 하지만, DAX는 페이지 캐시를 우회시키는 Direct I/O 흐름을 기반으로 설계되었기 때문에, 기존의 운영체제의 페이지 캐시를 이용한 성숙된 특성들을 사용할 수 없게 한다. 페이지 캐시의 사용으로 인한 이점은 read() 시스템 콜에 대한 빠른 응답 시간뿐만 아니라 파일 간의 복사, 압축, 암호화 등 여러 측면에서 존재하기 때문에, DAX를 사용하게 된다면, 이러한 이점들을 사용할 수 없게 된다. 

Direct Access for files

Original Source: https://www.kernel.org/doc/Documentation/filesystems/dax.txt

Motivation

The page cache is usually used to buffer reads and writes to files. It is also used to provide the pages which are mapped into userspace by a call to mmap. For block devices that are memory-like, the page cache pages would be unnecessary copies of the original storage. The DAX code removes the extra copy by performing reads and writes directly to the storage device. For file mappings, the storage device is mapped directly into userspace.

Usage

If you have a block device which supports DAX, you can make a filesystem on it as usual.  The DAX code currently only supports files with a block size equal to your kernel's PAGE_SIZE, so you may need to specify a block size when creating the filesystem.  When mounting it, use the "-o dax" option on the command line or add 'dax' to the options in /etc/fstab.

Implementation Tips for Block Driver Writers

To support DAX in your block driver, implement the 'direct_access' block device operation.  It is used to translate the sector number (expressed in units of 512-byte sectors) to a page frame number (pfn) that identifies the physical page for the memory.  It also returns a kernel virtual address that can be used to access the memory.

The direct_access method takes a 'size' parameter that indicates the number of bytes being requested. The function should return the number of bytes that can be contiguously accessed at that offset. It may also return a negative errno if an error occurs.

In order to support this method, the storage must be byte-accessible by the CPU at all times.  If your device uses paging techniques to expose a large amount of memory through a smaller window, then you cannot implement direct_access. Equally, if your device can occasionally stall the CPU for an extended period, you should also not attempt to implement direct_access.

These block devices may be used for inspiration:

- axonram: Axon DDR2 device driver

- brd: RAM backed block device driver

- dcssblk: s390 dcss block device driver

- pmem: NVDIMM persistent memory driver

Implementation Tips for Filesystem Writers

Filesystem support consists of

  • Adding support to mark inodes as being DAX by setting the S_DAX flag in i_flags
  • Implementing the direct_IO address space operation, and calling dax_do_io() instead of blockdev_direct_IO() if S_DAX is set
  • Implementing an mmap file operation for DAX files which sets the VM_MIXEDMAP and VM_HUGEPAGE flags on the VMA, and setting the vm_ops to include handlers for fault, pmd_fault and page_mkwrite (which should probably call dax_fault(), dax_pmd_fault() and dax_mkwrite(), passing the appropriate get_block() callback)
  • Calling dax_truncate_page() instead of block_truncate_page() for DAX files
  • Calling dax_zero_page_range() instead of zero_user() for DAX files
  • Ensuring that there is sufficient locking between reads, writes, truncates and page faults 

The get_block() callback passed to the DAX functions may return uninitialised extents.  If it does, it must ensure that simultaneous calls to get_block() (for example by a page-fault racing with a read() or a write()) work correctly.

These filesystems may be used for inspiration:

- ext2: see Documentation/filesystems/ext2.txt

- ext4: see Documentation/filesystems/ext4.txt

- xfs:  see Documentation/filesystems/xfs.txt

Handling Media Errors

The libnvdimm subsystem stores a record of known media error locations for each pmem block device (in gendisk->badblocks). If we fault at such location, or one with a latent error not yet discovered, the application can expect to receive a SIGBUS. Libnvdimm also allows clearing of these errors by simply writing the affected sectors (through the pmem driver, and if the underlying NVDIMM supports the clear_poison DSM defined by ACPI).

Since DAX IO normally doesn't go through the driver/bio path, applications or sysadmins have an option to restore the lost data from a prior backup/inbuilt redundancy in the following ways:

1. Delete the affected file, and restore from a backup (sysadmin route): This will free the file system blocks that were being used by the file, and the next time they're allocated, they will be zeroed first, which happens through the driver, and will clear bad sectors.

2. Truncate or hole-punch the part of the file that has a bad-block (at least an entire aligned sector has to be hole-punched, but not necessarily an entire filesystem block).

These are the two basic paths that allow DAX filesystems to continue operating in the presence of media errors. More robust error recovery mechanisms can be built on top of this in the future, for example, involving redundancy/mirroring provided at the block layer through DM, or  additionally, at the filesystem level. These would have to rely on the above two tenets, that error clearing can happen either by sending an IO through the driver, or zeroing (also through the driver).

Shortcomings

Even if the kernel or its modules are stored on a filesystem that supports DAX on a block device that supports DAX, they will still be copied into RAM.

The DAX code does not work correctly on architectures which have virtually mapped caches such as ARM, MIPS and SPARC.

Calling get_user_pages() on a range of user memory that has been mmaped from a DAX file will fail as there are no 'struct page' to describe those pages. This problem is being worked on. That means that O_DIRECT reads/writes to those memory ranges from a non-DAX file will fail (note that O_DIRECT reads/writes _of a DAX file_ do work, it is the memory that is being accessed that is key here). Other things that will not work include RDMA, sendfile() and splice().



'NVM' 카테고리의 다른 글

Persistent memory and page structures  (0) 2016.08.03
ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02
How to emulate Persistent Memory  (0) 2016.08.02
Supporting filesystems in persistent memory  (0) 2016.08.02
Persistent memory support progress  (0) 2016.08.02

How to emulate Persistent Memory


    Data allocated with NVML is put to the virtual memory address space, and concrete ranges are relying on result of mmap(2) operation performed on the user defined files. Such files can exist on any storage media, however data consistency assurance embedded within NVML requires frequent synchronisation of data that is being modified. Depending on platform capabilities, and underlying device where the files are, a different set of commands is used to facilitate synchronisation. It might be msync(2) for the regular hard drives, or combination of cache flushing instructions followed by memory fence instruction for the real persistent memory.

    Although application adaptation to NVML usage, and ability to operate on persistent memory might be done by relying on regular hard drive, it is not recommended due to the performance hit coming from msync(2) operation. That is the reason to work either with the real equipment or emulated environment. Since persistent memory is not yet commonly available we do recommend setting up emulation system, that will speed up development, and testing of the application you are converting. In the following steps we shall cover how to setup such system.

    Hardware and system requirements

    Emulation environment is available at the current stage only for Linux systems, and should work on any hardware or virtualized environment. Emulation of persistent memory is based on DRAM memory, that is seen by OS as Persistent Memory region. Due to being a DRAM based emulation it is very fast, but will likely loose all the data upon powering down the machine. It should as well work with any distribution able to handle official kernel.

    Linux Kernel

    Download kernel sources from official kernel pages. Support for persistent memory devices and emulation is present in Kernel since 4.0 version, however it is recommended to use Kernel newer then 4.2 due to easier configuration of it. Following instruction relies on 4.2 or newer. Using Kernel older then 4.2 will require a bit more work to setup, and will not be described here. Please note, that features and bug fixes around DAX support are being implemented as we speak, therefore it is recommended to use the newest stable Kernel if possible. To configure proper driver installation run nconfig and enable driver.

    $ make nconfig
    	Device Drivers ---> 
    		{*} NVDIMM (Non-Volatile Memory Device) Support --->
    			<M>   PMEM: Persistent memory block device support
    			<M>   BLK: Block data window (aperture) device support
    			[*]   BTT: Block Translation Table (atomic sector updates)
    			[*]   PFN: Map persistent (device) memory 

    Additionally you need to enable treatment of memory marked using the non-standard e820 type of 12 as used by the Intel Sandy Bridge-EP reference BIOS as protected memory. The kernel will offer these regions to the ‘pmem’ driver so they can be used for persistent storage.

    $ make nconfig
    	Processor type and features --->
    		[*] Support non-standard NVDIMMs and ADR protected memory
    		[*] Device memory (pmem, etc...) hotplug support
    	File systems --->
    		[*] Direct Access (DAX) support 

    You are ready to build your Kernel

    $ make -jX
    	where X is the number of cores on the machine

    Install the kernel

    # sudo make modules_install install

    Reserve memory region so it appears to be a persistent memory by modifying Kernel command line parameters. Region of memory to be used, from ss to ss+nn. [KMG] refers to kilo, mega, giga.

    memmap=nn[KMG]!ss[KMG]

    E.g. memmap=4G!12G reserves 4GB of memory between 12th and 16th GB. Configuration is done within GRUB, and varies between Linux distributions. Here are two examples of GRUB configuration.

    Ubuntu Server 15.04

    1
    2
    3
    # sudo vi /etc/default/grub
    GRUB_CMDLINE_LINUX="memmap=nn[KMG]!ss[KMG]"
    # sudo update-grub2
    

    CentOS 7.0

    1
    2
    3
    4
    5
    6
    # sudo vi /etc/default/grub
    GRUB_CMDLINE_LINUX="memmap=nn[KMG]!ss[KMG]"
    On BIOS-based machines:
    # sudo grub2-mkconfig -o /boot/grub2/grub.cfg
    On UEFI-based machines:
    # sudo grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
    

    After machine reboot you should be able to see the emulated device as /dev/pmem0. Please be aware of the memory ranges available to your OS, and try not to overlap with those. Trying to get reserved memory regions for persistent memory emulation will result in splitted memory ranges defining persistent (type 12) regions. General recommendation would be to either use memory from 4GB+ range (memmap=nnG!4G) or checking upfront e820 memory map and fitting within. If you don’t see the device, verify the memmap setting correctness, followed by dmesg(1) analysis. You should be able to see reserved ranges as shown on the dmesg output snapshot: dmesg

    You can see that there can be multiple non-overlapping regions reserved as a persistent memory. Putting multiple memmap="...!..." entries will result in multiple devices exposed by the kernel, and visible as /dev/pmem0/dev/pmem1/dev/pmem2, …

    DAX - Direct Access

    The DAX (direct access) extensions to the filesystem creates PM-aware environment. Having filesystem brings easy and reliable rights management, while with DAX add-on, any file that is memory maped with mmap(2) is directly mapped from physical addres range into process virtual memory addresses. For those files there is no paging, and load/store operations provide direct access to persistent memory.

    Install filesystem with DAX (available today for ext4 and xfs):

    1
    2
    3
    # sudo mkdir /mnt/mem
    # sudo mkfs.ext4 /dev/pmem0    OR    #sudo mkfs.xfs /dev/pmem0
    # sudo mount -o dax /dev/pmem0 /mnt/mem
    

    Now files can be created on the freshly mounted partition, and given as an input to NVML pools.

    It is additionally worth mentioning you can emulate persistent memory with ramdisk (i.e./dev/shm), or force pmem-like behavior by setting environment variablePMEM_IS_PMEM_FORCE=1, that would eliminate performance hit caused by msync(2).


    Original Source: http://pmem.io/2016/02/22/pm-emulation.html




    'NVM' 카테고리의 다른 글

    Persistent memory and page structures  (0) 2016.08.03
    ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02
    Direct Access for files (DAX in EXT4)  (0) 2016.08.02
    Supporting filesystems in persistent memory  (0) 2016.08.02
    Persistent memory support progress  (0) 2016.08.02

    Supporting filesystems in persistent memory

    Original Source: https://lwn.net/Articles/610174/


    Abstract

    본글은 NVMFS 설계에 필요한 요소에 대해 논의한다. Matthew Wilcox의 EXT2-XIP 로부터 EXT4-DAX까지의 간단한 설계 변경 과정을 설명하고, EXT4-DAX의 page cache bypass 방법에 대해 간단히 소개한다. Andrew Morton 의 경우, 기존의 tmpfs 와 같은 in-memory filesystem을 활용하지 않는가에 대해 의견을 제시했고, Dave Chinner는 DRAM based in-memory filesystem의 NVM 적용의 문제점을 robust 측면에서 제시한다. 
    By Jonathan Corbet
    September 2, 2014



    For a few years now, we have been told that upcoming non-volatile memory (NVM) devices are going to change how we use our systems. These devices provide large amounts (possibly terabytes) of memory that is persistent and that can be accessed at RAM speeds. Just what we will do with so much persistent memory is not entirely clear, but it is starting to come into focus. It seems that we'll run ordinary filesystems on it — but those filesystems will have to be tweaked to allow users to get full performance from NVM.


    It is easy enough to wrap a block device driver around an NVM device and make it look like any other storage device. Doing so, though, forces all data on that device to be copied to and from the kernel's page cache. Given that the data could be accessed directly, this copying is inefficient at best. Performance-conscious users would rather avoid use of the page cache whenever possible so that they can get full-speed performance out of NVM devices.

    The kernel has actually had some support for direct access to non-volatile memory since 2005, when execute-in-place (XIP) support was added to the ext2 filesystem. This code allows files from a directly-addressable device to be mapped into user space, allowing file data to be accessed without going through the page cache. The XIP code has apparently seen little use, though, and has not been improved in some years; it does not work with current filesystems.

    Last year, Matthew Wilcox began work on improving the XIP code and integrating it into the ext4 filesystem. Along the way, he found that it was not well suited to the needs of contemporary filesystems; there are a number of unpleasant race conditions in the code as well. So over time, his work shifted from enhancing XIP to replacing it. That work, currently a 21-part patch set, is getting closer to being ready for merging into the mainline, so it is beginning to get a bit more attention.

    Those patches replace the XIP code with a new subsystem called DAX (for "direct access," apparently). At the block device level, it replaces the existing direct_access() function in struct block_device_operations with one that looks like this:

    long (*direct_access)(struct block_device *dev, sector_t sector, void **addr, unsigned long *pfn, long size);

    This function accepts a sector number and a size value saying how many bytes the caller wishes to access. If the given space is directly addressable, the base (kernel) address should be returned in addr and the appropriate page frame number goes into pfn. The page frame number is meant to be used in page tables when arranging direct user-space access to the memory.

    블록장치는 섹터를 기반으로 하고 있지만, 직접 매핑이 가능한 디바이스에 대해 해당 섹터들은 주소로 변환되고, 해당 물리 페이지 넘버를 pfn으로 저장한다

    The use of page frame numbers and addresses may seem a bit strange; most of the kernel deals with memory at this level via struct page. That cannot be done here, though, for one simple reason: non-volatile memory is not ordinary RAM and has no page structures associated with it. Those missing page structures have a number of consequences; perhaps most significant is the fact that NVM cannot be passed to other devices for DMA operations. That rules out, for example, zero-copy network I/O to or from a file stored on an NVM device. Boaz Harrosh is working on a patch set allowing page structures to be used with NVM, but that work is in a relatively early state.

    Moving up the stack, quite a bit of effort has gone into pushing NVM support into the virtual filesystem layer so that it can be used by all filesystems. Various generic helpers have been set up for common operations (reading, writing, truncating, memory-mapping, etc.). For the most part, the filesystem need only mark DAX-capable inodes with the new S_DAX flag and call the helper functions in the right places; see the documentation in the patch set for (a little) more information. The patch set finishes by adding the requisite support to ext4.

    Andrew Morton expressed some skepticism about this work, though. At the top of his list of questions was: why not use a "suitably modified" version of an in-memory filesystem (ramfs or tmpfs, for example) instead? It seems like a reasonable question; those filesystems are already designed for directly-addressable memory and have the necessary optimizations. But RAM-based filesystems are designed for RAM; it turns out that they are not all that well suited to the NVM case.

    For the details of why that is, this message from Dave Chinner is well worth reading in its entirety. To a great extent, it comes down to this: the RAM-based filesystems have not been designed to deal with persistence. They start fresh at each boot and need never cope with something left over from a previous run of the system. Data stored in NVM, instead, is expected to persist over reboots, be robust in the face of crashes, not go away when the kernel is upgraded, etc. That adds a whole set of requirements that RAM-based filesystems do not have to satisfy.

    So, for example, NVM filesystems need all the tools that traditional filesystems have to recognize filesystems on disk, check them, deal with corruption, etc. They need all of the techniques used by filesystems to ensure that the filesystem image in persistent storage is in a consistent state at all times; metadata operations must be carefully ordered and protected with barriers, for example. Since compatibility with different kernels is important, no in-kernel data structures can be directly stored in the filesystem; they must be translated to and from an on-disk format. Ordinary filesystems do these things; RAM-based filesystems do not.

    Then, as Dave explained, there is the little issue of scalability:

    Further, it's going to need to scale to very large amounts of storage. We're talking about machines with *tens of TB* of NVDIMM capacity in the immediate future and so free space management and concurrency of allocation and freeing of used space is going to be fundamental to the performance of the persistent NVRAM filesystem. So, you end up with block/allocation groups to subdivide the space. Looking a lot like ext4 or XFS at this point.

    And now you have to scale to indexing tens of millions of everything. At least tens of millions - hundreds of millions to billions is more likely, because storing tens of terabytes of small files is going to require indexing billions of files. And because there is no performance penalty for doing this, people will use the filesystem as a great big database. So now you have to have a scalable posix compatible directory structures, scalable freespace indexation, dynamic, scalable inode allocation, freeing, etc. Oh, and it also needs to be highly concurrent to handle machines with hundreds of CPU cores.

    Dave concluded by pointing out that the kernel already has a couple of "persistent storage implementations" that can handle those needs: the XFS and ext4 filesystems (though he couldn't resist poking at the scalability of ext4). Both of them will work now on a block device based on persistent memory. The biggest thing that is missing is a way to allow users to directly address all of that data without copying it through the page cache; that is what the DAX code is meant to provide.

    There are groups working on filesystems designed for NVM from the beginning. But most of that work is in an early stage; none has been posted to the kernel mailing lists, much less proposed for merging. So users wanting to get full performance out of NVM will find little help in that direction for some years yet. It is thus not unreasonable to conclude that there will be some real demand for the ability to use today's filesystems with NVM systems.

    The path toward that capability would appear to be DAX. All that is needed is to get the patch set reviewed to the point that the relevant subsystem maintainers are comfortable merging it. That review has been somewhat slow in coming; the patch set is complex and touches a number of different subsystems. Still, the code has changed considerably in response to the reviews that have come in and appears to be getting close to a final state. Perhaps this functionality will find its way into the mainline in a near-future development cycle.



    'NVM' 카테고리의 다른 글

    Persistent memory and page structures  (0) 2016.08.03
    ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02
    Direct Access for files (DAX in EXT4)  (0) 2016.08.02
    How to emulate Persistent Memory  (0) 2016.08.02
    Persistent memory support progress  (0) 2016.08.02

    Persistent memory support progress 

    https://lwn.net/Articles/640113/

    By Jonathan Corbet

    April 15, 2015 Persistent memory (or non-volatile memory) has a number of nice features: it doesn't lose its contents when power is cycled, it is fast, and it is expected to be available in large quantities. Enabling proper support for this memory in the kernel has been a topic of discussion and development for some years; it was, predictably, an important topic at this year's Linux Storage, Filesystem, and Memory Management Summit. The 4.1 kernel will contain a new driver intended to improve support for persistent memory, but there is still a fair amount of work to be done.

    2015년 4월 15일Persistent memory (혹은 non-volatile memory)은 멋진 기능들을 가지고 있습니다. 전원이 꺼졌다가 켜져도 데이터가 그대로 남아있고, 빠르고, 대용량이기 때문입니다. 커널이 이 메모리를 제대로 지원하도록 구현하기 위해 몇년동안 토론을 해왔고 올해 LSF/MM에서 중요한 이슈중 하나였습니다. 4.1 커널이 persistent memory를 지원하기 위한 새로운 드라이버를 지원한 것입니다만 아직도 해야할 일이 많습니다.

    At a first glance, persistent memory looks like normal RAM to the processor, so it might be tempting to simply use it that way. There are, though, some good reasons for not doing that. The performance characteristics of persistent memory are still not quite the same as RAM; in particular, write operations can be slower. Persistent memory may not wear out as quickly as older flash arrays did, but it is still best to avoid rewriting it many times per second, as could happen if it were used as regular memory. And the persistence of persistent memory is a valuable feature to take advantage of in its own right — but, to do so, the relevant software must know which memory ranges in the system are persistent. So persistent memory needs to be treated a bit differently.

    간단하게는 persistent memory는 프로세서에게 보통 RAM처럼 보여야 합니다. 그래서 RAM에 접근하듯이 쓸 수 있어야 합니다. 하지만 몇가지 이유 때문에 그렇게 하지 않습니다. 성능이 아직 RAM하고 똑같지 않습니다. 특히 쓰기 동작이 느립니다. PM(persistent memory) 예전 플래시 메모리처럼 수명이 짧지는 않지만 어쨌든 일반 메모리처럼 초당 몇번씩 쓰기 동작을 하지 않아야 합니다. PM의 좋은 특성들을 제대로 활용하기 위해서 소프트웨어가 시스템의 어떤 메모리 영역이 persistent한지를 먼저 알야합니다. 그래서 결국 PM은 약간 다르게 접근되게 됩니다.

    The usual approach, at least for a first step, is to separate persistent memory from normal RAM and treat it as if it were a block device. Various drivers implementing this type of access have been circulating for a while now. It appears that this driver from Ross Zwisler will be merged for the 4.1 release. It makes useful reading as it is something close to the simplest possible example of a working block device driver. It takes a region of memory, registers a block device to represent that memory, and implements block read and write operations with memcpy() calls.

    일반적인 방법(초기 단계에서)은 보통 RAM 영역과 pm을 분리해서 블럭 장치처럼사용하는 것입니다. 많은 드라이버들이 이런 접근 방식으로 구현되서 배포되고 있습니다. Ross Swisler에서 이런 방식의 드라이버를 구현해서 4.1에 넣었습니다. 이것은 간단한 블럭 장치 드라이버처럼 동작합니다 메모리 영역을 만들고 해당 영영을 표현하는 블럭 장치를 만들어서 memcpy를 이용한 블럭 단위 읽기 쓰기를 구현합니다.

    In his pull request to merge this driver, Ingo Molnar noted that a number of features that one might expect, including mmap() and execute-in-place, are not supported yet, and that persistent-memory contents would be copied in the page cache. What Ingo had missed is that the DAX patch set providing direct filesystem access to persistent memory was merged for the 4.0 release. If a DAX-supporting filesystem (ext4 now, XFS soon) is built in a persistent memory region, file I/O will avoid the page cache and operations like mmap() will be properly supported.

    Ingo Molnar는 mmap이나 xip등 사람들이 기대할고 있는 많은 기능들이 아직 지원되지 않았다고 말했습니다. 그리고 pm의 내용이 페이지 캐시에도 복사되고 있다는 것도 거론했습니다. Ingo가 놓치고 있는건 DAX 패치를 이용해서 파일시스템이 pm에 직접 접근할 수 있게된게 4.0 부터라는 것입니다. DAX를 지원하는 파일시스템이 pm 영역에 적용된다면 파일 IO가 페이지 캐시를 사용하지 않을 것이고 mmap 연산이 제대로 지원될 것입니다.

    That said, there are a few things that still will not work quite as expected. One of those is mlock(), which, as Yigal Korman pointed out, may seem a bit strange: data stored in persistent memory is almost by definition locked in memory. As noted by Kirill Shutemov, though, supporting mlock() is not a simple no-op; the required behavior depends on how the memory mapping was set up in the first place. Private mappings still need copy-on-write semantics, for example. A perhaps weirder case is direct I/O: if a region of persistent memory is mapped into a process's address space, the process cannot perform direct I/O between that region and an ordinary file. There may also be problems with direct memory access (DMA) I/O operations, some network transfers, and the vmsplice() system call, among others.

    기대하는 것만큼 제대로 동작하지 않는게 더 있습니다. mlock이 그중 하나입니다. Yigal Korman이 지적한 것처럼 약간 이상하게도 pm에 저장된 데이터는 메모리에서 lock 된것과 거의 같습니다. 그렇지만 Kirill Shutemoy에 따르면 mlock을 그냥 no-op으로 만들 수도 없습니다. 최초에 메모리 맵핑이 어떻게 되었는지에 따라 동작이 달라지기 때문입니다. private 맵핑은 copy-on-write를 필요로 합니다. 더 이상한건 direct io입니다. pm 영역이 프로세스의 주소 공간에 맵핑되어 있을 때 프로세스는 일반 파일과 영역간의 direct io를 실행할 수 없습니다. DMA io 동작에도 문제가 있습니다. 네트워크 전송이나 vmsplice 시스템 콜도 그렇습니다.



    Whither struct page?

    In almost all cases, the restrictions with persistent memory come down to the lack of page structures for that memory. A page structure represents a page of physical memory in the system memory map; it contains just about everything the kernel knows about that page and how it is being used. See this article for the gory details of what can be found there. These structures are used with many internal kernel APIs that deal with memory. Persistent memory, lacking corresponding page structures, cannot be used with those APIs; as a result, various things don't work with persistent memory.

    pm을 사용하지 못하는 대부분의 이유가 충분한 페이지 구조체를 만들 수 없기 때문입니다. 페이지 구조체는 시스템 메모리 맵에 있는 물리 페이지를 나타냅니다. 커널이 페이지에 대해 알아야할 모든 것을 가지고 있고 어떻게 사용되고 있는지를 가지고 있습니다.... 생략...... 이 구조체는 메모리를 다루는 많은 커널 내부 API에서 사용됩니다. pm을 관리하는 페이지 구조체가 없으므로 이런 API에서 사용될 수 없고 결국 많은 것들이 pm을 처리할 수 없게 됩니다.

    Kernel developers have hesitated to add persistent memory to the system memory map because persistent-memory arrays are expected to be large — in the terabyte range. With the usual 4KB page size, 1TB of persistent memory would need 256 million page structures which would occupy several gigabytes of RAM. And they do need to be stored in RAM, rather than in the persistent memory itself; page structures can change frequently, so storing them in memory that is subject to wear is not advisable. Rather than dedicate a large chunk of RAM to the tracking of persistent memory, the development community has, so far, chosen to treat that memory as a separate type of device.

    커널 개발자들이 pm을 시스템 메모리 맵에 추가하길 망설이는 이유가 pm이 몇 TB까지 커질 수 있기 때문입니다. 보통의 4KB 페이지 크기를 생각하면 1TB의 pm은 256백만개의 페이지 구조체를 필요로 하고 몇 GB의 메모리를 차지하게 됩니다. 그것들을 RAM에 저장할 필요는 없고 pm에 저장할 수 있습니다. 하지만 페이지 구조체는 자주 변경되므로 수명이 짧은 pm에 저장하는 것은 좋지 않습니다. 많은 RAM을 pm 관리에 사용하는 대신에 커뮤니티에서는 아직까진 다른 타입의 장치로 취급하기로 선택했습니다.

    At some point, though, a way to lift the limitations around persistent memory will need to be found. There appear to be two points of view on how that might be done. One says that page structures should never be used with persistent memory. The logical consequence of this view is that the kernel interfaces that currently use page structures need to be changed to use something else — page-frame numbers, for example — that works with both RAM and persistent memory. Dan Williams posted a patch removing struct page usage from the block layer in March. It is not for the faint of heart: just over 100 files are touched to make this change. That led to complaints from some developers that getting rid of struct page usage in APIs would involve a lot of high-risk code churn and remove a useful abstraction while not necessarily providing a lot of benefit.

    어떻게든 pm의 한계를 극복할 방법이 필요합니다. 두가지의 방향으로 진행되는것 같습니다. 한쪽에서는 페이지 구조체가 pm에는 절대 사용될 수 없다고 말합니다. 이런 관점에서 보면 현재 페이지 구조체를 사용하는 커널 인터페이스들은 페이지 프레임 번호와 같이 뭔가 다른 것을 사용하도록 바껴야만 RAM과 pm에서 사용될 수 있게됩니다. 3월에 Dan Williams가 블럭 레이어에서 struct page를 제거하는 패치를 포스팅했습니다. 100개 이상의 파일이 수정됐습니다. 일부 개발자들은 API에서 struct page를 없애는 것이 장점도 있지만 그에 비해 코드가 위험해지고 추상화가 깨지는 단점이 있다는 불평을 했습니다.

    The alternative would be to bite the bullet and add struct page entries for persistent memory regions. Boaz Harrosh posted a patch to that end in August 2014; it works by treating persistent memory as a range of hot-pluggable memory and allocating the memory-map entries at initialization time. The patch is relatively simple, but it does nothing to address the memory-consumption issue.

    대안은 pm 영역을 위한 항목을 struct page에 추가하는 것입니다. Boaz Harrosh가 2014년 8월말 올린 패치가 있습니다. pm을 hot-pluggable 메모리의 영역으로 잡고 초기화때 메모리 맵으로 할당합니다. 패치는 비교적 간단하지만 메모리가 과소비되는 이슈를 해결하지는 못합니다.

    In the long run, the solution may take the form of something like a page structure that represents a larger chunk of memory. One obvious possibility is to make a version of struct page that refers to a huge page; that has the advantage of using a size that is understood by the processor's memory-management unit and would integrate well with the transparent huge page mechanism. An alternative would be a variable-size extent structure as is used by more recent filesystems. Either way, the changes required would be huge, so this is not something that is going to happen in the near future.

    MMU를 활용해서 huge page를 지원하게해서 큰 메모리 영역을 표시하는 방법도 있다. 최근 파일 시스템들에서 사용하는 variable-size extent 구조체를 사용하는 방법도 있다. 둘다 너무 큰 변화를 요구하기 때문에 가까운 시일내에 적용할 수는 없습니다.

    What will happen is that persistent memory devices will work on Linux as a storage medium for the major filesystems, providing good performance. There will be some rough edges with specific features that do not work, but most users are unlikely to run into them. With 4.1, the kernel will have a level of support for persistent-memory devices to allow that hardware to be put to good use, and to allow users to start figuring out what they actually want to do with that much fast, persistent storage.

    PM 디바이스들이 리눅스에서 주요한 파일시스템들에 대해 스토리지로서 사용이 될때 무슨일이 일어날 것인가, 좋은 성능을 보여줄 것입니다. 특정 요소는 동작하지 않는 다소 거친 면이 존재하지만, 대부분의 유저는 그것들을 동작시키지 않을 것입니다. 4.1에서 커널은 PM 디바이스가 하드웨어를 사용할 수 있는 레벨의 지원을 할 것이고, 이것은 사용자들이 더빠르고, 영속적인 스토리를 통해 그들이 무엇을 하고자 하는지를 허용할 것이다. 



    'NVM' 카테고리의 다른 글

    Persistent memory and page structures  (0) 2016.08.03
    ND: NFIT-Defined / NVDIMM Subsystem  (0) 2016.08.02
    Direct Access for files (DAX in EXT4)  (0) 2016.08.02
    How to emulate Persistent Memory  (0) 2016.08.02
    Supporting filesystems in persistent memory  (0) 2016.08.02

    What is the return address of kmalloc() ? Physical or Virtual?


    It is virtual.   But let us go below the C language function - assembly programming instead, to understand everything better. 

    Generally, at the assembly - every time you save or load from memory - either direct or through registers, it is also virtual.   This is because every addresses that comes out of the CPU address bus, is in virtual address:

    or this (From wikipedia):

    The virtual address will then be translated into physical address, which the CPU cannot see or even know its value.

    vmalloc() is physically non-contiguous and kmalloc() is physically contiguous - so how does the CPU know it is contiguous or not?   This is because of linear mapping, or also called one-to-one mapping (or direct mapping):

    Sometimes DMA devices require the physical addresses - and so through these "mapping", since we know the virtual address, the physical can thus be derived, but remember that the CPU cannot use it, ie,

    load (eax), ebx:   where eax contained the physical address, is wrong.

    Source from: https://www.quora.com/What-is-the-return-address-of-kmalloc-Physical-or-Virtual


    'Linux Kernel' 카테고리의 다른 글

    Block I/O Operation  (0) 2016.08.06
    What is wmb() in linux driver  (0) 2016.08.05
    Memory Mapping  (0) 2016.07.28
    고정 크기 ramdisk 만들기 및 swap 영역 사용  (0) 2016.03.22
    sysinfo 관련  (0) 2016.02.26

    Memory Mapping

    Linux Kernel2016. 7. 28. 09:04

    Memory Mapping



    (원소스: 리눅스 디바이스드라이버, 유영창 저)


    • MMU에는 MMU 테이블을 유지하기위한 별도의 관리 메모리가 없다. MMU는 보통 프로세서에 내장되고, 시스템 메모리를 같이 사용한다. 그래서 프로세서가 처음 부팅되면 리눅스는 시스템 메모리의 일부를 MMU 테이블에 할당하고, 관리할 정보를 MMU 테이블에 기록한다. 이 과정에서는 MMU가 동작하지 않는다. 리눅스 커널은 MMU 테이블에 관련된 정보를 메모리에 모두 기록한 후에 MMU 테이블에 해당하는 메모리 위치를 MMU에 알려주고 MMU를 동작시킨다.
    • 메모리맵I/O 방식: 

        char *videoptr;

        videoptr = (char *) 0x000B0000;

        *videoptr = 'A';

    • 메모리 접근 명령으로 처리하는 것을 Memory Mapped I/O라 한다. 하지만, MMU가 활성화되어 동작하는 커널 모드에서는 위의 예처럼 비디오의 물리주소 0x000B0000에 접근하면 페이지폴트가 발생하여 처리되지 않는다. 따라서, 디바이스드라이버는 하드웨어를 제어하기위해 물리주소가상주소로 매핑 (MMU 가 사용되지 않는 메모리 영역의 operation에 대해서는 PA 가 사용될 수 있음, i.e. DMA)해야 한다. 메모리맵 I/O 방식의 물리적 주소공간을 커널에서 사용 가능한 주소로 매핑하거나 매핑된 영역을 해제할 때는 ioremap(), ioremap_nocache(), iounmap() 함수를 사용해야 한다.
    • kmalloc: VA physically contiguous
    • vmalloc: VA physically in-contiguous
    • kzalloc: VA physically contiguous, kalloc initialized zero - use it to expose mem to user space
    • 응용 프로그램에서는 디바이스 파일로 하드웨어를 제어하기 위해 보통 read(), write(), ioctl() 함수를 사용한다. 이 함수들은 응용 프로그램에 하드웨어의 내부구조를 숨겨주는 효과가 있다. 그러나 이런 함수들은 프로세스 메모리공간과 커널메모리 공간사이의 메모리 전달 과정이 수반되기 때문에 매우 비효율적이다. 특히나 많은 용량의 데이터가 빠르게 전달되어야 하는 사운드나 비디오 장치에 메모리 복사가 수반되는 read(), write(), ioctl() 함수를 사용한다면 시스템 성능이 저하된다. (put_user(), copy_to_user(), get_user(), copy_from_user())
    • 이렇게 비효율적인 방법을 극복하기위해 리눅스에서는 mmap() 함수를 제공하여 응용 프로그램에서 직접 하드웨어의 I/O 주소공간을 메모리 복사없이 직접적으로 사용할 수 있도록 한다. mmap() 함수는 원래 메모리 주소를 이용해 파일에 접근할 수 있도록 하는 함수다. 그러나 디바이스 파일에 적용할 경우에는 디바이스에서 제공하는 물리주소 (I/O 메모리 주소 또는 할당된 메모리 공간주소)를 응용 프로그램에서 사용할 수 있게한다. 응용 프로그램이 동작하는 프로세스의 메모리 영역에 디바이스 드라이버가 제공하는 물리주소를 매핑하면 된다.
    • 동일한 물리주소를 가상주소로 매핑하는 방법에는 두가지가 있다. 하나는 mmap() 함수를 사용하여 응용프로그램의 프로세스 가상주소에 매핑하는 방법이고, 하나는 ioremap() 함수를 사용하여 커널의 가상주소에 매핑하는 방법이다.
    • nopage 매핑방식: 디바이스 드라이버에서 mmap을 처리하는 방식은 mmap() 함수에서remap_pfn_range() 함수를 이용하여 필요한 매핑을 직접처리하는 방법과 nopage 방식을 사용해 페이지 단위로 매핑하는 방법이 있다. 앞에서 설명한 방법이 remap_pfn_range() 함수를 이용해 매핑 대상이 되는 영역을 한꺼번에 매핑하는 방법이었다면, nopage 방법은 PAGE_SIZE 단위로 매핑을 처리한다. nopage 방식은 응용 프로그램에서 mmap() 함수를 호출하여 프로세스에서 사용할 수 있는 주소를 먼저 요구한다. nopage 방식은remap_pfn_range() 함수를 이용하는 방법처럼 응용 프로그램 mmap() 함수를 호출하면 디바이스 드라이버의 파일 오퍼레이션 구조체에 정의된 mmap() 함수가 호출된다. 그러나 핲서 설명한 mmap() 함수가 요청된 영역의 매핑을 remap_pfn_range() 함수를 이용해 처리하는 것과 달리 nopage 방식에서는 mmap() 함수가 remap_page_range() 함수를 수행하지 않는다. 그래서 커널이 해당 영역을 매핑하지 않기 때문에 응용 프로그램이 mmap()을 통해 주소에 접근하면 해당 메모리 주소를 유효하지 않은 영역으로 인식하여 페이지폴트가 발생한다. 커널은 페이지 폴트가 발생하면 디바이스 드라이버로 매핑하기 위해 해당 주소공간이 예약된 주소 영역인지를 확인하고, 예약된 영역이면 vma->vm_ops->nopage에 선언된 함수를 호출한다.  nopage 방식은 물리적인 I/O 메모리공간을 응용 프로그램의 프로세스 공간에 사용하기 보다는 주로 디바이스 드라이버에 의해 할당된 메모리 공간을 공유하기 위해 사용한다. nopage 방식으로 mmap을 구현하려면 디바이스 드라이버는 가장 먼저 페이지 폴트가 발생할 때 호출된 nopage() 함수를 만들어야 한다.


    • Linear MMAP

      1. 매핑할 파일에 대해 mmap file operation이 정의 되어 있는지 검사

      2. 파일 객체의 get_unmapped_area method 호출하여 메모리 매핑에 적합한 linear address range 를 할당

      3. 파일 시스템에서 vm_file field를 파일 객체의 주소로 초기화 하고 mmap method 호출 (generic_file_mmap)

      4. Done, 기타 자잘한 검사들은 생략

      매핑은 형성 되었지만, 그에 해당하는 PFN은 아직 할당 되지 않았기 때문에 demand paging을 해야한다. 이때 프로세스가 페이지 중 하나를 참조하여 page fault exception이 발생할 때까지 PFN할당을 늦춘다.커널은 폴트가 발생한 주소에 대응하는 페이지 테이블 엔트리를 검사하고, 엔트리가 없으면 do_no_page 함수를 호출한다. 

      do_no_page 함수는 페이지 프레임 할당과 페이지 테이블 갱신과 같이 모든 요구 페이징에 공통적인 연산을 수행한다. 또한 해당 memory region 에 nopage method를 정의하고 있는지 검사한다. nopage method 가 정의 되어 있을 경우, 

      1. nopage 메소드 호출하는데 요청한 페이지를 포함하는 PFN을 반환한다. 

      2.메모리 매핑이 private 이고, 프로세스가 페이지에 쓰기를 시도하면, 방금 읽은 페이지의 복사본을 만들고 이것을 inactive page list 에 삽입하여 'COW' fault가 발생하지 않도록 한다.

      3. fault가 발생한 주소에 대응하는 페이지 테이블 엔트리를 PFN과 memory region의 vm_page_prot  field에 포함된 페이지 접근 권한으로 설정한다. 

      4. ...


      nopage Method

      결국 매핑의 실제 동작의 키는 nopage method이며 nopage는 반드시 PFN을 반환해야 한다. nopage 는 요청한 페이지가 페이지 캐시에 있는지를 찾는다. 페이지를 찾지 못하면 method는 페이지를 디스크에서 읽어야 한다. 대부분의 파일 시스템은 filemap_nopage 함수를 사용하여 nopage method를 구현한다. 이 함수는 다음과 같은 새개의 매개 변수를 받는다. 

      (area,address,type) 

      ..........................

      나중에 채워 너을 거임.

      결국 nopage method는 페이지 캐시에 해당 파일의 블록이 있는지를 검사해서 없다면 페이지 캐시에 새로운 프레임을 할당, 블록을 읽어 들인다. 이후 해당 page cache 의 PFN 를 리턴한다. 


    출처

    http://linuxphil.blogspot.com/2011/12/blog-post.html

    Understanding the Linux Kernel, 3rd Edition