进程与线程
一、进程
1.1 进程定义
进程:进行(执行)中的程序
1.2 进程空间
每个进程拥有4GB的虚拟地址空间,其中:
-
用户空间:3GB(0x00000000 - 0xBFFFFFFF)
-
内核空间:1GB(0xC0000000 - 0xFFFFFFFF)
1.3 进程特点
1.3.1 优点
-
稳定性更高(相对线程)
-
进程间相互独立
-
一个进程崩溃不会直接影响其他进程
-
1.3.2 缺点
-
开辟及调度效率低
-
创建进程需要分配完整的4GB虚拟空间
-
上下文切换开销大
-
需要保存和恢复完整的进程状态
-
-
进程间通信复杂
-
需要使用专门的IPC机制
-
常见的进程间通信方式:
-
管道(Pipe)
-
消息队列(Message Queue)
-
共享内存(Shared Memory)
-
信号量(Semaphore)
-
套接字(Socket)
-
-
通信需要跨越进程边界
-
二、线程
2.1 线程定义
线程:程序中的一条执行线路
重要原则:一个进程至少有一个线程(主线程)
2.2 线程特点
2.2.1 优点
-
开辟及调度效率高
-
创建线程开销小
-
上下文切换快
-
共享进程的资源
-
-
线程间通信简单
-
共享相同的地址空间
-
可以直接访问全局变量
-
通过共享内存通信
-
2.2.2 缺点
-
稳定性低
-
线程间相互影响比较大
-
一个线程崩溃可能影响整个进程
-
需要谨慎处理共享资源的访问
-
2.3 线程独立资源
每个线程拥有独立的栈:
-
栈大小:8MB(通常配置)
-
栈内容:
-
局部变量
-
函数的返回地址
-
函数的参数
-
-
重要特性:线程独享自己的栈空间
三、进程内存布局(4GB地址空间)
3.1 用户空间(3GB)
从低地址到高地址:
1. code段(代码段)
-
存放二进制程序代码
-
只读属性
-
存储CPU执行的机器指令
2. DATA段(数据段)
-
全局静态区的一部分
-
存放已初始化且值不为0的:
-
全局变量
-
静态变量
-
-
位于BSS段之上
3. BSS段(未初始化数据段)
-
全局静态区的一部分
-
存放未初始化或初始化为0的:
-
全局变量
-
静态变量
-
-
程序启动时自动清零
注意顺序:code → DATA → BSS
4. heap(堆)
-
位置:BSS段之上
-
大小:小于3GB,通常2.8-2.9GB
-
特点:
-
用户自行管理的空间
-
使用时需要手动申请(malloc)
-
使用后需要手动释放(free)
-
动态分配,大小可变
-
5. stack(栈)
-
位置:用户空间顶部
-
大小:8MB/线程(可配置)
-
特点:
-
自动分配和释放
-
存储函数调用信息
-
线程独享
-
6. map(内存映射区)
3.2 内核空间(1GB)
-
位置:高地址1GB(0xC0000000 - 0xFFFFFFFF)
-
内容:内核代码和数据
-
权限:只有内核可以访问
四、内存布局总结
4.1 完整布局(从低到高)
0x00000000 ┌─────────────┐
│ code │ ← 二进制程序代码
├─────────────┤
│ DATA │ ← 已初始化的全局/静态变量(值≠0)
├─────────────┤
│ BSS │ ← 未初始化或初始化为0的全局/静态变量
├─────────────┤
│ heap │ ← 动态分配的内存(手动管理)
│ ↑ │
├─────────────┤
│ map │ ← 内存映射区(库函数地址)
├─────────────┤
│ stack │ ← 栈(线程独享,8MB/线程)
│ ↓ │
0xBFFFFFFF └─────────────┘ ← 用户空间结束(3GB)
0xC0000000 ┌─────────────┐
│ kernel │ ← 内核空间(1GB)
└─────────────┘
0xFFFFFFFF
4.2 简记顺序
低地址 → 高地址:
code → DATA → BSS → kernel → stack → heap → map
注意:实际顺序为code在最下面,stack在最上面,中间是heap和map
五、编程注意事项
5.1 野指针问题
野指针:非法的,指向未知的地址
危险示例:
{
p = malloc(); // 分配内存
free(p); // 释放内存
// 此时p成为野指针
// 错误:继续使用p
}
正确做法:
{
p = malloc(); // 分配内存
// 使用p...
free(p); // 释放内存
p = NULL; // 重要:将指针设为NULL
// 现在p不是野指针了
}
5.2 函数返回值传递
ARM架构特点 :函数的返回值用r0寄存器传递
原理:
-
r0寄存器用于存储函数返回的第一个值
-
如果返回值是结构体等较大数据,可能使用其他方式
示例:
int add(int a, int b) {
return a + b; // 结果通过r0寄存器返回
}
5.3 动态库使用
动态库特点:
-
文件名:libxxx.so
-
位置:映射到map区域
-
优点:节省内存,多个进程可共享
加载过程:
-
程序运行时,动态库被加载到map区域
-
建立libxxx.so与map区域的映射关系
-
程序通过映射地址调用库函数
六、关键点总结
6.1 进程 vs 线程
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 执行中的程序 | 程序中的执行线路 |
| 最小数量 | 1个 | 1个(主线程) |
| 地址空间 | 独立的4GB | 共享进程的4GB |
| 栈空间 | 有,但概念不同 | 独立栈(8MB/线程) |
| 开辟效率 | 低 | 高 |
| 调度效率 | 低 | 高 |
| 通信复杂度 | 复杂(需要IPC) | 简单(共享内存) |
| 稳定性 | 更高(相互独立) | 更低(相互影响) |
6.2 内存区域功能
| 区域 | 内容 | 管理方式 | 特点 |
|---|---|---|---|
| code | 程序代码 | 系统 | 只读,存储指令 |
| DATA | 已初始化变量 | 系统 | 全局/静态,值≠0 |
| BSS | 未初始化变量 | 系统 | 全局/静态,启动时清零 |
| heap | 动态分配内存 | 手动 | malloc/free,大小可变 |
| stack | 局部变量等 | 自动 | 函数调用,线程独享 |
| map | 库函数映射 | 系统 | 动态库加载区 |
| kernel | 内核代码数据 | 系统 | 内核专用,用户不可访问 |
6.3 编程规则
-
避免野指针:free后立即设指针为NULL
-
理解返回值:ARM中函数返回值通过r0传递
-
合理使用内存:
-
小数据、临时变量用栈
-
大数据、长期存在用堆
-
全局数据用DATA/BSS
-
-
线程安全:多个线程访问共享资源需要同步
-
资源管理:申请的资源要及时释放