操作系统进程管理笔记
1. 进程的基本概念
1.1 进程的定义
进程就是运行中的程序。程序本身是没有生命周期的,它只是存在磁盘上面的一些指令(也可能是一些静态数据)。是操作系统让这些字节运行起来,让程序发挥作用。
1.2 CPU的时分共享
操作系统通过让一个进程只运行一个时间片,然后切换到其他进程,提供了存在多个虚拟CPU的假象。这就是时分共享(time sharing)CPU技术,允许用户如愿运行多个并发进程。潜在的开销就是性能损失,因为如果CPU必须共享,每个进程的运行就会慢一点。
1.3 进程的机器状态
进程的机器状态包括:
- 内存:进程可以访问的内存(称为地址空间,address space)
- 指令存在内存中
- 正在运行的程序读取和写入的数据也在内存中
- 寄存器:
- 程序计数器(Program Counter,PC):告诉我们程序当前正在执行哪个指令
- 栈指针(stack pointer)和帧指针(frame pointer):用于管理函数参数栈、局部变量和返回地址
2. 程序与进程的区别
- 程序(Program):一个静态实体,通常是以可执行文件形式存储在磁盘上的指令和数据的集合
- 进程(Process):程序的动态执行实例,运行时加载到内存中,拥有独立的地址空间、执行状态和系统资源
简单来说,程序是静态的"蓝图",而进程是程序被激活后的"活体"。
3. 操作系统启动并运行程序的过程
3.1 加载程序到内存
- 定位可执行文件:根据用户命令或系统调用找到磁盘上的可执行文件
- 读取文件头部:
- 入口点(Entry Point):程序开始执行的地址
- 段信息:代码段、数据段等的地址和大小
- 从磁盘读取字节:
- 代码段:包含程序的指令,通常是只读的
- 数据段:包含初始化的全局变量和静态变量
- BSS段:包含未初始化的全局变量
- 分配虚拟地址空间:为进程分配独立的虚拟地址空间
- 按需加载(可选):延迟加载优化性能
3.2 分配堆栈和堆
- 堆栈(Stack):存储函数调用信息、局部变量等
- 堆(Heap):用于动态内存分配
3.3 设置执行环境
- 寄存器初始化
- 命令行参数和环境变量
- 文件描述符
3.4 创建进程控制块(PCB)
记录进程元数据,包括:
- 进程ID(PID)
- 进程状态
- 内存管理信息
- 打开的文件描述符
3.5 调度执行
进程被加入就绪队列,等待CPU调度
4. 进程状态
进程的三种基本状态:
- 运行(running):在处理器上运行,执行指令
- 就绪(ready):准备好运行,但操作系统选择不在此时运行
- 阻塞(blocked):执行了某种操作,直到发生其他事件时才会准备运行
从就绪到运行意味着该进程已经被调度(scheduled)。从运行转移到就绪意味着该进程已经取消调度(descheduled)。一旦进程被阻塞(例如,通过发起 I/O 操作),OS 将保持进程的这种状态,直到发生某种事件(例如,I/O 完成)。此时,进程再次转入就绪状态(也可能立即再次运行,如果操作系统这样决定)。
寄存器上下文将保存其寄存器的内容。当一个进程停止时,它的寄存器将被保存到这个内存位置。==通过恢复这些寄存器(将它们的值放回实际的物理寄存器中),==操作系统可以恢复运行该进程。我们将在后面的章节中更多地了解这种技术,它被称为上下文切换(contextswitch)。
5. 进程管理系统调用
5.1 fork系统调用
- 创建一个新进程,作为调用进程的副本
- 子进程复制父进程的地址空间、PCB等
- 父进程返回子进程PID,子进程返回0
#include <stdio.h> // 包含标准输入输出库,提供 printf() 等函数
#include <stdlib.h> // 包含标准库,提供 exit() 函数等
#include <unistd.h> // 包含 UNIX 标准函数声明,提供 fork()、getpid() 等int main(int argc, char *argv[]) // 程序入口,argc/argv 用于获取命令行参数{// 在创建子进程之前,先打印当前进程的 PID(进程标识符)printf("hello world (pid:%d)\n", (int)getpid());// 调用 fork(),创建一个新进程(子进程)int rc = fork();if (rc < 0) { // fork 返回值小于 0,表示创建子进程失败fprintf(stderr, "fork failed\n"); // 向标准错误输出错误信息exit(1); // 退出程序,并返回非零状态码表示异常} else if (rc == 0) { // fork 返回值等于 0,表示当前是子进程// 子进程执行的代码路径printf("hello, I am child (pid:%d)\n", (int)getpid());} else { // fork 返回值大于 0,表示当前是父进程// rc 存储的是子进程的 PIDprintf("hello, I am parent of %d (pid:%d)\n",rc, (int)getpid());}return 0; // 程序正常退出,返回值 0}
子进程并不是完全拷贝了父进程。具体来说,虽然它拥有自己的地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从fork()返回的值是不同的。父进程获得的返回值是新创建子进程的PID,而子进程获得的返回值是0。
系统显示父进程先执行,但是这是随机的,CPU调度程序(scheduler)决定了某个时刻哪个进程被执行
5.2 wait系统调用
- 父进程阻塞直到子进程结束
- 返回已结束子进程的PID
#include <stdio.h> // 标准输入输出,提供 printf()
#include <stdlib.h> // 标准库,提供 exit()
#include <unistd.h> // POSIX API,提供 fork()、getpid()
#include <sys/wait.h> // 等待子进程,提供 wait()int main(int argc, char *argv[])
{// 在 fork 之前,先打印当前进程(父进程)的 PIDprintf("hello world (pid:%d)\n", (int)getpid());// 创建一个新进程;父进程中 rc > 0,子进程中 rc == 0,失败时 rc < 0int rc = fork();if (rc < 0) {// fork 调用失败fprintf(stderr, "fork failed\n");exit(1);} else if (rc == 0) {// 子进程执行这里的代码printf("hello, I am child (pid:%d)\n", (int)getpid());} else {// 父进程执行这里的代码// wait(NULL) 阻塞直到任意子进程结束,返回值是已结束子进程的 PIDint wc = wait(NULL);printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",rc, wc, (int)getpid());}return 0; // 正常退出
}
5.3 exec系统调用
- 在当前进程中加载并执行新程序
- 替换当前地址空间
- 重置堆栈、堆和寄存器
#include <stdio.h> // 标准输入输出,提供 printf()
#include <stdlib.h> // 标准库,提供 exit()
#include <unistd.h> // POSIX API,提供 fork()、execvp()、getpid()
#include <string.h> // 字符串操作,提供 strdup()
#include <sys/wait.h> // 等待子进程,提供 wait()int main(int argc, char *argv[])
{// 程序启动时打印当前进程(父进程)的 PIDprintf("hello world (pid:%d)\n", (int)getpid());// 创建子进程:父进程 rc>0,子进程 rc==0,失败时 rc<0int rc = fork();if (rc < 0) {// fork 失败,打印错误并退出fprintf(stderr, "fork failed\n");exit(1);}else if (rc == 0) {// 子进程执行此路径printf("hello, I am child (pid:%d)\n", (int)getpid());// 准备 execvp 的参数数组// myargs[0] 指定要运行的程序名 "wc"// myargs[1] 指定要处理的文件 "p3.c"// myargs[2] 置 NULL,标记参数数组结束char *myargs[3];myargs[0] = strdup("wc"); myargs[1] = strdup("p3.c"); myargs[2] = NULL;// 用 execvp 替换当前子进程映像,执行 word count 程序execvp(myargs[0], myargs);// 如果 execvp 返回,说明执行失败,才会走到这里perror("execvp failed");exit(1);}else {// 父进程执行此路径// wait(NULL) 阻塞直到任意子进程结束,返回已结束子进程的 PIDint wc = wait(NULL);// 打印父进程信息,rc 是子进程的 PID,wc 是 wait 返回的 PIDprintf("hello, I am parent of %d (wc:%d) (pid:%d)\n",rc, wc, (int)getpid());}return 0; // 正常退出
}
要点说明
strdup()
:复制字符串并返回指向新内存的指针,用于给execvp
准备参数。execvp()
:用指定程序替换当前进程映像,不返回成功;如果失败,会返回 -1,此时应打印错误并退出。wait(NULL)
:父进程阻塞直到子进程结束,避免子进程成为僵尸。
6. 安全机制:地址空间布局随机化(ASLR)
6.1 ASLR的定义
ASLR是一种安全技术,通过随机化进程的内存地址布局,防止攻击者利用已知的内存地址执行恶意代码。
6.2 工作原理
随机化内存布局的关键区域:
- 堆栈(Stack)
- 堆(Heap)
- 可执行代码(Text Segment)
- 动态链接库(Shared Libraries)
6.3 优缺点
优点:
- 提升安全性
- 兼容性强
缺点:
- 非绝对防御
- 轻微性能开销
7. 内存分配:堆和栈
7.1 栈(Stack)
保存内容:
- 局部变量
- 函数参数
- 返回地址
- 栈帧
特点:
- 先进后出(LIFO)
- 速度快
- 大小有限
7.2 堆(Heap)
保存内容:
- 动态分配的对象
- 全局数据(部分情况)
特点:
- 手动管理
- 灵活性高
- 速度较慢
- 可能产生碎片
7.3 堆和栈的区别对比
特性 | 栈(Stack) | 堆(Heap) |
---|---|---|
分配方式 | 自动分配和释放 | 手动分配和释放 |
存储内容 | 局部变量、函数参数 | 动态分配的数据 |
生命周期 | 随函数调用结束而销毁 | 在手动释放前一直存在 |
大小限制 | 容量较小 | 容量较大 |
速度 | 操作更快 | 操作较慢 |