Linux空间及其操作

一 Linux 内存

  在 Linux 中,用户内存和内核内存是独立的,在各自的地址空间实现。由于地址空间是虚拟的,所以可以存在很多。事实上,内核本身驻留在一个地址空间中,每个进程驻留在自己的地址空间。这些地址空间由虚拟内存地址组成,允许一些带有独立地址空间的进程指向一个相对较小的物理地址空间(在机器的物理内存中)。因为每个地址空间是独立且隔离的,因此很安全。
  因为每个进程(和内核)会有相同地址指向不同的物理内存区域,不可能立即共享内存。幸运的是,有一些解决方案。用户进程可以通过 Portable Operating System Interface for UNIX? (POSIX) 共享的内存机制(shmem)共享内存,但有一点要说明,每个进程可能有一个指向相同物理内存区域的不同虚拟地址。
  虚拟内存到物理内存的映射通过页表完成,这是在底层软件中实现的。硬件本身提供映射,但是内核管理表及其配置。注意这里的显示,进程可能有一个大的地址空间,但是很少见,就是说小的地址空间的区域(页面)通过页表指向物理内存。这允许进程仅为随时需要的网页指定大的地址空间。
  由于缺乏为进程定义内存的能力,底层物理内存被过度使用。通过一个称为 paging(然而,在 Linux 中通常称为 swap)的进程,很少使用的页面将自动移到一个速度较慢的存储设备(比如磁盘),来容纳需要被访问的其它页面。这一行为允许,在将很少使用的页面迁移到磁盘来提高物理内存使用的同时,计算机中的物理内存为应用程序更容易需要的页面提供服务。注意,一些页面可以指向文件,在这种情况下,如果页面是脏(dirty)的,数据将被冲洗,如果页面是干净的(clean),直接丢掉。
  选择一个页面来交换存储的过程被称为一个页面置换算法,可以通过使用许多算法(至少是最近使用的)来实现。该进程在请求存储位置时发生,存储位置的页面不在存储器中(在存储器管理单元 [MMU] 中无映射)。这个事件被称为一个页面错误,并被硬件(MMU)删除,出现页面错误中断后该事件由防火墙管理。

二 页面置换

  Linux 提供一个有趣的交换实现,该实现提供许多有用的特性。Linux 交换系统允许创建和使用多个交换分区和优先权,这支持存储设备上的交换层次结构,这些存储设备提供不同的性能参数(例如,固态磁盘 [SSD] 上的一级交换和速度较慢的存储设备上的较大的二级交换)。为 SSD 交换附加一个更高的优先级使其可以使用直至耗尽;直到那时,页面才能被写入优先级较低的交换分区。
  并不是所有的页面都适合交换。考虑到响应中断的内核代码或者管理页表和交换逻辑的代码,显然,这些页面决不能被换出,因此它们是固定的,或者是永久地驻留在内存中。尽管内核页面不需要进行交换,然而用户页面需要,但是它们可以被固定,通过 mlock(或 mlockall)函数来锁定页面。这就是用户空间内存访问函数的目的。如果内核假设一个用户传递的地址是有效的且是可访问的,最终可能会出现内核严重错误(kernel panic)(例如,因为用户页面被换出,而导致内核中的页面错误)。该应用程序编程接口(API)确保这些边界情况被妥善处理。

三 内核 API

      用户空间内存访问 API

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

  1、access_ok函数
  您可以使用 access_ok 函数在您想要访问的用户空间检查指针的有效性。调用函数提供指向数据块的开始的指针、块大小和访问类型(无论这个区域是用来读还是写的)。函数原型:access_ok( type, addr, size );
  type 参数可以被指定为 VERIFY_READ 或 VERIFY_WRITE。VERIFY_WRITE 也可以识别内存区域是否可读以及可写(尽管访问仍然会生成 -EFAULT)。该函数简单检查地址可能是在用户空间,而不是内核。
  2、get_user 函数
  要从用户空间读取一个简单变量,可以使用 get_user 函数,该函数适用于简单数据类型,比如,char 和 int;但是像结构体这类较大的数据类型,必须使用 copy_from_user 函数。该原型接受一个变量(存储数据)和一个用户空间地址来进行 Read 操作:get_user(x, ptr);
  get_user 函数将映射到两个内部函数其中的一个。在系统内部,这个函数决定被访问变量的大小(根据提供的变量存储结果)并通过 __get_user_x 形成一个内部调用。成功时该函数返回 0,一般情况下,get_user 和 put_user 函数比它们的块复制副本要快一些,如果是小类型被移动的话,应该用它们。
  3、put_user 函数
  您可以使用 put_user 函数来将一个简单变量从内核写入用户空间。和 get_user 一样,它接受一个变量(包含要写的值)和一个用户空间地址作为写目标:put_user( x, ptr );
  和 get_user 一样,put_user 函数被内部映射到 put_user_x 函数,成功时,返回 0,出现错误时,返回 -EFAULT。
  4、clear_user 函数
  clear_user 函数被用于将用户空间的内存块清零。该函数采用一个指针(用户空间中)和一个型号进行清零,这是以字节定义的:clear_user(ptr, n);
  在内部,clear_user 函数首先检查用户空间指针是否可写(通过 access_ok),然后调用内部函数(通过内联组装方式编码)来执行 Clear 操作。使用带有 repeat 前缀的字符串指令将该函数优化成一个非常紧密的循环。它将返回不可清除的字节数,如果操作成功,则返回 0。
  5、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 之后,该过程只不过是一个优化的副本。
  6、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。从此开始,根据构架,为执行从用户缓冲区到内核缓冲区的零拷贝(不可用字节)而进行一个调用。优化组装函数包含管理功能。
  7、strnlen_user 函数
  strnlen_user 函数也能像 strnlen 那样使用,但前提是缓冲区在用户空间可用。strnlen_user 函数带有两个参数:用户空间缓冲区地址和要检查的最大长度。strnlen_user(src, n);
  strnlen_user 函数首先通过调用 access_ok 检查用户缓冲区是否可读。如果是 strlen 函数被调用,max length 参数则被忽略。
  8、strncpy_from_user 函数
  strncpy_from_user 函数将一个字符串从用户空间复制到一个内核缓冲区,给定一个用户空间源地址和最大长度。strncpy_from_user(dest, src, n);
  由于从用户空间复制,该函数首先使用 access_ok 检查缓冲区是否可读。和 copy_from_user 一样,该函数作为一个优化组装函数(在 ./linux/arch/x86/lib/usercopy_XX.c 中)实现。

四 用户 API

  Kernel Panic常见原因以及解决方法:https://www.cnblogs.com/cherishui/p/3881428.html
  用户空间动态申请内存用的函数是 malloc(),这个函数在各种操作系统上的使用是一致的,对应的用户空间内存释放函数是 free()。在内核空间中如何申请内存一般我们会用到 kmalloc()、kzalloc()、vmalloc() 等。

1 kmalloc()

  函数原型:void *kmalloc(size_t size, gfp_t flags);
  kmalloc() 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。
  较常用的 flags(分配内存的方法):
  GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
  GFP_KERNEL —— 正常分配内存;
  GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。

flags 的参考用法:
  |– 进程上下文,可以睡眠     GFP_KERNEL
  |– 进程上下文,不可以睡眠    GFP_ATOMIC
  |  |– 中断处理程序       GFP_ATOMIC
  |  |– 软中断          GFP_ATOMIC
  |  |– Tasklet         GFP_ATOMIC
  |– 用于DMA的内存,可以睡眠   GFP_DMA | GFP_KERNEL
  |– 用于DMA的内存,不可以睡眠  GFP_DMA |GFP_ATOMIC

  对应的内存释放函数为:void kfree(const void *objp);

2 kzalloc()

  kzalloc() 函数与 kmalloc() 非常相似,参数及返回值是一样的,可以说是前者是后者的一个变种,因为 kzalloc() 实际上只是额外附加了 __GFP_ZERO 标志。所以它除了申请内核内存外,还会对申请到的内存内容清零。

/**
  * kzalloc - allocate memory. The memory is set to zero. * @size: how many bytes of memory are required. * @flags: the type of memory to allocate (see kmalloc). 
  */
static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO);
}

  kzalloc() 对应的内存释放函数也是 kfree()

3 vmalloc()

  函数原型:void *vmalloc(unsigned long size);
  vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。
  对应的内存释放函数为:void vfree(const void *addr);
  注意:vmalloc() 和 vfree() 可以睡眠,因此不能从中断上下文调用。

4 总结

  kmalloc()、kzalloc()、vmalloc() 的共同特点是:

用于申请内核空间的内存;
内存以字节为单位进行分配;
所分配的内存虚拟地址上连续;

  kmalloc()、kzalloc()、vmalloc() 的区别是:

kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;
kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;

  一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。例如,当模块被动态加载到内核当中时,就把模块装载到由 malloc() 分配的内存上。

5 缓存IO

  缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。
  读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。
  写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令(详情参考《【珍藏】linux 同步IO: sync、fsync与fdatasync》)。

6 直接IO

  在Linux 2.6中,内存映射和直接访问文件没有本质上差异,因为数据从进程用户态内存空间到磁盘都要经过两次复制,即在磁盘与内核缓冲区之间以及在内核缓冲区与用户态内存空间。
  引入内核缓冲区的目的在于提高磁盘文件的访问性能,因为当进程需要读取磁盘文件时,如果文件内容已经在内核缓冲区中,那么就不需要再次访问磁盘;而当进程需要向文件中写入数据时,实际上只是写到了内核缓冲区便告诉进程已经写成功,而真正写入磁盘是通过一定的策略进行延迟的。
  直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。
  直接IO的缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓存。通常直接IO与异步IO结合使用,会得到比较好的性能。(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待)
  用Redis作缓存是因为,Redis就是设计来做缓存的,Reids作缓存的几大优势
  1、简单的K-V式数据存储方式,单一的 get set 模式比传统SQL性能提升显著
  2、纯in mem db 形式,将数据缓存在内存中,减少服务器磁盘IO时间。

7 内存映射

  减少数据在用户空间和内核空间之间的拷贝操作,适合大量数据传输。
  Linux内核提供一种访问磁盘文件的特殊方式,它可以将内存中某块地址空间和我们要指定的磁盘文件相关联,从而把我们对这块内存的访问转换为对磁盘文件的访问,这种技术称为内存映射(Memory Mapping)。
  操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的。
  操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的。这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。
  使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
  在大多数情况下,使用内存映射可以提高磁盘I/O的性能,它无须使用read()或write()等系统调用来访问文件,而是通过mmap()系统调用来建立内存和磁盘文件的关联,然后像访问内存一样自由地访问文件。
  有两种类型的内存映射,共享型和私有型,前者可以将任何对内存的写操作都同步到磁盘文件,而且所有映射同一个文件的进程都共享任意一个进程对映射内存的修改;后者映射的文件只能是只读文件,所以不可以将对内存的写同步到文件,而且多个进程不共享修改。显然,共享型内存映射的效率偏低,因为如果一个文件被很多进程映射,那么每次的修改同步将花费一定的开销。

8 PIO与DMA

  有必要简单地说说慢速I/O设备和内存之间的数据传输方式。
  PIO:我们拿磁盘来说,很早以前,磁盘和内存之间的数据传输是需要CPU控制的,也就是说如果我们读取磁盘文件到内存中,数据要经过CPU存储转发,这种方式称为PIO。显然这种方式非常不合理,需要占用大量的CPU时间来读取文件,造成文件访问时系统几乎停止响应。
  DMA:后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,是所有现代电脑的重要特色,它可以不经过CPU而直接进行磁盘和内存的数据交换。在DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据的传送即可,DMA控制器通过系统总线来传输数据,传送完毕再通知CPU,这样就在很大程度上降低了CPU占有率,大大节省了系统资源,而它的传输速度与PIO的差异其实并不十分明显,因为这主要取决于慢速设备的速度。

9 Zero-Copy&Sendfile()

  Linux 2.1版本内核引入了sendfile函数,用于将文件通过socket传送。通过sendfile传送文件只需要一次系统调用,当调用 sendfile时:

1、首先通过DMA copy将数据从磁盘读取到kernel buffer中
2、然后通过CPU copy将数据从kernel buffer copy到sokcet buffer中
3、最终通过DMA copy将socket buffer中数据copy到网卡buffer中发送

  sendfile与read/write方式相比,少了 一次模式切换一次CPU copy。但是从上述过程中也可以发现从kernel buffer中将数据copy到socket buffer是没必要的。为此,Linux2.4内核对sendfile做了改进:

1、DMA copy将磁盘数据copy到kernel buffer中
2、向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
3、DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡上。
经过上述过程,数据只经过了2次copy就从磁盘传送出去了。

五 内存编程

1.内存分配方式
1.1内存分配的几种方式
(1) 从静态存储区域分配。
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
(2) 在栈上创建。
在执行函数时,函数的参数值,内局部变量的存储单元都可以在栈上创建。函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。
程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
例子程序
//main.cpp
int a = 0; //静态存储区(初始化区域)
char *p1; //静态存储区(未初始化区域)
void example()
{
int b; //栈
char s[] = “abc”; //栈
char *p2; //栈
static int c =0; //静态存储区(初始化区域)
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); //分配得来的10和20字节的区域就在堆上
}
另外,在嵌入式系统中有ROM和RAM两类内存,程序被固化进ROM,变量和堆栈设在RAM中,用const定义的常量也会被放入ROM。
注:用const定义常量可以节省空间,避免不必要的内存分配。
例如:
#define PI 3.14159 //常量宏
const double g_pi = 3.14159; //此时并未将Pi放入ROM中

double a = g_pi; //此时为Pi分配内存,以后不再分配!
double b =PI;  //编译期间进行宏替换,分配内存
double c = g_pi; //没有内存分配
double d = PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
1.2几种分配方式的内存生命期
(1) 静态分配的区域的生命期是整个软件运行期,就是说从软件运行开始到软件终止退出。只有软件终止运行后,这块内存才会被系统回收。
(2) 在栈中分配的空间的生命期与这个变量所在的函数、类和Block(即由{}括起来的部分)相关。如果是函数中定义的局部变量,那么它的生命期就是函数被调用时,如果函数运行结束,那么这块内存就会被回收。如果是类中的成员变量,则它的生命期与类实例的生命期相同。如果在Block中定义的局部变量,则它的生命期仅在Block内。
(3) 在堆上分配的内存,生命期是从调用new或者malloc开始,到调用delete或者free结束。如果不调用delete或者free,则这块空间只有到软件运行结束后才会被Windows系统回收。
2.常见的内存错误及其对策
(1) 内存分配未成功,却使用了它。
犯下这种错误主要原因是没有意识到内存分配会不成功。
常用解决办法是,在使用内存之前检查指针是否为NULL。
如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。
如果是用new或者malloc来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。若指针为NULL,则应立即返回相应的错误码,说明内存不足而中止调用。
int Func(void)
{
char *p = (char *) malloc(100);
if(p == NULL)
{
return ERR_NO_MEMORY;
}

}
(2) 内存分配虽然成功,但是尚未初始化就引用它。
犯下这种错误主要原因有两个:
一是没有初始化的观念;
二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
内存的缺省初值究竟是什么并没有统一的标准。但是对于全局变量和静态变量如果没有手工初始化,编译器会将其初始化为零,而对栈内存和堆内存则不作任何处理。
另外,VC在Debug和Release状态下在初始化变量时所做的操作是不同的。Debug是将每个字节位都赋值成0xcc,以有利于调试。而Release的赋值是直接从内存中分配的,内容近似于随机。所以如果在没有初始化变量的情况下去使用它的值,就会导致问题发生。
无论用何种方式创建数组,都不要忘记赋初值,可以使用memset为数组赋零值。
#define AVP_STREAM_RCV_BUFFER_NUM (5)
#define AVP_STREAM_SND_BUFFER_NUM (5)
……
AVP_StreamRcvBuffer_t g_AVP_StreamRcvBufferList[AVP_STREAM_RCV_BUFFER_NUM];
AVP_StreamSndBuffer_t g_AVP_StreamSndBufferList[AVP_STREAM_SND_BUFFER_NUM];
……
memset( g_AVP_StreamRcvBufferList, 0, sizeof( g_AVP_StreamRcvBufferList ) );
memset( g_AVP_StreamSndBufferList, 0, sizeof( g_AVP_StreamSndBufferList ) );
……
此外,任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
例如
char *p = NULL;
int *i = (int *) malloc(100);
(3) 内存分配成功并且已经初始化,但操作越过了内存的边界,导致缓冲区溢出。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
越界?越谁的界?当然是内存。一个变量存放在内存里,想读的是这个变量,结果却读过头了,很可能读到了另一个变量的头上。这就造成了越界。
访问越界会出现什么结果?
首先,它并不会造成编译错误! 就是说,C/C++的编译器并不判断和指出代码“访问越界”了。此外,数组访问越界在运行时,它的表现是不定的,有时似乎什么事也没有,程序一直运行(当然,某些错误结果已造成);有时,则是程序一下子崩溃。
请看下面的例子:
让用户输入学生编号,查询学生的考试成绩。如果代码是这样:
int mark[100]; 

//让用户输入学生编号,设现实中学生编号由1开始:
cout << “请输入学生编号(在1~100之间):”
int i;
cin >> i;
//输出对应学生的考试成绩:
cout << info[i-1];

这段代码看上去没有什么逻辑错误。可是,某些用户会造成它出错。如果用户不输入1到100之间的数字,而是输入105,甚至是-1。这样程序就会去尝试输出:mark[104] 或 mark[-2],导致数组操作越界。
对于这类问题的解决办法就是,我们需要在输出时,做一个判断,发现用户输入了不在编号范围之内的数,则不输出或者提示用户重新输入合法值。这样就会避免错误出现。

以上是数组读操作的越界,同样地,在对一块缓冲区进行写操作时,如果向缓冲区内填充的数据位数超过了缓冲区本身的容量,便会发生缓冲区溢出。
当一个超长的数据进入到缓冲区时,超出部分就会被写入其他缓冲区,其他缓冲区存放的可能是数据、下一条指令的指针,或者是其他程序的输出内容,这些内容都被覆盖或者破坏掉。可见一小部分数据或者一套指令的溢出就可能导致一个程序或者操作系统崩溃。
请看下面的代码:
void DoSomething (char *cBuffSrc, DWORD dwBuffSrcLen)
{
  char cBuffDest[32] ;
  memcpy (cBuffDest, cBuffSrc, dwBuffSrcLen) ;
}
上面的函数在参数dwBuffSrcLen的实际值小于等于cBuffDest的长度时不会出现问题,但是如果dwBuffSrcLen的值大于cBuffDest的长度,当memcpy 将数据复制到 cBuffDest 中时,来自 DoSomething 的返回地址就会被更改,因为 cBuffDest 在函数的堆栈框架上与返回地址相邻。
如果将函数进行适当的修改,使 memcpy 的调用具有防御性,它将不会复制多于目标缓冲区存放能力的数据了。
void DoSomething (char cBuffSrc, DWORD dwBuffSrcLen)
{
  const DWORD dwBuffDestLen = 32 ;
  char cBuffDest[dwBuffDestLen] ;
  memcpy (cBuffDest, cBuffSrc, min(dwBuffDestLen, dwBuffSrcLen)) ;
}
(4) 分支处理不完整,错误处理不当,导致忘记释放内存,造成内存泄露。
我们常说的内存泄漏一般是指堆内存的泄漏。应用程序一般使用malloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
以下这段小程序演示了堆内存发生泄漏的情形:
void MyFunction(int nSize)
{
char
p= new char[nSize];
if( !GetStringFrom( p, nSize ) ){
MessageBox(“Error”);
return;
}
…//using the string pointed by p;
delete p;
}
当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是C函数可以在任何地方退出,所以一旦对分支处理不完整或者错误处理不当的话,就会发生内存泄漏。虽然函数体内的局部变量在函数结束时自动消亡,但是局部的指针变量所指向的内存并不会被自动释放。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,可能看不到错误,但终有一次程序突然死掉,系统出现提示:内存耗尽。
动态内存的申请与释放必须配对,如果程序在入口处动态申请了内存,那么在程序的每个出口处都必须释放该内存空间。
(5) 释放了内存却继续使用它。
有三种情况:
(a) 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(b) 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
© 使用free释放了内存后,没有将指针设置为NULL。导致产生“野指针”,即不是NULL指针,而是指向“垃圾”内存的指针。“野指针”是很危险的,因为使用if语句进行判断对它不起作用。
char *p = (char *) malloc(100);
strcpy(p, “hello”);
free§; // p 所指的内存被释放,但是p所指的地址仍然不变

if(p != NULL) // 没有起到防错作用
{
strcpy(p, “world”); // 出错
}
同样地,指针操作超越了变量的作用范围也造成“野指针”,示例程序如下:
class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // a 的生命期仅在Block内
}
p->Func(); // p是“野指针”
}
函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。由于a所占据的内存并没有被覆盖,所以暂时不会出现问题。但是当堆栈发生变化后,如调用函数或者定义了新的局部变量,则将发生内存错误。
3.指针与数组的对比
C/C++程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
下面以字符串为例比较指针与数组的特性。
3.1修改内容
//数组
char a[] = “hello”;
a[0] = ‘X’;
// 指针
char *p = “world”; // 注意p指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误
字符数组a的容量是6个字符,其内容为hello\0。a的内容可以改变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。
3.2内容复制与比较
不能对数组名进行直接复制与比较。
// 数组…
char a[] = “hello”;
char b[10];
strcpy(b, a); // 不能用b = a;
if(strcmp(b, a) == 0) // 不能用if (b == a)

// 指针…
int len = strlen(a);
char *p = (char )malloc(sizeof(char)(len+1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(ba) 来判断,应该用标准库函数strcmp进行比较。
语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p
a) 比较的不是内容而是地址,应该用库函数strcmp来比较。
3.3计算内存容量
用运算符sizeof可以计算出数组的容量(字节数)。
char a[] = “hello world”;
char p = a;
sizeof(a) = ? (12字节, 注意别忘了’\0’)
sizeof§ = ? (4字节)
sizeof§得到的是一个指针变量的字节数,相当于sizeof(char
),而不是p所指的内存容量。
注意:当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
void Func(char a[100])
{

}
sizeof(a) = ? (4字节而不是100字节)
不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。
4.实例解析
不要用函数的指针参数去申请动态内存
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, “hello”); // 运行错误
}
解析:Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL。问题出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”。
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意参数是 &str,而不是str
strcpy(str, “hello”);
printf(%s, str);
free(str);
}
不要返回临时变量的指针和引用
void loop();
void addr();
int main ()
{
addr();
loop();
}

long *p ;
void  loop()
{
long i, j ;
j = 0;
for ( i = 0 ; i < 10 ; i++ ){
(*p)–;
j++;
}
}
 void   addr()
{
long k;
k = 0;
p = &k;
}
解析:这里的问题出现在保存临时变量的地址上。由于addr函数中的变量k在函数返回后就已经不存在了,但是在全局变量p中却保存了它的地址。在下一个函数loop中,试图通过全局指针p访问一个不存在的变量,而这个指针实际指向的却是另一个临时变量i,这就导致了死循环的发生。
看一下这个程序中局部变量的地址分配。addr()中的局部变量k,loop()中的局部变量i、j,它们的地址分配可以如下图所示:
j
k/i

p------

可以理解为i和k占用同一个内存单元(因为他们都是局部变量,不可能同时出现在执行语句中而导致冲突)。在addr()函数中,系统为变量k安排了地址,并将指针p指向k所在的单元,当从addr()函数返回的时候,系统收回了分配给k的地址(这些实际上就是在栈里进行的)。在进入loop()函数以后,就一次为局部变量i,j分配地址,因为i的类型和k相同,所以它占用的空间大小和k相同,系统按序分配地址,很显然分配给i的地址就是在addr()中分配给k的地址。因为指针p是一个全局变量,它的值(此时即i所在的单元地址)未变,所以现在p所指的是现在的i所在的地址,故(*p)–实际上成了i–,所以i一直在-1和0之间变化,程序陷入死循环。
数组访问越界
int main ()
{
int i;
int a[10];

for(i=0; i<=10; ++i)
a[i] = 0;
return 0;
}
解析:在main中,i和数组a是采用静态存储分配策略的。它们所占的空间大小在编译时是确定的。但是从高地址开始分配空间的。如下所示:
a[0]
a[1]
a[2]
a[3]
a[4]
a[5]
a[6]
a[7]
a[8]
a[9]
i
低地址

高地址
数组实际上就是一块内存,a就是数组的首地址,[]中的值是偏移值,所以a[10]实际上就是i,a[i] = 0就是i=0,导致死循环。
int    i;
int    a[10] ;
只要将上面两句交换一下位置,在这个程序中就不会死循环了。
当然,访问a[10]本来就是一个错误,数组越界,后果不堪设想,由于C对数组没有越界检查,所以编译没问题。
将一个数组赋值为等差数列,并将会在函数的外部使用它
int *GetArray( int n )
{
int *p = new int[n];
for ( int i = 0; i < n; i++ )
{ p[i] = i; }
return p;
}
解析:检查内存泄露的最好办法,就是检查完全配对的申请和释放,在函数中申请而在外部释放,将导致代码的一致性变差,难以维护。而且,一个人写的函数不一定是他自己使用的,这样的函数别人会不知道该怎么适当的使用。因此最好的解决办法就是在函数调用的外面将内存申请好,函数只对数据进行复制。
void GetArray( int *p, int n )
{
for ( int i = 0; i < n; i++ )
{ p[i] = i; }
} 
写一个类封装对指针的申请内存、释放和其它一些基本操作
class A
{
public:
A( void ) {}
~A( void ) { delete []m_pPtr; }
void Create( int n ){ m_pPtr = new int[n]; }
private:
int *m_pPtr;
};
解析:不合理的代码就在于当重复调用Create的时候就会造成内存泄露,解决的办法就是在new之前判断一下指针是否为0。要能够有效的执行这个判断,则必须在构造的时候对指针进行初始化,并为这个类添加一个Clear函数来释放内存。
如果是C程序,可以使用自己的函数来封装malloc和free,虽然这样不能避免内存泄漏,但是至少可以使内存泄漏变得容易检查。
class A
{
public:
A( void ) : m_pPtr(0){}
~A( void ) { Clear(); }
bool Create( int n ){
if ( m_pPtr ) return false;
m_pPtr = new int[n];
return ture;
}
void Clear( void ) { delete []m_pPtr; m_pPtr = 0; }
private:
int *m_pPtr;
};
5.小结
内存编程的几点规则:
规则1-用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
规则2-不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
规则3-避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
规则4-动态内存的申请与释放必须配对,防止内存泄漏。
规则5-用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值