Linux实时策略与内存管理全解析
实时策略
实时策略旨在实现确定性。实时调度器总是会运行优先级最高且准备好运行的实时线程,并且实时线程总是会抢占分时线程。选择实时策略而非分时策略,意味着你对该线程的预期调度有深入了解,并希望覆盖调度器的内置假设。
实时策略类型
- SCHED_FIFO :这是一种运行至完成的算法。一旦线程开始运行,它将持续执行,直到被更高优先级的实时线程抢占、在系统调用中被阻塞或终止。
-
SCHED_RR
:这是一种轮询算法。如果同一优先级的线程超出了其时间片(默认值为100毫秒),调度器将在这些线程之间循环调度。自Linux 3.9起,可以通过
/proc/sys/kernel/sched_rr_timeslice_ms控制时间片的值。除此之外,它的行为与SCHED_FIFO相同。
每个实时线程的优先级范围是1到99,99为最高优先级。要为线程赋予实时策略,需要
CAP_SYS_NICE
权限,默认情况下只有root用户拥有该权限。
实时调度的问题及解决方案
实时调度存在一个问题,即计算密集型线程(通常是由于bug导致无限循环)会阻止低优先级的实时线程和所有分时线程运行,使系统变得不稳定甚至完全锁定。可以通过以下两种方法来防范这种情况:
-
预留CPU时间
:自Linux 2.6.25起,调度器默认会为非实时线程预留5%的CPU时间,以防止失控的实时线程完全停止系统。可以通过以下两个内核控制参数进行配置:
-
/proc/sys/kernel/sched_rt_period_us
:默认值为1,000,000(1秒)。
-
/proc/sys/kernel/sched_rt_runtime_us
:默认值为950,000(950毫秒),这意味着每秒会预留50毫秒用于非实时处理。如果希望实时线程能够占用100%的CPU时间,可以将
sched_rt_runtime_us
设置为 -1。
-
使用看门狗
:可以使用硬件或软件看门狗来监控关键线程的执行,并在它们开始错过截止日期时采取行动。
策略选择
在实践中,分时策略可以满足大多数计算工作负载的需求。I/O密集型线程大部分时间处于阻塞状态,当它们解除阻塞时,几乎会立即被调度。CPU密集型线程则会自然地占用剩余的CPU周期。可以为不太重要的线程设置正的
nice
值,为更重要的线程设置负的
nice
值。
如果需要更具确定性的行为,则需要使用实时策略。具有以下特征的线程通常需要实时策略:
- 有必须生成输出的截止日期。
- 错过截止日期会影响系统的有效性。
- 由事件驱动。
- 不是计算密集型的。
实时任务的示例包括经典的机器人手臂伺服控制器、多媒体处理和通信处理。
实时优先级选择
为所有预期工作负载选择合适的实时优先级是一项棘手的任务,这也是避免使用实时策略的一个重要原因。最广泛使用的优先级选择方法是速率单调分析(RMA),它适用于具有周期性线程的实时系统。每个线程都有一个周期和利用率(即该线程在一个周期内执行的时间比例)。RMA的目标是平衡负载,使所有线程能够在下一个周期开始之前完成执行阶段。要实现这一目标,需要满足以下条件:
- 为周期最短的线程分配最高优先级。
- 总利用率低于69%。
总利用率是所有线程利用率的总和,并且RMA假设线程之间的交互以及在互斥锁等上阻塞的时间可以忽略不计。
虚拟内存基础
Linux会配置CPU的内存管理单元(MMU),为运行的程序提供一个虚拟地址空间,在32位处理器上,该地址空间从0开始,到最高地址
0xffffffff
结束。这个地址空间被划分为4 KiB的页面。
Linux将虚拟地址空间划分为用户空间和内核空间,两者的划分由内核配置参数
PAGE_OFFSET
决定。在典型的32位嵌入式系统中,
PAGE_OFFSET
为
0xc0000000
,这意味着用户空间占用较低的3GB,内核空间占用最高的1GB。用户地址空间是按进程分配的,每个进程在独立的沙箱中运行,而内核地址空间对所有进程都是相同的。
虚拟地址空间中的页面通过MMU映射到物理地址,MMU使用页表来执行映射。每个虚拟内存页面可能处于以下几种状态:
- 未映射:访问这些地址将导致
SIGSEGV
信号。
- 映射到进程私有的物理内存页面。
- 映射到与其他进程共享的物理内存页面。
- 映射并设置了写时复制(CoW)标志:写入操作会在内核中被捕获,内核会复制该页面并将其映射到进程,然后再允许写入操作。
- 映射到内核使用的物理内存页面。
内核还可以将页面映射到预留的内存区域,例如用于访问设备驱动程序中的寄存器和内存缓冲区。
虚拟内存的优缺点
虚拟内存具有以下优点:
- 可以捕获无效的内存访问,并通过
SIGSEGV
信号提醒应用程序。
- 进程在独立的内存空间中运行,相互隔离。
- 通过共享公共代码和数据(如库),可以高效地使用内存。
- 可以通过添加交换文件来增加物理内存的表观容量,但在嵌入式目标上交换操作很少使用。
然而,虚拟内存也存在一些缺点:
- 难以确定应用程序的实际内存预算。
- 默认的分配策略是过度承诺,这可能导致棘手的内存不足情况。
- 内存管理代码在处理异常(页面错误)时引入的延迟会使系统的确定性降低,这对于实时程序来说尤为重要。
内核空间内存布局
内核内存的管理相对简单,它不是按需分页的,这意味着每次使用
kmalloc()
或类似函数进行分配时,都会分配实际的物理内存。内核内存不会被丢弃或换出。
一些架构在启动时会在内核日志消息中显示内存映射的摘要。例如,在一个32位ARM设备(BeagleBone Black)上,内核日志显示:
Memory: 511MB = 511MB total
Memory: 505980k/505980k available, 18308k reserved, 0K highmem
Virtual kernel memory layout:
vector : 0xffff0000 - 0xffff1000 ( 4 kB)
fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB)
vmalloc : 0xe0800000 - 0xff000000 ( 488 MB)
lowmem : 0xc0000000 - 0xe0000000 ( 512 MB)
pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)
modules : 0xbf800000 - 0xbfe00000 ( 6 MB)
.text : 0xc0008000 - 0xc0763c90 (7536 kB)
.init : 0xc0764000 - 0xc079f700 ( 238 kB)
.data : 0xc07a0000 - 0xc0827240 ( 541 kB)
.bss : 0xc0827240 - 0xc089e940 ( 478 kB)
其中,505980 KiB的可用内存是内核开始执行但尚未进行动态分配时看到的空闲内存量。
内核空间内存的消费者包括:
- 内核本身:即启动时从内核映像文件加载的代码和数据,如
.text
、
.init
、
.data
和
.bss
段所示。
.init
段在内核完成初始化后会被释放。
- 通过 slab 分配器分配的内存:用于各种内核数据结构,包括使用
kmalloc()
进行的分配,这些内存来自
lowmem
区域。
- 通过
vmalloc()
分配的内存:通常用于分配比
kmalloc()
更大的内存块,这些内存位于
vmalloc
区域。
- 设备驱动程序的映射:用于访问各种硬件的寄存器和内存,可以通过读取
/proc/iomem
查看。这些映射也来自
vmalloc
区域,但由于它们映射到主系统内存之外的物理内存,因此不会占用实际内存。
- 内核模块:加载到
modules
区域。
- 其他未被跟踪的低级分配。
内核内存使用量
要确定内核使用了多少内存并没有一个完整的答案,但可以通过以下方法获取相关信息:
-
查看内核日志或使用
size
命令
:可以查看内核日志中显示的内核代码和数据占用的内存,也可以使用
size
命令,例如:
$ arm-poky-linux-gnueabi-size vmlinux
text data bss dec hex filename
9013448 796868 8428144 18238460 1164bfc vmlinux
通常,内核的静态代码和数据段占用的内存与总内存相比是较小的。如果不是这种情况,则需要检查内核配置并移除不需要的组件。
-
读取
/proc/meminfo
:可以获取更多内存使用信息。内核内存使用量是以下各项的总和:
-
Slab
:slab 分配器分配的总内存。
-
KernelStack
:执行内核代码时使用的栈空间。
-
PageTables
:用于存储页表的内存。
-
VmallocUsed
:
vmalloc()
分配的内存。
对于 slab 分配,可以通过读取
/proc/slabinfo
获取更多信息;对于
vmalloc
区域,可以通过
/proc/vmallocinfo
查看分配明细。对于内核模块,可以使用
lsmod
命令查看代码和数据占用的内存空间。
用户空间内存布局
Linux对用户空间采用延迟分配策略,只有在程序访问物理内存页面时才会进行映射。例如,使用
malloc(3)
分配一个1 MiB的缓冲区时,会返回一个指向内存地址块的指针,但实际上并没有分配物理内存。当程序尝试访问这些地址时,内核会捕获访问操作,这被称为页面错误。此时,内核会尝试找到一个物理内存页面,并将其添加到该进程的页表映射中。
以下是一个简单的程序示例,用于演示页面错误的发生:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#define BUFFER_SIZE (1024 * 1024)
void print_pgfaults(void)
{
int ret;
struct rusage usage;
ret = getrusage(RUSAGE_SELF, &usage);
if (ret == -1) {
perror("getrusage");
} else {
printf ("Major page faults %ld\n", usage.ru_majflt);
printf ("Minor page faults %ld\n", usage.ru_minflt);
}
}
int main (int argc, char *argv[])
{
unsigned char *p;
printf("Initial state\n");
print_pgfaults();
p = malloc(BUFFER_SIZE);
printf("After malloc\n");
print_pgfaults();
memset(p, 0x42, BUFFER_SIZE);
printf("After memset\n");
print_pgfaults();
memset(p, 0x42, BUFFER_SIZE);
printf("After 2nd memset\n");
print_pgfaults();
return 0;
}
运行该程序时,输出可能如下:
Initial state
Major page faults 0
Minor page faults 172
After malloc
Major page faults 0
Minor page faults 186
After memset
Major page faults 0
Minor page faults 442
After 2nd memset
Major page faults 0
Minor page faults 442
可以看到,在初始化程序环境后遇到了172个小页面错误,调用
getrusage(2)
时又增加了14个(这些数字会根据架构和C库版本的不同而有所变化)。重要的是,在填充内存时页面错误的增加:442 - 186 = 256,而缓冲区为1 MiB,即256页。第二次调用
memset(3)
没有产生额外的页面错误,因为所有页面都已经映射好了。
页面错误分为小错误和大错误。小错误只需要内核找到一个物理内存页面并将其映射到进程地址空间;大错误则发生在虚拟内存映射到文件时,例如使用
mmap(2)
,此时内核不仅要找到一个内存页面并进行映射,还需要从文件中填充数据,因此大错误在时间和系统资源上的开销更大。
进程内存映射
可以通过
proc
文件系统查看进程的内存映射。例如,以下是PID为1的
init
进程的内存映射:
# cat /proc/1/maps
00008000-0000e000 r-xp 00000000 00:0b 23281745 /sbin/init
00016000-00017000 rwxp 00006000 00:0b 23281745 /sbin/init
00017000-00038000 rwxp 00000000 00:00 0 [heap]
b6ded000-b6f1d000 r-xp 00000000 00:0b 23281695 /lib/libc-2.19.so
b6f1d000-b6f24000 ---p 00130000 00:0b 23281695 /lib/libc-2.19.so
b6f24000-b6f26000 r-xp 0012f000 00:0b 23281695 /lib/libc-2.19.so
b6f26000-b6f27000 rwxp 00131000 00:0b 23281695 /lib/libc-2.19.so
b6f27000-b6f2a000 rwxp 00000000 00:00 0
b6f2a000-b6f49000 r-xp 00000000 00:0b 23281359 /lib/ld-2.19.so
b6f4c000-b6f4e000 rwxp 00000000 00:00 0
b6f4f000-b6f50000 r-xp 00000000 00:00 0 [sigpage]
b6f50000-b6f51000 r-xp 0001e000 00:0b 23281359 /lib/ld-2.19.so
b6f51000-b6f52000 rwxp 0001f000 00:0b 23281359 /lib/ld-2.19.so
beea1000-beec2000 rw-p 00000000 00:00 0 [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
前三个列显示了每个映射的起始和结束虚拟地址以及权限,权限的含义如下:
-
r
:读
-
w
:写
-
x
:执行
-
s
:共享
-
p
:私有(写时复制)
如果映射与文件相关,则文件名会出现在最后一列,第四、五、六列包含文件的偏移量、块设备号和文件的inode。大多数映射是到程序本身及其链接的库。程序可以在两个区域分配内存,即
[heap]
和
[stack]
。使用
malloc
分配的内存来自
[heap]
(除了非常大的分配),栈上的分配来自
[stack]
。这两个区域的最大大小由进程的
ulimit
控制:
-
堆
:
ulimit -d
,默认无限制。
-
栈
:
ulimit -s
,默认8 MiB。
超出限制的分配会被
SIGSEGV
信号拒绝。当内存不足时,内核可能会丢弃映射到文件且为只读的页面,如果再次访问该页面,会导致一个大页面错误,并从文件中重新读取。
综上所述,Linux的实时策略和内存管理是系统设计中非常重要的方面。了解这些知识可以帮助我们更好地编写稳定和高效的嵌入式应用程序。在实际应用中,需要根据具体需求选择合适的策略和方法,以确保系统的性能和可靠性。
Linux实时策略与内存管理全解析
内存管理工具与方法总结
在前面的内容中,我们详细探讨了Linux实时策略和内存管理的各个方面。接下来,我们将对内存管理相关的工具和方法进行总结,并进一步分析内存管理中可能遇到的问题及解决思路。
内存使用测量与问题检测工具
为了更好地管理内存,我们需要了解一些常用的工具:
-
简单工具
:
-
free
:可以快速查看系统的整体内存使用情况,包括总内存、空闲内存、已使用内存等信息。
-
top
:实时显示系统中各个进程的资源使用情况,包括内存使用量,方便我们监控系统的实时状态。
-
复杂工具
:
-
mtrace
:用于检测内存泄漏,它可以跟踪程序中内存的分配和释放情况,帮助我们找出未释放的内存块。
-
Valgrind
:功能强大的内存调试和性能分析工具,不仅可以检测内存泄漏,还可以发现内存越界、使用未初始化内存等问题。
内存管理问题分析与解决
在内存管理过程中,可能会遇到以下问题及相应的解决方法:
-
内存泄漏
:这是一个常见的问题,会导致系统内存逐渐耗尽。可以使用
mtrace
或
Valgrind
等工具来检测内存泄漏,找出泄漏的代码位置并进行修复。
-
内存不足
:当系统内存不足时,可能会导致程序运行缓慢甚至崩溃。可以通过以下方法解决:
-
优化程序代码
:减少不必要的内存分配,及时释放不再使用的内存。
-
增加物理内存
:如果条件允许,可以增加系统的物理内存。
-
调整内核参数
:例如调整
swappiness
参数,控制系统使用交换空间的倾向。
内存管理流程梳理
为了更清晰地展示内存管理的流程,我们可以用mermaid流程图来表示:
graph TD;
A[程序启动] --> B[虚拟内存分配];
B --> C{是否访问内存};
C -- 是 --> D[触发页面错误];
D --> E{小错误还是大错误};
E -- 小错误 --> F[内核找物理页映射];
E -- 大错误 --> G[内核找页并从文件填充数据];
F --> H[继续程序执行];
G --> H;
C -- 否 --> H;
H --> I{是否内存不足};
I -- 是 --> J[内核丢弃只读页];
J --> K{再次访问丢弃页};
K -- 是 --> G;
K -- 否 --> H;
I -- 否 --> H;
内存管理在嵌入式系统中的特殊考虑
在嵌入式系统中,由于系统内存通常有限,内存管理尤为重要。以下是一些在嵌入式系统中进行内存管理的特殊考虑:
-
减少内存开销
:尽量使用轻量级的库和算法,减少不必要的内存占用。
-
避免频繁的内存分配和释放
:频繁的内存操作会增加系统的开销,尽量预先分配好所需的内存。
-
优化内核配置
:根据嵌入式系统的具体需求,裁剪内核,去除不必要的功能,减少内核内存的使用。
实时策略与内存管理的综合应用
在实际的嵌入式应用开发中,实时策略和内存管理往往需要综合考虑。例如,对于实时任务,我们需要确保其有足够的内存资源,并且能够在规定的时间内完成任务。同时,我们也要避免实时任务过度占用内存,影响其他任务的正常运行。
为了实现实时策略和内存管理的平衡,可以采取以下措施:
-
合理分配内存
:根据任务的重要性和实时性要求,为不同的任务分配适当的内存资源。
-
监控和调整
:实时监控系统的内存使用情况和任务执行状态,根据实际情况调整实时策略和内存分配。
总结与展望
通过对Linux实时策略和内存管理的深入学习,我们了解到这些知识对于编写稳定、高效的嵌入式应用程序至关重要。实时策略可以确保关键任务在规定的时间内完成,而合理的内存管理可以提高系统的资源利用率,避免内存泄漏和不足等问题。
在未来的嵌入式系统开发中,随着硬件技术的不断发展和应用需求的不断提高,实时策略和内存管理将面临更多的挑战和机遇。我们需要不断学习和探索新的方法和技术,以适应不断变化的环境。同时,我们也要注重实践,将所学的知识应用到实际项目中,不断积累经验,提高自己的开发能力。
希望本文能够帮助读者更好地理解Linux实时策略和内存管理的相关知识,并在实际应用中发挥作用。如果你在学习和实践过程中遇到任何问题,欢迎留言讨论。
通过以上内容,我们对Linux实时策略和内存管理进行了全面的解析,从实时策略的类型、选择方法,到虚拟内存的基础知识、内核和用户空间的内存布局,再到内存管理工具的使用和实际应用中的问题解决,希望能为你在嵌入式系统开发中提供有价值的参考。
超级会员免费看

被折叠的 条评论
为什么被折叠?



