我的话:copy_from_user在内核函数的合适位置会主动调用,一般发生在用户态向核心态传递指针的时候。 

最近研究NetBSD,借助于TUN/TAP驱动程序,能够实现在一个系统中,创建一个虚拟网卡,来实施诸如Open×××、VTun等的功能。

那么,TUN/TAP驱动如何实现在内核空间和用户空间的数据拷贝呢?主要就靠的是这两个函数copy_to_user和copy_from_user。

Copy_from_user函数详细分析
copy_from_user函数的目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0.

这么简单的一个函数却含盖了许多关于内核方面的知识,比如内核关于异常出错的处理.从用户空间拷贝
数据到内核中时必须非常小心,如果用户空间的数据地址是个非法的地址,或是超出用户空间的范围,或是
那些地址还没有被映射到,都可能对内核产生很大的影响,如oops,或者被造成系统安全的影响.所以
copy_from_user函数的功能就不只是从用户空间拷贝数据那样简单了,它还要做一些指针检查以及处理这些
问题的方法.下面我们来仔细分析下这个函数.函数原型在[arch/i386/lib/usercopy.c]中

unsigned long
copy_from_user(void *to, const void __user *from, unsigned long n)
{
might_sleep(); 
if (access_ok(VERIFY_READ, from, n))
       n = __copy_from_user(to, from, n);
else
       memset(to, 0, n);
return n;
}

首先这个函数是可以睡眠的,它调用might_sleep()来处理,它在include/linux/kernel.h中定义,
本质也就是调用schedule(),转到其他进程.接下来就要验证用户空间地址的有效性.它在
[/include/asm-i386/uaccess.h]中定义.
#define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0)),进一步调用__rang_ok
函数来处理,它所做的测试很简单,就是比较addr+size这个地址的大小是否超出了用户进程空间的大小,
也就是0xbfffffff.可能有读者会问,只做地址范围检查,怎么不做指针合法性的检查呢,如果出现前面
提到过的问题怎么办?这个会在下面的函数中处理,我们慢慢看.在做完地址范围检查后,如果成功则调用
__copy_from_user函数开始拷贝数据了,如果失败的话,就把从to指针指向的内核空间地址到to+size范围
填充为0.__copy_from_user也在uaceess.h中定义,
static inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
   might_sleep();
   return __copy_from_user_inatomic(to, from, n);
}
这里继续调用__copy_from_user_inatomic.
static inline unsigned long
__copy_from_user_inatomic(void *to, const void __user *from, unsigned long n)
{
if (__builtin_constant_p(n)) {
       unsigned long ret;

       switch (n) {
       case 1:
         __get_user_size(*(u8 *)to, from, 1, ret, 1);
         return ret;
       case 2:
         __get_user_size(*(u16 *)to, from, 2, ret, 2);
         return ret;
       case 4:
         __get_user_size(*(u32 *)to, from, 4, ret, 4);
         return ret;
       }
}
return __copy_from_user_ll(to, from, n);
}
这里先判断要拷贝的字节大小,如果是8,16,32大小的话,则调用__get_user_size来拷贝数据.
这样做是一种程序设计上的优化了。
#define __get_user_size(x,ptr,size,retval,errret)          \
do {                                  \
retval = 0;                         \
__chk_user_ptr(ptr);                      \
switch (size) {                         \
case 1: __get_user_asm(x,ptr,retval,"b","b","=q",errret);break; \
case 2: __get_user_asm(x,ptr,retval,"w","w","=r",errret);break; \
case 4: __get_user_asm(x,ptr,retval,"l","","=r",errret);break; \
default: (x) = __get_user_bad();             \
}                                \
} while (0)

#define __get_user_asm(x, addr, err, itype, rtype, ltype, errret) \
__asm__ __volatile__(                      \
       "1: mov"itype" %2,%"rtype"1\n"          \
       "2:\n"                         \
       ".section .fixup,\"ax\"\n"             \
       "3: movl %3,%0\n"                    \
       " xor"itype" %"rtype"1,%"rtype"1\n"        \
       " jmp 2b\n"                    \
       ".previous\n"                      \
       ".section __ex_table,\"a\"\n"             \
       " .align 4\n"                    \
       " .long 1b,3b\n"                    \
       ".previous"                      \
       : "=r"(err), ltype (x)                    \
       : "m"(__m(addr)), "i"(errret), "0"(err))
实际上在完成一些宏的转换后,也就是利用movb,movw,movl指令传输数据了,对于
内嵌汇编中的.section .fixup, .section __ex_table,我们呆会要仔细讲。
如果不是那些特殊大小时,则调用__copy_from_user_ll处理。

unsigned long
__copy_from_user_ll(void *to, const void __user *from, unsigned long n)
{
if (movsl_is_ok(to, from, n))
       __copy_user_zeroing(to, from, n);
else
       n = __copy_user_zeroing_intel(to, from, n);
return n;
}

直接调用__copy_user_zeroing开始真正的拷贝数据了,绕了那么多弯,总算快看到
出路了。copy_from_user函数的精华部分也就都在这了。

#define __copy_user_zeroing(to,from,size)             \
do {                                  \
int __d0, __d1, __d2;                      \
__asm__ __volatile__(                      \
       " cmp   $7,%0\n"                    \
       " jbe   1f\n"                    \
       " movl %1,%0\n"                    \
       " negl %0\n"                    \
       " andl $7,%0\n"                    \
       " subl %0,%3\n"                    \
       "4: rep; movsb\n"                    \
       " movl %3,%0\n"                    \
       " shrl $2,%0\n"                    \
       " andl $3,%3\n"                    \
       " .align 2,0x90\n"             \
       "0: rep; movsl\n"                    \
       " movl %3,%0\n"                    \
       "1: rep; movsb\n"                    \
       "2:\n"                         \
       ".section .fixup,\"ax\"\n"             \
       "5: addl %3,%0\n"                    \
       " jmp 6f\n"                    \
       "3: lea 0(%3,%0,4),%0\n"             \
       "6: pushl %0\n"                    \
       " pushl %%eax\n"                    \
       " xorl %%eax,%%eax\n"             \
       " rep; stosb\n"                    \
       " popl %%eax\n"                    \
       " popl %0\n"                    \
       " jmp 2b\n"                    \
       ".previous\n"                      \
       ".section __ex_table,\"a\"\n"             \
       " .align 4\n"                    \
       " .long 4b,5b\n"                    \
       " .long 0b,3b\n"                    \
       " .long 1b,6b\n"                    \
       ".previous"                      \
       : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2) \
       : "3"(size), "0"(size), "1"(to), "2"(from)        \
       : "memory");                      \
} while (0)

这个函数的前一部分比较简单,也就是拷贝数据.关于后一部分就会涉及到我们前面
提到过的那些情况了,如果用户空间的地址没被映射怎么办呢?在一些老的内核版本
中是用verify_area()来验证地址地址合法性的,比如在早期的linux 0.11内核.

[linux0.11/kenrel/fork.c]
// 进程空间写前验证函数。在现代CPU中,其控制寄存器CR0有个写保护标志位(wp:16),内核可以通过设置
// 该位来禁止特权级0的代码向用户空间只读页面执行写数据,否则将导致写保护异常。
// addr为内存物理地址
void verify_area(void * addr,int size)
{
       unsigned long start;

       start = (unsigned long) addr;
       size += start & 0xfff;   // start & 0xfff为起始地址addr在页面中的偏移,2^12=4096
       start &= 0xfffff000; // start为页开始地址,即页面边界值。此时start为当前进程空间中的逻辑地址
       start += get_base(current->ldt[2]); // get_base(current->ldt[2])为进程数据段在线性地址空间中的开始地址,在加上start,变为系统这个线性空间中的地址

             页边界       addr ----size-----     页边界
       +--------------------------------------------------------+
       |   ... | start&0xfff |             |    |   ... |
       +--------------------------------------------------------+
                |           start          |
            start-----------size-------------

      while (size>0) {
            size -= 4096;
            write_verify(start); // 以页为单位,进行写保护验证,如果页为只读,则将其变为可写
            start += 4096;
       }
}