Linux 内存管理与信号处理全解析
1. 内存锁定对系统性能的影响
改变内核的分页行为可能会对系统整体性能产生负面影响。当一个应用程序的页面被锁定在内存中时,其他应用程序的页面可能会被换出。内核会选择最优的页面进行换出,即未来最不可能使用的页面。但当我们改变内核的分页行为时,它可能不得不换出次优的页面。
2. 地址空间部分锁定
POSIX 1003.1b - 1993 定义了两个将一个或多个页面锁定到物理内存的接口,确保这些页面不会被换出到磁盘。
2.1 mlock() 函数
#include <sys/mman.h>
int mlock (const void *addr, size_t len);
-
功能
:将从
addr开始、长度为len字节的虚拟内存锁定到物理内存。 -
返回值
:成功返回 0,失败返回 -1 并设置相应的
errno。 -
注意事项
:POSIX 标准要求
addr按页面边界对齐,Linux 会自动将addr向下舍入到最近的页面。具有可移植性要求的程序应确保addr位于页面边界。
2.2 错误码
| 错误码 | 描述 |
|---|---|
| EINVAL |
参数
len
为负数
|
| ENOMEM |
调用者试图锁定的页面数量超过了
RLIMIT_MEMLOCK
资源限制
|
| EPERM |
RLIMIT_MEMLOCK
资源限制为 0,且进程不具备
CAP_IPC_LOCK
能力
|
2.3 示例代码
int ret;
/* lock 'secret' in memory */
ret = mlock (secret, strlen (secret));
if (ret)
perror ("mlock");
3. 地址空间全部锁定
对于实时应用程序等需要将整个地址空间锁定到物理内存的场景,POSIX 定义了
mlockall()
系统调用。
#include <sys/mman.h>
int mlockall (int flags);
- 功能 :将当前进程地址空间中的所有页面锁定到物理内存。
-
参数
flags:是以下两个值的按位或,用于控制行为。-
MCL_CURRENT:锁定当前所有映射的页面,如栈、数据段、映射文件等。 -
MCL_FUTURE:确保未来映射到地址空间的所有页面也被锁定。
-
3.1 返回值和错误码
-
返回值
:成功返回 0,失败返回 -1 并设置相应的
errno。 -
错误码
:与
mlock()类似,包括EINVAL、ENOMEM和EPERM。
4. 解锁内存
POSIX 标准化了两个解锁物理内存页面的接口,允许内核根据需要将页面换出到磁盘。
#include <sys/mman.h>
int munlock (const void *addr, size_t len);
int munlockall (void);
-
munlock():解锁从addr开始、长度为len字节的页面,撤销mlock()的效果。 -
munlockall():撤销mlockall()的效果。
4.1 返回值和错误码
-
返回值
:成功返回 0,失败返回 -1 并设置相应的
errno。 -
错误码
:包括
EINVAL(仅munlock())、ENOMEM和EPERM。
4.2 内存锁定不嵌套
单个
mlock()
或
munlock()
调用将解锁已锁定的页面,无论该页面通过
mlock()
或
mlockall()
锁定了多少次。
5. 锁定限制
Linux 对进程可以锁定的页面数量进行了限制,因为锁定过多页面会影响系统整体性能,甚至导致内存分配失败。
- 具有
CAP_IPC_LOCK
能力的进程可以锁定任意数量的页面。
- 没有该能力的进程只能锁定
RLIMIT_MEMLOCK
字节,默认值为 32 KB。
6. 判断页面是否在物理内存中
Linux 提供了
mincore()
函数,用于调试和诊断,可确定给定内存范围是否在物理内存中或已换出到磁盘。
#include <unistd.h>
#include <sys/mman.h>
int mincore (void *start,
size_t length,
unsigned char *vec);
- 功能 :提供一个向量,指示映射的哪些页面在系统调用时位于物理内存中。
-
参数
:
-
start:必须按页面边界对齐。 -
length:不需要按页面边界对齐。 -
vec:用于返回向量,每个字节对应一个页面。
-
6.1 返回值和错误码
-
返回值
:成功返回 0,失败返回 -1 并设置相应的
errno。 -
错误码
:包括
EAGAIN、EFAULT、EINVAL和ENOMEM。
6.2 限制
目前,该系统调用仅适用于使用
MAP_SHARED
创建的基于文件的映射。
7. 机会主义内存分配
Linux 采用机会主义分配策略。当进程向内核请求额外内存时,内核会承诺分配内存,但实际上并不提供物理存储。只有当进程写入新分配的内存时,内核才会将内存承诺转换为物理内存分配。
7.1 优点
- 延迟大部分工作,直到最后一刻。
- 按需逐页满足请求,仅使用实际使用的物理内存。
- 允许承诺的内存量远远超过可用的物理内存和交换空间,即过度承诺。
7.2 过度承诺和 OOM
过度承诺允许系统运行更多、更大的应用程序。但如果进程试图满足的未完成承诺超过系统的物理内存和交换空间,就会发生内存不足(OOM)情况。
7.3 OOM 处理
当发生 OOM 情况时,内核会使用 OOM 杀手选择一个“值得”终止的进程,通常是消耗最多内存且最不重要的进程。
7.4 禁用过度承诺
可以通过
/proc/sys/vm/overcommit_memory
文件或
sysctl
参数
vm.overcommit_memory
禁用过度承诺。
-
0
:启发式过度承诺策略,合理过度承诺,但不允许过度的过度承诺。
-
1
:允许所有承诺成功。
-
2
:禁用过度承诺,启用严格记账,内存承诺限制为交换区大小加上可配置百分比的物理内存。
8. 信号概述
信号是软件中断,用于处理异步事件。这些事件可以来自系统外部,如用户按下
Ctrl - C
;也可以来自程序或内核内部,如进程执行除零操作。信号也是一种基本的进程间通信(IPC)方式,一个进程可以向另一个进程发送信号。
8.1 信号的生命周期
信号的生命周期包括三个阶段:
graph LR
A[信号产生] --> B[信号存储]
B --> C[信号处理]
- 产生 :信号被引发(也称为发送或生成)。
- 存储 :内核存储信号,直到能够传递它。
- 处理 :内核根据进程的要求采取相应的行动。
8.2 内核处理信号的三种方式
| 处理方式 | 描述 | 不可处理的信号 |
|---|---|---|
| 忽略信号 | 不采取任何行动 | SIGKILL 和 SIGSTOP |
| 捕获并处理信号 | 内核暂停当前代码执行,跳转到先前注册的函数,执行完后返回原位置 | SIGKILL 和 SIGSTOP |
| 执行默认操作 | 根据信号类型执行默认操作,通常是终止进程 | 无 |
9. 信号标识符
每个信号都有一个以
SIG
为前缀的符号名称,同时也与一个整数标识符相关联。信号编号从 1 开始,大约有 31 个信号。
9.1 注意事项
- 可移植的程序应始终使用信号的可读名称,而不是其整数值。
-
信号值为 0 的是特殊的空信号,某些系统调用(如
kill())将其用作特殊情况。 -
可以使用
kill -l命令生成系统支持的信号列表。
10. Linux 支持的信号
| 信号 | 描述 | 默认动作 |
|---|---|---|
| SIGABRT |
由
abort()
发送
| 终止并生成核心转储文件 |
| SIGALRM |
由
alarm()
和
setitimer()
(
ITIMER_REAL
标志)在闹钟到期时发送
| 终止 |
| SIGBUS | 硬件或对齐错误 | 终止并生成核心转储文件 |
| SIGCHLD | 子进程终止或停止 | 忽略 |
| SIGCONT | 进程停止后继续 | 忽略 |
| SIGFPE | 算术异常 | 终止并生成核心转储文件 |
| SIGHUP | 会话控制终端断开或会话领导者终止 | 终止 |
| SIGILL | 进程尝试执行非法指令 | 终止并生成核心转储文件 |
| SIGINT |
用户生成中断字符(
Ctrl - C
)
| 终止 |
| SIGIO | 异步 I/O 事件 | 终止(仅 Alpha 架构) |
| SIGKILL | 不可捕获的进程终止 | 终止 |
| SIGPIPE | 进程向无读者的管道写入 | 终止 |
| SIGPROF | 性能分析定时器到期 | 终止 |
| SIGPWR | 电源故障 | 终止 |
| SIGQUIT |
用户生成退出字符(
Ctrl - \
)
| 终止并生成核心转储文件 |
| SIGSEGV | 内存访问违规 | 终止并生成核心转储文件 |
| SIGSTKFLT | 协处理器栈故障(仅用于向后兼容) | 终止 |
| SIGSTOP | 无条件停止进程 | 停止 |
| SIGSYS | 进程尝试调用无效系统调用 | 终止并生成核心转储文件 |
| SIGTERM | 可捕获的进程终止 | 终止 |
| SIGTRAP | 遇到断点 | 终止并生成核心转储文件 |
| SIGTSTP |
用户生成暂停字符(
Ctrl - Z
)
| 停止 |
| SIGTTIN | 后台进程从控制终端读取 | 停止 |
| SIGTTOU | 后台进程向控制终端写入 | 停止 |
| SIGURG | 套接字上有带外(OOB)数据到达 | 忽略 |
| SIGUSR1 和 SIGUSR2 | 用户定义的信号 | 终止 |
| SIGVTALRM |
由
setitimer()
(
ITIMER_VIRTUAL
标志)生成
| 终止 |
| SIGWINCH | 控制终端窗口大小改变 | 忽略 |
| SIGXCPU | 处理器资源限制超出 | 终止并生成核心转储文件 |
| SIGXFSZ | 文件资源限制超出 | 终止并生成核心转储文件 |
10.1 部分信号详细解释
-
SIGABRT
:
abort()函数发送该信号,用于终止进程并生成核心文件。在 Linux 中,assert()等断言失败时会调用abort()。 -
SIGALRM
:
alarm()和setitimer()(ITIMER_REAL标志)在闹钟到期时发送该信号。 - SIGCHLD :子进程终止或停止时,内核向其父进程发送该信号。默认情况下被忽略,需要显式捕获和处理。
- SIGHUP :会话控制终端断开或会话领导者终止时发送,常用于守护进程重新加载配置文件。
-
SIGINT
:用户按下
Ctrl - C时发送,常用于进程在终止前进行清理。 - SIGKILL :用于系统管理员无条件杀死进程,不可捕获或忽略。
- SIGTERM :用于用户优雅地终止进程,进程可以捕获该信号进行清理,但应尽快终止。
11. 部分信号详细解释(续)
-
SIGBUS
:当进程发生除内存保护之外的硬件故障时,内核会引发此信号。在传统 Unix 系统中,该信号代表各种不可恢复的错误,如未对齐的内存访问。不过,Linux 内核会自动修复大多数此类错误,仅在进程不当访问通过
mmap()创建的内存区域时才会引发此信号。若未捕获该信号,内核将终止进程并生成核心转储文件。 - SIGCONT :当进程在停止后恢复运行时,内核会向其发送此信号。默认情况下,该信号会被忽略,但进程可捕获它以在继续运行后执行特定操作,常用于终端或编辑器刷新屏幕。
- SIGFPE :尽管名称为浮点异常信号,但它代表任何算术异常,包括溢出、下溢和除零操作。默认操作是终止进程并生成核心文件,进程也可选择捕获并处理该信号,但继续运行时,进程的行为和违规操作的结果是未定义的。
- SIGILL :当进程试图执行非法机器指令时,内核会发送此信号。默认操作是终止进程并生成核心转储文件,进程可选择捕获并处理该信号,但发生后其行为是未定义的。
- SIGIO :当生成 BSD 风格的异步 I/O 事件时会发送此信号,这种风格的 I/O 在 Linux 中很少使用。
- SIGPIPE :若进程向无读者的管道写入数据,内核会引发此信号。默认操作是终止进程,但该信号可被捕获和处理。
-
SIGPROF
:使用
setitimer()函数并带有ITIMER_PROF标志时,性能分析定时器到期会生成此信号。默认操作是终止进程。 -
SIGPWR
:此信号与系统相关,在 Linux 中表示低电池状况,如不间断电源(UPS)的情况。UPS 监控守护进程会将此信号发送给
init,init会进行清理并关闭系统,以避免在停电前出现问题。 -
SIGQUIT
:当用户提供终端退出字符(通常是
Ctrl-\)时,内核会为前台进程组中的所有进程引发此信号。默认操作是终止进程并生成核心转储文件。 - SIGSEGV :该信号名称源于分段违规,当进程尝试进行无效的内存访问时会发送,包括访问未映射的内存、从无读取权限的内存读取、在无执行权限的内存中执行代码或向无写入权限的内存写入。进程可捕获并处理该信号,但默认操作是终止进程并生成核心转储文件。
-
SIGSTOP
:仅由
kill()发送,用于无条件停止进程,且该信号不可被捕获或忽略。 -
SIGSYS
:当进程试图调用无效的系统调用时,内核会发送此信号。例如,在较新版本操作系统上构建的二进制文件在较旧版本上运行时可能会出现这种情况。通过
glibc进行系统调用的正确构建的二进制文件不应收到此信号,无效的系统调用应返回 -1 并将errno设置为ENOSYS。 - SIGTRAP :当进程遇到断点时,内核会发送此信号。一般来说,调试器会捕获此信号,其他进程则忽略它。
-
SIGTSTP
:当用户提供暂停字符(通常是
Ctrl-Z)时,内核会向所有前台进程组中的进程发送此信号。 - SIGTTIN :当处于后台的进程试图从其控制终端读取数据时,会发送此信号。默认操作是停止进程。
- SIGTTOU :当处于后台的进程试图向其控制终端写入数据时,会发送此信号。默认操作是停止进程。
- SIGURG :当套接字上有带外(OOB)数据到达时,内核会向进程发送此信号,带外数据超出了本文的讨论范围。
- SIGUSR1 和 SIGUSR2 :这两个信号可供用户自定义使用,内核不会引发它们。进程可根据自身需求使用这两个信号,常见用途是指示守护进程执行不同的操作。默认操作是终止进程。
-
SIGVTALRM
:由
setitimer()函数在使用ITIMER_VIRTUAL标志时生成,默认操作是终止进程。 - SIGWINCH :当控制终端窗口大小改变时,会发送此信号,默认情况下会被忽略。
- SIGXCPU :当处理器资源限制超出时,会发送此信号,默认操作是终止进程并生成核心转储文件。
- SIGXFSZ :当文件资源限制超出时,会发送此信号,默认操作是终止进程并生成核心转储文件。
11.1 信号处理的重要性
信号处理在 Linux 系统中至关重要,它能让程序对各种异步事件做出及时响应,保证程序的稳定性和可靠性。例如,捕获
SIGTERM
信号可让程序在终止前进行必要的清理工作,避免数据丢失或系统状态异常。
12. 信号处理的实际应用
12.1 捕获 SIGINT 信号进行清理
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
printf("Caught SIGINT, cleaning up...\n");
// 这里可以添加清理代码,如关闭文件、释放资源等
_exit(0);
}
int main() {
// 注册信号处理函数
signal(SIGINT, sigint_handler);
printf("Running... Press Ctrl-C to exit.\n");
while (1) {
sleep(1);
}
return 0;
}
上述代码展示了如何捕获
SIGINT
信号并进行清理工作。当用户按下
Ctrl-C
时,程序会调用
sigint_handler
函数,在该函数中可以添加清理代码,如关闭文件、释放资源等。
12.2 捕获 SIGTERM 信号优雅终止
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t terminate = 0;
void sigterm_handler(int signum) {
printf("Caught SIGTERM, preparing to terminate...\n");
terminate = 1;
}
int main() {
// 注册信号处理函数
signal(SIGTERM, sigterm_handler);
printf("Running... Send SIGTERM to terminate.\n");
while (!terminate) {
sleep(1);
}
printf("Cleaning up and terminating...\n");
// 这里可以添加清理代码
return 0;
}
此代码演示了如何捕获
SIGTERM
信号并优雅地终止程序。当收到
SIGTERM
信号时,程序会设置
terminate
标志,主循环检测到该标志后会跳出循环,进行清理工作并终止程序。
13. 信号处理的注意事项
13.1 可重入性
信号处理函数应是可重入的,即它可以在任何时刻被中断并再次调用,而不会导致数据不一致或其他问题。在信号处理函数中,应避免调用不可重入的函数,如
malloc()
、
printf()
等。
13.2 信号屏蔽
在信号处理函数执行期间,内核会自动屏蔽相同类型的信号,以避免信号嵌套处理。但对于不同类型的信号,可能需要手动屏蔽,以确保信号处理的正确性。
13.3 信号丢失
在某些情况下,信号可能会丢失。例如,当信号处理函数正在执行时,相同类型的信号可能会被丢弃。为避免信号丢失,可使用信号队列或其他机制来记录和处理信号。
14. 总结
本文详细介绍了 Linux 内存管理和信号处理的相关知识。在内存管理方面,包括内存锁定、解锁、锁定限制、判断页面是否在物理内存中以及机会主义内存分配等内容。通过合理使用内存锁定功能,可以提高某些应用程序的确定性和安全性,但也可能对系统性能产生影响。机会主义内存分配策略允许系统更高效地利用内存,但需要注意过度承诺可能导致的 OOM 情况。
在信号处理方面,介绍了信号的概念、生命周期、标识符以及 Linux 支持的各种信号。信号作为一种软件中断,为处理异步事件提供了强大的机制。通过正确处理信号,程序可以对各种异常情况做出响应,保证程序的稳定性和可靠性。同时,在进行信号处理时,需要注意可重入性、信号屏蔽和信号丢失等问题。
掌握这些知识,有助于开发者更好地理解 Linux 系统的工作原理,编写更高效、稳定的程序。
超级会员免费看
1836

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



