• 分享2:进程管理与调度器

1. 提纲

  • 进程的创建、调度和终止
  • 内核调度器的工作原理与算法
  • 进程间通信的机制

2. 摘要

在操作系统中,进程是计算机资源的管理和任务调度的基本单位。理解进程的创建、调度、终止过程以及内核调度器的工作原理和进程间通信的机制,对于深入理解操作系统的运行至关重要。本报告将详细介绍进程的生命周期、调度机制以及如何实现进程间的通信,同时提供多个实验案例,帮助加深理解。


3. 进程的创建、调度和终止

3.1 进程的创建

进程是操作系统进行资源管理的基本单位。进程创建通常由系统调用 fork() 完成,或者是由操作系统的调度程序为一个新的任务分配资源。进程的创建过程通常包括以下步骤:

  • 分配进程控制块 (PCB):包含进程的基本信息(如进程ID、状态、程序计数器等)。
  • 为进程分配内存空间:加载程序代码和相关数据。
  • 设置文件描述符:进程使用的文件、设备等资源。

3.2 进程的调度

操作系统通过调度程序决定哪个进程占用 CPU,通常有以下几种调度策略:

  • 先来先服务(FCFS):按照进程到达的顺序进行调度。
  • 短作业优先(SJF):优先执行预计执行时间最短的进程。
  • 时间片轮转(RR):为每个进程分配一个固定的时间片,时间片用完后切换到下一个进程。
  • 优先级调度(Priority Scheduling):根据进程优先级来调度,优先级高的进程先执行。

3.3 进程的终止

进程终止通常由以下几种情况引起:

  • 正常终止:进程执行完毕或退出。
  • 错误终止:由于某种错误或异常情况,进程被强制终止。
  • 父进程终止:父进程终止时,其子进程也会被终止。

进程终止后,操作系统会回收其占用的资源,并将其从就绪队列中移除。


4. 内核调度器的工作原理与算法

内核调度器的主要任务是选择合适的进程执行。在多核系统和多任务系统中,调度器是保证系统公平和效率的重要部分。内核调度器的工作原理如下:

  1. 就绪队列:内核维护一个就绪队列,存放所有准备执行的进程。每当一个进程处于就绪状态时,它就会被加入该队列。
  2. 调度决策:内核根据调度算法选择一个进程从就绪队列中调度执行。
  3. 时间片管理:对于时间片轮转调度算法,内核会为每个进程分配一定的时间片。当时间片耗尽时,进程会被挂起并调度下一个进程。
  4. 上下文切换:当进程被暂停时,内核会保存当前进程的状态,并加载下一个进程的状态。

常见的调度算法包括:

  • 先来先服务(FCFS)
  • 短作业优先(SJF)
  • 时间片轮转(RR)
  • 优先级调度(Priority Scheduling)
  • 多级反馈队列(MLFQ)

5. 进程间通信的机制

进程间通信(IPC)是操作系统提供的一组机制,用于不同进程之间交换数据。常见的进程间通信方式包括:

5.1 管道(Pipe)

管道是一种单向通信机制,用于父子进程或同一进程组中的进程间的通信。

# 使用管道示例
$ echo "Hello, World!" | grep "World"

5.2 消息队列(Message Queue)

消息队列是一种先进先出的通信机制,可以用于不同进程之间的消息传递。

5.3 共享内存(Shared Memory)

共享内存允许多个进程直接访问相同的内存区域,通常效率较高,但需要进程间协调访问。

5.4 信号量(Semaphore)

信号量主要用于进程之间的同步,避免多个进程同时访问共享资源引发冲突。


6. 实验案例

实验 1:使用 fork() 创建进程

目标:

通过 fork() 创建一个新进程,并了解父子进程之间的关系。

操作步骤:
  1. 编写一个 C 程序,通过 fork() 创建一个子进程。
  2. 使用 getpid()getppid() 显示父进程和子进程的进程 ID。
示例代码:
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        printf("Child Process: PID = %d, Parent PID = %d\n", getpid(), getppid());
    } else if (pid > 0) {
        // 父进程
        printf("Parent Process: PID = %d, Child PID = %d\n", getpid(), pid);
    } else {
        // fork 错误
        perror("fork failed");
    }

    return 0;
}
实验总结:
  • fork() 系统调用用于创建子进程,返回值区分父子进程。
  • 父进程返回子进程的 PID,子进程返回 0。

实验 2:实现时间片轮转调度算法(模拟)

目标:

模拟时间片轮转调度算法,通过程序展示进程调度。

操作步骤:
  1. 使用 Python 编写一个简单的进程调度模拟程序。
  2. 模拟多个进程,每个进程按时间片轮转执行。
示例代码:
import time
from collections import deque

# 模拟进程类
class Process:
    def __init__(self, pid, name, burst_time):
        self.pid = pid
        self.name = name
        self.burst_time = burst_time
        self.remaining_time = burst_time

# 时间片轮转调度器
def round_robin(processes, time_slice):
    queue = deque(processes)
    while queue:
        process = queue.popleft()
        if process.remaining_time > time_slice:
            process.remaining_time -= time_slice
            queue.append(process)
            print(f"Process {process.name} (PID {process.pid}) is running for {time_slice}ms, remaining time {process.remaining_time}ms")
        else:
            print(f"Process {process.name} (PID {process.pid}) is completed")
            process.remaining_time = 0

# 创建进程
processes = [
    Process(1, "P1", 10),
    Process(2, "P2", 5),
    Process(3, "P3", 7)
]

# 调用调度器
round_robin(processes, 3)
实验总结:
  • 时间片轮转算法确保每个进程都能公平地占用 CPU,直到所有进程完成。
  • 使用队列来管理待执行的进程。

实验 3:进程间通信(共享内存)

目标:

使用共享内存实现进程间通信。

操作步骤:
  1. 编写一个生产者进程将数据写入共享内存。
  2. 编写一个消费者进程从共享内存读取数据。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("shmfile", 65); // 创建唯一的键值
    int shmid = shmget(key, SHM_SIZE, 0666|IPC_CREAT); // 创建共享内存
    char *str = (char*) shmat(shmid, (void*)0, 0); // 将共享内存映射到进程空间

    printf("Write Data: ");
    fgets(str, SHM_SIZE, stdin); // 将数据写入共享内存

    printf("Data written: %s\n", str);

    shmdt(str); // 分离共享内存
    return 0;
}
实验总结:
  • 使用共享内存可以有效地进行进程间通信,但需要注意同步和资源管理。
  • 通过 shmget() 创建共享内存,通过 shmat() 映射到进程地址空间。