玳瑁的嵌入式日记D25-0825(进程)
编译流程
(一)源文件与编译工具
开发者编写的 C 语言代码存储于1.c
文件,其中包含程序的逻辑实现。gcc
作为编译工具,负责将1.c
编译为可执行文件A.out
。
(二)编译产物与格式
编译生成的A.out
为 ELF(Executable and Linkable Format )格式文件,该格式清晰划分代码、数据、符号等区域,便于系统加载运行,其中包含程序的代码段、数据段等内容。
(三)程序运行
执行./A.out
,系统为该程序分配独立的虚拟地址空间,程序在该空间内开始运行,后续各类内存区域的操作均基于此虚拟地址空间开展。
二、进程虚拟地址空间(以 Linux 系统为例)
程序运行后,操作系统为进程划分虚拟地址空间,各区域承担不同功能,保障程序有序、安全执行。
(一)代码段(code )
- 功能:存放程序编译后的机器指令,这些指令是程序运行的核心逻辑,决定程序的执行流程。
- 权限:设置为只读属性,防止程序在运行过程中意外篡改自身代码,保障程序执行逻辑的稳定性与安全性 。
(二)数据段(data )
- 功能:存储全局变量与静态变量,程序启动时会对这些变量进行初始化操作 。
- 权限:支持读写(
rw
),满足程序运行过程中对全局变量、静态变量的值修改需求,如统计计数、状态标记等场景会频繁操作该段数据 。
(三)堆(Heap )
- 功能:作为动态内存分配区域,程序通过
malloc
函数申请动态内存,使用free
函数释放不再需要的内存,常用于需要灵活管理内存大小的场景,如动态数组创建、复杂数据结构(链表、树等 )的内存分配 。 - 空间特性:空间可扩展,通常处于虚拟地址空间
<3G
范围,程序可根据实际内存需求向系统申请扩展堆空间,但也受系统内存资源限制 。
(四)共享映射区(Share/map )
- 功能:用于加载共享库,典型如
Libc.so
,printf
等标准库函数依赖该共享库运行。通过共享库机制,多个进程可共享同一份库代码,避免重复加载,极大节省系统内存资源 。
(五)栈(Stack )
- 功能:存储局部变量、函数参数、函数返回地址。函数调用时,相关数据入栈;函数返回时,数据出栈,遵循 “后进先出” 规则,保障函数调用与返回流程的正确性 。
- 空间特性:空间大小通常较小(一般为 8M 左右 ),系统为栈空间设定固定大小限制,若程序中局部变量过多、函数嵌套过深,可能引发栈溢出问题 。
三、虚拟地址空间特性
(一)隔离性
每个进程的虚拟地址空间相互独立,即便不同进程虚拟地址空间中出现相同地址,实际对应物理内存也不同。这种隔离机制保障了进程间互不干扰,一个进程的错误操作或崩溃不会影响其他进程正常运行 。
(二)权限控制
不同内存区域设置不同权限,如代码段只读、数据段可读写等。通过精准的权限控制,有效避免程序因错误操作(如误写代码段 )导致的异常崩溃,提升程序运行的安全性与稳定性 。
本架构图完整呈现 “C 代码编写 → 编译为可执行文件 → 程序运行占用虚拟地址空间并在各区域协作执行” 的全流程
进程的状态
进程在生命周期中会经历多种状态,
- 就绪态:进程已分配到除 CPU 外的所有资源,等待 CPU 调度执行。
- 例:多个打开的程序,都在 “排队” 等 CPU 处理。
- 运行态:进程正在 CPU 上执行指令。
- 例:当前正在操作的文档编辑器,CPU 正在执行其输入处理代码。
- 阻塞态(等待态):进程因等待某个事件(如 IO 操作、信号)而暂停,暂时无法运行。
- 例:下载文件时,浏览器进程因等待网络数据,会进入阻塞态,此时 CPU 可切换去处理其他进程。
- 终止态:进程完成执行或被强制结束,资源被释放。
- 例:关闭软件后,其进程进入终止态,操作系统回收它占用的内存。
进程通常由程序段(代码)、数据段(运行时数据) 和进程控制块(PCB) 组成。其中 PCB 是核心,记录进程 ID、状态、内存地址等关键信息,是操作系统管理进程的 “档案”
查询进程相关命令
1.ps aux
列出系统中所有正在运行的进程及其详细信息
ps aux|less
2.top
top
是 Linux/macOS 系统中一款实时实时监控系统资源使用情况和进程状态的命令行工具,它会动态刷新显示信息,默认每 3 秒更新一次。与 ps aux
(静态快照)不同,top
更适合实时观察系统负载、进程活动和资源占用变化。
3.kill 和killall 发送一个信号
Fork
pid_t fork() fork()
的作用是从当前进程(父进程) 复制出一个新进程(子进程)。
- 子进程会继承父进程的代码、数据、打开的文件描述符、环境变量等几乎所有资源。
- 子进程与父进程是独立的,拥有自己的进程 ID(PID),运行在各自的内存空间中(修改数据不会相互影响)。
- 执行顺序不确定:父进程和子进程的调度由操作系统决定,无法预知谁先执行。
fork()
调用一次,却会返回两次(在父进程和子进程中各返回一次):
- 父进程中:返回子进程的 PID(一个正整数),用于标识和管理子进程。
- 子进程中:返回
0
,表示当前是子进程。 - 出错时:返回
-1
(如内存不足、进程数达到系统上限)。
父子关系
工作原理:写时复制(Copy-On-Write, COW)
早期 fork()
会完整复制父进程的内存,效率较低。现代系统采用写时复制优化:
- 初始时,子进程与父进程共享同一块内存(只读)。
- 当任一进程修改数据时,系统才会为修改的部分创建副本(只复制需要修改的页),避免不必要的复制开销。
getpid () / getppid () 函数
功能:获取当前进程的进程 ID(PID) // 功能:获取当前进程的父进程 ID(PPID)
fork()&&fork()||fork();
最终会产生 5 个进程(1 个初始进程 + 4 个子进程),核心是利用 fork()
的返回特性和逻辑运算符的短路规则,导致不同分支执行不同数量的 fork()
调用
进程结束的情况
- 正常终止(主动可控)
main
中return
:程序执行到main
函数的return
,触发进程终止,会执行相关资源清理。exit()
:C 标准库函数,终止进程前会执行 I/O 清理(关闭流、文件等 ),调用atexit
注册的清理函数。_exit
/_Exit
:系统调用(或类似底层实现 ),仅关闭已打开文件,不执行atexit
等清理逻辑,更 “直接暴力”。- 主线程退出(针对多线程程序 ):主线程结束,若其他线程未妥善处理,可能影响进程整体终止逻辑;若为单线程程序,等同于进程终止。
- 主线程调用
pthread_exit
(多线程场景 ):主线程主动调用该函数退出,进程不会立即终止(其他线程可继续运行 ),仅主线程自身结束执行。
- 异常终止(被动或错误触发)
abort()
:C 标准库函数,主动触发异常终止,会生成核心转储(core dump
,用于调试 ),通常因严重错误(如断言失败 )调用,默认会发送SIGABRT
信号。signal
+kill
/pid
:进程收到特定信号(如SIGKILL
强制终止、SIGTERM
正常终止等 ),由操作系统或其他进程通过kill
命令(指定pid
)发送信号触发终止。- 最后一个线程被
pthread_cancel
:多线程中,最后一个线程被取消(通过pthread_cancel
操作 ),导致进程因无活跃线程而终止 。
僵尸进程和孤儿进程
僵尸进程 (重点)
- 定义:当一个子进程结束运行后,它的父进程没有通过调用
wait()
或waitpid()
等函数获取子进程的终止状态,此时子进程虽然已经结束了核心任务执行,但其进程描述符(包含进程 ID、退出状态等信息)仍会被保留在系统中,这个子进程就变成了僵尸进程。 - 子先结束
孤儿进程
- 定义:当一个子进程的父进程提前结束运行,而子进程还未结束时,这个子进程就会成为孤儿进程。
- 父先结束
exit / _exit
exit()
的行为
- 调用注册的退出处理函数(通过
atexit()
或on_exit()
注册)。 - 冲刷(
fflush
)所有打开的标准 I/O 流(如printf
缓冲区),确保缓冲区数据写入文件 / 终端。 - 关闭所有打开的文件描述符(如
open()
创建的文件、网络套接字)。 - 最终调用系统调用
_exit()
真正终止进程。
_exit()
的行为
- 不执行任何用户注册的退出处理函数(
atexit
无效)。 - 不冲刷标准 I/O 缓冲区(
printf
数据可能丢失)。 - 直接关闭文件描述符(内核层面释放资源,但用户态缓冲区数据可能残留)。
- 立即终止进程,返回给父进程退出状态。