我们常用/dev/zero读0写空,它是如何实现的,我们通过问题一探究竟
*内核代码/dev/zero
*用户程序可以通过open & mmap将/dev/zero映射到一个虚拟的内存空间;在读这段虚拟地址时 会发生page_fault,这种page_fault如何处理的?会申请新的内存吗?
1、先看/dev/zero的mmap在内核如何处理的
static int mmap_zero(struct file *file, struct vm_area_struct *vma)
{
vma_set_anonymous(vma);
}
static inline void vma_set_anonymous(struct vm_area_struct *vma)
{
vma->vm_ops = NULL;
}
内核处理非常简单,只是将这段虚拟地址的vma->vm_ops置null,即访问/dev/zero的page_fault要走anon(即匿名页)的处理;
神奇吧,本来是文件页的处理 却走的是匿名页的流程。
2、再看page_fault时 的处理
arm64的异常处理:el1_abort-->do_mem_abort-->do_translation_fault-->do_page_fault
走到common的处理:handle_mm_fault-->__handle_mm_fault-->handle_pte_fault
{
...
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf);
...
}
由于mmap /dev/zero时已经vma_set_anonymous,这里page_fault走到匿名页的处理
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
...
/* Use the zero-page for reads */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vmf->vma_page_prot));
...
}
可以看到,用户态的虚拟地址 都被映射到到了一个特殊的page上,即zero_pfn;随后的访问都是读这个zero_page;
3、什么是zero_page
zero_page是linux的COW(copy-on-write)的基础,也是/dev/zero等初始化成0的基础;
arm64的定义:
/* Empty_zero_page is a special page that is used for zero-initialized data
* and COW.
*/
unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_bss;
EXPORT_SYMBOL(empty_zero_page);
/*
* ZERO_PAGE is a global shared page that is always zero: used
* for zero-mapped memory areas etc..
*/
extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)];
#define ZERO_PAGE(vaddr) phys_to_page(__pa_symbol(empty_zero_page))
static int __init init_zero_pfn(void)
{
zero_pfn = page_to_pfn(ZERO_PAGE(0));
return 0;
}
early_initcall(init_zero_pfn);
static inline unsigned long my_zero_pfn(unsigned long addr)
{
extern unsigned long zero_pfn;
return zero_pfn;
}
# cat /proc/kallsyms |grep empty_zero_page
ffffffdb674fe000 B empty_zero_page
内核定义了一个静态数组empty_zero_page,大小一个page(即4K),且地址需要4K对齐(否则虚拟地址映射时、需要搞2个pte和其映射);早期内核是通过memblock reserve了一块4K大小的内存做zero_page。
COW机制简单点说,就是用户程序申请的内存 在只读的情况下、都只会和这个zero_page建立映射、内核并不会为其申请的内存,而发生write时、内核才会申请内存 copy相关内容过来、并建立映射。
zero_page机制节省了很多内存。
4、回答提出的问题,open & mmap将/dev/zero后并读这段虚拟地址时 会发生page_fault,走的是匿名页的处理流程,并且不会申请内存、只会和zero_page建立映射。
*再提出一个问题,用户程序通过open & read时,为什么读到的都是0?
1、这个和zero_page就没什么关系了,看看内核如何处理read_zero的:
static ssize_t read_zero(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
size_t cleared = 0;
while (count) {
size_t chunk = min_t(size_t, count, PAGE_SIZE);
size_t left;
left = clear_user(buf + cleared, chunk);
if (unlikely(left)) {
cleared += (chunk - left);
if (!cleared)
return -EFAULT;
break;
}
cleared += chunk;
count -= chunk;
if (signal_pending(current))
break;
cond_resched();
}
return cleared;
}
2、内核read_zero会将用户的buf通过clear_user直接清零,就是这么简单。
static inline unsigned long __must_check __clear_user(void __user *to, unsigned long n)
{
if (access_ok(to, n)) {
uaccess_enable_not_uao();
n = __arch_clear_user(__uaccess_mask_ptr(to), n);
uaccess_disable_not_uao();
}
return n;
}
#define clear_user __clear_user
SYM_FUNC_START(__arch_clear_user)
mov x2, x1 // save the size for fixup return
subs x1, x1, #8
b.mi 2f
1:
uao_user_alternative 9f, str, sttr, xzr, x0, 8
subs x1, x1, #8
b.pl 1b
2: adds x1, x1, #4
b.mi 3f
uao_user_alternative 9f, str, sttr, wzr, x0, 4
sub x1, x1, #4
3: adds x1, x1, #2
b.mi 4f
uao_user_alternative 9f, strh, sttrh, wzr, x0, 2
sub x1, x1, #2
4: adds x1, x1, #1
b.mi 5f
uao_user_alternative 9f, strb, sttrb, wzr, x0, 0
5: mov x0, #0
ret
SYM_FUNC_END(__arch_clear_user)
EXPORT_SYMBOL(__arch_clear_user)
.macro uao_user_alternative l, inst, alt_inst, reg, addr, post_inc
8888: \alt_inst \reg, [\addr];
add \addr, \addr, \post_inc;
这里提一点,由于安全问题、内核态是不允许直接访问用户态地址的(当然 更不允许用户态访问内核态地址),arm64是通过SET_PSTATE_PAN(privilege-accsee-non)禁止的,如要访问需要需要先enbale、访问结束再disable;
static inline void uaccess_enable_not_uao(void)
{
__uaccess_enable(ARM64_ALT_PAN_NOT_UAO);
}