第4章 现代操作系统中的进程管理与生命周期实现
操作系统的核心功能之一是管理计算机系统上运行的程序。通过将程序转变为进程,操作系统可以更有效地控制和分配系统资源,实现多任务处理。本章将深入探讨进程的本质、状态变化以及控制机制,帮助读者全面理解现代操作系统中进程管理的理论与实践。
4.1 进程的基本概念
4.1.1 进程的历史发展
进程概念的出现解决了早期计算机系统中程序执行的重大问题。在单任务操作系统时代,计算机一次只能运行一个程序,资源利用率低下。随着多道程序设计的发展,操作系统需要一种机制来管理并发执行的多个程序,这促使了进程概念的诞生。
早期的计算机系统直接在CPU上运行程序,没有抽象层,导致程序之间相互干扰。进程抽象的引入使得每个程序都运行在自己的虚拟空间中,互不干扰,显著提高了系统的稳定性和安全性。
c
// 早期的简单程序执行方式(无进程概念)
void early_computer_execution() {
load_program_to_memory(); // 将程序加载到内存
set_program_counter(); // 设置程序计数器
// 直接执行,直到程序结束
while (!program_finished) {
execute_next_instruction();
}
// 加载下一个程序
load_next_program();
}
4.1.2 进程定义与进程控制块结构
进程是一个执行中的程序实例,包含程序代码(文本段)、当前活动(程序计数器、处理器寄存器)、堆栈(临时数据)、数据段(全局变量)以及堆(动态分配的内存)。
进程控制块(PCB)是操作系统用来表示进程的数据结构,包含了进程的所有信息。PCB通常包括以下信息:
- 进程标识符(Process ID)
- 进程状态(运行、就绪、阻塞等)
- 程序计数器(下一条将执行的指令地址)
- CPU寄存器
- CPU调度信息(优先级、调度队列指针等)
- 内存管理信息(内存边界、页表等)
- 资源占用信息(打开的文件等)
- 账户信息(CPU使用时间等)
c
// 进程控制块的C语言表示
typedef struct {
int pid; // 进程ID
ProcessState state; // 进程状态(枚举类型)
int program_counter; // 程序计数器
int cpu_registers[16]; // CPU寄存器集合
int priority; // 进程优先级
// 内存管理信息
void* text_segment; // 代码段
void* data_segment; // 数据段
void* stack; // 栈
void* heap; // 堆
// 资源占用信息
int open_files[MAX_FILES];// 打开的文件描述符
// 记账信息
time_t creation_time; // 创建时间
int cpu_time_used; // 已使用的CPU时间
// 调度信息
struct pcb* next; // 链表中的下一个PCB
} ProcessControlBlock;
通过PCB,操作系统能够追踪每个进程的状态和资源使用情况,实现对进程的有效管理和控制。当进程切换时,操作系统保存当前进程的状态到其PCB中,然后加载下一个进程的PCB来恢复其执行环境。
c
// 进程上下文切换
void context_switch(ProcessControlBlock* current, ProcessControlBlock* next) {
// 保存当前进程状态
save_cpu_state(current);
current->program_counter = get_current_pc();
// 加载下一个进程状态
load_cpu_state(next);
set_program_counter(next->program_counter);
// 更新进程状态
current->state = READY;
next->state = RUNNING;
printf("进程切换: 从PID %d 切换到 PID %d\n", current->pid, next->pid);
}
4.2 进程的状态转换
进程在其生命周期中会经历不同的状态,操作系统通过管理这些状态转换来协调多个进程的执行。
4.2.1 双状态进程模型分析
最简单的进程模型只包含两个状态:运行(Running)和非运行(Not Running)。在这个模型中,进程要么正在执行,要么在等待被调度执行。
这种简单模型的优点是易于理解和实现,但它不能精确描述进程的各种等待情况,如等待I/O完成或等待某个事件发生。
c
// 双状态模型中的进程调度
void simple_scheduler() {
ProcessControlBlock* current_process;
while (1) {
// 选择下一个要运行的进程
current_process = select_next_process();
if (current_process == NULL) {
// 没有可运行的进程,系统空闲
idle();
continue;
}
// 运行选中的进程
current_process->state = RUNNING;
run_process(current_process);
// 进程运行结束或时间片用尽
current_process->state = NOT_RUNNING;
// 将进程放回队列(如果未结束)
if (!process_terminated(current_process)) {
enqueue_process(current_process);
}
}
}
4.2.2 进程的创建与终止机制
进程创建
操作系统中的进程可以通过多种方式创建:
- 系统初始化时创建
- 用户请求创建新进程
- 现有进程执行创建进程的系统调用
- 批处理作业初始化
在UNIX/Linux系统中,使用fork()系统调用创建新进程。fork()创建调用进程的副本,两个进程从fork()返回点继续执行。在子进程中,fork()返回0;在父进程中,返回子进程ID。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
// 创建新进程
pid = fork();
if (pid < 0) {
// fork失败
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("子进程: 我的PID是 %d,父进程PID是 %d\n",
getpid(), getppid());
} else {
// 父进程代码
printf("父进程: 我的PID是 %d,子进程PID是 %d\n",
getpid(), pid);
}
return 0;
}
在Windows系统中,使用CreateProcess()函数创建新进程,该函数直接创建一个运行指定程序的新进程。
进程终止
进程可以通过以下方式终止:
- 正常退出(主动)
- 错误退出(主动)
- 严重错误(被动)
- 被其他进程终止(被动)
在UNIX/Linux系统中,进程通过exit()系统调用终止自己,或通过kill()系统调用终止其他进程。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int status;
pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("子进程(%d)开始执行\n", getpid());
sleep(2);
printf("子进程正常退出\n");
exit(0); // 正常终止
} else {
// 父进程代码
printf("父进程创建了子进程(%d)\n", pid);
// 等待子进程结束
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子进程正常终止,退出状态: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程被信号 %d 终止\n", WTERMSIG(status));
}
}
return 0;
}
4.2.3 五状态进程模型详解
更复杂且现实的进程模型包含五个状态:
- 新建(New): 进程正在被创建,尚未就绪
- 就绪(Ready): 进程已准备好运行,等待CPU分配
- 运行(Running): 进程正在执行
- 阻塞/等待(Blocked/Waiting): 进程等待某个事件发生
- 终止(Terminated): 进程已结束执行
进程状态转换图展示了这些状态之间的转换关系:
- 新建 → 就绪:进程创建完成后转为就绪状态
- 就绪 → 运行:调度器选择进程执行
- 运行 → 就绪:时间片用尽或更高优先级进程到达
- 运行 → 阻塞:进程等待I/O或其他事件
- 阻塞 → 就绪:等待的事件发生
- 运行 → 终止:进程执行完成或被终止
c
// 五状态模型的进程状态定义
typedef enum {
NEW,
READY,
RUNNING,
BLOCKED,
TERMINATED
} ProcessState;
// 进程状态转换函数
void change_process_state(ProcessControlBlock* process, ProcessState new_state) {
ProcessState old_state = process->state;
process->state = new_state;
printf("进程 %d 状态从 %s 变为 %s\n",
process->pid,
state_to_string(old_state),
state_to_string(new_state));
// 根据新状态执行相应操作
switch (new_state) {
case READY:
// 如果进程变为就绪态,将其加入就绪队列
if (old_state != RUNNING) { // 避免时间片到期的重复入队
add_to_ready_queue(process);
}
break;
case RUNNING:
// 进程开始运行,从就绪队列中移除
remove_from_ready_queue(process);
break;
case BLOCKED:
// 进程阻塞,可能需要加入等待队列
add_to_wait_queue(process);
break;
case TERMINATED:
// 进程终止,释放资源
release_resources(process);
break;
default:
break;
}
}
// 状态枚举转字符串
const char* state_to_string(ProcessState state) {
switch (state) {
case NEW: return "新建";
case READY: return "就绪";
case RUNNING: return "运行";
case BLOCKED: return "阻塞";
case TERMINATED: return "终止";
default: return "未知";
}
}
4.2.4 被挂起进程的处理
在虚拟内存系统中,进程可能会被挂起(Suspended),即暂时从内存中交换到磁盘上,以释放内存空间给其他进程使用。这引入了两个附加状态:
- 就绪挂起(Ready-Suspended): 进程在磁盘上,但一旦回到内存就可以运行
- 阻塞挂起(Blocked-Suspended): 进程在磁盘上且在等待某个事件
这样,进程状态模型扩展为七状态模型:新建、就绪、运行、阻塞、就绪挂起、阻塞挂起和终止。
c
// 七状态模型
typedef enum {
NEW,
READY,
RUNNING,
BLOCKED,
READY_SUSPENDED,
BLOCKED_SUSPENDED,
TERMINATED
} ExtendedProcessState;
// 挂起进程
void suspend_process(ProcessControlBlock* process) {
if (process->state == READY) {
// 就绪进程挂起
change_extended_state(process, READY_SUSPENDED);
// 将进程映像写入磁盘
swap_out_process(process);
printf("就绪进程 %d 被挂起到磁盘\n", process->pid);
}
else if (process->state == BLOCKED) {
// 阻塞进程挂起
change_extended_state(process, BLOCKED_SUSPENDED);
// 将进程映像写入磁盘
swap_out_process(process);
printf("阻塞进程 %d 被挂起到磁盘\n", process->pid);
}
}
// 恢复挂起进程
void resume_process(ProcessControlBlock* process) {
if (process->state == READY_SUSPENDED) {
// 分配内存
if (allocate_memory(process)) {
// 从磁盘加载进程映像
swap_in_process(process);
// 更新状态为就绪
change_extended_state(process, READY);
printf("挂起的就绪进程 %d 被恢复到内存\n", process->pid);
}
}
else if (process->state == BLOCKED_SUSPENDED) {
// 分配内存
if (allocate_memory(process)) {
// 从磁盘加载进程映像
swap_in_process(process);
// 更新状态为阻塞
change_extended_state(process, BLOCKED);
printf("挂起的阻塞进程 %d 被恢复到内存\n", process->pid);
}
}
}
进程挂起机制让操作系统可以管理更多进程,超出物理内存的限制,通过需要时将进程在内存和磁盘之间交换,实现更高效的资源利用。
4.3 进程描述器设计
操作系统需要维护关于进程的信息,以便有效地管理和控制进程。这些信息存储在进程描述器中,UNIX/Linux系统中称为进程控制块(PCB)。
4.3.1 操作系统控制结构设计
操作系统维护四种主要类型的表格来管理资源:
- 内存表: 跟踪内存分配情况
- I/O表: 管理I/O设备和通道的状态
- 文件表: 包含文件信息和文件系统结构
- 进程表: 管理所有进程的信息
这些表格通常相互关联,例如,进程表会包含指向内存表、I/O表和文件表的指针,表示进程占用的各种资源。
c
// 操作系统主要控制表格
typedef struct {
// 内存表
MemoryTableEntry memory_table[MAX_MEMORY_REGIONS];
int memory_table_size;
// I/O表
IODeviceEntry io_table[MAX_IO_DEVICES];
int io_table_size;
// 文件表
FileTableEntry file_table[MAX_FILES];
int file_table_size;
// 进程表
ProcessTableEntry process_table[MAX_PROCESSES];
int process_table_size;
// 系统状态信息
int current_running_pid;
SystemStats stats;
} OSControlTables;
// 内存表项
typedef struct {
void* start_address;
size_t size;
bool is_allocated;
int allocated_to_pid; // 分配给哪个进程
} MemoryTableEntry;
// I/O表项
typedef struct {
int device_id;
DeviceType type;
DeviceStatus status;
int using_pid; // 正在使用设备的进程
} IODeviceEntry;
// 文件表项
typedef struct {
char filename[MAX_FILENAME_LENGTH];
FileType type;
int size;
int open_count; // 有多少进程打开了此文件
int* opening_pids; // 打开此文件的进程列表
} FileTableEntry;
4.3.2 进程控制结构的实现
进程控制块是操作系统中表示进程的核心数据结构,包含所有必要的信息来管理进程。一个完整的PCB可能包括以下字段:
c
// 详细的进程控制块结构
typedef struct {
// 进程标识信息
int pid; // 进程ID
int parent_pid; // 父进程ID
int process_group_id; // 进程组ID
int session_id; // 会话ID
char process_name[50]; // 进程名称
// 处理器状态信息
ProcessState state; // 进程状态
int priority; // 优先级
int* program_counter; // 程序计数器
RegisterSet registers; // 寄存器集合
// 进程控制信息
time_t creation_time; // 创建时间
int cpu_time_used; // CPU使用时间
int time_slice; // 时间片大小
bool is_privileged; // 是否特权进程
// 内存管理信息
void* text_segment; // 代码段
size_t text_size; // 代码段大小
void* data_segment; // 数据段
size_t data_size; // 数据段大小
void* stack; // 栈
size_t stack_size; // 栈大小
void* heap; // 堆
size_t heap_size; // 堆大小
MemoryMap* memory_map; // 内存映射表
// 文件管理信息
int open_files[MAX_OPEN_FILES]; // 打开的文件描述符
int current_directory; // 当前工作目录
// 进程间通信信息
MessageQueue* message_queues; // 消息队列
SharedMemory* shared_memory; // 共享内存段
Semaphore* semaphores; // 信号量
// 进程关系
PidList* children; // 子进程列表
WaitQueue* waiting_children; // 等待中的子进程
// 调度信息
struct pcb* next; // 队列中的下一个PCB
} ProcessControlBlock;
// 寄存器集合
typedef struct {
int general_registers[16]; // 通用寄存器
int floating_point_registers[8]; // 浮点寄存器
int status_register; // 状态寄存器
int stack_pointer; // 栈指针
int base_pointer; // 基址指针
} RegisterSet;
现代操作系统中PCB的实现通常非常复杂,包含大量信息以支持进程管理的各种功能。Linux内核中的task_struct就是PCB的一个典型例子,它包含了进程的所有信息。
4.4 进程控制技术
进程控制是操作系统管理进程生命周期的核心功能,包括进程的创建、执行、调度和终止。
4.4.1 进程执行模式探讨
进程可以在用户模式和内核模式下执行:
- 用户模式: 受限的执行环境,无法直接访问硬件和执行特权指令
- 内核模式: 完全访问系统资源,可执行任何指令
当进程执行系统调用、处理中断或发生异常时,会从用户模式切换到内核模式。完成后,返回用户模式继续执行。
c
// 模拟模式切换
void system_call_handler(int syscall_number, void* params) {
// 保存用户模式上下文
save_user_context();
// 切换到内核模式
switch_to_kernel_mode();
printf("进入内核模式,处理系统调用 %d\n", syscall_number);
// 根据系统调用号执行相应的内核函数
int result = execute_system_call(syscall_number, params);
// 切换回用户模式
switch_to_user_mode();
// 恢复用户上下文
restore_user_context();
printf("返回用户模式,系统调用结果: %d\n", result);
}
// 模拟用户程序调用系统函数
void user_program() {
printf("用户程序执行中...\n");
// 调用系统函数(例如打开文件)
int fd = open_file("example.txt", O_RDONLY);
printf("文件打开结果:文件描述符 %d\n", fd);
// 继续执行...
}
// 系统调用封装函数
int open_file(const char* filename, int flags) {
// 准备系统调用参数
OpenFileParams params;
params.filename = filename;
params.flags = flags;
// 触发系统调用(实际实现会使用特殊指令如int 0x80或syscall)
return syscall(SYS_OPEN, ¶ms);
}
4.4.2 进程创建流程
进程创建是操作系统的基本功能,通常包括以下步骤:
- 分配唯一的进程ID
- 分配空间给进程控制块
- 初始化进程控制块
- 设置适当的链接(如父子关系)
- 创建或扩展其他数据结构
- 分配进程需要的资源
以UNIX/Linux系统的fork()为例:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// 模拟操作系统内部的fork实现
pid_t os_fork() {
// 1. 分配新的PID
pid_t new_pid = allocate_pid();
// 2. 创建新的进程控制块
ProcessControlBlock* child_pcb = create_pcb(new_pid);
if (!child_pcb) {
return -1; // 失败
}
// 3. 获取当前(父)进程的PCB
ProcessControlBlock* parent_pcb = get_current_pcb();
// 4. 复制父进程的PCB内容到子进程
copy_pcb_contents(parent_pcb, child_pcb);
// 5. 设置父子关系
child_pcb->parent_pid = parent_pcb->pid;
add_child_to_parent(parent_pcb, new_pid);
// 6. 复制父进程的内存空间
if (!copy_address_space(parent_pcb, child_pcb)) {
free_pcb(child_pcb);
return -1; // 失败
}
// 7. 复制打开的文件描述符
copy_file_descriptors(parent_pcb, child_pcb);
// 8. 将子进程设置为就绪状态
child_pcb->state = READY;
add_to_ready_queue(child_pcb);
// 9. 在父进程中返回子进程ID,在子进程中返回0
return new_pid;
}
int main() {
printf("父进程开始执行 (PID: %d)\n", getpid());
pid_t pid = fork();
if (pid < 0) {
// fork失败
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程执行中 (PID: %d, 父PID: %d)\n", getpid(), getppid());
// 子进程特有的操作
for (int i = 1; i <= 3; i++) {
printf("子进程: 计数 %d\n", i);
sleep(1);
}
printf("子进程退出\n");
exit(0);
} else {
// 父进程
printf("父进程继续执行,创建的子进程PID: %d\n", pid);
// 父进程特有的操作
for (int i = 1; i <= 3; i++) {
printf("父进程: 计数 %d\n", i);
sleep(2);
}
// 等待子进程结束
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子进程正常退出,状态码: %d\n", WEXITSTATUS(status));
} else {
printf("子进程异常退出\n");
}
printf("父进程退出\n");
}
return 0;
}
4.4.3 进程切换实现方法
进程切换(也称为上下文切换)是操作系统中最关键的操作之一,它涉及:
- 保存当前进程的上下文
- 选择下一个要运行的进程
- 恢复被选中进程的上下文
上下文切换的效率对系统性能有重大影响,因为它是一个纯开销操作,不执行有用的工作。
c
// 上下文切换的详细实现
void context_switch(ProcessControlBlock* current, ProcessControlBlock* next) {
// 1. 保存当前进程上下文
save_cpu_registers(current->registers);
current->program_counter = get_program_counter();
// 2. 更新进程状态
if (current->state == RUNNING) {
current->state = READY;
}
// 3. 更新进程控制块队列
if (current->state == READY) {
add_to_ready_queue(current);
} else if (current->state == BLOCKED) {
// 已经在合适的等待队列中,不需要额外操作
}
// 4. 选择新进程(在这个函数中假设已经选好了next)
next->state = RUNNING;
// 5. 更新内存管理单元(如页表)
update_memory_mappings(next);
// 6. 恢复新进程的上下文
restore_cpu_registers(next->registers);
set_program_counter(next->program_counter);
// 7. 切换堆栈
switch_to_process_stack(next);
// 8. 更新系统的当前进程指针
current_running_process = next;
// 9. 记录上下文切换次数和时间戳
system_stats.context_switches++;
next->last_scheduled = get_current_time();
printf("上下文切换: 从进程 %d 切换到进程 %d\n",
current->pid, next->pid);
}
// 进程调度器
void schedule() {
// 获取当前正在运行的进程
ProcessControlBlock* current = current_running_process;
// 选择下一个要运行的进程
ProcessControlBlock* next = select_next_process();
if (next != NULL && next != current) {
// 执行上下文切换
context_switch(current, next);
}
}
// 选择下一个要运行的进程(简单的轮转调度算法)
ProcessControlBlock* select_next_process() {
if (ready_queue_empty()) {
return NULL; // 没有可运行的进程
}
// 从队列头取出下一个进程
return remove_from_ready_queue_head();
}
上下文切换是一个复杂的操作,通常涉及特权指令和硬件支持。在实际的操作系统中,上下文切换的具体实现依赖于处理器架构。
4.5 操作系统执行环境
操作系统是特殊的程序,它既管理其他程序的执行,又自己执行。这种双重角色导致了不同的执行模型。
4.5.1 无进程内核设计
在最简单的操作系统设计中,内核可以不作为进程运行,而是直接在裸机上执行。这种设计通常用于嵌入式系统和实时操作系统。
无进程内核的特点:
- 内核直接访问所有硬件资源
- 没有内核级的上下文切换
- 通常使用单地址空间
- 系统调用通过软中断或特殊指令实现
c
// 无进程内核示例(简化)
void simple_kernel() {
// 初始化硬件
init_hardware();
// 设置中断向量
setup_interrupt_vectors();
// 初始化系统服务
init_system_services();
// 创建初始用户进程
create_initial_process();
// 开始调度
while (1) {
// 选择并运行下一个用户进程
ProcessControlBlock* next = select_next_process();
if (next != NULL) {
run_process(next);
} else {
// 空闲时执行
cpu_idle();
}
}
}
// 中断处理
void interrupt_handler(int interrupt_number) {
// 保存用户进程状态
save_process_state();
// 处理中断
switch (interrupt_number) {
case TIMER_INTERRUPT:
handle_timer_interrupt();
break;
case SYSCALL_INTERRUPT:
handle_system_call();
break;
// 其他中断...
}
// 可能的进程切换
schedule();
// 恢复进程状态并返回
restore_process_state();
}
4.5.2 用户进程中的内核执行
在更复杂的操作系统中,内核可以在用户进程的上下文中执行,称为进程内核(Process Kernel)。当用户进程发起系统调用时,处理器模式从用户模式切换到内核模式,但仍在同一进程上下文中执行。
这种设计的特点:
- 系统调用在调用进程的上下文中执行
- 内核代码与用户代码共享同一进程
- 进程地址空间分为用户空间和内核空间
c
// 用户进程内核执行模式
void process_based_kernel() {
// 初始化系统
init_system();
// 创建初始进程
create_init_process();
// 将控制权交给调度器
schedule();
// 永不返回
}
// 系统调用处理(在进程上下文中)
void handle_system_call(int syscall_number, void* params) {
// 当前进程已经在系统调用中切换到了内核模式
// 获取当前进程
ProcessControlBlock* current = current_running_process;
printf("进程 %d 的系统调用 %d 正在执行\n",
current->pid, syscall_number);
// 执行相应的系统调用
int result;
switch (syscall_number) {
case SYS_READ:
result = sys_read(params);
break;
case SYS_WRITE:
result = sys_write(params);
break;
case SYS_FORK:
result = sys_fork();
break;
// 其他系统调用...
default:
result = -ENOSYS; // 未实现的系统调用
}
// 保存返回值供进程使用
current->syscall_result = result;
// 返回用户模式(通过特殊指令)
return_to_user_mode();
}
4.5.3 基于进程的操作系统
最复杂的设计是基于进程的操作系统,其中内核本身作为一个或多个特殊进程运行。这种设计更加模块化,使内核组件可以并行执行。
特点:
- 内核分为多个进程
- 内核进程可以并行执行
- 更好的模块性和可扩展性
- 更复杂的同步需求
c
// 基于进程的操作系统
void process_based_os() {
// 创建内核进程
create_kernel_process(memory_manager_process);
create_kernel_process(device_manager_process);
create_kernel_process(file_system_process);
create_kernel_process(network_manager_process);
// 创建初始用户进程
create_user_process(init_process);
// 启动调度器
scheduler_process();
}
// 内存管理器进程
void memory_manager_process() {
while (1) {
// 等待内存管理请求
MemoryRequest request = wait_for_memory_request();
// 处理请求
switch (request.type) {
case ALLOC_MEM:
handle_memory_allocation(request);
break;
case FREE_MEM:
handle_memory_free(request);
break;
case PAGE_FAULT:
handle_page_fault(request);
break;
// 其他内存管理操作...
}
}
}
// 文件系统进程
void file_system_process() {
// 初始化文件系统
init_file_system();
while (1) {
// 等待文件系统请求
FSRequest request = wait_for_fs_request();
// 处理请求
switch (request.type) {
case OPEN_FILE:
handle_file_open(request);
break;
case READ_FILE:
handle_file_read(request);
break;
case WRITE_FILE:
handle_file_write(request);
break;
// 其他文件操作...
}
}
}
4.6 进程安全与保护
随着计算机系统的复杂性增加,安全问题变得越来越重要。操作系统必须保护进程免受其他进程的干扰,同时保护自身免受恶意程序的攻击。
4.6.1 系统访问控制机制
操作系统通过多种机制控制进程对系统资源的访问:
- 处理器模式:用户模式和内核模式分离
- 内存保护:进程隔离,防止互相访问对方的内存
- 权限控制:基于用户ID和组ID的资源访问控制
- 能力列表:明确指定进程可以执行的操作
c
// 访问控制检查示例
bool check_access_permission(ProcessControlBlock* process,
ResourceType resource_type,
int resource_id,
AccessMode mode) {
// 检查进程是否有权限访问特定资源
// 超级用户(root)拥有所有权限
if (process->effective_user_id == 0) {
return true;
}
// 根据资源类型进行不同的权限检查
switch (resource_type) {
case RESOURCE_FILE:
return check_file_permission(process, resource_id, mode);
case RESOURCE_DEVICE:
return check_device_permission(process, resource_id, mode);
case RESOURCE_IPC:
return check_ipc_permission(process, resource_id, mode);
case RESOURCE_PROCESS:
return check_process_permission(process, resource_id, mode);
default:
// 未知资源类型,拒绝访问
return false;
}
}
// 文件权限检查
bool check_file_permission(ProcessControlBlock* process,
int file_id,
AccessMode mode) {
// 获取文件信息
FileInfo* file = get_file_info(file_id);
if (!file) {
return false; // 文件不存在
}
// 检查用户ID和组ID
if (file->owner_id == process->effective_user_id) {
// 文件所有者
if ((mode == ACCESS_READ && (file->permissions & OWNER_READ)) ||
(mode == ACCESS_WRITE && (file->permissions & OWNER_WRITE)) ||
(mode == ACCESS_EXECUTE && (file->permissions & OWNER_EXEC))) {
return true;
}
} else if (is_user_in_group(process->effective_group_id, file->group_id)) {
// 文件组成员
if ((mode == ACCESS_READ && (file->permissions & GROUP_READ)) ||
(mode == ACCESS_WRITE && (file->permissions & GROUP_WRITE)) ||
(mode == ACCESS_EXECUTE && (file->permissions & GROUP_EXEC))) {
return true;
}
} else {
// 其他用户
if ((mode == ACCESS_READ && (file->permissions & OTHER_READ)) ||
(mode == ACCESS_WRITE && (file->permissions & OTHER_WRITE)) ||
(mode == ACCESS_EXECUTE && (file->permissions & OTHER_EXEC))) {
return true;
}
}
// 没有匹配的权限
return false;
}
4.6.2 安全防护措施实现
为防止恶意程序对系统的攻击,现代操作系统实现了多种安全防护措施:
- 地址空间随机化(ASLR):随机化进程内存布局,使攻击者难以预测目标地址
- 不可执行内存:标记数据段为不可执行,防止代码注入攻击
- 栈保护:在函数栈帧中添加哨兵值,检测栈溢出
- 安全启动:验证操作系统加载的完整性
- 沙箱:限制应用程序的权限和资源访问
c
// 地址空间随机化实现示例
void* allocate_randomized_memory(ProcessControlBlock* process,
size_t size,
MemoryType type) {
// 获取合法的地址范围
void* min_addr = get_min_address(type);
void* max_addr = get_max_address(type);
// 计算可用范围
size_t range = (char*)max_addr - (char*)min_addr - size;
// 生成随机偏移量(页对齐)
size_t page_size = get_page_size();
size_t random_offset = (generate_random() % (range / page_size)) * page_size;
// 计算最终地址
void* address = (void*)((char*)min_addr + random_offset);
// 分配内存
if (allocate_memory_at(process, address, size, type)) {
printf("为进程 %d 分配随机地址空间: %p (大小: %zu)\n",
process->pid, address, size);
return address;
}
// 分配失败,尝试普通分配
return allocate_memory(process, size, type);
}
// 栈保护实现示例
void setup_stack_protection(ProcessControlBlock* process) {
// 获取栈底地址
void* stack_bottom = process->stack_base;
// 创建保护页
void* guard_page = (void*)((char*)stack_bottom - get_page_size());
// 将保护页标记为不可访问
set_page_protection(guard_page, PROT_NONE);
// 在栈底放置canary值
uint32_t* canary_location = (uint32_t*)stack_bottom;
*canary_location = generate_canary_value();
process->stack_canary = *canary_location;
printf("为进程 %d 设置栈保护: 保护页=%p, canary=%08X\n",
process->pid, guard_page, process->stack_canary);
}
// 检查栈完整性
bool check_stack_integrity(ProcessControlBlock* process) {
// 获取栈底的canary值
uint32_t* canary_location = (uint32_t*)process->stack_base;
uint32_t current_canary = *canary_location;
// 检查canary值是否被修改
if (current_canary != process->stack_canary) {
printf("警告: 进程 %d 的栈已被破坏! 预期canary=%08X, 实际=%08X\n",
process->pid, process->stack_canary, current_canary);
return false;
}
return true;
}
4.7 UNIX SVR4进程管理实例
UNIX System V Release 4(SVR4)是一个重要的UNIX版本,它的进程管理系统结合了AT&T System V和BSD UNIX的特性,成为后来许多UNIX变种和Linux的基础。
4.7.1 SVR4进程状态模型
SVR4使用了一个扩展的进程状态模型,包括:
- 创建(Created):进程正在被创建
- 就绪(Ready):准备运行,等待CPU
- 运行(Running):正在执行
- 睡眠(Sleeping):等待事件,分为可中断睡眠和不可中断睡眠
- 停止(Stopped):被暂停,通常由信号导致
- 僵尸(Zombie):已终止但父进程尚未回收其状态
- 交换出(Swapped out):被暂时移出内存
c
// SVR4进程状态枚举
typedef enum {
SIDL, // 进程创建中
SRUN, // 运行中或就绪
SSLEEP, // 可中断睡眠
SSTOP, // 暂停状态
SZOMB, // 僵尸状态
SWAIT, // 不可中断等待
SLOCK // 等待内存等系统资源
} SVR4ProcessState;
// 进程状态转换函数
void svr4_change_process_state(ProcessControlBlock* process,
SVR4ProcessState new_state,
const char* reason) {
SVR4ProcessState old_state = process->svr4_state;
process->svr4_state = new_state;
printf("进程 %d (%s) 状态从 %s 变为 %s, 原因: %s\n",
process->pid, process->process_name,
svr4_state_to_string(old_state),
svr4_state_to_string(new_state),
reason);
// 根据状态转换执行相应操作
switch (new_state) {
case SRUN:
// 添加到调度队列
if (old_state != SRUN) {
add_to_run_queue(process);
}
break;
case SSLEEP:
case SWAIT:
// 从运行队列移除
if (old_state == SRUN) {
remove_from_run_queue(process);
}
// 添加到等待队列
add_to_sleep_queue(process);
break;
case SSTOP:
// 从运行队列移除
if (old_state == SRUN) {
remove_from_run_queue(process);
}
break;
case SZOMB:
// 进程已终止,释放大部分资源但保留PCB
release_process_resources(process);
// 通知父进程
notify_parent_process(process);
break;
default:
break;
}
}
// 状态转字符串
const char* svr4_state_to_string(SVR4ProcessState state) {
switch (state) {
case SIDL: return "创建中(SIDL)";
case SRUN: return "运行/就绪(SRUN)";
case SSLEEP: return "可中断睡眠(SSLEEP)";
case SSTOP: return "已停止(SSTOP)";
case SZOMB: return "僵尸(SZOMB)";
case SWAIT: return "不可中断等待(SWAIT)";
case SLOCK: return "锁定(SLOCK)";
default: return "未知状态";
}
}
4.7.2 SVR4进程描述符
SVR4中的进程描述符包括多个数据结构:
- proc结构:每个进程的主要信息
- user结构(u区):进程的用户空间信息
- kthread结构:轻量级进程/内核线程信息
这些结构共同描述了一个进程的完整状态。
c
// SVR4 proc结构简化版
typedef struct proc {
unsigned int p_flag; // 进程标志
char p_stat; // 状态
char p_pri; // 优先级
char p_cpu; // CPU使用量
char p_nice; // nice值
unsigned int p_pid; // 进程ID
unsigned int p_ppid; // 父进程ID
unsigned int p_pgrp; // 进程组ID
unsigned int p_sid; // 会话ID
caddr_t p_addr; // 用户区地址
size_t p_size; // 进程大小
struct proc *p_link; // 链表中的下一项
struct proc *p_parent; // 父进程指针
struct proc *p_child; // 子进程链表
struct proc *p_sibling; // 兄弟进程链表
time_t p_utime; // 用户模式CPU时间
time_t p_stime; // 系统模式CPU时间
caddr_t p_wchan; // 等待通道
struct kthread *p_tlist; // 线程列表
// 其他字段...
} proc_t;
// SVR4 user结构简化版
typedef struct user {
struct proc *u_procp; // 指向proc结构
// 进程限制
unsigned int u_rlimit[RLIM_NLIMITS][2];
// 打开文件表
struct file *u_ofile[NOFILE];
char u_pofile[NOFILE];
// 当前目录和根目录
struct vnode *u_cdir;
struct vnode *u_rdir;
// 信号处理
void (*u_signal[NSIG])();
// 命令名称
char u_comm[MAXCOMLEN + 1];
// 其他字段...
} user_t;
// SVR4 kthread结构简化版
typedef struct kthread {
struct proc *t_procp; // 指向proc结构
unsigned int t_flag; // 线程标志
char t_state; // 线程状态
char t_pri; // 线程优先级
caddr_t t_wchan; // 等待通道
short t_stkbase; // 栈基址
struct kthread *t_link; // 下一个线程
// 线程寄存器
int t_regs[NREG];
// 其他字段...
} kthread_t;
4.7.3 SVR4进程控制实现
SVR4中进程控制主要通过以下系统调用实现:
- fork(): 创建新进程
- exec(): 执行新程序
- exit(): 终止进程
- wait(): 等待子进程终止
- kill(): 向进程发送信号
SVR4还实现了作业控制功能,允许用户暂停、恢复和在前后台运行进程。
c
// SVR4 fork实现(简化)
int svr4_fork() {
// 1. 分配并初始化新的proc结构
proc_t* child_proc = allocate_proc();
if (!child_proc) {
return -EAGAIN;
}
// 2. 获取当前进程
proc_t* parent_proc = u.u_procp;
// 3. 复制proc结构
*child_proc = *parent_proc; // 浅复制
// 4. 修改子进程proc中的特定字段
child_proc->p_pid = get_next_pid();
child_proc->p_ppid = parent_proc->p_pid;
child_proc->p_stat = SRUN;
child_proc->p_flag = 0;
child_proc->p_pri = parent_proc->p_pri;
// 5. 设置父子关系
link_child_to_parent(child_proc, parent_proc);
// 6. 分配并复制user区
if (copy_user_area(parent_proc, child_proc) != 0) {
free_proc(child_proc);
return -EAGAIN;
}
// 7. 复制地址空间
if (copy_address_space(parent_proc, child_proc) != 0) {
free_user_area(child_proc);
free_proc(child_proc);
return -EAGAIN;
}
// 8. 复制打开的文件描述符
copy_open_files(parent_proc, child_proc);
// 9. 创建内核线程结构
kthread_t* child_thread = allocate_kthread(child_proc);
if (!child_thread) {
free_address_space(child_proc);
free_user_area(child_proc);
free_proc(child_proc);
return -EAGAIN;
}
// 10. 初始化内核线程
init_kthread(child_thread, child_proc);
// 11. 将子进程添加到系统进程表
add_to_process_table(child_proc);
// 12. 将子进程添加到调度队列
add_to_run_queue(child_proc);
// 13. 在父进程中返回子进程ID
return child_proc->p_pid;
}
// SVR4 exec实现(简化)
int svr4_exec(const char* path, char* const argv[], char* const envp[]) {
// 1. 获取当前进程
proc_t* proc = u.u_procp;
// 2. 打开可执行文件
int fd = open(path, O_RDONLY);
if (fd < 0) {
return -ENOENT;
}
// 3. 检查文件格式,确保是可执行文件
if (!is_executable(fd)) {
close(fd);
return -ENOEXEC;
}
// 4. 保存需要的信息
pid_t pid = proc->p_pid;
pid_t ppid = proc->p_ppid;
uid_t uid = proc->p_uid;
gid_t gid = proc->p_gid;
// 5. 释放当前地址空间
free_address_space(proc);
// 6. 创建新地址空间
create_new_address_space(proc);
// 7. 加载可执行文件到新地址空间
if (load_executable(fd, proc) != 0) {
// 恢复失败,进程无法继续
close(fd);
exit(-1);
}
close(fd);
// 8. 设置参数和环境变量
setup_arguments(proc, argv, envp);
// 9. 重置进程状态但保留某些属性
proc->p_pid = pid;
proc->p_ppid = ppid;
proc->p_uid = uid;
proc->p_gid = gid;
// 10. 重置信号处理
reset_signal_handlers(proc);
// 11. 更新进程命令名
update_proc_name(proc, path);
// 12. 设置程序入口点
set_entry_point(proc);
return 0;
}
4.8 小结
本章详细探讨了进程的概念、状态转换、描述方式和控制机制。进程是现代操作系统的核心抽象,通过它,操作系统可以有效管理系统资源和控制程序执行。
我们了解了进程的不同状态模型,从简单的双状态模型到复杂的七状态模型,以及进程控制块(PCB)的结构和作用。同时,我们也探讨了进程创建、切换和终止的实现机制,以及操作系统如何保护进程安全。
通过UNIX SVR4的案例研究,我们看到了一个成熟操作系统中进程管理的实际实现。这些概念和技术构成了现代操作系统设计的基础,影响了包括Linux、Windows和macOS在内的所有主流操作系统。
4.9 推荐阅读资源
为深入理解进程管理,建议阅读以下资源:
- "操作系统概念"(Operating System Concepts) - Silberschatz, Galvin & Gagne
- "现代操作系统"(Modern Operating Systems) - Andrew S. Tanenbaum
- "UNIX内部实现"(The Design of the UNIX Operating System) - Maurice J. Bach
- "Linux内核开发"(Linux Kernel Development) - Robert Love
- "深入理解Linux内核"(Understanding the Linux Kernel) - Daniel P. Bovet & Marco Cesati
4.10 术语、复习题与实践
关键术语
- 进程(Process): 执行中的程序实例
- 进程控制块(PCB): 表示进程的数据结构,包含所有进程信息
- 上下文切换(Context Switch): 保存一个进程状态并恢复另一个进程状态的操作
- 调度(Scheduling): 决定哪个进程获得CPU执行时间的过程
- 进程状态(Process State): 进程在生命周期中所处的状态
- fork(): UNIX/Linux中创建新进程的系统调用
- exec(): 替换当前进程映像的系统调用
- 僵尸进程(Zombie Process): 已终止但父进程尚未获取其状态的进程
- 内核模式(Kernel Mode): CPU的特权执行模式,可访问所有系统资源
- 用户模式(User Mode): CPU的受限执行模式,防止直接访问关键资源
复习题
- 解释进程和程序的区别。
- 描述进程控制块(PCB)的主要组成部分及其作用。
- 画出五状态进程模型的状态转换图,并解释每种转换的触发条件。
- 比较UNIX/Linux中fork()和exec()系统调用的功能和用途。
- 说明上下文切换的步骤及其对系统性能的影响。
- 解释进程挂起的概念及其在虚拟内存系统中的作用。
- 描述UNIX SVR4中的进程描述结构及其组织方式。
- 分析操作系统如何防止恶意进程访问其他进程的内存空间。
编程练习
练习1: 实现一个简单的shell程序
编写一个简单的命令行shell程序,支持以下功能:
- 显示命令提示符
- 接受用户输入的命令
- 使用fork()和exec()执行命令
- 支持后台执行(命令后加&)
- 支持简单的内置命令(如cd, exit)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_CMD_LENGTH 1024
#define MAX_ARGS 64
// 解析命令行
int parse_command(char *cmd, char *args[]) {
int i = 0;
// 按空格分割命令行
args[i] = strtok(cmd, " \t\n");
while (args[i] != NULL && i < MAX_ARGS - 1) {
i++;
args[i] = strtok(NULL, " \t\n");
}
// 检查是否后台运行
int background = 0;
if (i > 0 && strcmp(args[i-1], "&") == 0) {
args[i-1] = NULL; // 移除&
background = 1;
}
return background;
}
// 执行内置命令
int execute_builtin(char *args[]) {
if (args[0] == NULL) {
return 1; // 空命令
}
// cd命令
if (strcmp(args[0], "cd") == 0) {
if (args[1] == NULL) {
// 没有参数,切换到主目录
chdir(getenv("HOME"));
} else {
if (chdir(args[1]) != 0) {
perror("cd");
}
}
return 1;
}
// exit命令
if (strcmp(args[0], "exit") == 0) {
exit(0);
}
return 0; // 不是内置命令
}
int main() {
char cmd[MAX_CMD_LENGTH];
char *args[MAX_ARGS];
while (1) {
// 显示提示符
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s$ ", cwd);
} else {
printf("myshell$ ");
}
fflush(stdout);
// 读取命令
if (fgets(cmd, MAX_CMD_LENGTH, stdin) == NULL) {
break; // EOF
}
// 解析命令
int background = parse_command(cmd, args);
if (args[0] == NULL) {
continue; // 空命令
}
// 尝试执行内置命令
if (execute_builtin(args)) {
continue;
}
// 创建子进程执行外部命令
pid_t pid = fork();
if (pid < 0) {
perror("fork");
continue;
} else if (pid == 0) {
// 子进程
execvp(args[0], args);
// 如果exec返回,说明出错了
perror(args[0]);
exit(1);
} else {
// 父进程
if (!background) {
// 前台执行,等待子进程结束
waitpid(pid, NULL, 0);
} else {
printf("[1] %d\n", pid); // 打印后台进程PID
}
}
}
return 0;
}
练习2: 进程状态监控程序
编写一个程序,使用/proc文件系统(在Linux中)读取和显示系统中所有进程的状态信息。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/types.h>
// 进程信息结构
typedef struct {
int pid;
char state;
char name[256];
int ppid;
unsigned long vsize; // 虚拟内存大小
long priority;
long nice;
} ProcessInfo;
// 读取进程信息
int read_process_info(int pid, ProcessInfo *info) {
char path[256];
char buffer[4096];
FILE *fp;
// 读取/proc/PID/stat文件
snprintf(path, sizeof(path), "/proc/%d/stat", pid);
fp = fopen(path, "r");
if (fp == NULL) {
return -1; // 进程可能已经结束
}
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
fclose(fp);
return -1;
}
fclose(fp);
// 解析stat文件内容
char state;
char comm[256];
int ppid;
unsigned long vsize;
long priority, nice;
sscanf(buffer, "%d (%[^)]) %c %d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %ld %ld %*d %*d %*d %*d %*u %lu",
&info->pid, comm, &state, &ppid, &priority, &nice, &vsize);
info->state = state;
strncpy(info->name, comm, sizeof(info->name));
info->ppid = ppid;
info->vsize = vsize;
info->priority = priority;
info->nice = nice;
return 0;
}
// 将进程状态字符转换为可读描述
const char* state_to_string(char state) {
switch (state) {
case 'R': return "运行中";
case 'S': return "可中断睡眠";
case 'D': return "不可中断睡眠";
case 'Z': return "僵尸";
case 'T': return "已停止";
case 't': return "跟踪停止";
case 'X': return "已死亡";
default: return "未知";
}
}
int main() {
DIR *dir;
struct dirent *entry;
ProcessInfo info;
printf("%-6s %-20s %-15s %-6s %-10s %-10s %-6s\n",
"PID", "名称", "状态", "PPID", "优先级", "NICE值", "内存(KB)");
printf("------------------------------------------------------------\n");
// 打开/proc目录
dir = opendir("/proc");
if (dir == NULL) {
perror("无法打开/proc目录");
return 1;
}
// 遍历/proc目录中的所有PID目录
while ((entry = readdir(dir)) != NULL) {
// 检查是否为数字目录名(PID)
int pid = atoi(entry->d_name);
if (pid > 0) {
// 读取进程信息
if (read_process_info(pid, &info) == 0) {
// 打印进程信息
printf("%-6d %-20s %-15s %-6d %-10ld %-10ld %-6lu\n",
info.pid, info.name, state_to_string(info.state),
info.ppid, info.priority, info.nice,
info.vsize / 1024);
}
}
}
closedir(dir);
return 0;
}
这些练习帮助理解进程创建、控制和监控的实际应用,巩固本章所学的理论知识。

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



