操作系统实验5 —— 进程互斥

实验书写的太复杂了。看这个示例:
以下示例实验程序应能模拟一个读者 / 写者问题 , 它应能实现一下功能:

  1. 任意多个读者可以同时读;
  2. 任意时刻只能有一个写者写;
  3. 如果写者正在写,那么读者就必须等待;
  4. 如果读者正在读,那么写者也必须等待;
  5. 允许写者优先;
  6. 防止读者或写者发生饥饿。

这个场景,有读者,作者,和管理员。

设计这样的管理员,

管理100张入场券count,读者进来时候发出一张,只有读者全部离馆才允许作者写入。

1. 读者来(Reader)
  • 动作count - 1
  • 结果
    • 如果结果 >= 0:说明还有空位,进去读。
    • 如果结果 < 0:说明虽然数值变负了,但在该算法逻辑下,这表示读者正在占用资源
2. 写者来(Writer)
  • 动作 :写者想进去,必须等图书馆没人。控制进程尝试 count - 100
  • 结果
    • 只有当 count 刚好是 100(没人)时,减去 100 变成 0。此时 count == 0,表示写者独占
    • 如果有读者在(count 不是 100),写者就得等着。
3. 离开(Finish)
  • 读者离开count + 1。直到加回 100,表示房间空了。
  • 写者离开count + 100。直接加回 100,恢复初始状态。

示例

我们可以利用上节实验中介绍的 IPC 机制中的消息队列来实验一下以上使用
消息传递算法的读写者问题的解法,看其是否能够满足我们的要求。仍采用共享内
存模拟要读写的对象,一写者向共享内存中写入一串字符后,多个读者可同时从共
享内存中读出该串字符

需要创建的文件

你需要创建以下 5 个文件,并将对应的代码复制进去:

  1. ipc.h:头文件,包含定义和结构体。
  2. ipc.c:辅助函数实现(虽然图片里没直接显示内容,但 Makefile 和编译命令里用到了它,通常包含 set_shm, set_msg 等封装函数)。注意:由于图片未给出 ipc.c 的具体代码,我将在下方为你补全一个标准的实现,否则代码无法编译。
  3. control.c:控制者程序(核心逻辑)。
  4. reader.c:读者程序。
  5. writer.c:写者程序。
  6. Makefile:编译脚本(可选,如果你想用 make 而不是手动 gcc)。

ipc.h

cpp 复制代码
/*
 * Filename : ipc.h
 * copyright : (C) 2006 by zhonghonglie
 * Function : 声明 IPC 机制的函数原型和全局变量
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>

#define BUFSZ 256
#define MAXVAL 100
#define STRSIZ 8
#define WRITEREQUEST 1   // 写请求标识
#define READERREQUEST 2  // 读请求标识
#define FINISHED 3       // 读写完成标识

/* 信号量控制用的共同体 */
typedef union semuns {
    int val;
} Sem_uns;

/* 消息结构体 */
typedef struct msgbuf {
    long mtype;
    int mid;
} Msg_buf;

// 全局变量声明
extern key_t buff_key;
extern int buff_num;
extern char *buff_ptr;
extern int shm_flg;

extern int quest_flg;
extern key_t quest_key;
extern int quest_id;

extern int respond_flg;
extern key_t respond_key;
extern int respond_id;

// 函数原型声明
int get_ipc_id(char *proc_file, key_t key);
char *set_shm(key_t shm_key, int shm_num, int shm_flag);
int set_msg(key_t msg_key, int msg_flag);
int set_sem(key_t sem_key, int sem_val, int sem_flag);
int down(int sem_id);
int up(int sem_id);

ipc.c

cpp 复制代码
#include "ipc.h"
#include <fcntl.h>
#include <sys/stat.h>

// 全局变量定义
key_t buff_key;
int buff_num;
char *buff_ptr;
int shm_flg;

int quest_flg;
key_t quest_key;
int quest_id;

int respond_flg;
key_t respond_key;
int respond_id;

/* 获取IPC标识符的辅助函数 */
int get_ipc_id(char *proc_file, key_t key) {
    // 简单实现,实际可能涉及文件读取,这里主要为了链接通过
    return 0;
}

/* 创建或获取共享内存 */
char *set_shm(key_t shm_key, int shm_num, int shm_flag) {
    int shmid;
    if ((shmid = shmget(shm_key, shm_num, shm_flag)) < 0) {
        perror("shmget error");
        exit(EXIT_FAILURE);
    }
    return (char *)shmat(shmid, NULL, 0);
}

/* 创建或获取消息队列 */
int set_msg(key_t msg_key, int msg_flag) {
    int msgid;
    if ((msgid = msgget(msg_key, msg_flag)) < 0) {
        perror("msgget error");
        exit(EXIT_FAILURE);
    }
    return msgid;
}

/* 信号量操作(通常本实验不需要复杂的P/V封装,但在头文件声明了) */
int set_sem(key_t sem_key, int sem_val, int sem_flag) {
    return 0; // 本实验主要通过消息控制,此函数留空或简单实现
}
int down(int sem_id) { return 0; }
int up(int sem_id) { return 0; }

control.c

cpp 复制代码
/*
 * Filename : control.c
 * copyright : (C) 2006 by zhonghonglie
 * Function : 建立并模拟控制者进程
 */
#include "ipc.h"

int main(int argc, char *argv[]) {
    int i;
    int rate;
    int w_mid;
    int count = MAXVAL;
    Msg_buf msg_arg;
    struct msqid_ds msg_inf;

    // 建立共享内存
    buff_key = 101;
    buff_num = STRSIZ + 1;
    shm_flg = IPC_CREAT | 0644;
    buff_ptr = (char *)set_shm(buff_key, buff_num, shm_flg);

    // 初始化共享内存内容为 'A'
    for (i = 0; i < STRSIZ; i++) buff_ptr[i] = 'A';
    buff_ptr[i] = '\0';

    // 建立请求消息队列
    quest_flg = IPC_CREAT | 0644;
    quest_key = 201;
    quest_id = set_msg(quest_key, quest_flg);

    // 建立响应消息队列
    respond_flg = IPC_CREAT | 0644;
    respond_key = 202;
    respond_id = set_msg(respond_key, respond_flg);

    printf("Wait quest \n");

    while (1) {
        // 当 count > 0 时,说明没有写者在写,可以接受新请求
        if (count > 0) {
            // 非阻塞接收消息
            quest_flg = IPC_NOWAIT;
            // 尝试接收 FINISHED 消息
            if (msgrcv(quest_id, &msg_arg, sizeof(msg_arg), FINISHED, quest_flg) >= 0) {
                // 有读者或写者完成
                count++;
                printf("%d reader finished\n", msg_arg.mid); // 原文此处打印reader,实际可能是writer或reader
            }
            // 尝试接收 READERREQUEST 消息
            else if (msgrcv(quest_id, &msg_arg, sizeof(msg_arg), READERREQUEST, quest_flg) >= 0) {
                // 有读者请求
                count--;
                msg_arg.mtype = msg_arg.mid;
                // 发送允许读消息到响应队列
                msgsnd(respond_id, &msg_arg, sizeof(msg_arg), 0);
                printf("%d quest read\n", msg_arg.mid);
            }
        }

        // 当 count == 0 时,说明可能有写者在等待,或者刚好资源空闲需优先处理写者
        if (count == 0) {
            // 阻塞接收 FINISHED 消息(意味着必须等当前占用的读者全部完成,或者处理写者)
            // 注意:这里的逻辑原文比较简略,通常是等待 FINISHED
            msgrcv(quest_id, &msg_arg, sizeof(msg_arg), FINISHED, 0);
            count = MAXVAL; // 重置计数,准备下一轮
            printf("%d write finished\n", msg_arg.mid);

            // 写完后检查是否有新的读者请求
            quest_flg = IPC_NOWAIT;
            if (msgrcv(quest_id, &msg_arg, sizeof(msg_arg), READERREQUEST, quest_flg) >= 0) {
                count--;
                msg_arg.mtype = msg_arg.mid;
                msgsnd(respond_id, &msg_arg, sizeof(msg_arg), 0);
                printf("%d quest read\n", msg_arg.mid);
            }
        }
    }
    return EXIT_SUCCESS;
}

reader.c

cpp 复制代码
/*
 * Filename : reader.c
 * copyright : (C) 2006 by zhonghonglie
 * Function : 建立并模拟读者进程
 */
#include "ipc.h"

int main(int argc, char *argv[]) {
    int i;
    int rate;
    Msg_buf msg_arg;

    if (argv[1] != NULL) rate = atoi(argv[1]);
    else rate = 3;

    // 关联共享内存
    buff_key = 101;
    buff_num = STRSIZ + 1;
    shm_flg = IPC_CREAT | 0644;
    buff_ptr = (char *)set_shm(buff_key, buff_num, shm_flg);

    // 关联请求消息队列
    quest_flg = IPC_CREAT | 0644;
    quest_key = 201;
    quest_id = set_msg(quest_key, quest_flg);

    // 关联响应消息队列
    respond_flg = IPC_CREAT | 0644;
    respond_key = 202;
    respond_id = set_msg(respond_key, respond_flg);

    msg_arg.mid = getpid();

    while (1) {
        // 发送读请求
        msg_arg.mtype = READERREQUEST;
        msgsnd(quest_id, &msg_arg, sizeof(msg_arg), 0);
        printf("%d reader quest\n", msg_arg.mid);

        // 等待允许读消息
        msgrcv(respond_id, &msg_arg, sizeof(msg_arg), msg_arg.mid, 0);
        printf("%d reading: %s\n", msg_arg.mid, buff_ptr);
        sleep(rate);

        // 发送完成消息
        msg_arg.mtype = FINISHED;
        msgsnd(quest_id, &msg_arg, sizeof(msg_arg), 0);
    }
    return EXIT_SUCCESS;
}

writer.c

cpp 复制代码
/*
 * Filename : writer.c
 * copyright : (C) 2006 by zhonghonglie
 * Function : 建立并模拟写者进程
 */
#include "ipc.h"

int main(int argc, char *argv[]) {
    int i, j = 0;
    int rate;
    Msg_buf msg_arg;

    if (argv[1] != NULL) rate = atoi(argv[1]);
    else rate = 3;

    // 关联共享内存
    buff_key = 101;
    buff_num = STRSIZ + 1;
    shm_flg = IPC_CREAT | 0644;
    buff_ptr = (char *)set_shm(buff_key, buff_num, shm_flg);

    // 关联请求消息队列
    quest_flg = IPC_CREAT | 0644;
    quest_key = 201;
    quest_id = set_msg(quest_key, quest_flg);

    // 关联响应消息队列
    respond_flg = IPC_CREAT | 0644;
    respond_key = 202;
    respond_id = set_msg(respond_key, respond_flg);

    msg_arg.mid = getpid();

    while (1) {
        // 发送写请求
        msg_arg.mtype = WRITEREQUEST;
        msgsnd(quest_id, &msg_arg, sizeof(msg_arg), 0);
        printf("%d writer quest\n", msg_arg.mid);

        // 等待允许写消息
        msgrcv(respond_id, &msg_arg, sizeof(msg_arg), msg_arg.mid, 0);

        // 写入数据
        for (i = 0; i < STRSIZ; i++) buff_ptr[i] = 'A' + j;
        j = (j + 1) % 26;
        printf("%d writing: %s\n", msg_arg.mid, buff_ptr);
        sleep(rate);

        // 发送完成消息
        msg_arg.mtype = FINISHED;
        msgsnd(quest_id, &msg_arg, sizeof(msg_arg), 0);
    }
    return EXIT_SUCCESS;
}
分别编译(推荐,因为需要运行多个程序)

你需要生成三个独立的可执行文件:control, reader, writer

  1. 编译控制者

    复制代码
    1gcc -o control control.c ipc.c
  2. 编译读者

    复制代码
    1gcc -o reader reader.c ipc.c
  3. 编译写者

    复制代码
    1gcc -o writer writer.c ipc.c

然后启动三个终端

运行步骤

  1. 先运行控制器(在一个终端窗口)

    复制代码
    ./control

    (屏幕会显示 Wait quest

  2. 运行读者(在另一个终端窗口)

    复制代码
    ./reader 2

    (2 表示休眠 2 秒,模拟读取时间)

  3. 运行写者(在第三个终端窗口)

    复制代码
    ./writer 3

我们设定 ./reader 2 ./writer3,意味着读者每2秒发送一次读请求,作者每3秒发送一次写请求。

在control面板可以看到管理员只响应读者请求,

完全不响应写响应,发生了阻塞。

这是因为只要count不到100,那么写者就没资格进去,而读者的请求频率远高于写者,所以一直阻塞。

问题11:不同启动顺序、不同延迟时间启动更多读者,是否仍能满足读写者问题的功能要求?

读写者问题的核心功能要求包括:

  • 多个读者可同时访问共享资源(读操作不互斥);
  • 写者必须独占访问共享资源(写操作互斥);
  • 读者和写者不能同时访问共享资源(读写互斥)。

分析

当启动更多读者,且采用不同的启动顺序和延迟时间时,只要程序正确实现了读写者问题的同步机制(如使用信号量、计数器、互斥锁等),仍能满足功能要求。例如,经典的读写者解决方案(如读者优先)中,通过read_count记录当前读者数量,mutex保护read_countrw_mutex保护共享资源:

  • 读者进入时,若read_count为0(第一个读者),则获取rw_mutex(阻止写者);
  • 读者离开时,若read_count减为0(最后一个读者),则释放rw_mutex(允许写者)。

即使读者启动顺序不同、延迟时间不同,只要同步机制正确,多个读者会同时访问资源,写者会被正确阻塞,不会出现读写冲突或写写冲突。因此,仍能满足读写者问题的功能要求

问题12:修改程序制造读者或写者的饥饿现象,观察并说明原因

饥饿现象的定义

饥饿是指某个进程(读者或写者)无限期等待访问共享资源,无法获得执行机会的现象。

我们刚刚看到的就是饥饿现象。

独立实验

这是一个经典的**睡眠理发师问题(Sleeping Barber Problem)**的变种。

要解决这个问题,我们需要使用 Linux 的 IPC(进程间通信) 机制。核心在于同步:理发师没顾客时要睡觉,顾客来了要叫醒理发师,资源(椅子、沙发、登记本)有限需要互斥。

以下是完整的解题思路和代码实现方案。

1. 问题分析与信号量设计

首先,我们需要定义好资源数量和对应的信号量(Semaphore):

  • 资源限制

    • 理发椅/理发师:3 个。
    • 沙发(等待理发):4 个。
    • 等候室(等待进沙发):13 个。
    • 总容量:3(理发)+ 4(沙发)+ 13(等候)= 20 人。
  • 共享资源(需要互斥)

    • 收银台(Cash Register):1 个,互斥信号量。
  • 信号量定义

    1. mutex_customers:保护顾客计数的互斥锁。
    2. mutex_cash:保护收银台的互斥锁。
    3. waiting_room:等候室空位(初始值 13)。
    4. sofa:沙发空位(初始值 4)。
    5. barber_chair:理发椅空位(初始值 3)。
    6. customer_ready:通知理发师有顾客(初始值 0)。
    7. haircut_done:通知顾客理发好了(初始值 0)。

2. 代码实现

我们需要创建一个头文件 barber.h 来定义共享结构和信号量,然后分别编写 barber.c(理发师进程)和 customer.c(顾客进程)。

第一步:创建头文件 barber.h

这个文件包含系统头文件、宏定义以及信号量操作的封装。

cpp 复制代码
/* barber.h */
#ifndef BARBER_H
#define BARBER_H

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>

// 资源定义
#define CHAIRS 3      // 理发椅数量
#define SOFA 4        // 沙发数量
#define WAITING_ROOM 13 // 等候室数量
#define TOTAL_CAP 20  // 总容量

// 联合体用于信号量操作
typedef union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
} semun;

// 全局信号量 ID
int sem_ids[7];

// 信号量索引枚举,方便管理
enum {
    MUTEX_CUST,     // 保护顾客计数
    MUTEX_CASH,     // 保护收银台
    SEM_WAITING,    // 等候室空位
    SEM_SOFA,       // 沙发空位
    SEM_CHAIR,      // 理发椅空位
    SEM_CUST_RDY,   // 顾客准备好(叫醒理发师)
    SEM_CUT_DONE    // 理发完成
};

// 封装 P 操作 (wait)
void sem_wait(int sem_id) {
    struct sembuf sb = {0, -1, 0};
    sb.sem_num = sem_id; // 注意:这里简化处理,实际需根据具体sem_id映射
    // 为了代码清晰,我们在主程序中直接使用 semop,或者写一个更通用的函数
    // 这里我们直接在代码中写 semop 结构体
}

// 获取时间字符串用于打印
char* get_time_str() {
    static char str[20];
    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);
    strftime(str, 20, "%H:%M:%S", tm_info);
    return str;
}

#endif
第二步:理发师进程 barber.c

理发师是死循环:等待顾客 -> 理发 -> 等待收款。

cpp 复制代码
/* barber.c */
#include "barber.h"

int main() {
    key_t key = ftok(".", 'b');
    if (key == -1) { perror("ftok"); exit(1); }

    // 获取信号量集
    int sem_set_id = semget(key, 7, 0666);
    if (sem_set_id == -1) { perror("semget barber"); exit(1); }

    printf("[系统] 理发店开门了,3位理发师已就位。\n");

    while(1) {
        // 1. 等待顾客 (P 操作 SEM_CUST_RDY)
        struct sembuf sb = {SEM_CUST_RDY, -1, 0};
        semop(sem_set_id, &sb, 1);

        // 2. 占用理发椅 (逻辑上其实已经在等待队列处理了,这里是模拟理发过程)
        printf("[%s] 理发师:开始理发...\n", get_time_str());
        sleep(2); // 模拟理发耗时

        // 3. 理发完成,通知顾客 (V 操作 SEM_CUT_DONE)
        struct sembuf sb_done = {SEM_CUT_DONE, 1, 0};
        semop(sem_set_id, &sb_done, 1);

        // 4. 等待顾客付款 (P 操作 MUTEX_CASH)
        // 注意:题目说只有一本登记册,理发师也要参与互斥
        struct sembuf sb_cash = {MUTEX_CASH, -1, 0};
        semop(sem_set_id, &sb_cash, 1);

        printf("[%s] 理发师:收到付款,服务结束。\n", get_time_str());
        sleep(1); // 模拟收款处理

        // 5. 释放收银台 (V 操作 MUTEX_CASH)
        struct sembuf sb_cash_rel = {MUTEX_CASH, 1, 0};
        semop(sem_set_id, &sb_cash_rel, 1);
    }

    return 0;
}
第三步:顾客进程 customer.c

顾客逻辑比较复杂:进店 -> 抢号 -> 等沙发 -> 等椅子 -> 理发 -> 付款。

cpp 复制代码
/* customer.c */
#include "barber.h"

int main(int argc, char *argv[]) {
    // 模拟不同顾客到达时间
    srand(time(NULL) + getpid());
    sleep(rand() % 3);

    key_t key = ftok(".", 'b');
    if (key == -1) { perror("ftok"); exit(1); }

    int sem_set_id = semget(key, 7, 0666);
    if (sem_set_id == -1) { perror("semget customer"); exit(1); }

    int pid = getpid();

    // 0. 尝试进入理发店 (检查总容量,这里简化为直接尝试获取等候室位置,
    //    如果等候室满了,代表总人数可能超了,或者刚好卡在边界。
    //    题目逻辑:超过20人不进。我们的信号量链是 13+4+3=20。
    //    只要成功 P(waiting_room),就说明还没满。)

    struct sembuf sb_wait = {SEM_WAITING, -1, IPC_NOWAIT}; // 非阻塞尝试

    if (semop(sem_set_id, &sb_wait, 1) == -1) {
        printf("[%s] 顾客 %d:店满了(>20人),我走了。\n", get_time_str(), pid);
        exit(0);
    }

    printf("[%s] 顾客 %d:进店,在等候室等待。\n", get_time_str(), pid);
    sleep(1); // 模拟等候室等待

    // 1. 从等候室移动到沙发 (P 沙发, V 等候室)
    struct sembuf sb_sofa_in = {SEM_SOFA, -1, 0};
    semop(sem_set_id, &sb_sofa_in, 1);

    struct sembuf sb_wait_out = {SEM_WAITING, 1, 0}; // 离开等候室
    semop(sem_set_id, &sb_wait_out, 1);

    printf("[%s] 顾客 %d:坐到沙发上等待叫号。\n", get_time_str(), pid);
    sleep(rand() % 3 + 1); // 模拟沙发等待时间

    // 2. 从沙发移动到理发椅 (P 椅子, V 沙发)
    struct sembuf sb_chair_in = {SEM_CHAIR, -1, 0};
    semop(sem_set_id, &sb_chair_in, 1);

    struct sembuf sb_sofa_out = {SEM_SOFA, 1, 0}; // 离开沙发
    semop(sem_set_id, &sb_sofa_out, 1);

    printf("[%s] 顾客 %d:坐到理发椅上,准备理发。\n", get_time_str(), pid);

    // 3. 通知理发师 (V 顾客准备好)
    struct sembuf sb_ready = {SEM_CUST_RDY, 1, 0};
    semop(sem_set_id, &sb_ready, 1);

    // 4. 等待理发完成 (P 理发完成)
    struct sembuf sb_done = {SEM_CUT_DONE, -1, 0};
    semop(sem_set_id, &sb_done, 1);

    printf("[%s] 顾客 %d:理发完毕,准备付款。\n", get_time_str(), pid);

    // 5. 付款 (P 收银台, V 收银台)
    struct sembuf sb_cash = {MUTEX_CASH, -1, 0};
    semop(sem_set_id, &sb_cash, 1);

    printf("[%s] 顾客 %d:正在付款...\n", get_time_str(), pid);
    sleep(1);

    struct sembuf sb_cash_rel = {MUTEX_CASH, 1, 0};
    semop(sem_set_id, &sb_cash_rel, 1);

    // 6. 离开 (V 理发椅)
    struct sembuf sb_chair_out = {SEM_CHAIR, 1, 0};
    semop(sem_set_id, &sb_chair_out, 1);

    printf("[%s] 顾客 %d:付款完成,离店。\n", get_time_str(), pid);

    return 0;
}
第四步:初始化脚本/程序 init.c

在运行之前,必须创建信号量集并初始化它们的值。

cpp 复制代码
/* init.c */
#include "barber.h"

int main() {
    key_t key = ftok(".", 'b');
    if (key == -1) { perror("ftok"); exit(1); }

    // 创建信号量集,共7个
    int sem_set_id = semget(key, 7, IPC_CREAT | 0666);
    if (sem_set_id == -1) { perror("semget init"); exit(1); }

    semun arg;

    // 1. 初始化互斥锁为 1
    arg.val = 1;
    semctl(sem_set_id, MUTEX_CUST, SETVAL, arg);
    semctl(sem_set_id, MUTEX_CASH, SETVAL, arg);

    // 2. 初始化资源数量
    arg.val = WAITING_ROOM; // 13
    semctl(sem_set_id, SEM_WAITING, SETVAL, arg);

    arg.val = SOFA;         // 4
    semctl(sem_set_id, SEM_SOFA, SETVAL, arg);

    arg.val = CHAIRS;       // 3
    semctl(sem_set_id, SEM_CHAIR, SETVAL, arg);

    // 3. 初始化同步信号量为 0
    arg.val = 0;
    semctl(sem_set_id, SEM_CUST_RDY, SETVAL, arg);
    semctl(sem_set_id, SEM_CUT_DONE, SETVAL, arg);

    printf(">>> 信号量初始化完成。Key: %d <<<\n", key);
    return 0;
}

编译文件

bash 复制代码
# 1. 编译初始化工具
gcc init.c -o init

# 2. 编译理发师
gcc barber.c -o barber

# 3. 编译顾客
gcc customer.c -o customer
运行步骤
  1. 初始化环境

    运行一次 init 程序来创建信号量。

    复制代码
    1./init
  1. 启动理发师

    打开一个终端,运行理发师程序(可以运行多次模拟3个理发师,或者在代码里用 fork() 创建3个子进程)。为了方便,我们在命令行启动 3 个

    复制代码
    1# 终端 1
    2./barber &
    3./barber &
    4./barber &
  2. 启动顾客

    打开另一个终端,大量启动顾客来测试并发和排队逻辑

    复制代码
    1# 终端 2
    2# 启动 30 个顾客,模拟超过总容量(20)的情况
    3for i in {1..30}; do ./customer & done

    启动后:

    bash 复制代码
    unicorn@unicorn:~/OS/OS5$ [系统] 理发店开门了,3位理发师已就位。
    ^C
    unicorn@unicorn:~/OS/OS5$ ./barber &
    ./barber &
    ./barber &
    [2] 2736
    [3] 2737
    [4] 2738
    [系统] 理发店开门了,3位理发师已就位。
    [系统] 理发店开门了,3位理发师已就位。
    unicorn@unicorn:~/OS/OS5$ [系统] 理发店开门了,3位理发师已就位。
    [16:43:58] 理发师:开始理发...
    [16:44:00] 理发师:开始理发...
    [16:44:00] 理发师:收到付款,服务结束。
    [16:44:02] 理发师:开始理发...
    [16:44:04] 理发师:收到付款,服务结束。
    [16:44:04] 理发师:开始理发...
    [16:44:06] 理发师:收到付款,服务结束。
    [16:44:06] 理发师:开始理发...
    [16:44:08] 理发师:收到付款,服务结束。
    [16:44:10] 理发师:收到付款,服务结束。
    [16:44:10] 理发师:开始理发...
    [16:44:12] 理发师:开始理发...
    [16:44:14] 理发师:收到付款,服务结束。
    [16:44:14] 理发师:开始理发...
    [16:44:16] 理发师:收到付款,服务结束。
    [16:44:16] 理发师:开始理发...
    [16:44:18] 理发师:收到付款,服务结束。
    [16:44:20] 理发师:收到付款,服务结束。
    [16:44:20] 理发师:开始理发...
    [16:44:22] 理发师:开始理发...
    [16:44:24] 理发师:收到付款,服务结束。
    [16:44:24] 理发师:开始理发...
    [16:44:26] 理发师:收到付款,服务结束。
    [16:44:26] 理发师:开始理发...
    [16:44:28] 理发师:收到付款,服务结束。
    [16:44:30] 理发师:收到付款,服务结束。
    [16:44:30] 理发师:开始理发...
    [16:44:34] 理发师:收到付款,服务结束。

问题

总结和分析示例实验和独立实验中观察到的调试和运行信息,说明您对与解决非对称性互斥操作的算法有哪些新的理解和认识?

1. 对解决非对称性互斥操作算法的新理解

在传统的互斥(如简单的二值信号量)中,进程是对等的。但在本实验的两个案例中,我们遇到了非对称性互斥

  • 读写者问题中的非对称性

    • 现象:读进程之间是共享的(不互斥),而写进程之间、读写进程之间是互斥的。
    • 理解 :为了解决这种不对称,算法引入了状态变量(如 countread_count。这不仅仅是一个计数器,它充当了"第一个进入者"和"最后一个离开者"的守门人。
    • 关键点 :我们需要两把锁:一把保护共享资源(rw_mutex),一把保护计数器本身(mutex)。这种双层锁机制是解决非对称互斥的核心------用互斥来管理共享,再用共享来优化并发
  • 睡眠理发师问题中的非对称性

    • 现象:生产者是多个顾客,消费者是多个理发师。顾客不仅生产"理发请求",还需要等待"理发完成";理发师则是被动触发。
  • 理解 :这里的非对称体现在资源的流向同步的依赖 上。顾客依赖空位,理发师依赖顾客。通过信号量 customers(顾客数)和 barbers(理发师数)的配对使用(P/V操作互补),实现了这种非对称的握手。
    为什么会出现进程饥饿现象?

2. 进程饥饿现象的分析

为什么会出现进程饥饿?

饥饿的根本原因是资源分配策略的不公平性,导致某些进程无限期地等待。

  • 在读写者问题中
    • 如果是读者优先 :只要读者源源不断地来,read_count 永远不会归零,rw_mutex 一直被读者持有。写者会因为无法获取 rw_mutex 而永远等待,导致写者饥饿
    • 如果是写者优先 :一旦有写者在等待,后续来的读者会被阻塞在入口,导致读者饥饿
  • 在理发师问题中
    • 如果顾客来得太快,沙发和等候室填满,后来的顾客直接被拒之门外(虽然这不算严格意义上的饥饿,但在高负载下也是一种服务不可达)。
  • 如果理发师处理速度极慢,顾客在队列中等待时间过长,也是一种广义的饥饿。
    本实验的饥饿现象是怎样表现的?
本实验中饥饿现象的表现
  • 读写者实验 :当你运行程序,先启动一个循环极快、不间断的读者进程 ,然后再启动写者进程。你会发现终端一直打印"读者在读...",而写者进程虽然运行了,但永远卡在等待状态,无法打印"写者正在写..."。
  • 理发师实验:如果模拟高并发的顾客(顾客生成速度远大于理发速度),且逻辑中没有公平队列(FIFO),可能会出现某些特定ID的顾客始终抢不到信号量(虽然信号量通常由内核调度保证公平,但在用户态逻辑处理不当时仍可能发生)。
    怎样解决并发进程间发生的饥饿现象?

3. 如何解决并发进程间的饥饿现象?

解决饥饿的核心思想是由"抢占式/优先级"转向"先来先服务"

  1. 读写者问题
    • 引入"写者优先"或"公平锁"机制:在读者进入前,检查是否有写者在等待。如果有写者在等待,新的读者必须在门外排队,不能进入。这样保证写者一旦获得机会,就能完成操作。
    • 使用队列:将请求按到达顺序排队,严格遵循 FIFO。
  2. 理发师问题
    • FIFO 队列:确保等候室和沙发的逻辑是先进先出的。
  • 限制并发度:如果系统过载,限制新顾客的进入速率,保护现有顾客的服务质量。
    您对于并发进程间使用消息传递解决进程通信问题有哪些新的理解和认识?

4. 对消息传递解决进程通信的新理解

通过实验(特别是读写者问题中控制进程与读/写进程通过消息队列交互),我有以下认识:

  1. 解耦同步与通信
    • 传统的信号量主要用于同步(控制顺序)。
    • 消息队列(Message Queue)不仅能同步,还能传递数据 (如请求类型 mtype)。这使得控制逻辑(Control Process)可以集中处理决策,而工作进程(Worker Process)只负责执行。
  2. 阻塞与非阻塞的灵活切换
    • msgrcvmsgsnd 默认是阻塞的。这种特性天然适合解决"睡眠理发师"问题------理发师执行 receive 时自动睡眠,有消息时自动唤醒,无需手动写 while(wait) 循环轮询,大大降低了 CPU 消耗。
  3. 集中式控制的弊端与优势
    • 示例程序使用了一个单独的控制进程来管理 count
    • 优势:逻辑集中,不易出错,易于调试。
  • 弊端:控制进程容易成为性能瓶颈(单点故障)。在分布式系统中,我们更倾向于去中心化的同步算法。
相关推荐
阳光九叶草LXGZXJ2 小时前
自制数据库迁移工具-C版-07-HappySunshineV1.6-(支持PG、达梦、Gbase8a)
linux·c语言·开发语言·数据库·学习·postgresql
剑神一笑2 小时前
深入理解 Linux gzip 压缩:从 DEFLATE 算法到实战优化
linux·运维·php
痕忆丶2 小时前
openharmony北向开发基础之OpenHarmony签名机制详解
linux·harmonyos
呉師傅2 小时前
佳能LBP251dw打印机恢复出厂设置后变成英文菜单没有中文选项如何恢复中文菜单方法
linux·运维·服务器·网络·电脑
陳10302 小时前
Linux:模拟实现进程池
linux·运维·服务器
Languorous.2 小时前
Linux 系统简介——开源世界的基石
linux·运维·开源
王翼鹏2 小时前
claude 配置Luma MCP 图像识别mcp
java·linux·服务器
minji...2 小时前
Linux 网络基础之传输层TCP(七)确认应答机制,超时重传机制,连接管理机制(三次握手四次挥手),流量控制,滑动窗口,快重传
linux·运维·服务器·网络·网络协议·tcp/ip·http
2401_858286112 小时前
OS74.【Linux】线程互斥(3) 线程安全、重入
linux·运维·服务器·开发语言·线程