XSI IPC(消息队列、信号量、共享内存)

来源:互联网 发布:个人简历h5源码 编辑:程序博客网 时间:2024/05/27 16:40

XSI IPC(消息队列、信号量、共享内存)

IPC我们很清楚它是指进程间的通信,其中有三种我们称之为XSI IPC即消息队列、信号量以及共享存储器,他们之间有好多相似之处。

System V(“系统五”)系统上发明了三种IPC机制(消息队列、信号量和共享内存),通常称为System V IPC。又因为后来被收录到Unix的XSI标准之中故又称为XSI IPC。所以当你看到System V IPC 和 XSI IPC的时候实际上指的是同一种东西。

System V消息队列相对于POSIX消息队列的区别主要是:

  • POSIX消息队列的读操作总是返回消息队列中优先级最高的最早消息,而对于System V消息队列可以返回任意指定优先级(通过消息类型)的消息。

  • 当向一个空消息队列中写入一个消息时,POSIX消息队列允许产生一个信号或启动一个线程,System V消息队列不提供类似的机制。

  • I P C 标识符:每一个内核中的IPC结构(消息队列,信号量,共享存储段)都用一个非负整数的标识符(identifier)加以引用。当一个消息队列发送或取消息,只需要知道其队列标示符。

    消息队列:

// 内核为每个IPC对象维护一个数据结构(/usr/include/linux/ipc.h)struct ipc_perm{    key_t __key; /* key supplied to xxxget(2) */    uid_t uid;  /* Effective UID of owner */    gid_t gid;  /* Effective GID of owner */    uid_t cuid; /* Effective UID of creator */    gid_t cgid; /* Effective GID of creator */    unsigned short mode;  /* Permission */    unsigned short __seq; /* Sequeence number*/}12345678910111234567891011
  • IPC关键字:因为IPC标识符是IPC结构的内部名。为使多个合作进程能够在同一IPC对象上会合,需要提供一个外部名方案。即键(key)每一个IPC对象都与一个键相关联,于是键就作为该结构的外部名。要想获得一个唯一标识符,必须使用一个IPC关键字。server和client进程必须双方都同意此关键字。 可以使用ftok( )函数为客户端和服务器产生关键字值。

//消息队列的结构 ( /usr/include/linux/msg.h)// message queue id// defined in <linux/ipc.h>struct msqid_ds{    struct ipc_perm msg_perm;    struct msg* msg_first;      /* first message on queue, unused */    struct msg* msg_last;       /* last message in queue, unused */    __kernel_time_t msg_stime;  /* last msgsnd time */    __kernel_time_t msg_rtime;  /* last msgrcv time */    __kernel_time_t msg_ctime;  /* last change time */    unsigned long msg_lcbytes;  /* Reuse junk fields for 32 bit */    unsigned long msg_lqbytes;  /* ditto  == 同上... */    unsigned short msg_cbytes;  /* current number of butes on queue */    unsigned short msg_qnum;    /* number of messages in queue */    __kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */    __kernel_ipc_pid_t msg_lrpid; /* last receive pid */}

key值和ID值

linux系统ipc机制都分配了唯一的ID,所有对该IPC机制的操作都是使用该ID值,因此,要进行通信,通信的双方需要通过某个方法来获取ID值创建着根据创建函数的返回值可获取该值,linux两个进程不能访问不能随意访问对方的空间,也就是不能直接获取这一ID值。如果在创建时使用相同的key值将得到同一个ipc对象的ID(即一方创建,;另一方直接获取ID),这样就保证了双方可以获取用于传递数据的ipc机制和ID值,key值为一个32位的整形数据。创建key值:    linux提供函数ftok()来创建key值,在此函数的参数中,需要特定文件做参数。    函数声明如下:        extern key_t ftok(_const char* _pathname_ ,int _proj_id)    注:pathname 为文件路径名,可以是特殊文件(如目录文件),也可以是当前目录".",通常设置此参数为当前目录。

消息队列:

1、创建消息队列    在使用一个消息队列前,必须先把消息队列创建出来创建一个消息队列使用msgget函数:        extern int msgget(key_t _key,int _msgflg)        注:第一个参数key为ftok创建的key值            第二个参数_msgflg:                其低位用来确定消息队列的访问权限,                其最终权限为当前进程的umask值与设定                值perm类似于open函数即最终权限为                pern&~umask;                高位包含:                    #define IPC_CREAT  00001000 //如果key不存在,则创建(一般)                    #define IPC_EXCL   00002000 //存在返回ID                    #define IPC_NOWAIT 000004000 //如果需要等待,直接返回错误2、消息队列属性的控制    创建消息队列后,可以对该消息队列的基本属性进行控制(修改),    控制消息队列属性的函数为msgctl,函数原型:        extern int msgctl(int _msqid,int _cmd,struct msqid_ds* _buf);        注:第一个参数_msqid为消息队列标识符,即为msgget函数的返回值            第二个参数_cmd为执行的控制命令,包括以下选项:               #define IPC_RMID  0   //删除消息队列               #define IPC_SET   1    //设置ipc_pern参数               #define IPC_STAT  2    //获取ipc_pern参数               #define IPC_INFO  3    //获取限制信息               第三个参数是一个临时的msqid_ds结构体类型的变量3、发送信息到消息队列:    使用函数msgsend()  ---将新的消息添加到消息队列尾端    exrern int magsend(int _msqid,_const void* _msgp,size_t _msgsz,int _msgflg)    注:第一个参数msqid为指定的消息队列标识符(由msgget生成的消息队列标识符),即将消息添加到哪个消息队列中。        第二个参数msgp指向的是用户定义缓冲区,缓冲区结构如下:                struct msgbuf{                long mtype;      //消息类型                char mtext[1];   //消息内容,在使用时自己重定义此结构            }            msgtyp:消息类型                msgtyp等于0 则返回队列的最早的一个消息。                msgtyp大于0,则返回其类型为mtype的第一个消息。                msgtyp小于0,则返回其类型小于或等于mtype参数的绝对值的最小的一个消息。        第三个参数为接收消息的大小(0到系统对消息队列的限制值)        第四个参数用来指定达到系统为消息队列所定的界限时应该采取的操作;            设置0,则阻塞调用进程4、从消息队列接收信息:    msgrcv用于从队列中取消息    extern int msgrcv(int _msqid,void* _msgp,size_t _msgsz,long int _msgtyp,int _msgflg);    第一个参数为读的对象,即从那个消息队列获得消息    第二个参数为一个临时变量消息数据结构,用于保存读取的信息        定义如下:            struct msgbuf{                long mtype;     //消息类型                char mtext[1];   //消息内容位置            }    第三个参数msgsz用于指定大小(以字节为单位);    第四个参数msgtyp用于指定请求的消息类型:        具体如下:            msgtyp = 0;接收队列中的第一条消息,任意类型            msgtyp > 0;接收第一条msgtyp类型的消息            msgtyp < 0;接收第一条最低类型(小于或等于msgtyp的绝对值)的消息。    第五个参数msgflg用于指定所需类型消息不在队列上时,将要采取的操作,        具体如下:        如果设置IPC_NOWAIT,如果现在没有消息,调用进程立即返回,同时返回-1;并将errno设置为[ENOMSG]        如果未设置IPC_NOWAIT,则阻塞调用进程行

几个与消息队列常见的指令:ipcs的用法ipcs -a 是默认的输出信息 打印出当前系统中所有的进程间通信方式的信息ipcs -m 打印出使用共享内存进行进程间通信的信息ipcs -q 打印出使用消息队列进行进程间通信的信息ipcs -s 打印出使用信号进行进程间通信的信息ipcrm -q[参数] ——这里的参数是消息队列的编号

信号量

一、什么是信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

二、信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

三、Linux的信号量机制

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

1、semget函数

它的作用是创建一个新信号量或取得一个已有信号量,原型为:

[cpp]view plaincopyprint?

  1. int semget(key_t key, int num_sems, int sem_flags);

第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。

第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。

第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

semget函数成功返回一个相应信号标识符(非零),失败返回-1.

2、semop函数

它的作用是改变信号量的值,原型为:

[cpp]view plaincopyprint?

  1. int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:

[cpp]view plaincopyprint?

  1. struct sembuf{

  2. ​ short sem_num;//除非使用一组信号量,否则它为0

  3. ​ short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,

  4. ​ //一个是+1,即V(发送信号)操作。

  5. ​ short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,

  6. ​ //并在进程没有释放该信号量而终止时,操作系统释放信号量

  7. };

3、semctl函数

该函数用来直接控制信号量信息,它的原型为:

[cpp]view plaincopyprint?

  1. int semctl(int sem_id, int sem_num, int command, ...);

如果有第四个参数,它通常是一个union semum结构,定义如下:

[cpp]view plaincopyprint?

  1. union semun{

  2. ​ int val;

  3. ​ struct semid_ds *buf;

  4. ​ unsigned short *arry;

  5. };

前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个

SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。

IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

四、进程使用信号量通信

下面使用一个例子来说明进程间如何使用信号量来进行通信,这个例子是两个相同的程序同时向屏幕输出数据,我们可以看到如何使用信号量来使两个进程协调工作,使同一时间只有一个进程可以向屏幕输出数据。注意,如果程序是第一次被调用(为了区分,第一次调用程序时带一个要输出到屏幕中的字符作为一个参数),则需要调用set_semvalue函数初始化信号并将message字符设置为传递给程序的参数的第一个字符,同时第一个启动的进程还负责信号量的删除工作。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

在main函数中调用semget来创建一个信号量,该函数将返回一个信号量标识符,保存于全局变量sem_id中,然后以后的函数就使用这个标识符来访问信号量。

源文件为seml.c,代码如下:

[cpp]view plaincopyprint?

  1. #include<unistd.h>

  2. #include<sys/types.h>

  3. #include<sys/stat.h>

  4. #include<fcntl.h>

  5. #include<stdlib.h>

  6. #include<stdio.h>

  7. #include<string.h>

  8. #include<sys/sem.h>

  9. union semun

  10. {

  11. ​ int val;

  12. ​ struct semid_ds *buf;

  13. ​ unsigned short *arry;

  14. };

  15. static int sem_id = 0;

  16. static int set_semvalue();

  17. static void del_semvalue();

  18. static int semaphore_p();

  19. static int semaphore_v();

  20. int main(int argc, char *argv[])

  21. {

  22. ​ char message = 'X';

  23. ​ int i = 0;

  24. ​ //创建信号量

  25. ​ sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

  26. ​ if(argc > 1)

  27. ​ {

  28. ​ //程序第一次被调用,初始化信号量

  29. ​ if(!set_semvalue())

  30. ​ {

  31. ​ fprintf(stderr, "Failed to initialize semaphore\n");

  32. ​ exit(EXIT_FAILURE);

  33. ​ }

  34. ​ //设置要输出到屏幕中的信息,即其参数的第一个字符

  35. ​ message = argv1;

  36. ​ sleep(2);

  37. ​ }

  38. ​ for(i = 0; i < 10; ++i)

  39. ​ {

  40. ​ //进入临界区

  41. ​ if(!semaphore_p())

  42. ​ exit(EXIT_FAILURE);

  43. ​ //向屏幕中输出数据

  44. ​ printf("%c", message);

  45. ​ //清理缓冲区,然后休眠随机时间

  46. ​ fflush(stdout);

  47. ​ sleep(rand() % 3);

  48. ​ //离开临界区前再一次向屏幕输出数据

  49. ​ printf("%c", message);

  50. ​ fflush(stdout);

  51. ​ //离开临界区,休眠随机时间后继续循环

  52. ​ if(!semaphore_v())

  53. ​ exit(EXIT_FAILURE);

  54. ​ sleep(rand() % 2);

  55. ​ }

  56. ​ sleep(10);

  57. ​ printf("\n%d - finished\n", getpid());

  58. ​ if(argc > 1)

  59. ​ {

  60. ​ //如果程序是第一次被调用,则在退出前删除信号量

  61. ​ sleep(3);

  62. ​ del_semvalue();

  63. ​ }

  64. ​ exit(EXIT_SUCCESS);

  65. }

  66. static int set_semvalue()

  67. {

  68. ​ //用于初始化信号量,在使用信号量前必须这样做

  69. ​ union semun sem_union;

  70. ​ sem_union.val = 1;

  71. ​ if(semctl(sem_id, 0, SETVAL, sem_union) == -1)

  72. ​ return 0;

  73. ​ return 1;

  74. }

  75. static void del_semvalue()

  76. {

  77. ​ //删除信号量

  78. ​ union semun sem_union;

  79. ​ if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)

  80. ​ fprintf(stderr, "Failed to delete semaphore\n");

  81. }

  82. static int semaphore_p()

  83. {

  84. ​ //对信号量做减1操作,即等待P(sv)

  85. ​ struct sembuf sem_b;

  86. ​ sem_b.sem_num = 0;

  87. ​ sem_b.sem_op = -1;//P()

  88. ​ sem_b.sem_flg = SEM_UNDO;

  89. ​ if(semop(sem_id, &sem_b, 1) == -1)

  90. ​ {

  91. ​ fprintf(stderr, "semaphore_p failed\n");

  92. ​ return 0;

  93. ​ }

  94. ​ return 1;

  95. }

  96. static int semaphore_v()

  97. {

  98. ​ //这是一个释放操作,它使信号量变为可用,即发送信号V(sv)

  99. ​ struct sembuf sem_b;

  100. ​ sem_b.sem_num = 0;

  101. ​ sem_b.sem_op = 1;//V()

  102. ​ sem_b.sem_flg = SEM_UNDO;

  103. ​ if(semop(sem_id, &sem_b, 1) == -1)

  104. ​ {

  105. ​ fprintf(stderr, "semaphore_v failed\n");

  106. ​ return 0;

  107. ​ }

  108. ​ return 1;

  109. }

运行结果如下:

注:这个程序的临界区为main函数for循环不的semaphore_p和semaphore_v函数中间的代码。

例子分析 :同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符‘O’,它用于区分是否为第一次调用,同时这个字符输出到屏幕中。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。

五、对比例子——进程间的资源竞争

看了上面的例子,你可能还不是很明白,不过没关系,下面我就以另一个例子来说明一下,它实现的功能与前面的例子一样,运行方式也一样,都是两个相同的进程,同时向stdout中输出字符,只是没有使用信号量,两个进程在互相竞争stdout。它的代码非常简单,文件名为normalprint.c,代码如下:

[cpp]view plaincopyprint?

  1. #include<stdio.h>

  2. #include<stdlib.h>

  3. int main(int argc, char *argv[])

  4. {

  5. ​ char message = 'X';

  6. ​ int i = 0;

  7. ​ if(argc > 1)

  8. ​ message = argv1;

  9. ​ for(i = 0; i < 10; ++i)

  10. ​ {

  11. ​ printf("%c", message);

  12. ​ fflush(stdout);

  13. ​ sleep(rand() % 3);

  14. ​ printf("%c", message);

  15. ​ fflush(stdout);

  16. ​ sleep(rand() % 2);

  17. ​ }

  18. ​ sleep(10);

  19. ​ printf("\n%d - finished\n", getpid());

  20. ​ exit(EXIT_SUCCESS);

  21. }

运行结果如下:

例子分析:

从上面的输出结果,我们可以看到字符‘X’和‘O’并不像前面的例子那样,总是成对出现,因为当第一个进程A输出了字符后,调用sleep休眠时,另一个进程B立即输出并休眠,而进程A醒来时,再继续执行输出,同样的进程B也是如此。所以输出的字符就是不成对的出现。这两个进程在竞争stdout这一共同的资源。通过两个例子的对比,我想信号量的意义和使用应该比较清楚了。

六、信号量的总结

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。

共享内存:

一、什么是共享内存

顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。

二、共享内存的使得

与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。它们声明在头文件 sys/shm.h中。

1、shmget函数

该函数用来创建共享内存,它的原型为:

[cpp]view plaincopyprint?

  1. int shmget(key_t key, size_t size, int shmflg);

第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.

不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。

第二个参数,size以字节为单位指定需要共享的内存容量

第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

2、shmat函数

第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

[cpp]view plaincopyprint?

  1. void *shmat(int shm_id, const void *shm_addr, int shmflg);

第一个参数,shm_id是由shmget函数返回的共享内存标识。

第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

第三个参数,shm_flg是一组标志位,通常为0。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

3、shmdt函数

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

[cpp]view plaincopyprint?

  1. int shmdt(const void *shmaddr);

参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

4、shmctl函数

与信号量的semctl函数一样,用来控制共享内存,它的原型如下:

[cpp]view plaincopyprint?

  1. int shmctl(int shm_id, int command, struct shmid_ds *buf);

第一个参数,shm_id是shmget函数返回的共享内存标识符。

第二个参数,command是要采取的操作,它可以取下面的三个值 :

​ IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。

​ IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值

​ IPC_RMID:删除共享内存段

第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。

shmid_ds结构至少包括以下成员:

[cpp]view plaincopyprint?

  1. struct shmid_ds

  2. {

  3. ​ uid_t shm_perm.uid;

  4. ​ uid_t shm_perm.gid;

  5. ​ mode_t shm_perm.mode;

  6. };

三、使用共享内存进行进程间通信

说了这么多,又到了实战的时候了。下面就以两个不相关的进程来说明进程间如何通过共享内存来进行通信。其中一个文件shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文件shmdata.c中。结构shared_use_st中的written作为一个可读或可写的标志,非0:表示可读,0表示可写,text则是内存中的文件。

shmdata.h的源代码如下:

[cpp]view plaincopyprint?

  1. #ifndefSHMDATA_HHEADER

  2. #defineSHMDATA_HHEADER

  3. #define TEXT_SZ 2048

  4. struct shared_use_st

  5. {

  6. ​ int written;//作为一个标志,非0:表示可读,0表示可写

  7. ​ char text[TEXT_SZ];//记录写入和读取的文本

  8. };

  9. #endif

源文件shmread.c的源代码如下:

[cpp]view plaincopyprint?

  1. #include<unistd.h>

  2. #include<stdlib.h>

  3. #include<stdio.h>

  4. #include<sys/shm.h>

  5. #include "shmdata.h"

  6. int main()

  7. {

  8. ​ int running = 1;//程序是否继续运行的标志

  9. ​ void *shm = NULL;//分配的共享内存的原始首地址

  10. ​ struct shared_use_st *shared;//指向shm

  11. ​ int shmid;//共享内存标识符

  12. ​ //创建共享内存

  13. ​ shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);

  14. ​ if(shmid == -1)

  15. ​ {

  16. ​ fprintf(stderr, "shmget failed\n");

  17. ​ exit(EXIT_FAILURE);

  18. ​ }

  19. ​ //将共享内存连接到当前进程的地址空间

  20. ​ shm = shmat(shmid, 0, 0);

  21. ​ if(shm == (void*)-1)

  22. ​ {

  23. ​ fprintf(stderr, "shmat failed\n");

  24. ​ exit(EXIT_FAILURE);

  25. ​ }

  26. ​ printf("\nMemory attached at %X\n", (int)shm);

  27. ​ //设置共享内存

  28. ​ shared = (struct shared_use_st*)shm;

  29. ​ shared->written = 0;

  30. ​ while(running)//读取共享内存中的数据

  31. ​ {

  32. ​ //没有进程向共享内存定数据有数据可读取

  33. ​ if(shared->written != 0)

  34. ​ {

  35. ​ printf("You wrote: %s", shared->text);

  36. ​ sleep(rand() % 3);

  37. ​ //读取完数据,设置written使共享内存段可写

  38. ​ shared->written = 0;

  39. ​ //输入了end,退出循环(程序)

  40. ​ if(strncmp(shared->text, "end", 3) == 0)

  41. ​ running = 0;

  42. ​ }

  43. ​ else//有其他进程在写数据,不能读取数据

  44. ​ sleep(1);

  45. ​ }

  46. ​ //把共享内存从当前进程中分离

  47. ​ if(shmdt(shm) == -1)

  48. ​ {

  49. ​ fprintf(stderr, "shmdt failed\n");

  50. ​ exit(EXIT_FAILURE);

  51. ​ }

  52. ​ //删除共享内存

  53. ​ if(shmctl(shmid, IPC_RMID, 0) == -1)

  54. ​ {

  55. ​ fprintf(stderr, "shmctl(IPC_RMID) failed\n");

  56. ​ exit(EXIT_FAILURE);

  57. ​ }

  58. ​ exit(EXIT_SUCCESS);

  59. }

源文件shmwrite.c的源代码如下:

[cpp]view plaincopyprint?

  1. #include<unistd.h>

  2. #include<stdlib.h>

  3. #include<stdio.h>

  4. #include<string.h>

  5. #include<sys/shm.h>

  6. #include "shmdata.h"

  7. int main()

  8. {

  9. ​ int running = 1;

  10. ​ void *shm = NULL;

  11. ​ struct shared_use_st *shared = NULL;

  12. ​ char buffer[BUFSIZ + 1];//用于保存输入的文本

  13. ​ int shmid;

  14. ​ //创建共享内存

  15. ​ shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);

  16. ​ if(shmid == -1)

  17. ​ {

  18. ​ fprintf(stderr, "shmget failed\n");

  19. ​ exit(EXIT_FAILURE);

  20. ​ }

  21. ​ //将共享内存连接到当前进程的地址空间

  22. ​ shm = shmat(shmid, (void*)0, 0);

  23. ​ if(shm == (void*)-1)

  24. ​ {

  25. ​ fprintf(stderr, "shmat failed\n");

  26. ​ exit(EXIT_FAILURE);

  27. ​ }

  28. ​ printf("Memory attached at %X\n", (int)shm);

  29. ​ //设置共享内存

  30. ​ shared = (struct shared_use_st*)shm;

  31. ​ while(running)//向共享内存中写数据

  32. ​ {

  33. ​ //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本

  34. ​ while(shared->written == 1)

  35. ​ {

  36. ​ sleep(1);

  37. ​ printf("Waiting...\n");

  38. ​ }

  39. ​ //向共享内存中写入数据

  40. ​ printf("Enter some text: ");

  41. ​ fgets(buffer, BUFSIZ, stdin);

  42. ​ strncpy(shared->text, buffer, TEXT_SZ);

  43. ​ //写完数据,设置written使共享内存段可读

  44. ​ shared->written = 1;

  45. ​ //输入了end,退出循环(程序)

  46. ​ if(strncmp(buffer, "end", 3) == 0)

  47. ​ running = 0;

  48. ​ }

  49. ​ //把共享内存从当前进程中分离

  50. ​ if(shmdt(shm) == -1)

  51. ​ {

  52. ​ fprintf(stderr, "shmdt failed\n");

  53. ​ exit(EXIT_FAILURE);

  54. ​ }

  55. ​ sleep(2);

  56. ​ exit(EXIT_SUCCESS);

  57. }

再来看看运行的结果:

分析:

1、程序shmread创建共享内存,然后将它连接到自己的地址空间。在共享内存的开始处使用了一个结构struct_use_st。该结构中有个标志written,当共享内存中有其他进程向它写入数据时,共享内存中的written被设置为0,程序等待。当它不为0时,表示没有进程对共享内存写入数据,程序就从共享内存中读取数据并输出,然后重置设置共享内存中的written为0,即让其可被shmwrite进程写入数据。

2、程序shmwrite取得共享内存并连接到自己的地址空间中。检查共享内存中的written,是否为0,若不是,表示共享内存中的数据还没有被完,则等待其他进程读取完成,并提示用户等待。若共享内存的written为0,表示没有其他进程对共享内存进行读取,则提示用户输入文本,并再次设置共享内存中的written为1,表示写完成,其他进程可对共享内存进行读操作。

四、关于前面的例子的安全性讨论

这个程序是不安全的,当有多个程序同时向共享内存中读写数据时,问题就会出现。可能你会认为,可以改变一下written的使用方式,例如,只有当written为0时进程才可以向共享内存写入数据,而当一个进程只有在written不为0时才能对其进行读取,同时把written进行加1操作,读取完后进行减1操作。这就有点像文件锁中的读写锁的功能。咋看之下,它似乎能行得通。但是这都不是原子操作,所以这种做法是行不能的。试想当written为0时,如果有两个进程同时访问共享内存,它们就会发现written为0,于是两个进程都对其进行写操作,显然不行。当written为1时,有两个进程同时对共享内存进行读操作时也是如些,当这两个进程都读取完是,written就变成了-1.

要想让程序安全地执行,就要有一种进程同步的进制,保证在进入临界区的操作是原子操作。例如,可以使用前面所讲的信号量来进行进程的同步。因为信号量的操作都是原子性的。

五、使用共享内存的优缺点

1、优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。

2、缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。

阅读全文
0 0
原创粉丝点击