知識は未だ霧の中

スパコン眼鏡NET。HPCのことはあまり書かない。

Linux Kernel 4.4 の新機能 "mlock2"

この記事は Linux Advent Calendar 2015 - Qiita の 24日目の記事です。

みなさんUbuntuつかってますか?

次期LTS版である、Ubuntu16.04では、Linux Kernel 4.4を利用することが検討されています。*1

来年から5年間をUbuntu16.04とともに過ごすであろうというあなたのために、今回はLinux Kernel 4.4の新機能のひとつmlock2システムコールについてざっくりとまとめてみたいとおもいます。

* Kernel4.4関連の面白い新機能というと、AMDの新しいAPUおよびGPUのサポートやRaspberry Pi KMS ドライバの追加、AVX2を利用したSHA1/SHA256の高速化などなどあるのですが、そのへんはまたそのうち。


目次

1. mlock / mlock2システムコールとは

まずはmlockの解説。

int mlock(const void *addr, size_t len);

mlockでは、仮想アドレスとそこからのサイズを渡すことによって、そのアドレス空間の含まれるページが スワップされなくなります。

そして、その空間を再びスワップするように戻すのにはmunlockというシステムコールが使われます。まあその他関連システムコールとしてmlockallとかmunlockallとかもあります。


さて、今回追加された mlock2は以下のようになっています。

int mlock2(const void *addr, size_t len, int flags);

アドレス空間の指定はmlockと変わらず、それにflagsという変数が追加された形となっています。

このflagsに現在指定可能な定数はMLOCK_LOCKEDとMLOCK_ONFAULTのふたつです。

従来と同じく、MLOCK_LOCKEDを指定した場合、指定したアドレス空間のページは直ちにメモリ上に固定されます。一方、MLOCK_ONFAULTを指定した場合、ページは初回のページフォールト後、メモリに固定されます。

これにより、利用する物理メモリ上の最大メモリが大きい一方、必要とされている固定されたメモリが小さいことが多いといった用途(暗号化など)においては、ページの固定にかかるコストを削減することが可能となります。

2. mlock, mlock2の実装

mlockとmlock2はそれぞれ以下のように実装されています。

linux/mm/mlock.c

mlock

SYSCALL_DEFINE2(mlock, unsigned long, start, size_t, len) 
{ 
return do_mlock(start, len, VM_LOCKED); 
}

mlock2

SYSCALL_DEFINE3(mlock2, unsigned long, start, size_t, len, int, flags) 
{ 
vm_flags_t vm_flags = VM_LOCKED; 


if (flags & ~MLOCK_ONFAULT) 
return -EINVAL; 

if (flags & MLOCK_ONFAULT) 
vm_flags |= VM_LOCKONFAULT; 

return do_mlock(start, len, vm_flags); 
}

どうやら、mlock2では、MLOCK_ONFAULTフラグが立っているときは、do_mlockにVM_LOCKONFAULTというフラグを追加で渡しているようです。

ではdo_mlockはどういった実装になっているのでしょうか?

static int do_mlock(unsigned long start, size_t len, vm_flags_t flags) 
{ 
unsigned long locked; 
unsigned long lock_limit; 
int error = -ENOMEM; 

if (!can_do_mlock()) 
return -EPERM; 

lru_add_drain_all(); /* flush pagevec */ 

len = PAGE_ALIGN(len + (offset_in_page(start))); 
start &= PAGE_MASK; 

lock_limit = rlimit(RLIMIT_MEMLOCK); 
lock_limit >>= PAGE_SHIFT; 
locked = len >> PAGE_SHIFT; 

down_write(&current->mm->mmap_sem); 

locked += current->mm->locked_vm; 

/* check against resource limits */ 
if ((locked <= lock_limit) || capable(CAP_IPC_LOCK)) 
error = apply_vma_lock_flags(start, len, flags); 

up_write(&current->mm->mmap_sem); 
if (error) 
return error; 

error = __mm_populate(start, len, 0); 
if (error) 
return __mlock_posix_error_return(error); 
return 0; 
}

いろいろ調べたり前処理を行い、apply_vma_lock_flagsに処理を渡しています。

static int apply_vma_lock_flags(unsigned long start, size_t len,
				vm_flags_t flags)
{
	unsigned long nstart, end, tmp;
	struct vm_area_struct * vma, * prev;
	int error;

	VM_BUG_ON(offset_in_page(start));
	VM_BUG_ON(len != PAGE_ALIGN(len));
	end = start + len;
	if (end < start)
		return -EINVAL;
	if (end == start)
		return 0;
	vma = find_vma(current->mm, start);
	if (!vma || vma->vm_start > start)
		return -ENOMEM;

	prev = vma->vm_prev;
	if (start > vma->vm_start)
		prev = vma;

	for (nstart = start ; ; ) {
		vm_flags_t newflags = vma->vm_flags & VM_LOCKED_CLEAR_MASK;

		newflags |= flags;

		/* Here we know that  vma->vm_start <= nstart < vma->vm_end. */
		tmp = vma->vm_end;
		if (tmp > end)
			tmp = end;
		error = mlock_fixup(vma, &prev, nstart, tmp, newflags);
		if (error)
			break;
		nstart = tmp;
		if (nstart < prev->vm_end)
			nstart = prev->vm_end;
		if (nstart >= end)
			break;

		vma = prev->vm_next;
		if (!vma || vma->vm_start != nstart) {
			error = -ENOMEM;
			break;
		}
	}
	return error;
}

ここでもまたいろいろと作業を行っていますが、本命はmlock_fixupのようです。

static int mlock_fixup(struct vm_area_struct *vma, struct vm_area_struct **prev,
	unsigned long start, unsigned long end, vm_flags_t newflags)
{
	struct mm_struct *mm = vma->vm_mm;
	pgoff_t pgoff;
	int nr_pages;
	int ret = 0;
	int lock = !!(newflags & VM_LOCKED);

	if (newflags == vma->vm_flags || (vma->vm_flags & VM_SPECIAL) ||
	    is_vm_hugetlb_page(vma) || vma == get_gate_vma(current->mm))
		/* don't set VM_LOCKED or VM_LOCKONFAULT and don't count */
		goto out;

	pgoff = vma->vm_pgoff + ((start - vma->vm_start) >> PAGE_SHIFT);
	*prev = vma_merge(mm, *prev, start, end, newflags, vma->anon_vma,
			  vma->vm_file, pgoff, vma_policy(vma),
			  vma->vm_userfaultfd_ctx);
	if (*prev) {
		vma = *prev;
		goto success;
	}

	if (start != vma->vm_start) {
		ret = split_vma(mm, vma, start, 1);
		if (ret)
			goto out;
	}

	if (end != vma->vm_end) {
		ret = split_vma(mm, vma, end, 0);
		if (ret)
			goto out;
	}

success:
	/*
	 * Keep track of amount of locked VM.
	 */
	nr_pages = (end - start) >> PAGE_SHIFT;
	if (!lock)
		nr_pages = -nr_pages;
	mm->locked_vm += nr_pages;

	/*
	 * vm_flags is protected by the mmap_sem held in write mode.
	 * It's okay if try_to_unmap_one unmaps a page just after we
	 * set VM_LOCKED, populate_vma_page_range will bring it back.
	 */

	if (lock)
		vma->vm_flags = newflags;
	else
		munlock_vma_pages_range(vma, start, end);

out:
	*prev = vma;
	return ret;
}

ということで、メモリリージョンにVM_LOCKONFAULTのフラグを設定していることがわかりました。(以前はアンマップ不可を意味するVM_RESERVEDであったものがVM_LOCKONFAULTに名前を変え使われている模様です。)



さて、次に問題となるのはこのVM_LOCKONFAULTがどの場面で使われているのか?ということです。

この答えはpopulate_vma_page_rangeにあります。

linux/mm/gup.c

long populate_vma_page_range(struct vm_area_struct *vma,
		unsigned long start, unsigned long end, int *nonblocking)
{
	struct mm_struct *mm = vma->vm_mm;
	unsigned long nr_pages = (end - start) / PAGE_SIZE;
	int gup_flags;

	VM_BUG_ON(start & ~PAGE_MASK);
	VM_BUG_ON(end   & ~PAGE_MASK);
	VM_BUG_ON_VMA(start < vma->vm_start, vma);
	VM_BUG_ON_VMA(end   > vma->vm_end, vma);
	VM_BUG_ON_MM(!rwsem_is_locked(&mm->mmap_sem), mm);

	gup_flags = FOLL_TOUCH | FOLL_POPULATE | FOLL_MLOCK;
	if (vma->vm_flags & VM_LOCKONFAULT)
		gup_flags &= ~FOLL_POPULATE;

	/*
	 * We want to touch writable mappings with a write fault in order
	 * to break COW, except for shared mappings because these don't COW
	 * and we would not want to dirty them for nothing.
	 */
	if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)
		gup_flags |= FOLL_WRITE;

	/*
	 * We want mlock to succeed for regions that have any permissions
	 * other than PROT_NONE.
	 */
	if (vma->vm_flags & (VM_READ | VM_WRITE | VM_EXEC))
		gup_flags |= FOLL_FORCE;

	/*
	 * We made sure addr is within a VMA, so the following will
	 * not result in a stack expansion that recurses back here.
	 */
	return __get_user_pages(current, mm, start, nr_pages, gup_flags,
				NULL, NULL, nonblocking);
}

上記のようにメモリの配置の際、確保するユーザーページのFOLL_POPULATEフラグを削除します。


すると、ユーザーページが不足していた際に、呼ばれることとなる以下のfaultin_pageの処理がスキップされるようになります。

static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
		unsigned long address, unsigned int *flags, int *nonblocking)
{
	struct mm_struct *mm = vma->vm_mm;
	unsigned int fault_flags = 0;
	int ret;

	/* mlock all present pages, but do not fault in new pages */
	if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)
		return -ENOENT;
	/* For mm_populate(), just skip the stack guard page. */
	if ((*flags & FOLL_POPULATE) &&
			(stack_guard_page_start(vma, address) ||
			 stack_guard_page_end(vma, address + PAGE_SIZE)))
		return -ENOENT;
	if (*flags & FOLL_WRITE)
		fault_flags |= FAULT_FLAG_WRITE;
	if (nonblocking)
		fault_flags |= FAULT_FLAG_ALLOW_RETRY;
	if (*flags & FOLL_NOWAIT)
		fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
	if (*flags & FOLL_TRIED) {
		VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);
		fault_flags |= FAULT_FLAG_TRIED;
	}

	ret = handle_mm_fault(mm, vma, address, fault_flags);
	if (ret & VM_FAULT_ERROR) {
		if (ret & VM_FAULT_OOM)
			return -ENOMEM;
		if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))
			return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;
		if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))
			return -EFAULT;
		BUG();
	}

	if (tsk) {
		if (ret & VM_FAULT_MAJOR)
			tsk->maj_flt++;
		else
			tsk->min_flt++;
	}

	if (ret & VM_FAULT_RETRY) {
		if (nonblocking)
			*nonblocking = 0;
		return -EBUSY;
	}

	/*
	 * The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
	 * necessary, even if maybe_mkwrite decided not to set pte_write. We
	 * can thus safely do subsequent page lookups as if they were reads.
	 * But only do so when looping for pte_write is futile: in some cases
	 * userspace may also be wanting to write to the gotten user page,
	 * which a read fault here might prevent (a readonly page might get
	 * reCOWed by userspace write).
	 */
	if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
		*flags &= ~FOLL_WRITE;
	return 0;
}


以上の処理によってmlock2は実装されています。

3. 備考

mlock2関連の関数・変数の名称は開発中に一度変更がなされているようです。
またそのうち変更が加わるかもしれません。

4. まとめ

暗号系のソフトを開発している人はこのシステムコールを使ってみてはいかがでしょうか?