Exception -6- (MM Fault Handler)

 

메모리 Fault 핸들러

 

유저 태스크에서 sys_mmap()이나 sys_brk() 등을 통해 커널에 메모리를 요청하면 커널은 곧바로 해당 태스크에 메모리를 할당하여 매핑하지 않는다. 커널은 요청을 받으면 요청한 유저 태스크의 rb 트리(로 관리하는 vma 정보에 메모리 range 및 플래그 등의 정보만을 기록(추가 또는 변경)한다. 그런 후 유저 태스크에서 실제 해당 페이지에 접근하려할 때 fault가 발생하도록 유도한다. fault 핸들러는 이러한 요구에 대한 처리를 수행한다. 요청한 페이지가 처음 0으로 초기화된 메모리를 요청하는 경우 zero 페이지에 매핑을 하거나, 다른 태스크에서 사용중인 페이지로 매핑하여 페이지를 공유하게 하는 방법을 사용한다. 이렇게 처리하게 되면 다음 몇 가지의 장점을 얻게된다.

  • 유저 태스크가 요청한 메모리를 할당 받자마자 모두 다 사용하는 것이 아니므로 필요한 메모리에 접근할 때에만 물리 메모리를 할당하면 실제 메모리의 사용을 절약할 수 있다.
    • anon 매핑: 파일과 관계 없는 유저 태스크가 요구하는 heap, stack  메모리 요청
    • file 매핑: 유저가 파일을 가상 주소 공간에 매핑(memory-mapped 파일) 요청 – mmap 메모리 요청
  • 유저 태스크가 요청한 메모리에 대한 처리를 커널이 빠르게 수행할 수 있다. – COW(Copy On Write) 방법
    • 부모 태스크가 child 태스크의 생성을 위해 fork 또는 clone등을 수행할 때 child 태스크가 사용할 메모리 할당 요청에 대해 즉각적으로 물리 페이지를 할당하지 않고 부모 태스크가 사용하던 메모리 페이지 테이블을 복사하여 사용한다. 이렇게 하여 빠른 태스크의 생성이 가능해진다.
  • h/w 아키텍처가 실제 메모리의 접근에 대한 모니터링을 지원하지 못하는 경우에도 이러한 fault 처리를 통해 리눅스가 알아낼 수 있도록 표식을 남길 수 있다. (young 플래그)

 

MMU가 TLB를 통해 Table walk를 수행하다 매핑되지 않은 페이지이거나  페이지 fault 핸들러인 do_page_fault() -> __do_page_fault() 함수를 통해 handle_mm_fault() 함수가 호출되었다. 다음 그림에서 그 이후 함수 호출 흐름을 보여준다.

 

handle_mm_fault()

mm/memory.c

/*
 * By the time we get here, we already hold the mm semaphore
 *
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
                unsigned int flags)
{
        vm_fault_t ret;

        __set_current_state(TASK_RUNNING);

        count_vm_event(PGFAULT);
        count_memcg_event_mm(vma->vm_mm, PGFAULT);

        /* do counter updates before entering really critical section. */
        check_sync_rss_stat(current);

        if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
                                            flags & FAULT_FLAG_INSTRUCTION,
                                            flags & FAULT_FLAG_REMOTE))
                return VM_FAULT_SIGSEGV;

        /*
         * Enable the memcg OOM handling for faults triggered in user
         * space.  Kernel faults are handled more gracefully.
         */
        if (flags & FAULT_FLAG_USER)
                mem_cgroup_enter_user_fault();

        if (unlikely(is_vm_hugetlb_page(vma)))
                ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
        else
                ret = __handle_mm_fault(vma, address, flags);

        if (flags & FAULT_FLAG_USER) {
                mem_cgroup_exit_user_fault();
                /*
                 * The task may have entered a memcg OOM situation but
                 * if the allocation error was handled gracefully (no
                 * VM_FAULT_OOM), there is no need to kill anything.
                 * Just clean up the OOM state peacefully.
                 */
                if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
                        mem_cgroup_oom_synchronize(false);
        }

        return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);

유저 공간의 vma 영역에 대해 접근하다 fault가 발생하여 진입한 핸들러이다. vma 영역 상태에 따라 처리 방법이 다르다.

  • 코드 라인 6에서 현재 태스크를 TASK_RUNNING 상태로 설정한다.
  • 코드 라인 8~9에서 PGFAULT vm 카운터를 증가시기고, 현재 태스크가 memcg 통제를 받는 경우 이에 대해서도 증가시킨다.
  • 코드 라인 12에서 해당 태스크에서 TASK_RSS_EVENTS_THRESH(64)번 만큼 fault가 발생한 경우 per-cpu rss 관련 메모리 통계를 글로벌에 갱신한다.
  • 코드 라인 14~17에서 코드 영역에 기록을 수행하려 할 때 특정 아키텍처가 지원하지 않는 경우 VM_FAULT_SIGSEGV를 반환한다.
    • arch_vma_access_permitted() 함수는 x86, powerpc에 해당 코드를 지원하며 그 밖의 경우 항상 true를 반환한다.
  • 코드 라인 23~24에서 유저 영역에 대한 fault인 경우 memcg OOM 기능을 동작하도록 enable 한다.
  • 코드 라인 26~29에서 hugetlb를 사용하는 영역 여부에 따라 각각의 fault 함수를 호출한다.
  • 코드 라인 31~41에서 유저 영역에 대한 fault인 경우 memcg OOM 기능을 disable 한다. 만일 태스크가 memcg oom을 진행중이고 fault 핸들러 결과가 fault OOM이 아닌 경우 oom 동기화도 수행한다.

 

__handle_mm_fault()

mm/memory.c -1/2-

/*
 * By the time we get here, we already hold the mm semaphore
 *
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 */
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
                unsigned long address, unsigned int flags)
{
        struct vm_fault vmf = {
                .vma = vma,
                .address = address & PAGE_MASK,
                .flags = flags,
                .pgoff = linear_page_index(vma, address),
                .gfp_mask = __get_fault_gfp_mask(vma),
        };
        unsigned int dirty = flags & FAULT_FLAG_WRITE;
        struct mm_struct *mm = vma->vm_mm;
        pgd_t *pgd;
        p4d_t *p4d;
        vm_fault_t ret;

        pgd = pgd_offset(mm, address);
        p4d = p4d_alloc(mm, pgd, address);
        if (!p4d)
                return VM_FAULT_OOM;

        vmf.pud = pud_alloc(mm, p4d, address);
        if (!vmf.pud)
                return VM_FAULT_OOM;
        if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
                ret = create_huge_pud(&vmf);
                if (!(ret & VM_FAULT_FALLBACK))
                        return ret;
        } else {
                pud_t orig_pud = *vmf.pud;

                barrier();
                if (pud_trans_huge(orig_pud) || pud_devmap(orig_pud)) {

                        /* NUMA case for anonymous PUDs would go here */

                        if (dirty && !pud_write(orig_pud)) {
                                ret = wp_huge_pud(&vmf, orig_pud);
                                if (!(ret & VM_FAULT_FALLBACK))
                                        return ret;
                        } else {
                                huge_pud_set_accessed(&vmf, orig_pud);
                                return 0;
                        }
                }
        }

p4d, pud, pmd 및 pte 테이블이 필요한 경우 할당하여 연결(population)하고 루틴의 마지막에서 handle_pte_fault() 함수를 호출하여 pte 테이블에서의 fault 처리를 수행한다.

pud population
  • 코드 라인 4~10에서 fault 처리를 위해 여러 함수가 사용되는데 필요한 인자가 많아서 이를 쉽게 전달할 구조체 vm_fault에 담는다. 주요 정보로 다음들을 담는다.
    • .vma
      • fault 발생한 vma 영역
    • .address
      • fault 발생한 가상 주소 페이지의 시작 주소
    • .flags
    • .pgoff
      • vma 영역내에서 fault 발생한 가상 주소 페이지의 offset 페이지 번호
    • .gfp_mask
      • 파일 매핑된 경우 fs, io를 포함한 매핑에 사용한 gfp 플래그를 사용하고, 그 외의 경우 GFP_KERNEL에 해당하는 gfp 플래그를 사용한다.
  • 코드 라인 11에서 write 중에 fault가 발생했는지 여부를 dirty에 대입한다.
  • 코드 라인 17에서 fault 가상 주소에 해당하는 pgd 엔트리를 알아온다.
  • 코드 라인 18~20에서 pgd 엔트리가 빈 경우 연결을 위해 다음 레벨인 p4d 테이블을 할당한다.
  • 코드 라인 22~24에서 p4d 엔트리가 빈 경우 연결을 위해 다음 레벨인 pud 테이블을 할당한다.
  • 코드 라인 25~28에서 pud 엔트리가 비어있고, pud 단위의 블록 매핑을 사용할 조건을 만족하면 huge pud를 할당한다.
    • ARM64의 경우 pud 단위의 블럭 매핑은 1G에 해당한다.
  • 코드 라인 29~46에서 pud_trans_huge() 함수에선 pud 단위의 thp가 가능한 경우에 한해 다음과 같이 처리한다.
    • pud 엔트리가 읽기 전용 매핑되었고, write 시도하여 fault가 발생한 경우 파일 매핑된 파일 시스템이 (*huge_fault) 후크를 지원하는 경우 이를 실행한다. 그 외 anonymous 등은 아직 지원하지 않아 VM_FAULT_FALLBACK을 반환받아 오므로 계속 코드를 진행한다.
    • 그 외의 경우 pud 엔트리에 읽었음을 의미하는 young 플래그를 기록한다. 또한 write 시도였던 경우 dirty 플래그도 기록한다.

 

DAX(Direct Access)와 huge pud fault

DAX를 지원하는 파일 시스템이 (*huge_fault) 후크 함수를 지원하면, 이를 통해 이 영역에 접근 하여 fault가 발생하는 경우 pte 단위(4K) 보다 더 큰 pmd(2M) 단위 또는 pud 단위(ARM64에서 1G )로 크게 매핑하면 그 만큼 fault 발생 횟수를 줄이므로 성능을 향상시킨다.

  • 현재 리눅스에서 DAX를 지원하는 파일 시스템과 지원하는 fault 매핑 크기는 다음과 같다.
    • ext2의 경우 pte(4K) 단위의 fault 매핑을 지원한다.
    • ext4의 경우 pte(4K) 및 pmd(2M) 단위의 huge fault 매핑을 지원한다.
    • xfs의 경우 pte(4K), pmd(2M) 및 pud(1G) 단위의 huge fault 매핑을 지원한다.
  • MS 윈도우 ntfs의 경우에도 dax를 지원하나, 리눅스에서는 아직 지원하지 않는다.
  • 참고: Persistent Memory & DAX | 문c

 

mm/memory.c -2/2-

        vmf.pmd = pmd_alloc(mm, vmf.pud, address);
        if (!vmf.pmd)
                return VM_FAULT_OOM;
        if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
                ret = create_huge_pmd(&vmf);
                if (!(ret & VM_FAULT_FALLBACK))
                        return ret;
        } else {
                pmd_t orig_pmd = *vmf.pmd;

                barrier();
                if (unlikely(is_swap_pmd(orig_pmd))) {
                        VM_BUG_ON(thp_migration_supported() &&
                                          !is_pmd_migration_entry(orig_pmd));
                        if (is_pmd_migration_entry(orig_pmd))
                                pmd_migration_entry_wait(mm, vmf.pmd);
                        return 0;
                }
                if (pmd_trans_huge(orig_pmd) || pmd_devmap(orig_pmd)) {
                        if (pmd_protnone(orig_pmd) && vma_is_accessible(vma))
                                return do_huge_pmd_numa_page(&vmf, orig_pmd);

                        if (dirty && !pmd_write(orig_pmd)) {
                                ret = wp_huge_pmd(&vmf, orig_pmd);
                                if (!(ret & VM_FAULT_FALLBACK))
                                        return ret;
                        } else {
                                huge_pmd_set_accessed(&vmf, orig_pmd);
                                return 0;
                        }
                }
        }

        return handle_pte_fault(&vmf);
}
pmd population
  • 코드 라인 1~3에서 pud 엔트리가 빈 경우 연결을 위해 다음 레벨인 pmd 테이블을 할당한다.
  • 코드 라인 4~7에서 pmd 엔트리가 비어있고, pmd 단위의 블록 매핑을 사용할 조건을 만족하면 huge pmd를 할당한다.
    • ARM64의 경우 pmd 단위의 블럭 매핑은 2M에 해당한다.
  • 코드 라인 8~18에서 pmd 엔트리가 존재하는 경우이다. 낮은 확률로 pmd 엔트리가 swap 된 상태에서 fault가 발생한 경우 swap 영역으로 부터 로딩 중이다. 잠시 기다렸다 성공 값 0을 반환한다.
  • 코드 라인 19~32에서 pmd_trans_huge() 함수에선 pmd 단위의 thp가 가능하거나 pmd 단위의 디바이스 메모리의 맵이 가능한 경우에 한해 다음과 같이 처리한다.
    • vma 영역에 접근이 가능한 상태이면 do_huge_pmd_numa_page() 함수를 통해 huge pmd에 대한 처리를 수행한다.
    • pmd 엔트리가 읽기 전용 매핑되었고, write 시도하여 fault가 발생한 경우 파일 매핑된 파일 시스템이 (*huge_fault) 후크를 지원하는 경우 이를 실행한다. 그 외 anonymous 등은 아직 지원하지 않아 VM_FAULT_FALLBACK을 반환받아 오므로 계속 코드를 진행한다.
    • 그 외의 경우 pmd 엔트리에 읽었음을 의미하는 young 플래그를 기록한다. 또한 write 시도였던 경우 dirty 플래그도 기록한다.
pte fault 처리
  • 코드 라인 34에서  마지막 pte fault를 처리하기 위해 handle_pte_fault() 함수를 호출한다.

 

handle_pte_fault()

mm/memory.c

/*
 * These routines also need to handle stuff like marking pages dirty
 * and/or accessed for architectures that don't do it in hardware (most
 * RISC architectures).  The early dirtying is also good on the i386.
 *
 * There is also a hook called "update_mmu_cache()" that architectures
 * with external mmu caches can use to update those (ie the Sparc or
 * PowerPC hashed page tables that act as extended TLBs).
 *
 * We enter with non-exclusive mmap_sem (to exclude vma changes, but allow
 * concurrent faults).
 *
 * The mmap_sem may have been released depending on flags and our return value.
 * See filemap_fault() and __lock_page_or_retry().
 */
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
        pte_t entry;

        if (unlikely(pmd_none(*vmf->pmd))) {
                /*
                 * Leave __pte_alloc() until later: because vm_ops->fault may
                 * want to allocate huge page, and if we expose page table
                 * for an instant, it will be difficult to retract from
                 * concurrent faults and from rmap lookups.
                 */
                vmf->pte = NULL;
        } else {
                /* See comment in pte_alloc_one_map() */
                if (pmd_devmap_trans_unstable(vmf->pmd))
                        return 0;
                /*
                 * A regular pmd is established and it can't morph into a huge
                 * pmd from under us anymore at this point because we hold the
                 * mmap_sem read mode and khugepaged takes it in write mode.
                 * So now it's safe to run pte_offset_map().
                 */
                vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
                vmf->orig_pte = *vmf->pte;

                /*
                 * some architectures can have larger ptes than wordsize,
                 * e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and
                 * CONFIG_32BIT=y, so READ_ONCE cannot guarantee atomic
                 * accesses.  The code below just needs a consistent view
                 * for the ifs and we later double check anyway with the
                 * ptl lock held. So here a barrier will do.
                 */
                barrier();
                if (pte_none(vmf->orig_pte)) {
                        pte_unmap(vmf->pte);
                        vmf->pte = NULL;
                }
        }

        if (!vmf->pte) {
                if (vma_is_anonymous(vmf->vma))
                        return do_anonymous_page(vmf);
                else
                        return do_fault(vmf);
        }

        if (!pte_present(vmf->orig_pte))
                return do_swap_page(vmf);

        if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
                return do_numa_page(vmf);

        vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);
        spin_lock(vmf->ptl);
        entry = vmf->orig_pte;
        if (unlikely(!pte_same(*vmf->pte, entry)))
                goto unlock;
        if (vmf->flags & FAULT_FLAG_WRITE) {
                if (!pte_write(entry))
                        return do_wp_page(vmf);
                entry = pte_mkdirty(entry);
        }
        entry = pte_mkyoung(entry);
        if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
                                vmf->flags & FAULT_FLAG_WRITE)) {
                update_mmu_cache(vmf->vma, vmf->address, vmf->pte);
        } else {
                /*
                 * This is needed only for protection faults but the arch code
                 * is not yet telling us if this is a protection fault or not.
                 * This still avoids useless tlb flushes for .text page faults
                 * with threads.
                 */
                if (vmf->flags & FAULT_FLAG_WRITE)
                        flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
        }
unlock:
        pte_unmap_unlock(vmf->pte, vmf->ptl);
}

요청한 가상 주소에서 fault된 경우 마지막 pte에 엔트리에 대한 처리이다. fault에 의해 처리되는 주요 항목은 다음과 같다.

  • file 매핑 타입으로 사용되는 vma 영역에서 fault된 경우 file과 관련한 fault 처리를 수행한다.
  • anon 타입으로 사용되는 vma 영역에서 fault된 경우 새 페이지를 매핑하고 reverse anon에 역매핑 관리를 추가한다.
  • swap된 anon 페이지에서 fault된 경우 새 페이지를 swap 파일로부터 읽는다.
  • write protect 매핑되어 있는 공유메모리에 write 하려다가 fault된 경우 해당 페이지를 새 페이지에 복사한 후 write 매핑한다. (COW)
  • numa 페이지 migration
    • numa 밸런싱을 위해 fault된 경우에는 페이지를 migration 한다.

 

  • 코드 라인 5~12에서 마지막 pte 테이블을 가리키는 pmd 엔트리가 비어있는 경우이다. vmf->pte에 null을 대입하여 이어지는 루틴에서 이에 대한 처리를 하도록 한다.
  • 코드 라인 13~39에서 pmd 엔트리가 존재하는 경우이다. vmf->pte에 pte 엔트리를 알아온다. 단 pmd 단위로 동작하는 디바이스 메모리이거나 pmd 단위로 동작하는 thp인 경우 더 이상 진행할 필요 없으므로 0을 반환한다.
fault에 의해 처리되는 주요 항목들
  • 코드 라인 41~46에서 테이블을 pte 엔트리가 매핑되어 있지 않은 경우 fault된 vma 영역이 file 매핑 또는 anon 매핑인지 구분하여 해당 처리 함수를 호출한다.
    • anon 매핑을 사용하는 경우 do_anonymous_page() 함수를 호출하여 새로운 페이지를 할당받아 매핑한다. 이 방법을 lazy 페이지 할당이라고 한다.
    • file 매핑을 사용하는 경우 file로부터 페이지를 읽어들이기 위해 do_fault() 함수를 호출한다.
  • 코드 라인 48~49에서 swap 엔트리인 경우 swap 된 페이지를 불러오기 위해 do_swap_page()를 호출한다.
  • 코드 라인 51~52에서 NUMA 밸런싱을 위해 fault가 발생한 경우 페이지를 migration하기 위해 do_numa_page() 함수를 호출한다.
  • 코드 라인 54~58에서 지금부터 pte 엔트리 값을 변경하기 위해 페이지 테이블 락을 획득한다.
    • pmd 엔트리 값에 해당하는, 즉 pte 페이지 테이블에 대한 페이지의 ptl(page table lock) 값을 가져오고 lock을 수행한다.
  • 코드 라인 59~63에서 페이지에 write 요청을 한 경우 엔트리 값에 dirty 설정을 한다. 또한 write protect 엔트리 즉 read-only 공유 페이지에 write 요청이 온 경우 기존 공유페이지를 새 페이지에 복사하기 위해 do_wp_page() 함수를 호출한다.
    • 유저가 공유메모리에 대해 write 요청을 하는 경우 기존 공유 페이지를 새 페이지에 COW(Copy-On-Write) 한다.
pte 엔트리 갱신
  • 코드 라인 64에서 엔트리 값에 현재 페이지에 acceess 하였음을 표시하는 young 비트 설정을 한다.
  • 코드 라인 65~77에서 pte 값과 엔트리 값이 다른 경우 pte 테이블 엔트리 값을 갱신한다. 실제 업데이트가 이루어진 경우 캐시도 업데이트한다. 만일 값이 같아 pte 테이블 엔트리 값을 업데이트 하지 않더라도 write 요청이 있었으면 tlb flush를 진행해야한다.
    • update_mmu_cache() 함수
      • arm 아키텍처 v6 이상 및 arm64에서는 아무런 동작을 하지 않아도 된다.

 

Page Translation Fault

ARM 및 ARM64의 경우 페이지 테이블 단계별로 엔트리에 접근하는데 이 때 해당 가상 주소에 대한 각 단계별 페이지 테이블의 엔트리 값중 bit0가 0인 경우 fault가 발생한다.

 

Lazy Page Allocation

유저 프로세스가 malloc() 함수를 호출할 때 Heap 매니저가 메모리가 부족 시 커널로 메모리 할당을 요청하는데 이 때 커널은 물리메모리는 할당하지 않고 가상 주소 영역(vma)만 지정한 후 페이지 테이블에는 null 매핑을 해둔다. 이렇게 비어있는 pte 엔트리를 비워두고 유저가 이 공간에 접근할 때 매핑되지 않은 공간이므로 fault가 발생하게 되는데 이 때에 실제 물리 메모리를 할당하고 해당 가상 주소에 매핑하는 방식을 사용한다.

 

swap 엔트리
  • pte 엔트리 값의 bit0 값이 0이면 매핑되지 않아서 fault가 발생한다. 그 엔트리의 다른 비트들에 어떠한 값이 존재하면 그 pte 엔트리는 swap 엔트리 값으로 사용한다.
  • swap 엔트리의 관리 메모리를 절약하기 위해 pte 엔트리를 사용한다.

 

NUMA 밸런싱 & protnone()
  • NUMA 시스템에서 해당 페이지를 읽을 때 accesss 권한 실패로 인해 abort exception이 발생되어 fault된 후 해당 페이지를 사용하는 태스크의 migration을 고려하는 Automatic NUMA balancing을 위해 사용된다. NUMA 밸런싱을 사용하지 않는 UMA 시스템에서는 항상 0을 반환한다.
  • ARM64
    • return (pte_val(pte) & (PTE_VALID | PTE_PROT_NONE)) == PTE_PROT_NONE;
    • none 비트만 설정되고 valid 비트는 없는 상태
  • x86
    • return (pmd_flags(pmd) & (_PAGE_PROTNONE | _PAGE_PRESENT)) == _PAGE_PROTNONE;
    • none 비트만 설정되고 present 비트는 없는 상태

 


파일 매핑(mmap)된 주소에서의 fault 처리

 

do_fault()

mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults).
 * The mmap_sem may have been released depending on flags and our
 * return value.  See filemap_fault() and __lock_page_or_retry().
 * If mmap_sem is released, vma may become invalid (for example
 * by other thread calling munmap()).
 */
static vm_fault_t do_fault(struct vm_fault *vmf)
{
        struct vm_area_struct *vma = vmf->vma;
        struct mm_struct *vm_mm = vma->vm_mm;
        vm_fault_t ret;

        /*
         * The VMA was not fully populated on mmap() or missing VM_DONTEXPAND
         */
        if (!vma->vm_ops->fault) {
                /*
                 * If we find a migration pmd entry or a none pmd entry, which
                 * should never happen, return SIGBUS
                 */
                if (unlikely(!pmd_present(*vmf->pmd)))
                        ret = VM_FAULT_SIGBUS;
                else {
                        vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
                                                       vmf->pmd,
                                                       vmf->address,
                                                       &vmf->ptl);
                        /*
                         * Make sure this is not a temporary clearing of pte
                         * by holding ptl and checking again. A R/M/W update
                         * of pte involves: take ptl, clearing the pte so that
                         * we don't have concurrent modification by hardware
                         * followed by an update.
                         */
                        if (unlikely(pte_none(*vmf->pte)))
                                ret = VM_FAULT_SIGBUS;
                        else
                                ret = VM_FAULT_NOPAGE;

                        pte_unmap_unlock(vmf->pte, vmf->ptl);
                }
        } else if (!(vmf->flags & FAULT_FLAG_WRITE))
                ret = do_read_fault(vmf);
        else if (!(vma->vm_flags & VM_SHARED))
                ret = do_cow_fault(vmf);
        else
                ret = do_shared_fault(vmf);

        /* preallocated pagetable is unused: free it */
        if (vmf->prealloc_pte) {
                pte_free(vm_mm, vmf->prealloc_pte);
                vmf->prealloc_pte = NULL;
        }
        return ret;
}

file 매핑 타입으로 사용되는 vma 영역에서 fault된 경우 다음 3가지 유형으로 처리한다.

  • write 권한 요청이 없는 경우 페이지 캐시에 있는 페이지를 읽어와 매핑한다. 만일 페이지 캐시에 없으면 file로 부터 읽어 다시 페이지 캐시에 저장한다.
  • read only 비 공유 파일에 대해 write 권한 요청이 있는 경우 file로 부터 읽어 페이지 캐시에 저장한 후 새 페이지에 복사하고 그 페이지에 write 권한을 부여하여 anon 매핑한다.
  • read only 공유 파일에 대해 write 권한 요청이 있는 경우 write 권한을 설정한다.

 

  • 코드 라인 10~35에서 매핑된 디바이스나 파일 시스템에 vm_ops->fault 후크 함수가 지원되지 않는 경우의 처리이다.
    • pte 엔트리의 unmap을 처리한다.
      • 32비트 시스템에서는 pte 엔트리를 highmem에 매핑하는 옵션을 사용할 수 있다. 이러한 경우에 한해 pte 테이블을 사용하지 않을 때 unmap 처리를 해야 한다.
      • 64비트 시스템에서는 pte 엔트리가 항상 매핑 상태를 유지하는 normal 메모리를 사용하므로 별도로 언매핑 처리하지 않는다.
      • CONFIG_HIGHPTE 커널 옵션을 사용하면 pte 엔트리를 highmem에 할당하여 사용할 수 있다.
    • 다음과 같이 fault 처리한다.
      • pmd 엔트리가 준비되어 있지 않으면 VM_FAULT_SIGBUS fault를 반환한다.
      • fault 주소에 대응하는 pte 엔트리 값이 없으면 VM_FAULT_SIGBUS, 있으면 VM_FAULT_NOPAGE를 반환한다.
  • 코드 라인 36~37에서 write 요청이 없는 경우 매핑된 file로부터 페이지를 읽기 위해 do_read_fault() 함수를 호출한다.
  • 코드 라인 38~39에서 vma 영역이 공유 설정된 경우 매핑된 file로부터 페이지를 읽은 후에 새 페이지에 복사하고 write 설정하기 위해 do_cow_fault() 함수를 호출한다.
  • 코드 라인 40~41에서 그 외의 경우 매핑된 file로부터 페이지를 읽은 후 write 설정하기 위해 do_shared_fault() 함수를 호출한다.
  • 코드 라인 44~48에서 사전에 준비한 사용되지 않는 pte 페이지 테이블을 할당해제 한다.

 

file 매핑
  • 디바이스 또는 파일을 vma 공간에 매핑하여 사용한다.
  • 페이지 캐시를 검색하여 없으면 새 페이지를 file로부터 읽은 후 페이지 캐시로 등록한다. 페이지 캐시가 검색되는 경우 이 페이지를 가상 공간에 매핑하여 사용한다.
  • DAX를 지원하는 파일시스템의 경우 페이지 캐시를 사용하지 않고, 파일 시스템에 연결된 persistent 메모리 등을 직접 가상 공간에 매핑하여 사용한다.

 

다음 그림은 file 또는 디바이스(pmem 등)를 가상 주소에 mmap() 매핑한 후 해당 영역에서 fault가 발생한 함수 처리 경로를 보여준다.

 

 

—커널 5.4 코드로 수정 중—

 

 

do_read_fault()

mm/memory.c

static int do_read_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pmd_t *pmd,
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
        struct page *fault_page;
        spinlock_t *ptl;
        pte_t *pte;
        int ret = 0;

        /*
         * Let's call ->map_pages() first and use ->fault() as fallback
         * if page by the offset is not ready to be mapped (cold cache or
         * something).
         */
        if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
                pte = pte_offset_map_lock(mm, pmd, address, &ptl);
                do_fault_around(vma, address, pte, pgoff, flags);
                if (!pte_same(*pte, orig_pte))
                        goto unlock_out;
                pte_unmap_unlock(pte, ptl);
        }

        ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                return ret;

        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*pte, orig_pte))) {
                pte_unmap_unlock(pte, ptl);
                unlock_page(fault_page);
                page_cache_release(fault_page);
                return ret;
        }
        do_set_pte(vma, address, fault_page, pte, false, false);
        unlock_page(fault_page);
unlock_out:
        pte_unmap_unlock(pte, ptl);
        return ret;
}

파일 시스템의 vm_ops의 map_pages 핸들러를 통해 페이지 캐시로부터 exception된 가상 주소의 주변(readahead 기능) 페이지들을 읽어오는데 만일 그러한 핸들러가 필요 없는 블럭 디바이스(FS_DAX)이거나 페이지 캐시로 부터 읽어오지 못한 경우 fallback되어 vm_ops의 fault 핸들러를 통해 파일에서 해당 페이지에 대해서 read I/O 요청을 한다.

  • 코드 라인 15~21에서 vma에 map_pages 핸들러가 있고 fault_around_bytes가 2 페이지 이상인 경우 pte 테이블 페이지에 대한 ptl lock을 걸고 fault 주소 주변의 몇 개 페이지들을 불러 온다. pte 값이 변경된 경우 unlock_out 레이블로 이동하여 ptl 락을 풀고 함수를 빠져나간다.
    • 이렇게 미리 주변 페이지를 불러오는 경우 fault 처리를 줄이고자 하는데 목적이 있다.
    • ext2나 ext4 파일 시스템에서 generic 함수인 filemap_map_pages() 함수가 사용된다.
    • DAX(Direct Access eXicting) 기능에서는 앞으로 더 읽을 것이라고 예측하여 주변 페이지를 미리 더 읽어오게 하는 readahead 기능을 사용하지 않으므로 map_pages에 해당하는 핸들러는 제공하지 않는다. 따라서 map_pages 핸들러에서 항상 fallback된다.
    • fault_around_bytes
      • default  값으로 65536이며 CONFIG_DEBUG_FS 커널 옵션이 사용될 때 “/sys/kernel/debug/fault_around_bytes” 파일을 통해 설정 값을 바꿀 수 있다. (2의 차수 단위가 적용되며 최소 페이지 사이즈이다)
      • 참고: [PATCH] mm: make fault_around_bytes configurable | LKML.org
  • 코드 라인 23~25에서 페이지 캐시를 통해 페이지들을 가져올 수 없어 fallback된 경우 해당 페이지 들을 파일에서 읽기 위해 __do_fault() 함수를 호출한다. 만일 작은 확률로 errror, nopage 또는 retry 결과를 얻게되면 바로 그 결과를 반환한다.
  • 코드 라인 27~33에서 다시 pte 테이블 페이지에 대한 ptl lock을 건다. 만일 pte 값이 orig_pte 값과 다른 경우 fault 페이지를 할당 해제하고 함수를 빠져나간다.
  • 코드 라인 34~35 fault 페이지에 대한 pte 매핑을 하고 fault 페이지에 대한 락(PG_locked) 플래그를 클리어한다.
  • 코드 라인 37~38에서 pte 테이블 페이지의 ptl 락을 풀고 함수를 빠져나간다.

 

do_fault_around()

mm/memory.c

/*
 * do_fault_around() tries to map few pages around the fault address. The hope
 * is that the pages will be needed soon and this will lower the number of
 * faults to handle.
 *
 * It uses vm_ops->map_pages() to map the pages, which skips the page if it's
 * not ready to be mapped: not up-to-date, locked, etc.
 *
 * This function is called with the page table lock taken. In the split ptlock
 * case the page table lock only protects only those entries which belong to
 * the page table corresponding to the fault address.
 *
 * This function doesn't cross the VMA boundaries, in order to call map_pages()
 * only once.
 *
 * fault_around_pages() defines how many pages we'll try to map.
 * do_fault_around() expects it to return a power of two less than or equal to
 * PTRS_PER_PTE.
 *
 * The virtual address of the area that we map is naturally aligned to the
 * fault_around_pages() value (and therefore to page order).  This way it's
 * easier to guarantee that we don't cross page table boundaries.
 */
static void do_fault_around(struct vm_area_struct *vma, unsigned long address,
                pte_t *pte, pgoff_t pgoff, unsigned int flags)
{
        unsigned long start_addr, nr_pages, mask;
        pgoff_t max_pgoff;
        struct vm_fault vmf;
        int off;

        nr_pages = ACCESS_ONCE(fault_around_bytes) >> PAGE_SHIFT;
        mask = ~(nr_pages * PAGE_SIZE - 1) & PAGE_MASK;

        start_addr = max(address & mask, vma->vm_start);
        off = ((address - start_addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
        pte -= off;
        pgoff -= off;

        /*
         *  max_pgoff is either end of page table or end of vma
         *  or fault_around_pages() from pgoff, depending what is nearest.
         */
        max_pgoff = pgoff - ((start_addr >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) +
                PTRS_PER_PTE - 1;
        max_pgoff = min3(max_pgoff, vma_pages(vma) + vma->vm_pgoff - 1,
                        pgoff + nr_pages - 1);

        /* Check if it makes any sense to call ->map_pages */
        while (!pte_none(*pte)) {
                if (++pgoff > max_pgoff)
                        return;
                start_addr += PAGE_SIZE;
                if (start_addr >= vma->vm_end)
                        return;
                pte++;
        }

        vmf.virtual_address = (void __user *) start_addr;
        vmf.pte = pte;
        vmf.pgoff = pgoff;
        vmf.max_pgoff = max_pgoff;
        vmf.flags = flags;
        vma->vm_ops->map_pages(vma, &vmf);
}

파일에 대해 radix tree로 관리되는 페이지 캐시로부터 요청 가상 주소를 기준으로 fault_around_bytes(64K) 단위로 정렬된 범위 만큼의 페이지를 가져와 매핑한다.

  • 코드 라인 32~33에서 fault_around_bytes(default: 64K) 를 페이지 단위로 바꾸고, 이에 대한 mask 값을 산출한다.
    • 예 1) fault_around_bytes=64K, PAGE_SIZE=4K
      • -> nr_pages=16, mask=0xffff_0000
  • 코드 라인 35에서 요청 가상 주소를 mask하여 절삭 한 값과 vma 영역의 시작 주소 중 가장 큰 주소를 start_addr에 대입한다.
  • 코드 라인 36~38에서 start_addr를 기준으로 요청 했던 주소와 차이나는 페이지 수를 산출한 후 off에 대입하고, pte와 pgoff에서 그 차이를 각각 뺀다.
  • 코드 라인 44~47에서 start_addr를 기준으로 pte 테이블의 마지막 엔트리에 해당하는 가상 주소에 대한 pgoff 값을 max_pgoff에 대입한다. 그 후 max_pgoff, vma 페이지의 마지막 pgoff 및 fault_around_bytes 단위로 읽고자 하는 마지막 페이지에 대한 pgoff 중 가장 작은 수를 max_pgoff로 대입한다.
    • 예) 아래 그림 중 처음 fault된 경우: address=0x1000_5000, pgoff=5
      • -> nr_pages=4, mask=0xffff_c000, off=1, 최종 pgoff=4, max_pgoff=7=min(255, 12, 7)
  • 코드 라인 50~57에서 이미 매핑된 페이지들은 skip한다.  pgoff 부터 max_pgoff를 벗어나거나 페이지가 vma의 영역의 끝 주소를 넘어가는 경우 이미 다 매핑이 된 경우로 함수를 빠져나간다.
  • 코드 라인 59~64에서  매핑되지 않은 pte 엔트리에 대한 가상 주소 start_addr와 이 주소에 대한 pgoff와 max_pgoff 등을 가지고 vm_fault 구조체를 채운 후 마운팅된 파일 시스템의 map_pages 핸들러 함수를 호출한다.
    • ext2 및 ext4 파일 시스템에서는 generic 코드인 mapfiles_map_pages() 함수를 호출한다.

 

아래 그림과 같이 예를 들어 vma 영역이 13개 페이지가 있을 때 매핑된 file에 대해 0x1000_5000 ~ 0x1000_9fff까지 5개 페이지를 읽으려 할 때 2 번의 fault가 발생하는 모습을 보여준다.

 

__do_fault()

mm/memory.c

/*
 * The mmap_sem must have been held on entry, and may have been
 * released depending on flags and vma->vm_ops->fault() return value.
 * See filemap_fault() and __lock_page_retry().
 */
static int __do_fault(struct vm_area_struct *vma, unsigned long address,
                        pgoff_t pgoff, unsigned int flags,
                        struct page *cow_page, struct page **page)
{
        struct vm_fault vmf;
        int ret;

        vmf.virtual_address = (void __user *)(address & PAGE_MASK);
        vmf.pgoff = pgoff;
        vmf.flags = flags;
        vmf.page = NULL;
        vmf.cow_page = cow_page;

        ret = vma->vm_ops->fault(vma, &vmf);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                return ret;
        if (!vmf.page)
                goto out;

        if (unlikely(PageHWPoison(vmf.page))) {
                if (ret & VM_FAULT_LOCKED)
                        unlock_page(vmf.page);
                page_cache_release(vmf.page);
                return VM_FAULT_HWPOISON;
        }

        if (unlikely(!(ret & VM_FAULT_LOCKED)))
                lock_page(vmf.page);
        else
                VM_BUG_ON_PAGE(!PageLocked(vmf.page), vmf.page);

 out:
        *page = vmf.page;
        return ret;
}

vm_fault 구조체 정보를 채워 vma의 fault 핸들러를 호출한다. 출력 인수 page에 파일로 부터 읽어들인 페이지 정보를 전달한다.

  • 코드 라인 13~19에서 인수로 받은 정보를 vm_struct 구조체에 대입하고 vma에 등록한 fault 핸들러 함수를 호출한다.
    • 예) ext2 및 ext4 파일 시스템을 사용하는 경우 등록하여 사용하는 핸들러는 다음 둘 중 하나를 사용한다.
      • generic mmap fault 핸들러: mm/filemap.c – filemap_fault()
      • CONFIG_FS_DAX 커널 옵션이 사용되는 경우 dax mmap fault 핸들러: fs/dax.c – dax_fault()
        • DAX(Direct Access eXiciting)
          • 블럭 디바이스에 RAM이 장착되어 사용되므로, 리눅스에서 별도로 버퍼를 만들어 핸들링 하지 않도록 한다.
          • 실제 마운트 옵션에서 -o dax 옵션을 주어 사용한다.
          • arm, mips, sparc 아키텍처에서는 사용하지 않는다.
          • 참고: DAX: Page cache bypass for filesystems on memory storage | LWN.net
  • 코드 라인 20~21에서 작은 확률로 error, nopage, retry 등의 결과를 얻게되면 그대로 반환한다.
  • 코드 라인 22~23에서 vfm.page에 읽어들인 정보를 할당받은 페이지를 가리키는데 지정되지 않은 경우 함수를 빠져나간다.
  • 코드 라인 25~30에서 작은 확률로 vmf.page가 hwpoison 된 경우 페이지 할당을 해제한 후 hwpoison 에러를 반환한다.
    • HWPOISON
      • 메모리 에러를  검출할 수 있는 하드웨어와 이를 지원하는 시스템에서 사용된다. 주로 ECC 메모리에 대한 에러 교정에 사용된다.
      • 참고: HWPOISON | LWN.net
  • 코드 라인 32~33에서 작은 확률로 locked 가 설정되지 않은 경우 다시 lock을 수행한다.

 

do_cow_fault()

mm/memory.c

static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pmd_t *pmd,
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
        struct page *fault_page, *new_page;
        struct mem_cgroup *memcg;
        spinlock_t *ptl;
        pte_t *pte;
        int ret;

        if (unlikely(anon_vma_prepare(vma)))
                return VM_FAULT_OOM;

        new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
        if (!new_page) 
                return VM_FAULT_OOM;

        if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) {
                page_cache_release(new_page);
                return VM_FAULT_OOM;
        }

        ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                goto uncharge_out;

        if (fault_page)
                copy_user_highpage(new_page, fault_page, address, vma);
        __SetPageUptodate(new_page);

        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*pte, orig_pte))) {
                pte_unmap_unlock(pte, ptl);
                if (fault_page) {
                        unlock_page(fault_page);
                        page_cache_release(fault_page);
                } else {
                        /*
                         * The fault handler has no page to lock, so it holds
                         * i_mmap_lock for read to protect against truncate.
                         */
                        i_mmap_unlock_read(vma->vm_file->f_mapping);
                }
                goto uncharge_out;
        }
        do_set_pte(vma, address, new_page, pte, true, true);
        mem_cgroup_commit_charge(new_page, memcg, false);
        lru_cache_add_active_or_unevictable(new_page, vma);
        pte_unmap_unlock(pte, ptl);
        if (fault_page) {
                unlock_page(fault_page);
                page_cache_release(fault_page);
        } else {
                /*
                 * The fault handler has no page to lock, so it holds
                 * i_mmap_lock for read to protect against truncate.
                 */
                i_mmap_unlock_read(vma->vm_file->f_mapping);
        }
        return ret;
uncharge_out:
        mem_cgroup_cancel_charge(new_page, memcg);
        page_cache_release(new_page);
        return ret;
}

파일 시스템의 vm_ops의 map_pages 핸들러를 통해 페이지 캐시로부터 exception된 가상 주소에 대한 페이지를 읽어와서 새 페이지에 복사하고 write 권한으로 anon 매핑한다.

  • 코드 라인 11~12에서 요청 vma에 anon_vma가 준비되지 않은 경우 할당하고 준비를 해온다. 만일 준비가 안된 경우 가상 메모리 부족(VM_FAULT_OOM) 에러를 반환한다.
  • 코드 라인 14~16에서 하나의 새 유저 페이지를 할당받는다. 만일 할당되지 않으면 가상 메모리 부족 에러를 반환한다.
    • highmem이 있는 경우 가능하면 highmem에서 movable 타입으로 한 개 페이지를 할당 받는다.
  • 코드 라인 18~21에서 가상 페이지 수가 memcg 설정된 commit 할당량을 벗어난 경우 가상 메모리 부족 에러를 반환한다.
    • 주로 memory control group을 사용하여 지정한 태스크의 메모리 사용량을 제어하기 위해 사용한다.
  • 코드 라인 23~25에서 파일로부터 읽어온다. 만일 결과가 error, nopage 또는 retry 에러인 경우 uncharge_out 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 27~29에서 파일로 부터 요청한 페이지 fault_page를 읽어온 경우 새 페이지에 복사하고 새 페이지에 대해 PG_uptodate 플래그를 설정한다.
  • 코드 라인 31~45에서 출력 인수로 받아온 pte와 orig_pte가 다른 경우 매핑 없이 uncharge_out 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 46에서 새 페이지를 write 속성으로 매핑하고 anon reverse map에 페이지를 추가한다.
  • 코드 라인 47에서 memcg에 한 페이지가 추가되었음을 commit 한다.
  • 코드 라인 48에서 새 페이지를 active 설정하고 lru 캐시에 추가한다.
    • lru_add_pvec에 추가한다.
  • 코드 라인 50~52에서 fault_page에 대해 참조 카운터를 감소시키고 0이되면 할당 해제도 한다.

 

do_shared_fault()

mm/memory.c

static int do_shared_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pmd_t *pmd,
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
        struct page *fault_page;
        struct address_space *mapping;
        spinlock_t *ptl;
        pte_t *pte;
        int dirtied = 0;
        int ret, tmp;

        ret = __do_fault(vma, address, pgoff, flags, NULL, &fault_page);
        if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
                return ret;

        /*
         * Check if the backing address space wants to know that the page is
         * about to become writable
         */
        if (vma->vm_ops->page_mkwrite) {
                unlock_page(fault_page);
                tmp = do_page_mkwrite(vma, fault_page, address);
                if (unlikely(!tmp ||
                                (tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
                        page_cache_release(fault_page);
                        return tmp;
                }
        }

        pte = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*pte, orig_pte))) {
                pte_unmap_unlock(pte, ptl);
                unlock_page(fault_page);
                page_cache_release(fault_page);
                return ret;
        }
        do_set_pte(vma, address, fault_page, pte, true, false);
        pte_unmap_unlock(pte, ptl);

        if (set_page_dirty(fault_page))
                dirtied = 1;
        /*
         * Take a local copy of the address_space - page.mapping may be zeroed
         * by truncate after unlock_page().   The address_space itself remains
         * pinned by vma->vm_file's reference.  We rely on unlock_page()'s
         * release semantics to prevent the compiler from undoing this copying.
         */
        mapping = fault_page->mapping;
        unlock_page(fault_page);
        if ((dirtied || vma->vm_ops->page_mkwrite) && mapping) {
                /*
                 * Some device drivers do not set page.mapping but still
                 * dirty their pages
                 */
                balance_dirty_pages_ratelimited(mapping);
        }

        if (!vma->vm_ops->page_mkwrite)
                file_update_time(vma->vm_file);

        return ret;
}

공유 파일 매핑 영역에서 write 요청으로 fault된 경우 해당 페이지의 매핑을 write로 변경한다. 만일 write로 변경을 할 수 없는 상황인 경우 포기한다.

  • 코드 라인 12~14에서 파일 캐시로 부터 fault된 주소의 페이지를 읽어온다. 만일 결과가 error, nopage 또는 retry 에러인 경우 포기하고 그 결과를 반환한다.
  • 코드 라인 20~28에서 vma의 오퍼레이션 핸들러 중 page_mkwrite 후크 함수가 설정된 경우 write 설정을 위해 do_page_mkwrite() 함수를 호출한다. 만일 실패하는 경우 fault_page 를 release 하고 함수를 빠져나간다.
  • 코드 라인 30~36에서 페이지 테이블 락을 획득해온다. 만일 락 획득 과정에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 포기하고 빠져나간다.
  • 코드 라인 37에서 pte 엔트리에 대한 매핑을 write로 변경한다.
  • 코드 라인 40~41에서 fault_page가 dirty 설정이 되어 있는지 여부를 알아온다.
    • dirty 설정이 된 경우 페이지 캐시의 내용이 파일에 기록이 되지 않았음을 알 수 있다.
  • 코드 라인 48~56에서 fault_page의 매핑과 vma의 page_mkwrite 핸들러가 설정되었거나 dirty 된 경우 balance_dirty_pages_ratelimited() 함수를 호출한다.
    • dirty 페이지가 제한치를 초과하는 경우 밸런싱을 맞추기 위해 블록디바이스가 백그라운드에서 writeback을 수행할 수 있게 요청한다.
  • 코드 라인 58~59에서 vma의 page_mkwrite 핸들러가 설정되지 않은 경우 이 루틴에서 파일의 시간을 업데이트 한다.
    • inode의 mtime(수정 시각)과 ctime(작성 시각)을 현재 시간으로 업데이트 한다.

 

anon 매핑된 주소에서의 fault 처리

anon 매핑 주소에서의 fault 처리

do_anonymous_page()

mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with mmap_sem still held, but pte unmapped and unlocked.
 */
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags)
{
        struct mem_cgroup *memcg;
        struct page *page;
        spinlock_t *ptl;
        pte_t entry;

        pte_unmap(page_table);

        /* Check if we need to add a guard page to the stack */
        if (check_stack_guard_page(vma, address) < 0)
                return VM_FAULT_SIGSEGV;

        /* Use the zero-page for reads */
        if (!(flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(mm)) {
                entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
                                                vma->vm_page_prot));
                page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
                if (!pte_none(*page_table))
                        goto unlock;
                goto setpte;
        }

        /* Allocate our own private page. */
        if (unlikely(anon_vma_prepare(vma)))
                goto oom;
        page = alloc_zeroed_user_highpage_movable(vma, address);
        if (!page)
                goto oom;
        /*
         * The memory barrier inside __SetPageUptodate makes sure that
         * preceeding stores to the page contents become visible before
         * the set_pte_at() write.
         */
        __SetPageUptodate(page);

        if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg))
                goto oom_free_page;

        entry = mk_pte(page, vma->vm_page_prot);
        if (vma->vm_flags & VM_WRITE)
                entry = pte_mkwrite(pte_mkdirty(entry));

        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (!pte_none(*page_table))
                goto release;

        inc_mm_counter_fast(mm, MM_ANONPAGES);
        page_add_new_anon_rmap(page, vma, address);
        mem_cgroup_commit_charge(page, memcg, false);
        lru_cache_add_active_or_unevictable(page, vma);
setpte:
        set_pte_at(mm, address, page_table, entry);

        /* No need to invalidate - it was non-present before */
        update_mmu_cache(vma, address, page_table);
unlock:
        pte_unmap_unlock(page_table, ptl);
        return 0;
release:
        mem_cgroup_cancel_charge(page, memcg);
        page_cache_release(page);
        goto unlock;
oom_free_page:
        page_cache_release(page);
oom:
        return VM_FAULT_OOM;
}

anon 매핑된 영역에서 exception된 가상 주소에 대한 새 페이지를 할당하고 anon 매핑한다.

  • 코드 라인 15에서 pte 테이블이 fixmap에 임시로 매핑되어 있는 경우 매핑을 해제한다.
  • 코드 라인 18~19에서 영역이 스택이고  확장 불가능한 경우 VM_FAULT_SIGSEGV 에러를 반환한다.
  • 코드 라인 22~29에서 write 요청이 아니고 zero 페이지를 사용할 수 있는 경우 별도로 페이지를 할당하지 않고 기존 zero 페이지를 매핑하도록 준비하고 setpte 레이블로 이동한다.
  • 코드 라인 32~33에서 요청 vma에 anon_vma가 준비되지 않은 경우 할당하고 준비를 해온다. 만일 준비가 안된 경우 가상 메모리 부족(VM_FAULT_OOM) 에러를 반환한다.
  • 코드 라인 34~36에서 하나의 새 유저 페이지를 할당받는다. 만일 할당되지 않으면 가상 메모리 부족 에러를 반환한다.
    • highmem이 있는 경우 가능하면 highmem에서 movable 타입으로 한 개 페이지를 할당 받는다.
  • 코드 라인 42에서 새 페이지에 대해 PG_uptodate 플래그를 설정한다.
  • 코드 라인 44~45에서 가상 페이지 수가 memcg 설정된 commit 할당량을 벗어난 경우 가상 메모리 부족 에러를 반환한다.
    • 주로 memory control group을 사용하여 지정한 태스크의 메모리 사용량을 제어하기 위해 사용한다.
  • 코드 라인 47에서 vma 영역에 설정된 페이지 테이블 속성을 사용하여 pte 엔트리의 매핑을 준비한다.
  • 코드 라인 48~49에서 vma 영역에 write 속성이 있는 경우 pte 엔트리의 dirty 플래그를 추가하고 read only 플래그를 제거한다.
  • 코드 라인 51~53에서 해당 가상주소에 대한 pte 테이블 페이지의 lock을 걸고 pte 엔트리를 알아온다. 만일 엔트리가 이미 설정되어 있는 경우 release 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 55에서 MM_ANONPAGES 카운터를 증가시킨다.
  • 코드 라인 56에서 anon rmap(reverse map)에 페이지를 추가한다.
  • 코드 라인 57에서 memcg에 한 페이지가 추가되었음을 commit 한다.
  • 코드 라인 58에서 새 페이지를 active 설정하고 lru 캐시에 추가한다.
    • lru_add_pvec에 추가한다.
  • 코드 라인 60에서 pte 엔트리에 매핑한다.
  • 코드 라인 63에서 tlb 캐시를 업데이트(flush) 한다.
    • armv6 이전 아키텍처는 tlb 캐시를 flush한다.

 

 

anon 매핑에서 swap 페이지의 fault 처리

do_swap_page()

mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with pte unmapped and unlocked.
 *
 * We return with the mmap_sem locked or unlocked in the same cases
 * as does filemap_fault().
 */
static int do_swap_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags, pte_t orig_pte)
{
        spinlock_t *ptl;
        struct page *page, *swapcache;
        struct mem_cgroup *memcg;
        swp_entry_t entry;
        pte_t pte;
        int locked;
        int exclusive = 0;
        int ret = 0;

        if (!pte_unmap_same(mm, pmd, page_table, orig_pte))
                goto out;

        entry = pte_to_swp_entry(orig_pte);
        if (unlikely(non_swap_entry(entry))) {
                if (is_migration_entry(entry)) {
                        migration_entry_wait(mm, pmd, address);
                } else if (is_hwpoison_entry(entry)) {
                        ret = VM_FAULT_HWPOISON;
                } else {
                        print_bad_pte(vma, address, orig_pte, NULL);
                        ret = VM_FAULT_SIGBUS;
                }
                goto out;
        }
        delayacct_set_flag(DELAYACCT_PF_SWAPIN);
        page = lookup_swap_cache(entry);
        if (!page) {
                page = swapin_readahead(entry,
                                        GFP_HIGHUSER_MOVABLE, vma, address);
                if (!page) {
                        /*
                         * Back out if somebody else faulted in this pte
                         * while we released the pte lock.
                         */
                        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
                        if (likely(pte_same(*page_table, orig_pte)))
                                ret = VM_FAULT_OOM;
                        delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
                        goto unlock;
                }

                /* Had to read the page from swap area: Major fault */
                ret = VM_FAULT_MAJOR;
                count_vm_event(PGMAJFAULT);
                mem_cgroup_count_vm_event(mm, PGMAJFAULT);
        } else if (PageHWPoison(page)) {
                /*
                 * hwpoisoned dirty swapcache pages are kept for killing
                 * owner processes (which may be unknown at hwpoison time)
                 */
                ret = VM_FAULT_HWPOISON;
                delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
                swapcache = page;
                goto out_release;
        }

        swapcache = page;
        locked = lock_page_or_retry(page, mm, flags);

        delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
        if (!locked) {
                ret |= VM_FAULT_RETRY;
                goto out_release;
        }

swap 되어 있는 페이지에 접근했을 때 fault 되어 진입한 경우 swap 캐시 또는 swap file로 부터 페이지를 읽어들인다.

  • 코드 라인 22~23에서 pte 엔트리 값과 orig_pte 엔트리 값이 다른 경우 out 레이블로 이동한 후 함수를 빠져나간다.
  • 코드 라인 24에서 orig_pte 엔트리에 대한 swap 엔트리를 구해온다.
  • 코드 라인 25~36에서 작은 확률로 swap 엔트리가 아닌 경우 다음을 처리한 후 out 레이블로 이동하고 함수를 빠져나간다.
    • 코드 라인 27~28에서 실제 swap 정보로 구성된 swap 엔트리가 아니라 migration 정보가 담긴 swap 엔트리인 경우 해당 페이지의 migration이 완료되어 page가 unlock될 때까지 기다린다.
      • 해당 페이지의 migration이 완료되어 PG_locked가 클리어될 때까지 대기한다.
    • 코드 라인 29~30에서 엔트리가 hwpoison 엔트리인 경우 에러 코드로 hwpoison을 담는다.
    • 코드 라인 31~34에서 pte 정보를 출력하고 에러 코드로 sigbus를 담는다.
  • 코드 라인 37에서 현재 태스크의 delay 플래그에 delay accounting을 위해 DELAYACCT_PF_SWAPIN 플래그를 설정한다.
  • 코드 라인 38에서 swap 엔트리로 swap 캐시 페이지를 알아온다.
  • 코드 라인 39~41에서 페이지를 알아올 수 없으면 swapin_readahead() 함수를 호출하여 swap file로 부터 요청 페이지를 포함한 일정량의 페이지를 읽어온다.
  • 코드 라인 42~52에서 그래도 페이지를 가져올 수 없으면 설정해둔 DELAYACCT_PF_SWAPIN 플래그를 제거하고 unlock 레이블을 통해 함수를 빠져나간다.
    • swap file을 블럭 디바이스에서 비동기로 읽어들여 바로 가져오지 못할 확률이 크다. 그래서 delay를 발생시킬 목적으로 그냥 함수를 빠져나간다.
  • 코드 라인 55~57에서 에러 코드로 major fault로 대입하고, PGMAJFAULT 카운터를 증가시키고 memcg 설정된 경우에서도 PGMAJFAULT 카운터를 증가시킨다.
    • fault된 페이지가 swap 캐시에서 검색되지 않은 경우 swap file에서 페이지를 읽어온 경우 pgmajfault 카운터가 증가된다.
  • 코드 라인 58~67에서 hwpoison 페이지인 경우 hwpoison 에러 코드를 설정하고 설정해둔 DELAYACCT_PF_SWAPIN 플래그도 제거하며 out_release 레이블을 통해 함수를 빠져나간다.
  • 코드 라인 69~76에서 가져온 페이지에 대해 lock을 시도하고 안되면 retry 에러를 추가하여 out_release 레이블을 통해 함수를 빠져나간다.

 

.       /*
         * Make sure try_to_free_swap or reuse_swap_page or swapoff did not
         * release the swapcache from under us.  The page pin, and pte_same
         * test below, are not enough to exclude that.  Even if it is still
         * swapcache, we need to check that the page's swap has not changed.
         */
        if (unlikely(!PageSwapCache(page) || page_private(page) != entry.val))
                goto out_page;

        page = ksm_might_need_to_copy(page, vma, address);
        if (unlikely(!page)) {
                ret = VM_FAULT_OOM;
                page = swapcache;
                goto out_page;
        }

        if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg)) {
                ret = VM_FAULT_OOM;
                goto out_page;
        }

        /*
         * Back out if somebody else already faulted in this pte.
         */
        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (unlikely(!pte_same(*page_table, orig_pte)))
                goto out_nomap;

        if (unlikely(!PageUptodate(page))) {
                ret = VM_FAULT_SIGBUS;
                goto out_nomap;
        }

        /*
         * The page isn't present yet, go ahead with the fault.
         *
         * Be careful about the sequence of operations here.
         * To get its accounting right, reuse_swap_page() must be called
         * while the page is counted on swap but not yet in mapcount i.e.
         * before page_add_anon_rmap() and swap_free(); try_to_free_swap()
         * must be called after the swap_free(), or it will never succeed.
         */

        inc_mm_counter_fast(mm, MM_ANONPAGES);
        dec_mm_counter_fast(mm, MM_SWAPENTS);
        pte = mk_pte(page, vma->vm_page_prot);
        if ((flags & FAULT_FLAG_WRITE) && reuse_swap_page(page)) {
                pte = maybe_mkwrite(pte_mkdirty(pte), vma);
                flags &= ~FAULT_FLAG_WRITE;
                ret |= VM_FAULT_WRITE;
                exclusive = 1;
        }
        flush_icache_page(vma, page);
        if (pte_swp_soft_dirty(orig_pte))
                pte = pte_mksoft_dirty(pte);
        set_pte_at(mm, address, page_table, pte);
        if (page == swapcache) {
                do_page_add_anon_rmap(page, vma, address, exclusive);
                mem_cgroup_commit_charge(page, memcg, true);
        } else { /* ksm created a completely new copy */
                page_add_new_anon_rmap(page, vma, address);
                mem_cgroup_commit_charge(page, memcg, false);
                lru_cache_add_active_or_unevictable(page, vma);
        }
  • 코드 라인 7~8에서 작은 확률로 swap 캐시 페이지가 아니거나 private 페이지의 private 값이 entry 값과 다른 경우 out_page 레이블로 이동하여 함수를 빠져나간다.
  • 코드 라인 10~15에서 ksm 페이지인 경우 새 페이지를 할당 받고 기존 ksm 페이지로 부터 복사하고, 그렇지 않으면 기존 페이지를 그대로 반환한다. 만일 새 페이지 할당이 실패하는 경우 oom 에러를 반환한다.
  • 코드 라인 17~20에서 가상 페이지 수가 memcg 설정된 commit 할당량을 벗어난 경우 가상 메모리 부족 에러를 반환한다.
    • 주로 memory control group을 사용하여 지정한 태스크의 메모리 사용량을 제어하기 위해 사용한다.
  • 코드 라인 25~27에서 해당 가상주소에 대한 pte 테이블 페이지의 lock을 걸고 pte 엔트리를 알아와서 orig_pte와 다른 경우 누군가 이미 매핑한 경우이므로 내버려 두고 함수를 빠져나간다. 다시 fault될지 모르지만…
  • 코드 라인 29~32에서 작은 확률로 uptodate 플래그가 설정되지 않은 경우 sigbus 에러를 반환한다.
  • 코드 라인 44~45에서 anon 페이지가 추가되었고 swap 페이지가 감소되었으므로 MM_ANONPAGES 카운터를 증가시키고 MM_SWAPENTS 카운터를 감소시킨다.
  • 코드 라인 46에서 해당 페이지에 대해 vma 영역에 설정된 권한 속성을 더해 pte 엔트리를 만든다.
  • 코드 라인 47~52에서 write 요청이 있으면서 swap 캐시를 재 사용할 수 있는 상황인 경우 COW 없이 그냥 swap 캐시에서 제거하고 그 페이지를 그대로 anon 페이지로 전환한다. 그런 후 pte 속성 에서 read only를 제거하고 dirty를 설정하여 준비하고 write 플래그는 제거한다. 반환 값에 VM_FAULT_WRITE를 추가하고 exclusive를 1로 설정한다.
  • 코드 라인 53에서 해당 페이지에 대해 instruction 캐시를 flush 한다.
    • 특정 아키텍처에서 사용되며, arm, arm64, x86 아키텍처 등에서는 아무런 동작도 하지 않는다.
  • 코드 라인 54~55에서 orig_pte 값에 soft-dirty 플래그가 설정된 경우 pte 엔트리에도 soft-dirty 설정을 한다.
    • CONFIG_MEM_SOFT_DIRTY
      • 메모리 변경 시 트래킹을 하기 위해 사용하며 현재는 x86 아키텍처에만 구현되어 있는 커널 옵션이다.
      • 참고: SOFT-DIRTY PTEs – vm/soft-dirty.txt
  • 코드 라인 56에서 page_table 엔트리를 pte 값으로 설정하여 매핑한다.
  • 코드 라인 57~59에서 swap 캐시페이지를 anon 페이지로 그대로 활용한 경우 그 페이지를 anon reverse map에 추가하고 memcg에 에 페이지가 추가되었음을 commit 한다.
  • 코드 라인 60~64에서 swap 캐시 페이지를 새 페이지에 복사하여 사용하게 된 경우 anon reverse map에 새 페이지를 추가하고 memcg에 에 한 페이지가 추가되었음을 commit 하며 lru 캐시에 추가한다.
    • 페이지는 PG_SwapBacked 플래그 설정하고 _mapcount=0(-1부터 시작)으로 초기화한다.

 

.       swap_free(entry);
        if (vm_swap_full() || (vma->vm_flags & VM_LOCKED) || PageMlocked(page))
                try_to_free_swap(page);
        unlock_page(page);
        if (page != swapcache) {
                /*
                 * Hold the lock to avoid the swap entry to be reused
                 * until we take the PT lock for the pte_same() check
                 * (to avoid false positives from pte_same). For
                 * further safety release the lock after the swap_free
                 * so that the swap count won't change under a
                 * parallel locked swapcache.
                 */
                unlock_page(swapcache);
                page_cache_release(swapcache);
        }

        if (flags & FAULT_FLAG_WRITE) {
                ret |= do_wp_page(mm, vma, address, page_table, pmd, ptl, pte);
                if (ret & VM_FAULT_ERROR)
                        ret &= VM_FAULT_ERROR;
                goto out;
        }

        /* No need to invalidate - it was non-present before */
        update_mmu_cache(vma, address, page_table);
unlock:
        pte_unmap_unlock(page_table, ptl);
out:
        return ret;
out_nomap:
        mem_cgroup_cancel_charge(page, memcg);
        pte_unmap_unlock(page_table, ptl);
out_page:
        unlock_page(page);
out_release:
        page_cache_release(page);
        if (page != swapcache) {
                unlock_page(swapcache);
                page_cache_release(swapcache);
        }
        return ret;
}
  • 코드 라인 1에서 엔트리를 swap 캐시에서 제거한다.
  • 코드 라인 2~3에서 swap 캐시가 50% 이상 가득 차거나 locked vma 영역이거나 mlock 설정된 페이지인 경우 좀 더 swap 캐시를 비우려고 시도한다.
  • 코드 라인 4에서 페이지를 unlock한다.
  • 코드 라인 5~16에서 swap 캐시페이지를 새 페이지에 COW한 경우 swapcache 페이지도 unlock하고 release 한다.
  • 코드 라인 18~23에서 write 요청이 있는 경우 새 페이지를 할당받고 COW 하기 위해 do_wp_page() 함수를 호출하고 빠져나간다.
  • 코드 라인 26에서 tlb 캐시를 업데이트(flush) 한다.
    • armv6 이전 아키텍처는 tlb 캐시를 flush한다.
  • 코드 라인 28~30에서 page_table 페이지에 대한 ptl 언락을 하고 함수를 마친다.

 

 

 

swapin_readahead()

mm/swap_state.c

/**
 * swapin_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vma: user vma this address belongs to
 * @addr: target address for mempolicy
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * Primitive swap readahead code. We simply read an aligned block of
 * (1 << page_cluster) entries in the swap area. This method is chosen
 * because it doesn't cost us any seek time.  We also make sure to queue
 * the 'original' request together with the readahead ones...
 *
 * This has been extended to use the NUMA policies from the mm triggering
 * the readahead.
 *
 * Caller must hold down_read on the vma->vm_mm if vma is not NULL.
 */
struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
                        struct vm_area_struct *vma, unsigned long addr)
{
        struct page *page;
        unsigned long entry_offset = swp_offset(entry);
        unsigned long offset = entry_offset;
        unsigned long start_offset, end_offset;
        unsigned long mask;
        struct blk_plug plug;

        mask = swapin_nr_pages(offset) - 1;
        if (!mask)
                goto skip;

        /* Read a page_cluster sized and aligned cluster around offset. */
        start_offset = offset & ~mask;
        end_offset = offset | mask;
        if (!start_offset)      /* First page is swap header. */
                start_offset++;

        blk_start_plug(&plug);
        for (offset = start_offset; offset <= end_offset ; offset++) {
                /* Ok, do the async read-ahead now */
                page = read_swap_cache_async(swp_entry(swp_type(entry), offset),
                                                gfp_mask, vma, addr);
                if (!page)
                        continue;
                if (offset != entry_offset)
                        SetPageReadahead(page);
                page_cache_release(page);
        }
        blk_finish_plug(&plug);

        lru_add_drain();        /* Push any new pages onto the LRU now */
skip:
        return read_swap_cache_async(entry, gfp_mask, vma, addr);
}

swap 파일로 부터 swap 캐시로 요청 offset 페이지에 대한 readahead(한 번에 미리 읽어올 적정 페이지 수)페이지들을 읽어오도록 비동기 요청한다. 단 해당 offset 페이지는 읽혀올 때까지 블럭된다.

  • 코드 라인24에서 swap 엔트리 값에서 offset을 추출한다.
  • 코드 라인 30~32에서 swap 파일에서 swap 캐시로 읽어올 readahead(한 번에 미리 읽어올 적정 페이지 수) 페이지 수에서 -1을 하여 mask 값을 구한다. 만일 mask 값이 0인 경우 skip 레이블로 이동한다.
  • 코드 라인 35~36에서 offset을 mask를 사용하여 시작 offset과 끝 offset을 구한다.
    • 예) mask=7, offset=30
      • start_offset=24, end_offset=31
  • 코드 라인 37~38에서 start_offset이 0인 경우 1부터 시작하게 한다.
    • 특별히 0에는 swap 헤더가 존재한다.
  • 코드 라인 40에서 태스크에서 blk_plug를 초기화 하고 pending I/O에 의한 데드락 트래킹하도록 구성한다.
  • 코드 라인 41~50에서 swap 파일에서 해당 offset에 대한 페이지를 읽어오도록 비동기 요청을 한다. 이미 요청 offset이 아닌 다른 offset 페이지에는 PG_readahead 플래그를 설정한다.
  • 코드 라인 51에서 태스크에 설정해 둔 blk_plug를 flush한다.
  • 코드 라인 53~55에서 per cpu lru들을 lruvec으로 이동시키고 다시 swap 파일에서 해당 offset에 대한 페이지를 읽어오도록 비동기 요청을 한다.
    • 두 번째 호출 시 내부에서는 swap file로 부터 해당 페이지를 불러오는데 완료될 때까지 블럭된다.

 

swap 캐시용 raadahead 페이지 산출

swapin_nr_pages()

mm/swap_state.c

static unsigned long swapin_nr_pages(unsigned long offset)
{
        static unsigned long prev_offset;
        unsigned int pages, max_pages, last_ra;
        static atomic_t last_readahead_pages;

        max_pages = 1 << ACCESS_ONCE(page_cluster);
        if (max_pages <= 1)
                return 1;

        /*
         * This heuristic has been found to work well on both sequential and
         * random loads, swapping to hard disk or to SSD: please don't ask
         * what the "+ 2" means, it just happens to work well, that's all.
         */
        pages = atomic_xchg(&swapin_readahead_hits, 0) + 2;
        if (pages == 2) {
                /*
                 * We can have no readahead hits to judge by: but must not get
                 * stuck here forever, so check for an adjacent offset instead
                 * (and don't even bother to check whether swap type is same).
                 */
                if (offset != prev_offset + 1 && offset != prev_offset - 1)
                        pages = 1;
                prev_offset = offset;
        } else {
                unsigned int roundup = 4;
                while (roundup < pages)
                        roundup <<= 1;
                pages = roundup;
        }

        if (pages > max_pages)
                pages = max_pages;

        /* Don't shrink readahead too fast */
        last_ra = atomic_read(&last_readahead_pages) / 2;
        if (pages < last_ra)
                pages = last_ra;
        atomic_set(&last_readahead_pages, pages);

        return pages;
}

swap 파일에서 swap 캐시로 읽어올 readahead(한 번에 미리 읽어올 적정 페이지 수) 페이지 수를 산출한다.

  • readahead 페이지 수는 swap 캐시 hit 를 기반으로 huristic 하게 관리된다.
  • readahead 수는 유지, 증가, 감소가 가능하되 증가 시엔 항상 2배씩 증가하고, 감소 시에는 절반씩 감소한다.
    • 범위는 최소 1 ~ 최대 클러스터 당 페이지 수이며 2의 차수 단위이다.
      • 예) 4 -> 8(증가) -> 8(유지) -> 16(증가) -> 32(증가) -> 16(감소) -> 16(유지) -> 8(감소) -> 4(감소) -> 2(감소) -> 1(감소)

 

  • 코드 라인 7~9에서 2^page_cluster 값을 max_pages에 대입한다.
    • 클러스터 단위가 최대 처리할 수 있는 페이지이다.
  • 코드 라인 16에서 swapin_readahead_hits  +2를 하여 pages에 대입하고 swapin_readahead_hits 값은 0으로 초기화한다.
    • swapin_readahead_hits
      • default 값은 4부터 출발하고 통계 기반으로 적중률이 높아지면 증가된다.
        • swap 캐시가 hit되면 1씩 증가한다.
        • swap 캐시가 miss되어 이 함수가 호출되면 0으로 리셋된다.
  • 코드 라인 17~25에서 기존 swapin_readahead_hits 값이 0인 경우 즉, 이전에도 swap 캐시에서 페이지 lookup이 실패한 경우 이전 offset 요청이 현재 offset 요청과 +1/-1 차이로 연속된 번호가 아닌 경우 pages에 1을 대입한다. 그리고 현재 offset을 기억해둔다.
  • 코드 라인 26~31에서 초기화 전 swapin_readahead_hits 값이 0이 아닌 경우 즉, 이전에 swap 캐시에서 페이지 lookup이 실패하지 않았던 경우 pages 값을 최소 4부터 시작하여 2의 차수 단위로 올림 정렬한다.
  • 코드 라인 33~34에서 결정된 pages 값이 max_pages를 초과하는 경우 max_pages로 변경한다.
  • 코드라인 37~42에서 페이지가 마지막에 읽었던 readhead 페이지의 절반보다 작으면 그 절반 값을 last_readahead_pages에 대입하고 반환한다.
    •  last_readahead_pages
      • 가장 마지막에 사용한 swap 사용 시 readahead 페이지 수를 보관한다.

 

radix tree로 구성된 swap 캐시 검색

lookup_swap_cache()

mm/swap_state.c

/*
 * Lookup a swap entry in the swap cache. A found page will be returned
 * unlocked and with its refcount incremented - we rely on the kernel
 * lock getting page table operations atomic even if we drop the page
 * lock before returning.
 */
struct page * lookup_swap_cache(swp_entry_t entry)
{
        struct page *page;

        page = find_get_page(swap_address_space(entry), entry.val);

        if (page) {
                INC_CACHE_INFO(find_success);
                if (TestClearPageReadahead(page))
                        atomic_inc(&swapin_readahead_hits);
        }

        INC_CACHE_INFO(find_total);
        return page;
}

swap 엔트리로 radix tree로 구성된 swap 캐시에서 페이지를 구해 반환한다.

  • 코드 라인 11에서 swap 엔트리로 radix tree로 구성된 swap 캐시에서 페이지를 구해 반환한다.
    • #define swap_address_space(entry) (&swapper_spaces[swp_type(entry)])
  • 코드 라인 13~17에서 만일 페이지가 발견되면 swap_cache_info.find_success 카운터를 증가시킨다. 그리고 readahead 플래그가 설정되어 있으면 클리어하고 swapin_readahead_hits 값을 증가시킨다.
    • 이 값은 swap 캐시에서의 lookup이 계속 성공할 때마다 증가되고 실패하는 순간 0으로 초기화된다.
  • 코드 라인 19에서 swap_cache_info.find_total 카운터를 증가시킨다.

 

anon 매핑에서 protnone 페이지의 fault 처리 (numa migration for automatic numa balancing)

do_numa_page()

mm/memory.c

static int do_numa_page(struct mm_struct *mm, struct vm_area_struct *vma,
                   unsigned long addr, pte_t pte, pte_t *ptep, pmd_t *pmd)
{
        struct page *page = NULL;
        spinlock_t *ptl;
        int page_nid = -1;
        int last_cpupid;
        int target_nid;
        bool migrated = false;
        bool was_writable = pte_write(pte);
        int flags = 0;

        /* A PROT_NONE fault should not end up here */
        BUG_ON(!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE)));

        /*
        * The "pte" at this point cannot be used safely without
        * validation through pte_unmap_same(). It's of NUMA type but
        * the pfn may be screwed if the read is non atomic.
        *
        * We can safely just do a "set_pte_at()", because the old
        * page table entry is not accessible, so there would be no
        * concurrent hardware modifications to the PTE.
        */
        ptl = pte_lockptr(mm, pmd);
        spin_lock(ptl);
        if (unlikely(!pte_same(*ptep, pte))) {
                pte_unmap_unlock(ptep, ptl);
                goto out;
        }

        /* Make it present again */
        pte = pte_modify(pte, vma->vm_page_prot);
        pte = pte_mkyoung(pte);
        if (was_writable)
                pte = pte_mkwrite(pte);
        set_pte_at(mm, addr, ptep, pte);
        update_mmu_cache(vma, addr, ptep);

        page = vm_normal_page(vma, addr, pte);
        if (!page) {
                pte_unmap_unlock(ptep, ptl);
                return 0;
        }

        /*
         * Avoid grouping on RO pages in general. RO pages shouldn't hurt as
         * much anyway since they can be in shared cache state. This misses
         * the case where a mapping is writable but the process never writes
         * to it but pte_write gets cleared during protection updates and
         * pte_dirty has unpredictable behaviour between PTE scan updates,
         * background writeback, dirty balancing and application behaviour.
         */
        if (!(vma->vm_flags & VM_WRITE))
                flags |= TNF_NO_GROUP;

        /*
         * Flag if the page is shared between multiple address spaces. This
         * is later used when determining whether to group tasks together
         */
        if (page_mapcount(page) > 1 && (vma->vm_flags & VM_SHARED))
                flags |= TNF_SHARED;

        last_cpupid = page_cpupid_last(page);
        page_nid = page_to_nid(page);
        target_nid = numa_migrate_prep(page, vma, addr, page_nid, &flags);
        pte_unmap_unlock(ptep, ptl);
        if (target_nid == -1) {
                put_page(page);
                goto out;
        }

        /* Migrate to the requested node */
        migrated = migrate_misplaced_page(page, vma, target_nid);
        if (migrated) {
                page_nid = target_nid;
                flags |= TNF_MIGRATED;
        } else
                flags |= TNF_MIGRATE_FAIL;

out:
        if (page_nid != -1)
                task_numa_fault(last_cpupid, page_nid, 1, flags);
        return 0;
}

참고: Automatic NUMA Balancing | redhat – 다운로드 pdf

 

write protect 페이지의 fault 처리

do_wp_page()

mm/memory.c

/*
 * This routine handles present pages, when users try to write
 * to a shared page. It is done by copying the page to a new address
 * and decrementing the shared-page counter for the old page.
 *
 * Note that this routine assumes that the protection checks have been
 * done by the caller (the low-level page fault routine in most cases).
 * Thus we can safely just mark it writable once we've done any necessary
 * COW.
 *
 * We also mark the page dirty at this point even though the page will
 * change only once the write actually happens. This avoids a few races,
 * and potentially makes it more efficient.
 *
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), with pte both mapped and locked.
 * We return with mmap_sem still held, but pte unmapped and unlocked.
 */
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                spinlock_t *ptl, pte_t orig_pte)
        __releases(ptl)
{
        struct page *old_page, *new_page = NULL;
        pte_t entry;
        int ret = 0;
        int page_mkwrite = 0;
        bool dirty_shared = false;
        unsigned long mmun_start = 0;   /* For mmu_notifiers */
        unsigned long mmun_end = 0;     /* For mmu_notifiers */
        struct mem_cgroup *memcg;

        old_page = vm_normal_page(vma, address, orig_pte);
        if (!old_page) {
                /*
                 * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
                 * VM_PFNMAP VMA.
                 *
                 * We should not cow pages in a shared writeable mapping.
                 * Just mark the pages writable as we can't do any dirty
                 * accounting on raw pfn maps.
                 */
                if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
                                     (VM_WRITE|VM_SHARED))
                        goto reuse;
                goto gotten;
        }

write 권한이 없어 fault된 페이지에 대해 해당 페이지 또는 COW(Copy On Write) anon 페이지에 write 권한 매핑을 한다.

  • 코드 라인 33~47에서 special 페이지인 경우 vma가 공유 write 매핑된 경우 재사용하기 위해 reuse: 레이블로 이동하고, 그렇지 않은 경우 COW를 위해 gotten: 레이블로 이동한다.
    • null을 반환하는 경우 VM_PFMMAP 또는 VM_MIXEDMAP에서 사용하는 special 페이지이다.

 

.       /*
         * Take out anonymous pages first, anonymous shared vmas are
         * not dirty accountable.
         */
        if (PageAnon(old_page) && !PageKsm(old_page)) {
                if (!trylock_page(old_page)) {
                        page_cache_get(old_page);
                        pte_unmap_unlock(page_table, ptl);
                        lock_page(old_page);
                        page_table = pte_offset_map_lock(mm, pmd, address,
                                                         &ptl);
                        if (!pte_same(*page_table, orig_pte)) {
                                unlock_page(old_page);
                                goto unlock;
                        }
                        page_cache_release(old_page);
                }
                if (reuse_swap_page(old_page)) {
                        /*
                         * The page is all ours.  Move it to our anon_vma so
                         * the rmap code will not search our parent or siblings.
                         * Protected against the rmap code by the page lock.
                         */
                        page_move_anon_rmap(old_page, vma, address);
                        unlock_page(old_page);
                        goto reuse;
                }
                unlock_page(old_page);
        } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
                                        (VM_WRITE|VM_SHARED))) {
                page_cache_get(old_page);
                /*
                 * Only catch write-faults on shared writable pages,
                 * read-only shared pages can get COWed by
                 * get_user_pages(.write=1, .force=1).
                 */
                if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
                        int tmp;

                        pte_unmap_unlock(page_table, ptl);
                        tmp = do_page_mkwrite(vma, old_page, address);
                        if (unlikely(!tmp || (tmp &
                                        (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
                                page_cache_release(old_page);
                                return tmp;
                        }
                        /*
                         * Since we dropped the lock we need to revalidate
                         * the PTE as someone else may have changed it.  If
                         * they did, we just return, as we can count on the
                         * MMU to tell us if they didn't also make it writable.
                         */
                        page_table = pte_offset_map_lock(mm, pmd, address,
                                                         &ptl);
                        if (!pte_same(*page_table, orig_pte)) {
                                unlock_page(old_page);
                                goto unlock;
                        }
                        page_mkwrite = 1;
                }

                dirty_shared = true;
  • 코드 라인 5에서 anon 페이지이면서 KSM(Kernel Same Merge) 페이지가 아닌 경우
  • 코드 라인 6~17에서 페이지의 PG_locked를 획득(설정)하고 페이지를 release 한다.  만일 락 획득 과정에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 포기하고 빠져나간다.
  • 코드 라인 18~27에서 swap 페이지를  재사용할 수 있는 경우 PG_dirty 설정을 하고 기존 reserve map을 재배치하고 PG_locked를 해제한다. 그리고 계속 reuse: 레이블을 진행한다.
    • swap 캐시가 참조되지 않았으면서 PG_writeback이 없는 경우 swap 캐시를 지우고 PG_dirty 설정을 한다.
  • 코드 라인 28에서 페이지의 PG_locked를 해제한다.
  • 코드 라인 29~31에서 낮은 확률로 vma가 공유 write 설정된 경우(file) old 페이지의 참조 카운터를 증가시킨다.
  • 코드 라인 37~46에서 vma의 ops 핸들러에 page_mkwrite가 설정된 경우 pte 엔트리가 있는 페이지 테이블 락을 해제하고 do_page_mkwrite()를 수행하여 write 설정을 한다.  이 과정에서 mkwrite 수행 시 에러가 있는 경우 페이지를 release하고 빠져나간다.
  • 코드 라인 53~58에서 다시 페이지 테이블 락을  얻는다. 이 과정에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 포기하고 빠져나간다.
  • 코드 라인 59에서 page_mkwrite를 설정하여 아래 reuse: 레이블의 코드에서 vma의 ops 핸들러에 page_mkwrite 핸들러가 없는 경우에만 동작시킬 코드를 구분하기 위한 플래그이다.
  • 코드 라인 62에서 아래 reuse: 레이블의 코드를 3군데에서 호출하여 사용하는데, 특별히 위의 조건에서만 진행할 코드를 수행할 수 있도록 이를 판단하기 위해 dirty_shared를 설정한다.

 

reuse:
                /*
                 * Clear the pages cpupid information as the existing
                 * information potentially belongs to a now completely
                 * unrelated process.
                 */
                if (old_page)
                        page_cpupid_xchg_last(old_page, (1 << LAST_CPUPID_SHIFT) - 1);

                flush_cache_page(vma, address, pte_pfn(orig_pte));
                entry = pte_mkyoung(orig_pte);
                entry = maybe_mkwrite(pte_mkdirty(entry), vma);
                if (ptep_set_access_flags(vma, address, page_table, entry,1))
                        update_mmu_cache(vma, address, page_table);
                pte_unmap_unlock(page_table, ptl);
                ret |= VM_FAULT_WRITE;

                if (dirty_shared) {
                        struct address_space *mapping;
                        int dirtied;

                        if (!page_mkwrite)
                                lock_page(old_page);

                        dirtied = set_page_dirty(old_page);
                        VM_BUG_ON_PAGE(PageAnon(old_page), old_page);
                        mapping = old_page->mapping;
                        unlock_page(old_page);
                        page_cache_release(old_page);

                        if ((dirtied || page_mkwrite) && mapping) {
                                /*
                                 * Some device drivers do not set page.mapping
                                 * but still dirty their pages
                                 */
                                balance_dirty_pages_ratelimited(mapping);
                        }

                        if (!page_mkwrite)
                                file_update_time(vma->vm_file);
                }

                return ret;
        }
  • 코드 라인 7~8에서 old 페이지의 플래그 중 cpupid 정보를 제거하기 위해 모든 해당 비트를 1로 설정한다.
  • 코드 라인 10에서 해당 페이지에 대한 캐시를 각 아키텍처의 고유한 방법으로 flush한다.
  • 코드 라인 11~14에서 young, dirty, write 비트를 설정하고 pte 엔트리를 업데이트한다. 또한 해당 페이지에 대한 캐시를 각 아키텍처의 고유한 방법으로 update 한다.
  • 코드 라인 15에서 페이지 테이블 락(pte 페이지 테이블에 대한 page->ptl)을 해제한다.
  • 코드 라인 16에서 결과 코드에 VM_FAULT_WRITE 플래그를 설정한다.
  • 코드 라인 18~23에서 dirty_shared가 설정된 경우 (vma가 공유 파일 write 설정)  vma의 page_mkwrite 핸들러가 별도로 지정되지 않은 경우 페이지의 락을 획득한다. (PG_locked)
  • 코드 라인 25~29에서 페이지에 PG_dirty 설정을하고 페이지 락을 다시 해제한 후 페이지를 release 한다.
  • 코드 라인 31~37에서 file 매핑된 페이지가 기존에 PG_dirty 설정이 있었거나 vma에 page_mkwrite 핸들러가 있으면 balance_dirty_pages_ratelimited() 함수를 호출한다.
    • dirty 페이지가 제한치를 초과하는 경우 밸런싱을 맞추기 위해 블록디바이스가 백그라운드에서 writeback을 수행할 수 있게 요청한다.
  • 코드 라인 39~40에서 vma에 page_mkwrite 핸들러 함수가 없는 경우 file_update_time() 함수를 호출하여 파일에 update 시간을 기록한다.

 

        /*
         * Ok, we need to copy. Oh, well..
         */
        page_cache_get(old_page);
gotten:
        pte_unmap_unlock(page_table, ptl);

        if (unlikely(anon_vma_prepare(vma)))
                goto oom;

        if (is_zero_pfn(pte_pfn(orig_pte))) {
                new_page = alloc_zeroed_user_highpage_movable(vma, address);
                if (!new_page)
                        goto oom;
        } else {
                new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
                if (!new_page)
                        goto oom;
                cow_user_page(new_page, old_page, address, vma);
        }
        __SetPageUptodate(new_page);

        if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg))
                goto oom_free_new;

        mmun_start  = address & PAGE_MASK;
        mmun_end    = mmun_start + PAGE_SIZE;
        mmu_notifier_invalidate_range_start(mm, mmun_start, mmun_end);
  • 코드 라인 4에서 기존 페이지를 복사하기 위해 페이지 참조 카운터를 증가시킨다.
  • 코드 라인 6에서 페이지 테이블 페이지의 ptl 락을 해제한다.
  • 코드 라인 8~9에서 낮은 확률로  메모리가 부족하여 anon_vma를 준비하지 못하면 oom 레이블로 이동한다.
  • 코드 라인 11~14에서 zero 페이지가 매핑되어 있었던 경우 0으로 초기화된 새로운 페이지를 준비한다. 메모리 부족으로 새 페이지가 준비되지 않으면 oom 레이블로 이동한다.
  • 코드 라인 15~20에서 zero 페이지가 아닌 경우 새로운 페이지를 준비하고 기존 페이지로부터 복사한다. 만일 메모리 부족으로 새 페이지가 준비되지 않으면 oom 레이블로 이동한다.
  • 코드 라인 21에서 새 페이지의 PG_uptodate 플래그를 설정한다.
  • 코드 라인 23~24에서 memcg에 새 페이지의 할당 한계가 초과되었는지 알아보고 초과한 경우 oom_free_new 레이블로 이동한다.
  • 코드 라인 26~28에서 새 가상 주소 페이지의 주소 범위를 인수로 mmu notifier 리스트에 등록한 invalidate_range_start 콜백 함수들을 호출한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 invalidate_range_start 콜백 함수들
      • virt/kvm/kvm_main.c – kvm_mmu_notifier_invalidate_range_start()
      • drivers/gpu/drm/radeon/radeon_mn.c – radeon_mn_invalidate_range_start()
      • drivers/infiniband/core/umem_odp.c – ib_umem_notifier_invalidate_range_start()
      • drivers/misc/mic/scif/scif_dma.c – scif_mmu_notifier_invalidate_range_start()
    • mmu notifier
      • CONFIG_MMU_NOTIFIER 커널 옵션을 사용하는 경우 CONFIG_SRCU 커널 옵션이 자동 선택되며 read-side 크리티컬 섹션에서 슬립이 가능한 SRCU(Sleepable RCU)를 사용할 수 있다.
      • mm이 변경되는 경우 notify 하는 메커니즘을 제공하는데 리스트  탐색 시 SRCU를 사용한다.

 

.       /*
         * Re-check the pte - we dropped the lock
         */
        page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
        if (likely(pte_same(*page_table, orig_pte))) {
                if (old_page) {
                        if (!PageAnon(old_page)) {
                                dec_mm_counter_fast(mm, MM_FILEPAGES);
                                inc_mm_counter_fast(mm, MM_ANONPAGES);
                        }
                } else
                        inc_mm_counter_fast(mm, MM_ANONPAGES);
                flush_cache_page(vma, address, pte_pfn(orig_pte));
                entry = mk_pte(new_page, vma->vm_page_prot);
                entry = maybe_mkwrite(pte_mkdirty(entry), vma);
                /*
                 * Clear the pte entry and flush it first, before updating the
                 * pte with the new entry. This will avoid a race condition
                 * seen in the presence of one thread doing SMC and another
                 * thread doing COW.
                 */
                ptep_clear_flush_notify(vma, address, page_table);
                page_add_new_anon_rmap(new_page, vma, address);
                mem_cgroup_commit_charge(new_page, memcg, false);
                lru_cache_add_active_or_unevictable(new_page, vma);
                /*
                 * We call the notify macro here because, when using secondary
                 * mmu page tables (such as kvm shadow page tables), we want the
                 * new page to be mapped directly into the secondary page table.
                 */
                set_pte_at_notify(mm, address, page_table, entry);
                update_mmu_cache(vma, address, page_table);
                if (old_page) {
                        /*
                         * Only after switching the pte to the new page may
                         * we remove the mapcount here. Otherwise another
                         * process may come and find the rmap count decremented
                         * before the pte is switched to the new page, and
                         * "reuse" the old page writing into it while our pte
                         * here still points into it and can be read by other
                         * threads.
                         *
                         * The critical issue is to order this
                         * page_remove_rmap with the ptp_clear_flush above.
                         * Those stores are ordered by (if nothing else,)
                         * the barrier present in the atomic_add_negative
                         * in page_remove_rmap.
                         *
                         * Then the TLB flush in ptep_clear_flush ensures that
                         * no process can access the old page before the
                         * decremented mapcount is visible. And the old page
                         * cannot be reused until after the decremented
                         * mapcount is visible. So transitively, TLBs to
                         * old page will be flushed before it can be reused.
                         */
                        page_remove_rmap(old_page);
                }

                /* Free the old page.. */
                new_page = old_page;
                ret |= VM_FAULT_WRITE;
        } else
                mem_cgroup_cancel_charge(new_page, memcg);
  • 코드 라인 4~5에서 페이지 테이블 페이지의 ptl 락을 획득한 후에도 pte 엔트리 값이 변경된 적이 없으면 해당 pte 엔트리에 대해 다른 cpu와의 경쟁이 없었거나 경쟁 상황에서 이긴 경우이다.
  • 코드 라인 6~12에서 기존 페이지가 anon 페이지가 아닌 경우 MM_FILEPAGES 카운터를 줄이고 MM_ANONPAGES 카운터를 증가시킨다. 기존 페이지가 없었던 경우는 MM_ANONPAGES만 증가시킨다.
  • 코드 라인 13에서 orig_pte에 해당하는 페이지 영역을 캐시 flush 한다.
  • 코드 라인 14~15에서 새 페이지에 해당하는 pte 엔트리 속성을 준비하고 L_PTE_DIRTY를 추가하고 vma에 write 설정된 경우 L_PTE_RDONLY를 제거한다.
  • 코드 라인 22에서 pte 엔트리를 언매핑(0) 한 후 아키텍처에 따라 해당 페이지에 대해 TLB 캐시를 flush한다. 그런 후 mmu notifier 리스트에 등록한 invalidate_range 콜백 함수들을 호출한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 invalidate_range 콜백 함수들
      • drivers/infiniband/hw/mlx5/odp.c – mlx5_ib_invalidate_range()
      • drivers/net/ethernet/adi/bfin_mac.c – blackfin_dcache_invalidate_range()
      • drivers/iommu/amd_iommu_v2.c – mn_invalidate_range()
  • 코드 라인 23~25에서 anon reverse map에 새 페이지를 추가하고, memcg에 새 페이지 추가를 commit 한 후 lru 캐시에 추가한다.
  • 코드 라인 31~32에서 mmu notifier 리스트에 등록한 change_pte 콜백 함수들을 호출한 후 pte 엔트리를 변경한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 change_pte 콜백 함수
      • virt/kvm/kvm_main.c – kvm_mmu_notifier_change_pte()
  • 코드 라인 33~57에서 기존 페이지가 있는 경우 anon reverse map에서 제거한다.
  • 코드 라인 62~63에서 pte 엔트리가 다른 cpu와의 경쟁 상황으로 인해 변경된 경우 memcg에 새 페이지를 commit 계량하지 않게 취소 요청한다.

 

        if (new_page)
                page_cache_release(new_page);
unlock:
        pte_unmap_unlock(page_table, ptl);
        if (mmun_end > mmun_start)
                mmu_notifier_invalidate_range_end(mm, mmun_start, mmun_end);
        if (old_page) {
                /*
                 * Don't let another task, with possibly unlocked vma,
                 * keep the mlocked page.
                 */
                if ((ret & VM_FAULT_WRITE) && (vma->vm_flags & VM_LOCKED)) {
                        lock_page(old_page);    /* LRU manipulation */
                        munlock_vma_page(old_page);
                        unlock_page(old_page);
                }
                page_cache_release(old_page);
        }
        return ret;
oom_free_new:
        page_cache_release(new_page);
oom:
        if (old_page)
                page_cache_release(old_page);
        return VM_FAULT_OOM;
}
  • 코드 라인 1~2에서 새 페이지가 있는 경우 페이지 참조를 release 한다.
  • 코드 라인 4에서 페이지 테이블 락을 해제한다.
    • pte 엔트리가 있는 페이지 테이블 페이지의 ptl 락을 해제한다.
  • 코드 라인 5~6에서  mmu notifier 리스트에 등록한 invalidate_range_end 콜백 함수들을 호출한다.
    • mm->mmu_notifier_mm 리스트에 등록한 mn ops 핸들러에 연결된 invalidate_range_end 콜백 함수들
      • virt/kvm/kvm_main.c – kvm_mmu_notifier_invalidate_range_end()
      • drivers/misc/sgi-gru/grutlbpurge.c – gru_invalidate_range_end()
      • drivers/infiniband/core/umem_odp.c – ib_umem_notifier_invalidate_range_end()
  • 코드 라인 7~18에서 old 페이지가 있는 경우 페이지의 참조 카운터를 감소시키고 0이되는 경우 할당 해제한다. 만일 mlocked 페이지에서 COW 하였었던 경우 기존 페이지에 lock을 걸고 PG_mlocked 을 제거하고 NR_MLOCK 카운터를 감소시킨다. 또한 lru 페이지인 경우 PG_lru를 제거하고, lru 리스트에서도 제거한다.  마지막으로 다시 old 페이지에서 lock을 해제한다.(PG_locked 해제)

 

 

빠른 RSS 카운팅을 위해 분리

SPLIT_RSS_COUNTING

include/linux/mm_types.h

#if USE_SPLIT_PTE_PTLOCKS && defined(CONFIG_MMU)
#define SPLIT_RSS_COUNTING
/* per-thread cached information, */
struct task_rss_stat {
        int events;     /* for synchronization threshold */
        int count[NR_MM_COUNTERS]; 
};
#endif /* USE_SPLIT_PTE_PTLOCKS */

USE_SPLIT_PTE_PTLOCKS와 MMU가 설정된 경우 빠른 RSS 카운팅을 위해 RSS 카운팅을 이원화 시킨다.

  • 2개의 rss_stat으로 나뉘어 운영한다.
    • mm->rss_stat.count[] (RSS 통계값 원본)
    • task->rss_stat.count[] (이원화할 때 빠른 카운터로 추가)
  • sync는 task->stat_envents를 증가시키다 TASK_RSS_EVENTS_THRESH(64) 값에 도달하면 sync를 위해 task의 카운터들을 mm에 옮긴다.

 

USE_SPLIT_PTE_PTLOCKS

include/linux/mm_types.h

#define USE_SPLIT_PTE_PTLOCKS   (NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS)

USE_SPLIT_PTE_PTLOCKS은 CONFIG_SPLIT_PTLOCK_CPUS이상의 NR_CPUS가 설정된 경우 동작한다.

  •  rpi2: NR_CPUS=4, CONFIG_SPLIT_PTLOCK_CPUS=4로 설정되어 USE_SPLIT_PTE_PTLOCKS가 운용된다.

 

check_sync_rss_stat()

mm/memory.c

static void check_sync_rss_stat(struct task_struct *task)
{
        if (unlikely(task != current))
                return;
        if (unlikely(task->rss_stat.events++ > TASK_RSS_EVENTS_THRESH))
                sync_mm_rss(task->mm);
}

요청 태스크가 현재 태스크인 경우에 한해 task의 sync 카운터인 rss_stat.events를 증가시킨다. 만일 sync 카운터가 스레졸드(64)를 초과하는 경우 다시 0으로 설정하고 이원화된 rss 카운터를 동기화하도록 요청 한다.

 

sync_mm_rss()

mm/memory.c

void sync_mm_rss(struct mm_struct *mm)
{               
        int i;

        for (i = 0; i < NR_MM_COUNTERS; i++) {
                if (current->rss_stat.count[i]) {
                        add_mm_counter(mm, i, current->rss_stat.count[i]);
                        current->rss_stat.count[i] = 0;
                }
        }
        current->rss_stat.events = 0;
}

현재 태스크의 rss_stat.count[]의 값들을 모두 원래 카운터 값인 메모리 디스크립터의 rss_stat.count[]에 옮기고 sync 카운터를 다시 0으로 한다.

 

참고

 

 

Swap 엔트리

<kernel v5.0>

Swap 엔트리

리눅스 커널의 Swap 엔트리 관리는 커널 v4.20-rc1에서 Radix Tree Exceptional 방식에서 XArray를 사용하는 방식으로 변경되었다.

 

swap_entry_t 구조

다음 그림은 32비트 시스템에서 운영하는 swap 엔트리 구조를 보여준다.

  • 이 값은 arm h/w pte 엔트리가 아니라 arm 리눅스 pte 엔트리에 저장되며 이 페이지가 swap 엔트리임을 의미한다.

 

다음 그림은 64비트 시스템에서 운영하는 swap 엔트리 구조를 보여준다.

 

swap_entry_t 구조체

include/linux/mm_types.h

 /*
  * A swap entry has to fit into a "unsigned long", as the entry is hidden
  * in the "index" field of the swapper address space.
  */
typedef struct {
        unsigned long val;
} swp_entry_t;

 

아키텍처 독립 Swap 엔트리

swp_entry()

include/linux/swapops.h

/*
 * Store a type+offset into a swp_entry_t in an arch-independent format
 */
static inline swp_entry_t swp_entry(unsigned long type, pgoff_t offset)
{
        swp_entry_t ret;

        ret.val = (type << SWP_TYPE_SHIFT(ret)) | (offset & SWP_OFFSET_MASK);
        return ret;
}

offset 값과 type 값으로 swap 엔트리를 구성한다.

 

swp_type()

include/linux/swapops.h

/*
 * Extract the `type' field from a swp_entry_t.  The swp_entry_t is in
 * arch-independent format
 */
static inline unsigned swp_type(swp_entry_t entry)
{
        return (entry.val >> SWP_TYPE_SHIFT);
}

swap 엔트리에서 type 값을 반환한다.

 

swp_offset()

include/linux/swapops.h

/*
 * Extract the `offset' field from a swp_entry_t.  The swp_entry_t is in
 * arch-independent format
 */
static inline pgoff_t swp_offset(swp_entry_t entry)
{
        return entry.val & SWP_OFFSET_MASK;
}

swap 엔트리에서 offset 값을 반환한다.

 

SWP_TYPE_SHIFT() & SWP_OFFSET_MASK()

include/linux/swapops.h

/*
 * swapcache pages are stored in the swapper_space radix tree.  We want to
 * get good packing density in that tree, so the index should be dense in
 * the low-order bits.
 *
 * We arrange the `type' and `offset' fields so that `type' is at the seven
 * high-order bits of the swp_entry_t and `offset' is right-aligned in the
 * remaining bits.  Although `type' itself needs only five bits, we allow for
 * shmem/tmpfs to shift it all up a further two bits: see swp_to_radix_entry().
 *
 * swp_entry_t's are *never* stored anywhere in their arch-dependent format.
 */
#define SWP_TYPE_SHIFT       (BITS_PER_XA_VALUE - MAX_SWAPFILES_SHIFT)
#define SWP_OFFSET_MASK      ((1UL << SWP_TYPE_SHIFT) - 1)

 

매크로 상수

include/linux/swap.h

/*
 * MAX_SWAPFILES defines the maximum number of swaptypes: things which can
 * be swapped to.  The swap type and the offset into that swap type are
 * encoded into pte's and into pgoff_t's in the swapcache.  Using five bits
 * for the type means that the maximum number of swapcache pages is 27 bits
 * on 32-bit-pgoff_t architectures.  And that assumes that the architecture packs
 * the type/offset into the pte as 5/27 as well.
 */
#define MAX_SWAPFILES_SHIFT     5

 

include/linux/swap.h

/*
 * NUMA node memory migration support
 */
#ifdef CONFIG_MIGRATION
#define SWP_MIGRATION_NUM 2
#define SWP_MIGRATION_READ      (MAX_SWAPFILES + SWP_HWPOISON_NUM)
#define SWP_MIGRATION_WRITE     (MAX_SWAPFILES + SWP_HWPOISON_NUM + 1)
#else
#define SWP_MIGRATION_NUM 0
#endif

 

ARM32 Swap 엔트리

__swp_entry()

arch/arm/include/asm/pgtable.h

#define __swp_entry(type,offset) ((swp_entry_t) { ((type) << __SWP_TYPE_SHIFT) | ((offset) << __SWP_OFFSET_SHIFT) })

offset 값과 type 값으로 arm swap 엔트리를 구성한다.

 

__swp_type()

arch/arm/include/asm/pgtable.h

#define __swp_type(x)           (((x).val >> __SWP_TYPE_SHIFT) & __SWP_TYPE_MASK)

arm swap 엔트리에서 type 값을 반환한다.

 

__swp_offset()

arch/arm/include/asm/pgtable.h

#define __swp_offset(x)         ((x).val >> __SWP_OFFSET_SHIFT)

arm swap 엔트리에서 offset 값을 반환한다.

 

매크로 상수

arch/arm/include/asm/pgtable.h

/*      
 * Encode and decode a swap entry.  Swap entries are stored in the Linux
 * page tables as follows:
 *
 *   3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
 *   1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
 *   <--------------- offset ------------------------> < type -> 0 0
 *
 * This gives us up to 31 swap files and 128GB per swap file.  Note that
 * the offset field is always non-zero.
 */
#define __SWP_TYPE_SHIFT        2
#define __SWP_TYPE_BITS         5
#define __SWP_TYPE_MASK         ((1 << __SWP_TYPE_BITS) - 1)
#define __SWP_OFFSET_SHIFT      (__SWP_TYPE_BITS + __SWP_TYPE_SHIFT)

 

ARM64 Swap 엔트리

__swp_entry()

arch/arm64/include/asm/pgtable.h

#define __swp_entry(type,offset) ((swp_entry_t) { ((type) << __SWP_TYPE_SHIFT) | ((offset) << __SWP_OFFSET_SHIFT) })

offset 값과 type 값으로 arm64 swap 엔트리를 구성한다.

 

__swp_type()

arch/arm64/include/asm/pgtable.h

#define __swp_type(x)           (((x).val >> __SWP_TYPE_SHIFT) & __SWP_TYPE_MASK)

arm64 swap 엔트리에서 type 값을 반환한다.

 

__swp_offset()

arch/arm64/include/asm/pgtable.h

#define __swp_offset(x)         (((x).val >> __SWP_OFFSET_SHIFT) & __SWP_OFFSET_MASK)

arm64 swap 엔트리에서 50bit offset 값을 반환한다.

 

매크로 상수

arch/arm64/include/asm/pgtable.h

/*
 * Encode and decode a swap entry:
 *      bits 0-1:       present (must be zero)
 *      bits 2-7:       swap type
 *      bits 8-57:      swap offset
 *      bit  58:        PTE_PROT_NONE (must be zero)
 */
#define __SWP_TYPE_SHIFT        2
#define __SWP_TYPE_BITS         6
#define __SWP_OFFSET_BITS       50
#define __SWP_TYPE_MASK         ((1 << __SWP_TYPE_BITS) - 1)
#define __SWP_OFFSET_SHIFT      (__SWP_TYPE_BITS + __SWP_TYPE_SHIFT)
#define __SWP_OFFSET_MASK       ((1UL << __SWP_OFFSET_BITS) - 1)


Swap PTE 엔트리 식별

is_swp_pte()

include/linux/swapops.h

/* check whether a pte points to a swap entry */
static inline int is_swap_pte(pte_t pte)
{
        return !pte_none(pte) && !pte_present(pte);
}

swap된 pte 엔트리인지 여부를 반환한다.

  • PTE가 NONE 설정이 아니면서 PRESENT 설정도 없는 경우가 swap 상태이다.

 

Swap 캐시

backing storage에 예약된 슬롯을 가진 공유 페이지는 swap 캐시로 간주된다. swap 캐시는 파일 캐시와 다음 2 가지가 다르다.

  • page->mapping 이 &swapper_space[]를 사용한다. (address_space)
  • add_to_page_cache() 대신 add_to_swap_cache() 함수를 사용한다.

 

Exception -4- (ARM32 VFP & FPE)

<kernel v5.4>

VFP(Vector Floating Point)

  • VFP는 반정도(half precision), 단정도(single precision) 및 배정도(double precision) 고정 소수점 연산을 지원하는 보조 연산 장치이다.
  • SIMD(Single Instruction Multiple Data)라고도 불린다.

 

버전별 특징

  • VPFv1
    • ARM10에서 사용
    • VFP 예외를 트래핑해야 한다. (VFP 지원 코드가 필요 하다)
    • gcc 컴파일 시: -mfpu=vfp
  • VFPv2
    • ARM11, ARMv5, ARMv6에서 사용
      • ARM10200E가 제공하는 VFP10 수정 버전 1
      • ARM926E/946E/966E에 대해 별도로 허가된 옵션으로 사용되는 VFP9-S
      • ARM1136JF-S, ARM1176JZF-S 및 ARM11 MPCore에서 제공되는 VFP11
    • VFP 예외를 트래핑해야 한다. (VFP 지원 코드가 필요 하다)
    • 16개의 64-bit FPU 레지스터
    • gcc 컴파일 시: -mfpu=vfp
  • VFPv3
    • ARMv7인 Cortex-A8부터 사용하고 NEON으로 불린다.
      • VFPv3-D16
        • 오직 16개의 64-bit FPU 레지스터
      • VFPv3-F16 variant
        • 일반적이지 않지만 단정도 고정 소수점 연산을 지원한다.
    • VFP에서 벡터 기능이 퇴색하고(deprecated) 파이프라인화되어 advanced SIMD로 진보되어 VFPv2의 2배 성능을 확보하였다.
      • 실제 배정도 고정 소수점 연산을 지원하는 scalar 명령셋과 지원하지 않는 vector 명령셋으로 구분된다.
        • 단 ARMv8에서는 vector 명령셋에서도 배정도 고정 소수점 연산을 지원한다.
        • 기존의 벡터 명령도 advanced SIMD와 같이 병렬 처리된다.
    • VFP 예외를 트래핑할 필요가 없다는 점을 제외하고 VFPv2와 동일하다. (VFP 지원 코드가 필요 없다)
    • 32개의 64-bit FPU 레지스터
    • 단정도 고정 소수점 연산(optional)
    • gcc 컴파일 시: -mfpu=neon
    • 참고: NEON | arm

 

GCC로 FP 코드 연동

  • -mfloat-abi=
    • soft
      • Floating Point 라이브러리를 호출하도록 생성하고 호출 방법으로 Floating Poing ABI 규격의 calling convention을 사용한다.
    • hard
      • -mfpu에서 지정한 VFP 하드웨어 코드를 생성하고 호출 방법 역시 각자의 방법을 사용한다.
    • softfp
      • -mfpu에서 지정한 VFP 하드웨어 코드를 생성하되 호출 방법은 soft 방식을 따른다.
  • -mfpu=
    • vfpv2’, ‘vfpv3’, ‘vfpv3-fp16’, ‘vfpv3-d16’, ‘vfpv3-d16-fp16’, ‘vfpv3xd’, ‘vfpv3xd-fp16’, ‘neon-vfpv3’, ‘neon-fp16’, ‘vfpv4’, ‘vfpv4-d16’, ‘fpv4-sp-d16’, ‘neon-vfpv4’, ‘fpv5-d16’, ‘fpv5-sp-d16’, ‘fp-armv8’, ‘neon-fp-armv8’ 및 ‘crypto-neon-fp-armv8’
    • neon
      • VFP 코드를 생성할 경우에 NEON(‘vfpv3’ alias)용으로 생성한다.
    • vfp
      • VFP 코드를 생성할 경우에 VFPv2(‘vfpv2’ alias)용으로 생성한다.
  • 참고: ABI(Application Binary Interface) | 문c

 

VFP 지원 코드

하드웨어 VFP 만으로는 처리할 수 없어서 아래와 같은 상황에서 VFP 지원을 받아 소프트웨어적으로 처리한다.

  • NaN 관련된 부동 소수점 연산
  • 비정규 값과 관련된 부동 소수점 연산
  • 부동 소수점 오버플로
  • 부동 소수점 언더플로
  • 정확하지 않은 결과
  • 0으로 나누기 오류
  • 잘못된 연산

 


FPE(Floating Point Emulation)

  • VFP가 없는 arm 아키텍처에서 커널이 제공하는 라이브러리를 통해 소프트 에뮬레이션 방법으로 동작한다.
  • arm에서는 VFP가 여러 가지 아키텍처에 따라 다르므로 특별히 고성능을 요구하지 않는 경우 호환 목적의 코드를 만들기 위해 FPE를 사용하기도 한다.

 

Undefined instruction으로부터 진입

call_fpe

arch/arm/kernel/entry-armv.S

call_fpe:
        get_thread_info r10                     @ get current thread
#ifdef CONFIG_NEON
        adr     r6, .LCneon_arm_opcodes
2:      ldr     r5, [r6], #4                    @ mask value
        ldr     r7, [r6], #4                    @ opcode bits matching in mask
        cmp     r5, #0                          @ end mask?
        beq     1f
        and     r8, r0, r5
        cmp     r8, r7                          @ NEON instruction?
        bne     2b
        mov     r7, #1
        strb    r7, [r10, #TI_USED_CP + 10]     @ mark CP#10 as used
        strb    r7, [r10, #TI_USED_CP + 11]     @ mark CP#11 as used
        b       do_vfp                          @ let VFP handler handle this
1:
#endif
        tst     r0, #0x08000000                 @ only CDP/CPRT/LDC/STC have bit 27
        tstne   r0, #0x04000000                 @ bit 26 set on both ARM and Thumb-2
        reteq   lr
        and     r8, r0, #0x00000f00             @ mask out CP number
 THUMB( lsr     r8, r8, #8              )
        mov     r7, #1
        add     r6, r10, #TI_USED_CP
 ARM(   strb    r7, [r6, r8, lsr #8]    )       @ set appropriate used_cp[]
 THUMB( strb    r7, [r6, r8]            )       @ set appropriate used_cp[]
#ifdef CONFIG_IWMMXT
        @ Test if we need to give access to iWMMXt coprocessors
        ldr     r5, [r10, #TI_FLAGS]
        rsbs    r7, r8, #(1 << 8)               @ CP 0 or 1 only
        movscs  r7, r5, lsr #(TIF_USING_IWMMXT + 1)
        bcs     iwmmxt_task_enable      
#endif
 ARM(   add     pc, pc, r8, lsr #6      )
 THUMB( lsl     r8, r8, #2              )
 THUMB( add     pc, r8                  )
        nop

instruction이 VFPv3(NEON) 또는 소프트 지원이 필요한 VFPv1/v2 아키텍처에서 동작되는 경우 각각 커널의 VFP  소프트 지원 또는  FPE(Floating Point Emulation) 라이브러리를 통해 해당 예외 처리를 수행한다.

  • 코드 라인 3~15에서 instruction이 VFP이고 지원되지 않는 아키텍처에서 undefined exception으로 진입한 경우이므로 이에 대응하는 VFP 소프트 지원 라이브러리를 통해 해당 명령을 수행한다.
    • 코드 라인 3~8에서 instruction이 VFP 명령어인지 비교하기 위해 LCneon_arm_opcodes 테이블의 값들과 비교하는데 테이블의 끝까지 비교해도 일치하지 않은 경우 이므로 레이블 1로 이동한다. 즉 VFP 명령이 아닐 경우 보조 프로세서 명령 여부를 찾을 계획이다.
    • 코드 라인 9~11에서 instruction(r0)을 mask(r5)하여 opcode(r7) 값과 비교하여 같지 않은 경우 다음 테이블 엔트리와 비교하기 위해 레이블 2로 이동하여 루프를 돈다.
    • 코드 라인 12~15에서 보조 프로세서 인수 전달용 구조체 curr->used_cp[] 배열의 10번째와 11번째에 1을 저장한 후 do_vfp 레이블로 이동한다.
  • 코드 라인 15~33에서 VFP가 없는 아키텍처에서 undefined exception으로 진입한 경우이므로 이에 대응하기 위해 FPE(Floating Point Emulation)를 호출하여 지원한다.
    • 코드 라인 15~19에서 보조프로세서관련 명령이 아닌 경우 fault 처리를 위해 lr(__und_usr_fault_32 )주소로 복귀한다.
      • 보조 프로세서 관련 명령
        • CDP(Coprocessor Data oPerations), CPRT(COprocessor Register Transfer)
        • LDC(Load Data from Coprocessor) 및 STC(Store To Coprocessor) 등
    • 코드 라인 20~25에서 CP 번호를 r8 레지스터에 대입하고 이 값을 인덱스로 curr->used_cp[]에 1을 저장한다.
    • 코드 라인 26~32에서 보조프로세서가 iWMMXt인지 판단되는 경우 iwmmxt_task_enable 레이블로 이동한다.
    • 코드 라인 33에서 보조 프로세서 번호에 해당하는 테이블 위치로 jump 한다.
      • CP 번호가 1~2번인 경우 FPE(Floating Point Emulation)를 처리하러 do_fpe 레이블로 이동한다.
      • CONFIG_CRUNCH 커널 옵션이 설정되고 CP 번호가 4~6번인 경우 MaverickCrunch를 처리하러 crunch_task_enable 레이블로 이동한다.
      • CONFIG_VFP가 설정되고 CP번호가 10~11번인 경우 VFP를 처리하러 do_vfp 레이블로 이동한다.
      • 이 외의 경우에는 fault 처리를 위해 lr(__und_usr_fault_32) 주소로 복귀한다.

 

CP 별(보조프로세서 인덱스) jump 테이블

        ret.w   lr                              @ CP#0
        W(b)    do_fpe                          @ CP#1 (FPE)
        W(b)    do_fpe                          @ CP#2 (FPE)
        ret.w   lr                              @ CP#3
#ifdef CONFIG_CRUNCH
        b       crunch_task_enable              @ CP#4 (MaverickCrunch)
        b       crunch_task_enable              @ CP#5 (MaverickCrunch)
        b       crunch_task_enable              @ CP#6 (MaverickCrunch)
#else
        ret.w   lr                              @ CP#4
        ret.w   lr                              @ CP#5
        ret.w   lr                              @ CP#6
#endif
        ret.w   lr                              @ CP#7
        ret.w   lr                              @ CP#8
        ret.w   lr                              @ CP#9
#ifdef CONFIG_VFP
        W(b)    do_vfp                          @ CP#10 (VFP)
        W(b)    do_vfp                          @ CP#11 (VFP)
#else
        ret.w   lr                              @ CP#10 (VFP)
        ret.w   lr                              @ CP#11 (VFP)
#endif
        ret.w   lr                              @ CP#12
        ret.w   lr                              @ CP#13
        ret.w   lr                              @ CP#14 (Debug)
        ret.w   lr                              @ CP#15 (Control)

 

.LCneon_arm_opcodes

arch/arm/kernel/entry-armv.S

#ifdef CONFIG_NEON
        .align  6

.LCneon_arm_opcodes:
        .word   0xfe000000                      @ mask
        .word   0xf2000000                      @ opcode

        .word   0xff100000                      @ mask
        .word   0xf4000000                      @ opcode

        .word   0x00000000                      @ mask
        .word   0x00000000                      @ opcode

NEON instruction을 구분하기 위한 mask와 opcode이다.

  • 마지막 word 두 개는 종료를 구분하기 위한 값이다.

 

 

ARM instruction set format

bit 27~26이 설정된 명령이 보조 프로세서 관련 명령이다.

  • STC, STC2, LDC, LDC2, MCRR, MCRR2, MRRC, MRRC2, CDP, CDP2, MCR, MCR2, MRC, MRC2

 

Advanced SIMD (NEON) instruction set format

ARMv7에서 동작하는 Floating Point 장치이다.

  • VLD1~4, VST1~4, VADD, VSUB, VDIV, …)
  • 참고: VFP Instruction Set Quick Reference | arm – 다운로드 pdf

 

VFP 소프트 지원 호출 함수

do_vfp

arch/arm/vfp/entry.S

@ VFP entry point.
@
@  r0  = instruction opcode (32-bit ARM or two 16-bit Thumb)
@  r2  = PC value to resume execution after successful emulation
@  r9  = normal "successful" return address
@  r10 = this threads thread_info structure
@  lr  = unrecognised instruction return address
@  IRQs enabled.
@
ENTRY(do_vfp)
        inc_preempt_count r10, r4
        ldr     r4, .LCvfp
        ldr     r11, [r10, #TI_CPU]     @ CPU number
        add     r10, r10, #TI_VFPSTATE  @ r10 = workspace
        ldr     pc, [r4]                @ call VFP entry point
ENDPROC(do_vfp)

VFP 소프트 지원용 핸들러 함수를 호출한다.

  • VFPv1 및 VFPv2 아키텍처에서는 고정 소수 연산의 예외처리를 소프트웨어의 도움을 받아 처리하게 되어 있다.
  • .LCvfp
    • vfp/vfpmodule.c – core_initcall(vfp_init)을 통해 초기화된다.
      • 초기화 전에는 .LCvfp 값은 vfp_null_entry() 함수 주소를 가리킨다.
      • 초기화가 정상적으로 완료되면 .LCvfp 값은 vfp_support_entry() 함수 주소를 가리킨다.

 

vfp_null_entry()

arch/arm/vfp/entry.S

ENTRY(vfp_null_entry)
        dec_preempt_count_ti r10, r4
        ret     lr
ENDPROC(vfp_null_entry)

preemption 카운터를 감소시키고 복귀한다.

 

FPE 호출 함수

do_fpe

arch/arm/kernel/entry-armv.S

do_fpe:
        ldr     r4, .LCfp
        add     r10, r10, #TI_FPSTATE           @ r10 = workspace
        ldr     pc, [r4]                        @ Call FP module USR entry point

FPE(Floating Point Emulation) 핸들러 함수를 호출한다.

  • .LCfp
    • nwfpe/fpmodule.c – module_init(fpe_init)을 통해 초기화된다.
      • 초기화되면 nwfpe_enter: 레이블 주소를 가리킨다.

 

참고

 

Exception -3- (ARM32 Handler 2)

<kernel v5.4>

Data Abort 핸들러

__dabt_usr

arch/arm/kernel/entry-armv.S

        .align  5
__dabt_usr:
        usr_entry uaccess=0
        kuser_cmpxchg_check
        mov     r2, sp
        dabt_helper
        b       ret_from_exception
 UNWIND(.fnend          )
ENDPROC(__dabt_usr)

user 모드에서 data abort exception을 만나 진입하게 되면 data abort 핸들러를 수행한 후 다시 user 모드로 복귀한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 pt_regs 구조체 순서로 백업한다.
  • 코드 라인 4에서 atomic 연산을 지원하지 못하는 아키텍처에서 atomic 하게 처리해야 하는 구간에서 인터럽트를 맞이하고 복귀할 때 그 atomic operation 구간의 시작부분으로 다시 돌아가도록 pt_regs의 pc를 조작한다.
  • 코드 라인 5~6에서 r2 레지스터에 스택 위치를 담고 data abort 핸들러를 호출한다.
  • 코드 라인 7에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 user 모드로 복귀한다.

 

dabt_helper 매크로

arch/arm/kernel/entry-armv.S

        .macro  dabt_helper

        @
        @ Call the processor-specific abort handler:
        @
        @  r2 - pt_regs
        @  r4 - aborted context pc
        @  r5 - aborted context psr
        @
        @ The abort handler must return the aborted address in r0, and
        @ the fault status register in r1.  r9 must be preserved.
        @
#ifdef MULTI_DABORT
        ldr     ip, .LCprocfns
        mov     lr, pc
        ldr     pc, [ip, #PROCESSOR_DABT_FUNC]
#else
        bl      CPU_DABORT_HANDLER
#endif
        .endm

data abort 핸들러 함수를 호출한다.

  • 코드 라인 13~16에서 data abort 핸들러가 2 개 이상 있어야 하는 경우 MULTI_DABORT가 설정된다.
  • 코드 라인 17~19에서 빌드 타임에 정해진 아키텍처의 data abort 핸들러를 호출한다.

 

.LCprocfns

arch/arm/kernel/entry-armv.S

#ifdef MULTI_DABORT
.LCprocfns:
        .word   processor
#endif

 

CPU_DABORT_HANDLER

arch/arm/include/asm/glue-df.h

#ifdef CONFIG_CPU_ABRT_EV7
# ifdef CPU_DABORT_HANDLER
#  define MULTI_DABORT 1
# else
#  define CPU_DABORT_HANDLER v7_early_abort
# endif
#endif

빌드타임에 아키텍처가 ARMv7으로 정해진 경우 data abort 핸들러 함수는 v7_early_abort() 이다.

 

v7_early_abort()

arch/arm/mm/abort-ev7.S

/*
 * Function: v7_early_abort
 *
 * Params  : r2 = pt_regs
 *         : r4 = aborted context pc
 *         : r5 = aborted context psr
 *
 * Returns : r4 - r11, r13 preserved
 *
 * Purpose : obtain information about current aborted instruction.
 */
        .align  5
ENTRY(v7_early_abort)
        mrc     p15, 0, r1, c5, c0, 0           @ get FSR
        mrc     p15, 0, r0, c6, c0, 0           @ get FAR
        uaccess_disable ip                      @ disable userspace access

        /*
         * V6 code adjusts the returned DFSR.
         * New designs should not need to patch up faults.
         */

#if defined(CONFIG_VERIFY_PERMISSION_FAULT)
        /*
         * Detect erroneous permission failures and fix
         */
        ldr     r3, =0x40d                      @ On permission fault
        and     r3, r1, r3
        cmp     r3, #0x0d
        bne     do_DataAbort

        mcr     p15, 0, r0, c7, c8, 0           @ Retranslate FAR
        isb
        mrc     p15, 0, ip, c7, c4, 0           @ Read the PAR
        and     r3, ip, #0x7b                   @ On translation fault
        cmp     r3, #0x0b
        bne     do_DataAbort
        bic     r1, r1, #0xf                    @ Fix up FSR FS[5:0]
        and     ip, ip, #0x7e
        orr     r1, r1, ip, LSR #1
#endif

        b       do_DataAbort
ENDPROC(v7_early_abort)

data abort exception 상황이 발생 시 DFSR(Data Fault Status Register)에서 읽은 상태 코드를 갖고 doDataAbort() 함수를 호출하여 관련 fault 처리기를 호출한다.

  • 코드 라인 3에서 DFSR(Data Fault Status Register)를 통해 data abort exception에 대한 상태 코드를 읽어 r1 레지스터에 대입한다.
  • 코드 라인 4에서 DFAR(Data Fault Address Register)를 통해 data abort exception 당시의 물리 주소를 읽어 r0 레지스터에 대입한다.
  • 코드 라인 12~30에서 CONFIG_VERIFY_PERMISSION_FAULT 커널 옵션은 스냅드래곤의 qsd8x60 SoC의 오류를 교정하기 위해 사용된다.
  • 코드 라인 32에서 data fault 상태 코드에 따른 fault 처리기를 호출한다.

 

do_DataAbort()

arch/arm/mm/fault.c

/*
 * Dispatch a data abort to the relevant handler.
 */
asmlinkage void 
do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
        
        if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs))
                return; 

        pr_alert("8<--- cut here ---\n");
        pr_alert("Unhandled fault: %s (0x%03x) at 0x%08lx\n",
                inf->name, fsr, addr);
        show_pte(KERN_ALERT, current->mm, addr);

        arm_notify_die("", regs, inf->sig, inf->code, (void __user *)addr,
                       fsr, 0);
}

DFSR(Data Fault Status Register)로 부터 읽은 5비트로 구성된 fault 상태 코드에 따른 fault 핸들러 함수를 호출한다.

  • 코드 라인 4~7에서 fsr을 인덱스로 fsr_info[] 배열에서 fsr_info 구조체에 설정된 fault 핸들러 함수를 실행한다. 만일 결과가 성공(0)인 경우 함수를 빠져나간다.
  • 코드 라인 9~12에서 “Unhandled fault: ……” 크리티컬 메시지 와 pte 정보를 출력한다.
  • 코드 라인 14~15에서 관련 태스크의 종료 처리를 요청한다.
    • 유저 태스크인 경우 시그널을 보내 종료 처리를 하고, 커널 태스크인경우 시스템의 die() 처리를 진행한다.
      • 유저 태스크에 보낼 시그널은 fsr_info 구조체에 설정된 sig 값을 가져온다. (SIGSEGV, SIGBUS, SIGKILL 중 하나)

 

fsr_info[]

arch/arm/mm/fsr-2level.c

static struct fsr_info fsr_info[] = {
.       /*
         * The following are the standard ARMv3 and ARMv4 aborts.  ARMv5
         * defines these to be "precise" aborts.
         */
        { do_bad,               SIGSEGV, 0,             "vector exception"                 },
        { do_bad,               SIGBUS,  BUS_ADRALN,    "alignment exception"              },
        { do_bad,               SIGKILL, 0,             "terminal exception"               },
        { do_bad,               SIGBUS,  BUS_ADRALN,    "alignment exception"              },
        { do_bad,               SIGBUS,  0,             "external abort on linefetch"      },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "section translation fault"        },
        { do_bad,               SIGBUS,  0,             "external abort on linefetch"      },
        { do_page_fault,        SIGSEGV, SEGV_MAPERR,   "page translation fault"           },
        { do_bad,               SIGBUS,  0,             "external abort on non-linefetch"  },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "section domain fault"             },
        { do_bad,               SIGBUS,  0,             "external abort on non-linefetch"  },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "page domain fault"                },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_sect_fault,        SIGSEGV, SEGV_ACCERR,   "section permission fault"         },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_page_fault,        SIGSEGV, SEGV_ACCERR,   "page permission fault"            },
        /*
         * The following are "imprecise" aborts, which are signalled by bit
         * 10 of the FSR, and may not be recoverable.  These are only
         * supported if the CPU abort handler supports bit 10.
         */
        { do_bad,               SIGBUS,  0,             "unknown 16"                       },
        { do_bad,               SIGBUS,  0,             "unknown 17"                       },
        { do_bad,               SIGBUS,  0,             "unknown 18"                       },
        { do_bad,               SIGBUS,  0,             "unknown 19"                       },
        { do_bad,               SIGBUS,  0,             "lock abort"                       }, /* xscale */
        { do_bad,               SIGBUS,  0,             "unknown 21"                       },
        { do_bad,               SIGBUS,  BUS_OBJERR,    "imprecise external abort"         }, /* xscale */
        { do_bad,               SIGBUS,  0,             "unknown 23"                       },
        { do_bad,               SIGBUS,  0,             "dcache parity error"              }, /* xscale */
        { do_bad,               SIGBUS,  0,             "unknown 25"                       },
        { do_bad,               SIGBUS,  0,             "unknown 26"                       },
        { do_bad,               SIGBUS,  0,             "unknown 27"                       },
        { do_bad,               SIGBUS,  0,             "unknown 28"                       },
        { do_bad,               SIGBUS,  0,             "unknown 29"                       },
        { do_bad,               SIGBUS,  0,             "unknown 30"                       },
        { do_bad,               SIGBUS,  0,             "unknown 31"                       },
};

DFSR(Data Fault Status Register)로 부터 읽은 5비트로 구성된 fault 상태 코드에 따라 설정된 fault 핸들러 함수를 dispatch하기 위해 사용된다.

  • do_bad()
    • 기타 exception으로 아무것도 수행하지 않고 1을 반환한다.
  • do_translation_fault()
    • 섹션 변환 실패 시 호출한다.
  • do_page_fault()
    • 2 가지의 경우로 페이지 변환 실패 또는 페이지 권한 위반 시 호출한다.
  • do_sect_fault()
    • 섹션 페이지의 권한 위반 시 호출한다.

 

fsr_fs()

arch/arm/mm/fault.h

static inline int fsr_fs(unsigned int fsr) 
{
        return (fsr & FSR_FS3_0) | (fsr & FSR_FS4) >> 6;
}

DFSR(Data Fault Status Register)로부터 읽어온 fsr 값에서 bit[0~3]과 bit[10]으로 5개의 bit를 연달아 구성해서 반환한다.

  • 예) fsr=0x0000_040a -> 0x1a

 

__dabt_invalid

arch/arm/kernel/entry-armv.S

__dabt_invalid:
        inv_entry BAD_DATA
        b       common_invalid
ENDPROC(__dabt_invalid)

허용하지 않은 모드에서 data abort exception 핸들러에 진입하여 실패 처리를 위한 루틴이다

  • 코드 라인 2에서 레지스터를 스택에 백업한다.
    • 스택에 레지스터들을 백업하기 위한 공간(struct pt_regs)을 확보하고  r1~r14(lr)까지 백업해둔다. r1 레지스터에는 reason 값을 대입한다.
  • 코드 라인 3에서 common_invalid 레이블로 이동하야 스택의 pt_regs 위치에 레지스터들을 백업하고 “Oops” 출력 및 panic() 처리한다.

 

__dabt_svc

arch/arm/kernel/entry-armv.S

        .align  5
__dabt_svc:
        svc_entry uaccess=0
        mov     r2, sp
        dabt_helper
 THUMB( ldr     r5, [sp, #S_PSR]        )       @ potentially updated CPSR
        svc_exit r5                             @ return from exception
 UNWIND(.fnend          )
ENDPROC(__dabt_svc)

svc 모드에서 data abort exception을 만나 진입하게 되면 data abort 핸들러를 수행한 후 다시 svc 모드로 복귀한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 pt_regs(svc_pt_regs) 구조체 순서로 백업한다.
  • 코드 라인 4~5에서 r2 레지스터에 스택 위치를 담고 data abort 핸들러를 호출한다.
  • 코드 라인 7에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 svc 모드로 복귀한다.

 


Prefetch Abort 핸들러

__pabt_usr

arch/arm/kernel/entry-armv.S

        .align  5
__pabt_usr:
        usr_entry
        mov     r2, sp                          @ regs
        pabt_helper
 UNWIND(.fnend          )
        /* fall through */
/*
 * This is the return code to user mode for abort handlers
 */
ENTRY(ret_from_exception)
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )
        get_thread_info tsk
        mov     why, #0
        b       ret_to_user
 UNWIND(.fnend          )
ENDPROC(__pabt_usr)
ENDPROC(ret_from_exception)

user 모드에서 pre-fetch abort exception을 만나 진입하게 되면 pre-fetch abort 핸들러를 수행한 후 다시 user 모드로 복귀한다.

  • 처리가 유사한 __dabt_usr 소스 설명 참고

 

pabt_helper 매크로

arch/arm/kernel/entry-armv.S

        .macro  pabt_helper
        @ PABORT handler takes pt_regs in r2, fault address in r4 and psr in r5
#ifdef MULTI_PABORT
        ldr     ip, .LCprocfns
        mov     lr, pc
        ldr     pc, [ip, #PROCESSOR_PABT_FUNC]
#else
        bl      CPU_PABORT_HANDLER
#endif
        .endm

pre-fetch abort 핸들러 함수를 호출한다.

  • 처리가 유사한 dabt_helper 소스 설명 참고

 

CPU_PABORT_HANDLER

arch/arm/include/asm/glue-pf.h

#ifdef CONFIG_CPU_PABRT_V7
# ifdef CPU_PABORT_HANDLER
#  define MULTI_PABORT 1
# else 
#  define CPU_PABORT_HANDLER v7_pabort
# endif
#endif

빌드타임에 아키텍처가 ARMv7으로 정해진 경우 pre-fetch abort 핸들러 함수는 v7_pabort() 이다.

 

v7_pabort

arch/arm/mm/pabort-v7.S

/*
 * Function: v7_pabort
 *
 * Params  : r2 = pt_regs
 *         : r4 = address of aborted instruction
 *         : r5 = psr for parent context
 *
 * Returns : r4 - r11, r13 preserved
 *
 * Purpose : obtain information about current prefetch abort.
 */
        .align  5
ENTRY(v7_pabort)
        mrc     p15, 0, r0, c6, c0, 2           @ get IFAR
        mrc     p15, 0, r1, c5, c0, 1           @ get IFSR
        b       do_PrefetchAbort
ENDPROC(v7_pabort)

pre-fetch abort exception 상황이 발생 시 IFSR에서 읽은 상태 코드를 갖고 do_PrefetchAbort() 함수를 호출하여 관련 fault 처리기를 호출한다.

 

do_PrefetchAbort()

arch/arm/mm/fault.c

asmlinkage void
do_PrefetchAbort(unsigned long addr, unsigned int ifsr, struct pt_regs *regs)
{
        const struct fsr_info *inf = ifsr_info + fsr_fs(ifsr);

        if (!inf->fn(addr, ifsr | FSR_LNX_PF, regs))
                return;

        pr_alert("Unhandled prefetch abort: %s (0x%03x) at 0x%08lx\n",
                inf->name, ifsr, addr);

        arm_notify_die("", regs, inf->sig, inf->code, (void __user *)addr,
                       ifsr, 0);
}

pre-fetch abort exception 상황이 발생 시 PFSR(Pre-fetch Fault Status Register)에 기록된 5비트의 상태 코드를 인덱스로 ifsr_info[] 배열에 설정된 fault 처리기를 호출한다.

  • 처리가 유사한 do_DataAbort() 소스 설명 참고

 

ifsr_info[]

arch/arm/mm/fsr-2level.c

static struct fsr_info ifsr_info[] = {
        { do_bad,               SIGBUS,  0,             "unknown 0"                        },
        { do_bad,               SIGBUS,  0,             "unknown 1"                        },
        { do_bad,               SIGBUS,  0,             "debug event"                      },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "section access flag fault"        },
        { do_bad,               SIGBUS,  0,             "unknown 4"                        },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "section translation fault"        },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "page access flag fault"           },
        { do_page_fault,        SIGSEGV, SEGV_MAPERR,   "page translation fault"           },
        { do_bad,               SIGBUS,  0,             "external abort on non-linefetch"  },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "section domain fault"             },
        { do_bad,               SIGBUS,  0,             "unknown 10"                       },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "page domain fault"                },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_sect_fault,        SIGSEGV, SEGV_ACCERR,   "section permission fault"         },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_page_fault,        SIGSEGV, SEGV_ACCERR,   "page permission fault"            },
        { do_bad,               SIGBUS,  0,             "unknown 16"                       },
        { do_bad,               SIGBUS,  0,             "unknown 17"                       },
        { do_bad,               SIGBUS,  0,             "unknown 18"                       },
        { do_bad,               SIGBUS,  0,             "unknown 19"                       },
        { do_bad,               SIGBUS,  0,             "unknown 20"                       },
        { do_bad,               SIGBUS,  0,             "unknown 21"                       },
        { do_bad,               SIGBUS,  0,             "unknown 22"                       },
        { do_bad,               SIGBUS,  0,             "unknown 23"                       },
        { do_bad,               SIGBUS,  0,             "unknown 24"                       },
        { do_bad,               SIGBUS,  0,             "unknown 25"                       },
        { do_bad,               SIGBUS,  0,             "unknown 26"                       },
        { do_bad,               SIGBUS,  0,             "unknown 27"                       },
        { do_bad,               SIGBUS,  0,             "unknown 28"                       },
        { do_bad,               SIGBUS,  0,             "unknown 29"                       },
        { do_bad,               SIGBUS,  0,             "unknown 30"                       },
        { do_bad,               SIGBUS,  0,             "unknown 31"                       },
};

IFSR(Pre-fetch Fault Status Register)로 부터 읽은 5비트로 구성된 fault 상태 코드에 따라 설정된 fault 핸들러 함수를 dispatch하기 위해 사용된다.

  • do_bad()
    • 기타 exception으로 아무것도 수행하지 않고 1을 반환한다.
  • do_translation_fault()
    • 섹션 변환 실패 시 호출한다.
  • do_page_fault()
    • 2 가지의 경우로 페이지 변환 실패 또는 페이지 권한 위반 시 호출한다.
  • do_sect_fault()
    • 섹션 페이지의 권한 위반 시 호출한다.

 

__pabt_invalid

arch/arm/kernel/entry-armv.S

__pabt_invalid:
        inv_entry BAD_PREFETCH
        b       common_invalid
ENDPROC(__pabt_invalid)

허용하지 않은 모드에서 pre-fetch abort exception 핸들러에 진입하여 실패 처리를 위한 루틴이다

  • 처리가 유사한 __dabt_invalid 소스 설명 참고

 

__pabt_svc

arch/arm/kernel/entry-armv.S

__pabt_svc:
        svc_entry trace=0
        mov     r2, sp                          @ regs
        pabt_helper
        svc_exit r5                             @ return from exception
 UNWIND(.fnend          )
ENDPROC(__pabt_svc)

svc 모드에서 pre-fetch abort exception을 만나 진입하게 되면 pre-fetch abort 핸들러를 수행한 후 다시 svc 모드로 복귀한다.

  • 처리가 유사한 __dabt_svc 소스 설명 참고

 


Fault 처리 관련 -1-

do_bad()

arch/arm/mm/fault.c

/*
 * This abort handler always returns "fault".
 */
static int
do_bad(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        return 1;
}

특별한 처리 없이 항상 실패로 반환한다.

 

Fault 처리 관련 -2-

do_translation_fault()

arch/arm/mm/fault.c

/*
 * First Level Translation Fault Handler
 *
 * We enter here because the first level page table doesn't contain
 * a valid entry for the address.
 *
 * If the address is in kernel space (>= TASK_SIZE), then we are
 * probably faulting in the vmalloc() area.
 *
 * If the init_task's first level page tables contains the relevant
 * entry, we copy the it to this task.  If not, we send the process
 * a signal, fixup the exception, or oops the kernel.
 *
 * NOTE! We MUST NOT take any locks for this case. We may be in an
 * interrupt or a critical region, and should only copy the information
 * from the master page table, nothing more.
 */
#ifdef CONFIG_MMU
static int __kprobes
do_translation_fault(unsigned long addr, unsigned int fsr,
                     struct pt_regs *regs)
{
        unsigned int index;
        pgd_t *pgd, *pgd_k;
        pud_t *pud, *pud_k;
        pmd_t *pmd, *pmd_k;

        if (addr < TASK_SIZE)
                return do_page_fault(addr, fsr, regs);

        if (user_mode(regs))
                goto bad_area;
        
        index = pgd_index(addr);

        pgd = cpu_get_pgd() + index;
        pgd_k = init_mm.pgd + index;
        
        if (pgd_none(*pgd_k))
                goto bad_area;
        if (!pgd_present(*pgd))
                set_pgd(pgd, *pgd_k);
        
        pud = pud_offset(pgd, addr);
        pud_k = pud_offset(pgd_k, addr);

        if (pud_none(*pud_k))
                goto bad_area;
        if (!pud_present(*pud))
                set_pud(pud, *pud_k);

        pmd = pmd_offset(pud, addr);
        pmd_k = pmd_offset(pud_k, addr);

#ifdef CONFIG_ARM_LPAE
        /*
         * Only one hardware entry per PMD with LPAE.
         */
        index = 0;
#else
        /*
         * On ARM one Linux PGD entry contains two hardware entries (see page
         * tables layout in pgtable.h). We normally guarantee that we always
         * fill both L1 entries. But create_mapping() doesn't follow the rule.
         * It can create inidividual L1 entries, so here we have to call
         * pmd_none() check for the entry really corresponded to address, not
         * for the first of pair.
         */
        index = (addr >> SECTION_SHIFT) & 1;
#endif
        if (pmd_none(pmd_k[index]))
                goto bad_area;

        copy_pmd(pmd, pmd_k);
        return 0;

bad_area:
        do_bad_area(addr, fsr, regs);
        return 0;
}

첫 번째 레벨에서 translation fault가 발생한 경우 유저 영역이거나 커널 영역이면서 테이블 엔트리가 비어있는 경우 do_page_fauilt() 함수에서 처리하게 한다. 커널 영역인 경우 해당 주소의 테이블 엔트리(pgd, pud, pmd)가 present 플래그만 빠져있는 경우라면 커널 엔트리에서 유저 엔트리로 복사한다.

  • 코드 라인 11~12에서 fault 주소 addr이 user 주소 공간인 경우 do_page_fault() 함수를 호출한다.
  • 코드 라인 14~15에서 kernel address space이지만 user mode에서 fault된 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 17에서 fault 주소로 pgd 엔트리의 인덱스 번호를 알아온다.
  • 코드 라인 19~20에서 유저 테이블의 pgd 엔트리 및 커널 테이블의 pgd 엔트리 주소를 산출한다.
  • 코드 라인 22~23에서 커널용 pgd 엔트리 값이 0으로 비어 있는 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 24~25에서 유저용 pgd 엔트리가 present 설정이 없는 경우 커널용 pgd 엔트리 값을 유저용 pgd 엔트리에 복사한다.
  • 코드 라인 27~28에서 유저용 pud 엔트리 및 커널용 pud 엔트리 주소를 산출한다.
  • 코드 라인 30~31에서 커널용 pud 엔트리 값이 0으로 비어 있는 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 32~33에서 유저용 pud 엔트리가 present 설정이 없는 경우 커널용 pud 엔트리 값을 유저용 pud 엔트리에 복사한다.
  • 코드 라인 35~36에서 유저용 pmd 엔트리 및 커널용 pmd 엔트리 주소를 산출한다.
  • 코드 라인 52에서 pmd 엔트리가 홀 수 섹션인 경우 짝 수 섹션 단위로 절삭한다.
  • 코드 라인 54~55에서 커널용 pmd 엔트리 값이 0으로 비어 있는 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 57~58에서 커널용 pmd 엔트리 값을 유저용 pmd 엔트리에 복사하고 성공(0)으로 복귀한다.
  • 코드 라인 60~62에서 bad_area: 레이블에서는 do_bad_area() 함수를 호출한 후 성공(0)으로 복귀한다.

 

cpu_get_pgd()

arch/arm/include/asm/proc-fns.h

#define cpu_get_pgd()   \
        ({                                              \
                unsigned long pg;                       \
                __asm__("mrc    p15, 0, %0, c2, c0, 0"  \
                         : "=r" (pg) : : "cc");         \
                pg &= ~0x3fff;                          \
                (pgd_t *)phys_to_virt(pg);              \
        })
#endif

유저 테이블의 가상 주소를 산출한다.

  • 코드 라인 4~7에서 물리 주소를 담고 있는 TTBR0 값을 읽어와서 lsb 14비트를 절삭한 물리 주소를 가상 주소로 반환한다.

 

Fault 처리 관련 -3-

do_sect_fault()

arch/arm/mm/fault.c

/*
 * Some section permission faults need to be handled gracefully.
 * They can happen due to a __{get,put}_user during an oops.
 */
static int
do_sect_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{       
        do_bad_area(addr, fsr, regs);
        return 0;               
}

섹션 접근 권한 fault가 발생된 경우 do_bad_area() 함수를 호출하여 다음과 같은 처리를 한다.

  • 유저 모드에서 exception된 경우 해당 유저 태스크는 SIGSEGV 시그널을 받아서 종료된다.
  • 유저 모드가 아닌 모드에서 exception된 경우 die() 처리를 한다. 만일 ex_table(exception table)에 별도의 fixup 코드가 있는 경우 해당 코드를 실행시킨다

 

do_bad_area()

arch/arm/mm/fault.c()

void do_bad_area(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        struct task_struct *tsk = current;
        struct mm_struct *mm = tsk->active_mm;

        /*
         * If we are in kernel mode at this point, we
         * have no context to handle this fault with.
         */
        if (user_mode(regs))
                __do_user_fault(tsk, addr, fsr, SIGSEGV, SEGV_MAPERR, regs);
        else
                __do_kernel_fault(mm, addr, fsr, regs);
}

유저 모드에서 exception 된 경우 __do_user_fault() 함수를 호출하고 그 외의 모드는 __do_kernel_fault() 함수를 처리한다.

  • 코드 라인 10~11에서 유저 모드에서 exception되어 매핑되지 않은 영역에 접근하려 하는 경우 해당 유저 태스크에 SIGSEGV 시그널을 전송하여 태스크를 종료시킨다.
  • 코드 라인 12~13에서 유저 모드가 아닌 모드에서 exception되어 매핑되지 않은 영역에 접근하려 하는 경우 die() 처리를 한다. 단 ex_table(exception table)에 별도의 fixup 코드가 있는 경우 해당 코드를 실행시킨다.
    • get_user() 등의 매크로 함수에서 ex_table(exception table)에 fixup 코드를 등록하여 사용한다.

 

__do_user_fault()

arch/arm/mm/fault.c

/*
 * Something tried to access memory that isn't in our memory map..
 * User mode accesses just cause a SIGSEGV
 */
static void
__do_user_fault(unsigned long addr, unsigned int fsr, unsigned int sig, 
                int code, struct pt_regs *regs)
{
        struct task_struct *tsk = current;

        if (addr > TASK_SIZE)
                harden_branch_predictor();

#ifdef CONFIG_DEBUG_USER
        if (((user_debug & UDBG_SEGV) && (sig == SIGSEGV)) ||
            ((user_debug & UDBG_BUS)  && (sig == SIGBUS))) {
                printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
                       tsk->comm, sig, addr, fsr);
                show_pte(tsk->mm, addr);
                show_regs(regs);
        }
#endif
#ifndef CONFIG_KUSER_HELPERS
        if ((sig == SIGSEGV) && ((addr & PAGE_MASK) == 0xffff0000))
                printk_ratelimited(KERN_DEBUG
                                   "%s: CONFIG_KUSER_HELPERS disabled at 0x%08lx\n",
                                   tsk->comm, addr);
#endif

        tsk->thread.address = addr;
        tsk->thread.error_code = fsr;
        tsk->thread.trap_no = 14;
        force_sig_info(sig, code, (void __user *)addr);
}

유저 모드에서 커널 영역의 매핑되지 않은 페이지를 엑세스하려할 때 해당 유저 태스크에 인수로 요청 받은 sigSIGSEGV, SIGBUS, SIGKILL 중 하나 시그널을 전달한다.

 

__do_kernel_fault()

arch/arm/mm/fault.c

/*
 * Oops.  The kernel tried to access some page that wasn't present.
 */
static void
__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
                  struct pt_regs *regs)
{
        /*
         * Are we prepared to handle this kernel fault?
         */
        if (fixup_exception(regs))
                return;

        /*
         * No handler, we'll have to terminate things with extreme prejudice.
         */
        bust_spinlocks(1);
        pr_alert("8<--- cut here ---\n");
        pr_alert("Unable to handle kernel %s at virtual address %08lx\n",
                 (addr < PAGE_SIZE) ? "NULL pointer dereference" :
                 "paging request", addr);

        show_pte(KERN_ALERT, mm, addr);
        die("Oops", regs, fsr);
        bust_spinlocks(0);
        do_exit(SIGKILL);
}

유저 모드가 아닌 모드에서 커널 영역의 매핑되지 않은 페이지를 엑세스하려할 때 ex_table(exception table) 에 등록한 별도의 fixup 코드를 실행시킨다. 만일 등록된 별도의 fixup 코드가 없는 경우 “Unable to handle kernel…” 메시지와 함께 pte 정보를 출력하고 die() 처리한다.

  • 코드 라인 8~9에서 ex_table(exception table) 에 등록한 별도의 fixup 코드가 있는 경우 이를 실행하고 복귀한다.
  • 코드 라인 15~21에서 “Unable to handle kernel…” 메시지와 함께 pte 정보를 출력하고 die() 처리한다

 

Fault 처리 관련 -4-

do_page_fault()

arch/arm/mm/fault.c

static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        struct task_struct *tsk;
        struct mm_struct *mm;
        int sig, code;
        vm_fault_t fault;
        unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

        if (kprobe_page_fault(regs, fsr))
                return 0;

        tsk = current;
        mm  = tsk->mm;

        /* Enable interrupts if they were enabled in the parent context. */
        if (interrupts_enabled(regs))
                local_irq_enable();

        /*
         * If we're in an interrupt or have no user
         * context, we must not take the fault..
         */
        if (faulthandler_disabled() || !mm)
                goto no_context;

        if (user_mode(regs))
                flags |= FAULT_FLAG_USER;
        if ((fsr & FSR_WRITE) && !(fsr & FSR_CM))
                flags |= FAULT_FLAG_WRITE;

abort exception되어 fault 처리를 하는데 exception 되기 전의 모드에 따라 처리를 다음과 같이 수행한다.

  • 유저 모드
    • vma에 포함되지 않은 영역인 경우 태스크 die 처리
    • vma에 포함되었지만 페이지 테이블 엔트리가 present 플래그가 없어 즉, 매핑되지 않은 영역은 mm_fault 처리하여 실제 메모리를 할당하고 매핑시킨다.
      • lazy alloc, swap, file 매핑 등
  • 유저 모드가 아닌 모드(커널 모드)
    • vma에 포함되지 않은 영역인 경우 die 처리
    • vma에 포함되었지만 페이지 테이블 엔트리가 present 플래그가 없어 즉, 매핑되지 않은 영역은 mm_fault 처리하여 실제 메모리를 할당하고 매핑시킨다. (vmalloc 영역에서의 lazy alloc)

 

  • 코드 라인 8에서 allow retry와 killable 플래그를 기본 플래그로 대입한다.
  • 코드 라인 10~11에서 페이지 fault 처리에 앞서 커널 디버거 kprobe를 지원하고 동작중인 경우 kprobe용 fault 핸들러 함수를 처리할 수 있게 한다.
  • 코드 라인 13~14에서 현재 태스크의 메모리 디스크립터를 가리킨다.
  • 코드 라인 17~18에서 exception되기 전에 인터럽트가 가능한 상태인 경우 local irq를 enable한다.
  • 코드 라인 24~25에서 atomic 하게 처리해야 하는 스케쥴링하는 동안이거나 메모리 디스크립터가 지정되지 않은 경우 no_context: 레이블로 이동하여 커널 fault를 처리 한다. (die)
  • 코드 라인 27~28에서 user 모드에서 exception된 경우 user 플래그 표시를 한다.
  • 코드 라인 29~30에서 fault 상태 값에 FSR_WRITE가 있는 경우 write 플래그를 추가한다.

 

        /*
         * As per x86, we may deadlock here.  However, since the kernel only
         * validly references user space from well defined areas of the code,
         * we can bug out early if this is from code which shouldn't.
         */ 
        if (!down_read_trylock(&mm->mmap_sem)) {
                if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))
                        goto no_context;
retry:
                down_read(&mm->mmap_sem);
        } else {
                /*
                 * The above down_read_trylock() might have succeeded in
                 * which case, we'll have missed the might_sleep() from
                 * down_read()
                 */   
                might_sleep();
#ifdef CONFIG_DEBUG_VM
                if (!user_mode(regs) &&
                    !search_exception_tables(regs->ARM_pc))
                        goto no_context;
#endif
        }
        
        fault = __do_page_fault(mm, addr, fsr, flags, tsk);

        /* If we need to retry but a fatal signal is pending, handle the
         * signal first. We do not need to release the mmap_sem because
         * it would already be released in __lock_page_or_retry in
         * mm/filemap.c. */
        if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current)) { 
                if (!user_mode(regs))
                        goto no_context;
                return 0;
        }
        
        /*
         * Major/minor page fault accounting is only done on the
         * initial attempt. If we go through a retry, it is extremely
         * likely that the page will be found in page cache at that point.
         */

        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
        if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
                if (fault & VM_FAULT_MAJOR) {
                        tsk->maj_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
                                        regs, addr);
                } else {
                        tsk->min_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
                                        regs, addr);
                }
                if (fault & VM_FAULT_RETRY) {
                        /* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk
                        * of starvation. */
                        flags &= ~FAULT_FLAG_ALLOW_RETRY;
                        flags |= FAULT_FLAG_TRIED;
                        goto retry;
                }
        }

        up_read(&mm->mmap_sem);
  • 코드 라인 6~10에서 매핑과 관련한 세마포어 락을 시도하여 실패하면 천천히 다시 락을 획득한다. 단 유저 모드가 아니면서 ex_table에도 해당 주소가 없는 경우는 no_context: 레이블로 이동하여 커널 fault를 처리 한다. (die)
  • 코드 라인 11~23에서 preemption point를 수행하며 유저 모드가 아니면서 CONFIG_DEBUG_VM 커널 옵션을 사용하는 경우 ex_table에도 해당 주소가 없는 경우는 no_context: 레이블로 이동하여 커널 fault를 처리 한다. (die)
  • 코드 라인 25~35에서 페이지 fault 처리를 수행하고 수행 후 fault retry 요청이 있지만 시그널 지연중인 경우는 그냥 함수를 빠져나간다. 단 커널 모드에서 exception된 경우 no_context 레이블로 이동한다.
  • 코드 라인 43에서 CONFIG_PERF_EVENTS 커널 옵션을 사용하는 경우 커널에서 제공하는 s/w perf 카운터 중 하나인 PERF_COUNT_SW_PAGE_FAULTS 카운터를 증가시킨다.
  • 코드 라인 44~61에서 vm_fault 에러이면서 retry를 허용한 경우 retry 플래그를 제거 하고, retry 중(FAULT_FLAG_TRIED)이라고 설정한다음 다시 한 번 시도한다. 그리고 major/minor fault인지 여부에 따라 maj_flt 또는 min_flt를 증가시키고 해당 perf 카운터도 증가시킨다.

 

        /*
         * Handle the "normal" case first - VM_FAULT_MAJOR
         */
        if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
                return 0;

        /*
         * If we are in kernel mode at this point, we
         * have no context to handle this fault with.
         */
        if (!user_mode(regs))
                goto no_context;

        if (fault & VM_FAULT_OOM) {
                /*
                 * We ran out of memory, call the OOM killer, and return to
                 * userspace (which will retry the fault, or kill us if we
                 * got oom-killed)
                 */
                pagefault_out_of_memory();
                return 0;
        }

        if (fault & VM_FAULT_SIGBUS) {
                /*
                 * We had some memory, but were unable to
                 * successfully fix up this page fault.
                 */
                sig = SIGBUS;
                code = BUS_ADRERR;
        } else {
                /*
                 * Something tried to access memory that
                 * isn't in our memory map..
                 */
                sig = SIGSEGV;
                code = fault == VM_FAULT_BADACCESS ?
                        SEGV_ACCERR : SEGV_MAPERR;
        }

        __do_user_fault(addr, fsr, sig, code, regs);
        return 0;

no_context:
        __do_kernel_fault(mm, addr, fsr, regs);
        return 0;
}

유저 페이지 fault에 대한 처리를 수행한다.

  • 코드 라인 4~5에서 높은 확률로 fault 에러, badmap, badaccess가 없는 경우 함수를 종료한다.
  • 코드 라인 11~12에서 user 모드가 아닌 모드에서 exception된 경우 함수를 종료한다.
  • 코드 라인 14~22에서 OOM fault로 인해 매핑을 못한 경우 OOM 킬러가 동작하는 경우 OOM kill 처리를 수행한다.
    • 메모리를 많이 사용하는 태스크를 평가하여 kill 한다
  • 코드 라인 24~42에서 SIGBUS fault인 경우 시그널에 SIGBUS, code에 BUS_ADRERR을 담고 user fault 핸들러를 수행하고, SIGBUS가 아닌 경우 시그널에 SIGSEGV, code에 bad access 여부에 따라 SEGV_ACCERR 또는 SEGV_MAPERR를 담고 user fault 핸들러를 수행하고 복귀한다.
  • 코드 라인 44~46에서 no_context: 레이블이다. kernel fault 핸들러를 수행하고 복귀한다.

 

__do_page_fault()

arch/arm/mm/fault.c

static vm_fault_t __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
                unsigned int flags, struct task_struct *tsk)
{
        struct vm_area_struct *vma;
        vm_fault_t fault;

        vma = find_vma(mm, addr);
        fault = VM_FAULT_BADMAP;
        if (unlikely(!vma))
                goto out;
        if (unlikely(vma->vm_start > addr))
                goto check_stack;

        /*
         * Ok, we have a good vm_area for this
         * memory access, so we can handle it.
         */
good_area:
        if (access_error(fsr, vma)) {
                fault = VM_FAULT_BADACCESS;
                goto out;
        }

        return handle_mm_fault(vma, addr & PAGE_MASK, flags);

check_stack:
        /* Don't allow expansion below FIRST_USER_ADDRESS */
        if (vma->vm_flags & VM_GROWSDOWN &&
            addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
                goto good_area;
out:
        return fault;
}

유저 영역에 대한 fault 여부를 다음과 같이 확인한다.

  • 유저 태스크에 등록된 vma 영역을 벗어난 경우 VM_FAULT_BADMAP
  • 사용 권한이 없는 vma 영역인 경우 VM_FAULT_BADACCESS
  • vma 영역이지만 매핑만 하지 않은 경우이므로 물리 메모리를 할당하고 lazy alloc 처리, swap 또는 file 로딩 후 매핑처리한다.

 

  • 코드 라인 8~11에서 vma 영역에서 찾을 수 없는 경우 VM_FAULT_BADMAP을 반환한다.
  • 코드 라인 12~13에서 찾은 vma 영역 아래에 위치한 경우 check_stack 레이블로 이동한다.
  • 코드 라인 19~23에서 access 권한 에러인 경우 VM_FAULT_BADACCESS를 반환한다.
  • 코드 라인 25에서 vma 영역에 해당하지만 메모리 매핑이 아직 안된 페이지를 처리 하러 handle_mm_fault() 함수를 호출한다.
  • 코드 라인 27~31에서 check_stack: 레이블이다. vma가 밑으로 증가되는 스택이고 요청 주소가 유저 영역 하한 주소인 FIRST_USER_ADDRESS(arm에서는 2 페이지) 이상이고 스택이 확장된 스택이 아니면 good_area 레이블로 이동한다.

 


Undefined Instruction 핸들러

__und_usr

arch/arm/kernel/entry-armv.S

        .align  5
__und_usr:
        usr_entry uaccess=0

        mov     r2, r4
        mov     r3, r5

        @ r2 = regs->ARM_pc, which is either 2 or 4 bytes ahead of the
        @      faulting instruction depending on Thumb mode.
        @ r3 = regs->ARM_cpsr
        @
        @ The emulation code returns using r9 if it has emulated the
        @ instruction, or the more conventional lr if we are to treat
        @ this as a real undefined instruction
        @
        badr     r9, ret_from_exception

        @ IRQs must be enabled before attempting to read the instruction from
        @ user space since that could cause a page/translation fault if the
        @ page table was modified by another CPU.
        enable_irq

        tst     r3, #PSR_T_BIT                  @ Thumb mode?
        bne     __und_usr_thumb
        sub     r4, r2, #4                      @ ARM instr at LR - 4
1:      ldrt    r0, [r4]
 ARM_BE8(rev    r0, r0)                         @ little endian instruction

        @ r0 = 32-bit ARM instruction which caused the exception
        @ r2 = PC value for the following instruction (:= regs->ARM_pc)
        @ r4 = PC value for the faulting instruction
        @ lr = 32-bit undefined instruction function
        adr     lr, BSYM(__und_usr_fault_32)
        b       call_fpe

user 모드에서 undefined instruction exception을 만나 진입하게 되면 VFP 예외 처리, FPE 수행 등을 수행한 후 다시 user 모드로 복귀한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 pt_regs 구조체 순서로 백업한다.
  • 코드 라인 5~6에서 exception된 주소가 담기 r4 레지스터를 r2 레지스터에 담고, cpsr을 담고 있는 r5 레지스터를 r3 레지스터에 담는다.
  • 코드 라인 16에서 레지스터 r9에 ret_from_exception 레이블의 주소를 담는다.
  • 코드 라인 21에서 irq를 enable한다.
  • 코드 라인 23~24에서 cpsr의 thumb 모드 비트가 설정된 경우 __und_usr_thumb 레이블로 이동한다.
  • 코드 라인 25~26에서 exception된 instruction 코드를 r0 레지스터로 읽어온다.
    • exception 당시 pc – 4의 주소이다.
  • 코드 라인 33~34에서 __und_usr_fault_32 레이블의 주소를 돌아갈 주소로 지정하기 위해 lr 레지스터에 대입하고 Floating Point 관련 예외 처리 또는 에뮬레이션을 수행하기 위해 call_fpe 레이블로 이동한다.

 

__und_usr_fault_32

arch/arm/kernel/entry-armv.S

__und_usr_fault_32:
        mov     r1, #4
        b       1f
__und_usr_fault_16_pan:
        uaccess_disable ip
__und_usr_fault_16:
        mov     r1, #2
1:      mov     r0, sp
        badr     lr, ret_from_exception
        b       __und_fault
ENDPROC(__und_usr_fault_32)
ENDPROC(__und_usr_fault_16)

r1 레지스터에 4를 담고 ret_from_exception 레이블 주소를 복귀 주소로 lr 레지스터에 저장한 후 fault 처리를 위해 __und_fault 레이블로 이동한다.

 

__und_invalid

arch/arm/kernel/entry-armv.S

__und_invalid:
        inv_entry BAD_UNDEFINSTR

        @
        @ XXX fall through to common_invalid
        @

허용하지 않은 모드에서 undefined instruction exception 핸들러에 진입하여 실패 처리를 위한 루틴이다. 이어서 common_invalid: 레이블을 계속 진행한다.

  • 처리가 유사한 __dabt_invalid 소스 설명 참고

 

__und_svc

arch/arm/kernel/entry-armv.S

        .align  5
__und_svc:
#ifdef CONFIG_KPROBES
        @ If a kprobe is about to simulate a "stmdb sp..." instruction,
        @ it obviously needs free stack space which then will belong to
        @ the saved context.
        svc_entry MAX_STACK_SIZE
#else
        svc_entry
#endif
        @
        @ call emulation code, which returns using r9 if it has emulated
        @ the instruction, or the more conventional lr if we are to treat
        @ this as a real undefined instruction
        @
        @  r0 - instruction
        @
#ifndef CONFIG_THUMB2_KERNEL
        ldr     r0, [r4, #-4]
#else
        mov     r1, #2
        ldrh    r0, [r4, #-2]                   @ Thumb instruction at LR - 2
        cmp     r0, #0xe800                     @ 32-bit instruction if xx >= 0
        blo     __und_svc_fault
        ldrh    r9, [r4]                        @ bottom 16 bits
        add     r4, r4, #2
        str     r4, [sp, #S_PC]
        orr     r0, r9, r0, lsl #16
#endif
        badr     r9, __und_svc_finish
        mov     r2, r4
        bl      call_fpe

        mov     r1, #4                          @ PC correction to apply
__und_svc_fault:
        mov     r0, sp                          @ struct pt_regs *regs
        bl      __und_fault

__und_svc_finish:
        get_thread_info tsk
        ldr     r5, [sp, #S_PSR]                @ Get SVC cpsr
        svc_exit r5                             @ return from exception
 UNWIND(.fnend          )
ENDPROC(__und_svc)

svc 모드에서 undefined instruction exception을 만나 진입하게 되면 VFP 예외 처리, FPE 수행 등을 수행한 후 다시 svc 모드로 복귀한다.

  • 코드 라인 3~10에서 전체 레지스터를 스택에 pt_regs(svc_pt_regs) 구조체 순서로 백업한다.
    • kprobes를 사용 시 MAX_STACK_SIZE(64) 바이트 만큼의 공간을 스택에 추가로 확보한다.
  • 코드 라인 19에서 exception된 instruction 코드를 r0 레지스터에 가져온다.
    • exception 당시 pc – 4의 주소이다.
  • 코드 라인 30~32에서 __und_svc_finish 레이블의 주소를 r9에 담고 r4를 r2에 담고 VFP 예외 처리 및 FPE 수행 등을 처리하기 위해 call_fpe 함수를 호출한다.
  • 코드 라인 34~37에서 r1에 4를 더하고, r0에 스택위치를 대입한  후 __und_fault() 함수를 호출한다.
  • 코드 라인 40~42에서 r5 레지스터에 백업해두었던 pt_regs의 psr 값을 담은 후 스택에 백업해 둔 레지스터들을 복구하고 svc 모드로 빠져나간다.

 

__und_fault

arch/arm/kernel/entry-armv.S

__und_fault:
        @ Correct the PC such that it is pointing at the instruction
        @ which caused the fault.  If the faulting instruction was ARM
        @ the PC will be pointing at the next instruction, and have to
        @ subtract 4.  Otherwise, it is Thumb, and the PC will be
        @ pointing at the second half of the Thumb instruction.  We
        @ have to subtract 2.
        ldr     r2, [r0, #S_PC]
        sub     r2, r2, r1
        str     r2, [r0, #S_PC]
        b       do_undefinstr
ENDPROC(__und_fault)

복귀 주소에 correction(4) 만큼을 뺀다. undefined 훅에 등록된 명령인 경우 정상적으로 함수를 리턴하고 그렇지 않은 경우 유저 모드인 경우 task의 kill 처리를 위한 시그널을 요청하고, 그렇지 않은 경우 시스템을 die 처리한다.

  • 코드 라인 8~10에서 스택에 위치한 pt_regs의 pc 값을 읽어 r1 (correction)값을 뺀 후 다시 pt_regs의 pc 위치에 저장한다.
  • 코드 라인 11에서 설치된 undefined 훅에 등록된 명령인 경우 함수를 리턴하고 그렇지 않은 경우 유저 모드인 경우 task의 kill 처리를 위한 시그널을 요청하고, 그렇지 않은 경우 시스템을 die 처리한다.

 

do_undefinstr()

arch/arm/kernel/traps.c

asmlinkage void do_undefinstr(struct pt_regs *regs)
{
        unsigned int instr;
        siginfo_t info;
        void __user *pc;

        pc = (void __user *)instruction_pointer(regs);

        if (processor_mode(regs) == SVC_MODE) {
#ifdef CONFIG_THUMB2_KERNEL
                if (thumb_mode(regs)) {
                        instr = __mem_to_opcode_thumb16(((u16 *)pc)[0]);
                        if (is_wide_instruction(instr)) {
                                u16 inst2;
                                inst2 = __mem_to_opcode_thumb16(((u16 *)pc)[1]);
                                instr = __opcode_thumb32_compose(instr, inst2);
                        }
                } else
#endif
                        instr = __mem_to_opcode_arm(*(u32 *) pc);
        } else if (thumb_mode(regs)) {
                if (get_user(instr, (u16 __user *)pc))
                        goto die_sig;
                instr = __mem_to_opcode_thumb16(instr);
                if (is_wide_instruction(instr)) { 
                        unsigned int instr2;
                        if (get_user(instr2, (u16 __user *)pc+1))
                                goto die_sig;
                        instr2 = __mem_to_opcode_thumb16(instr2);
                        instr = __opcode_thumb32_compose(instr, instr2);
                }
        } else {
                if (get_user(instr, (u32 __user *)pc))
                        goto die_sig;
                instr = __mem_to_opcode_arm(instr);
        }

        if (call_undef_hook(regs, instr) == 0)
                return;

die_sig:
#ifdef CONFIG_DEBUG_USER
        if (user_debug & UDBG_UNDEFINED) {
                pr_info("%s (%d): undefined instruction: pc=%p\n",
                        current->comm, task_pid_nr(current), pc);
                __show_regs(regs);
                dump_instr(KERN_INFO, regs);
        }
#endif
        arm_notify_die("Oops - undefined instruction", regs, 
                       SIGILL, ILL_ILLOPC, pc, 0, 6);
}
NOKPROBE_SYMBOL(do_undefinstr)

undefined 훅에 등록된 명령인 경우 정상적으로 함수를 리턴한다. 만일 등록되지 않은 경우 다음과 같이 처리한다.

  • 유저 모드인 경우 task의 kill 처리를 위한 시그널을 요청
  • 유저 모드가 아닌(커널) 경우 시스템을 die 처리한다.

 

  • 코드 라인 7에서 스택에 백업한 pt_regs 구조체의 pc 주소 값을 대입한다.
  • 코드 라인 9~20에서 exception 되기 전의 모드가 svc 모드가 아닌 경우 __mem_to_opcode_arm() 함수를 사용하여 명령어 값을 가져온다.
  • 코드 라인 32~36에서 exception 되기 전의 모드가 svc 모드가 아닌 경우 __mem_to_opcode_arm() 함수를 사용하여 명령어 값을 가져오되 단 usr 모드인 경우 die_sig 레이블로 이동한다.
  • 코드 라인 38~39에서 undefined hook이 설치된 경우 인스트럭션과 모드 등을 비교하여 해당 훅 함수를 호출한다. 호출 결과가 성공(0)인 경우 함수를 빠져나간다.
  • 코드 라인 41~51에서 “Oops – undefined instruction” 메시지를 출력하며 die() 함수를 호출한다.
    • 커널인 경우 시스템 die, 유저 모드인 경우 태스크만 die 처리한다.

 

Undefined Hook

call_undef_hook()

arch/arm/kernel/traps.c

static int call_undef_hook(struct pt_regs *regs, unsigned int instr)
{
        struct undef_hook *hook;
        unsigned long flags;
        int (*fn)(struct pt_regs *regs, unsigned int instr) = NULL;

        raw_spin_lock_irqsave(&undef_lock, flags);
        list_for_each_entry(hook, &undef_hook, node)
                if ((instr & hook->instr_mask) == hook->instr_val &&
                    (regs->ARM_cpsr & hook->cpsr_mask) == hook->cpsr_val)
                        fn = hook->fn;
        raw_spin_unlock_irqrestore(&undef_lock, flags);

        return fn ? fn(regs, instr) : 1;
}

undefined hook이 설치된 경우 인스트럭션과 모드 등을 비교하여 해당 훅 함수를 호출한다.  성공=0, 매칭된 훅 함수가 없거나 실패=1

  • register_undef_hook() 함수를 통해 hook이 추가된다.
  • CONFIG_TLS_REG_EMUL 커널 옵션을 사용하는 경우 SMP를 사용하는 ARMv6+ 이전 아키텍처를 위해 TLS 에뮬레이션 목적으로 hook를 하나 설치한다.
    • late_initcall(arm_mrc_hook_init); -> get_tp_trap() 함수

 


Die 처리

arm_notify_die()

arch/arm/kernel/traps.c

void arm_notify_die(const char *str, struct pt_regs *regs,
                int signo, int si_code, void __user *addr,
                unsigned long err, unsigned long trap)
{
        if (user_mode(regs)) {
                current->thread.error_code = err;
                current->thread.trap_no = trap;

                force_sig_info(signo, si_code, addr);
        } else {
                die(str, regs, err);
        }
}

exception 되기 전의 모드가 유저 모드인 경우 태스크에 signal을 보내 해당 태스크만 죽이고 커널 모드인 경우 die() 함수를 호출한다.

 

참고

 

Kernel-provided User Helpers

 

커널 메모리의 코드 및 데이터 일부를 유저가 직접 호출 또는 접근할 수 있도록 유일하게 허용한 페이지이다.

  • Posix syscall을 사용하지 않고 직접 유저 공간에서 호출하므로 매우 빠른 속도가 요구되는 코드를 수행할 수 있다.
  • 하이 벡터를 사용하는 프로세서에서 CONFIG_KUSER_HELPERS 커널 옵션을 사용하여 제공한다.
    • 로우 벡터에서는 kuser helper 코드를 지원하지 않는다.
  • arm에서 가상 주소 0xffff_0000로 시작하는 페이지를 사용하는 하이 벡터 페이지의 사용하지 않는 윗 부분을 이용한다.
    • 현재 0xffff_0f60 ~ 0xffff0fff 까지 범위에서 사용하고 있다.

 

코드 위치

현재 커널은 하이벡터 페이지의 가장 윗 부분을 이용하여 다음 4개의 함수와 1개의 상수 값을 제공한다.

  • __kuser_cmpxchg64()
  • __kuser_memory_barrier()
  • __kuser_cmpxchg()
  • __kuser_get_tls()
  • __kuser_helper_version 상수

 

코드가 위치한 주소는 다음 그림과 같다.

 

Kernel provided User Helper 버전 확인

가상 주소: 0xffff_0ffc

 

사용 방법

#define __kuser_helper_version (*(int32_t *)0xffff0ffc)

void check_kuser_version(void)
{
        if (__kuser_helper_version < 2) {
                fprintf(stderr, "can't do atomic operations, kernel too old\n");
                abort();
        }
}

버전 값이 2보다 작은 경우 커널이 너무 오래되어서 atomic operation을 지원하지 않는 것을 알 수 있다.

 

__kuser_cmpxchg64() 함수

가상 주소: 0xffff_0f60

 

사용방법

typedef int (__kuser_cmpxchg64_t)(const int64_t *oldval,
                                  const int64_t *newval,
                                  volatile int64_t *ptr);
#define __kuser_cmpxchg64 (*(__kuser_cmpxchg64_t *)0xffff0f60)

int64_t atomic_add64(volatile int64_t *ptr, int64_t val)
{
        int64_t old, new;

        do {
                old = *ptr;
                new = old + val;
        } while(__kuser_cmpxchg64(&old, &new, ptr));

        return new;
}

64bit 값이 담긴 ptr 포인터 주소에 val 값을 atomic하게 더한다.

  • __kuser_cmpxchg64() 함수는 64bit 현재 값이 oldval과 같은 경우 newval을 atomic 하게 저장하고 성공리에 0을 반환한다. 그 외의 값은 oldval과 newval이 달라 저장하지 않은 경우이다.

 

 3가지 구현  방법

Kernel-provided User Helper code 중 kuser_cmpxchg64() 함수는 시스템에 따라 3 가지의 구현 중 하나를 사용한다.

  1. fastpath
    • ARMv6K를 포함하여 이후 버전의 ARM 아키텍처는 user space에서 직접 cmpxchg 및 cmpxchg64 atomic operation을 수행할 수 있다.
      • ARM SMP 시스템에서 사용하는 atomic operation
        • ARMv6: swp
        • ARMv7: ldrex/strex이 권장 사용되며, swp는 호환목적으로 s/w emulation 방법을 사용하여 구현되어 있다.(비권장)
  2. slowpath for SMP
    • fastpath를 지원하지 않는 SMP 아키텍처를 사용하는 경우 CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션을 사용하여 arm syscall을 사용하여 커널로 진입하여 64bit cmpxchg atomic operation을 수행하는 방법이다.
  3. slowpath for UP
    • UP 시스템에서 atomic을 포기하고 순서대로 진행하는 방법으로 인터럽트가 커널에서 호출되는 경우에 한하여 fixup을 수행하여 보강한다.

 

NEEDS_SYSCALL_FOR_CMPXCHG

  • arm 아키텍처로 집중해서 이 커널 옵션을 보면 ARMv6 이전 SMP 프로세서(아직은 이렇게 만든 SoC가 없는 것 같다. 하지만 미래에 어떤 회사가 만들지 모르는 법이므로…)는 직접 user space에서 atomic하게 cmpxchg를 수행할 수 없다. 따라서 느리더라도 POSIX call을 사용하여 커널에 진입한 후 처리하도록 이 방법을 사용하여야 한다.

 

__kuser_cmpxchg64:

user space에서 64 bit atomic operation을 처리하기 위해 이러한 기능에 대한 아키텍처의 지원 여부에 따라 구현 방법을 3가지로 달리 구현되었다.

1) 커널에 위탁 처리하는 방법

arch/arm/kernel/entry-armv.S

/*
 * Due to the length of some sequences, __kuser_cmpxchg64 spans 2 regular
 * kuser "slots", therefore 0xffff0f80 is not used as a valid entry point.
 */

__kuser_cmpxchg64:                              @ 0xffff0f60

#if defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

        /*
         * Poor you.  No fast solution possible...
         * The kernel itself must perform the operation.
         * A special ghost syscall is used for that (see traps.c).
         */
        stmfd   sp!, {r7, lr}
        ldr     r7, 1f                  @ it's 20 bits
        swi     __ARM_NR_cmpxchg64
        ldmfd   sp!, {r7, pc}
1:      .word   __ARM_NR_cmpxchg64

CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션은 주로 user space에서 atomic operation을 지원하지 못하는 SMP 아키텍처에서 사용한다. atomic operation이 필요한 경우 커널은 인터럽트되지 않도록 블럭한 후 처리할 수 있기 때문에 user space에서의 kuser_cmpxchg64 요청은 arm syscall을 사용하여 커널에 위탁하여 처리하고 그 결과를 내려보내주게 구현되었다.

  • 코드 라인 15에서 r7, lr 레지스터 사용해야 하므로 잠시 스택에 보관한다.
  • 코드 라인 16~17에서 r7에 __ARM_NR_cmpxchg64에 해당하는 arm syscall 넘버를 대입한 후 swi swi 명령을 사용하여 arm syscall 호출을 수행한다.
  • 코드 라인 18에서 스택으로 부터 r7을 복구하고, lr 주소로 jump 한다.

 

2) atomic이 지원되는 아키텍처에서 직접 수행하는 방법

#elif defined(CONFIG_CPU_32v6K)

        stmfd   sp!, {r4, r5, r6, r7}
        ldrd    r4, r5, [r0]                    @ load old val
        ldrd    r6, r7, [r1]                    @ load new val
        smp_dmb arm
1:      ldrexd  r0, r1, [r2]                    @ load current val
        eors    r3, r0, r4                      @ compare with oldval (1)
        eoreqs  r3, r1, r5                      @ compare with oldval (2)
        strexdeq r3, r6, r7, [r2]               @ store newval if eq
        teqeq   r3, #1                          @ success?
        beq     1b                              @ if no then retry
        smp_dmb arm
        rsbs    r0, r3, #0                      @ set returned val and C flag
        ldmfd   sp!, {r4, r5, r6, r7}
        usr_ret lr

CONFIG_CPU_32v6K는 user space에서도 atomic operation을 사용할 수 있는 armv6 이상의 SMP 아키텍처에서 사용하는 커널 옵션이다. 이러한 경우 user space에서 직접 ldrex 및 strex를 사용하여 atomic operation을 수행할 수 있다.

  • 코드 라인 3에서 r4~r7까지 레지스터를 스택에 백업한다.
  • 코드 라인 4에서 r0(old)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r4와 r5 레지스터에 저장한다.
    • ldr r4, [r0]; ldr r5, [r0, #4]와 동일한 결과다.
  • 코드 라인 5에서 r1(new)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r6와 r7 레지스터에 저장한다.
  • 코드 라인 6에서 다음 ldrexd를 호출하기 전에 dmb를 사용하여 메모리에 대해 order 문제가 생기지 않도록 한다.
  • 코드 라인 7에서 r2(ptr)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r0와 r1 레지스터에  저장한다.
  • 코드 라인 8~10에서 읽은 ptr 값(r0, r1)과 old 값(r4, r5)가 비교를 하여 같은 경우 r2 주소에 new(r6, r7) 값을 저장하되 결과를 r3에 저장한다.
  • 코드 라인 11~12에서 저장 시 이 캐시 라인이 exclusive되지 않아 결과(r3)이 실패(#1)한 경우 다시 레이블 1로 이동하여 성공할 때까지 반복한다.
    • 다른 cpu에서 이 exclusive한 캐시라인에 접근하는 경우 open되어 strex 명령을 수행 시 실패를 얻게된다.
  • 코드 라인 13에서 다시 메모리 배리어를 사용하여 order 문제가 생기지 않도록 한다.
  • 코드 라인 14에서 0에서 r3(0=기록한 경우, 그 외=oldval과 newval이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 15~16에서 스택에 백업해둔 레지스터들을 복구하고 lr 주소로 복귀한다.

 

3) UP 시스템에서 atomic 구현

#elif !defined(CONFIG_SMP)

#ifdef CONFIG_MMU

        /*
         * The only thing that can break atomicity in this cmpxchg64
         * implementation is either an IRQ or a data abort exception
         * causing another process/thread to be scheduled in the middle of
         * the critical sequence.  The same strategy as for cmpxchg is used.
         */
        stmfd   sp!, {r4, r5, r6, lr}
        ldmia   r0, {r4, r5}                    @ load old val
        ldmia   r1, {r6, lr}                    @ load new val
1:      ldmia   r2, {r0, r1}                    @ load current val
        eors    r3, r0, r4                      @ compare with oldval (1)
        eoreqs  r3, r1, r5                      @ compare with oldval (2)
2:      stmeqia r2, {r6, lr}                    @ store newval if eq
        rsbs    r0, r3, #0                      @ set return val and C flag
        ldmfd   sp!, {r4, r5, r6, pc}

        .text
kuser_cmpxchg64_fixup:
        @ Called from kuser_cmpxchg_fixup.
        @ r4 = address of interrupted insn (must be preserved).
        @ sp = saved regs. r7 and r8 are clobbered.
        @ 1b = first critical insn, 2b = last critical insn.
        @ If r4 >= 1b and r4 <= 2b then saved pc_usr is set to 1b.
        mov     r7, #0xffff0fff
        sub     r7, r7, #(0xffff0fff - (0xffff0f60 + (1b - __kuser_cmpxchg64)))
        subs    r8, r4, r7
        rsbcss  r8, r8, #(2b - 1b)
        strcs   r7, [sp, #S_PC]
#if __LINUX_ARM_ARCH__ < 6
        bcc     kuser_cmpxchg32_fixup
#endif
        ret     lr
        .previous
#endif

        kuser_pad __kuser_cmpxchg64, 64

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg64 함수의 기능 구현은 atomic과 관련 없이 단순하게 구현되어 있다. 그 구현 루틴 아래에 위치한 kuser_cmpxchg64_fixup 루틴을 살펴보기 전에는 그 어떠한 atomic 관련한 부가 루틴도 찾아 볼 수가 없다. atomic에 대한 보장을 위해 자세한 것은 kuser_cmpxchg64_fixup 레이블에서 설명하기로 한다.

  • 코드 라인 11에서 r4, r5, r6, lr 레지스터를 스택에 백업해둔다.
  • 코드 라인 12에서 r0위치에 있는 64bit old 값을 r4와 r5 레지스터에 로드한다.
  • 코드 라인 13에서 r1 위치에 있는 64bit new 값을 r6와 lr 레지스터에 로드한다.
  • 코드 라인 14에서 r2 위치에 있는 64bit ptr 값을 r0와 r1 레지스터에 로드한다.
  • 코드 라인 15~17에서 old 값과 ptr 값을 비교하여 같은 경우 new 값을 ptr에 저장한다.
  • 코드 라인 18에서 0에서 r3(0=기록한 경우, 그 외=old 값과 new 값이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 19에서 스택에 백업해둔 레지스터들을 복구하고 lr 주소로 복귀한다.
kuser_cmpxchg64_fixup:

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg64 루틴을 수행 시 atomic하게 처리해야 하는 구간 즉, 레이블 1과 레이블 2 사이를 수행하는 도중에 irq, fiq, dabt 등을 만나게 되는 경우 해당 exception을 처리한 후 되돌아갈 주소를 atomic 구간의 가장 윗 부분으로 바꾸기 위해 스택의 pt_regs의 pc를 조작하는 방법을 사용한다.

  • 코드 라인 28~29에서 atomic operation이 시작되는 레이블 1:의 가상 주소 값을 알아온다.
    • r7 <- ffff0f60 + (1b – __kuser_cmpxchg64)를 대입하고 싶지만 operand에 사용하는 값의 크기 제한으로 인해 두 개의 명령을 사용하였다.
  • 코드 라인 30~32에서 인터럽트 되었을 때의 pc 주소가 담긴 r4 레지스터 값이 atomic 하게 처리할 구간 범위(레이블 1: ~ 레이블 2:)인 경우 스택에 저장해 둔 pt_regs 구조체 중 pc 위치에 레이블 1 주소를 저장하여 다시 인터럽트 복귀 시 atomic operation을 다시 시도하게 변경한다.
  • 코드 라인 33~35에서 arm 아키텍처가 버전 6보다 이전인 경우에는 위의 atomic opeation 수행 도중 인터럽트 된 것이 아니라면  kuser_cmpxchg32 명령 수행 도중에 발생한 일인지 확인하여 역시 같은 방식으로 복귀 주소를 변경하게 한다.

 

__kuser_memory_barrier:

arch/arm/kernel/entry-armv.S

__kuser_memory_barrier:                         @ 0xffff0fa0
        smp_dmb arm
        usr_ret lr

        kuser_pad __kuser_memory_barrier, 32

SMP 시스템에서 메모리 배리어를 사용하여 이전 로직의 메모리 액세스와 연관하여 order 문제가 생기지 않도록 막는다.

 

__kuser_cmpxchg:

user space에서 32 bit atomic operation을 처리하기 위해 이러한 기능에 대한 아키텍처의 지원 여부에 따라 구현 방법을 3가지로 달리 구현되었다.

1) 커널에 위탁 처리하는 방법

arch/arm/kernel/entry-armv.S

__kuser_cmpxchg:                                @ 0xffff0fc0

#if defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

        /*
         * Poor you.  No fast solution possible...
         * The kernel itself must perform the operation.
         * A special ghost syscall is used for that (see traps.c).
         */
        stmfd   sp!, {r7, lr}
        ldr     r7, 1f                  @ it's 20 bits
        swi     __ARM_NR_cmpxchg
        ldmfd   sp!, {r7, pc}
1:      .word   __ARM_NR_cmpxchg

CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션은 주로 user space에서 atomic operation을 지원하지 못하는 SMP 아키텍처에서 사용한다. atomic operation이 필요한 경우 커널은 인터럽트되지 않도록 블럭한 후 처리할 수 있기 때문에 user space에서의 kuser_cmpxchg 요청은 arm syscall을 사용하여 커널에 위탁하여 처리하고 그 결과를 내려보내주게 구현되었다.

  • 코드 라인 10에서 r7, lr 레지스터 사용해야 하므로 잠시 스택에 보관한다.
  • 코드 라인 11~12에서 r7에 __ARM_NR_cmpxchg64에 해당하는 arm syscall 넘버를 대입한 후 swi swi 명령을 사용하여 arm syscall 호출을 수행한다.
  • 코드 라인 13에서 스택으로 부터 r7을 복구하고, lr 주소로 jump 한다.

 

2) UP 시스템에서 atomic 구현

#elif __LINUX_ARM_ARCH__ < 6

#ifdef CONFIG_MMU

        /*
         * The only thing that can break atomicity in this cmpxchg
         * implementation is either an IRQ or a data abort exception
         * causing another process/thread to be scheduled in the middle
         * of the critical sequence.  To prevent this, code is added to
         * the IRQ and data abort exception handlers to set the pc back
         * to the beginning of the critical section if it is found to be
         * within that critical section (see kuser_cmpxchg_fixup).
         */
1:      ldr     r3, [r2]                        @ load current val
        subs    r3, r3, r0                      @ compare with oldval
2:      streq   r1, [r2]                        @ store newval if eq
        rsbs    r0, r3, #0                      @ set return val and C flag
        usr_ret lr

        .text
kuser_cmpxchg32_fixup:
        @ Called from kuser_cmpxchg_check macro.
        @ r4 = address of interrupted insn (must be preserved).
        @ sp = saved regs. r7 and r8 are clobbered.
        @ 1b = first critical insn, 2b = last critical insn.
        @ If r4 >= 1b and r4 <= 2b then saved pc_usr is set to 1b.
        mov     r7, #0xffff0fff
        sub     r7, r7, #(0xffff0fff - (0xffff0fc0 + (1b - __kuser_cmpxchg)))
        subs    r8, r4, r7
        rsbcss  r8, r8, #(2b - 1b)
        strcs   r7, [sp, #S_PC]
        ret     lr
        .previous

#else
#warning "NPTL on non MMU needs fixing"
        mov     r0, #-1
        adds    r0, r0, #0
        usr_ret lr
#endif

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg 함수의 기능 구현은 atomic과 관련 없이 단순하게 구현되어 있다. 그 구현 루틴 아래에 위치한 kuser_cmpxchg_fixup 루틴을 살펴보기 전에는 그 어떠한 atomic 관련한 부가 루틴도 찾아 볼 수가 없다. atomic에 대한 보장을 위해 자세한 것은 kuser_cmpxchg_fixup 레이블에서 설명하기로 한다.

  • 코드 라인 14에서 r2(ptr)위치에 있는 값을 r3 레지스터에 로드한다.
  • 코드 라인 15~16에서 r0(old) 레지스터 값과 r3 레지스터 값을 비교하여 같은 경우 r1(new) 레지스터 값을 r2(ptr) 레지스터가 가리키는 주소에 저장한다.
  • 코드 라인 17~18은 0에서 r3(0=기록한 경우, 그 외=old 값과 new 값이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담고 lr 주소로 복귀한다.
kuser_cmpxchg_fixup:

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg 루틴을 수행 시 atomic하게 처리해야 하는 구간 즉, 레이블 1과 레이블 2 사이를 수행하는 도중에 irq, fiq, dabt 등을 만나게 되는 경우 해당 exception을 처리한 후 되돌아갈 주소를 atomic 구간의 가장 윗 부분으로 바꾸기 위해 스택의 pt_regs의 pc를 조작하는 방법을 사용한다.

  • 코드 라인 27~28에서 atomic operation이 시작되는 레이블 1:의 가상 주소 값을 알아온다.
    • r7 <- ffff0fc0 + (1b – __kuser_cmpxchg)를 대입하고 싶지만 operand에 사용하는 값의 크기 제한으로 인해 두 개의 명령을 사용하였다.
  • 코드 라인 29~32에서 인터럽트 되었을 때의 pc 주소가 담긴 r4 레지스터 값이 atomic 하게 처리할 구간 범위(레이블 1: ~ 레이블 2:)인 경우 스택에 저장해 둔 pt_regs 구조체 중 pc 위치에 레이블 1 주소를 저장하여 다시 인터럽트 복귀 시 atomic operation을 다시 처음 부터 시도하게 변경하고 lr 주소로 복귀한다.

 

3) atomic이 지원되는 아키텍처에서 직접 수행하는 방법

#else

        smp_dmb arm
1:      ldrex   r3, [r2]
        subs    r3, r3, r0
        strexeq r3, r1, [r2]
        teqeq   r3, #1
        beq     1b
        rsbs    r0, r3, #0
        /* beware -- each __kuser slot must be 8 instructions max */
        ALT_SMP(b       __kuser_memory_barrier)
        ALT_UP(usr_ret  lr)

#endif

        kuser_pad __kuser_cmpxchg, 32

user space에서도 atomic operation을 사용할 수 있는 armv6 이상의 SMP 아키텍처에서 사용하는 커널 옵션이다. 이러한 경우 user space에서 직접 ldrex 및 strex를 사용하여 atomic operation을 수행할 수 있다.

  • 코드 라인 3에서 메모리 배리어를 사용하여 이전 로직의 메모리 액세스와 연관하여 order 문제가 생기지 않도록 막는다.
  • 코드 라인 4~6에서 r2(ptr)가 가리키는 주소에서 값을 읽은 값을 r3 레지스터에 저장한다. 이 값과 r0(old) 값을 비교하여 같은 경우 r1(new) 값을 r2(ptr)가 가리키는 주소에 저장한다. 저장 결과는 r3에 담는다. (성공=0, 실패=1)
  • 코드 라인 7~8에서 저장 시 이 캐시 라인이 exclusive되지 않아 결과(r3)이 실패(#1)한 경우 다시 레이블 1로 이동하여 성공할 때까지 반복한다.
    • 다른 cpu에서 이 exclusive한 캐시라인에 접근하는 경우 open되어 strex 명령을 수행 시 실패를 얻게된다.
  • 코드 라인 9는 0에서 r3(0=기록한 경우, 그 외=oldval과 newval이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 11~12에서 복귀를 하되 SMP 시스템인 경우 다시 메모리 배리어를 사용하여 order 문제가 생기지 않도록 한다.

 

__kuser_get_tls:

arch/arm/kernel/entry-armv.S

__kuser_get_tls:                        @ 0xffff0fe0
        ldr     r0, [pc, #(16 - 8)]     @ read TLS, set in kuser_get_tls_init
        usr_ret lr
        mrc     p15, 0, r0, c13, c0, 3  @ 0xffff0fe8 hardware TLS code
        kuser_pad __kuser_get_tls, 16
        .rep    3
        .word   0                       @ 0xffff0ff0 software TLS value, then
        .endr

TLS(Thread Local Storage) 값을 알아온다.  2가지의 구현을 사용한다.

  • S/W TLS
    • 0xffff_0ff0 주소에 TLS 값을 보관해두고 그 값을 읽어 사용한다.
  • H/W TLS
    • user 에서 읽기만 가능한 TPIDRURO 레지스터에서 값을 읽어 사용한다.

 

  • 코드 라인 2~3에서 0xfff_0ff0 위치에서 TLS 값을 가져와 r0 레지스터에 담고 lr 주소로 복귀한다.
  • 코드 라인 4에는 TPIDRURO 레지스터에서 값을 읽어 r0 레지스터에 담는 코드를 두었다.
    • H/W TLS 레지스터를 지원하는 경우 이 코드를 복사하여 코드 라인 2의 주소인 0xffff_0fe0에 복사하기 위해 사용된다.
      • setup_arch() -> paging_init() -> devicemaps_init() -> early_trap_init() -> kuser_init() 함수 내부에 다음 코드를 찾을 수 있다.
        • memcpy(vectors + 0xfe0, vectors + 0xfe8, 4);

 

매크로 함수

usr_ret 매크로

arch/arm/kernel/entry-armv.S

        .macro  usr_ret, reg
#ifdef CONFIG_ARM_THUMB
        bx      \reg
#else
        ret     \reg
#endif
        .endm

\reg 주소로 복귀한다.

 

kuser_pad 매크로

arch/arm/kernel/entry-armv.S

        .macro  kuser_pad, sym, size
        .if     (. - \sym) & 3
        .rept   4 - (. - \sym) & 3
        .byte   0
        .endr
        .endif
        .rept   (\size - (. - \sym)) / 4
        .word   0xe7fddef1
        .endr
        .endm

현재 주소 위치 – sym 주소가 4바이트 단위로 정렬되지 않은 경우 정렬을 위해 0으로 채운다.(0~3 바이트) 그 이후 sym 주소부터 size 만큼의 공간 중 현재 위치부터 0xe7fddef1 값으로 채운다.

  • 예) kuser_pad __kuser_memory_barrier, 32
    • __kuser_memory_barrier 함수 위치 부터 32 바이트 공간내에서 빈 자리를 0xe7fddef1 값으로 채운다.
      • 0xffff0fa0: 0xf57ff05b 0xe12fff1e 0xe7fddef1 0xe7fddef1
      • 0xffff0fb0: 0xe7fddef1 0xe7fddef1 0xe7fddef1 0xe7fddef1

 

참고