早期计算机一次只能运行一个程序,CPU 利用率极低。当程序等待 I/O 时,CPU 空闲空转。多道程序设计的目标是让 CPU 在等待 I/O 时去执行其他程序,从而提高系统吞吐量。但多个程序同时存在,谁先运行?运行多久?这就诞生了调度。
调度器维护一组就绪队列,按照特定算法从队列中选择进程投入运行。核心机制包括:
多级反馈队列 (MFQ) 是现代调度器的基石:设置多个不同优先级的队列,高优先级队列时间片短,低优先级队列时间长。进程用完时间片降级,I/O 密集型进程留在高优先级,CPU 密集型进程逐渐降级。这种设计兼顾了响应时间和吞吐量。
// 简化版调度器:时间片轮转 + 优先级队列
struct task {
int pid;
int priority; // 优先级 0~3 (0 最高)
int time_slice; // 剩余时间片
struct task* next;
};
void schedule(struct task* ready_queue) {
struct task* current = ready_queue;
if (current == NULL) return;
// 选择优先级最高的就绪任务
struct task* best = current;
while (current != NULL) {
if (current->priority < best->priority) {
best = current;
}
current = current->next;
}
// 运行该任务(模拟)
best->time_slice--;
if (best->time_slice <= 0) {
// 时间片用完,降级或放回就绪队列
best->priority++; // 降级
best->time_slice = 10; // 重置时间片
}
}
早期程序直接使用物理地址,两个问题:地址冲突(无法同时运行两个大程序)和 内存碎片(分配和回收导致碎片)。更严重的是,物理内存容量有限,而程序越来越大(如大型游戏、数据库)。
虚拟内存将程序地址空间划分为固定大小的页 (Page),物理内存划分为相同大小的页框 (Frame)。通过页表记录虚拟页到物理页框的映射关系。
TLB (Translation Lookaside Buffer) 是页表的硬件缓存,存储最近使用的地址映射。由于局部性原理,TLB 命中率可达 99% 以上,大大降低地址转换开销。缺页中断发生在访问未映射页面时,操作系统从磁盘加载页面到内存,更新页表,然后重新执行指令。
// 缺页中断处理函数(简化)
void page_fault_handler(uintptr_t fault_addr) {
// 1. 获取缺页的虚拟地址
uintptr_t page_start = fault_addr & ~(0xFFF); // 对齐到页边界
int vpn = page_start / PAGE_SIZE; // 虚拟页号
// 2. 检查地址是否合法
if (!is_valid_address(vpn)) {
kill_process(current_process); // 非法访问 → 段错误
return;
}
// 3. 从磁盘读取页面
int phys_frame = allocate_frame(); // 分配物理页框
read_from_swap(vpn, phys_frame); // 从交换分区读入
// 4. 更新页表
current_process->page_table[vpn] = phys_frame | PRESENT_BIT;
}
内存数据在断电后丢失,需要磁盘(HDD/SSD)来持久化存储。但磁盘操作远慢于内存(纳秒 vs 毫秒),且按块(通常 4KB)访问。文件系统解决的核心问题是:如何组织、命名、查找、管理磁盘上的数据块,并提供高效的访问接口。
文件系统将磁盘划分为超级块(元数据)、inode 区域(索引节点)、数据块区域。每个文件对应一个 inode,存储文件元数据(权限、大小、时间、数据块指针)。
目录本质是特殊的文件,存储文件名到 inode 的映射。日志 (Journaling) 在写数据前先记录操作意图,系统崩溃后可以恢复,保证元数据一致性。现代文件系统如 ext4、XFS 都支持日志。
// 读文件:路径解析 → 查 inode → 读数据块
int read_file(const char* path, char* buffer, size_t size) {
// 1. 路径解析:从根目录开始查找
struct inode* ino = lookup_path(path);
if (!ino) return -1;
// 2. 检查权限 & 类型
if (!(ino->mode & REGULAR_FILE)) return -1;
// 3. 计算需要读取的块
size_t offset = 0;
int block_idx = 0;
while (offset < size) {
int phys_block = get_data_block(ino, block_idx);
read_block(phys_block, buffer + offset);
offset += BLOCK_SIZE;
block_idx++;
}
return offset;
}
CPU 主频高达 GHz 级别,而磁盘、网卡等外设速度慢几个数量级。如果 CPU 等待设备完成 I/O,将浪费大量时间。需要设计合理的 I/O 模型,让 CPU 在设备工作时做其他事情,或者减少等待。
操作系统提供多种 I/O 模型,核心区别在于数据准备和数据拷贝两个阶段是否阻塞。
IO 多路复用(select/poll/epoll)是高性能网络服务的基础。它允许一个进程同时监视多个文件描述符,当任意一个准备好数据时,进程被唤醒。epoll 是 Linux 下的高效实现,使用事件驱动机制,支持水平触发和边缘触发。
// 使用 epoll 同时监听多个 socket
int epoll_demo() {
int epfd = epoll_create1(0);
struct epoll_event ev, events[10];
// 添加监听 socket
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = listen_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, 10, -1); // 阻塞等待
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_sock) {
// 新连接到达
accept_connection(listen_sock);
} else {
// 普通数据可读
handle_data(events[i].data.fd);
}
}
}
}