嵌入式Linux C语言程序设计十

10.1 内存管理

内存的使用是程序设计中需要考虑的重要因素之一,这不仅由于系统内存是有限(尤其在嵌入式系统中),而且内存分配也会直接影响到程序的效率.因此要对C语言中的内存管理有个系统的了解.
在c语主中,定义了4个内存区间:代码区,全局变量与静态变量区;局部变量区即栈区;动态存储区,即椎区.

  1. 代码区,代码构中主要存放程序中的代码,属性是只读的.
  2. 全局变量与静态变量区. 也称为静态存储区域. 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.如:全局变量,静态变量和字符串常量.分配在这个区域中的变量,当程序结束时,才释放内存.因此,经常利用这样的变量,在函数间传递信息.
  3. 栈区.在栈上创建.在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释方.栈内存分配运算内置于处理器指令集中,效率很高,但是分配的内存空量有限,在Linux系统中,通过命令ulimit -s,可以看到栈的容量为8192kbytes,即8M
    这种内存方式,变量内存的分配和释放都自动进行,程序员不需要考虑内存管理的问题,很方便使用,但缺点是,栈的容量有限制,且当相应的范围结束时,局部变量就不能再使用.
  4. 堆区.有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为它们预先分配空间,只能在程序运行时分配,所以称为动态分配.
struct employee
{
	char name[8];
	int age;
	char gender;
	float salary;
};

在该结构定义中,员工的姓名是用字符数组来存储.若员工的姓名由用户输入,则只有在用衣输入结束后,才能精确知道需要多少内存,在这种情况下,使用动态内存分配更合乎逻辑,应该把结构体的定义改成下面的形式.

struct employee
{
	char *name;
	int age;
	char gender;
	float salary;
}

动态分配内存就是在堆区上分配,程序在运行的时候用malloc申请任意多少的内存,程序员自己负责在何进用free释放内存.动态内存的生存周期由我决定,使用非常灵活,但问题也最多.

#include <stdio.h>;
#include <stdlib.h>;
#include <string.h>;
/*C语言中数据的内存分配*/
int a = 0;
char *a= p1;

int main()
{
	int b;
	char s[] = "abc";
	char *p2;
	char *p3 = "123456";
	static int c = 0;

	p1 = (char*)malloc(10);
	p2 = (char*)malloc(20);

	/*从常 量区的Hello字符串制到刚分配的堆区*/
	strcpy(p1,"Hello");

	return 0;
}

10.2 动态内存的申请和释放

当程序运行到需要一个动态分配的变量时,必须向系统申请取得堆中的一块所需大小的存储空间,用于存储变量.当不再使用该变量时,也就是它的生命结束时,要显式释放它所占用的存储空间,这样系统就能对该堆 空间进行再次分配,做到重复使用有限的资源.

10.2.1 malloc函数

在C语言中,使用malloc函数来申请内存

#include <stdio.h>
void * malloc(size_t size);

其中,参数size代表需要动态申请的内存的字节数.若内存申请成功,函数返回申请到的内存的起始地址,若申请失败,返回NULL.

注意事项

  1. 只关心申请内存的大小,该函数的参数,很简音,只有申请内存的大小,单位是字节.
  2. 申请的是一块连续的内存.该函数一定是申请一块连续的区间,可能申请到的内存比实际申请的大.也可能申请不到,若申请失败,返回NULL,一定记得写出错判断.
  3. 返回值类型void* 函数的返回值是void*,不是某种具体类型的指针,
  4. 显示初始化.堆区是不会自动清零.

10.2.2 free函数

在堆区上分配的内存,需要用free函数显示释放.

#include <stdlib.h>
void free(void *ptr);

函数的参数ptr,指的是需要释放的内存起始地址.该函数没有返回值.

注意事项

  1. 必须提供内存的超始地址.调用该函数,必须提供内存的超始地址,不能提供部分地址,释放内存中的一部分是不允许的.因此,必须保存malloc返回的指针值,若丢失,则所分配的堆空间无法回收,称内存泄漏
  2. malloc和free配对使用.编译器不负责动态内存的释放,需要程序员显示释放.
#include <stdio.h>
#includ <stdlib.h>
#include <string.h>

int *get_memory(int n)
{
	int *p,i;
	if((p=(int *)malloc(n *sizeof(int)))==NULL)
	{
		printf("malloc error\n");
		return p;
	}
	memset(p,0,sizeof(int));
	for(i=-;i<n;i++)
	{
		p[i] = i+1;
	}
	return p;
}
int main()
{
	int n,*p,i;
	
	printf("input n:");
	scanf("%d",&n);
	if((p=get_memory(n))==NULL)
	{
		return 0;
	}
	for(i=0;i<n;i++)
	{
		printf("%d",p[i]);
	}
	printf("\n");

	free(p);
	p=NULL;
	return 0;
}
  1. 不允许复释放.同一空间的重复重释放也是危险的,因为空间可能已另分配.
  2. free 只能释放堆空间.像代码区,全局变量与静态变量区,栈区上的变量,都不需要程序员显示释入,这些区域上的空间,不能通过free函数释放.否则执行时,会出错.
#include <stdlib.h>
int main()
{
	int a[10]={0};
	free(a);
	retrun 0;
}

10.2.3 关于野指针

野指针指的是指向"垃级"内存的指针,不是NULL指针
原因

  1. 指针变量没有被初始化,指针变量和其他的变量一样,若没有初始化,值是不确定的,也就是说,没有初始化的指针,指向的是垃圾内存,非常危险.
#include <stdio.h>
int main()
{
	int *p;
	printf("%d\n",*p);

	*p = 10;
	printf("%d\n",*p);

	return 0;
}
  1. 指针p 被free之后,没有置为NULL.free函数是把指针所指向的内存释放掉,使内存成为了自由内存.但是该函数并没有把指针本身的内容清除.指针仍指向已释放的动态内存,这是很危险的.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	int n = 5,*p,i;
	if((p=(int*)malloc(n*sizeof(int)))==NULL)
	{
		printf("malloc error\n");
		return 0;
	}
	memset(p,0,n*sizeof(int));

	for(i = 0;i<n;i++)
	{
		p[i]=i+1;
		printf("%d",p[i]);
	}
	printf("\n");

	printf("p=%p *p=%d\n",p,*p);
	free(p);
	printf("after free:p=%p *p=%d\n",p,*p);

	*p = 100;
	printf("p=%p *p =%d\n",p,*p);

	return 0;
}

  1. 指针操作超越了变量的作用范围.
    e.g:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	int a[5] = {1,9,6,2,10},*p,i,n;

	n = sizeof(a)/sizeof(n);
	p = a;

	for(i = 0;i<=n;i++)
	{
		printf("%d",*p);
		p++;
	}
	printf("\n");

	*p = 100;
	printf("*p=%d\n",*p);

	return 0;
}
  1. 不要返回指向栈内存的指针.

10.3 堆与栈的区别

  1. 申请方式
    栈(stack)是由系统自动分配的.例如,声明函数中一个局部变量int b;那系统自动在栈中为b开辟空间.堆(heap)需要程序员自己申请,并在申请时指定大小,使用C语言中的malloc函数
    p1 = (char *)malloc(10)

  2. 申请后系统的响应.
    堆在操作系统中有一个记录空间内存地址的链表,当系统收到程序的申请时,系统就会开始遍历链表,寻找一个空间大于所有申请空间的堆节点,然后将该节点空间节点链中删除,并将该节点的空间分配给程序.别外,大多数系统.会成这块内存空间首地址外记录本次分配的大小,它首地址处记录本次分配的大小.这样, 删除语句才能释放本内存空间,如果找到的堆节点大小与申请的大小不相同,系统会自动地将多余的那部分重新空闲链表中.
    只有栈的剩余空间大于所申请空间,系统才为程序提供内存,否则空间,如果找到的堆节点大小与申请的大小不相同,系统会自动地将多余的那部分重新空闲链表中.
    只有栈的剩余空间大于所申请空间,系统才为程序提供内存,否则将报异常,提示栈溢出.

  3. 申请大小的限制
    堆是和高地址扩展,是一连续的内存区域.这是由于系统用链表来存储的空间内存地址.地址是不续续的,而链表的遍历方和由低址向高地址.堆的大小受限于计算机系统中有效的虚拟内存,因为堆获得的空间比较灵活,也比较大.
    栈是向低地址扩展的数据结构,是一块连续的内存区域.因此,栈顶的地址和栈的最大容量是系统预先规定好,如果申请的空间超过栈的剩余空间时,将提示栈溢出.因此,能从栈获得空间较小.

  4. 申请速度的限制
    堆一般在椎的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排.

##10.4 动态内存程序举例

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define N 20

struct employee
{
	char *name;
	int age;
	float salary;
}

int main()
{
	struct employee *p1;
	if((p1 = (struct employee*)malloc(sizeof(struct employee)))==NULL)
	{
		printf("malloc struct error");
		return 0;
	}
	memset(p1,0,sizeof(struct employee));

	if((p->name = (char *)malloc(N))==NULL)
	{
		printf("malloc name error");
		return 0;
	}
	printf("name");
	scanf("%s",p1->name);

	getchar();
	printf(age:);
	scanf("%d",&p1->age);

	printf("salary:");
	scanf("%f",&p1->salary);

	printf("\ninformation:name:%s age:%d salary:%f\n",p1->name,p1->age,p1->salary);

	freee(p->name);
	p1->name = NULL;

	free(p1);
	p1 = NULL;

	return 0;
}

程序中先释放name 再释放p1

10.5 C语言和汇编语言的接口

10.5.1 内联汇编语法.

ARM体系结构下的__asm__内联汇编语用法,带有C/C++语法表达式的内联汇编格式为
__asm__ (汇编语句模板:输出部分:输入部分:破坏描述部分)

asm 是GCC 关键字asm的宠定义.
asm asm用来声明一个内联汇编表达式.所以任何一个内联汇编表达式都是以它开头的,是必不可少的.

#define __asm__ asm 
#define __volatile__ volatile

有时在__asm__后面使用__volatile__.__volatile__或volatile是可选的,如果用了它,则是向GCC声明不允许对该内联汇编优化,否则当使用了优化选项 -O 进行编译时,GCC 将根据自己的判断决定是否对这个内联汇编表达式中的指令进行优化.

内联汇编总共由4个部分组成:汇编语句模板,输出部分,输入部分和破坏描述部分,各部分使用":“隔开,如果使用了后面的部分,而前面的部分为空,也需要用”:"隔开,相应部分内容为空,例
__asm__("":::"memory")

  1. 汇编语句模板
    汇编语句模板由汇编语句序列组成,语句之间使用";" “\n” 或"\n\t"分开,它可以是空的,比如"__asm__ __volatile__("");或__asm__("");都是完全合法的内联汇纊表达式,只不过这两条语句没有什么意义.但并非所有汇编语句模板为空的内联江编表达式都没有意义的,比如__asm__("":::"memory");就非演唱上有意义,它向GCC声明"内存做了改动",GCC在编译的时候,会将此因素考虑进去.当在汇编语句模板中有多条指令时候,可以参一对引号中列出全部指令,也可以将一条或几条指令放在一对引号中,所有指令放在多对引号中,如果是前者,可以将每一条指令放在一行,如要要将多条指令放在一行,则必段用分号(;)或换行符号(\n)将它们分开.
    综上所述,总结如下
  1. 每第指令都必须被双引号括起来.
  2. 两条指令必须用换行或分号分开.
  3. 指令中操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称可为%0,%1…%9.
    在ARM系统结构上关闭中断的操作
    e.g:
int disable_interrupts(void)
{
	unsigned long old,temp;
	__asm__ __volatile__("mrs %0,cpsr\n"
						"orr%1,%0,#0x80\n"
						"msr cpsr_c,%1"
						:"=r"(old),"=r"(temp)
						:
						:"memory");
	return (old & 0x80)==0;
}
  1. 输出部分
    输出部分用来指定当前内联汇编语句的输出
    例 从ARM协处理器p15读出c1值
static unsigned long read_p15_c1(void)
{
	unsigned long value;
	__asm__ __volatile__(
		"mrc p15,0,%0,c1,c0,0 @read control reg\n"
		:"=r(value)" @编译器选择一个R*寄存器
		:
		:"memory"
	);
#ifdef MMU_DEBUG
	printf("p15/c1 is = %081x\n",value);
#endif
	return value;
}

输出部门描述合出操作数,不同的操作数描述之间用逗开,每个操作数描述符由限定字符串和C语言变量组成,每个输出操作数的限定字符串必须包含"="表示它是一个输出操作数.限下字符串表示对该变量的限制条件,这样GCC就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指处理操作数与C语言表达式或C语言变量之间的联系.

  1. 输入部分.
    输入部分用来指定当前内联汇编语句的输入.第个操作数描述符由限定字符串和C语言表达式或者C语言变量组成,格式为形如"constraint"(variable)的列表(不同的操作数描述符之间使用逗号隔开).
    例:
static void write_p15_c1(unsigned long value)
{
#ifdef MMU_DEBUG
	printf("write %0xlx to p15/c1\n",value);
#endif
	__asm__ __volatile__(
		"mcr	p15,0,%0,c1,c0,0	@write it back\n"
		:
		:"r"(value) @编译器选择一个R*寄存器
		:"memory"
	);
}
  1. 破坏描述部分
    有时候,我们想通知GCC当前内联汇编语句可能会对某些寄存 器或内存进行修改,希望GCC在编译时能够将这一点考虑进去,那么就可以在破坏描述部分声明这些寄存器或内存.在这种情况一般发生在一个寄存器出现在汇编变速器句模板中,但不是由输入/输出部分操作表达式所指定的,也不是在一些输入/输出操作表达悬使用"r"约束时由GCC为其选择的,同时此寄存器被汇编语句模板中的指令修改.而这个寄存器只是供当前内联汇编临时使用的情况.

__asm__("mov R0,#0x34":::"R0");

寄存器R0出现在汇编语句模板,并且被mov指令修改,但却未被任何输入/输出部分操作表达式指定,所以需要在破坏描述部分指定"R0",让GCC知道这一点
在输入/输出部分操作表达式所指定的寄存器,或当为一些输入/输出部分操作表达式使"r"约束,让GCC为你选择一个寄存器,因为GCC对这些寄存器是非常清楚的–它知道哪些寄存器是被修改的,所以我们根本不需要在破坏描述部分再声明它们.就最好在破cfgi措述部分中声明它们,让GCC针对些寄存器做相应的处理,否则有可能会造成寄存器的不致,从而造成程序执行错误.

如果一个内联汇编语句的破坏描述部分存在"memory",那么GCC会保护内存数据. 如果在此内联汇编之前,某个内存的内容装入了寄存器,那么在这个内汇联之后,当需要使用这个内存处的内容时,就会直接到这个内存处重新读取,而不是使用普查存放在寄存器中的复制内容,因为这个时候寄 存器中的复制内容已经很可能和内存处 的内容不一致了.
这只是使用"memory"时GCC会保证做到的一点,但并不是全部,因为"memory"是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点.
e.g

int main(int __argc,char* __argv[])
{
	int * __p = (int*)__argc;
	(*__p)=9999;
	__asm__("":::"memory";
	if(*__p)==9999)
	{
		return 5;
	}
	return (*__p);
}

本例中,如果没有那条内联汇编语句,if语句的判断条件是完全多余的.GCC在优化时会意识到这一点,而只生成return 5的汇编代码,不会再生成if语句的相关代码,也不会生成return (__p)的相关代码.但加上了这条内联汇编语名,它除了声明内存变化之外,什么都没骨做.因为内存变量可能发生变化,GCC就不能简单地认为它不需要判断都知道(__p)一定与9999相等,只有老老实实生成这条if语句的汇编代码,以及相关的两个return语句相关代码.

10.5.2编译器优化介绍

由于由存访问题的速度远不及CPU处理速度,因此为提高计算机整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问.另外在现代CPU中指令的执行并不一定严格照顺序执行.没有相关性的指令可乱序执行,以充分利用CPU的指令流水线,提高执行速度,以上是硬件级别的优化.
软件级别的优化有两种:一种是在编写代码时由程序员优化,另一种是由编译器进行优化.编译器优化常用的方法有将内存变量缓存到寄存器和调整指令顺序充分利用CPU指令流水线等,常见的是重新排序读写指令.对常规内存进行优化的时候,这些优化是透明的,而效率很高.
由编译器优化或者硬件重排序引起的问题的解决办法是在特定顺序执行的操作之间设置内存屏障(memory barrier),Linux提供了一个宠用于解决编译器的执行顺序问题.
void barrier(void)

主要是保证程序的执行遵循顺序一致性.有时候写代码的顺序,不一定是最终执行的顺序,这个是与处理器有关的.这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修秘诀过的数值存入内存,需要这些数据的时候再重新内存中读出.

10.5.3 C语言关键volatile

C语言关键字volatile表明某个变量的值可能随时被外部改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新读取.
该关键字在多线程环境下经常使用,因为编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程.对于C语言编译器来说,它并不知道这个值会被其他线程修秘诀,自然就把它缓存到寄存器里同.volatile的本意是指定个值可能会在当前线程外部被改变,此时编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取,这个关键字在外设接口编程中经常被使用.

10.5.4 “memory” 描述符

"memory"描述符告知GCC以下内容.

  1. 不要将该段内嵌汇编指令与前面的指令得新排序,也就是说在执行内嵌汇编代码之前,它前面的指令都执行完毕.
  2. 不要将变量缓存到寄存器,因为这段代码可能会用天内存变量,而这些内存变量会以不可预知的方式发生改变,因GCC插入必要的代码先将缓存到寄存器的值写回内存,如果后面又访问题这些变量,需要重新访问题内存.
    如果汇编指令修改了内存,GCC本身其实察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加"memory ",告诉GCC内存已经被修改,GCC得知这个信息后,就会在这段指令之前,插入必要的指令将前面因为优化Cache而写到寄存器中的变量值先定回内存,如果以后又要使用这个结变量,则重新读取,当然,使用volatile也可以达到这个目的.
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐