进程的同步与互斥

记录于2022.7.7——南林操作系统课设心得

ps:这个实验我写了三天!!!!!!一是因为要参加夏令营和一些学校的宣讲会,二是因为网上大部分关于生产者消费者问题都是用线程实现的,用进程实现比较困难(因为进程比较难实现对信号量和缓冲区等变量的共享,只能通过共享内存来实现,而想通过共享内存来实现对这些变量的共享,要调用一些系统函数,网上关于这些系统函数的介绍又非常少,在我查阅了大量资料后,终于实现了用进程完成生产者消费者问题!!!)


<任务>

用程序实现生产者——消费者问题

问题描述:

  一个仓库可以存放K件物品。生产者每生产一件产品,将产品放入仓库,仓库满了就停止生产。消费者每次从仓库中去一件物品,然后进行消费,仓库空时就停止消费。

数据结构:

进程:Producer - 生产者进程,Consumer - 消费者进程

       buffer: array [0..k-1] of integer;

         in,out: 0..k-1;

       in记录第一个空缓冲区,out记录第一个不空的缓冲区

         s1,s2,mutex: semaphore;

       s1控制缓冲区不满,s2控制缓冲区不空,mutex保护临界区;

       初始化s1=k,s2=0,mutex=1

原语描述

producer(生产者进程):

   Item_Type item;

  {

     while (true)

     {

       produce(&item);

       p(s1);

       p(mutex);

       buffer[in]:=item;

       in:=(in+1) mod k;

       v(mutex);

       v(s2);

     }

  }

  consumer(消费者进程):

   Item_Type item;

  {

     while (true)

     {

       p(s2);

       p(mutex);

        item:=buffer[out];

        out:=(out+1) mod k;

        v(mutex);

        v(s1);

     }

   }

#include <iostream>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* KEY 为申请信号量的键值,通过键值可以用于不同进程间的通信 */
#define KEY (key_t)14010322
/* 仓库大小 */
#define K 5

/*
        union semun 常用于semctl的最后一个参数,
        有的系统上sem.h已包含,可能会因为重复而报错
 */
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
 /*   如果union semun已经存在   */
#else
 /*   如果不存在   */
union semun {
    int val;
    struct semid_ds* buf;
    unsigned short* array;
};
#endif

/*
 仓库,buffer数组用于存贮生产的商品编号(product)
 in用于记录生产者生产的下一个商品应存贮在仓库的位置
 out用于记录消费者消费的下一个商品在仓库中的位置
 */
typedef struct ShareBuffer {
    int buffer[K];
    // in记录第一个空缓冲区,out记录第一个不空的缓冲区
    int in;
    int out;
}ShareBuffer;

pid_t producer, consumer;

/*
struct sembuf{
    short sem_num;
    short sem_op;
    short sem_flg;
};
sem_num是信号量的编号,如果你的工作不需要使用一组信号量,这个值一般就取为0。
sem_op是信号量一次PV操作时加减的数值,一般只会用到两个值,一个是“-1”,也就是P操作,
等待信号量变得可用;另一个是“+1”,也就是我们的V操作,发出信号量已经变得可用
sem_flag通常被设置为SEM_UNDO.她将使操作系统跟踪当前进程对该信号量的修改情况
*/

/*
  int semop(int semid, struct sembuf *sops, unsigned nsops);
    功能描述:
    操作一个或一组信号。
  参数:
    semid :信号集的识别码,可通过 semget 获取。
    sops :指向存储信号操作结构的数组指针,信号操作结构的原型如下
        nsops :信号操作结构的数量,恒大于或等于 1 。
*/

/* p操作 */
void p(int semid, int semNum) {
    struct sembuf sb;
    sb.sem_num = semNum;
    sb.sem_op = -1;
    sb.sem_flg = SEM_UNDO;
    semop(semid, &sb, 1);
}

/* v操作 */
void v(int semid, int semNum) {
    struct sembuf sb;
    sb.sem_num = semNum;
    sb.sem_op = 1;
    sb.sem_flg = SEM_UNDO;
    semop(semid, &sb, 1);
}

// 随机生产一个1~100的元素
void produce(int& product) { 
    product = rand() % 100 + 1;
}

void killChildren(int sig) {
    // 杀死子进程1,放出信号SIGUSR1
    kill(producer, SIGUSR1);
    // 杀死子进程2,放出信号SIGUSR2
    kill(consumer, SIGUSR2);

    // 父进程结束
    std::cout << "Parent process is exit!" << std::endl;
}

int main()
{
    int shmid;                          // 共享内存的id
    char* shmPtr;                       // 信号量指针
    int semid;
    ShareBuffer* pSharebuffer;          // 共享内存的指针
    srand((unsigned)time(NULL));        // 初始化随机数种子

    /*
        int shmget(key_t key, size_t size, int shmflg);
        
        函数说明——创建/打开共享内存
        key——共享内存名称(ftok()函数获取)
        size——开辟共享内存大小(以字节计)
        shmflg——权限标志
        返回值——成功返回共享内存的标识符。错误返回-1
    */
    /* 创建共享内存 */
    if ((semid = semget(KEY, 3, IPC_CREAT | 0660)) == -1)
    {
        std::cout << "semget error!" << std::endl;
        return -1;
    }

    /* 三个信号量的初始赋值 */
    // s1控制缓冲区不满, s2控制缓冲区不空, mutex保护临界区;
    union semun s1, s2, mutex;
    s1.val = K;
    s2.val = 0;
    mutex.val = 1;

    /*
        int semctl(int semid, int semnum, int cmd, union semun arg)
        功能:控制信号量的信息。
        返回值:成功返回0,失败返回-1;
        参数:
             semid   信号量集标识符
             semnum,  操作信号在信号集中的编号。从0开始。
             cmd    命令,表示要进行的操作(SETVAL设置信号量集中的一个单独的信号量的值。)
    */
    // 定义信号量集
    if (semctl(semid, 0, SETVAL, s1) == -1) {       // s1编号为0
        std::cout << "semctl failed!" << std::endl;
        return -1;
    }
    if (semctl(semid, 1, SETVAL, s2) == -1) {       // s2编号为1
        std::cout << "semctl failed!" << std::endl;
        return -1;
    }
    if (semctl(semid, 2, SETVAL, mutex) == -1) {    // mutex编号为2
        std::cout << "semctl failed!" << std::endl;
        return -1;
    }

    /*
        int shmget(key_t key, size_t size, int shmflg);

        函数说明——创建/打开共享内存
        key——共享内存名称(ftok()函数获取)
        size——开辟共享内存大小
        shmflg——权限标志
        返回值——成功返回共享内存的标识符。错误返回-1
    */
    // 此共享内存的权限为可读可写
    if ((shmid = shmget(IPC_PRIVATE, sizeof(ShareBuffer), IPC_CREAT | 0600)) < 0)
    {
        std::cout << "shmget error!" << std::endl;
        return -1;
    }

    /*
        void *shmat(int shmid, const void *shmaddr, int shmflg);

        函数说明——将共享内存与当前进程相关联
        shmid——共享内存的标识符(shmget的返回值)
        shmaddr——使用0(内核选择存储地址)
        shmflg —— 使用0 (内存空间可读可写)
        返回值——成功返回该共享内存的首地址。失败返回-1,错误代码在errno
    */
    // 将共享内存与当前进程相关联
    if ((shmPtr = (char*)shmat(shmid, 0, 0)) == (void*)-1)
    {
        std::cout << "shmat error!" << std::endl;
        return -1;
    }

    /*
        void * memset ( void * ptr, int value, size_t num );
        memset() 函数用来将指定内存的前num个字节设置为特定的值
    */
    memset((void*)shmPtr, 0, sizeof(ShareBuffer));
    // 通过强制转换,来操控共享内存
    pSharebuffer = (ShareBuffer*)shmPtr;    

    // 创建进程
    while ((producer = fork()) == -1);

    /* 子进程,生产者 */
    if (producer == 0) {
        // SIGUSR1为用户自定义信号
        signal(SIGUSR1, NULL);

        while (true)
        {
            int product;
            produce(product);
            p(semid, 0);        // P(empty)
            p(semid, 2);        // P(mutex)

            pSharebuffer->buffer[pSharebuffer->in] = product;
            std::cout << "Producer process put element " << product << " in subscript " << pSharebuffer->in << std::endl;
            pSharebuffer->in = (pSharebuffer->in + 1) % K;

            v(semid, 2);        // V(mutex)
            v(semid, 1);        // V(full)
            // 睡眠10000~50000微秒(10~50毫秒)
            int sleepTime = rand() % 40000 + 10000;
            usleep(sleepTime);
        }
    }
    else {
        while ((consumer = fork()) == -1);

        /* 子进程,消费者 */
        if (consumer == 0) {
            // SIGUSR2为用户自定义信号
            signal(SIGUSR2, NULL);

            while (true) {
                int product;
                p(semid, 1);    // P(full)
                p(semid, 2);    // P(mutex)

                product = pSharebuffer->buffer[pSharebuffer->out];
                std::cout << "Consumer process get element " << product << " from subscript " << pSharebuffer->out << std::endl;
                pSharebuffer->out = (pSharebuffer->out + 1) % K;

                v(semid, 2);    // V(mutex)
                v(semid, 0);    // V(empty)
                // 睡眠10000~50000微秒(10~50毫秒)
                int sleepTime = rand() % 40000 + 10000;
                usleep(sleepTime);
            }
        }
        else {
            // 在指定闹钟时间到期后执行
            signal(SIGALRM, killChildren);
            // 定时1秒
            alarm(1);

            // 等待子进程结束
            wait(NULL);
            wait(NULL);

            //去关联共享内存
            shmdt(shmPtr);
            
            // 删除信号量
            // IPC_RMID 从内核中删除信号量集合
            semctl(semid, 0, IPC_RMID, s1);
            semctl(semid, 0, IPC_RMID, s2);
            semctl(semid, 0, IPC_RMID, mutex);
        }
    }

    return 0;
}

结果:

说明:为了能使进程之间共享信号量与缓冲区,s1、s2、mutex采用信号量集来实现,缓冲区采用共享内存来实现;为了体现出生产者和消费者进程之间的差异,给生产者与消费者进程设置随机睡眠时间。

        当生产者进程生产一个商品时,首先利用信号量in来判断当前是否有空缓冲区,若没用,则生产者进程被阻塞,直到消费者进程释放出一个空缓冲区,生产者进程才从阻塞状态变为就绪状态;若当前存在空缓冲区,则消耗一个空缓冲区,通过互斥信号量mutex对缓冲区进行上锁,然后将生产的商品放入缓冲区中,然后释放mutex,并增加一个满缓冲区。

        当生产者进程生产一个商品时,首先利用信号量out来判断当前是否有满缓冲区,若没用,则消费者进程被阻塞,直到生产者进程产生一个满缓冲区,消费者进程才从阻塞状态变为就绪状态;若当前存在满缓冲区,则消耗一个满缓冲区,并通过互斥信号量mutex对缓冲区进行上锁,从满缓冲区中获得商品,然后释放mutex,并增加一个空缓冲区。

       由于缓冲区是通过循环队列来实现,所以增加s1、s2必须对K取余。

 


难点在于如何实现进程间对信号量以及缓冲区的共享,以及如何进行P、V操作,实现了这些,之后就没什么难度了。

至于为什么使用union semun,因为P、V操作里需要用到,调用系统函数就是这么奇奇怪怪,有兴趣的朋友可以深入研究一下。

定义ShareBuffer结构体,是为了方便对缓冲区以及in、out指针进行共享(可以理解为捆绑在一起,方便操作)。

另外,这里使用signal()等来终止子进程,可以看我另一篇博客:

进程的软中断通信_陈阿土i的博客-CSDN博客

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈阿土i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值