Linux内存管理和定时器

内存管理和定时器

内存堆栈管理

函数的局部变量保存在栈中,使用malloc申请的动态内存则在堆空间中分配。
区别:一块由系统维护,一块由用户自己申请和释放。

进程与内存
  • Linux中的进程
    每个进程都是用一个task_struct结构体表示,各个task_struct构成一个链表,由操作系统的调度器管理和维护,每一个进程都会接受操作系统的任务调度,轮流占用CPU去运行。只要轮换的速度够用,就会让你有种错觉:你可以一边听歌,一边聊天打字……

程序是一个二进制文件,而进程是一个程序运行的实例。操作系统会从磁盘上加载这个程序到内存,分配相应的资源,初始化相关的环境,然后调度运行。一个进程实例不仅包含汇编指令代码、数据,还包括进程上下文环境、CPU寄存器状态、打开的文件描述符、信号、分配的物理内存等资源。

在一个进程的地址空间中,在程序加载运行后,地址就已经固定了。在整个运行期间不会变化。这部分内存称为静态内存。而在程序中使用malloc申请的内存(堆内存)和函数调用过程中的栈(栈内存)在程序运行期间是不断变化的。这部分内存称为动态内存

Linux环境下的内存管理

在Linux环境下运行的程序,在编译时链接的初始地址都是相同的,而且是一个虚拟地址。Linux内核通过页表和MMU硬件来管理内存,完成虚拟地址到物理地址的转换、内存读写权限管理等功能。
编译器在编译时不用考虑每个程序在实际物理内存中的地址分配问题。

未初始化的全局变量保存在.bss段
初始化过的全局变量保存在.data段
常量数据保存在.rodata段
代码 .text段
栈 stack
堆 heap

栈的管理

栈分为递增栈和递减栈。栈是C语言运行的基础。C语言函数中的局部变量、传递的实参、返回的结果、编译器生成的临时变量都是保存在栈中的。在很多嵌入式系统的启动代码中,你会看到:系统一上电开始运行的都是汇编代码,再跳到第一个C语言函数运行之前,都要先初始化栈空间。

栈的初始化

即栈指针的初始化。内存初始化后,将栈指针指向内存中的一段空间就完成了。ARM处理器使用的是满递减栈,在Linux环境下,栈的起始地址一般就是进程用户空间的最高地址,紧挨内核(有随机的偏移防止黑客攻击)。

防止栈的溢出

  • 尽量不要再函数内使用大数据,如果确实需要可以使用malloc。
  • 函数的嵌套层数不宜太深。
  • 递归的层数不宜太深

堆内存管理

使用malloc/free函数 申请/释放的动态内存就属于堆内存。堆是Linux进程空间中一片可动态扩展或缩减的内存区域,一般位于BSS段后面。

//分配指定大小内存
malloc();
//申请n个长度为size的连续空间
calloc();
//重新调整内存块的大小
realloc();
//释放内存
free();

分配内存后需要使用free释放内存,否则会造成内存泄露

栈与堆内存的区别
  • 堆内存是匿名的,不能像变量那样使用名字直接访问,一般通过指针间接访问。
  • 函数运行期间,对函数栈帧内的内存访问也不能像变量那样通过变量名直接访问,一般通过栈指针FP或SP相对寻址访问。
  • 堆内存由程序员维护,程序退出时没有free就会造成内存泄露。栈内存由系统维护,程序退出时,随之消失。
裸机环境下的堆内存管理

堆内存如果没有专门的维护和管理,经过频繁的申请与释放后,很容易产生内存碎片。用户如果想申请一片完整的大块内存可能会失败。

  • 内存碎片化

在裸机环境下一片连续的对内存空间,经过多次小块内存的申请和释放后,就会造成内存碎片化,在内存中留下越来越多、越来越碎片化的空闲小内存块。此时如果再去申请一片连续的大块内存就会失败。由于这个原因,在嵌入式裸机环境下,一般不建议使用堆内存。大块内存可以使用全局数组代替。也可以自己实现堆内存管理。如采用内存池,将堆内存空间划分为固定大小的内存块,自己管理与维护。

为了改善这一状况,需要操作系统介入堆内存管理。

Linux堆内存管理

Linux堆内存管理包含堆内存管理、读写权限管理、地址映射等。每个Linux用户进程都有各自的4GB的虚拟空间,除去3GB-4GB的内核空间,还有0-3GB的用户空间可用。
如果想申请一块内存使用,需要向内核申请。释放同理。malloc()/free()底层实现就是通过系统调用brk向内核的内存管理系统申请内存。

  • 申请内存的地址变化
    如果用户要申请的内存比较大时,一般会通过mmap系统调用直接映射一片内存,使用结束后通过ummap系统调用归还这块内存。
    对于用户创建的每一个Linux用户进程,Linux内核都会用一个task_struct结构体来描述它,其中内嵌一个mm_struct结构体,用来描述该进程代码片段、数据段、堆栈的起始地址。
    mm_struct中的start_brk成员表示堆区的起始地址。当用户使用malloc申请内存大小大于当前的堆区时,malloc就会通过brk系统调用,修改成员变量brk拓展堆区的大小。brk系统调用的核心就是通过拓展数据段的边界来改变数据段的大小。

  • glibc中实现的内存分配器
    内存分配器通过系统调用brk()/mmap()向Linux内存管理子系统“批发”内存,同时实现了malloc()/free()等API函数给用户使用,满足用户动态内存的申请与释放请求。

    当用户使用free释放内存是,释放的内存并不会立即返回给内核,而是被内存分配器接收,缓存在用户空间。内存分配器将这些内存通过链表收集起来,当下次分配内存时就直接从链表上寻找合适的大小分配。如果链表上的内存不够使用就通过brk系统调用“批发”内存。大大减少了系统调用的次数。

mmap映射区

文件读写需要从磁盘上将可执行文件加载到内存。通常是两种方法,①文件IO操作。②使用mmap系统调用将文件映射到进程的虚拟空间,直接对映射区域读写。
I/O操作频繁的系统调用会带来一定的性能开销。Linux内核提供了一种磁盘缓冲机制。将read()/write()封装成了fread()/fwrite()函数。
当应用程序通过fread()读取磁盘文件时,数据从内核的页缓存复制到I/O缓冲区,然后复制到用户的buf。第二次读取时回去buf中查找,有就直接读取。没有就复制到缓冲区中。

  • mmap()
    这样减少系统调用的次数来降低系统调用的开销,但是增加了不同缓冲区复制的次数。文件较大时就用到mmap。
    mmap 系统调用将文件直接映射到进程的虚拟地址空间中,地址与文件数据一一对应,对这片内存映射区域进行读写操作相当于对磁盘上的文件读写。
内存泄露

简单的内存泄漏程序如下:

#include <stdlib.h>

int main(void) {

       int* p = NULL;

       p = (int*)malloc(32);

       strcpy(p, "hello");

       puts(p);

       return 0;

}

在上述程序当中,我们malloc分配内存后赋值给p指针。通过操作p指针就可以操作这块匿名内存。但是函数退出时,p指针随着函数栈帧删除。用户无法通过指针变量p访问这块内存。内存管理子系统和ptmalloc也就失去了对这块内存的控制权。这就在内存链表中产生了一个漏洞。如果漏洞越来越多,最后就无法分配内存。

预防内存泄漏
  1. malloc和free搭配使用。
  2. 内存释放后将指针设置为NULL。
  3. 使用内存指针前判空。
    一般本着“水污染谁治理”的原则。在一个函数内申请的内存,函数退出前释放。但是当函数嵌套深,在本函数申请的内存需要在外部释放内存时可能会遗漏。需要添加相应的注释表明要在哪里释放。
内存泄漏检测:MTrace

MTrace是Linux系统自带的一个工具,通过跟踪内存的使用记录来动态定位用户代码中内存泄露的位置。使用方法如下:

void mtrace(void);//开启跟踪
void muntrace(void);//关闭跟踪

#include <mcheck.h>
int main(void)
{
    mtrace();
    //需要检测的代码片段
    muntrace();
}
内存错误

常见的内存错误一般分为:内存越界、内存踩踏、多次释放、非法指针。

  • 段错误
#include <stdio.h>

int main(void)
{
    char *p;
    *p = 1;
    return 0;
}

未初始化的指针它的值是随机的。如果属于安全访问区,那不会有问题。如果刚好分配到内核区就会发生段错误。

访问零地址,也会发生段错误。这个指针也就成了非法指针。

在Linux环境下,每一个用户进程默认有8MB大小的栈空间,如果你在函数内定义大容量的数组或局部变量,就可能造成栈溢出。也会发生段错误。内核中的线程同理。

访问数组时,访问超越数组边界的也会发生段错误。

使用malloc分配的内存,多次使用free释放也会发生段错误。

内存踩踏

申请两块内存,对其中一块内存写数据时发生了溢出,将溢出部分的数据写入到了另一个缓冲区里。在释放前时不会发生错误的。被覆写的部分就称为被踩了。

定时器

不同于单片机定时器,Linux内核定时器是一种基于未来时间点的计时方式,它以当前时刻为启动的时间点,以未来的某一时刻为终止点,类似于我们的闹钟。内核定时器的精度不高,不能作为高精度定时器使用,其内核定时器不是周期性运行的,超时以后就会自动关闭,因此要想实现周期性的定时,就需要在定时处理函数中重新开启定时器。

最强大的定时器接口来自POSIX时钟系列,其创建、初始化以及删除一个定时器的行动被分为三个不同的函数:timer_create()(创建定时器)、timer_settime()(初始化定时器)以及timer_delete(销毁它)。

//创建一个POSIX标准得进程定时器
int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid)

参数:具体可在man手册中查看
@clock_id 可选系统得宏,

@sevp环境值,结构体struct sigevent变量得地址

@timerid定时器标识, 结构体timer_t变量得地址

//关联或启动一个定时器
int timer_settime(timer_t timerid, int flags,
                         const struct itimerspec *new_value,
                         struct itimerspec *old_value);
//获取定时器剩余时间
int timer_gettime(timer_t timerid, struct itimerspec *curr_value);
//删除定时器
int timer_delete(timer_t timerid);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值