实际上这也是进程之间的两种关系,在学习这两种关系以前,须要回顾一下顺序程序与并发程序的特征:算法
①、顺序性安全
顺序程序执行的顺序是按照指令的前后顺序来执行的,当前的指令需依赖于前一条指令,并与前一条指令构成了必定的因果关系,后一条指令的执行必定要在前一条指令的基础之上才可以运行。bash
②、封闭性:(运行环境的封闭性)服务器
也就是说顺序程序在运行过程当中,它的运行环境不会受其它程序的影响。网络
③、肯定性数据结构
只要给程序必定的输入,无论程序是运行在比较快的机器上,仍是运行在比较慢的机器上,它必定会有特定的输出。并发
④、可再现性框架
一个程序在必定时期运行的结果,跟另一个时期运行的结果能够是同样的,只要是具备相同的输入,就必定具备相同的输出,这跟"肯定性"是颇有关系。socket
①、共享性函数
②、并发性
③、随机性
下面来正式理解进程同步与进程互斥:
如两个小孩争抢同一个玩具,这是一种互斥关系。
下面来举一个同步的示例:汽车售票
一般状况下,将这两种关系统称为同步关系。
①、数据传输:一个进程须要将它的数据发送给另外一个进程
②、资源共享:多个进程之间共享一样的资源。
③、通知事件:一个进程须要向另外一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。【如:上面说的汽车售票的例子】
④、进程控制:有些进程但愿彻底控制另外一个进程的执行(如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)
 struct msgbuf {
   long mtype; /* message type, must be > 0 */
   char mtext[1]; /* message data */
 };
#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,不然,实际上客户端是自已发给本身,就形成了客户端有回显,而服务端没有,改变以后,再次运行:
发现服务器的消息显示还有些问题,缘由其实很简单,就是因为没有清空以前数据形成,修改以下:
实际上,这里还存在一种隐含的问题,会产生“死锁”现象,下面来分析一下产生的缘由:
避免死锁的方案须要采用另一种实现方式,这里只是探讨一下,能够本身去实现:
一、用管道或者消息队列传递数据
这个示意图的功能是服务器向客户端传输文件,以下:
①、首先要将文件从内核读取到进程的用户空间当中,因此这里就涉及到了一次read()系统调用。
②、服务器须要将读取到的数据拷贝到管道或消息队列当中,涉及到第二次系统调用。
③、对于客户端来讲,须要从管道或消息队列中读取这些数据,涉及到第三次系统调用。
④、读到这些数据到应用的数据缓冲区当中,而后将缓冲区的内容写到输出文件中,涉及到第四次系统调查用。
从以上步骤来看,总共涉及到了四次系统调用, 四次内存拷贝(从内核空间到用户空间),那共享内容方式又如何呢?
二、用共享内存传递数据
下面来学习一下相关函数的使用:
下面来看一下内存映射文件示意图:
下面则用代码来实践一下:
下面来建立一个文件:
接下来对文件进行映射:
当映射成功以后,接下来则往文件中写入一些数据,这时候就能够直接经过指针写入了,对文件的操做就好像对内存的访问,以下:
可见就经过内存的方式来对文件进行了数据写入,这就是内存映射文件的做用。
下面来写一个读取文件内容的功能,将写入的五个同窗的数据读出来,基于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.