“写时复制”的概念已经不算陌生了,它大大节省了新进程需要的内存和产生新进程所需的时间,但是有一个美中不足的地方,如果允许内核随意写用户空间地址,哪怕是写保护的地址,那么写时复制将很难实现,因为内核必须向用户空间写东西,比如read调用的结果就是从内核的页高速缓存中复制过来的;如果用户空间向一个写保护的页面写数据,那么将会产生缺页异常,而在fork的时候,父进程的空间将全部设置为写保护来和子进程共享,然后子进程或父进程中的任意一个对地址空间进行写操作时,将会产生一个写保护缺页异常,而缺页异常处理程序会复制页面到一个新分配的页面,这就是写时复制,为了让内核写用户写保护页面也 能产生一个写保护缺页异常,有两种实现方案,一个基于硬件,一个基于软件,还好,现代处理器很多都提供了这样的功能,比如intel的处理器的cr0的 wp位就是干这个的,如果置1就使能用户空间写保护,这样内核在写用户空间的写保护页面时会产生页面异常,进而写保护缺页异常处理会处理一切,正如上面所 述,分配新页面,拷贝旧页面到新页面,如果wp位置0则不使能用户空间写保护,要实现完全的用户空间写保护必须通过软件实现,那么怎么实现呢?实际上我们想一下就明白,这个实现并不算难,毕竟不用加很多代码,只要在内核往用户空间写数据的点上放一个关卡就可以了,如果要写的是写保护页面,则分配新页面,复 制旧页面到新页面,若不是,直接拷贝,用memspy就行,于是我们来看一下哪里需要写用户空间,无非就是put_user/copy_to...之类的函数,而这些函数最终都要调用__copy_to_user_ll,下面看一下__copy_to_user_ll(内核版本2.6.9):
unsigned long __copy_to_user_ll(void __user *to, const void *from, unsigned long n)
{
#ifndef CONFIG_X86_WP_WORKS_OK //我一向喜欢忽略条件编译,但是这次不能忽略了
if (unlikely(boot_cpu_data.wp_works_ok == 0) &&((unsigned long )to) < TASK_SIZE)
{//wp_works_ok在硬件使能用户空间写保护的时候置为1,否则为0,-1为未确定,如果if为真,那么肯定就是硬件没有使能用户空间写保护了,看看unlikely就知道,这个判断为真的可能性比较小,说明硬件一般都提供了硬件用户空间写保护实现
while (n) { //按照大小循环,直到拷贝完毕
unsigned long offset = ((unsigned long)to)%PAGE_SIZE;
unsigned long len = PAGE_SIZE - offset;
int retval;
struct page *pg;
void *maddr;
if (len > n)
len = n;
survive:
down_read(¤t->mm->mmap_sem);
retval = get_user_pages(current, current->mm, (unsigned long )to, 1, 1, 0, &pg, NULL); //get_user_pages会处理页面写保护问题,如果get的是一个写保护页面,那么它会调用handle_mm_fault来新分配一个新页面,否则返回这个得到的页面。
if (retval == -ENOMEM && current->pid == 1) {
up_read(¤t->mm->mmap_sem);
blk_congestion_wait(WRITE, HZ/50);
goto survive;
}
if (retval != 1) {
up_read(¤t->mm->mmap_sem);
break;
}
maddr = kmap_atomic(pg, KM_USER0);//将得到的老页面(对于非写保护)或者新分配的页面(对于写保护页面)映射进内核的临时映射空间
memcpy(maddr + offset, from, len);//拷贝数据
kunmap_atomic(maddr, KM_USER0);//解映射
set_page_dirty_lock(pg);
put_page(pg);
up_read(¤t->mm->mmap_sem);
from += len;
to += len;
n -= len;
}
return n;
}
#endif
if (movsl_is_ok(to, from, n)) // 如果定义了CONFIG_X86_WP_WORKS_OK或者没有定义CONFIG_X86_WP_WORKS_OK宏但是硬件实现了用户空间写保护,那 么就交给硬件来做吧,以下的俩__copy_系列函数都是汇编函数,它们可以在写写保护页面的时候触发页面异常。还有一种情况就是定义 了 CONFIG_X86_WP_WORKS_OK宏但是硬件没有实现,那么很简单,函数会使内核oops的。
__copy_user(to, from, n);
else
n = __copy_user_intel(to, from, n);
return n;
}
总听到有人问内核为何不用memcpy来拷贝给用户数据,这下明白了吧,内核空间的执行绪虽然很有特权,但是遇到硬件还得让步,硬件才是老大,硬件不允许内核访问用户空间的只写页面那他就不能访问。其实看看__copy_user和memcpy张的确实挺像,只是前者增加了一个异常表,这个异常表就涉及到另一个知识了,看看copy_from/to_user的开头,只是简单验证了用户空间的地址是否逾越了内核界限并没有验证它是否在进程的虚拟内存空间,也就是没有验证进程的vma中是否包括这个要读/写的地址,这样就把真正的验证交给了硬件的缺页中断处理,毕竟这是小概率事件,不能为了使小概率事件不发生每次在copy_from/to_user的时候都确保地址在进程的vma中,这样太消耗资源,因此就将小概率事件交给了实际发生缺页时的缺页中断来处理,当进程的vma内没有找到该地址时,就会进入异常处理来执行善后工作,默认就是退出当前进程。