[PATCH v5 14/15] arm64: kernel: Add support for hibernate/suspend-to-disk

Lorenzo Pieralisi lorenzo.pieralisi at arm.com
Thu Feb 18 09:13:36 PST 2016


On Tue, Feb 16, 2016 at 03:49:26PM +0000, James Morse wrote:
> Add support for hibernate/suspend-to-disk.
> 
> Suspend borrows code from cpu_suspend() to write cpu state onto the stack,
> before calling swsusp_save() to save the memory image.
> 
> Restore creates a set of temporary page tables, covering only the
> linear map, copies the restore code to a 'safe' page, then uses the copy to
> restore the memory image. The copied code executes in the lower half of the
> address space, and once complete, restores the original kernel's page
> tables. It then calls into cpu_resume(), and follows the normal
> cpu_suspend() path back into the suspend code.
> 
> To restore a kernel using KASLR, the address of the page tables, and
> cpu_resume() are stored in the hibernate arch-header and the el2
> vectors are pivotted via the 'safe' page in low memory. This also permits
> us to resume using a different version of the kernel to the version that
> hibernated, but because the MMU isn't turned off during resume, the
> MMU settings must be the same between both kernels. To ensure this, the
> value of the translation control register (TCR_EL1) is also included in the
> hibernate arch-header, this means your resume kernel must have the same
> page size, and virtual address space size.
> 
> Signed-off-by: James Morse <james.morse at arm.com>
> Tested-by: Kevin Hilman <khilman at baylibre.com> # Tested on Juno R2
> ---

[...]

> +/*
> + * el2_setup() moves lr into elr and erets. Copy lr to another register to
> + * preserve it.
> + *
> + * int swsusp_el2_setup_helper(phys_addr_t el2_setup_pa);
> + */
> +ENTRY(swsusp_el2_setup_helper)
> +	mov	x16, x30
> +	hvc	#0
> +	mov	x30, x16
> +	ret
> +ENDPROC(swsusp_el2_setup_helper)
> +
> +/*
> + * Restore the hyp stub. Once we know where in memory el2_setup is, we
> + * can use it to re-initialise el2. This must be done before the hibernate
> + * page is unmapped.
> + *
> + * x0: The physical address of el2_setup __pa(el2_setup)
> + */
> +el1_sync:
> +	mov	x1, x0
> +	mrs	lr, elr_el2
> +	mrs	x0, sctlr_el1

You may make el2_setup return straight to swsusp_el2_setup_helper caller,
but anyway, I think you can do something even simpler.

You can stash el2_setup physical address (and a flag to check if you
have to reboot HYP or not) in the hibernate header, then, before
jumping to the _cpu_resume address you store in the header you restore
HYP by calling el2_setup_phys and you are done with that, this saves
you an extra call in C.

[...]

> +int swsusp_arch_suspend(void)
> +{
> +	int ret = 0;
> +	unsigned long flags;
> +	struct sleep_stack_data state;
> +
> +	local_dbg_save(flags);
> +
> +	if (__cpu_suspend_enter(&state)) {
> +		ret = swsusp_save();
> +	} else {
> +		void *lm_kernel_start;
> +
> +		/* Clean kernel to PoC for secondary core startup */
> +		lm_kernel_start = phys_to_virt(virt_to_phys(KERNEL_START));
> +		__flush_dcache_area(lm_kernel_start, KERNEL_END - KERNEL_START);
> +
> +		/* Reconfigure EL2 */
> +		if (is_hyp_mode_available())
> +			swsusp_el2_setup_helper(virt_to_phys(el2_setup));

I am referring to this code here, I think it is nicer to restore el2
before getting here anyway as I say above, you do know you have to call
el2_setup anyway, stash the data/address you need in the hibernate header
and just execute it in the code page set aside to resume from hibernate
(ie swsusp_arch_suspend_exit).

Thanks !
Lorenzo

> +
> +		/*
> +		 * Tell the hibernation core that we've just restored
> +		 * the memory
> +		 */
> +		in_suspend = 0;
> +
> +		__cpu_suspend_exit();
> +	}
> +
> +	local_dbg_restore(flags);
> +
> +	return ret;
> +}
> +
> +static int copy_pte(pmd_t *dst_pmd, pmd_t *src_pmd, unsigned long start,
> +		    unsigned long end)
> +{
> +	unsigned long next;
> +	unsigned long addr = start;
> +	pte_t *src_pte = pte_offset_kernel(src_pmd, start);
> +	pte_t *dst_pte = pte_offset_kernel(dst_pmd, start);
> +
> +	do {
> +		next = addr + PAGE_SIZE;
> +		if (pte_val(*src_pte))
> +			set_pte(dst_pte,
> +				__pte(pte_val(*src_pte) & ~PTE_RDONLY));
> +	} while (dst_pte++, src_pte++, addr = next, addr != end);
> +
> +	return 0;
> +}
> +
> +static int copy_pmd(pud_t *dst_pud, pud_t *src_pud, unsigned long start,
> +		    unsigned long end)
> +{
> +	int rc = 0;
> +	pte_t *dst_pte;
> +	unsigned long next;
> +	unsigned long addr = start;
> +	pmd_t *src_pmd = pmd_offset(src_pud, start);
> +	pmd_t *dst_pmd = pmd_offset(dst_pud, start);
> +
> +	do {
> +		next = pmd_addr_end(addr, end);
> +		if (!pmd_val(*src_pmd))
> +			continue;
> +
> +		if (pmd_table(*(src_pmd))) {
> +			dst_pte = (pte_t *)get_safe_page(GFP_ATOMIC);
> +			if (!dst_pte) {
> +				rc = -ENOMEM;
> +				break;
> +			}
> +
> +			set_pmd(dst_pmd, __pmd(virt_to_phys(dst_pte)
> +					       | PMD_TYPE_TABLE));
> +
> +			rc = copy_pte(dst_pmd, src_pmd, addr, next);
> +			if (rc)
> +				break;
> +		} else {
> +			set_pmd(dst_pmd,
> +				__pmd(pmd_val(*src_pmd) & ~PMD_SECT_RDONLY));
> +		}
> +	} while (dst_pmd++, src_pmd++, addr = next, addr != end);
> +
> +	return rc;
> +}
> +
> +static int copy_pud(pgd_t *dst_pgd, pgd_t *src_pgd, unsigned long start,
> +		    unsigned long end)
> +{
> +	int rc = 0;
> +	pmd_t *dst_pmd;
> +	unsigned long next;
> +	unsigned long addr = start;
> +	pud_t *src_pud = pud_offset(src_pgd, start);
> +	pud_t *dst_pud = pud_offset(dst_pgd, start);
> +
> +	do {
> +		next = pud_addr_end(addr, end);
> +		if (!pud_val(*src_pud))
> +			continue;
> +
> +		if (pud_table(*(src_pud))) {
> +			if (PTRS_PER_PMD != 1) {
> +				dst_pmd = (pmd_t *)get_safe_page(GFP_ATOMIC);
> +				if (!dst_pmd) {
> +					rc = -ENOMEM;
> +					break;
> +				}
> +
> +				set_pud(dst_pud, __pud(virt_to_phys(dst_pmd)
> +						       | PUD_TYPE_TABLE));
> +			}
> +
> +			rc = copy_pmd(dst_pud, src_pud, addr, next);
> +			if (rc)
> +				break;
> +		} else {
> +			set_pud(dst_pud,
> +				__pud(pud_val(*src_pud) & ~PMD_SECT_RDONLY));
> +		}
> +	} while (dst_pud++, src_pud++, addr = next, addr != end);
> +
> +	return rc;
> +}
> +
> +static int copy_page_tables(pgd_t *dst_pgd, unsigned long start,
> +			    unsigned long end)
> +{
> +	int rc = 0;
> +	pud_t *dst_pud;
> +	unsigned long next;
> +	unsigned long addr = start;
> +	pgd_t *src_pgd = pgd_offset_k(start);
> +
> +	dst_pgd += pgd_index(start);
> +
> +	do {
> +		next = pgd_addr_end(addr, end);
> +		if (!pgd_val(*src_pgd))
> +			continue;
> +
> +		if (PTRS_PER_PUD != 1) {
> +			dst_pud = (pud_t *)get_safe_page(GFP_ATOMIC);
> +			if (!dst_pud) {
> +				rc = -ENOMEM;
> +				break;
> +			}
> +
> +			set_pgd(dst_pgd, __pgd(virt_to_phys(dst_pud)
> +					       | PUD_TYPE_TABLE));
> +		}
> +
> +		rc = copy_pud(dst_pgd, src_pgd, addr, next);
> +		if (rc)
> +			break;
> +	} while (dst_pgd++, src_pgd++, addr = next, addr != end);
> +
> +	return rc;
> +}
> +
> +/*
> + * Setup then Resume from the hibernate image using swsusp_arch_suspend_exit().
> + *
> + * Memory allocated by get_safe_page() will be dealt with by the hibernate code,
> + * we don't need to free it here.
> + */
> +int swsusp_arch_resume(void)
> +{
> +	int rc = 0;
> +	size_t exit_size;
> +	pgd_t *tmp_pg_dir;
> +	void *lm_restore_pblist;
> +	phys_addr_t phys_hibernate_exit;
> +	void __noreturn (*hibernate_exit)(phys_addr_t, phys_addr_t, void *, void *);
> +
> +	/*
> +	 * Copy swsusp_arch_suspend_exit() to a safe page. This will generate
> +	 * a new set of ttbr0 page tables and load them.
> +	 */
> +	exit_size = __hibernate_exit_text_end - __hibernate_exit_text_start;
> +	rc = create_safe_exec_page(__hibernate_exit_text_start, exit_size,
> +				   (void **)&hibernate_exit,
> +				   &phys_hibernate_exit,
> +				   get_safe_page, GFP_ATOMIC);
> +	if (rc) {
> +		pr_err("Failed to create safe executable page for hibernate_exit code.");
> +		goto out;
> +	}
> +
> +	/*
> +	 * The hibernate exit text contains a set of el2 vectors, that will
> +	 * be executed at el2 with the mmu off in order to reload hyp-stub.
> +	 */
> +	__flush_dcache_area(hibernate_exit, exit_size);
> +
> +	/*
> +	 * Restoring the memory image will overwrite the ttbr1 page tables.
> +	 * Create a second copy of just the linear map, and use this when
> +	 * restoring.
> +	 */
> +	tmp_pg_dir = (pgd_t *)get_safe_page(GFP_ATOMIC);
> +	if (!tmp_pg_dir) {
> +		pr_err("Failed to allocate memory for temporary page tables.");
> +		rc = -ENOMEM;
> +		goto out;
> +	}
> +	rc = copy_page_tables(tmp_pg_dir, PAGE_OFFSET, 0);
> +	if (rc)
> +		goto out;
> +
> +	/*
> +	 * Since we only copied the linear map, we need to find restore_pblist's
> +	 * linear map address.
> +	 */
> +	lm_restore_pblist = phys_to_virt(virt_to_phys(restore_pblist));
> +
> +	/*
> +	 * Both KASLR and restoring with a different kernel version will cause
> +	 * the el2 vectors to be in a different location in the resumed kernel.
> +	 * Load hibernate's temporary copy into el2.
> +	 */
> +	if (is_hyp_mode_available()) {
> +		phys_addr_t el2_vectors = phys_hibernate_exit;  /* base */
> +		el2_vectors += hibernate_el2_vectors -
> +			       __hibernate_exit_text_start;     /* offset */
> +
> +		__hyp_set_vectors(el2_vectors);
> +	}
> +
> +	hibernate_exit(virt_to_phys(tmp_pg_dir), resume_hdr.ttbr1_el1,
> +		       resume_hdr.reenter_kernel, lm_restore_pblist);
> +
> +out:
> +	return rc;
> +}
> diff --git a/arch/arm64/kernel/vmlinux.lds.S b/arch/arm64/kernel/vmlinux.lds.S
> index 282e3e64a17e..02184c39f7e2 100644
> --- a/arch/arm64/kernel/vmlinux.lds.S
> +++ b/arch/arm64/kernel/vmlinux.lds.S
> @@ -46,6 +46,16 @@ jiffies = jiffies_64;
>  	*(.idmap.text)					\
>  	VMLINUX_SYMBOL(__idmap_text_end) = .;
>  
> +#ifdef CONFIG_HIBERNATION
> +#define HIBERNATE_TEXT					\
> +	. = ALIGN(SZ_4K);				\
> +	VMLINUX_SYMBOL(__hibernate_exit_text_start) = .;\
> +	*(.hibernate_exit.text)				\
> +	VMLINUX_SYMBOL(__hibernate_exit_text_end) = .;
> +#else
> +#define HIBERNATE_TEXT
> +#endif
> +
>  /*
>   * The size of the PE/COFF section that covers the kernel image, which
>   * runs from stext to _edata, must be a round multiple of the PE/COFF
> @@ -107,6 +117,7 @@ SECTIONS
>  			LOCK_TEXT
>  			HYPERVISOR_TEXT
>  			IDMAP_TEXT
> +			HIBERNATE_TEXT
>  			*(.fixup)
>  			*(.gnu.warning)
>  		. = ALIGN(16);
> @@ -182,6 +193,10 @@ ASSERT(__hyp_idmap_text_end - (__hyp_idmap_text_start & ~(SZ_4K - 1)) <= SZ_4K,
>  	"HYP init code too big or misaligned")
>  ASSERT(__idmap_text_end - (__idmap_text_start & ~(SZ_4K - 1)) <= SZ_4K,
>  	"ID map text too big or misaligned")
> +#ifdef CONFIG_HIBERNATION
> +ASSERT(__hibernate_exit_text_end - (__hibernate_exit_text_start & ~(SZ_4K - 1))
> +	<= SZ_4K, "Hibernate exit text too big or misaligned")
> +#endif
>  
>  /*
>   * If padding is applied before .head.text, virt<->phys conversions will fail.
> -- 
> 2.6.2
> 



More information about the linux-arm-kernel mailing list