DMA -3- (DMA Pool)

<kernel v5.0>

DMA -3- (DMA Pool)

Coherent per-device Memory는 페이지 단위로 할당관리를 하는데 이보다 더 작은 단위의 사이즈를 관리하는 DMA pool을 여러 개 추가하여 사용할 수 있다.

다음 그림은 3개의 DMA pool을 생성한 모습을 보여준다.

  • 디바이스 전용으로 5M bytes coherent 메모리가 구성되어 있다.
  • 256, 512, 8192 바이트를 제공하는 각각의 DMA pool이 생성되어 있다.

 

DMA Pool 생성

dma_pool_create()

mm/dmapool.c

/**
 * dma_pool_create - Creates a pool of consistent memory blocks, for dma.
 * @name: name of pool, for diagnostics
 * @dev: device that will be doing the DMA
 * @size: size of the blocks in this pool.
 * @align: alignment requirement for blocks; must be a power of two
 * @boundary: returned blocks won't cross this power of two boundary
 * Context: !in_interrupt()
 *
 * Returns a dma allocation pool with the requested characteristics, or
 * null if one can't be created.  Given one of these pools, dma_pool_alloc()
 * may be used to allocate memory.  Such memory will all have "consistent"
 * DMA mappings, accessible by the device and its driver without using
 * cache flushing primitives.  The actual size of blocks allocated may be
 * larger than requested because of alignment.
 *
 * If @boundary is nonzero, objects returned from dma_pool_alloc() won't
 * cross that size boundary.  This is useful for devices which have
 * addressing restrictions on individual DMA transfers, such as not crossing
 * boundaries of 4KBytes.
 */
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                                 size_t size, size_t align, size_t boundary)
{
        struct dma_pool *retval;
        size_t allocation;
        bool empty = false;

        if (align == 0)
                align = 1;
        else if (align & (align - 1))
                return NULL;

        if (size == 0)
                return NULL;
        else if (size < 4)
                size = 4;

        if ((size % align) != 0)
                size = ALIGN(size, align);

        allocation = max_t(size_t, size, PAGE_SIZE);

        if (!boundary)
                boundary = allocation;
        else if ((boundary < size) || (boundary & (boundary - 1)))
                return NULL;

        retval = kmalloc_node(sizeof(*retval), GFP_KERNEL, dev_to_node(dev));
        if (!retval)
                return retval;

        strlcpy(retval->name, name, sizeof(retval->name));

        retval->dev = dev;

        INIT_LIST_HEAD(&retval->page_list);
        spin_lock_init(&retval->lock);
        retval->size = size;
        retval->boundary = boundary;
        retval->allocation = allocation;

        INIT_LIST_HEAD(&retval->pools);

        /*
         * pools_lock ensures that the ->dma_pools list does not get corrupted.
         * pools_reg_lock ensures that there is not a race between
         * dma_pool_create() and dma_pool_destroy() or within dma_pool_create()
         * when the first invocation of dma_pool_create() failed on
         * device_create_file() and the second assumes that it has been done (I
         * know it is a short window).
         */
        mutex_lock(&pools_reg_lock);
        mutex_lock(&pools_lock);
        if (list_empty(&dev->dma_pools))
                empty = true;
        list_add(&retval->pools, &dev->dma_pools);
        mutex_unlock(&pools_lock);
        if (empty) {
                int err;

                err = device_create_file(dev, &dev_attr_pools);
                if (err) {
                        mutex_lock(&pools_lock);
                        list_del(&retval->pools);
                        mutex_unlock(&pools_lock);
                        mutex_unlock(&pools_reg_lock);
                        kfree(retval);
                        return NULL;
                }
        }
        mutex_unlock(&pools_reg_lock);
        return retval;
}
EXPORT_SYMBOL(dma_pool_create);

요청한 디바이스용으로 블럭 사이즈(@size)를 할당해줄 수 있는 DMA coherent 메모리 풀을 생성한다. 보통 블럭 사이즈는 한 개 페이지보다 작은 단위의 크기(4~)를 사용하지만 클 수도 있다. 블럭 사이즈의 정렬 단위는 @align 값을 사용하고 2의 제곱승 단위 값만 허용한다.(1, 2, 4, 8, 16, …)  dma 페이지 내에서 @boundary 경계에 할당 블럭들이 자리잡지 못하게 막는 기능이다.  이 함수는 인터럽트 context에서 호출되면 안된다.

  • 코드 라인 8~11에서 @align 값이 지정되지 않은 경우 1 바이트로 지정된다. 또한 2의 제곱승 단위가 아닌 경우 실패 값인 null을 반환한다.
    • 예) @align=1, 2, 4, 8, 16, …
  • 코드 라인 13~16에서 @size 값이 지정되지 않은 경우 실패 값인 null을 반환한다. @size 값은 최소 4로 제한한다.
  • 코드 라인 18~19에서 @size 값은 @align 단위로 올림 처리한다.
    • 예) @align=8인 경우
      • @size=8, 16, 24, 32, … 와 같이 8 단위로 올림 정렬된다.
  • 코드 라인 21에서 실제 할당되는 크기는 1 페이지 또는 요청한 블럭 사이즈(@size) 중 큰 값을 사용한다.
  • 코드 라인 23~26에서 @boundary가 지정되지 않은 경우 할당 사이즈와 같다. 만일 @boundary가 지정되었지만 블럭 사이즈(@size)보다 작거나 2의 제곱승 단위로 지정되지 않은 경우 실패 값인 null을 반환한다.
  • 코드 라인 28~42에서 슬랩 메모리에서 dma_pool 구조체 사이즈만큼 할당해온 후 초기 설정 값들로 초기화한다.
  • 코드 라인 52~72에서 할당한 dma_pool 구조체를 디바이스의 dma_pools 리스트에 추가하고 “pools” 속성 파일을 생성한다.
    • 이 파일을 통해 이 풀의 통계 정보를 출력할 수 있다.

 

다음 그림은 256 바이트 @size와 @align 값을 사용하여 dma pool 정보를 디바이스에 추가하는 모습을 보여준다.

  • @size가 1 페이지 미만이므로 allocation 값은 최소값인 1 페이지에 해당하는 바이트를 지정한다.
  • @boundary 값으로 0을 사용하여 allocation 값과 동일하게 한다. 즉 allocation 내에 boundary가 없는 것이다.

 

DMA Pool에서 블럭 할당

dma_pool_alloc()

mm/dmapool.c

/**
 * dma_pool_alloc - get a block of consistent memory
 * @pool: dma pool that will produce the block
 * @mem_flags: GFP_* bitmask
 * @handle: pointer to dma address of block
 *
 * This returns the kernel virtual address of a currently unused block,
 * and reports its dma address through the handle.
 * If such a memory block can't be allocated, %NULL is returned.
 */
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags,
                     dma_addr_t *handle)
{
        unsigned long flags;
        struct dma_page *page;
        size_t offset;
        void *retval;

        might_sleep_if(gfpflags_allow_blocking(mem_flags));

        spin_lock_irqsave(&pool->lock, flags);
        list_for_each_entry(page, &pool->page_list, page_list) {
                if (page->offset < pool->allocation)
                        goto ready;
        }

        /* pool_alloc_page() might sleep, so temporarily drop &pool->lock */
        spin_unlock_irqrestore(&pool->lock, flags);

        page = pool_alloc_page(pool, mem_flags & (~__GFP_ZERO));
        if (!page)
                return NULL;

        spin_lock_irqsave(&pool->lock, flags);

        list_add(&page->page_list, &pool->page_list);
 ready:
        page->in_use++;
        offset = page->offset;
        page->offset = *(int *)(page->vaddr + offset);
        retval = offset + page->vaddr;
        *handle = offset + page->dma;
#ifdef  DMAPOOL_DEBUG
        {
                int i;
                u8 *data = retval;
                /* page->offset is stored in first 4 bytes */
                for (i = sizeof(page->offset); i < pool->size; i++) {
                        if (data[i] == POOL_POISON_FREED)
                                continue;
                        if (pool->dev)
                                dev_err(pool->dev,
                                        "dma_pool_alloc %s, %p (corrupted)\n",
                                        pool->name, retval);
                        else
                                pr_err("dma_pool_alloc %s, %p (corrupted)\n",
                                        pool->name, retval);

                        /*
                         * Dump the first 4 bytes even if they are not
                         * POOL_POISON_FREED
                         */
                        print_hex_dump(KERN_ERR, "", DUMP_PREFIX_OFFSET, 16, 1,
                                        data, pool->size, 1);
                        break;
                }
        }
        if (!(mem_flags & __GFP_ZERO))
                memset(retval, POOL_POISON_ALLOCATED, pool->size);
#endif
        spin_unlock_irqrestore(&pool->lock, flags);

        if (mem_flags & __GFP_ZERO)
                memset(retval, 0, pool->size);

        return retval;
}
EXPORT_SYMBOL(dma_pool_alloc);

요청한 DMA pool에서 블럭을 하나 할당해온다. 할당한 블럭의 가상 주소가 반환되며, @handle에는 물리 주소가 저장된다.

  • 코드 라인 9에서 preemption point로 blocking 가능한 할당 요청인 경우 스케줄링 요청에 따라 sleep할 수 있다.
  • 코드 라인 11~18에서 dma pool에 등록된 dma 페이지를 대상으로 할당하지 않고 free한 페이지가 있는지 확인한다.
    • 모든 블럭이 할당 상태인 dma 페이지는 dma->offset이 dma->allocation을 초과한다.
  • 코드 라인 20~22에서 free한 상태의 dma 페이지가 없으므로 dma pool에서 dma 페이지를 할당해온다.
  • 코드 라인 24~26에서 할당받은 dma 페이지를 dma pool에 추가한다.
  • 코드 라인 28에서사용 블럭 수를 의미하는 in_use를 증가시킨다.
  • 코드 라인 29~30에서 page->offset이 빈블럭의 offset을 지정한다.
    • dma_page 가상 주소 + 기존 page->offset의 값을 읽어와서 다시 page->offset에 대입한다.
  • 코드 라인 31~32에서 할당받은 블럭에 해당하는 가상 주소 retval과 물리 주소 *handle에 지정한다.
  • 코드 라인 33~60에서 DMAPOOL_DEBUG가 설정된 경우 할당 받은 메모리가 POOL_POISON_FREED(0xa7) 상태가 아닌 경우 이를 경고 출력한다. 정상적인 경우 할당 받은 메모리를  POOL_POISON_ALLOCATED(0xa9) 값으로 채운다.
  • 코드 라인 63~64에서 __GFP_ZERO gfp 플래그로 요청한 경우 할당 블럭을 0으로 모두 채운다.

 

다음 그림은 DMA pool에 등록된 1개의 DMA page에 8개의 DMA 블럭이 있고, 현재 3번째 DMA 블럭 할당 요청이 수행된 후의 상태를 보여준다.

  • page->offset 값이 빈 DMA 블럭(4번)에 해당하는 offset이 지정된 것을 알 수 있다.
  • DMA 블럭이 하나도 할당되지 않았을 때의 page->offset은 0이다.
    • 할당될 때마다 pool->size 만큼 offset이 지정된다.
      • 512, 1024, 1536, …

 

pool_alloc_page()

mm/dmapool.c

static struct dma_page *pool_alloc_page(struct dma_pool *pool, gfp_t mem_flags)
{
        struct dma_page *page;

        page = kmalloc(sizeof(*page), mem_flags);
        if (!page)
                return NULL;
        page->vaddr = dma_alloc_coherent(pool->dev, pool->allocation,
                                         &page->dma, mem_flags);
        if (page->vaddr) {
#ifdef  DMAPOOL_DEBUG
                memset(page->vaddr, POOL_POISON_FREED, pool->allocation);
#endif
                pool_initialise_page(pool, page);
                page->in_use = 0;
                page->offset = 0;
        } else {
                kfree(page);
                page = NULL;
        }
        return page;
}

요청한 DMA pool에 하나의 DMA 페이지를 할당한 후 추가한다.

  • 코드 라인 5~7에서 dma_page 구조체를 할당해온다.
  • 코드 라인 8~9에서 coherent per-device 메모리에서 pool->allocation 사이즈(페이지 단위)만큼 할당해온다.
  • 코드 라인 10~21에서 할당이 성공한 경우 할당한 공간을 초기화한다. dma 페이지 내부의 초기화는 블럭 사이즈(pool->size) 단위로 offset 값을 기록한다.

 

다음 그림은 DMA 페이지를 할당받은 후 초기화를 한 모습을 보여준다.

  • 아직 할당해준 블럭이 없으므로 page->in_use는 0이고, page->offset도 처음 블럭을 의미하는 0 값이다.
  • 각 블럭의 첫 4바이트는 다음 블럭에 해당하는 offset 값을 갖는다.

 

pool_initialise_page()

mm/dmapool.c

static void pool_initialise_page(struct dma_pool *pool, struct dma_page *page)
{
        unsigned int offset = 0;
        unsigned int next_boundary = pool->boundary;

        do {
                unsigned int next = offset + pool->size;
                if (unlikely((next + pool->size) >= next_boundary)) {
                        next = next_boundary;
                        next_boundary += pool->boundary;
                }
                *(int *)(page->vaddr + offset) = next;
                offset = next;
        } while (offset < pool->allocation);
}

 

요청한 DMA pool의 DMA 페이지를 초기화한다.

  • 블럭마다  다음 블럭의 offset 값에 해당하는 4바이트의 offset 값을 지정한다.
  • 만일 boundary가 설정된 경우 각 블럭이 boundary 경계를 침범하지 않도록 조정한다.

 

다음 그림은 boundary가 설정된 DMA 페이지의 초기화 모습을 보여준다.

  • boundary가 DMA 페이지 중간에 설정되어 있으므로 블럭이 그 경계에 할당되지 않도록 offset을 조절한다.

 

DMA Pool로 블럭 할당 해제

dma_pool_free()

mm/dmapool.c

/**
 * dma_pool_free - put block back into dma pool
 * @pool: the dma pool holding the block
 * @vaddr: virtual address of block
 * @dma: dma address of block
 *
 * Caller promises neither device nor driver will again touch this block
 * unless it is first re-allocated.
 */
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma)
{
        struct dma_page *page;
        unsigned long flags;
        unsigned int offset;

        spin_lock_irqsave(&pool->lock, flags);
        page = pool_find_page(pool, dma);
        if (!page) {
                spin_unlock_irqrestore(&pool->lock, flags);
                if (pool->dev)
                        dev_err(pool->dev,
                                "dma_pool_free %s, %p/%lx (bad dma)\n",
                                pool->name, vaddr, (unsigned long)dma);
                else
                        pr_err("dma_pool_free %s, %p/%lx (bad dma)\n",
                               pool->name, vaddr, (unsigned long)dma);
                return;
        }

        offset = vaddr - page->vaddr;
#ifdef  DMAPOOL_DEBUG
        if ((dma - page->dma) != offset) {
                spin_unlock_irqrestore(&pool->lock, flags);
                if (pool->dev)
                        dev_err(pool->dev,
                                "dma_pool_free %s, %p (bad vaddr)/%pad\n",
                                pool->name, vaddr, &dma);
                else
                        pr_err("dma_pool_free %s, %p (bad vaddr)/%pad\n",
                               pool->name, vaddr, &dma);
                return;
        }
        {
                unsigned int chain = page->offset;
                while (chain < pool->allocation) {
                        if (chain != offset) {
                                chain = *(int *)(page->vaddr + chain);
                                continue;
                        }
                        spin_unlock_irqrestore(&pool->lock, flags);
                        if (pool->dev)
                                dev_err(pool->dev, "dma_pool_free %s, dma %pad already free\n",
                                        pool->name, &dma);
                        else
                                pr_err("dma_pool_free %s, dma %pad already free\n",
                                       pool->name, &dma);
                        return;
                }
        }
        memset(vaddr, POOL_POISON_FREED, pool->size);
#endif

        page->in_use--;
        *(int *)vaddr = page->offset;
        page->offset = offset;
        /*
         * Resist a temptation to do
         *    if (!is_page_busy(page)) pool_free_page(pool, page);
         * Better have a few empty pages hang around.
         */
        spin_unlock_irqrestore(&pool->lock, flags);
}
EXPORT_SYMBOL(dma_pool_free);

할당받은 DMA 블럭 메모리를 할당 해제하여 다시 DMA pool로 회수한다.

  • 코드 라인 7~19에서 해제할 물리 주소(@dma)를 사용하여 관련된 DMA 페이지를 알아온다. 만일 발견되지 않는 경우 에러 메시지를 출력하고 함수를 빠져나간다.
  • 코드 라인 21에서 할당 해제하고자 하는 블럭에 해당하는 offset 값을 알아온다.
  • 코드 라인 22~52에서 DMAPOOL_DEBUG 커널 옵션을 사용하는 경우 DMA 블럭의 가상 주소 offset과 물리 주소 offset이 서로 다른 경우 에러 메시지를 출력하고 함수를 빠져나간다. 정상적으로 offset이 서로 일치하는 경우 page->offset부터 마지막 블럭까지 chain이 offset과 하나라도 같으면 에러 메시지를 출력하고 함수를 빠져나간다. 최종적으로 이상이 없는 경우 할당 해제한 블럭을 POOL_POISON_FREED(0xa7) 값으로 채운다.
  • 코드 라인 54에서 할당 블럭 수를 의미하는 in_use를 1 감소시킨다.
  • 코드 라인 55에서 할당 해제한 블럭의 첫 4바이트에 기존에 page->offset 값으로 치환한다.
  • 코드 라인 56에서 page->offset 값은 다시 할당 해제한 블럭에 대한 offset 값을 지정한다.

 

다음 그림은 할당된 3 개의 DMA 블럭 중 가운데에 있는 DMA 블럭을 할당해제하는 경우를 보여준다.

 

pool_find_page()

mm/dmapool.c

static struct dma_page *pool_find_page(struct dma_pool *pool, dma_addr_t dma)
{
        struct dma_page *page;

        list_for_each_entry(page, &pool->page_list, page_list) {
                if (dma < page->dma)
                        continue;
                if ((dma - page->dma) < pool->allocation)
                        return page;
        }
        return NULL;
}

DMA 블럭 주소가 위치한 DMA 페이지를 검색한다.

 

다음 그림은 DMA pool에 등록된 3개의 DMA 페이지를 검색하는 모습을 보여준다.

 

참고

댓글 남기기