一、进程和线程的概念及基础用法
在Linux系统中,进程(Process)和线程(Thread)是操作系统进行任务调度的基本单位,它们既有联系又有区别。
1.1 进程和线程介绍
1.1.1 进程(Process)
定义:
进程是程序的一次执行实例,是操作系统资源分配的基本单位。每个进程拥有独立的地址空间、文件描述符、环境变量等资源。
特点:
(1)独立性:进程之间相互隔离,一个进程崩溃通常不会直接影响其他进程(通过进程间通信(IPC)才能交互)。
(2)资源开销大:创建、销毁或切换进程时,需要分配或回收内存、文件句柄等资源,成本较高。
(3)上下文切换:进程切换需要保存和恢复完整的运行环境(如寄存器、内存映射等),速度较慢。
Linux实现:
(1)通过 fork() 系统调用创建子进程(写时复制技术优化性能)。
(2)进程描述符为 task_struct(内核数据结构),所有进程通过树形结构组织( init 进程为根)。
1.1.2 线程(Thread)
定义:
线程是进程内的执行单元,是CPU调度的基本单位。同一进程的多个线程**共享进程的资源**(如内存、文件描述符),但拥有独立的栈和寄存器。
特点:
(1)轻量级:创建、切换线程的开销远小于进程,因为无需分配新地址空间或资源。
(2)共享资源:线程间可直接访问共享数据(需同步机制如互斥锁避免竞态条件)。
(3)协作性:一个线程崩溃可能导致整个进程终止(共享同一地址空间)。
Linux实现:
(1)线程通过 pthread_create()(POSIX线程库)创建,内核视角中线程被称为轻量级进程(LWP)。
(2)线程与进程使用相同的`task_struct`描述符,通过共享资源(如`mm_struct`内存描述符)区分。
1.1.3 进程 vs 线程(关键区别)
特性 | 进程 | 线程 |
---|---|---|
资源分配 | 独立地址空间和系统资源 | 共享进程资源 |
创建/切换开销 | 高 | 低 |
通信方式 | IPC(管道、信号、共享内存等) | 直接读写共享变量(需同步) |
健壮性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致进程终止 |
并发性 | 多进程可跨CPU核心并行 | 多线程可并行(同一进程内) |
1.1.4 Linux的独特实现
线程与进程的统一:
Linux内核不严格区分线程和进程,线程被视为共享资源的进程(通过clone()系统调用指定共享级别,如CLONE_VM共享地址空间)。
调度方式:
线程和进程均由内核调度器统一调度(Linux线程是内核线程,而非用户态线程)。
查看命令:
ps -ef:查看进程列表。
ps -eLf:查看进程及其线程(LWP列显示线程ID)。
top -H:显示线程级别的CPU占用。
1.2 基础用法练习
1.2.1 查看系统中各进程的编号pid
在Linux系统中,PID(Process ID/进程ID)是操作系统分配给每个运行进程的唯一数字标识符。
通过以下命令可以查看系统中各进程的编号pid:
ps -a
显示的进程编号
其中PID是进程的唯一标识符,比如2150507是ps的进程ID;TTY是进程关联的终端;CMD是进程对应的命令。
1.2.2 终止一个进程pid
终止进程是操作系统提供的“紧急制动”机制,本质是在资源有限、程序不完美的现实条件下,保障系统可控性的必要手段。合理使用终止操作是Linux系统管理的基本技能。
方式如下(优先SIGTERM,慎用SIGKILL):<PID>
信号 | 作用 | 使用场景 |
---|---|---|
SIGTERM (15) | 请求进程正常退出(允许清理) | 优雅关闭服务( kill <PID>) |
SIGKILL (9) | 强制立即终止(不可捕获或忽略) | 进程无响应时(kill -9 <PID>) |
首先创建一个用于测试的进程:
sleep 100 &
使用ps -a再次查看系统进程:
可以看到成功添加测试进程
通过以下命令终止一个进程pid:
测试进程已终止
二、Linux的虚拟内存管理/stm32的真实物理内存(内存映射)
2.1 Linux的虚拟内存管理
核心概念:
- 虚拟内存(Virtual Memory):
(1)每个进程拥有独立的虚拟地址空间(32位系统通常为4GB,64位系统更大),与物理内存分离。
(2)由MMU(Memory Management Unit)负责虚拟地址到物理地址的转换(页表映射)。
(3)支持分页(Paging)机制,内存按固定大小(如4KB)划分管理。
- 核心机制:
(1)地址转换:CPU访问虚拟地址 → MMU查询页表 → 映射到物理内存或磁盘(Swap)。
(2)内存保护:不同进程的虚拟空间隔离,防止非法访问。
(3)按需分配:物理内存仅在访问时分配(如 malloc() 申请内存后,实际使用时触发缺页异常)。
(4)Swap交换:当物理内存不足时,将不活跃的页面换出到磁盘。
优势:
(1)进程隔离:防止进程间内存冲突。
(2)大地址空间:程序可使用的内存远超物理内存大小(依赖Swap)。
(3)内存共享:动态库、文件映射(mmap)等可共享同一物理内存。
(4)安全性:通过权限位(读/写/执行)控制内存访问。
典型场景:
// Linux 程序申请内存(虚拟内存)
char *buf = malloc(1GB); // 仅分配虚拟地址,物理内存尚未占用
buf[0] = 'A'; // 触发缺页异常,分配物理页
2.2 STM32 真实物理内存(内存映射)
核心概念:
- 物理内存直接访问:
(1)STM32(Cortex-M系列)通常无MMU,CPU直接访问物理地址。
(2)内存划分由链接脚本(.ld文件)静态定义(如Flash、SRAM、外设寄存器区域)。
(3)外设寄存器通过内存映射I/O(Memory-Mapped I/O, MMIO)访问。
- 关键特点:
(1)无虚拟地址:所有地址均为物理地址(如SRAM地址 0x20000000)。
(2)静态分配:编译时确定变量和代码的内存位置(无运行时动态映射)。
(3)无内存保护:错误访问(如越界)直接导致硬件错误(HardFault)。
(4)无Swap:物理内存大小即极限(无法扩展)。
内存布局示例(Cortex-M):
地址范围 | 用途 |
---|---|
0x00000000 | Flash(程序代码) |
0x20000000 | SRAM(运行时数据) |
0x40000000 | 外设寄存器(GPIO、UART等) |
典型场景:
// STM32 直接操作物理地址
volatile uint32_t *gpio_a = (uint32_t*)0x40020000; // GPIOA寄存器地址
*gpio_a |= 0x01; // 直接写物理内存(控制外设)
2.3 主要区别对比
特性 | Linux虚拟内存 | STM32物理内存 |
---|---|---|
地址空间 | 虚拟地址(MMU转换) | 物理地址(直接访问) |
内存管理 | 动态分页、Swap | 静态分配(链接脚本定义) |
隔离性 | 进程间隔离(安全性高) | 无隔离(直接操作硬件) |
外设访问 | 通过设备驱动(/dev 或mmap ) | 直接MMIO(操作寄存器) |
动态内存分配 | 支持(malloc /free ) | 需手动管理(如静态数组或堆分配) |
适用场景 | 通用计算(多任务、复杂OS) | 实时嵌入式系统(低延迟、确定性) |
2.4 关键差异分析
(1) 灵活性 vs 确定性
Linux:
1)灵活性强,适合多任务、复杂应用。
2)但地址转换和缺页处理引入不确定性(不适合硬实时系统)。
STM32:
1)无MMU开销,访问延迟确定(适合实时控制)。
2)但需开发者手动管理内存,易出错(如栈溢出直接崩溃)。
(2) 内存扩展能力
Linux:通过Swap扩展可用内存(牺牲性能)。
STM32:物理内存固定(如64KB SRAM),无法扩展。
(3) 外设访问方式
Linux:外设需通过内核驱动(如 write()操作设备文件)。
STM32:直接读写寄存器(如 GPIOA = 1),效率极高。
2.5 总结
Linux虚拟内存:面向通用计算,提供隔离性、安全性和大内存支持,但牺牲实时性。需要多任务、安全隔离时进行选择。
STM32物理内存:为实时嵌入式设计,直接操作硬件,高效但缺乏保护机制。需要低延迟、确定性控制时进行选择。
三、 Linux系统调用函数练习---fork()、wait()、exec()
3.1 三种函数介绍
在 Linux 系统中,fork()、wait() 和 exec() 是进程管理的核心函数,用于创建新进程、控制进程执行和进程间同步。
3.1.1 fork() - 创建子进程
功能:
(1)复制当前进程,创建一个几乎完全相同的子进程。
(2)子进程获得父进程的代码段、数据段、堆栈、文件描述符等副本(采用写时复制(Copy-On-Write, COW)优化,初始时共享内存,只有修改时才真正复制)。
(3)调用一次,返回两次:
- 父进程返回 子进程的 PID(>0)。
- 子进程返回 0(若失败返回 -1)。
3.1.2 exec() - 替换进程映像
功能:
(1)替换当前进程的代码和数据,加载并执行一个新程序(如 /bin/ls)。
(2)调用成功后,原进程的代码不再执行,被新程序完全替代。
(3)若失败则返回 -1(原进程继续执行)。
常见函数族:
函数 | 特点 |
---|---|
execl() | 参数列表(可变参数) |
execv() | 参数数组(char *argv[] ) |
execle() | 可指定环境变量 |
execvp() | 自动从 PATH 查找程序 |
3.1.3 wait() - 等待子进程结束
功能:
(1)父进程阻塞等待子进程终止,并回收其资源(避免僵尸进程)。
(2)获取子进程退出状态(通过 status 参数)。
(3)若没有子进程,则立即返回 -1。
常见函数:
函数 | 特点 |
---|---|
wait(int *status) | 等待任意子进程结束 |
waitpid(pid_t pid, int *status, int options) | 等待指定子进程,支持非阻塞 |
3.2 函数调用练习
3.2.1 Xterminal
使用命令创建homework文件夹:
mkdir ~/homework && cd ~/homework
在homework文件夹中使用 vi 命令编辑一个c代码:
vi syscall_example.c
测试代码如下,在命令行输入 :wq 保存代码文件并退出:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程:替换为 ls -l 命令
printf("Child process (PID=%d) is running 'ls -l'...\n", getpid());
execl("/bin/ls", "ls", "-l", NULL); // 调用 exec
perror("exec failed"); // 只有 exec 失败时才会执行
return 1;
} else {
// 父进程:等待子进程结束
printf("Parent process (PID=%d) waiting for child...\n", getpid());
int status;
wait(&status); // 等待子进程
if (WIFEXITED(status)) {
printf("Child exited with code %d\n", WEXITSTATUS(status));
}
}
return 0;
}
关键步骤解析(三种函数结合):
fork():父进程继续执行 else 分支,子进程执行 if (pid == 0) 分支。
exec():子进程调用 execl() 替换为 ls -l 命令,原代码不再执行。
wait():父进程阻塞,直到子进程结束,并通过 WEXITSTATUS 获取退出码(0 表示成功)。
在终端界面使用gcc编译代码:
gcc syscall_example.c -o syscall_example
运行程序:
./syscall_example
结果展示:
3.2.2 树莓派
步骤与Xterminal一致,但是这里使用nano命令进行c文件编辑,因为树莓派默认安装了nano:
结果展示:
四、心得体会及参考资料
通过本次在Xterminal以及树莓派上实践Linux系统调用fork()、wait()、exec()等函数,我深入理解了多进程管理的核心机制。fork()创建子进程的一次调用两次返回特性、exec()替换进程映像的彻底性,以及wait()对资源回收的关键作用。在调试过程中,通过strace跟踪系统调用和perror定位错误,我掌握了排查系统调用问题的实用技巧,同时体会到树莓派这类嵌入式设备与通用计算机在系统设计上的异同。这次实验不仅验证了理论知识,更让我获得了在Linux环境下进行多进程编程的宝贵实践经验,为后续学习进程通信和并发控制奠定了坚实基础。
参考资料:
https://zhuanlan.zhihu.com/p/511801984