操作系统实验之 Linux系统中进程的通信与同步

实验二 Linux系统中进程的通信与同步实验

一、实验目的和要求

1.理解进程间通信的概念和方法。
2.掌握常用的Linux 进程间通信的方法。
3.加强对进程同步和互斥的理解,学会使用信号蜇解决资源共享问题。
4.熟悉Linux 进程同步原语。
5. 掌握信号量wait/signal 原语的使用方法,理解信号量的定义、赋初值及w扣t/signal操作。
##二、实验设备和环境
硬件环境:HuaWei MateBook 14
软件环境:RedHat CentOS 7.0操作系统与g++编译环境

三、进程通信实验:实验内容

 编写C 程序,使用Linux 中的IPC 机制完成“石头、剪刀、布“游戏。
 修改上述C 程序,使之能够在网络上运行。

四、进程通信实验:实验步骤

针对实验内容 ,可以创建三个进程,其中一个进程为裁判进程,另外两个进程为选手进程。可将“石头” “剪刀” “布“这三招定义为三个整型值,胜负关系为:石头> 剪刀>布>石头。
选手进程按照随机产生出招,然后交给裁判进程判断大小。裁判进程将对手的出招和胜负结果通知选手。比赛采取多轮定胜负机制,并由裁判宣布最后结果。每次出招由裁判限定时间,超时则判负。
每一轮的胜负结果可以存放在文件或其他数据结构中。比赛结束后,打印每一轮的胜负情况利总的结果。
具体的实验步骤如下。
(1) 设计表示“石头” “剪刀”, 0——石头,1——剪刀,2——布
(2) 设计比赛结果的存放方式——字符数组

(3) 选择 IPC 方法。
(4) 根据所选择的 IPC 方法,创建对应的 IPC 资源。
(5) 完成选手进程。
(6) 完成裁判进程。
程序使用 fork()函数创建了两个选手进程,当前进程为裁判进程。裁判进程创建了两个消息队列,且两个选手进程会发送出拳信息至不同的消息队列,最后由裁判进程从消息队列取得出拳信息并判断结果。

五、进程通信实验:代码及注释

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#define FIFO1 "/tmp/myfifo1"
#define FIFO2 "/tmp/myfifo2"
#define SIZE 5
#define COUNT 100
int judge(char a, char b);
int main(void)
{
    int fp, fp1, fp2, i = 1;
    int status;
    int nread;
    printf("请输入进行的轮数:\n");
    int lun=0;
    scanf("%d",&lun);
    char buf[SIZE] = {0};
    char c1[COUNT] = {0}; //用来存放p1发送的消息
    char c2[COUNT] = {0}; //用来存放p2发送的消息
    pid_t p1 = fork();    //产生子进程p1
    /*********************************/
    if (p1 == 0)
    {
        srand(time(NULL));
        while ((fp = open(FIFO1, O_WRONLY | O_NONBLOCK)) == -1)
            ; //只写打开管道1,不断尝试直到成功
        for (; i <= lun; i++)
        {
            sprintf(buf, "%d", rand() % 3); //随机产生0-2的数字写入管道
            while (write(fp, buf, SIZE) == -1)
                ; //不断尝试写直至成功
        }
        close(fp);
        return 0;
    }
    /*********************************/
    pid_t p2 = fork(); //产生子进程p2,程序结构同p1
                       /*********************************/
    if (p2 == 0)
    {
        srand(time(NULL) + 100);
        while ((fp = open(FIFO2, O_WRONLY | O_NONBLOCK)) == -1)
            ;
        for (; i <= lun; i++)
        {
            sprintf(buf, "%d", rand() % 3);
            while (write(fp, buf, SIZE) == -1)
                ;
        }
        close(fp);
        exit(0);
    }

    /**************************************/
    //创建2个管道
    if ((mkfifo(FIFO1, 0777) < 0) && (errno != EEXIST))
    {
        printf("cannot create fifo.\n");
        exit(1);
    }
    if ((mkfifo(FIFO2, 0777) < 0) && (errno != EEXIST))
    {
        printf("cannot create fifo.\n");
        exit(2);
    }

    memset(buf, 0, sizeof(buf)); //清空缓冲区
    //只读方式打开两个命名管道
    while ((fp1 = open(FIFO1, O_RDONLY | O_NONBLOCK, 0)) == -1)
        ;
    while ((fp2 = open(FIFO2, O_RDONLY | O_NONBLOCK, 0)) == -1)
        ;

    sleep(3);             //等待两个子进程中打开管道写端并输入数据,必要
    for (; i <= lun; i++) //连续读取lun个数据
    {
        nread = read(fp1, buf, SIZE);
        if (nread != -1 && nread != 0)
        {
            c1[i] = buf[0]; //结果存放至c1
        }
    }
    i = 1;
    for (; i <= lun; i++)
    {
        nread = read(fp2, buf, SIZE);
        if (nread != -1 && nread != 0)
        {
            c2[i] = buf[0]; //结果存放至c2
        }
    }

    int j = 1;
    int p1w = 0, p2w = 0, pd = 0;
    for (; j <= lun; j++)
    {
        int tmp = judge(c1[j], c2[j]);
        printf("round %d:", j);
        if (tmp == 0)
        {
            printf("in a draw!\n");
            pd++; //平局
        }
        else
        {
            printf("%s wins!\n", (tmp > 0) ? "p1" : "p2");
            if (tmp > 0)
                p1w++; // p1胜
            else
                p2w++; // p2胜
        }
    }
    //打印最终统计结果
    printf("In summary:\n");
    printf("p1 wins %d  rounds.\n", p1w);
    printf("p2 wins %d  rounds.\n", p2w);
    printf("%d rounds end in a draw.\n", pd);
    printf("%s wins in the game!\n", (p1w > p2w) ? "p1" : "p2");

    //等待两个子进程结束
    if (waitpid(p1, &status, 0) < 0)
    {
        perror("waitpid");
        exit(5);
    }
    if (waitpid(p2, &status, 0) < 0)
    {
        perror("waitpid");
        exit(6);
    }
    exit(0);
}

// 0——石头,1——剪刀,2——布
int judge(char a, char b) //规定游戏判定规则
{
    int r = 0;
    if (a == b)
        r = 0;
    else
    {
        if (a == '0' && b == '1')
            r = 1;
        if (a == '0' && b == '2')
            r = -1;
        if (a == '1' && b == '2')
            r = 1;
        if (a == '1' && b == '0')
            r = -1;
        if (a == '2' && b == '0')
            r = 1;
        if (a == '2' && b == '1')
            r = -1;
    }
    return r;
}

##六、进程通信实验:实验结果

第一道题:
当输入的比赛轮数是10时

在这里插入图片描述

图2.1进程通信的运行结果_1
当输入的比赛轮数为20
在这里插入图片描述

图2.1进程通信的运行结_2

七、进程同步实验:实验内容

 编写C 程序,使用Linux 操作系统中的信号量机制模拟解决经典的进程同步问题:生产者-消费者问题。假设有一个生产者和一个消费者,缓冲区可以存放产品,生产者不断生产产品并存入缓冲区,消费者不断从缓冲区中取出产品并消费。

八、进程同步实验:实验步骤

定义同步信号量empty
定义同步信号量full
定义互斥信号量mutex
定义信号量的PV操作
给缓冲区分配内存空间
使用pthread_create创建生产者消费者进程
运行程序后,提示下用户输入内容到缓冲区中,输入完成后消费者读出缓冲区的内容输出到屏幕上

九、进程同步实验:代码及注释

#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define MAX 256
char *buffer;
sem_t empty;                   //定义同步信号量empty
sem_t full;                        //定义同步信号量full
sem_t mutex;                  //定义互斥信号量mutex
//生产者
void * producer()
{
    sem_wait(&empty);           //empty的P操作
    sem_wait(&mutex);           //mutex的P操作
    printf("input something to buffer:");
    buffer=(char *)malloc(MAX);        //给缓冲区分配内存空间
    fgets(buffer,MAX,stdin);           //输入产品至缓冲区
    sem_post(&mutex);           //mutex的V操作
    sem_post(&full);            //full的V操作
}
//消费者
void * consumer()
{
    sem_wait(&full);            //full的P操作
    sem_wait(&mutex);           //mutex的P操作
    printf("read product from buffer:%s",buffer);  //给缓冲区中读出产品
    memset(buffer,0,MAX);       //清空缓冲区
    sem_post(&mutex);           //mutex的V操作
    sem_post(&empty);           //empty的V操作
}
int main()
{
    pthread_t id_producer;
    pthread_t id_consumer;
    int i;
    sem_init(&empty,0,10);       //设置empty的初值为10
    sem_init(&full,0,0);         //设置full的初值为0
    sem_init(&mutex,0,1);        //设置mutex的初值为1
    i=pthread_create(&id_producer,NULL,producer,NULL);      //创建生产者进程
    i=pthread_create(&id_consumer,NULL,consumer,NULL);      //创建消费者进程
    pthread_join(id_producer,NULL);   //等待生产者进程结束
    pthread_join(id_consumer,NULL);   //等待消费者进程结束
    //删除信号量
    sem_destroy(&empty);        
    sem_destroy(&full);
    sem_destroy(&mutex);
    printf("The End\n");
}

十、进程同步实验:实验结果

在这里插入图片描述图2.2进程同步代码的运行结果

十一、实验思考

1、 代码中随机数的取值对于模拟“石头、剪刀、布“游戏很重要,如果取值不当,就可能出现大量平局的情况,故请思考Linux 随机数的合理取值方法。

生成随机数的方法有7种
1.通过时间获取随机数
1)date +%s (随机生成10位数字)
用于获得时间戳。

   如果用它做随机数,相同一秒的数据是一样的。在做循环处理,多线程里面基本不能满足要求了。

在这里插入图片描述

2)date +%N (随机生成9位数字)

   获得当前时间的纳秒数据,精确到亿分之一秒。

   这个在同一秒里面,也很难出现相同结果,不过不同时间里面还会有大量重复碰撞

在这里插入图片描述

   额外扩展:date +%s%N (随机生成19位数字)

在这里插入图片描述

2.通过系统环境变量($RANDOM)
echo $RANDOM (随机生成5位数字)(范围0~32767)
在这里插入图片描述

   echo “$RANDOM”|md5sum|cut -c 5-14(随机生成10个字符的字符串)

在这里插入图片描述

3.通过系统内部唯一数据生成随机数(/dev/urandom)
head /dev/urandom|cksum (9位,4位,如果用cat或more则虚拟机会自动打开打印机,cksum是用来解决乱码的)。

   cksum 将读取文件内容,生成唯一的表示整型数据,只有文件内容不变,生成结果就不会变化,与php crc函数。

在这里插入图片描述

4.通过opensssl产生随机数
rand -base64 10(表示10位字符串)。
在这里插入图片描述

   只有当位数为3的倍数时,后面才会没有等号。

5.通过UUID生成随机数
cat /proc/sys/kernel/random/uuid 或 uuidgen。

   UUID码全称是通用唯一识别码(Universally Unique Identifier,UUID)它是一个软件建构的标准,亦为自由软件基金会的组织在分布式计算环境领域的一部分;

   UUID的目的是让分布式系统中的所有元素都能有唯一的辨别信息,而不需要通过中央控制端来做辨别信息的指定,如此一来,每个人都可以创建不与其他人发生冲突的UUID,在这种情况下,就不需要考虑数据库创建时的名称重复问题了,它会让网络中任何一台计算机所生成的UUID码都是互联网整个服务器网路中唯一的编码。它的原信息会加入硬件、时间、机器当期那运行信息等。

在这里插入图片描述

   指定长度:uuidgen|md5sum|cut -c 2-10(8位长的字符串)

6.mkpaswd
mkpasswd(默认产生9位的字符串,其中数字固定位2位,特殊字符固定位1位,其余都是字母)。

   相关参数:
   -l (length of password,        default = 9) :指定密码长度;
   -d (min # of digits, default = 2) :指定密码中数字的数量;
   -c (min # of lowercase chars, default = 2) :指定密码中小写字母的数量;
   -C (min # of uppercase chars, default = 2) :指定密码中大写字母的数量;
   -s (min # of special chars, default = 1) :指定密码中特殊字符的数量;
   -p prog (program to set password, default = /bin/passwd) :程序设置密码,默认是passwd;

在这里插入图片描述

7.mktemp
/tmp/tmp.**********(10位长的字符串)。
这是一个比较特殊的随机数,它有指定的目录/tmp。
在这里插入图片描述

   如果在创建文件时引用此命令,那么它会自动在/tmp目录下产生文件。

在这里插入图片描述

2、 比较Linux 操作系统中的几种IPC 机制,并说明它们各自适用于哪些场合。

  1. UNIX早期IPC:管道、FIFO、信号;
  2. system V IPC:System V消息队列、System V信号灯、System V共享内存区;
  3. Posix IPC: Posix消息队列、Posix信号灯、Posix共享内存区;
  4. 基于socket的IPC;
    1.socket
    a、使用socket通信的方式实现起来简单,可以使用因特网域和UNIX域来实现,使用因特网域可以实现不同主机之间的进出通信。
    b、该方式自身携带同步机制,不需要额外的方式来辅助实现同步。
    c、随进程持续。
    2.共享内存
    a、最快的一种通信方式,多个进程可同时访问同一片内存空间,相对其他方式来说具有更少的数据拷贝,效率较高。
    b、需要结合信号灯或其他方式来实现多个进程间同步,自身不具备同步机制。
    c、随内核持续,相比于随进程持续生命力更强。
    3.管道
    a、较早的一种通信方式,缺点明显:只能用于有亲缘关系进程之间的通信;只支持单向数据流,如果要双向通信需要多创建一个管道来实现。
    b、自身具备同步机制。
    c、随进程持续。
    4.FIFO
    a、是有名管道,所以支持没有亲缘关系的进程通信。和共享内存类似,提供一个路径名字将各个无亲缘关系的进程关联起来。但是也需要创建两个描述符来实现双向通信。
    b、自身具备同步机制。
    c、随进程持续。
    5.信号
    a、这种通信可携带的信息极少。不适合需要经常携带数据的通信。
    b、不具备同步机制,类似于中断,什么时候产生信号,进程是不知道的。
    6.消息队列
    a、与共享内存和FIFO类似,使用一个路径名来实现各个无亲缘关系进程之间的通信。消息队列相比于其他方式有很多优点:它提供有格式的字节流,减少了开发人员的工作量;消息具有类型(system V)或优先级(posix)。其他方式都没有这些优点。
    b、具备同步机制。
    c、随内核持续。

3、本实验只模拟实现了一个产品的放入与取出,请修改代码,以模拟实现多个产品的放入与取出。

适用

  1. 管道:只能用于具有亲缘关系的进行通信,使用面相对较窄,实际开发中较少使用;

  2. FIFO(命名管道):可以用于任意进程间的通信,对于大块数据的传输效率较高,可应用于单进程大量数据传递,和多个进程向一个进程传递数据;

  3. 信号:无法传递数据,而且信号的种类有限,只适用于完成一些简单的事件通知任务,如配置跟新信号通知,一个服务通过信号告知另一个服务自身状态;

  4. 共享内存:最为高效的进程间通信方式,进程可以直接读写内存,不需要任何数据拷贝,适用于多个进程共享数据,或进程间频繁的进行大量的数据交互;–建议使用mmap方式;

  5. 消息队列:进程间传递简单的命令和控制消息,如配置更新通知,多进程对多进程的通信等,可以简化代码逻辑;–建议使用全双工管道替代;

  6. 信号量:某种资源数为N,多个进程都在使用该资源,为了进行进程间的互斥,可以使用初始值为N的信号量;–建议使用记录锁替代;

  7. 多线程并发与多进程并发有何不同与相同之处?

多进程并发与多线程并发的区别主要在有没有共享数据,多进程间的通信较复杂且代价较大,主要的进程间通信渠道有管道、信号、文件、套接字等。

多线程并发只是表面和感觉上的并发,并不是实质上的并发。一个线程要运行,它必须占有CPU,而我们目前用的计算机大多都是单CPU的,所以一次最多只能有一个线程获取CPU并运行。多线程的实质是“最大限度地利用CPU资源”,当某一个线程的处理不需要占用CPU而只需要和I/O等资源打交道时,让其他线程有机会获得CPU资源。虽然CPU只有一个,但是它在多个线程之间频繁切换,当切换的频率高到一定程度时,我们就感觉所有的线程在同时运行,于是感觉这多个线程是并发的。

多进程并发是将应用程序分为多个、独立的、单纯和的进程,它们运行在同一时刻,就像你可以同时进行网页浏览和文字处理,这些独立的进程可以通过所有常规的进程间通信渠道互相传递信息(信号、套接字、文件、管道等)。并使用独立的进程实现并发,可以在网络连接的不同的机器上运行独立的进程,虽然这增加了通信成本,但在一个精心设计的系统上,这可能是一个提高并行可用行和提高性能的低成本方法。
5. 模拟实现读者-写者问题。

模拟读者写者问题:

利用多线程模拟读者与写者对临界区的互斥访问。读者和写者问题的读写操作限制:可以有一个或多个数量的读进程同时读这个文件;一次只有一个写进程可以写文件;若一个写进程正在写文件,则禁止任何读进程读文件;即读读允许、读写互斥、写写互斥。

在读者写者问题中,使用读者优先和写者优两种解决方案。在读者优先策略中,读进程具有优先权,也就是说,当至少有一个读进程在读时,随后的读进程就无需等待,可以直接开始读操作,此过程中写进程可能会饥饿。在写者优先策略中,写进程具有优先权,写者优先与读者优先类似。不同之处在于一旦一个写者到来,它应该尽快对文件进行写操作,如果有一个写者在等待,则新到来的读者不允许进行读操作,读者必须等到没有写者处于等待状态才能开始读操作。

算法描述:

读者优先描述

如果读者来:

  1. 无读者、写着,新读者可以读;

  2. 无写者等待,但有其他读者正在读,新读者可以读;

  3. 有写者等待,但有其他读者正在读,新读者可以读;

  4. 有写者写,新读者等

如果写者来:

  1. 无读者,新写者可以写;

  2. 有读者,新写者等待;

  3. 有其他写者写或等待,新写者等待

写者优先描述

如果读者来:

  1. 无读者、写者,新读者可以读;

  2. 无写者等待,但有其他读者正在读,新读者可以读;

  3. 有写者等待,但有其他读者正在读,新读者等;

  4. 有写者写,新读者等

如果写者来:

  1. 无读者,新写者可以写;

  2. 有读者,新写者等待;

  3. 有其他写者或等待,新写者等待

信号量和互斥锁的区别

互斥量用于线程的互斥,信号量用于线程的同步。

这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

互斥量值只能为0/1,信号量值可以为非负整数。

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

读者优先

使用互斥锁来确保同一时间只能一个进程写文件,实现互斥。使用信号量来实现访问资源的同步。

首先,写者的代码应该是这样一种形式,才能保证同一时刻只有一个写者修改数据。

考虑到写者对读者的影响是:当任何读者想读时,写者都必须被阻塞;并且,读者阻塞了写者并停止阻塞之前,后续的任何写者都会读者优先于执行。这就如同有一个读者队列,当第一个读者入队时,写者完全被阻塞,直到最后一个读者离开队列。

据此,可以用 readerCnt来统计读者的数量,而用信号量 sem_read来互斥各线程对 readerCnt的访问。

### 使用信号量实现进程同步Linux环境中,可以利用POSIX信号量来管理多个进程之间的同步问题。下面是一个简单的例子,展示了两个子进程如何使用命名信号量来进行同步操作。 #### 创建并初始化信号量 为了使不同进程能够访问同一个信号量对象,在这里选择了创建一个命名型的信号量(`sem_open()`函数)。此函数接受两个参数:一个是表示该信号量名称字符串;另一个是指定初始计数值以及权限设置等选项结构体指针[^1]。 ```c #include <fcntl.h> #include <sys/stat.h> /* For mode constants */ #include <semaphore.h> // 定义信号量名 #define SEM_NAME "/my_semaphore" int main() { sem_t *sem; // 打开或创建一个新的有名信号量, 初始值设为0 sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, S_IRUSR | S_IWUSR, 0); } ``` #### 子进程A等待信号量 当第一个子进程启动后会调用`sem_wait()`阻塞自己直到其他某个时刻有第二个子进程执行了相应的`sem_post()`增加信号量计数器的操作为止。这一步骤确保了只有当前面的任务完成之后才会继续向下执行后续逻辑[^2]。 ```c pid_t pid; if ((pid = fork()) == 0) { // Child process A printf("Child Process A is waiting...\n"); sem_wait(sem); // Wait until semaphore value becomes positive // Critical section starts here... printf("Child Process A enters critical section.\n"); sleep(2); // End of the critical section. printf("Child Process A leaves critical section.\n"); exit(EXIT_SUCCESS); } else if (pid > 0){ wait(NULL); // Parent waits for child to finish its work before proceeding with next steps } ``` #### 子进程B发信号量通知 此同时,另外一个独立运行着的子进程会在适当时候通过调用`sem_post()`方法向上述提到的那个共享资源发出释放请求从而唤醒处于休眠状态下的前序任务去处理接下来的工作流. ```c if ((pid = fork()) == 0){ // Child process B sleep(1); // Simulate some delay or computation time. // Signal that it's okay now for another process to enter into their critical sections by posting on our named semaphore. printf("Child Process B posts a signal\n"); sem_post(sem); exit(EXIT_SUCCESS); } // Clean up resources when done using them. unlink(SEM_NAME); exit(EXIT_SUCCESS); ``` 以上就是基于Linux环境下采用C语言编写的简单案例说明了怎样借助于信号量机制达到多线程之间安全有序地交互目的。需要注意的是实际应用当中还需要考虑更多边界情况比如错误检测等方面的内容以保证程序健壮性和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@小冯@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值