nbhkdz.com冰点文库

嵌入式操作系统深入分析

时间:2011-02-26


嵌入式操作系统分析
本文分为以下几部分: 1. 2. 3. 4. 5. 6.
实时嵌入式系统 中断机制 任务调度与上下文切换 进程间通信与同步 嵌入式系统内存管理 浅析动态内存分配及 Malloc/free 的实现

实时嵌入式系统 实时操作系统(RTOS)设计成提供一个对真实世界的事件的及时响应(timely response)。出现在真实世界 中的事件可能有一个时间限制(deadline),在此期限之前,实时/嵌入式系统必须确保在有限时间时间内对 此事件做出相应的响应,根据相应事件的时间限制,嵌入式实时操作系统可以分为两类: 1、硬实时嵌入式系统 此类实时系统必须保证在一定的时间里,对一定的外部事件做出相应的响应,如果在一定时间里没有能够 做出相应的响应,就会带来灾难性的后果,也即是说,此类系统对于时间的相应快慢有非常严格的要求, 比如:电力监控系统等。 2、软实时嵌入式系统 此类实时系统保证在一定的时间里,对一定的外部事件做出相应的响应,但是如果在一定时间里没有能够 做出相应的响应,不会带来灾难性的后果,只是给用户带来一些不快乐的体验,比如:用户界面的显示。 不同的系统,有不同实现方式,对于硬实时嵌入式系统,主要注重与实时性和可靠性,一般没有文件系统, 虚拟内存管理等,主要用于实时监控等,典型的有:VxWorks,pSOS,uCOS-II 等。软实时嵌入式系统注 重实时性,也注重实用性,主要用于消费电子等,典型的有:嵌入式 Linux。 开源实时嵌入式系统: 实时嵌入式操作系统在现在应用越来越多,其中大部分是商业的嵌入式操作系统,这些系统提高了丰富的 开发工具和稳定的系统,很大的推动了嵌入式系统地发展,但是由于商业性,使我们很少有机会了解嵌入 式操作系统的实现内幕。 随着开源软件的兴起, 使得我们有机会了解嵌入式操作系统的内部实现成为可能。

由于多年的嵌入式开发,对于嵌入式操作系统特别的感兴趣,趁春节的时间,想读读开源的嵌入式操作系 统源码,大体比较一下嵌入式操作系统的具体实现。 对于开源的嵌入式操作系统,现在比较流行的主要有:嵌入式 Linux,uCOS-II, eCOS,FreeRTOS 等, 这里主要想以嵌入式 Linux, uCOS-II 为例, 对嵌入式操作系统的一些基本概念及其实现方式进行一些比较。 之所以选择这两操作系统,因为: 1、嵌入式 Linux 是现在用得最多的软实时嵌入式操作系统,而且也是最著名的开源软件之一, 2、uCOS-II 是一用得较多的硬实时嵌入式操作系统,尽管商业应用需要 Licence,对于科研等可以免费使 用,而且许多爱好者已经把它移植到了各种硬件平台。 3、尽管硬实时嵌入式操作系统和软实时嵌入式操作系统有很大的区别,但是可以通过对他们的比较,使我 们更容易了解实时嵌入式操作系统的基本概念和组成要素。 实时多任务嵌入式系统基本概念: 对于嵌入式系统,不像通用的计算机,提供通用的软硬件平台,而是对于特定的需求定制的系统,一般要 求便携化,资源少,利用比较少的资源配置来实现特定的功能。如何在较小尺寸,较少的资源情况下,快 速相应外部多种特定的事件,成为嵌入式系统的主要需求。 这就要求嵌入式系统要有以下的模块: 1、中断处理 在没有嵌入式系统之前,对于一般的工控,都是用前后台机制实现的,中断是整个系统的推动力。在嵌入 式系统相中,中断处理也是响应外部事件的主要途径。 2、时间管理 对于实时操作系统来说,时间管理是系统的核心,整个系统就是由一定间隔的时钟中断驱动的。在一般的 实时操作系统中,主要有两种时间管理:OS 定时器和 RTC 定时器。 3、资源管理与资源共享 由于有限的资源,在嵌入式系统中,资源的管理也非常的重要,CPU,IO,内存等是系统基本的资源,如 何有效的应用和管理是一个很大的话题。同时如何共享,如何提供任务间资源的互斥,也是一稳定系统必 不可缺的条件。 4、多任务

在嵌入式系统中,任务也可以叫进程,对于多任务系统,一般有多个任务同时存在于系统中,任务在嵌入 式系统中有各种各样的状态,比如:运行态,IDLE 态等。 5、任务的实时调度与切换 对于多任务的管理及其实时调度,决定了此系统的主要特征。任务的调度与如何切换,也是嵌入式多任务 系统的一个基本问题。 6、进程间(任务间)通讯 对于不同任务间的通讯,在不同的系统中有不同的实现,大体与 POSIX 的 IPC 相似。但在硬实时嵌入式 系统中,事件和消息用得较多。 对于 LINUX 系统,还有虚拟内存管理和文件系统管理,在硬实时嵌入式系统中,由于关注点不同,这方 面的应用不多。

中断机制
1. 概述:
中断控制是计算机发展中一种重要的技术。最初它是为克服对 I/O 接口控制采用程序查询所带来的处 理器低效率而产生的。中断控制的主要优点是只有在 I/O 需要服务时才能得到处理器的响应,而不需要处 理器不断地进行查询。由此,最初的中断全部是对外部设备而言的,即称为外部中断(或硬件中断)。但 随着计算机系统结构的不断改进以及应用技术的提高, 中断的适用范围也扩大, 出现了所谓的内部中断 (或 异常),是为解决机器运行时所出现的某些随机事件及编程方便而出现的。因而形成了一个完整的中断系 统。中断控制是所有计算机系统的一个核心模块,不同的硬件平台,有不同的中断机制.不管怎样,中断机制最 核心的部分是中断向量表,每一种硬件体系根据自己的实现提供一张中断向量表。

2. 中断向量表:
此向量表提供了所有支持的中断定义以及相应的中断服务程序,当发生异常时,首先要保存当前的处 理器状态,然后进入到相应的异常向量地址,一般来说在异常向量地址是一个跳转指令,使程序进入相应 的异常处理过程。 X86 中,中断向量表存储于存储器的前 1024 字节中,它包括 256 种不同的 4 字节中断向量。 ARM 中,中断向量表存储于存储器的低端或者高端地址的前 0x1c(依赖于硬件的具体配置),它支 持 7 种类型的异常,其中第一个中断向量是复位中断向量,当系统复位后,从此开始重新执行。

在 Motorola 的 68K 中,中断向量表占用存储器的 1024 字节中,它包括 256 种不同的中断向量。其 地址根据 VBR 的配置的不同而不同,其中第一个中断向量是复位中断向量,当系统复位后,从此开始重新 执行。

3. 中断种类:
根据中断向量表,可以把这些中断具体分为同步中断,异步中断,软中断: 同步中断(exception),也即是我们通常所说的异常(),是在 CPU 执行特定指令时出现的非法情况,它 的产生是由于前面的 CPU 操作引起的,是同步发生的。比较典型的异常有:除零异常。一些体系结构下的 复位中断也可以算是一种异常。 异步中断(interrupt):主要是由外部事件触发的,这就是我们常说的中断,它的发生不可预料的, 是异步的。比如说,键盘中断等。在不同的体系结构中,中断机制也不一样。一般可以分为可屏蔽中断和 非屏蔽中断。 软中断(software interrupt ):在大多数硬件体系结构中,都提供软中断的功能,主要用于进入管理模 式等的手段。 X86 中,除前 32 个作为 Intel 专用中断向量外,其他 224 作为用户自定义中断。0~31 的向量对应于异 常和非屏蔽中断。32~47 的向量(即由 I/O 设备引起的中断)分配给屏蔽中断。48~255 的向量用来标识软 中断。Linux 只用了其中的一个(即 128 或 0x80 向量)用来实现系统调用。 ARM 中,除了定义的 5 个异常外,有 2 个专门用于中断,这 2 个中断被分为快中断和慢中断。由于 快中断有专门的寄存器保存上下文,避免了许多上下文切换开销,所以快中断能够比慢中断快,而且在处 理快中断时,慢中断会被屏蔽掉。这 2 个中断提供了系统的所有中断入口,系统查询相应的中断控制器来 区分具体的中断。 在 Motorola 的 68K 中,中断向量表提供了 7 个中断优先级和 192 个可分配的中断向量,其中 7 级是 不可屏蔽中断。

参考: 1。Intel Xscale ProcessorDeveloper's Manual 2。MC680360 QUad 通讯控制器手册 3。Intel 系列微处理器结构,编程和接口技术大全 4。Arm 嵌入式处理器结构与应用基础

任务调度与上下文切换 1. 任务调度概述:
任务调度(schedulers)是内核的主要职责,实际上它就是一个法官,决定当前由哪个任务占用 CPU, 多数实时内核都是基于优先级调度算法的,每个任务根据其重要程度的不同被赋予一定的优先级。基于此 算法,CPU 总是让处于就绪而且优先级最高的任务优先运行,然而何时高优先级任务能够得到 CPU 使用 权,由内核的类型而定。基于优先级的内核有两种: 不可抢占型和抢占型。 1) 不可抢占型内核: 不可抢占型内核要求每个任务主动放弃 CPU 的使用权,其间不能被高优先级任务抢占。它的有点是: A. 由于不需要在中断返回是进行任务切换,所以中断响应快。 B. 在任务级中可以调用不可重入函数而不必担心造成数据破坏。 C. 几乎不需要信号量来保护共享数据,也就是说,在任务运行过程中,数据是独享的。 但它的最大缺点是:响应时间不确定,当有更高优先级任务就绪后,不知道什么时候才能得到执行,这在 实时系统中是致命的缺陷。所以不可抢占型内核最要用于前后台系统中。 2) 抢占型内核: 在嵌入式系统中,进程(任务)都是抢占型的,通过给每个进程(任务)设置一个优先级,当系统中有 优先级比当前运行的进程(任务)的优先级更高的进程(任务)时,当前的进程(任务)执行被中断,并 调用调度程序选择优先级高的进程(任务)运行。利用抢占式内核,可以保证高优先级的进程(任务)被 优先执行,从而保证系统的实时响应。 在多任务系统中,进程(任务)的调度主要包括以下一些方面:

2. 任务调度:
在多任务系统中,都会提供一个系统函数来进行进程(任务)间切换,综合来说,他们有两种进程(任务) 切换方式: 1) 由进程(任务)本身直接调用任务切换函数进行进程(任务)切换: 在当前进程 (任务) 因为不能获得必须的资源而立即被堵塞时, 就由进程 (任务) 本生直接调用进程 (任 务)切换函数进行进程(任务)间调度。 在 Linux 中可以直接调用 schedule()函数来实现。 在 UCos 中,通过调用 OSSched()来完成。 2) 延迟调用任务切换函数进行进程(任务)切换: 此方式是把当前进程(任务)设置一调度标志而以延迟方式调用任务切换函数进行进程(任务)切换。

在 Linux 系统中,总是在恢复用户态进程执行之前,检查这一调度标志,在这里标志是: TIF_NEED_RESCHED,如果有这一标志,就调用调度函数进行进程切换。此种情况主要包括以下几种: A. 当前进程用完了它的 CPU 时间片,有 scheduler_tick()函数完成 schedule()的延迟调用。 B.当一个被唤醒进程的优先级比当前进程优先级高时,由 try_to_wake_up()函数完成 schedule()的延迟 调用。 C.当发出系统调用 sched_setscheduler()时。 在这些情况中,主要由于系统调用或中断而进入内核态,或者当前进程本来在内核态时,返回用户态时发 生的。 在 UCOS 中,所有的任务有不同的优先级,不会出现同一优先级上有多个任务的情况,而且也没有系统调 用的概念,所以任务调度的延迟调用只能出现在中断处理完成返回时,在 OSIntExt()函数中,检查是否有 高优先级的任务就绪,如果有高优先级的任务就绪,进行任务切换。

3. 调度算法:
在 Linux 系统中,选用了比较复杂的调度算法,按照调度类型可以分为以下几种: SCHED_FIFO:此算法主要应用于实时进程,当调度程序把 CPU 分配给当前进程后,如果没有更高优先级 的进程可以运行时,此进程会一直占用 CPU 直到此进程退出或者自愿放弃 CPU,即使此时有其他相同优 先级的进程存在。 SCHED_RR:时间片轮询的实时进程,对于不同优先级的进程会调度优先级高的进程运行,对具有相同优 先级的进程,会根据时间片来调度,当当前进程的时间片用完后,会调度相同优先级的其他进程运行,从 而保证相同优先级进程的 CPU 调度公平性。 SCHED_NORMAL:此算法主要用于普通进程,利用分时进行调度。 在 UCOS 系统中,所有的任务都是实时任务,所以没有普通任务调度机制,而且为了简化调度算法,不 同的任务有不同的优先级,不可能出现同一优先级有多个任务的情况,实际上它的调度算法就只有 Linux 中 SCHED_FIFO 这一种,即优先级高的任务抢占优先级低任务。

4. 上下文切换:
上下文切换是多任务调度的核心内容,也是我们感觉在一个 CPU 上并行运行多个程序的基础。 任务上下文(Task Context): 任务上下文是指任务运行的环境。例如,针对 x86 的 CPU,任务上下文可包 括程序计数器、堆栈指针、通用寄存器的内容。 上下文切换(Context Switching):在多任务系统中,上下文切换是指 CPU 的控制权由运行任务转移到另 外一个就绪任务时所发生的事件,当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪 任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。上下文的 内容依赖于具体的 CPU。

对于不同的硬件体系结构,上下文切换的内容不一样,本质上有下面两步: A. 如果有虚拟内存,则切换页全局目录以安装一个新的地址空间。 B. 切换内核堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括 CPU 信息。 在抢占式内核中,利用中断来实现上下文切换是一个非常理想的机制。中断发生时,中断会强制 CPU 把控 制权交给操作系统,也就相当于一次上下文切换。这样不仅可以减少程序出错的后果,而且提高切换的效 率。UCOS 就是利用中断机制进行上下文切换的典型例子。 在 UCOS 中,如果调度程序决定任务需要切换,就会调用上下文切换 OS_TASK_SW()进行实际的上下 文切换。OS_TASK_SW()是宏调用,含有微处理器的软中断指令,利用此中断来实现任务之间的上下文切 换。所以 OS_TASK_SW()是一个体系结构相关的宏,对于不同的硬件体系机构,有不同的实现方式,这 也是 UCOS 在不同硬件体系结构中移植的一个要点。 由于 UCOS 不支持虚拟内存,所以不需要进行页目录切换,其他许多实时多任务嵌入式系统的一个特征, 也是区别 Linux 系统的一个重要方面。 在 2.6 Linux kernel 中,引入了一个全新的调度机制 O(1)调度器,它能在固定的时间内完成进程切换。 如果调度程序决定任务需要切换,就会调用上下文切换函数 context_switch()函数进行上下文切换,此函数 会调用 switch_mm()切换页全局目录以安装一个新的地址空间, 然后调用 switch_to()切换具体硬件上下文。 总结: 这里主要介绍了多任务系统中的任务调度及其算法,比较了 Linux 系统和 UCOS 系统中的上下文切换, 具体实现可以参考 Linux 内核源代码和 UCOS 源代码。 参考: 1. Linux kernel Primer 2. Understanding the Linux kernel 3. 嵌入式实时操作系统 uC/OS-II 4. 嵌入式计算机系统设计原理 嵌入式系统内存管理 1、概述 操作系统的内存管理功能用于向操作系统提供一致的地址映射功能和内存页面的申请、释放操作。在嵌 入式实时系统中,内存管理根据不同的系统,有不同的策略,对于有些系统支持的虚拟内存管理机制,对 于另外一些系统,可能只有 flat 式的简单内存管理机制。

2、内存管理机制: 大体上来说,嵌入式系统所用到的内存管理机制主要有以下两种: 虚拟内存管理机制: 有一些嵌入式处理器提供了 MMU,在 MMU 具备内存地址映射和寻址功能,它使操作系统的内存管理 更加方便。如果存在 MMU ,操作系统会使用它完成从虚拟地址到物理地址的转换, 所有的应用程序只需 要使用虚拟地址寻址数据。 这种使用虚拟地址寻址整个系统的主存和辅存的方式在现代操作系统中被称为 虚拟内存。MMU 便是实现虚拟内存的必要条件。 虚拟内存的管理方法使系统既可以运行体积比物理内存还要大的应用程序, 也可以实现“按需调页”策略, 既满足了程序的运行速度,又节约了物理内存空间。 在 L inux 系统中,虚拟内存机制的实现实现为我们提供了一个典型的例子:在不同的体系结构下, 使 用了三级或者两级页式管理,利用 MMU 完成从虚拟地址到物理地址之间的转换。基于虚拟内存管理的内 存最大好处是:由于不同进程有自己单独的进程空间,十分有效的提高了系统可靠性和安全性。 非虚拟内存管理机制 在实时性要求比较高的情况下,很多嵌入式系统并不需要虚拟内存机制:因为虚拟内存机制会导致不确 定性的 I/O 阻塞时间, 使得程序运行时间不可预期,这是实时嵌入式系统的致命缺陷;另外,从嵌入式处 理器的成本考虑,大多采用不装配 MMU 的嵌入式微处理器。所以大多嵌入式系统采用的是实存储器管理 策略。因而对于内存的访问是直接的,它对地址的访问不需要经过 MMU,而是直接送到地址线上输出,所 有程序中访问的地址都是实际的物理地址;而且,大多数嵌入式操作系统对内存空间没有保护,各个进程 实际上共享一个运行空间。一个进程在执行前,系统必须为它分配足够的连续地址空间,然后全部载入主 存储器的连续空间。 由此可见,嵌入式系统的开发人员不得不参与系统的内存管理。从编译内核开始,开发人员必须告诉系 统这块开发板到底拥有多少内存;在开发应用程序时,必须考虑内存的分配情况并关注应用程序需要运行 空间的大小。另外,由于采用实存储器管理策略,用户程序同内核以及其它用户程序在一个地址空间,程 序开发时要保证不侵犯其它程序的地址空间,以使得程序不至于破坏系统的正常工作,或导致其它程序的 运行异常;因而,嵌入式系统的开发人员对软件中的一些内存操作要格外小心。 UCOS 就是使用非虚拟内存管理的一个例子,在 UCOS 中,所有的任务共享所有的物理内存,任务之间 没有内存保护机制,这样能够提高系统的相应时间,但是任务内存操作不当,会引起系统崩溃。 3、内存在系统中的生命期:

对于内存在整个嵌入式运行过程中,以 3 中方式存在: 1、 在 bootstraping 阶段,内存以临时内存分配的形式出现,当完成系统启动后,这些内存会回收供以后 系统使用。 2、 在正常运行阶段,内存以两种方式存在: ( 1) 系统为代码,数据分配的永久内存,这些内存在系统运行过程中是不会改变的,有的硬件的 I/O 等外 设也把相应的地址映射到固定的内存空间。 ( 2) 动态内存分配空间:这些内存不会固定分配,而是根据系统需要而动态分配的,如果利用非虚拟内存 管理机制,一般需要改造动态内存分配机制以提高性能。 4、内存管理的具体应用: A、Linux 系统的内存管理机制 Linux 内存管理机制中,X86 体系结构是利用虚拟内存管理的典型,在 i386CPU 上,首先要进行段式映 射,Linux 没有用到段式管理,它的做法是把 GDT 中段描述符段的大小定义为 4GB, 也就是说只分了一 段, 从而使段式映射没有起作用。在页式映射中,对于嵌入式 i386 芯片 来说,实际上是两层映射, 跳 过中间的 PMD 层次。对于程序来说,并非所有虚存都映射都到物理空间了,而是动态映射, 如果程序运 行时内核发现虚拟页面没有映射或映射的是磁盘页面, 会作相应的缺页处理——分配内存页面并建立映 射,然后恢复程序运行。 在程序运行的过程中, 涉及到的内存操作主要有内存分配、内存使用、内存回收、内存页面换出、页 面换入。内存分配会在管理区的空闲区进行, 通过 Buddy 算法在管理区的 free_area 中获得需要的内存 块。如果内存不足, 则会启动 Kswapd 这个守护进程腾出部分物理内存。除了被调用, Kswapd 进程还 会定时启动。Kswapd 的工作分两部分: ( 1) 检测物理内存剩余的情况,如果短缺,则按 LRU 策略断开 active_list 队列中部分可交换页面的映射, 使页面变为不活跃状态,链入 inactive_clean_list 队列或者 inactive_dirty_list 队列, 为换出做准备。 ( 2) 每次都执行, 把 inactive_ dirty_list 中的页面写入交换设备, 并且回收一部分 inactive_clean_list 中 的页面。 Linux 系统 虚拟内存机制的屏蔽 由于虚拟内存在时间上的不可预期性, 对于实时性要求很高的系统, 必须屏蔽虚拟内存机制。 uCLinux 在 中就利用了这种技术一保证系统的实时性,下面是屏蔽虚拟内存机制的思路:

为了满足在工业控制中一些任务的实时性要求,必须屏蔽内核的虚拟内存管理机制以增强 Linux 的实时 性。当要更改内核的某项机制时,一般不必大规模的改写代码,可采用条件编译的方法。思路是用#ifdef 或 #ifndef 屏蔽现有语句,在#else 宏编译语句中包括自己编写的代码。实现虚拟内存的机制有:地址映射 机制、内存分配和回收机制,缓存和刷新机制、请页机制、交换机制、内存共享机制,将实现这些机制的 数据结构和函数屏蔽或修改,还要修改与之相关的文件。需要改动的文件主要在 /include/linux、/mm、 /drivers/char、/fs、/ipc/kernel、/init 目录下。主要的改动如下:与虚存有关的主要的数据结构是 vm_area_struct,将进程的 mm_struct 结构中的 vm_area_struct 去掉,vm_area_struct 利用了 vm_ops 来抽象出对虚拟内存的处理方法,屏蔽与虚拟内存操作有关的函数。内存映射主要由 do_mmap()实现,改 写此函数的代码。取消交换操作,屏蔽用于交换的结构和函数声明,以及实现交换的代码。取消内核守护 进程 kswapd。 B、UCOS 的内存管理: UCOS 的内存管理与大多数嵌入式系统一样,是 flat 内存,但在此 flat 内存的基础上进行了优化,使在 动态内存分配的时候,减少了内存粹片,提高了系统性能。 UCOS 的具体方法是:把连续的大块内存进行分区,每个分区包含整数个大小相同的内存块,在一个系统 中有多个不同内存大小的分区。这样,应用程序根据不同的需求,从不同大小的内存分区中分配相应大小 的内存。对于不用的内存,又重新释放回原来的分区。通过这样的内存管理算法,解决了内存粹片的问题, 提高了系统性能。UCOS 具体的实现可以参看源码。主要由以下几个函数实现: OSMemCreate():创建内存分区。 OSMenGet() :为应用程序分配一段内存。 OSMenPut():回收应用程序不再使用的内存。 5、结论: 内存管理是嵌入式系统的一个重要方面,虚拟内存管理机制在为进程安全提供很好保证的同时,也为开 发人员提供了一个管理内存的方法,使开发人员更多的关注其他的方面。但是它也带来了时间不确定性的 缺陷。根据不同的系统需求,我们可以选取相应的内存管理策略。在现在大多数的实时系统中,非虚拟内 存管理机制用得比较多,这样保证了系统的实时性,但是增加了开发的难度,任务内存操作不当,可能引 起系统崩溃。 由于虚拟内存管理的请求换页机制在很大程度上影响了系统的实时性能,现在有些开发人员提出了一个 折衷方案,即不用虚拟内存管理的请求换页机制,只考虑进程保护、内存映射、共享虚拟内存等功能,这 样既能提高系统的实时性,又能提高系统的安全性,具体请参考文献 4。

参考文献: 1、基于 Linux 的嵌入式系统在测控系统中的设 计. http://article.ednchina.com/Embeded/20070630090542.htm 2、The Linux kernel Primer. 3、嵌入式 L inux 操作系统的研究. 刘文峰, 李程远, 李善平 4、嵌入式软件虚拟内存管理技术的研究和实现. 钱 静, 芦东昕, 谢 鑫, 徐立锋 5、Understanding the Linux kernel. 进程间通信与同步 一、概述 在单任务系统中,任务是线性执行,任务不可能被抢占,所以不需要同步来保护共享资源与临界资源, 同时单任务也不存在数据交换的问题,但对于多任务操作系统,会出现与但任务系统不同的问题,进程间 通信与同步就是为了解决这些问题而提出的特有机制,它们为多任务系统提供了不同进程的通信机制,同 时也提供了对于临界资源和共享资源的保护。 进程间通信与同步是多任务系统中的不同表现形式,, 对于一些嵌入式操作系统, 进程间通信与同步处于 同一地址空间,这样使一些进程间通信机制同时可以用作同步机制,比如在 UCOS 中的一些进程间通信与 同步机制。对于另外一些嵌入式操作系统中,进程间通信与同步处于不同的地址空间,它们是截然不同的 两种机制, 相互间没有什么联系(不包括同一进程中的不同线程同步), 最典型的就是嵌入式 Linux 中的进程 间通信与同步机制。 二、进程间通信与同步区别与联系 在不同的嵌入式系统中,进程间通信与同步的实现方式有所不同,但是基本原理都差不多。对于进程间 通信与同步,主要有 2 种方式:虚拟内存系统中的进程间通信与同步和 Falt 内存系统中的进程间通信与同 步。 Linux 系统中的进程间通信是典型的虚拟内存系统中的进程间通信: Linux 的进程间通信是不同进程间交 换数据的机制。由于 Linux 系统支持虚拟内存,对于每一个进程,系统会为它分配一个单独的进程空间, 不同的进程有不同的进程空间,相互之间没有什么关系,在这种系统中,不能用全局变量等一般的手段来 实现不同进程间的数据交换,所以必须提供特有的数据交换机制:进程间通信。

同时此类系统中,由于内核空间是由不同的进程共享,所以在内核的数据可以由不同的进程共享,不同的 进程也可能同时需要访问临界资源,同时要考虑内核中的中断,这种情况下,进程间的数据交换已经不是 问题,现在的问题是如何保护临界资源和共享资源不被同时访问造成资源的紊乱,这就是 Linux 的同步机 制。 UCOS 是比较典型的 Falt 内存系统,它不支持虚拟内存机制,也没有用户空间和内核空间的区别,实际 上它就象是 Linux 的内核空间,不同任务间可以相互访问,没有不同进程间内存保护机制。所以可以完全 利用 Linux 系统中的同一进程中不同线程的通信机制。由于所有的任务与中断都共享同一地址空间,所以 同步机制也与任务间通信在同一空间中实现,是这 2 种机制的相互替换成为可能。

浅析动态内存分配及 Malloc/free 的实现

一、概述: 动态内存分配,特别是开发者经常接触的 Malloc/Free 接口的实现,对许多开发者来说,是一个永远的 话题,而且有时候也是一个比较迷惑的问题,本文根据自己的理解,尝试简单的探究一下在嵌入式系统中, 两类典型系统中动态内存分配以及 Malloc/Free 的实现机制。 二、内存分配方式 Malloc/Free 主要实现的是动态内存分配,要理解它们的工作机制,就必须先了解操作系统内存分配的 基本原理。

在操作系统中,内存分配主要以下面三种方式存在:

(1)静态存储区域分配。内存在程序编译的时候或者在操作系统初始化的时候就已经分配好,这块内 存在程序的整个运行期间都存在, 而且其大小不会改变, 也不会被重新分配。 例如全局变量, static 变量等。

(2)栈上的内存分配。栈是系统数据结构,对于进程/线程是唯一的,它的分配与释放由操作系统来 维护,不需要开发者来管理。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时,这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,不同的操作系 统对栈都有一定的限制。

(3) 堆上的内存分配,亦称动态内存分配。程序在运行的期间用 malloc 申请的内存,这部分内存由 程序员自己负责管理,其生存期由开发者决定:在何时分配,分配多少,并在何时用 free 来释放该内存。 这是唯一可以由开发者参与管理的内存。使用的好坏直接决定系统的性能和稳定。 三、动态内存分配概述 首先,对于支持虚拟内存的操作系统, 动态内存分配(包括内核加载,用户进程加载,动态库加载等等)都是 建立在操作系统的虚拟内存分配之上的,虚拟内存分配主要包括: 1、进程使用的内存地址是虚拟的(每个进程感觉自己拥有所有的内存资源), 需要经过页表的映射才能最 终指向系统实际的物理地址。 2、主内存和磁盘采用页交换的方式加载进程和相关数据,而且数据何时加载到主内存,何时缓存到磁 盘是 OS 调度的,对应用程序是透明的。 3、 虚拟存储器给用户程序提供了一个基于页面的内存大小, 32 位系统中, 在 用户可以页面大小为单位, 分配到最大可以到 4G(内核要使用 1G 或 2G 等内存地址)字节的虚拟内存。 4、对于虚拟内存的分配,操作系统一般先分配出应用要求大小的虚拟内存,只有当应用实际使用时, 才会调用相应的操作系统接口,为此应用程序分配大小以页面为单位的实际物理内存。

5、不是所有计算机系统都有虚拟内存机制,一般在有 MMU 硬件支持的系统中才有虚拟内存的实现。 许多嵌入式操作系统中是没有虚拟内存机制的,程序的动态分配实际是直接针对物理内存进行操作的。许 多典型的实时嵌入式系统如 Vxworks、Uc/OS 等就是这样。 四、动态内存分配的实现 由于频繁的进行动态内存分配会造成内存碎片的产生,影响系统性能,所以在不同的系统中,对于动 态内存管理,开发了许多不同的算法(具体的算法实现不想在这里做详细的介绍,有兴趣的读者可以参考 Glib C 的源代码和附录中的资料)。不同的操作系统有不同的实现方式,为了程序的可移植性,一般在开 发语言的库中都提供了统一接口。对于 C 语言,在标准 C 库和 Glib 中,都实现了以 malloc/free 为接口的 动态内存分配功能。也就是说,malloc/free 库函索包装了不同操作系统对动态内存管理的不同实现,为开 发者提供了一个统一的开发环境。 对于我们前面提到的一些嵌入式操作系统, 因为实时系统的特殊要求 (实 时性要求和开发者订制嵌入式系统),可能没有提供相应的接口。一般 C 库中的 malloc/free 函数会实现 应用层面的内存管理算法,在系统真正需要内存时,才通过操作系统的 API(系统调用)来获取实际的物理内 存,当然,你也可以使用第三方的内存管理器,或者通过自己改写 malloc/free 函数来实现应用层面的内存 管理。

4.1、动态内存管理的一般机制:

动态内存管理机制会随操作系统和系统架构的不同而不同,一些操作系统利用 malloc 等分 配器, 在支持虚拟内存的操作系统中就利用这种方式来实现进程动态内存管理的。 而另外一些系 统利用预先分配的内存区间来进行动态内存管理, 该方式主要应用于不支持虚拟内存机制的嵌入 式操作系统中。对于进程动态内存管理机制的具体实现,许多系统提供了统一的接口,并由 malloc/free 函数来具体实现。 下面以嵌入式 Linux 和 Uc/OS 为例, 简单解析一下动态内存管理 的具体实现。
4.2、Linux 下动态内存分配的实现: 在 Linux 下,glibc 的 malloc 提供了下面两种动态内存管理的方法:堆内存分配和 mmap 的内存分 配,此两种分配方法都是通过相应的 Linux 系统调用来进行动态内存管理的。具体使用哪一种方式分配, 根据 glibc 的实现,主要取决于所需分配内存的大小。一般情况中,应用层面的内存从进程堆中分配,当 进程堆大小不够时,可以通过系统调用 brk 来改变堆的大小,但是在以下情况,一般由 mmap 系统调用来 实现应用层面的内存分配:A、应用需要分配大于 1M 的内存,B、在没有连续的内存空间能满足应用所需 大小的内存时。 (1)、调用 brk 实现进程里堆内存分配 在 glibc 中,当进程所需要的内存较小时,该内存会从进程的堆中分配,但是堆分配出来的内

存空间, 系统一般不会回收, 只有当进程的堆大小到达最大限额时或者没有足够连续大小的空间 来为进程继续分配所需内存时,才会回收不用的堆内存。在这种方式下,glibc 会为进程堆维护 一些固定大小的内存池以减少内存脆片。
(2)、使用 mmap 的内存分配 在 glibc 中,一般在比较大的内存分配时使用 mmap 系统调用,它以页为单位来分配内存的

(在 Linux 中,一般一页大小定义为 4K),这不可避免会带来内存浪费,但是当进程调用 free 释放所分配的内存时,glibc 会立即调用 unmmap,把所分配的内存空间释放回系统。

注意:这里我们讨论的都是虚拟内存的分配(即应用层面上的内存分配),主要由 glibc 来实现, 它与内核中实际物理内存的分配是不同的层面, 进程所分配到的虚拟内存可能没有对应的物理内 存。 如果所分配的虚拟内存没有对应的物理内存时, 操作系统会利用缺页机制来为进程分配实际 的物理内存。
4.3、Uc/OS 下内存分配的实现:

在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内存都需要开发人员 参与分配,他们直接操作物理内存,所分配的内存不能超过系统的物理内存,于系统的堆栈的管理,都由 开发者显式进行。 在 Uc/OS 中,主要利用内存分区来管理系统内存,系统中一般有多个分区,每个分区相当于一些固定 大小内存块的内存池,应用可以从这些内存池中分配内存,当内存使用完成后,也需要把该内存释放回对 应的内存池中。Uc/OS 的内存管理可以分为以下过程: (1)、创建内存分区:在使用内存之前,开发者必须首先调用 OSMemCreare()函数来创建相应的内存 分区,在创建内存分区成功后,就会在系统中存在一个以开发者指定内存大小,指定内存块数目的内存池。 在此过程中,开发者需要明确的知道系统的内存分布,并指明内存池的基址。 (2) 申请内存: 、 当系统内存分区创建好了后, 系统就可以从相应的内存分区中获取内存了。 Uc/OS 中, 在 主要利用 OSMemGet()来申请内存,应用程序会根据所需要内存的大小,从开发者指定的内存池中申请内 存。 (3)、释放内存:因为内存是系统的紧缺资源,当应用不再需要使用所申请的内存时,应该及时释放该内 存。在 Uc/OS 中,主要利用 OSMemPut()来释放不再需要的内存,在此过程中,开发者应该保证把该内 存释放回原内存的分区。 在一些实时嵌入式系统中,系统也会提供一些比较复杂的内存管理机制,并为应用提供类 malloc/free 接口供开发者使用,如 Vxworks 等。不管怎么样,在这些系统中,都得由开发者显式的参与内存的管理, 而且应用程序只能使用实际物理内存大小的内存。

五、小结: 动态内存管理是开发者唯一能够参与管理的内存分配机制,这给开发者灵活使用系统主存提供了一个 手段,但是由于内存的分配和释放都得依靠开发者显式进行,很容易出现内存泄露的问题。另外,不好的 动态内存管理算法对系统的性能影响有着也不可忽视影响。 在本文简单解析了两种动态内存管理实现,它们互有忧缺点:对于以虚拟内存机制和分页机制为基础的动 态内存管理(如 Linux 等),由于请求分页机制的存在,不能满足系统实时性方面的要求,但是它能为应 用提供最多能到 4G 的内存空间,而且由于应用虚拟内存机制,可以为进程空间提供保护,一个进程的崩 溃不会影响其他的进程。对于基于非虚拟内存管理机制的系统,由于可以直接操作物理内存,提高了系统 实时性,在这样的系统中,开发者的参与度比基于虚拟内存机制的要高。其缺点是没有进程间的保护机制, 一个进程或任务的错误很容易导致整个系统的崩溃。

参考: 1、Glibc 源代码 2、嵌入式实时操作系统 Uc/OS-II 3、经典收藏之 - C++内存管理详解:http://www.oneedu.cn/xxyd/jzjs/aspnet/200703/14963.html 4、Understanding Linux kernel 5、The Virtual-Memory Manager in Windows NT: http://msdn2.microsoft.com/en-us/library/ms810616.aspx

作者:gogofly 邮箱:gogofly_lee@yahoo.com.cn


赞助商链接