引言
操作系统是计算机科学的核心课程之一,它连接了计算机硬件与上层应用软件。哈尔滨理工大学(哈理工)的操作系统实验课程旨在帮助学生将抽象的理论知识转化为具体的编程实践,深入理解操作系统的核心机制。本指南将系统性地介绍哈理工操作系统实验的完整流程,从理论基础到实践操作,并解析常见问题,帮助学生高效完成实验任务。
第一部分:理论基础回顾
1.1 操作系统核心概念
在开始实验前,必须掌握以下核心概念:
- 进程管理:进程的创建、调度、同步与通信
- 内存管理:分页、分段、虚拟内存
- 文件系统:文件的组织、存储与访问
- 设备管理:I/O调度与设备驱动
- 并发控制:信号量、互斥锁、死锁处理
1.2 实验环境准备
哈理工操作系统实验通常使用以下环境:
- 操作系统:Linux(推荐Ubuntu或CentOS)
- 编程语言:C语言(主要)、C++、Python(辅助)
- 开发工具:GCC编译器、GDB调试器、Make工具
- 虚拟机:VirtualBox或VMware(用于安全实验)
1.3 实验教材与参考资料
- 《操作系统概念》(恐龙书)
- 《现代操作系统》
- 哈理工自编实验指导书
- Linux内核源码(可选,用于深入研究)
第二部分:核心实验项目详解
2.1 实验一:进程创建与管理
理论要点
- 进程控制块(PCB)结构
- fork()、exec()系统调用
- 进程状态转换
实践代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
printf("父进程开始,PID: %d\n", getpid());
pid = fork(); // 创建子进程
if (pid < 0) {
// 创建失败
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("子进程开始,PID: %d, 父进程PID: %d\n", getpid(), getppid());
sleep(2); // 模拟子进程工作
printf("子进程结束\n");
exit(0);
} else {
// 父进程代码
printf("父进程继续,子进程PID: %d\n", pid);
int status;
wait(&status); // 等待子进程结束
printf("父进程收到子进程结束信号,状态: %d\n", status);
printf("父进程结束\n");
}
return 0;
}
实验步骤
- 编写上述代码并保存为
process_demo.c - 编译:
gcc -o process_demo process_demo.c - 运行:
./process_demo - 使用
ps aux | grep process_demo观察进程状态
常见问题解析
问题1:fork()返回值混淆
- 现象:无法区分父子进程
- 解决:严格检查返回值,
pid == 0为子进程,pid > 0为父进程,pid < 0为错误
问题2:僵尸进程
- 现象:子进程结束但父进程未调用wait()
- 解决:父进程必须调用wait()或waitpid()回收子进程资源
2.2 实验二:进程同步与互斥
理论要点
- 临界区问题
- 信号量机制(PV操作)
- 经典同步问题(生产者-消费者、读者-写者)
实践代码示例:生产者-消费者问题
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
sem_t mutex; // 互斥信号量
sem_t empty; // 空缓冲区数量
sem_t full; // 满缓冲区数量
void* producer(void* arg) {
int item;
for (int i = 0; i < 10; i++) {
item = rand() % 100; // 生成随机数
sem_wait(&empty); // 等待空缓冲区
sem_wait(&mutex); // 进入临界区
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
printf("生产者生产了: %d\n", item);
sem_post(&mutex); // 离开临界区
sem_post(&full); // 增加满缓冲区数量
sleep(1); // 模拟生产时间
}
return NULL;
}
void* consumer(void* arg) {
int item;
for (int i = 0; i < 10; i++) {
sem_wait(&full); // 等待满缓冲区
sem_wait(&mutex); // 进入临界区
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
printf("消费者消费了: %d\n", item);
sem_post(&mutex); // 离开临界区
sem_post(&empty); // 增加空缓冲区数量
sleep(2); // 模拟消费时间
}
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
// 初始化信号量
sem_init(&mutex, 0, 1); // 互斥信号量初始为1
sem_init(&empty, 0, BUFFER_SIZE); // 空缓冲区初始为缓冲区大小
sem_init(&full, 0, 0); // 满缓冲区初始为0
// 创建线程
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
// 等待线程结束
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
// 销毁信号量
sem_destroy(&mutex);
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}
编译与运行
gcc -o producer_consumer producer_consumer.c -lpthread
./producer_consumer
常见问题解析
问题1:死锁
- 现象:程序卡住,无输出
- 原因:信号量顺序错误,如先P(mutex)再P(empty)
- 解决:确保信号量获取顺序一致,避免循环等待
问题2:缓冲区溢出
- 现象:生产者写入超出缓冲区大小
- 解决:使用循环队列,通过取模运算控制in和out指针
2.3 实验三:内存管理模拟
理论要点
- 连续分配(首次适应、最佳适应)
- 分页管理
- 页面置换算法(FIFO、LRU、OPT)
实践代码示例:LRU页面置换算法模拟
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define PAGE_SIZE 3 // 内存中最多3个页面
#define REFERENCE_SIZE 10 // 页面引用序列长度
// 页面结构
typedef struct {
int page_num; // 页面号
int last_access; // 最后访问时间
bool valid; // 是否在内存中
} Page;
// LRU算法实现
int lru_simulation(int reference[], int ref_len) {
Page memory[PAGE_SIZE];
int page_faults = 0;
int current_time = 0;
// 初始化内存
for (int i = 0; i < PAGE_SIZE; i++) {
memory[i].valid = false;
}
printf("页面引用序列: ");
for (int i = 0; i < ref_len; i++) {
printf("%d ", reference[i]);
}
printf("\n\n");
for (int i = 0; i < ref_len; i++) {
int page = reference[i];
bool found = false;
// 检查页面是否已在内存中
for (int j = 0; j < PAGE_SIZE; j++) {
if (memory[j].valid && memory[j].page_num == page) {
found = true;
memory[j].last_access = current_time;
printf("时间%d: 页面%d命中,更新访问时间\n", current_time, page);
break;
}
}
if (!found) {
// 页面不在内存中,发生缺页
page_faults++;
printf("时间%d: 页面%d缺页,", current_time, page);
// 检查是否有空闲位置
bool empty_slot = false;
for (int j = 0; j < PAGE_SIZE; j++) {
if (!memory[j].valid) {
memory[j].page_num = page;
memory[j].last_access = current_time;
memory[j].valid = true;
empty_slot = true;
printf("放入空闲位置%d\n", j);
break;
}
}
if (!empty_slot) {
// 找到LRU页面
int lru_index = 0;
int min_time = memory[0].last_access;
for (int j = 1; j < PAGE_SIZE; j++) {
if (memory[j].last_access < min_time) {
min_time = memory[j].last_access;
lru_index = j;
}
}
printf("替换页面%d\n", memory[lru_index].page_num);
memory[lru_index].page_num = page;
memory[lru_index].last_access = current_time;
}
}
// 打印当前内存状态
printf("当前内存: ");
for (int j = 0; j < PAGE_SIZE; j++) {
if (memory[j].valid) {
printf("[%d] ", memory[j].page_num);
} else {
printf("[ ] ");
}
}
printf("\n\n");
current_time++;
}
return page_faults;
}
int main() {
// 页面引用序列(示例)
int reference[] = {1, 2, 3, 4, 1, 2, 5, 1, 2, 3};
int ref_len = sizeof(reference) / sizeof(reference[0]);
int faults = lru_simulation(reference, ref_len);
printf("总缺页次数: %d\n", faults);
printf("缺页率: %.2f%%\n", (float)faults / ref_len * 100);
return 0;
}
实验步骤
- 编译运行:
gcc -o lru_sim lru_sim.c && ./lru_sim - 尝试不同的页面引用序列
- 比较不同置换算法的缺页率
常见问题解析
问题1:LRU实现复杂度高
- 现象:直接实现LRU需要维护访问时间链表,效率低
- 解决:使用近似LRU(如时钟算法)或使用栈/队列优化
问题2:页面引用序列设计不合理
- 现象:缺页率过高或过低,无法体现算法差异
- 解决:设计局部性明显的序列,如:1,2,3,1,2,3,4,5,4,5
2.4 实验四:文件系统模拟
理论要点
- 文件控制块(FCB)
- 目录结构(单级、多级)
- 文件分配方式(连续、链接、索引)
实践代码示例:简单文件系统模拟
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define MAX_FILES 100
#define MAX_FILENAME 20
#define BLOCK_SIZE 512
#define TOTAL_BLOCKS 1000
// 文件控制块
typedef struct {
char filename[MAX_FILENAME];
int size; // 文件大小(字节)
int start_block; // 起始块号
int block_count; // 占用块数
bool used; // 是否被使用
} FCB;
// 磁盘块状态
bool disk_blocks[TOTAL_BLOCKS];
// 文件系统
FCB file_table[MAX_FILES];
int file_count = 0;
// 初始化文件系统
void init_filesystem() {
// 初始化磁盘块
for (int i = 0; i < TOTAL_BLOCKS; i++) {
disk_blocks[i] = false;
}
// 初始化文件表
for (int i = 0; i < MAX_FILES; i++) {
file_table[i].used = false;
}
file_count = 0;
printf("文件系统初始化完成\n");
}
// 创建文件
bool create_file(const char* filename, int size) {
if (file_count >= MAX_FILES) {
printf("错误:文件数量已达上限\n");
return false;
}
// 检查文件名是否重复
for (int i = 0; i < MAX_FILES; i++) {
if (file_table[i].used && strcmp(file_table[i].filename, filename) == 0) {
printf("错误:文件名已存在\n");
return false;
}
}
// 计算需要的块数(向上取整)
int blocks_needed = (size + BLOCK_SIZE - 1) / BLOCK_SIZE;
// 寻找连续的空闲块
int start_block = -1;
for (int i = 0; i <= TOTAL_BLOCKS - blocks_needed; i++) {
bool found = true;
for (int j = 0; j < blocks_needed; j++) {
if (disk_blocks[i + j]) {
found = false;
break;
}
}
if (found) {
start_block = i;
break;
}
}
if (start_block == -1) {
printf("错误:磁盘空间不足\n");
return false;
}
// 分配磁盘块
for (int i = 0; i < blocks_needed; i++) {
disk_blocks[start_block + i] = true;
}
// 创建文件表项
strcpy(file_table[file_count].filename, filename);
file_table[file_count].size = size;
file_table[file_count].start_block = start_block;
file_table[file_count].block_count = blocks_needed;
file_table[file_count].used = true;
file_count++;
printf("文件创建成功: %s, 大小: %d字节, 占用块: %d\n",
filename, size, blocks_needed);
return true;
}
// 删除文件
bool delete_file(const char* filename) {
for (int i = 0; i < MAX_FILES; i++) {
if (file_table[i].used && strcmp(file_table[i].filename, filename) == 0) {
// 释放磁盘块
for (int j = 0; j < file_table[i].block_count; j++) {
disk_blocks[file_table[i].start_block + j] = false;
}
// 从文件表中移除
file_table[i].used = false;
file_count--;
printf("文件删除成功: %s\n", filename);
return true;
}
}
printf("错误:文件不存在\n");
return false;
}
// 显示文件信息
void show_files() {
printf("\n=== 文件列表 ===\n");
printf("%-20s %-10s %-10s %-10s\n", "文件名", "大小", "起始块", "块数");
printf("------------------------------------------------\n");
for (int i = 0; i < MAX_FILES; i++) {
if (file_table[i].used) {
printf("%-20s %-10d %-10d %-10d\n",
file_table[i].filename,
file_table[i].size,
file_table[i].start_block,
file_table[i].block_count);
}
}
printf("\n");
}
// 显示磁盘使用情况
void show_disk_usage() {
int used_blocks = 0;
for (int i = 0; i < TOTAL_BLOCKS; i++) {
if (disk_blocks[i]) used_blocks++;
}
printf("磁盘块总数: %d\n", TOTAL_BLOCKS);
printf("已使用块数: %d\n", used_blocks);
printf("空闲块数: %d\n", TOTAL_BLOCKS - used_blocks);
printf("使用率: %.2f%%\n", (float)used_blocks / TOTAL_BLOCKS * 100);
}
int main() {
init_filesystem();
// 测试操作
create_file("document.txt", 1024);
create_file("image.jpg", 2048);
create_file("program.c", 512);
show_files();
show_disk_usage();
delete_file("image.jpg");
show_files();
show_disk_usage();
return 0;
}
实验步骤
- 编译运行:
gcc -o fs_sim fs_sim.c && ./fs_sim - 尝试创建不同大小的文件
- 观察磁盘空间分配情况
常见问题解析
问题1:磁盘碎片化
- 现象:连续分配导致外部碎片
- 解决:引入文件分配表(FAT)或索引节点(inode)支持非连续分配
问题2:文件名冲突
- 现象:创建同名文件导致数据覆盖
- 解决:添加文件名检查机制,支持目录结构
第三部分:高级实验项目
3.1 实验五:简单Shell实现
理论要点
- 命令解析
- 进程创建与执行
- 管道与重定向
实践代码示例:基础Shell
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#define MAX_CMD_LEN 256
#define MAX_ARGS 20
// 解析命令行
int parse_command(char* cmd, char** args) {
int arg_count = 0;
char* token = strtok(cmd, " \t\n");
while (token != NULL && arg_count < MAX_ARGS - 1) {
args[arg_count++] = token;
token = strtok(NULL, " \t\n");
}
args[arg_count] = NULL; // 参数列表以NULL结尾
return arg_count;
}
// 执行命令
void execute_command(char** args) {
if (args[0] == NULL) return;
// 内置命令:cd
if (strcmp(args[0], "cd") == 0) {
if (args[1] != NULL) {
if (chdir(args[1]) != 0) {
perror("cd failed");
}
}
return;
}
// 内置命令:exit
if (strcmp(args[0], "exit") == 0) {
exit(0);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
} else if (pid == 0) {
// 子进程执行命令
if (execvp(args[0], args) < 0) {
perror("execvp failed");
exit(1);
}
} else {
// 父进程等待
int status;
waitpid(pid, &status, 0);
}
}
// 处理管道
void handle_pipe(char* cmd) {
char* pipe_pos = strchr(cmd, '|');
if (pipe_pos == NULL) {
// 没有管道,直接执行
char* args[MAX_ARGS];
int arg_count = parse_command(cmd, args);
execute_command(args);
return;
}
// 分割管道命令
*pipe_pos = '\0';
char* left_cmd = cmd;
char* right_cmd = pipe_pos + 1;
// 跳过管道符前后的空格
while (*right_cmd == ' ') right_cmd++;
// 创建管道
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe failed");
return;
}
// 左侧命令(写入管道)
pid_t left_pid = fork();
if (left_pid == 0) {
close(pipefd[0]); // 关闭读端
dup2(pipefd[1], STDOUT_FILENO); // 重定向标准输出到管道
close(pipefd[1]);
char* left_args[MAX_ARGS];
parse_command(left_cmd, left_args);
execute_command(left_args);
exit(0);
}
// 右侧命令(从管道读取)
pid_t right_pid = fork();
if (right_pid == 0) {
close(pipefd[1]); // 关闭写端
dup2(pipefd[0], STDIN_FILENO); // 重定向标准输入从管道
close(pipefd[0]);
char* right_args[MAX_ARGS];
parse_command(right_cmd, right_args);
execute_command(right_args);
exit(0);
}
// 父进程关闭管道并等待
close(pipefd[0]);
close(pipefd[1]);
waitpid(left_pid, NULL, 0);
waitpid(right_pid, NULL, 0);
}
// 主循环
int main() {
char cmd[MAX_CMD_LEN];
while (1) {
printf("myshell> ");
if (fgets(cmd, MAX_CMD_LEN, stdin) == NULL) {
break;
}
// 移除换行符
cmd[strcspn(cmd, "\n")] = '\0';
// 处理管道
handle_pipe(cmd);
}
return 0;
}
编译与运行
gcc -o myshell myshell.c
./myshell
测试示例
myshell> ls -l
myshell> echo "Hello World"
myshell> ls | grep .c
myshell> cd /tmp
myshell> exit
常见问题解析
问题1:命令执行失败
- 现象:输入命令后无响应或报错
- 解决:检查命令路径,使用绝对路径或确保PATH环境变量正确
问题2:管道通信异常
- 现象:管道命令无法正常工作
- 解决:确保正确关闭管道文件描述符,避免资源泄漏
3.2 实验六:线程池实现
理论要点
- 线程池工作原理
- 任务队列管理
- 线程同步机制
实践代码示例:简单线程池
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <stdbool.h>
// 任务结构
typedef struct task {
void (*function)(void*); // 任务函数
void* arg; // 任务参数
struct task* next; // 下一个任务
} task_t;
// 线程池结构
typedef struct {
task_t* task_queue; // 任务队列
int task_count; // 任务数量
int thread_count; // 线程数量
pthread_t* threads; // 线程数组
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t cond; // 条件变量
bool shutdown; // 关闭标志
} thread_pool_t;
// 创建线程池
thread_pool_t* create_thread_pool(int thread_count) {
thread_pool_t* pool = malloc(sizeof(thread_pool_t));
if (!pool) return NULL;
pool->task_queue = NULL;
pool->task_count = 0;
pool->thread_count = thread_count;
pool->shutdown = false;
// 初始化互斥锁和条件变量
pthread_mutex_init(&pool->mutex, NULL);
pthread_cond_init(&pool->cond, NULL);
// 创建线程数组
pool->threads = malloc(sizeof(pthread_t) * thread_count);
if (!pool->threads) {
free(pool);
return NULL;
}
// 创建线程
for (int i = 0; i < thread_count; i++) {
if (pthread_create(&pool->threads[i], NULL, worker, pool) != 0) {
// 创建失败,清理已创建的线程
for (int j = 0; j < i; j++) {
pthread_cancel(pool->threads[j]);
}
free(pool->threads);
free(pool);
return NULL;
}
}
return pool;
}
// 工作线程函数
void* worker(void* arg) {
thread_pool_t* pool = (thread_pool_t*)arg;
while (1) {
pthread_mutex_lock(&pool->mutex);
// 等待任务或关闭信号
while (pool->task_queue == NULL && !pool->shutdown) {
pthread_cond_wait(&pool->cond, &pool->mutex);
}
// 如果线程池关闭,退出
if (pool->shutdown) {
pthread_mutex_unlock(&pool->mutex);
break;
}
// 从队列中取出任务
task_t* task = pool->task_queue;
if (task) {
pool->task_queue = task->next;
pool->task_count--;
}
pthread_mutex_unlock(&pool->mutex);
// 执行任务
if (task) {
task->function(task->arg);
free(task);
}
}
return NULL;
}
// 添加任务到线程池
bool add_task(thread_pool_t* pool, void (*function)(void*), void* arg) {
if (!pool || !function) return false;
task_t* new_task = malloc(sizeof(task_t));
if (!new_task) return false;
new_task->function = function;
new_task->arg = arg;
new_task->next = NULL;
pthread_mutex_lock(&pool->mutex);
// 添加到任务队列尾部
if (pool->task_queue == NULL) {
pool->task_queue = new_task;
} else {
task_t* current = pool->task_queue;
while (current->next) {
current = current->next;
}
current->next = new_task;
}
pool->task_count++;
// 通知等待的线程
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
return true;
}
// 销毁线程池
void destroy_thread_pool(thread_pool_t* pool) {
if (!pool) return;
// 设置关闭标志
pthread_mutex_lock(&pool->mutex);
pool->shutdown = true;
pthread_cond_broadcast(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
// 等待所有线程结束
for (int i = 0; i < pool->thread_count; i++) {
pthread_join(pool->threads[i], NULL);
}
// 清理任务队列
task_t* task = pool->task_queue;
while (task) {
task_t* next = task->next;
free(task);
task = next;
}
// 清理资源
free(pool->threads);
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->cond);
free(pool);
}
// 示例任务函数
void example_task(void* arg) {
int* num = (int*)arg;
printf("线程%lu处理任务: %d\n", pthread_self(), *num);
sleep(1); // 模拟耗时操作
free(num);
}
int main() {
// 创建线程池(4个线程)
thread_pool_t* pool = create_thread_pool(4);
if (!pool) {
printf("创建线程池失败\n");
return 1;
}
printf("线程池创建成功,添加10个任务\n");
// 添加10个任务
for (int i = 0; i < 10; i++) {
int* num = malloc(sizeof(int));
*num = i;
add_task(pool, example_task, num);
}
// 等待所有任务完成
sleep(5);
// 销毁线程池
destroy_thread_pool(pool);
printf("线程池已销毁\n");
return 0;
}
编译与运行
gcc -o thread_pool thread_pool.c -lpthread
./thread_pool
常见问题解析
问题1:线程池无法正常工作
- 现象:任务不被执行或程序卡死
- 解决:检查互斥锁和条件变量的使用,确保线程正确等待和唤醒
问题2:内存泄漏
- 现象:程序运行后内存占用持续增长
- 解决:确保任务执行后释放内存,销毁线程池时清理所有资源
第四部分:实验技巧与最佳实践
4.1 调试技巧
使用GDB调试多线程程序
gdb ./program (gdb) break main (gdb) run (gdb) info threads # 查看所有线程 (gdb) thread 2 # 切换到线程2 (gdb) bt # 查看调用栈使用Valgrind检测内存泄漏
valgrind --leak-check=full ./program使用strace跟踪系统调用
strace ./program
4.2 性能优化建议
- 减少系统调用次数:批量处理I/O操作
- 合理使用缓存:减少磁盘访问
- 避免频繁的上下文切换:合理设计线程数量
4.3 代码规范
- 错误处理:检查所有系统调用的返回值
- 资源管理:确保打开的文件描述符被正确关闭
- 线程安全:共享数据必须加锁保护
第五部分:常见问题综合解析
5.1 编译与运行问题
问题:编译时出现”undefined reference to”错误
- 原因:缺少必要的库链接
- 解决:添加正确的链接选项,如
-lpthread、-lrt
5.2 运行时问题
问题:程序运行时出现段错误(Segmentation Fault)
- 原因:访问非法内存地址
- 解决:使用GDB调试,检查指针初始化和数组越界
5.3 实验报告撰写
- 实验目的:明确实验要解决的问题
- 实验原理:简述相关理论知识
- 实验步骤:详细记录操作过程
- 实验结果:展示代码输出和分析
- 问题与解决:记录遇到的问题及解决方案
- 心得体会:总结学习收获
第六部分:扩展学习资源
6.1 推荐书籍
- 《深入理解计算机系统》(CSAPP)
- 《Linux内核设计与实现》
- 《操作系统精髓与设计原理》
6.2 在线资源
- MIT 6.828(操作系统课程)
- Berkeley CS162(操作系统)
- Linux内核源码(https://www.kernel.org/)
6.3 开源项目参考
- xv6(教学用操作系统)
- Linux内核源码
- Minix操作系统
结语
哈理工操作系统实验是理论与实践结合的绝佳机会。通过本指南,希望你能系统掌握操作系统核心概念,熟练运用编程技术解决实际问题。记住,操作系统实验不仅是完成代码,更重要的是理解背后的原理和设计思想。遇到问题时,善用调试工具,查阅资料,与同学讨论,这些都是提升能力的重要途径。祝你在操作系统实验中取得优异成绩!
