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" \
      ...
      : "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;
}
}
[linux0.11/mm/memory.c]
// 验证线性地址是否可写
void write_verify(unsigned long address)
{
unsigned long page;
// 假如对应页表为空的话,直接返回
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
return;
page &= 0xfffff000;
page += ((address>>10) & 0xffc);
// 经过运算后page为页表项的内容,指向实际的一页物理地址
if ((3 & *(unsigned long *) page) == 1) // 验证页面是否可写,不可写则执行un_wp_page,取消写保护.
un_wp_page((unsigned long *) page);
return;
}
但是假如每次在用户空间复制数据时,都要做这种检查是很浪费时间的,毕竟坏指针是很少
存在的,在新内核中的做法是,在从用户空间复制数据时,取消验证指针合法性的检查,
只多地址范围的检查,就象access_ok()所做的那样,一但碰上了坏指针,就要页异常出错处理
程式去处理他了.我们去看看do_page_fault函数.
[arch/asm-i386/mm/fault.c/do_page_falut()]
fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
...
if (!down_read_trylock(&mm->mmap_sem)) {
if ((error_code & 4) == 0 &&
!search_exception_tables(regs->eip))
goto bad_area_nosemaphore;
down_read(&mm->mmap_sem);
}
...
   if (fixup_exception(regs))
return;
...
}
error_code保存的是出错码,(error_code & 4) == 0代表产生异常的原因是在内核中.
他调用fixup_exception(regs)来处理这个问题.既然出错了,那么如何来修复他呢?
先看下fixup_exception()函数的实现:
[arch/asm-i386/mm/extable.c]
int fixup_exception(struct pt_regs *regs)
{
const struct exception_table_entry *fixup;
...
fixup = search_exception_tables(regs->eip);
if (fixup) {
regs->eip = fixup->fixup;
return 1;
}
...
}
[kernel/extable.c]
const struct exception_table_entry *search_exception_tables(unsigned long addr)
{
const struct exception_table_entry *e;
e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
if (!e)
e = search_module_extables(addr);
return e;
}
[/lib/extable.c]
const struct exception_table_entry *
search_extable(const struct exception_table_entry *first,
const struct exception_table_entry *last,
unsigned long value)
{
while (first insn insn > value)
last = mid - 1;
else
return mid;
}
return NULL;
}
在内核中有个异常出错地址表,在地址表中有个出错地址的修复地址也气对应,他结构如下:
[/include/asm-i386/uaccess.h]
struct exception_table_entry
{
unsigned long insn, fixup;
};
insn是产生异常指令的地址,fixup用来修复出错地址的地址,也就是当异常发生后,用他的
地址来替换异常指令发生的地址。__copy_user_zeroing中的.section __ex_table代表异常出错
地址表的地址,.section .fixup代表修复的地址。他们都是elf文档格式中的2个特别节。
".section __ex_table,\"a\"\n" \
" .align 4\n" \
" .long 4b,5b\n" \
" .long 0b,3b\n" \
" .long 1b,6b\n" 
4b,5b的意思是当出错地址在4b标号对应的地址上时,就转入5b标号对应的地址去接着运行,
也就是修复的地址。依次类推。所以理解这一点后,fixup_exception()函数就很容易看明白了
就是根据出错地址搜索异常地址表,找到对应的修复地址,跳转到那里去执行就ok了。
ok,到这里copy_from_user函数也就分析完了,假如有什么不明白的话,能够通过阅读
/usr/src/linux/Documentation/exception.txt来得到更多关于异常处理方面的知识。copy_from_user的使用,有一个前提:
1) 当前进程必须未锁定from所在的page,
或者,
2)from所在的page已经up_to_data,并且page -> count多余一个引用。
否则,如果from所在的page不在影射中,则缺页异常处理程序会搜索/新增这个page,在page未up_to_data时,要求锁定这个page,然后提交IO读page。
如 ( 当前进程已锁定本page ) && (page未up_to_data)成立,则死锁。
那么,在generic_file_write中,因to所在的page必须被当前进程锁定,则当(from所在page == to所在page)时,只能用第二种保证办法。
kernel好象并没有这样做,而只是在锁定to所在page之前,另from所在page为up_to_data,但并没有增加任何多余引用

COPY_FROM_USER 详解相关推荐

  1. Android-Binder进程间通讯机制-多图详解

    本系列: Android-Binder进程间通讯机制-多图详解 一次Binder通信最大可以传输多大的数据?​​​​​​​ 关于Binder (AIDL)的 oneway 机制 概述 最近在学习Bin ...

  2. Linux IO复用区别与epoll详解

    转载:http://blog.csdn.net/hacker00011000/article/details/52160590 一.select.poll.epoll之间的区别总结[整理]  sele ...

  3. android4.0网络编程配书源码_linux网络编程之epoll源码重要部分详解

    一.epoll相关的数据结构 最重要的两个数据结构是红黑树和就绪链表,红黑树用于管理所有的文件描述符fd,就绪链表用于保存有事件发生的文件描述符. 当向系统中添加一个fd时,就创建一个epitem结构 ...

  4. linux 驱动器发送信号,Linux设备驱动并发控制详解(自旋锁,信号量)

    转发:Linux设备驱动并发控制详解(自旋锁,信号量) 作者:jinhaijun 提交日期:2008-3-12 14:08:00 | 分类: | 访问量:144 link:http://www.emb ...

  5. Android BINDER详解

    1.   进程间通信的本质(2个进程) 用户空间的进程如果想相互通信, 必须经过内核, 因为不同进程的用户地址空间是独立的, 但是共享同一个内核空间. 内核为了支持进程间通信, 一般会有一个驱动, 以 ...

  6. imx6ul 驱动详解

    链表的知识: struct list_head {struct list_head *next, *prev; }; API函数 函数 功能 LIST_HEAD 声明并初始化双向链表. INIT_LI ...

  7. 【epoll】epoll使用详解(精髓)--研读和修正

    目录 epoll 和select epoll的接口 如何来使用epoll epoll程序框架 伪代码: 示例代码 大致流程 实例源码 相关知识 Socket的阻塞模式和非阻塞模式 如何动态的改变lis ...

  8. Linux系统调用详解(实现机制分析)

    为什么需要系统调用   linux内核中设置了一组用于实现系统功能的子程序,称为系统调用.系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户 ...

  9. php调用linux摄像头,Linux_Linux中开发USB摄像头驱动详解,USB摄像头以其良好的性能和低 - phpStudy...

    Linux中开发USB摄像头驱动详解 USB摄像头以其良好的性能和低廉的价格得到广泛应用.同时因其灵活.方便的特性,易于集成到嵌入式系统中.但是如果使用现有的符合Video for Linux标准的驱 ...

最新文章

  1. 微信小程序-canvas绘制文字实现自动换行
  2. 【PM模块】外包服务、工作清场管理、预防性维护
  3. 浅析Python中深拷贝和浅拷贝
  4. Deeplearnng.AI第四部分第一周、卷积神经网络
  5. 仓库中应用的计算机设备有哪些,智能仓储设备系统中有哪些常见的应用工具
  6. HDU5853 Jong Hyok and String(二分 + 后缀数组)
  7. 部署项目的问题(一)—— vue工程打包上线样式错乱问题
  8. C语言模拟实现标准库函数之qsort() 2
  9. android ble 实现自动连接,Android:自动重新连接BLE设备
  10. python遍历数组冒泡排序_Python算法(一) 数组冒泡排序(难度等级:easy)
  11. ref:PHP反序列化漏洞成因及漏洞挖掘技巧与案例
  12. bat写的自动部署脚本
  13. 86. php 绘图体系(2)
  14. 辽宁专科php教材用什么,辽宁新高考改革方案的具体内容是什么?
  15. unknown source怎么解决?unknown source是什么意思【详解】
  16. 多传感器融合用卡尔曼滤波的话也逃不开状态方程观测方程
  17. win10锁屏壁纸路径
  18. 史上最简SLAM零基础解读(7) - Jacobian matrix(雅可比矩阵) → 理论分析与应用详解(Bundle Adjustment)
  19. onkeydown基本用法
  20. CentOS 7 不显示ip

热门文章

  1. Hive(2):Apache Hive 安装部署
  2. 50个热门语义分割数据集免费、高速下载资源分享,涵盖通用视觉、遥感、自动驾驶、医疗等多种场景题
  3. UltraEdit-32 v14.10 简体中文版
  4. 语音识别、声纹识别的区别及测试
  5. 联发科MT6797/x20开发板设计,MT6797方案定制,MT6797芯片模块资料
  6. 达梦数据库初始化实例参数说明(页大小 (page_size)、簇大小 (extent_size)、大小写敏感 (case_sensitive)、字符集 (charset))
  7. php遍历windows下中文目录下的所有文件名
  8. 全流程DevOps工具链汇总(全)
  9. “QQ显示iPhone在线”背后的虚荣与焦虑
  10. 深入理解SMTP协议之邮件客户端