精华内容
下载资源
问答
  • linux内核数据结构以及内核调试

    千次阅读 2013-11-19 21:51:10
    内核数据使用的数据类型分为 3 个主要类型 标准C类型明确大小的类型用作特定内核对象的类型 1.1.1 标准 C 类型 使用标准C类型时,必须知道它们的长度不同架构上可能是会变的,标准C对每种类型的长度没有一个很...

    一、可移植性

    1.1 数据类型可移植性

    由于内核可能运行在不同的架构上,不同的架构具有不同的机器字长,因而可移植性对内核编程非常重要。内核数据使用的数据类型分为 3 个主要类型
    • 标准C类型
    • 明确大小的类型
    • 用作特定内核对象的类型

    1.1.1 标准 C 类型

    使用标准C类型时,必须知道它们的长度在不同架构上可能是会变的,标准C对每种类型的长度没有一个很严格的规定,对于很多类型,它们的长度都可能是会变化的。

    1.1.2明确大小的类型

    有时内核代码需要一个特定大小的数据,内核提供了下列数据类型来使用:
    u8; /* unsigned byte (8 bits) */
    u16; /* unsigned word (16 bits) */
    u32; /* unsigned 32-bit value */
    u64; /* unsigned 64-bit value */
    如果需要带符号的固定长度的类型,可以用
    s8; /* unsigned byte (8 bits) */
    s16; /* unsigned word (16 bits) */
    s32; /* unsigned 32-bit value */
    s64; /* unsigned 64-bit value */
    或者也可以使用以下数据类型:
    u_int8_t;
    u_int16_t;
    u_int32_t;
    u_int64_t

    int8_t;
    int16_t;
    int32_t;
    int64_t

    1.1.3 接口特定的类型

    内核中一些场景中使用的数据类型是特别定义的类型,使用这种类型可以具有很好的可移植性。比如进程标识符的类型时pid_t,其它的一些包括:uid_t,timer_t等等,具体的可见include/linux/types.h文件。

    2.2 时间可移植性

    对于时间可移植性来说,linux保证HZ个时钟滴答需要时间为1秒。

    2.3 页大小

    对于内存页,内核保证每个内存页大小为PAGE_SIZE。

    2.4 对齐

    当需要存取不对齐数据时(比如读写非4字节对齐的4字节值),需要考虑该问题,因为不是所有架构都支持非对齐地址上的访问。在文件asm/unaligned.h中定义了两个宏:
    get_unaligned(ptr);
    put_unaligned(val, ptr);
    用于支持这种访问。

    2.5字节序

    由于内核可能运行在不同的架构上,不同架构的字节序是不同的,编写程序时应该尽量不要依赖于字节序,当确实需要特定的字节序时,有两个方法:
    • 根据内核提供的大小端宏来编写相应代码,如果是大端模式,则内核会定义宏__BIG_ENDIAN,如果是小端模式,内核会定义宏__LITTLE_ENDIAN
    • 使用内核提供的转换函数在不同的类型之间进行转换,可以通过asm/byteorder.h文件找到最终被包含的文件并找到这些定义

    二、内核数据结构

    2.1 链表

    链表是最常见的基本数据结构,内核提供了一套操作链表的API,这些API包含了链表的所有操作。这套API定义在linux/list.h中。其中包括两个版本:
    • 常规的双向循环链表
    • 用于散列表的双向链表
    其相关数据结构如下:
    struct list_head {
    struct list_head *next, *prev;
    };

    该结构既用于常规链表的头,也用于常规链表的节点。当一个数据结构包含该结构或者指向该结构的指针时,它就可以使用常规链表的API来操作,因为API只需要该结构的地址作为参数。

    struct hlist_head {
    struct hlist_node *first;
    };

    它是用于散列表版本的链表的头部数据结构。
    struct hlist_node {
    struct hlist_node *next, **pprev;
    };
    它是用于散列表版本的链表的节点数据结构,使用该版本的链表的数据结构需要包含该结构或者指向该结构的指针,在使用链表API时,将该结构的指针作为参数传递给API即可。
    二者的区别:
    • 当使用常规版本时,需要使用链表的数据结构只需要包括一个struct list_head结构或其指针,即可使用API进行操作,链表头包含两个指针,一个指向链表尾部,一个指向链表头部。
    • 而使用用于散列表版本时,链表头使用数据结构struct hlist_head,它只包含一个链表指向链表第一个元素的指针,使用链表的数据结构需要包含的是struct hlist_node数据结构或其指针,然后即可使用API进行操作。之所以有这个区别是因为当使用散列表时,可以期望链表的长度较短,使用两个头会浪费宝贵的内存。
    • 用于散列表的版本不是循环链表
    使用用于散列表版本的链表的一个图示如下:
     
    另外需要注意的是这些API没有提供任何的同步、互斥支持。
    详细的API接口可以参考该文件。

    2.2 kref

    kref是一个引用计数器,它被嵌套进其它的结构中,记录所嵌套结构的引用计数。其定义如下:
    struct kref {
    atomic_t refcount;
    };
    它用于跟踪它所嵌入的结构的使用情况。一般情况下,当其为0时,就要执行清理动作了。
    相关API也比较简单,可以参考文件include/linux/kref.h,不过需要说明的是当调用kref_put时,如果引用计数变为了0,则kref_put会执行调用者提供的release函数,调用者可以在该函数中左自己想要的清理工作。
    初始化之后,kref的使用应该遵循以下三条规则:
    1. 如果你创建了一个该结构,除非它不给被人使用,否则必须对它调用kref_get增加引用计数。
    2. 当不在使用该结构时,必须对它调用kref_put
    3. 如果代码试图在还没拥有引用计数的情况下就调用kref_get,就必须串行化kref_put和kref_get的执行。因为很可能在kref_get执行之前或者执行中,kref_put就被调用并把整个结构释放掉了。内核的文档中给出了一个例子:
    static DEFINE_MUTEX(mutex);
    static LIST_HEAD(q);
    struct my_data
    {
    	struct kref      refcount;
    	struct list_head link;
    };
    
    static struct my_data *get_entry()
    {
    	struct my_data *entry = NULL;
    	mutex_lock(&mutex);
    	if (!list_empty(&q)) {
    		entry = container_of(q.next, struct my_data, link);
    		kref_get(&entry->refcount);
    	}
    	mutex_unlock(&mutex);
    	return entry;
    }
    
    static void release_entry(struct kref *ref)
    {
    	struct my_data *entry = container_of(ref, struct my_data, refcount);
    
    	list_del(&entry->link);
    	kfree(entry);
    }
    
    static void put_entry(struct my_data *entry)
    {
    	mutex_lock(&mutex);
    	kref_put(&entry->refcount, release_entry);
    	mutex_unlock(&mutex);
    }
    

    由于在kref_put时可能会调用提供的release函数,因此API也提供了两个其它版本的put接口:

    1. kref_put_spinlock_irqsave:该版本需要多提东一个spinlock作为参数,它保证减小ref计数和调release函数的操作在锁住该自旋锁并关闭中断的情况下进行,也就是说在SMP架构下也是中断安全的。
    1. kref_put_mutex:该版本需要多提东一个mutex作为参数,它保证减小ref计数和掉release函数的操作在该mutex的保护下进行,因而是“线程”安全的。

    2.3 klist

    klist是一种增强的链表,主要用于设备驱动模型中,它是为了适应动态变化的设备和驱动而专门设计的链表。相关数据结构定义如下:
    struct klist_node;
    struct klist {
    	spinlock_t		k_lock;
    	struct list_head	k_list;
    	void			(*get)(struct klist_node *);
    	void			(*put)(struct klist_node *);
    } __attribute__ ((aligned (sizeof(void *))));
    
    #define KLIST_INIT(_name, _get, _put)					\
    	{ .k_lock	= __SPIN_LOCK_UNLOCKED(_name.k_lock),		\
    	  .k_list	= LIST_HEAD_INIT(_name.k_list),			\
    	  .get		= _get,						\
    	  .put		= _put, }
    
    #define DEFINE_KLIST(_name, _get, _put)					\
    	struct klist _name = KLIST_INIT(_name, _get, _put)
    
    extern void klist_init(struct klist *k, void (*get)(struct klist_node *),
    		       void (*put)(struct klist_node *));
    
    struct klist_node {
    	void			*n_klist;	/* never access directly */
    	struct list_head	n_node;
    	struct kref		n_ref;
    };
    由这些数据结构可知struct klist结构是klist的链表头,klist的链表节点使用的是struct klist_node结构。
    klist链表头的四个域分别为:
    • 链表头k_list,它就是klist的链表头
    • 自旋锁k_lock,用于保护链表
    • get,引用链表中的节点时将被调用
    • put,当不在引用链表中的节点时将被调用,它和get一起维护了klist_node中的引用计数。
    klist_node包含三个域:
    • n_klist:指向节点所在的klist头,由于klist是4字节对齐的,因而该指针的最低两个比特必为0,其中第0比特有特殊用途。
    • n_node:链表元素
    • n_ref:kref引用计数,跟踪记录了该节点被引用的次数,引用次数由链表头的get和put维护。
    相关API可以查看include/linux/klist.h。
    需要说明的是:
    • klist中删除节点时,可能节点的引用计数还不为0,因此节点并不会被删除,但是有的场景下,使用者可能期望节点确实被删除后再继续进行操作,为此klist提供了两个API接口来进行删除的:
      • klist_remove,该API不仅会递减引用计数并提交删除请求,并且会等待节点确实被删除
      • klist_del,该API仅仅递减引用计数,并提交删除请求,但是不会等待,不过如果它触发了真正的删除动作,则会唤醒等待删除真正完成的任务

    另外当提交删除请求时,klist会将节点中n_klist的0比特设置为1,表示该节点已经被请求删除了,只是暂时还没真正删除。标记了该比特的节点在遍历时将会被忽略

    • 由于需要考虑被忽略的节点,即已经被请求删除的节点,因而klist的遍历稍微复杂些。它用函数实现,并用struct klist_iter记录中间状态。klist机制也提供了两个API用于遍历:
      • klist_iter_init_node用于从klist中的某个节点开始遍历,使用它初始化遍历时,可以直接访问当前节点或者用klist_next访问下一节点
      • klist_iter_init用于从链表头开始遍历的,使用它初始化遍历时,只能用klist_next访问下一节点

    2.4 红黑树

    内核在include/linux/rbtree.h中提供了红黑树API,想要使用红黑树的内核代码可以直接使用它。有兴趣的可以查看其代码。

    三、内核调试

    由于内核的特殊性,内核代码很难在调试器控制下运行,也很难跟踪,并且由于内核代码用于服务整个系统,无法简单的将某个故障与特定的“任务”关联起来,因而内核程序的调试是比较特殊的。

    3.1内核对调试的支持

    为了方便调试,内核提供了很多选项,这些选项为内核调试提供了比较丰富的调试支持,它们覆盖了内存管理、分配,内核同步、互斥,驱动子系统等等内核的基本子系统,因而对于内核开发者来说是很有用的。以下是一些其中一些选项(大多选项都位于kernel hacking菜单中)。
    • CONFIG_DEBUG_KERNEL:这个用于使能内核调试选项,它本身不激活任何调试特性。CONFIG_DEBUG_SLAB:使能内存分配的调试功能,打开该选项,则slab子系统会在申请和释放内存时进行一些检查。
    • CONFIG_DEBUG_PAGEALLOC:使能页面的分配调试。
    • CONFIG_DEBUG_SPINLOCK:使能自旋锁的调试支持,可用于检测是否存在重复解锁同
    • CONFIG_MAGIC_SYSRQ:使能"魔术 SysRq"键
    • CONFIG_DEBUG_STACKOVERFLOW:使能栈溢出的检查。
    • CONFIG_DEBUG_STACK_USAGE:使能栈使用信息的调试支持。内核会监测堆栈使用并作一些统计, 这些统计可以用魔术 SysRq 键得到
    • CONFIG_KALLSYMS和CONFIG_KALLSYMS_ALL:它们位于"Generl setup"菜单中,用于将内核符号表包含在系统中。如果没有内核符号表,则oops信息无法给出回溯的符号信息,只能给出16进制的地址信息。
    • CONFIG_IKCONFIG和CONFIG_IKCONFIG_PROC:它们位于"Generl setup"菜单中,使能它后才能在/proc/config.gz中来访问内核配置信息。
    • CONFIG_DEBUG_DRIVER:该选项在”Device Drivers->Generic Driver Options”菜单中,用于使能驱动框架的调试信息。
    • CONFIG_INPUT_EVBUG:该选项在"Device drivers-> Input device support "菜单中,它用于大卖输入事件的详细日志,需要注意的是它会记录了输入设备的所有输入,包括密码。
    • CONFIG_PROFILING:该选项位于"Profiling support"菜单中。用于打开系统系能调试跟踪的功能,它对于剖析系统系能非常有用,也可用于系统挂起的调试。
    以上只是一些例子,具体的调试选项可以查看kernel hacking菜单,里边有很多的调试支持用于支持内核开发调试。

    3.2 用log调试

    用打开来进行调试或者说用log来调试是最基本的调试手段,无论是内核还是用户程序,因为有些bug是在特定场合和应用场景下才出现的,换了环境后很难复现,而有的bug需要长时间运行才能复现,这时候一个精心设计的log系统就能起到大的用途,通过精心选择log点和所要记录的信息,我们可以收集bug现场的所有我们想要的信息,还可以跟踪bug产生的过程,因而log是一个非常重要的调试手段。
    内核中打印需要用printk来实现。

    3.2.1 printk

    printk类似于用户空间的printf,但是也有不同,printk允许调用者指定消息的log等级,系统定义的log等级定义在include/linux/kern_levels.h中,包括: 
    • KERN_EMERG:用于紧急消息, 常常是那些崩溃前的消息.
    • KERN_ALERT:需要立刻动作的情形.
    • KERN_CRIT:严重情况, 常常与严重的硬件或者软件失效有关.
    • KERN_ERR:用来报告错误情况; 设备驱动常常使用 KERN_ERR 来报告硬件故障.
    • KERN_WARNING:有问题的情况的警告, 这些情况自己不会引起系统的严重问题.
    • KERN_NOTICE:正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告.
    • KERN_INFO:信息型消息. 在这个级别, 很多驱动在启动时打印它们发现的硬件的信息.
    • KERN_DEBUG:用作调试消息
    它们的等级依次下降。
    如果调用printk时没有指定消息的log等级,则将使用默认的log等级。
    系统也有一个log等级,如果printk的log等级大于等于当前系统的log等级,则消息会被打印到当前控制台。用户空间对于系统log的处理涉及到两个daemon:klogd和syslogd。
    • klogd会通过syslog()系统调用或者读取proc文件系统来获取内核的log信息,如果 klogd 没有运行,则用户空间只能通过读 /proc/kmsg 来获取信息或者使用dmesg来获取信息。
    • syslogd这个守护进程根据/etc/syslog.conf,将不同的服务产生的log记录到不同的文件中,它是通过klogd来读取系统内核log信息的,如果 klogd 和 syslogd 都在运行,则无论内核log等级为多少,内核都会将消息添加到/var/log/messages(如果syslog的配置文件有某个log的等级的设置,就按照syslogd的配置进行处理)。

    即:如果 klogd 进程在运行, 它获取内核消息并分发给 syslogd, syslogd 接着检查/etc/syslog.conf 来找出如何处理它们. syslogd 根据log类型和一个优先级来区分消息; log类型和优先级的允许值在 <sys/syslog.h> 中定义, 内核消息由 LOG_KERN 来表示.如果 klogd 没有运行, 数据保留在printk的环形缓存中直到有人读它或者缓存被覆盖.

    全局变量console_loglevel记录了系统当前的log等级,可以通过/proc/sys/kernel/printk文件读写它,这个文件有 4 个整型值,分别为:

    当前log级别,适用没有明确log级别的消息的缺省级别,允许的最小log级别,启动时缺省log级别。

    写单个值到这个文件将修改当前log级别为这个值。另外使用dmesg –n {数值}也可以用于修改系统的当前log等级为{数值}
    系统的log被记录在一个环形缓存中,缓存的长度可以使用dmesg –s {大小}来修改,printk将信息写入该环形缓存,如果环形缓存填满,printk 绕回并在缓存的开头增加新数据,覆盖掉最老的数据。

    3.2.2 速率限制

    采用log机制时,有时候需要限制打印的速率,否则打印信息可能将系统拖垮,内核提供了一个函数用于进行打印速率限制printk_ratelimit,如果要使用它来限制打印速率,则应该首先调用它,如果它返回非零值则可以继续打印,否则不打印。

    3.3 通过查询来调试

    Linux系统提供了一些机制用于向用户空间提供查询内核信息的接口。这些信息也可以帮我们分析定位问题。
    用log机制不失为一种很好的调试方式,但是大量的log会影响系统性能。而Linux内核提供的查询机制可以让我们在需要某些信息时再来获取信息,而不是随时打印,这有助于降低系统的负载。*nix系统提供许多工具来获取系统消息:ps, netstat, vmstat, 等等。
    内核提供的两个重要的查询内核信息的机制是:/proc文件系统,/sysfs文件系统以及ioctl,这几种方式都可以用于向用户空间提供内核信息。它们提供了一套机制给内核部件使用,内核部件只要使用这些API就能很方便的向用户空间开发接口。不同的是:
    • 使用两个文件系统时,接口会出现在这两个文件系统相应的位置,即接口以文件的形式存在,可以直接使用cat/echo等命令来操作,而使用ioctl时则开发的接口是编程接口,需要写程序来使用。
    • ioctl比使用文件系统要快。
    • 通过ioctl实现的调试机制,如果不公开其它人无法知道无法使用。

    3.4 使用strace来调试

    使用strace命令可以跟踪所有的用户空间程序发出的系统调用。它不仅显示调用, 还以符号形式显示调用的参数和返回值。当一个系统调用失败,错误的符号值和对应的字符串都会被输出出来。该命令有助于我们分析是哪个调用导致程序无法运行了。

    3.5调试系统故障

    内核程序出现异常(比如oops时)时,一般都会打印一些log信息出来,这些信息对于分析定位问题是很有帮助的,下边的信心是从kernel的git tree里取的一个oops信息:
    On CONFIG_X86_32 this results in the following oops:


      BUG: unable to handle kernel paging request at f7f22280
      IP: [<c10257b9>] reserve_ram_pages_type+0x89/0x210
      *pdpt = 0000000001978001 *pde = 0000000001ffb067 *pte = 0000000000000000
      Oops: 0000 [#1] PREEMPT SMP
      Modules linked in:


      Pid: 0, comm: swapper Not tainted 3.0.0-acpi-efi-0805 #3
       EIP: 0060:[<c10257b9>] EFLAGS: 00010202 CPU: 0
       EIP is at reserve_ram_pages_type+0x89/0x210
       EAX: 0070e280 EBX: 38714000 ECX: f7814000 EDX: 00000000
       ESI: 00000000 EDI: 38715000 EBP: c189fef0 ESP: c189fea8
       DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
      Process swapper (pid: 0, ti=c189e000 task=c18bbe60 task.ti=c189e000)
      Stack:
       80000200 ff108000 00000000 c189ff00 00038714 00000000 00000000 c189fed0
       c104f8ca 00038714 00000000 00038715 00000000 00000000 00038715 00000000
       00000010 38715000 c189ff48 c1025aff 38715000 00000000 00000010 00000000
      Call Trace:
       [<c104f8ca>] ? page_is_ram+0x1a/0x40
       [<c1025aff>] reserve_memtype+0xdf/0x2f0
       [<c1024dc9>] set_memory_uc+0x49/0xa0
       [<c19334d0>] efi_enter_virtual_mode+0x1c2/0x3aa
       [<c19216d4>] start_kernel+0x291/0x2f2
       [<c19211c7>] ? loglevel+0x1b/0x1b
       [<c19210bf>] i386_start_kernel+0xbf/0xc8
    当在内核代码中使用一个非法指针时,内核通常会给出一个oops消息。Oops消息包含了是什么样的错误,出错时的处理器状态,包括CPU 寄存器内容和一些其它的信息。比较重要的是EIP和Call Trace,通常通过它们就可以找到出问题的位置和原因。
    • EIP包含出问题的位置,比如EIP is at reserve_ram_pages_type+0x89/0x210表明问题出在reserve_ram_pages_type中,该函数大小为0x210,问题出在该函数起始地址偏移0x89处。
    • Call Trace包含了出问题时的内核栈,如果内核没有包含符号表,则这个打印是以16进制地址打印的。
    在找到出问题的位置之后(通过EIP)还要进一步找是哪一条指令导致的问题,这个时候就要分析汇编指令了。得到对应汇编指令的方法有:

    3.5.1 有编译好的内核镜像

    gdb vmlinux
    (gdb)b *func+offset
    或者
    (gdb)l *func+offset

    3.5.2 有编译好的二进制文件, 用objdump看

    objdump -S file.o > /tmp/file.s
    然后查看该反汇编指令文件

    3.5.3 有编译好的内核镜像,用addr2line看

    addr2line -e vmlinux func+offset
    另外, 内核源代码目录的./scripts/decodecode文件是用来解码Oops的:
    ./scripts/decodecode < Oops.txt

    3.6 系统挂起

    尽管内核代码的大部分 bug 以 oops 消息结束,但有时候bug也可能导致系统完全挂起,没有任何打印消息。例如如果代码进入一个死循环,内核就会停止调度。
    调试这种问题的一种方式是在自己的代码中加入log,定期打印一些信息到控制台,如果一段时间log没有被更新,就可以根据这个信息找到哪里导致挂起了。
    另外一个可用的工具是SysRq魔法键。通过该机制我们能够获取很多当前系统的信息。魔法键的功能可以在编译内核时通过配置文件打开,也可以在系统启动后通过修改文件/proc/sys/kernel/sysrq来打开或者关闭该功能,sysrq的取值及其含义:
    • 0:关闭该功能
    • 1:打开所有的功能表示打开
    • 大于1:所允许的功能的掩码(具体每个比特位表示什么含义,最好查看Documents/sysrq.txt文件
    在该功能打开后,可以通过按下魔法键来触发特定的功能(魔法键在不同的架构是不同的,具体的可参见Documents/sysrq.txt文件),也可以通过向文件/proc/sysrq-trigger写入字符来触发特定的SysRq功能。其中一些字符及其含义如下:
    • b:立刻重启系统,并且不会对磁盘进行同步也不会卸载磁盘
    • d:显示所有被持有的锁
    • e:向除init进程之外的所有进程发送SIGTERM信号
    • i:向除init进程之外的所有进程发送SIGKILL信号
    • l:为所有在活动状态的CPU打印其堆栈信息
    • m:打印当前内存信息
    • p:打印当前的寄存器状态以及标记
    • q:为所有CPU打印armed的高精度定时器以及所有的时钟设备的详细信息。
    • t:打印当前任务以及它们的堆栈信息
    • w:打印处于不可中断阻塞状态的任务的信息


    展开全文
  • 内核数据类型

    千次阅读 2013-01-31 19:56:35
    现代版本的Linux内核的可移植性是非常好的,可以运行许多不同的体系架构上。由于Linux的多平台特性,任何一个重要的驱动程序都应该是可移植的。 与内核代码相关的核心问题是这些代码应该能够同时访问已知长度的...

    在讨论更高级的话题前,我们需要讨论一下可移植问题。现代版本的Linux内核的可移植性是非常好的,可以运行在许多不同的体系架构上。由于Linux的多平台特性,任何一个重要的驱动程序都应该是可移植的。

    与内核代码相关的核心问题是这些代码应该能够同时访问已知长度的数据项,并充分利用不同的处理器的能力。

    坚持使用严格的数据类型,并且使用-Wall -Wstrict-prototypes选项编译可以防止大多数的代码缺陷。

    内核使用的数据类型主要被分成三大类:类似int这样的标准C语言类型,类似u32这样的有确定大小的类型,以及像pid_t这样的用于特定内核对象的类型。我们将讨论应该在什么情况下使用这三种典型类型,以及如何使用。

    使用标准C语言类型

    尽管大多数程序员习惯于自由使用像int和long这样的标准类型,但编写驱动程序时应该格外小心,以避免类型冲突和代码缺陷。

    问题是,当我们需要“两个字节的填充符”或者“用四个字节字符串表示的某个东西”时,我们不能使用标准类型,因为在不同的体系上,普通C语言的数据类型所占空间的大小并不相同。下面是在不同平台上类型所占空间大小:

    尽管在混合使用不同数据类型时我们必须小心谨慎,但有时有理由这样做。这样的一种情况是内存地址,使用无符号整数类型可以更好地实现内存管理;内核把物理内存看作是一个巨型数组,一个内存地址就是该数组的一个索引。此外,我们可以很方便地对指针取值;但在直接处理内存地址时,我们乎从来不会以这种方式对它们取值。使用一个整数类型可以防止这种取值,因而可避免代码缺陷。所以内核中的普通内存地址通常是unsigned long,这利用了如下事实:至少在当前Linux支持的所有平台上,指针和long整型的大小总是相同的。

    为数据项分配确定的空间大小

    有时内核代码需要特定大小的数据项,多半是用来匹配预定义的二进制结构,或者和用户空间进行通信,或者通过在结构体中插入“填白”字段来对齐数据。

    内核提供了下列数据类型,在<asm/types.h>中声明,这个文件又被<linux/types.h>包含:

    u8;  /*无符号字节8位*/
    u16; /*无符号字16位*/
    u32; /*无符号32位值*/
    u64; /*无符号64位值*/

    相应的有符号类型也存在,但是几乎没用。

    如果一个用户空间程序需要使用这些类型,它可以在名字前加上两个下划线作为前缀:__u8和其他类型是独立于__KERNEL__定义的。

    这些类型是Linux特有的,使用它们将阻碍软件向其他Unix变种的移植。使用新的编译器将支持C99标准类型,如uint8_t和uint32_t;如果考虑到可移植性,可以使用这些类型而不是Linux特有的变种。

    接口特定的类型

    内核中最常用的数据类型是由它们自己的typedef声明,这样可以防止出现任何移植性问题。例如,一个进程的标识符通常使用pid_t类型,而不是int。使用pit_t屏蔽了在实际的数据类型中的任何可能的差异。

    即使没有定义接口特定的类型,也应该始终使用和内核其余部分一致的、适当的数据类型。例如,jiffies计数总是属于unsigned long类型,而不管它的实际大小如何,因此,在使用jiffies的时候应该始终使用unsigned long类型。

    完整的_t类型在<linux/types.h>中定义,但很少使用这个清单。当需要要某个特定类型时,可在所需调用的函数原型或者所使用的数据结构中找到这个类型。

    当我们需要打印一些接口特定的数据类型时,最行之有效的方法,就是将其强制转换成可能的最大类型,然后利用相应的格式打印。

    其他有关移植性的问题

    在编写一个能在不同的Linux平台间移植的驱动程序时,除了数据类型定义的问题之外,还必须注意其他一些软件上的问题。

    一个通用的原则是要避免使用显式的常量值。通常代码会使用预处理的宏来使之参数化。这一节列出了最重要的移植性问题。

    时间间隔

    在处理时间间隔时,不要假定每秒一定有100个jiffies。因为HZ值是因平台不同是可能不同的。例如,为了检测半秒,可以将消逝的时间与HZ/2比较。更常见的,与mesc毫秒对应的jiffies数目总是msec*HZ/1000。

    页大小

     

    使用内存是地,要记住页的大小为PAGE_SIZE字节,而不是4KB。

    让我们看一种重要的情形。如果一个驱动程序需要16KB空间来储存临时数据,我们不应该指定传递给get_free_pages的参数为2的幂,而是需要一个可移植的方案,幸运的是,内核开发人员提供了一个名为get_order的解决方案

    #include <asm/page.h>
    int oder = get_order(16*1024);
    buf = get_free_pages(GFP_KERNEL, order);

    order是要申请的页面的以2为底的对数。记住,传递给get_order的参数必须是2的幂。

    字节序

    小心不要做字节序的假设。尽管PC是按照先是低字节(小端)的方式存储多字节数值的,但某些高端平台是以另一种方式工作的(大端)。

    在处理字节序问题是时,我们可能要编写一组#indef __LITTLE_ENDIAN条件语句,但是还有一个更好的方法。Linux内核定义了一组宏,它可以在处理器字节序和特殊字节序之间进行转换。例如:
    u32 cpu_to_le32(u32);
    u32 le32_to_cpu(u32);

    使用这些宏可以使编写可移植代码的工作变得更加容易,而无需使用很多的条件编译。

    类似的例子还有很多,我们可以在头文件<linux/byteorder/big_endian.h>和<linux/byteorder/little_endian.h>中看到完整的列表。

    数据对齐

    编写可移植代码的最后一个问题是如何访问未对齐的数据。i386用户常常访问未对齐的数据项,但不是所有平台都允许这样做的。如果需要访问未对齐的数据,则应该使用下面的宏:

    #include <asm/unaligned.h>
    get_unaligned(ptr);
    put_unaligned(val, ptr);

    这些宏是与数据类型无关的,对各种数据项都有效。所有版本的内核都定义了这么宏。

    另一个关于对齐的问题是数据结构的跨平台可移植性。同样的数据结构(在C语言源文件中定义的)在不同的平台上可能会编译成不同的布局。编译器根据平台的习惯来对齐数据结构的字段,而不同平台的习惯是不同的。

    为了编写可以在不同平台之间可移植的数据项的数据结构,除了规定特定的字节序以外,还应该始终强制数据项的自然对齐。自然对齐是指在数据项大小的整数倍(例如,8字节数据项存入8的整数倍的地址)的地址处存储数据项。强制自然对齐可以防止编译器移动数据结构的字段,你应该使用填充符字段来避免数据结构中留下空洞。

    值得注意的是,不是所有平台都在64位边界对齐64位数值,所以需要填充符字段来强制对齐并确保可移植性。

    最后,要注意编译器本身也许会悄悄地往结构体中插入填充数据,来确保每个字段的对齐可以在目标处理器上取得好的性能。如果正在定义一个和设备所要求的结构体相匹配的结构体,这种自动填充会破坏你的意图。解决办法是告诉编译器该结构体必须是“填满的”,不能添加填充符。

    指针和错误值

    许多内部的内核函数返回一个指针值给调用者,而这些函数中很多可能会失败。在大部分情况下,失败是通过返回一个NULL指针来表示的。这种技巧很有用,但它不能传递问题的确切性质。某些接口确实需要返回一个实际的错误编码,以使调用都可以根据实际出错的情况做出正确的决策。

    许多内核接口通过把错误值编码到一个指针值中来返回错误信息。这种函数必须小心使用,因为它们的返回值不能简单的和NULL比较。为了帮助创建和使用这种类型的接口,<linux/err.h>中提供了一小组函数。

    返回指针类型可以通过如下函数返回一个错误值:

    void *ERR_PTR(long error);

    这里error是通常的负的错误编码。调用者可以使用IS_ERR来检查所返回的指针是否是一个错误编码:

    long IS_ERR(const void *prt);

    如果需要实际的错误编码,可以通过如下函数把它提取出来:

    long PTR_ERR(const void *ptr);

    应该只有在IS_ERR对某值返回真时才对该值使用PTR_ERR,因为任何其他值都是有效的指针。

    链表

    有时,Linux内核同时存在多个链表的实现代码,为了减少重复代码的数量,内核开发者已经建立了一套标准的循环、双向链表的实现。如果需要操作链表,那么建议直接使用这一内核设施。

    当使用这些链表接口时,应该始终牢记这些链表函数不进行任何锁定。如果驱动程序有可能试图对同一个链表执行并发操作的话,则有责任实现一个锁方案。否则,崩溃的链表结构休、数据丢失、内核混乱等问题是很难诊断的。驱动程序必须包含头文件<linux/list.h>来使用内核链表接口。

     

     

     

     

     

     

    展开全文
  • “任务管理器”中,页面错误是进程中当数据内存而必须从磁盘检索的次数。页面错误值从进程启动的时间开始累计。 页面错误增量 “任务管理器”中,自上一次更新开始的页面错误次数的变化。 具体参见: 帮助...


    页面错误

    页面错误指当软件试图读取或写入标记为“不存在”的虚拟内存位置时发生的中断。页面错误记录了一个进程必须从硬盘上恢复的次数。

    在“任务管理器”中,页面错误是进程中当数据不在内存而必须从磁盘检索的次数。页面错误值从进程启动的时间开始累计。

    页面错误增量

    在“任务管理器”中,自上一次更新开始的页面错误次数的变化。

    具体参见: 帮助--任务管理器--使用进程--进程计数器列标题--页面错误

    页面错误不表示程序存在错误。不管内存多大,WINDOWS都离不开虚拟内存(利用磁盘),多少都会利用虚拟内存来存入一些数据,比如程序被最小化。

    页面错误 任务管理器 主要检测内存情况的。

    转载地址: http://blog.sina.com.cn/s/blog_51396f890100qjtb.html

    内存监控

        开发的一个项目中,遇到了内存泄露的情况,发现此时页面错误增量在300多,什么是页面错误增量?

        上网看了一下,这个就是当程序需要访问内存时,待访问的地址不再物理内存中,需要换页将虚拟内存页换到物理内存中。页面错误增量大,会导致运行效率差,赃页多。

        但是为什么会出现这个情况?是OS的问题还是APP的问题?还不清楚,需要再查一下。

        如果要查看程序使用情况,可以调用接口GetProcessMemoryInfo

    1. BOOL GetProcessMemoryInfo(  
    2.   HANDLE Process,  
    3.   PPROCESS_MEMORY_COUNTERS ppsmemCounters,  
    4.   DWORD cb  
    5. );  

        如果需要限制程序使用虚拟内存的大小,可使用SetProcessWorkingSetSize,注意:这样可能会降低运行效率.

    1. BOOL SetProcessWorkingSetSize 
    2.   HANDLE hProcess,  
    3.   SIZE_T dwMinimumWorkingSetSize,  
    4.   SIZE_T dwMaximumWorkingSetSize  
    5. );  
    内存管理和缓存管理的细节,所以记录详细点,其他的东西(仅指基本组件)理解还可以。。以备忘的形式慢慢po上来~

    页面交换


    使用技巧

    对于虚拟内存如何设置的问题,微软已经给我们提供了官方的解决办法,对于一般情况下,我们推荐采用如下的设置方法:
    • 页面文件,文件的大小由你对系统的设置决定。具体设置方法如下:打开"我的电脑"的"属性"设置窗口,切换到"高级"选项卡,在"启动和故障恢复"窗口的"写入调试信息"栏,如果你采用的是试和错误报告了。所以折中的办法是在系统盘设置较小的页面文件,只要够用就行了。
    • 内存,其最小值设置为物理内存的1.5倍,最大值设置为物理内存的3倍,该分区专门用来存储页面文件,不要再存放其它任何文件。之所以单独划分一个分区用来设置虚拟内存,主要是基于两点考虑:其一,由于该分区上没有其它文件,这样分区不会产生磁盘碎片,这样能保证页面文件的数据读写不受磁盘碎片的干扰;其二,按照Windows对内存的管理技术,Windows会优先使用不经常访问的分区上的页面文件,这样也减少了读取系统盘里的页面文件的机会,减轻了系统盘的压力。
    • 页面文件,则其它硬盘分区不设置任何页面文件。因为过多的分区设置页面文件,这样会导致,硬盘磁头反复的在不同的分区来回读取。这样既耽误了系统速率,也会减少硬盘的寿命。当然,如果你有多个硬盘,则可以为每个硬盘都创建一个页面文件。当信息分布在多个页面文件上时,硬盘控制器可以同时在多个硬盘上执行读取和写入操作。这样系统性能将得到提高。

    换页错误

    换页错误,即Page fault。
    Page Fault 是在进程尝试执行代码指导,或者引用进程所映射物理内存中并不存在的数据页时,操作系统记录的事件。换句话说,进程需要的内存页实际上可能还处于物理内存中,但是由于它无法再分配到进程中,所以当进程将此页读取回到它的内存页时,就发生了Page Fault。

    在开发上,我认为主要是优化内存读取方式,如果存在大量的文件读取,虚拟内存也就多,换页次数就多,自然也很多,不作为主要的评测指标。













    现在内存都是分页的, 如果你要读或者写的页还没分在内存里, 就出现缺页错了。 这种事情在程序启动的时候可能非常频繁, 但是也不用你自己处理, 一般系统会自己搞定这事的, 搞不定就直接死机。。

    转载:http://en.wikipedia.org/wiki/Page_fault


    页缺失 (计算机科学)

    维基百科,自由的百科全书
    (重定向自Page fault

    页缺失英语Page fault,又名硬错误分页错误寻页缺失缺页中断页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器内存管理单元所发出的中断

    通常情况下,用于处理此中断的程序是操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。而如果访问是不被允许的,那么操作系统通常会结束相关的进程[1]

    虽然其名为“页缺失”错误,但实际上这并不一定是一种错误。而且这一机制对于利用虚拟内存来增加程序可用内存空间的操作系统(比如Microsoft Windows和各种类Unix系统)中都是常见且有必要的。

    微软在较新版Windows(Windows Vista及以上)的资源监视器中使用“硬错误”这一术语来指代“页缺失”。[2]

    目录

       [隐藏

    [编辑]分类

    [编辑]软性

    软性页缺失指页缺失发生时,相关的页已经被加载进内存,但是没有向MMU注册的情况。操作系统只需要在MMU中注册相关页对应的物理地址即可。[1]

    发生这种情况的可能性之一,是一块物理内存被两个或多个程序共享,操作系统已经为其中的一个装载并注册了相应的页,但是没有为另一个程序注册。

    可能性之二,是该页已被从CPU的工作集中移除,但是尚未被交换到磁盘上。比如OpenVMS这样的使用次级页缓存的系统,就有可能会在工作集过大的情况下,将某页从工作集中去除,但是不写入硬盘也不擦除(比如说这一页被读出硬盘后没被修改过),只是放入空闲页表。除非有其他程序需要,导致这一页被分配出去了,不然这一页的内容不会被修改。当原程序再次需要该页内的数据时,如果这一页确实没有被分配出去,那么系统只需要重新为该页在MMU内注册映射即可。[3]

    [编辑]硬性

    与软性页缺失相反,硬性页缺失是指相关的页在页缺失发生时未被加载进内存的情况。这时操作系统需要:[4]

    1. 寻找到一个空闲的页。或者把另外一个使用中的页写到磁盘上(如果其在最后一次写入后发生了变化的话),并注销在MMU内的记录
    2. 将数据读入被选定的页
    3. 向MMU注册该页

    硬性页缺失导致的性能损失是很大的。以一块7200rpm的主流机械硬盘为例,其平均寻道时间为8.5毫秒,读入内存需要0.05毫秒。相对的,DDR3内存的访问延迟通常在数十到100纳秒之间,性能差距可能会达到8万到22万倍。

    另外,有些操作系统会将程序的一部分延迟到需要使用的时候再加载入内存执行,以此来提升性能。这一特性也是通过捕获硬性页缺失达到的。[5]

    当硬性页缺失过于频繁的发生时,称发生系统颠簸

    [编辑]无效

    当程序访问的虚拟地址是不存在于虚拟地址空间内的时候,则发生无效页缺失。一般来说这是个软件问题,但是也不排除硬件可能,比如因为内存故障而损坏了一个正确的指针

    具体动作与所使用的操作系统有关,比如Windows会使用异常机制向程序报告,而类Unix系统则会使用信号机制。如果程序未处理相关问题,那么操作系统会执行默认处理方式,通常是转储内存、终止相关的程序,然后向用户报告。[4][6]

    [编辑]参考与延伸阅读

    1. 1.0 1.1 Red Hat Enterprise Linux 3: Introduction to System Administration, 4.4. Virtual Memory: The Details. RedHat [2013-02-11].
    2. ^ 参见资源监视器帮助
    3. ^ HP OpenVMS Systems Documentation, Chapter 7 Evaluating the Memory Resource. HP [2013-02-11].
    4. 4.0 4.1 So What Is A Page Fault?. OSR Online. 2003-05-07 [2013-02-11].
    5. ^ Red Hat Enterprise Linux 3: Introduction to System Administration, 4.5. Virtual Memory Performance Implications. RedHat [2013-02-11]. See note.
    6. ^ UnhandledExceptionFilter function. Microsoft MSDN [2013-02-11].
    • John L. Hennessy, David A. Patterson, Computer Architecture, A Quantitative Approach (ISBN 1-55860-724-2)
    • Tanenbaum, Andrew S. Operating Systems: Design and Implementation (Second Edition). New Jersey: Prentice-Hall 1997.
    • Intel Architecture Software Developer's Manual–Volume 3: System Programming

    非法访问和无效页错误处理

    非法访问和无效页错误可能会导致程序崩溃,分割错误,总线错误或核心转储的操作系统环境。这些问题通常是由于软件缺陷,但可能会损坏硬件内存错误,如由超频所引起的,指针和正确的软件故障。

    WindowsUNIX的操作系统(以及其他类UNIX系统)提供不同的页故障引起的错误报告机制。Windows使用结构化异常处理报告故障无效的存取访问冲突异常,UNIX(UNIX-like)的系统通常使用信号,如SIGSEGV,报告这些错误条件的方案。

    如果收到错误的程序不处理,操作系统执行的默认操作,一般涉及终止正在运行的过程中导致错误的条件,并通知用户,该计划已发生了故障。最新版本的Windows中经常报道这样的问题,就类似“这个程序必须关闭”(有经验的用户或程序员提供一个调试器仍然可以获取详细信息)。最新的Windows版本UNIX类UNIX操作系统报告这些条件的用户提供的错误信息,如“分割违反”或“巴士,也可以编写一个小型转储(类似的原则,以一个核心转储)描述的状态崩溃的过程。错误“,也可能产生核心转储。

    编辑 ]性能

    页故障,由于其本身的性质,一个程序或操作系统的性能降低和退化的情况可能会导致颠簸优化程序和操作系统数量减少的页面错误,提高程序性能,甚至整个系统。的两个主要侧重的优化工作,降低整体内存使用率和改善记忆的地方。为了减少页面系统中的故障,程序员必须使用适当的页面置换算法,适合当前需求和最大限度地提高了页面的点击。许多人都被提出,如实施启发式算法,以减少发病的页面错误。一般情况下,提供更多的物理内存,也减少了页面错误。

    主要页错误的传统(硬盘)计算机上可以有一个显着的性能影响。平均的硬盘具有的平均旋转等待时间为3ms,寻道时间为5ms,和转印时间为0.05毫秒/页。因此,总的寻呼时间是8ms的(8 000我们)附近。如果内存访问时间为0.2,那么页面故障,使操作约40,000倍的速度。

    MMU

    求助编辑百科名片

    MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权。

    编辑本段历史

    许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。但随着图形界面的兴起还有用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。覆盖块0首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由OS完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。人们必须找到更好的办法从根本上解决这个问题。不久人们找到了一个办法,这就是虚拟存储器(virtual memory).虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上。比如对一个16MB的程序和一个内存只有4MB的机器,操作系统通过选择,可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。

    编辑本段相关概念

    ——地址范围、虚拟地址映射为物理地址 以及 分页机制
    任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由CPU的位数决定,例如一个32位的CPU,它的地址范围是0~0xFFFFFFFF (4G),而对于一个64位的CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF (16E).这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集。这里举一个最简单的例子直观地说明这两者,对于一台内存为256M的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),而物理地址空间范围是0x000000000~0x0FFFFFFF(256M)。
    在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
    大多数使用虚拟存储器的系统都使用一种称为分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页帧(frame).页和页帧的大小必须相同。在这个例子中我们有一台可以生成32位地址的机器,它的虚拟地址范围从0~0xFFFFFFFF(4G),而这台机器只有256M的物理地址,因此他可以运行4G的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放4G程序的外部存储器(例如磁盘或是FLASH),以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页帧大小与页相同——这点是必须保证的,因为内存和外围存储器之间的传输总是以页为单位的。对应4G的虚拟地址和256M的物理存储器,他们分别包含了1M个页和64K个页帧。

    编辑本段功能

    1、将虚拟地址映射为物理地址

    现代的多用户多进程操作系统,需要MMU,才能达到每个用户进程都拥有自己独立的地址空间的目标。使用MMU,操作系统划分出一段地址区域,在这块地址区域中,每个进程看到的内容都不一定一样。例如MICROSOFT WINDOWS操作系统将地址范围4M-2G划分为用户地址空间,进程A在地址0X400000(4M)映射了可执行文件,进程B同样在地址0X400000(4M)映射了可执行文件,如果A进程读地址0X400000,读到的是A的可执行文件映射到RAM的内容,而进程B读取地址0X400000时,则读到的是B的可执行文件映射到RAM的内容。
    这就是MMU在当中进行地址转换所起的作用。

    2、提供硬件机制的内存访问授权

    多年以来,微处理器一直带有片上存储器管理单元(MMU),MMU能使单个软件线程工作于硬件保护地址空间。但是在许多商用实时操作系统中,即使系统中含有这些硬件也没采用MMU。
    应用程序的所有线程共享同一存储器空间时,任何一个线程将有意或无意地破坏其它线程的代码、数据或堆栈。异常线程甚至可能破坏内核代码或内部数据结构。例如线程中的指针错误就能轻易使整个系统崩溃,或至少导致系统工作异常。
    就安全性和可靠性而言,基于进程的实时操作系统(RTOS)的性能更为优越。为生成具有单独地址空间的进程,RTOS只需要生成一些基于RAM的数据结构并使MMU加强对这些数据结构的保护。基本思路是在每个关联转换中“接入”一组新的逻辑地址。MMU利用当前映射,将在指令调用或数据读写过程中使用的逻辑地址映射为存储器物理地址。MMU还标记对非法逻辑地址进行的访问,这些非法逻辑地址并没有映射到任何物理地址
    这些进程虽然增加了利用查询表访问存储器所固有的系统开销,但其实现的效益很高。在进程边界处,疏忽或错误操作将不会出现,用户接口线程中的缺陷并不会导致其它更关键线程的代码或数据遭到破坏。目前在可靠性和安全性要求很高的复杂嵌入式系统中,仍然存在采无存储器保护的操作系统的情况,这实在有些不可思议。
    采用MMU还有利于选择性地将页面映射或解映射到逻辑地址空间。物理存储器页面映射至逻辑空间,以保持当前进程的代码,其余页面则用于数据映射。类似地,物理存储器页面通过映射可保持进程的线程堆栈。RTOS可以在每个线程堆栈解映射之后,很容易地保留逻辑地址所对应的页面内容。这样,如果任何线程分配的堆栈发生溢出,将产生硬件存储器保护故障,内核将挂起该线程,而不使其破坏位于该地址空间中的其它重要存储器区,如另一线程堆栈。这不仅在线程之间,还在同一地址空间之间增加了存储器保护。
    存储器保护(包括这类堆栈溢出检测)在应用程序开发中通常非常有效。采用了存储器保护,程序错误将产生异常并能被立即检测,它由源代码进行跟踪。如果没有存储器保护,程序错误将导致一些细微的难以跟踪的故障。实际上,由于在扁平存储器模型中,RAM通常位于物理地址的零页面,因此甚至NULL指针引用的解除都无法检测到

    展开全文
  • linux用户空间和内核空间交换数据

    千次阅读 2012-12-26 14:28:46
    研究dahdi驱动的时候,见到了一些get_user,put_user的函数,不知道其来由,故而搜索了这篇文章,前面对linux内存的框架描述不是很清晰,描述的有一点乱,如果没有刚性需求,建议不用怎么关注,倒不如直接看那几个...

    转载地址:http://www.poluoluo.com/server/201107/138420.html

    在研究dahdi驱动的时候,见到了一些get_user,put_user的函数,不知道其来由,故而搜索了这篇文章,前面对linux内存的框架描述不是很清晰,描述的有一点乱,如果没有刚性需求,建议不用怎么关注,倒不如直接看那几个图片。对我非常有用的地方就是几个函数的介绍,介绍的比较详细,对应用有需求的可以着重看一个这几个函数。


    Linux 内存

      在 Linux 中,用户内存和内核内存是独立的,在各自的地址空间实现。地址空间是虚拟的,就是说地址是从物理内存中抽象出来的(通过一个简短描述的过程)。由于地址空间是虚拟的,所以可以存在很多。事实上,内核本身驻留在一个地址空间中,每个进程驻留在自己的地址空间。这些地址空间由虚拟内存地址组成,允许一些带有独立地址空间的进程指向一个相对较小的物理地址空间(在机器的物理内存中)。不仅仅是方便,而且更安全。因为每个地址空间是独立且隔离的,因此很安全。

      但是与安全性相关联的成本很高。因为每个进程(和内核)会有相同地址指向不同的物理内存区域,不可能立即共享内存。幸运的是,有一些解决方案。用户进程可以通过 Portable Operating System Interface for UNIX? (POSIX) 共享的内存机制(shmem)共享内存,但有一点要说明,每个进程可能有一个指向相同物理内存区域的不同虚拟地址。

      虚拟内存到物理内存的映射通过页表完成,这是在底层软件中实现的(见图 1)。硬件本身提供映射,但是内核管理表及其配置。注意这里的显示,进程可能有一个大的地址空间,但是很少见,就是说小的地址空间的区域(页面)通过页表指向物理内存。这允许进程仅为随时需要的网页指定大的地址空间。

    图 1. 页表提供从虚拟地址到物理地址的映射 
    从 Linux 内核访问用户空间内存

      由于缺乏为进程定义内存的能力,底层物理内存被过度使用。通过一个称为 paging(然而,在 Linux 中通常称为 swap)的进程,很少使用的页面将自动移到一个速度较慢的存储设备(比如磁盘),来容纳需要被访问的其它页面(见图 2 )。这一行为允许,在将很少使用的页面迁移到磁盘来提高物理内存使用的同时,计算机中的物理内存为应用程序更容易需要的页面提供服务。注意,一些页面可以指向文件,在这种情况下,如果页面是脏(dirty)的,数据将被冲洗,如果页面是干净的(clean),直接丢掉。

    图 2. 通过将很少使用的页面迁移到速度慢且便宜的存储器,交换使物理内存空间得到了更好的利用 
    从 Linux 内核访问用户空间内存

      MMU-less 架构

      不是所有的处理器都有 MMU。因此,uClinux 发行版(微控制器 Linux)支持操作的一个地址空间。该架构缺乏 MMU 提供的保护,但是允许 Linux 运行另一类处理器。

      选择一个页面来交换存储的过程被称为一个页面置换算法,可以通过使用许多算法(至少是最近使用的)来实现。该进程在请求存储位置时发生,存储位置的页面不在存储器中(在存储器管理单元 [MMU] 中无映射)。这个事件被称为一个页面错误 并被硬件(MMU)删除,出现页面错误中断后该事件由防火墙管理。该栈的详细说明见 图 3。

      Linux 提供一个有趣的交换实现,该实现提供许多有用的特性。Linux 交换系统允许创建和使用多个交换分区和优先权,这支持存储设备上的交换层次结构,这些存储设备提供不同的性能参数(例如,固态磁盘 [SSD] 上的一级交换和速度较慢的存储设备上的较大的二级交换)。为 SSD 交换附加一个更高的优先级使其可以使用直至耗尽;直到那时,页面才能被写入优先级较低的交换分区。

    图 3. 地址空间和虚拟 - 物理地址映射的元素 
    从 Linux 内核访问用户空间内存

      并不是所有的页面都适合交换。考虑到响应中断的内核代码或者管理页表和交换逻辑的代码,显然,这些页面决不能被换出,因此它们是固定的,或者是永久地驻留在内存中。尽管内核页面不需要进行交换,然而用户页面需要,但是它们可以被固定,通过 mlock(或 mlockall)函数来锁定页面。这就是用户空间内存访问函数的目的。如果内核假设一个用户传递的地址是有效的且是可访问的,最终可能会出现内核严重错误(kernel panic)(例如,因为用户页面被换出,而导致内核中的页面错误)。该应用程序编程接口(API)确保这些边界情况被妥善处理。

      内核 API

      现在,让我们来研究一下用户操作用户内存的内核 API。请注意,这涉及内核和用户空间接口,而下一部分将研究其他的一些内存 API。用户空间内存访问函数在表 1 中列出。

    表 1. 用户空间内存访问 API

    函数描述
    access_ok检查用户空间内存指针的有效性
    get_user从用户空间获取一个简单变量
    put_user输入一个简单变量到用户空间
    clear_user清除用户空间中的一个块,或者将其归零。
    copy_to_user将一个数据块从内核复制到用户空间
    copy_from_user将一个数据块从用户空间复制到内核
    strnlen_user获取内存空间中字符串缓冲区的大小
    strncpy_from_user从用户空间复制一个字符串到内核

      正如您所期望的,这些函数的实现架构是独立的。例如在 x86 架构中,您可以使用 ./linux/arch/x86/lib/usercopy_32.c 和 usercopy_64.c 中的源代码找到这些函数以及在 ./linux/arch/x86/include/asm/uaccess.h 中定义的字符串。

      当数据移动函数的规则涉及到复制调用的类型时(简单 VS. 聚集),这些函数的作用如图 4 所示。

    图 4. 使用 User Space Memory Access API 进行数据移动 
    从 Linux 内核访问用户空间内存

      access_ok 函数

      您可以使用 access_ok 函数在您想要访问的用户空间检查指针的有效性。调用函数提供指向数据块的开始的指针、块大小和访问类型(无论这个区域是用来读还是写的)。函数原型定义如下:

    access_ok( type, addr, size ); 

      type 参数可以被指定为 VERIFY_READ 或 VERIFY_WRITE。VERIFY_WRITE 也可以识别内存区域是否可读以及可写(尽管访问仍然会生成 -EFAULT)。该函数简单检查地址可能是在用户空间,而不是内核。

      get_user 函数

      要从用户空间读取一个简单变量,可以使用 get_user 函数,该函数适用于简单数据类型,比如,char 和 int,但是像结构体这类较大的数据类型,必须使用 copy_from_user 函数。该原型接受一个变量(存储数据)和一个用户空间地址来进行 Read 操作:

    get_user( x, ptr ); 

      get_user 函数将映射到两个内部函数其中的一个。在系统内部,这个函数决定被访问变量的大小(根据提供的变量存储结果)并通过 __get_user_x 形成一个内部调用。成功时该函数返回 0,一般情况下,get_user 和 put_user 函数比它们的块复制副本要快一些,如果是小类型被移动的话,应该用它们。

      put_user 函数

      您可以使用 put_user 函数来将一个简单变量从内核写入用户空间。和 get_user 一样,它接受一个变量(包含要写的值)和一个用户空间地址作为写目标:

    put_user( x, ptr ); 

      和 get_user 一样,put_user 函数被内部映射到 put_user_x 函数,成功时,返回 0,出现错误时,返回 -EFAULT。

      clear_user 函数

      clear_user 函数被用于将用户空间的内存块清零。该函数采用一个指针(用户空间中)和一个型号进行清零,这是以字节定义的:

    clear_user( ptr, n ); 

      在内部,clear_user 函数首先检查用户空间指针是否可写(通过 access_ok),然后调用内部函数(通过内联组装方式编码)来执行 Clear 操作。使用带有 repeat 前缀的字符串指令将该函数优化成一个非常紧密的循环。它将返回不可清除的字节数,如果操作成功,则返回 0。

      copy_to_user 函数

      copy_to_user 函数将数据块从内核复制到用户空间。该函数接受一个指向用户空间缓冲区的指针、一个指向内存缓冲区的指针、以及一个以字节定义的长度。该函数在成功时,返回 0,否则返回一个非零数,指出不能发送的字节数。

    copy_to_user( to, from, n ); 

      检查了向用户缓冲区写入的功能之后(通过 access_ok),内部函数 __copy_to_user 被调用,它反过来调用 __copy_from_user_inatomic(在 ./linux/arch/x86/include/asm/uaccess_XX.h 中。其中 XX 是 32 或者 64 ,具体取决于架构。)在确定了是否执行 1、2 或 4 字节复制之后,该函数调用 __copy_to_user_ll,这就是实际工作进行的地方。在损坏的硬件中(在 i486 之前,WP 位在管理模式下不可用),页表可以随时替换,需要将想要的页面固定到内存,使它们在处理时不被换出。i486 之后,该过程只不过是一个优化的副本。

      copy_from_user 函数

      copy_from_user 函数将数据块从用户空间复制到内核缓冲区。它接受一个目的缓冲区(在内核空间)、一个源缓冲区(从用户空间)和一个以字节定义的长度。和 copy_to_user 一样,该函数在成功时,返回 0 ,否则返回一个非零数,指出不能复制的字节数。

    copy_from_user( to, from, n ); 

      该函数首先检查从用户空间源缓冲区读取的能力(通过 access_ok),然后调用 __copy_from_user,最后调用 __copy_from_user_ll。从此开始,根据构架,为执行从用户缓冲区到内核缓冲区的零拷贝(不可用字节)而进行一个调用。优化组装函数包含管理功能。

      strnlen_user 函数

      strnlen_user 函数也能像 strnlen 那样使用,但前提是缓冲区在用户空间可用。strnlen_user 函数带有两个参数:用户空间缓冲区地址和要检查的最大长度。

    strnlen_user( src, n ); 

      strnlen_user 函数首先通过调用 access_ok 检查用户缓冲区是否可读。如果是 strlen 函数被调用,max length 参数则被忽略。

      strncpy_from_user 函数

      strncpy_from_user 函数将一个字符串从用户空间复制到一个内核缓冲区,给定一个用户空间源地址和最大长度。

    strncpy_from_user( dest, src, n ); 

      由于从用户空间复制,该函数首先使用 access_ok 检查缓冲区是否可读。和 copy_from_user 一样,该函数作为一个优化组装函数(在 ./linux/arch/x86/lib/usercopy_XX.c 中)实现。

      内存映射的其他模式

      上面部分探讨了在内核和用户空间之间移动数据的方法(使用内核初始化操作)。Linux 还提供一些其他的方法,用于在内核和用户空间中移动数据。尽管这些方法未必能够提供与用户空间内存访问函数相同的功能,但是它们在地址空间之间映射内存的功能是相似的。

      在用户空间,注意,由于用户进程出现在单独的地址空间,在它们之间移动数据必须经过某种进程间通信机制。Linux 提供各种模式(比如,消息队列),但是最着名的是 POSIX 共享内存(shmem)。该机制允许进程创建一个内存区域,然后同一个或多个进程共享该区域。注意,每个进程可能在其各自的地址空间中映射共享内存区域到不同地址。因此需要相对的寻址偏移(offset addressing)。

      mmap 函数允许一个用户空间应用程序在虚拟地址空间中创建一个映射,该功能在某个设备驱动程序类中是常见的,允许将物理设备内存映射到进程的虚拟地址空间。在一个驱动程序中,mmap 函数通过 remap_pfn_range 内核函数实现,它提供设备内存到用户地址空间的线性映射。

      结束语

      本文讨论了 Linux 中的内存管理主题,然后讨论了使用这些概念的用户空间内存访问函数。在用户空间和内核空间之间移动数据并没有表面上看起来那么简单,但是 Linux 包含一个简单的 API 集合,跨平台为您管理这个复杂的任务。


    展开全文
  • 内核需要保持该结构尽可能小,因为对于现代计算机,物理内存包含大量的页帧,即便是增加page一点点空间,都会导致保存所有页帧page结构多占用大量物理内存。例如当页长度为4KB,主内存384MB时大约需要100000页。  ...
  • Google的chrome莫名其妙突然所有页面都显示“喔唷 崩溃啦”,各种插件右下角弹出报错!这个问题我之前遇到过一次,后来通过改快捷方式的名字解决了。可是这次,隔离回来上班,打开电脑,又一次出现这种现象。折腾...
  • 一定要注意的是不能直接访问用户空间数据内核代码可以通过特殊的函数来访问用户空间数据,copy_to_user copy_from_user这两个函数就是内核代码访问用户空间数据的函数,但是内核不能直接通过像是memcpy函数来直接...
  • 如何通过数据包套接字攻击Linux内核

    千次阅读 2017-08-11 13:41:42
    最近我花了一些时间使用syzkaller工具对Linux内核中与网络有关的接口进行了模糊测试(fuzz)。除了最近发现的DCCP套接字漏洞之外,我还发现了另一个漏洞,该漏洞位于数据包套接字(packet sockets)中。这篇文章...
  • linux内核源代码学习(5)内存页面的换入换出---flyli 关于物理页面的换入换出,其实其主要的目的也就是当物理内存不够使用的时候,将不常用到的内存页面交换到硬盘上,以保证程序能够正常运行。 计算机理论里面...
  • 列举web报表工具中数据集报错错误代码和解释。
  • 内核调试

    千次阅读 2012-02-14 09:31:32
    1.调试技术 内核编程带来了它自己的,独特...本章将介绍如此恼人的情况下你可以用来监视内核代码和跟踪错误的技术。 1.1.内核中的调试支持 第二章中,我们建议你编译和安装你自己的内核,而不是运行你所使用的发
  • 【Linux】Linux内核空间的slab分配模式

    千次阅读 2018-07-22 22:42:16
    内核在运行时,经常需要在内核空间3G~3G+high_memory这个内存空间申请动态内存,以存放一些结构类型的数据。例如,创建一个程序时,它就要为该程序控制块task_struct申请一段内存空间;撤销这个程序时,又要释放...
  • 【Linux】在内核中申请内存

    千次阅读 2011-05-22 16:04:00
    在内核中申请内存和用户空间中申请内存不同,有以下因素引起了复杂性,包括: 1,内核的虚拟和物理地址被限制到1GB。...尽管在内核中访问大量内存不再奢侈,但是一点错误就要耗费很长的时间来解决。
  • 2019仿互站网源码T5友价内核PC+社区+博客+手机+整站数据 全新运营 源码介绍:  仿新版互站网/站长任务网源码完整源码分享,友价T5内核二开版在线虚拟交易商城整站 修复说明: 1:修复了一些细节的问题和完善了教程 2...
  • Linux内核裁减

    千次阅读 2015-06-23 16:42:28
    Linux内核裁减 (1)安装新内核: i)将新内核copy到/usr/src下, #tar xzvflinux-2.6.38.4.tar.gz -----解压缩. ii) 将名为linux的符号链接删掉,这是旧版本内核的符号链接. #ln-s linux-2.6.38.4 linux ------建立...
  • 内核参数说明

    千次阅读 2015-12-22 13:46:32
     因个人能力有限,不能保证所有描述都正确,还请大家集思广益,有错误的地方欢迎大家留言指正,同时也欢迎大家留言对未标注项进行补偿,谢谢。   内核参数列表 kernel.acct acct功能用于系统记录进程信息,...
  • 3.3 Linux内核的组成3.3.1 Linux内核源代码的目录结构Linux内核源代码包含如下目录。...arch目录下,存放的是各个平台以及各个平台的芯片对Linux内核进程调度、内存管理、中断等的支持,以及每个具体的SoC...
  • 内核调试技术

    千次阅读 2012-11-07 15:10:28
    1.调试技术 内核编程带来了它自己的,独特...本章将介绍如此恼人的情况下你可以用来监视内核代码和跟踪错误的技术。 1.1.内核中的调试支持 第二章中,我们建议你编译和安装你自己的内核,而不是运行你所使用的发
  • 1.5.1 Linux内核在整个操系统中的位置 1.5.2 Linux内核的作用 1.5.3 Linux内核的抽象结构 1.6 Linux内核源代码 1.6.1 多版本的内核源代码 1.6.2 Linux内核源代码的结构 1.6.3 从何处开始阅读源代码 ...
  • 详解神秘Linux内核

    万次阅读 2012-04-28 20:12:42
    本章要先浏览一下内核发出的启动信息,然后再逐个讲解一些有意思的点。 2.1 启动过程 图2-1显示了基于x86计算机Linux系统的启动顺序。第一步是BIOS从启动设备中导入主引导记录(MBR),接下来MBR中的代码查看分区表...
  • 内核模块调试

    千次阅读 2009-08-16 16:37:00
    对于任何一位内核代码的编写者来说,最急迫的问题之一就是如何完成调试。由于内核是一个不与特定进程相关的功能集合,所以内核... 本章将介绍这种令人痛苦的环境下监视内核代码并跟踪错误的技术。 4.1 通过打印调试
  • 疯狂内核之——Linux虚拟内存

    热门讨论 2011-05-30 23:38:37
    2.4.3 释放页面到每CPU 页面高速缓存 83 2.5 slab分配器 85 2.5.1 数据结构 86 2.5.2 分配/释放slab页面 92 2.5.3 增加slab数据结构 93 2.5.4 高速缓存内存布局 94 2.5.5 slab着色 95 2.5.6 分配slab对象 96 2.5.7 ...
  • Linux内核目录和常用头文件的含义

    千次阅读 2018-03-21 14:30:01
    一、linux内核目录 -(2.6内核) ① arch目录:包括了所有和体系结构相关的核心代码。它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel CPU及与之相兼容体系结构的子目录。PC机一般都基于...
  • 国嵌Linux视频内核开发

    千次阅读 2012-02-26 16:10:54
    1.1 Linux内核简介 从上图得知,Linux由用户空间和内核空间两...用户模式(usr)、快速中断(fiq)、外部中断(irq)、管理模式(svc)、数据访问中止 (abt)、系统模式(sys)、未定义指令异常(und) X86也实现了4个不同的级别
  • Linux 内核引导选项简介

    千次阅读 2017-02-23 20:20:41
    比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"__setup
  • Linux内核工程导论——内核调试

    千次阅读 2015-08-27 19:50:45
    健壮性是printk最容易被接受的一个特质,几乎任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。 系统启动过程中,终端初始化之前,某些地方是
  • 要了解什么是用户态,什么是内核态,我们需要先了解什么是进程的用户空间和内核空间:Linux虚拟内存的大小为2^32(32位的x86机器上),内核将这4G字节的空间分为两部分。最高的1G字节(从虚地址0xC0000000到0...
  • Linux内核源码特殊用法

    千次阅读 2013-12-30 23:08:18
    Linux内核源码特殊用法 1 前言 Linux内核源码主要以C语言为主,有一小部分涉及汇编语言,编译器使用的是Gcc。初次看内核源码,会遇到一些难以理解、晦涩的代码;而恰恰是这些晦涩的代码,在内核源码中经常出现。...
  • 54-System V IPC 内核对象

    千次阅读 2017-01-09 16:41:27
    你基本掌握了共享内存的使用方法后,接...具体的对于共享内存、消息队列和信号量,他们在内核空间中都有对应的结构体来描述。当你使用 get 后缀创建内核对象时,内核中就会为它开辟一块内存保存它。只要你不显式删
  • 读书笔记:LINUX内核完全剖析 IBM PC及其兼容机主要使用独立编址方式,采用独立的I/O地址空间对控制设备中的寄存器进行寻址和访问,IBM PC也部分地使用统一编址。对于使用EISA、PCI等总线结构的PC,有64KB的I/O地址...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 70,297
精华内容 28,118
关键字:

内核数据在页面错误