Linux - System v 进程间通信

实际上这也是进程之间的两种关系,在学习这两种关系之前,需要回顾一下顺序程序与并发程序的特征:

顺序程序特征

①、顺序性

顺序程序执行的顺序是按照指令的先后顺序来执行的,当前的指令需依赖于前一条指令,并与前一条指令构成了一定的因果关系,后一条指令的执行一定要在前一条指令的基础之上才能够运行。

②、封闭性:(运行环境的封闭性)

也就是说顺序程序在运行过程中,它的运行环境不会受其它程序的影响。

③、确定性

只要给程序一定的输入,不管程序是运行在比较快的机器上,还是运行在比较慢的机器上,它一定会有特定的输出。

④、可再现性

一个程序在一定时期运行的结果,跟另外一个时期运行的结果可以是一样的,只要是具有相同的输入,就一定具有相同的输出,这跟"确定性"是很有关系。

并发程序特征【就没有以上顺序程序的特征了】

①、共享性

②、并发性

③、随机性

下面来正式理解进程同步与进程互斥:

如两个小孩争抢同一个玩具,这是一种互斥关系。

下面来举一个同步的示例:汽车售票

通常情况下,将这两种关系统称为同步关系。

①、数据传输:一个进程需要将它的数据发送给另一个进程

②、资源共享:多个进程之间共享同样的资源。

③、通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。【如:上面说的汽车售票的例子】

④、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。【可以通过信号的方式来实现,如:SIGTRAP信号】

①、管道

其中匿名管道可以用于亲缘关系的进程之间进行通信,而有名管道可以用于不相关的进程之间进行通信。

②、System V进程间通信【使用最广泛】

这个是之后发展出来的,之后还会介绍。

③、POSIX进程间通信

①、文件

这个其实也是进程间通信的一种试,一个进程向一个文件写数据,而另外一个进程向一个文件读数据。

②、文件锁:是为了互斥和同步用的

③、管道(pipe)和有名管理(FIFO)

④、信号(signal)

⑤、消息队列

⑥、共享内存

⑦、信号量

其中System V包含:消息队列、共享内存、信号量

⑧、互斥量

⑨、条件变量

⑩、读写锁

⑪、套接字(socket)

下面来介绍一下System V IPC & POSIX IPC:

由于这三种共享方式,就出现了三种进程间通信对象的持续性,如下:

①、随进程持续:一直存在直到打开的最后一个进程结束。(如pipe和FIFO)

②、随内核持续:一直存在直到内核自举或显式删除(如System V消息队列、共享内存、信号量)

③、随文件系统持续:一直存在直到显式删除,即使内核自举还存在。(POSIX消息队列、共享内存、信号量如果是使用映射文件来实现)

在上面已经介绍了进程的两种关系:互斥和同步,死锁则是进程的另外一种关系:

死锁是指多个进程之间相互等待对方的资源,而在得到对方资源之前又不释放自己的资源,这样,造成循环等待的一种现象。如果所有进程都在等待一个不可能发生的事,则进程就死锁了。

①、资源一次性分配:(破坏请求和保持条件)

②、可剥夺资源:破坏不可剥夺条件)

③、资源有序分配法:(破坏循环等待条件)

①、预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。

②、由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。

下面来对死锁进行举例,其中最具有代表性的避免死锁算法是银行家算法。

为保证资金的安全,银行家规定:

(1) 当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;

(2) 顾客可以分期贷款,但贷款的总数不能超过最大需求量

(3) 当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款

(4) 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.

另外还有一个很经典的例子:哲学家就餐问题

1965年,Dijkstra(迪杰斯特拉)提出并解决了一个他称之为哲学家就餐的同步问题。从那时起,每个发明新的同步原语的人都希望通过解决哲学家就餐问题来展示其同步原语的精妙之处。这个问题可以简单地描述如下:五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘通心粉。由于通心粉很滑,所以需要两把叉子才能夹住。相邻两个盘子之间放有一把叉子,餐桌如图2-44所示。

哲学家的生活中有两种交替活动时段:即吃饭和思考(这只是一种抽象,即对哲学家而言其他活动都无关紧要)。当一个哲学家觉得饿了时,他就试图分两次去取其左边和右边的叉子,每次拿一把,但不分次序。如果成功地得到了两把叉子,就开始吃饭,吃完后放下叉子继续思考。关键问题是:能为每一个哲学家写一段描述其行为的程序,且决不会死锁吗?(要求拿两把叉子是人为规定的,我们也可以将意大利面条换成中国菜,用米饭代替通心粉,用筷子代替叉子。)

哲学家就餐问题解法:

①、服务生解法

哲学家在拿刀叉之前,需要得到服务生的同意,也就是服务生是管理者,他在统一的分配刀叉,他在判定当前的资源是否处于一个安全的状态,如果资源处于一个安全的状态,服务生就允许哲学家将叉子拿起,否则就不允许。

②、最多4个哲学家

这不是解决问题的最好方案,因为将我们的限定条件更改了,4个哲学家有5把叉子势必有一个哲学家能得到两把叉子,这实际上就是一种抽屉原则。

③、仅当一个哲学家两边叉子都可用时才允许他拿叉子

④、给所有哲学家编号,奇数号的哲学家必须首先拿左边的筷子,偶数号的哲学家则反之

信号量和P、V原语由Dijkstra(迪杰斯特拉)提出,其中他有很多贡献:

①、在程序设计中,提出了goto语句是有害的,所以可以认为他是程序设计语言之父。

②、在操作系统,提出了信号量、PV原语。

③、在网络上,提出了最短路径。

根据上面的描述,很容易得到信号量它所拥有的数据结构,如下:

另外还可以得出PV原语的伪代码:

用PV原语主要是来解决进程的同步互斥的问题,所以,下面举例来说明:

有一汽车租赁公司有两部敞篷车可以出租,假定同时来了四个顾客都要租敞篷车,那么肯定会有两个人租不到:

用一个简单的图来描述一下:

另外需要注意:必须是同类的资源才能够进行PV操作,如果一部是敞篷车,一部是普通的汽车,是不能够进行PV来解决同步问题的。

其实还可以通过管道,但是,管道是基于字节流的,所以通常会将它称为流管道,数据与数据之间是没有边界的;而消息队列是基于消息的,数据与数据之间是有边界的,这是消息队列跟管道有区别的地方,另外一个差别就是在于接收:消息队列在接收是不一定按先入先出,而管道一定是按照先入先出的原则来进行接收的。

关于这些,可以通过命令来查看其值,如下:

上次提到过,System_V IPC对象有三种,如下:

这些IPC对象都是随内核持续的,也就是说当访问这些对象的最后一个进程结束时候,内核也不会自动删除这些对象,直到我们显示删除这些对象才能够从内核中删除掉,所以说内核必须为每个IPC对象维护一个数据结构,其形式如下:

下面来看下消息队列的具体结构是怎么样的:

这里先学前两个函数:

下面则用代码来学习一下该函数:

从图中可以看出创建失败了,这是为什么呢?这时可以查看其帮助:

实际上msgget函数类似于open函数一样,如果在open一个文件时没有指定O_CREATE选项,则不能够创建一个文件,同样的:

如果创建失败,则会返回:

所以修改代码如下:

那创建成功的消息队列怎么查看呢?可以通过如下命令:

如果再次运行呢?

其错误代码是:

可见每运行一次则就创建成功一个新的消息队列,而且key都是为0,这意味着两个进程就无法共享同一个消息队列了,但是同一个进程还是可以共享的,其实也可以有一个办法达到两个不同进程进行共享,就是将消息队列id保存到文件当中,另一个进程读取消息队列id来获得消息队列,这样也能实现共享,只是麻烦一些,这里就不多赘述了。另外如果key值为IPC_PRIVATE,那么没有IPC_CREATE选项也一样会创建成功,如下:

另外一旦一个消息队列创建成功之后,如果要打开一个消息队列,这时候就不用指定IPC_CREATE了,而且参数值可以直接填0:

接下来删除一些已经创建的消息队列,有两种方式:

那如果像这剩下key全为0的,用这种方式还能起作用么,咱们来试一下:

下面来说一个权限的问题:

下面来以600更高的权限来打开刚才创建低权限的消息队列:

那有木有一种办法,在打开消息队列时,就以原创建的权限打开,当然有,也就是打开时不指定权限既可,如下:

上面演示了各种msgget创建用法,下面来用图来总结一下各个情况:

接下来学习一下消息队列的按制函数,如下:

上面已经用命令知道怎么删除已经创建的消息队列了,下面采用代码来实现消息队列的删除:

接下来来获取消息队列的信息,这时需要就需要关注第三个参数了,man查看一下:

而其中ipc_perm结构体内容如下:

下面来更改一下消息队列的状态,将权限666改为600,具体做法如下:

【说明】:上图中用到了sscanf函数来将一个指定的字符串赋值给变量,对于scanf函数大家应该都不陌生,它是从标准输入中赋值变量,而sscanf是将指定的字符串按指定的格式赋给变量,两者的唯一区别就是数据的来源变了,很容易理解。

编译运行:

下面则开始用代码来使用一下该发送函数:

在运行之前,先查看一下1234消息队列是否已经创建:

用上次编写的查看消息队列状态的程序来查看一下此时的状态:

接下来运行发送消息程序:

接下来再来发送一个消息:

目前发送的字节总数为300,还没有超过最大字节数msgmnb=16384,下面来看下如果超过了这个字节数,会怎么样?所以继续发送消息:

这是由于每条消息最大长度是有上限的(MSGMAX),它的上线就等于8192:

这已经在上节中介绍过,所以,将上面的8193改为8192,就不会发送失败了,如下:

发送这时阻塞了,这是由于原有的消息队列中的总字节数8492+要发送的8192已经大于16384(消息队列总的字节数),默认就会阻塞,也就是发送还是没成功,查看一下状态既可知道:

这时可以指定一个发送选项来更改阻塞行为:

可见就不会阻塞了,返回EAGAIN错误了。

其中最主要是要了解第四个参数,msgtyp,如下:

下面用程序来验证下,在运行这个程序时,可以这样使用:

要实现这样的功能,需要用到getopt函数,所以首先需要通过该函数来解析命令参数,下面先来熟悉一下该函数,其实不是太难:

下面运行一下:

下面来解析-n-t选项,修改代码如下:

关于getopt函数的使用基本就这些,还是比较简单,下面则正式开始编写消息的接收功能:

msg_recv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

&emsp;struct msgbuf {
&emsp;&emsp;&emsp;long mtype; /* message type, must be > 0 */
&emsp;&emsp;&emsp;char mtext[1]; /* message data */
&emsp;};

#define MSGMAX 8192//定义一个宏,表示一条消息的最大字节数
int main(int argc, char *argv[])
{
    int flag = 0;
    int type = 0;
    int opt;

    while (1)
    {
        opt = getopt(argc, argv, "nt:");
        if (opt == '?')
            exit(EXIT_FAILURE);

        if (opt == -1)
            break;
        
        switch (opt)
        {
        case 'n':
            /*printf("AAAA\n");*/
            flag |= IPC_NOWAIT;
            break;
        case 't':
            /*
            printf("BBBB\n");
            int n = atoi(optarg);
            printf("n=%d\n", n);
            */
            type = atoi(optarg);
            break;
        }
    }

    int msgid;
    msgid = msgget(1234, 0);
    if (msgid == -1)
        ERR_EXIT("msgget");

    struct msgbuf *ptr;
    ptr = (struct msgbuf*)malloc(sizeof(long) + MSGMAX);
    ptr->mtype = type;
    int n = 0;
    if ((n = msgrcv(msgid, ptr, MSGMAX, type, flag)) < 0)//接收消息
        ERR_EXIT("msgsnd");

    printf("read %d bytes type=%ld\n", n, ptr->mtype);

    return 0;
}
复制代码

下面再来发送一些消息:

下面来验证一下当消息类型为负数时的情况,先清空消息:

当消息类型为负时,还有一个特点,如下:

下面来验证一下,重新发送几个消息,并来分析接收的顺序:

默认情况下每条消息最大长度是有上限的(MSGMAX),它的上线就等于8192,当发送消息超过这个大小时,则会报错,上面也已经论证过:

但是如果将msgflg设置成MSG_NOERROR,消息超过时,是不会报错的,只是消息会被截断。

下面用一个示意图来表示其实现原理:

那么服务器端是如何区分消息是发送给不同的客户端的呢?很自然想到的就是用类型进行区分,给不同客户端发送的是不同类型的消息,客户端则接收对应类型的消息,那这个类型用什么标识不同的客户端呢?进程的pid则是一个很好的类型方案,如下:

首先实现服务器端:

其中服务器要干的事,就是不断地接收类型为1的消息,并且将其消息回射给不同的客户端,所以下面来实现一下:

然后取出前4个字节,代表客户端的进程ID,如下:

接下来则将真正的内容打印出来,并且将其回射给客户端:

这样服务端就编写好了,接下来编写客户端,其实现跟服务器很类似:

首先是不断地从键盘中获取数据,发送给服务器:

接下来,则是处理从服务器回射回来的数据,其写法跟服务端的很类似:

【说明】:为啥不清空前四个字节,如果清空了则下次发送消息时又得存,所以为了简单,就不清空了。

下面来发送消息:

从以上实验结果来看,回射功能是没问题,但是,服务器的回显只打印了一条消息,这看样子是个小bug,下面来解决一下:

而主要原因是由于服务器回显给客户端时造成type的改变,如下:

所以在客户端每次循环时,就得每次都指定一下msg.mtype = 1,否则,实际上客户端是自已发给自己,就造成了客户端有回显,而服务端没有,改变之后,再次运行:

发现服务器的消息显示还有些问题,原因其实很简单,就是由于没有清空之前数据造成,修改如下:

实际上,这里还存在一种隐含的问题,会产生“死锁”现象,下面来分析一下产生的原因:

避免死锁的方案需要采用另外一种实现方式,这里只是探讨一下,可以自己去实现:

1、用管道或者消息队列传递数据

这个示意图的功能是服务器向客户端传输文件,如下:

①、首先要将文件从内核读取到进程的用户空间当中,所以这里就涉及到了一次read()系统调用。

②、服务器需要将读取到的数据拷贝到管道或消息队列当中,涉及到第二次系统调用。

③、对于客户端来说,需要从管道或消息队列中读取这些数据,涉及到第三次系统调用。

④、读到这些数据到应用的数据缓冲区当中,然后将缓冲区的内容写到输出文件中,涉及到第四次系统调查用。

从以上步骤来看,总共涉及到了四次系统调用, 四次内存拷贝(从内核空间到用户空间),那共享内容方式又如何呢?

2、用共享内存传递数据

下面来学习一下相关函数的使用:

下面来看一下内存映射文件示意图:

下面则用代码来实践一下:

下面来创建一个文件:

接下来对文件进行映射:

当映射成功之后,接下来则往文件中写入一些数据,这时候就可以直接通过指针写入了,对文件的操作就好像对内存的访问,如下:

可见就通过内存的方式来对文件进行了数据写入,这就是内存映射文件的作用。

下面来写一个读取文件内容的功能,将写入的五个同学的数据读出来,基于mmap_write.c来写,代码差不多:

mmap_read.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)


typedef struct stu
{
    char name[4];
    int age;
} STU;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int fd;
    fd = open(argv[1], O_RDWR);
    if (fd == -1)
        ERR_EXIT("open");

    STU *p;
    p = (STU*)mmap(NULL, sizeof(STU)*5, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == NULL)
        ERR_EXIT("mmap");

    //从映射内存中,来读取文件的内容
    int i;
    for (i=0; i<5; i++)
    {
        printf("name = %s age = %d\n", (p+i)->name, (p+i)->age);
    }

    munmap(p, sizeof(STU)*5);
    printf("exit ...\n");

    return 0;
}
复制代码

对共享内存进行写操作的时候:

实际上,这些操作并没有立刻写回到文件当中,内核也会选择一个比较好的时机将这些内容写入到文件当中,如果我们想要立刻写回到文件当中,则就可以用msync函数了。

①、映射不能改变文件的大小

下面来修改一下程序来验证一下:

②、可用于进程间通信的有效地址空间不完全受限于被映射文件的大小

mmap在映射的时候,是将其映射到一个内存页面当中,也就是说,我们可以通讯的区域是以内存页面为单位的,比如我们映射了40个字节,但是内存页面肯定是大于40个字节的,所以能够通信的有效地址空间肯定是超过了40个字节,下面也用程序来说明一下:

在写进程还没结束时,再快速地运行读进程时,从中可以发现读到了10个学生信息,这也就论证了这点,为什么能读到10个学生信息,因为我们所映射的内存共享区大于文件的内容,因为映射的时候是基于页面来分配的,我们映射了40个字节,可能分配了4K的空间,只要我们在这4K的地址空间中访问就不会出错,如果我们超过了4K的地址空间,则很可能会产生一个SIGBUS的信号,如果我们访问的大小超过了几个内存页面,则有可能还会产生一个SIGSEGV信号,这取决于我们超出部份的大小。

而如果写进程结束了,再来读取,从实验结果来看,后面的五个学生信息就读取不到了,为什么呢?因为写进程结束了,也就是先前的那块内存映射区域,对于读端进程来说已经看不到了,这时读端进程又去从文件当中进行映射,只能够看到五个学生的信息了,所以说为啥要用sleep 10来说明这一问题。

③、文件一旦被映射后,所有对映射区域的访问实际上是对内存区域的访问。映射区域内容写回文件时,所写内容不能超过文件的大小,

跟消息队列一样,共享内存也是有自己的数据结构的,system v共享内存也是随内核持续的,也就是说当最后一个访问内存共享的进程结束了,内核也不会自动删除共享内存段,除非显示去删除共享内在,其数据结构跟消息队列很类似:

跟消息队列一样,共享内存也提供了四个函数:

下面详细来看一下各函数的用法:

用法跟msgget函数一模一样,下面用代码来实验一下:

当共享内存创建好之后,则希望往共享内存当中进行写入操作,在写入之前,需要将共享内存映射到进程的地址空间,接下来来看一下第二个函数:

关于这点,其实可以从帮助文档中查看到:

其中“SHMLBA”是等于4K=4096b的值,比如说shmaddr地址指定为4097,而这时所连接地址并不是4097,而是4097-(4097%4096)=4097-1=4096,而如果shmaddr地址指定为8193,这时所连接的地址为:8193-(8193%4096)=8193-1=8192,就是这个意思。

通常情况下,shmflg都指定为0,表示连接到的共享内存既可读也可以写。

下面来看另外一个函数:

所以接下来将共享内存映射到进程的地址空间当中,具体写法如下:

当执行完之后,可以解决映射:

为了观看到连接数,可以sleep一段时间,修改程序如下:

接下来从共享内存中读取数据:

之前已经说过,共享内存是随内核持续存在的,也就是它不会自动从内核中删除,可以通过下面这个函数来手动删除它,如下:

其中cmd的值可取如下:

下面则用此函数来删除共享内存段,当进程结束之后:

当然这样使用有点粗暴,还没等别人从共享内存中读走数据就被删除了,下面来让程序更加合理一点从内核删除,当有人读取了共享内存的数据时,会将quit字串写到共享内存的前面四个字节当中,所以在写程序中可以循环来做一个监听,如下:

接下来,修改读取程序:

这时查看一下共享内存是否还存在于内核当中:

另外要注意的一点是:共享内存的前面四个字节的数据类型不一定,可以为整型,也可以为字符串,如下:

目前前四个字节为整型,也可以将数据写入到姓名字段中,将结构体修改一下:

运行效果其实也是一样的,为什么呢,因为我们比较的仅仅只是内存:

只要保证有四个字节的空间,就能够存放四个字符,不在乎是什么数据类型。

通过上面的描述,很容易就能想到信号量的一上数据结构:

下面再来回顾一下P、V原语:

所谓的原语就是指这段代码是原子性的,是不会被其它信号中断的,

在Linux中,system v 信号量是以信号量集来实现的,跟其它system v IPC对象一样,也有自己的数据结构:

信号量集也提供了一些函数来操作:

下面用具体代码来实践一下,会封装一些对信号量集的一些函数:

另外,可以用命令来删除已经创建的信号量集,跟消息队列一样(ipcrm -S key 或ipcrm -s semid两种):

下面创建了之后,则可以封装一个打开信号量集的方法:

当创建了一个信号量集,并里面有一个信号量时,这时候最想做的事情是对进信号量设置一个计数值,于是第二个函数出现了:

其中先看下SETVAL参数,查看MAN帮助:

于是将其结构体拷贝一下,来给信号集来设置一个计数值:

有了设置计数值,那接下来就可以用GETVAL来获取信号量集中的信号量的计数值:

接下来封装一个删除指定的信号量集,注意:不可以直接删除某个信号量,只能删除一个信号量集,并将里面所有的信号量给删除了,如下:

下面则在main中修改一下,来实验下删除功能是否有效:

可以看出五秒之后,已经成功删除了信号集。

接下来第三个函数是一个比较核心的函数,用来进行P、V操作的:

下面用它来封装一下P、V操作:

下面对其sembuf进行进一步说明:

其中sem_op表示我们要操作的方式,而代码中我们写的是-1跟+1,实际上还可以-2,-3,+2,+3,减一个大于零的数字,表示要将信号量的数值减去相应的值,如果当前的个数小于计数值时则会阻塞,处于等待状态,当前前提是sem_flg等于0,如果sem_flg为IPC_NOWAIT而又没有可用资源时,这时semop函数就会返回失败,返回-1,并且错误代码为EAGAIN;而当sem_flg为SEM_UNDO,表示撤消,当一个进程终止的时候,对信号量所做的P或V操作会被撤消, 比如我们对信号进行了一个P操作,对其进行了-1,当进程结束时,最后一次-1将会被撤消,同样的,如果进行了一个V操作,也就是对其进行了+1,当进程结束时,最后的一次+1则会被撤消。

接下来再通过一个例子,来更好的理解信号量的一些机制:

当运行此程序时,会给出命令使用方法,带不同的参数则会有不同的功能,下面具体解释一下:

①、创建一个信号量集

②、删除一个信号量集

③、进行一个P操作

④、进行一个V操作

⑤、对信号量集中的信号量设置一个初始的计数值

⑥、获取信号量集中信号量的计数值

⑦、查看信号量集的权限

⑧、更改信号量集的权限

下面则具体来使用一下:

但是并不能无限往上加,整数的最大值是有限制的,实际上计数值内部是一个short类型,也就是范围为-32768~32767

接下来分析下程序,首先解析参数:

其中查看一下ftok函数帮助:

这里用"s"一个字符的低八位可以确何不为0,而“.”表示当前路径,两个参数通过ftok就可以产生唯一的一个key_t,至于内部怎么实现不需要关心。

接下来来判断这些参数选项:

【说明】:关于这里面用到的方法全是上面封装的

下面两个参数是还没有在上面进行封装过,关于权限的获取和设置,下面来看下:

其中权限保存的字段可以从man帮助中查看到:

所以很容易理解,下面为了进一步演示信号量的其它P、V用法,下面更改一下程序:

也就是所做的最后一次操作将会被撤消,同样的,对于v操作这个SEM_UNDO也同样适用,这里就不演示了。

下面会举例用信号量来实现进程互斥,来进一步加深对信号量的认识。

先用图来描述一下这个程序的一个意图:

下面则开始实现,基于之前信号量的封装:

print.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>


union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int sem_create(key_t key)
{
    int semid;
    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1)
        ERR_EXIT("semget");

    return semid;
}

int sem_open(key_t key)
{
    int semid;
    semid = semget(key, 0, 0);
    if (semid == -1)
        ERR_EXIT("semget");

    return semid;
}

int sem_setval(int semid, int val)
{
    union semun su;
    su.val = val;
    int ret;
    ret = semctl(semid, 0, SETVAL, su);
    if (ret == -1)
        ERR_EXIT("sem_setval");

    return 0;
}

int sem_getval(int semid)
{
        int ret;
        ret = semctl(semid, 0, GETVAL, 0);
        if (ret == -1)
                ERR_EXIT("sem_getval");

        return ret;
}

int sem_d(int semid)
{
    int ret;
    ret = semctl(semid, 0, IPC_RMID, 0);
    if (ret == -1)
        ERR_EXIT("semctl");

    return 0;
}

int sem_p(int semid)
{
    struct sembuf sb = {0, -1, 0};
    int ret;
    ret = semop(semid, &sb, 1);
    if (ret == -1)
        ERR_EXIT("semop");

    return ret;
}

int sem_v(int semid)
{
        struct sembuf sb = {0, 1, 0};
        int ret;
        ret = semop(semid, &sb, 1);
        if (ret == -1)
                ERR_EXIT("semop");

        return ret;
}

int semid;

int main(int argc, char *argv[])
{
    semid = sem_create(IPC_PRIVATE);//由于是父子进程,所以可以创建私有的信号量集
    sem_setval(semid, 0);//初始化信号量计数值为0
    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid > 0)
    {//父进程
    }
    else
    {//子进程
    }
    return 0;
}
复制代码

接下来则进行值打印:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>


union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int sem_create(key_t key)
{
    int semid;
    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1)
        ERR_EXIT("semget");

    return semid;
}

int sem_open(key_t key)
{
    int semid;
    semid = semget(key, 0, 0);
    if (semid == -1)
        ERR_EXIT("semget");

    return semid;
}

int sem_setval(int semid, int val)
{
    union semun su;
    su.val = val;
    int ret;
    ret = semctl(semid, 0, SETVAL, su);
    if (ret == -1)
        ERR_EXIT("sem_setval");

    return 0;
}

int sem_getval(int semid)
{
        int ret;
        ret = semctl(semid, 0, GETVAL, 0);
        if (ret == -1)
                ERR_EXIT("sem_getval");

        return ret;
}

int sem_d(int semid)
{
    int ret;
    ret = semctl(semid, 0, IPC_RMID, 0);
    if (ret == -1)
        ERR_EXIT("semctl");

    return 0;
}

int sem_p(int semid)
{
    struct sembuf sb = {0, -1, 0};
    int ret;
    ret = semop(semid, &sb, 1);
    if (ret == -1)
        ERR_EXIT("semop");

    return ret;
}

int sem_v(int semid)
{
        struct sembuf sb = {0, 1, 0};
        int ret;
        ret = semop(semid, &sb, 1);
        if (ret == -1)
                ERR_EXIT("semop");

        return ret;
}

int semid;

void print(char op_char)
{
    int pause_time;
    srand(getpid());//以当前进程做为随机数的种子
    int i;
    for (i=0; i<10; i++)//各输出十次
    {
        sem_p(semid);//进行一个P操作
        printf("%c", op_char);
        fflush(stdout);//由于没有用\n,所以要想在屏幕中打印出字符,需要强制清空一下缓冲区
        pause_time = rand() % 3;//在0,1,2秒中随机
        sleep(pause_time);
        printf("%c", op_char);
        fflush(stdout);
        sem_v(semid);//进行一个V操作
        pause_time = rand() % 2;
        sleep(pause_time);//最后在0,1秒中随机
    }
}

int main(int argc, char *argv[])
{
    semid = sem_create(IPC_PRIVATE);//由于是父子进程,所以可以创建私有的信号量集
    sem_setval(semid, 0);//初始化信号量计数值为0
    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid > 0)
    {//父进程
        sem_setval(semid, 1);//由于计数值初使为0,所以进行P操作时则会等待,为了进行p操作,则设置值为1
        print('O');
        wait(NULL);//等待子进程的退出
        sem_d(semid);//最后删除信号量值
    }
    else
    {//子进程
        print('X');
    }
    return 0;
}
复制代码

从运行结果来看,o跟x一定是成对出现的,不可能出现ox一起打印,这就是信号量达到互斥作用的效果。

下面回归到实际代码上来,由于这次的信号集中有多个信号量,所以这个实验中就不能用之前封装的方法了,需重新编写:

dining.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>


union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int semid;

int main(int argc, char *argv[])
{
    semid = semget(IPC_PRIVATE, 5, IPC_CREAT | 0666);//创建一个信号量集,里面包含五个信号量,这里也用私有的方式,因为会用子进程的方式模拟
    if (semid == -1)
        ERR_EXIT("semget");

    //将五个信号量的计数值都初始为1,资源都可用,模拟的是五把叉子
    union semun su;
    su.val = 1;
    int i;
    for (i=0; i<5; i++)
    {
        semctl(semid, i, SETVAL, su);
    }

    return 0;
}
复制代码

而哲学家所做的事情如下:

接下来则实现wait_for_2fork()、free_2fork()两个函数:

结合图来想,就很容易明白这个算法,如下:

同样的,释放叉子类似:

至此解决哲学家就餐问题的代码就写完,下面来编译运行一下:

从中可以看到,没有出现死锁问题,下面从输出结果来分析一下:

从结果分析来看:不可能两个相邻的哲学家同时处于“吃”的状态,同时只能够有两个哲学家处于“吃”的状态。

接下来再来模拟一下死锁的情况,在模拟之前,注意:需手动将创建的信号量集给删掉,因为刚才运行是强制关闭程序的,另外在实现之前,需要思考一下怎么样能产生死锁,其实思路很简单,就是申请叉子的时候,一个个申请,而不是当只有两个都有的情况下才能申请,所以,修改代码如下:

接下来实现wait_1fork():

从结果来看确实是阻塞了,由于都拿起了左边的叉子,而且都在等待右边叉子,而都没人释放左叉子,于是乎死锁就产生了。

最后贴上完整代码:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>


union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

#define DELAY (rand() % 5 + 1)//定义一个睡眠时间,1~5秒中
int semid;


//等待一把叉子
int wait_1fork(int no)
{
    struct sembuf sb = {no, -1, 0};
    int ret;
    ret = semop(semid, &sb, 1);
    if (ret == -1)
        ERR_EXIT("semop");

    return ret;
}

//等待左右两个叉子
void wait_for_2fork(int no)
{
    int left = no;
    int right = (no + 1) % 5;

    struct sembuf buf[2] = {
        {left, -1, 0},
        {right, -1, 0}
    };

    semop(semid, buf, 2);
}

//释放左右两信叉子
void free_2fork(int no)
{
    int left = no;
    int right = (no + 1) % 5;

    struct sembuf buf[2] = {
            {left, 1, 0},
            {right, 1, 0}
    };

    semop(semid, buf, 2);
}

void philosophere(int no)
{
    srand(getpid());//设置随机的种子
    for (;;)
    {//不断循环执行
        /*
        printf("%d is thinking\n", no);//首先思考
        sleep(DELAY);
        printf("%d is hungry\n", no);//饿了
        wait_for_2fork(no);
        printf("%d is eating\n", no);//当获取到了左右两把叉子,则开吃
        sleep(DELAY);
        free_2fork(no);//吃完则放下左右两把叉子
        */

        int left = no;
        int right = (no + 1) % 5;
        printf("%d is thinking\n", no);
        sleep(DELAY);
        printf("%d is hungry\n", no);
        wait_1fork(left);
        sleep(DELAY);
        wait_1fork(right);
        printf("%d is eating\n", no);
        sleep(DELAY);
        free_2fork(no);
    }
}

int main(int argc, char *argv[])
{
    semid = semget(IPC_PRIVATE, 5, IPC_CREAT | 0666);//创建一个信号量集,里面包含五个信号量,这里也用私有的方式,因为会用子进程的方式模拟
    if (semid == -1)
        ERR_EXIT("semget");

    //将五个信号量的计数值都初始为1,资源都可用,模拟的是五把叉子
    union semun su;
    su.val = 1;
    int i;
    for (i=0; i<5; i++)
    {
        semctl(semid, i, SETVAL, su);
    }

    //接下来创建四个子进程,加上父进程则为5个,来模拟5个哲学家
    int no = 0;
    pid_t pid;
    for (i=1; i<5; i++)
    {
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");

        if (pid == 0)
        {
            no = i;
            break;
        }
    }

    philosophere(no);

    return 0;
}
复制代码

下面用图来说明一下该问题:

以上就是生产者消费者从逻辑上的一个解决方案,从中可以看到这是互斥跟同步相结合的例子,下面则用所画的这些模型来实现一下shmfifo

为什么要实现共享内存的先进先出的缓冲区(shmfifo)呢?实际上要实现进程间通信可以直接用消息队列来实现先进先出的队列,但是,由于消息队列还实现了其它的功能,如果仅仅只是想要先进先出这样的一个功能的话,能使用共享内存来实现的话,效率会更高,因为对共享内存的访问不涉及到对内核的操作,这个之前也有讲过,因此就有必要实现一个shmfifo。

要实现这样的一个缓冲区,我们可以做一些假定,假定放到缓冲区当中的数据块是定长的,并且可以有多个进程往缓冲区中写入数据,也有多个进程往缓冲区中读取数据,所以这是典型的生产者消费者问题,这块缓冲区刚才说过可以用共享内存的方式来实现,但是有一个问题需要思考:生产者进程当前应该在什么位置添加产品,消费者进程又从什么位置消费产品呢?所以说还需要维护这些状态,所以很自然地就能想到将这些状态保存在共享内存当中,如下:

由于多个生产者都能往里面添加产品,多个消费者也能够从里面消费产品,那生产者在生产产品的时候应该放在什么位置呢?消费者又该从哪里消费产品呢?下面来说明下:

而这时再次生产就会是在0的位置上开始了:

可见这是一个环形缓冲区,可以重复利用的,基于这些分析下面来看一下所定义出来的数据结构:

有了这些数据结构实际上就能够实现了shmfifo了,下面实现一下:

由于用到了信号量,所以将之前的信号量相关的函数及定义放到一个单独的文件当中,里面代码都是之前学过的,就不多解释了:

ipc.h:

#ifndef _IPC_H_
#define _IPC_H_

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

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

union semun {
    int val;                  /* value for SETVAL */
    struct semid_ds *buf;     /* buffer for IPC_STAT, IPC_SET */
    unsigned short *array;    /* array for GETALL, SETALL */
                              /* Linux specific part: */
    struct seminfo *__buf;    /* buffer for IPC_INFO */
};

int sem_create(key_t key);
int sem_open(key_t key);

int sem_p(int semid);
int sem_v(int semid);
int sem_d(int semid);
int sem_setval(int semid, int val);
int sem_getval(int semid);
int sem_getmode(int semid);
int sem_setmode(int semid,char* mode);


#endif /* _IPC_H_ */
复制代码

ipc.c:

#include "ipc.h"

int sem_create(key_t key)
{
    int semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL);
    if (semid == -1)
        ERR_EXIT("semget");

    return semid;
}

int sem_open(key_t key)
{
    int semid = semget(key, 0, 0);
    if (semid == -1)
        ERR_EXIT("semget");

    return semid;
}

int sem_p(int semid)
{
    struct sembuf sb = {0, -1, 0};
    int ret = semop(semid, &sb, 1);
    if (ret == -1)
        ERR_EXIT("semop");

    return ret;
}

int sem_v(int semid)
{
    struct sembuf sb = {0, 1, 0};
    int ret = semop(semid, &sb, 1);
    if (ret == -1)
        ERR_EXIT("semop");

    return ret;
}

int sem_d(int semid)
{
    int ret = semctl(semid, 0, IPC_RMID, 0);
    /*
    if (ret == -1)
        ERR_EXIT("semctl");
    */
    return ret;    
}

int sem_setval(int semid, int val)
{
    union semun su;
    su.val = val;
    int ret = semctl(semid, 0, SETVAL, su);
    if (ret == -1)
        ERR_EXIT("semctl");

    //printf("value updated...\n");
    return ret;
}

int sem_getval(int semid)
{
    int ret = semctl(semid, 0, GETVAL, 0);
    if (ret == -1)
        ERR_EXIT("semctl");

    //printf("current val is %d\n", ret);
    return ret;
}

int sem_getmode(int semid)
{
        union semun su;
        struct semid_ds sem;
        su.buf = &sem;
        int ret = semctl(semid, 0, IPC_STAT, su);
        if (ret == -1)
                ERR_EXIT("semctl");

        printf("current permissions is %o\n",su.buf->sem_perm.mode);
        return ret;
}

int sem_setmode(int semid,char* mode)
{
        union semun su;
        struct semid_ds sem;
        su.buf = &sem;

        int ret = semctl(semid, 0, IPC_STAT, su);
        if (ret == -1)
                ERR_EXIT("semctl");

        printf("current permissions is %o\n",su.buf->sem_perm.mode);
        sscanf(mode, "%o", (unsigned int*)&su.buf->sem_perm.mode);
        ret = semctl(semid, 0, IPC_SET, su);
        if (ret == -1)
                ERR_EXIT("semctl");

        printf("permissions updated...\n");

        return ret;
}
复制代码

以上文件是为了实现shmfifo提供辅助功能的,下面则开始实现它,分头文件及具体实现:

shmfifo.h:

#ifndef _SHM_FIFO_H_
#define _SHM_FIFO_H_

#include "ipc.h"

typedef struct shmfifo shmfifo_t;
typedef struct shmhead shmhead_t;

struct shmhead
{
    unsigned int blksize;        // 块大小
    unsigned int blocks;        // 总块数
    unsigned int rd_index;        // 读索引
    unsigned int wr_index;        // 写索引
};

struct shmfifo
{
    shmhead_t *p_shm;            // 共享内存头部指针
    char *p_payload;            // 有效负载的起始地址

    int shmid;                    // 共享内存ID
    int sem_mutex;                // 用来互斥用的信号量
    int sem_full;                // 用来控制共享内存是否满的信号量
    int sem_empty;                // 用来控制共享内存是否空的信号量
};

shmfifo_t* shmfifo_init(int key, int blksize, int blocks);//初始化
void shmfifo_put(shmfifo_t *fifo, const void *buf);//添加数据到环形缓冲区
void shmfifo_get(shmfifo_t *fifo, void *buf);//从缓冲区中取数据
void shmfifo_destroy(shmfifo_t *fifo);//释放共享内存的环形缓冲区

#endif /* _SHM_FIFO_H_ */
复制代码

下面来具体实现一下些这函数:

这个方法既可以创建共享内存信号量,也可以打开共享内存信号量,所以下面可以做一个判断:

接下来还得初始化共享内存中的其它字段:

接下来对其信号量集中的信号进行初始化:

shmfifo的初始化函数就已经写完了,接下来来实现第二个函数:shmfifo_put(生产产品),对于生产者的过程,上面也说明过,则严格按照该步骤来进行实现:

下面则开始实现,首先先按照流程把代码框架写出来:

那如何生产产品呢?先来看下图:

首先进行数据偏移:

【说明】:关于memcpy函数的使用,说明如下:

在生产一个产品之后,下一次要生产的位置则要发生改变,所以:

这样生产产品的函数实现就如上,类似的,消费产品实现就容易了,依照这个流程:

接下来实现最后一个函数,就是资源释放:

shmfifo.c:

#include "shmfifo.h"
#include <assert.h>

shmfifo_t* shmfifo_init(int key, int blksize, int blocks)
{
    //分配内存空间
    shmfifo_t *fifo = (shmfifo_t *)malloc(sizeof(shmfifo_t));
    assert(fifo != NULL);
    memset(fifo, 0, sizeof(shmfifo_t));

    int shmid;
    shmid = shmget(key, 0, 0);
    int size = sizeof(shmhead_t) + blksize*blocks;
    if (shmid == -1)
    {//创建共享内存
        fifo->shmid = shmget(key, size, IPC_CREAT | 0666);
        if (fifo->shmid == -1)
            ERR_EXIT("shmget");

        fifo->p_shm = (shmhead_t*)shmat(fifo->shmid, NULL, 0);
        if (fifo->p_shm == (shmhead_t*)-1)
            ERR_EXIT("shmat");

        fifo->p_payload = (char*)(fifo->p_shm + 1);

        fifo->sem_mutex = sem_create(key);
        fifo->sem_full = sem_create(key+1);
        fifo->sem_empty = sem_create(key+2);

        sem_setval(fifo->sem_mutex, 1);
        sem_setval(fifo->sem_full, blocks);
        sem_setval(fifo->sem_empty, 0);
    }
    else
    {//打开共享内存
        fifo->shmid = shmid;
        fifo->p_shm = (shmhead_t*)shmat(fifo->shmid, NULL, 0);
        if (fifo->p_shm == (shmhead_t*)-1)
            ERR_EXIT("shmat");

        fifo->p_payload = (char*)(fifo->p_shm + 1);

        fifo->sem_mutex = sem_open(key);
        fifo->sem_full = sem_open(key+1);
        fifo->sem_empty = sem_open(key+2);
    }

    return fifo;
}

void shmfifo_put(shmfifo_t *fifo, const void *buf)
{
    sem_p(fifo->sem_full);
    sem_p(fifo->sem_mutex);

    //生产产品
    memcpy(fifo->p_payload+fifo->p_shm->blksize*fifo->p_shm->wr_index, 
        buf, fifo->p_shm->blksize);
    fifo->p_shm->wr_index = (fifo->p_shm->wr_index + 1) % fifo->p_shm->blocks;

    sem_v(fifo->sem_mutex);
    sem_v(fifo->sem_empty);
}

void shmfifo_get(shmfifo_t *fifo, void *buf)
{
    sem_p(fifo->sem_empty);
    sem_p(fifo->sem_mutex);

    memcpy(buf, fifo->p_payload+fifo->p_shm->blksize*fifo->p_shm->rd_index, 
        fifo->p_shm->blksize);
    fifo->p_shm->rd_index = (fifo->p_shm->rd_index + 1) % fifo->p_shm->blocks;
    sem_v(fifo->sem_mutex);
    sem_v(fifo->sem_full);
}

void shmfifo_destroy(shmfifo_t *fifo)
{
    //删除创建的信息量集
    sem_d(fifo->sem_mutex);
    sem_d(fifo->sem_full);
    sem_d(fifo->sem_empty);

    //删除共享内存
    shmdt(fifo->p_shm);//删除共享内存头部
    shmctl(fifo->shmid, IPC_RMID, 0);//删除整个共享内存
    
    //释放fifo的内存 
    free(fifo);
}
复制代码

下面则写两个测试程序,分别用来生产、消费产品:

同样的,取出存放进去的学生信息,如下:

居然生产第一个产品的时候就已经报错了,从这个错误当中很难定位到问题在哪,而这个例外肯定是产生了一个信号,所以下面用gdb来调试一下程序,再正式调查试之前,需要将之前创建的共享内存及信号量集给清掉,否则再次运行就不会报这个错,而是阻塞了:

而手动一个个去删除这些比较麻烦,因为我们已经编写好了资源的释放函数了,所以可以编写一个专门释放的程序,如下:

下面先将之前资源清除掉:

下面再次运行就会抛出例外,所以这次就可以进行gdb调试来看问题出在哪?

所以怎么来修复这个问题就比较容易了,只要做下初始化操作既可:

接下来运行一下接收产品的程序,一起来看下生产者消费者的一个效果:

从实验结果来看,当接收了数据之后,原来还在等待的2个学生信息就被成功发送了,这就是生产者与消费者的一个效果,实际中生产者可以有多个,消费者也可以有多个,这次学的内容有些多,通过这个例子可以学习怎么利用共享内存和信号量来实现一个先进先出的环形缓冲区shmfifo.

转载于:https://juejin.im/post/5ced580be51d4510835e022e

猜你喜欢

转载自blog.csdn.net/weixin_33991727/article/details/91476113
今日推荐