linux c 共享内存 消息队列 信号量

 

linux进程间的通信(C): 共享内存

一、共享内存介绍
共享内存是三个IPC(Inter-Process Communication)机制中的一个。
它允许两个不相关的进程访问同一个逻辑内存。
共享内存是在两个正在进行的进程之间传递数据的一种非常有效的方式。
大多数的共享内存的实现,
都把由不同进程之间共享的内存安排为同一段物理内存
 
共享内存是由IPC为进程创建一个特殊的地址范围,
它将出现在该进程的地址空间中。
其他进程可以将同一段共享内存连接它们自己的地址空间中。
所有进程都可以访问共享内存中的地址,
就好像它们是由malloc分配的一样。
 
如果某个进程向共享内存写入了数据,
所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。
 
二、共享内存的同步
共享内存为在多个进程之间共享和传递数据提供了一种有效的方式。
但是它并未提供同步机制
所以我们通常需要用其他的机制来同步对共享内存的访问。
我们通常是用共享内存来提供对大块内存区域的有效访问,
同时通过传递小消息来同步对该内存的访问。
 
在第一个进程结束对共享内存的写操作之前,
并无自动的机制可以阻止第二个进程开始对它进行读取。
对共享内存访问的同步控制必须由程序员来负责。
 
下图显示了共享内存是如何共存的:
 
图中的箭头显示了每个进程的逻辑地址空间到可用物理内存的映射关系。
 
 
三、共享内存使用的函数
  1. #include <sys/shm.h>
  2. int shmget(key_t key, size_t size, int shmflg);
  3. void *shmat(int shm_id, const void *shm_addr, int shmflg);
  4. int shmdt(const void *shm_addr);
  5. int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
 
1. shmget函数
该函数用来创建共享内存:
  1. int shmget(key_t key, size_t size, int shmflg);
参数:
key : 和信号量一样,程序需要提供一个参数key,
      它有效地为共享内存段命名。
      
      有一个特殊的键值IPC_PRIVATE, 
      它用于创建一个只属于创建进程的共享内存,
      通常不会用到。
size: 以字节为单位指定需要共享的内存容量。
shmflag: 包含9个比特的权限标志,
         它们的作用与创建文件时使用的mode标志是一样。
         由IPC_CREAT定义的一个特殊比特必须和权限标志按位或
         才能创建一个新的共享内存段。
 
NOTE:
权限标志对共享内存非常有用,
因为它允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,
同时其它用户创建的进程只能读取共享内存。
 
我们可以利用这个功能来提供一种有效的对数据进行只读访问的方法,
通过将数据放共享内存并设置它的权限,
就可以避免数据被其他用户修改。
 
返回值:
创建成功,则返回一个非负整数,即共享内存标识;
如果失败,则返回-1.
 
2. shmat函数
第一次创建共享内存段时,它不能被任何进程访问。
要想启动对该内存的访问,
必须将其连接到一个进程的地址空间。
这个工作由shmat函数完成:
  1. void *shmat(int shm_id, const void *shm_addr, int shmflg);
参数:
shm_id : 由shmget返回的共享内存标识。
shm_add: 指定共享内存连接到当前进程中的地址位置。
         它通常是一个空指针, 
         表示让系统来选择共享内存出现的地址。
shmflg : 是一组标志。
         它的两个可能取值是:
         SHM_RND, 和shm_add联合使用,
                  用来控制共享内存连接的地址。
         SHM_RDONLY, 它使连接的内存只读
         
返回值:
如果调用成功, 返回一个指向共享内存第一个字节的指针;
如果失败,返回-1.
 
共享内存的读写权限由它的属主(共享内存的创建者),
它的访问权限和当前进程的属主决定。
共享内存的访问权限类似于文件的访问权限。
 
3. shmdt
将共享内存从当前进程中分离。
  1. int shmdt(const void *shm_addr);
shm_addr: shmat返回的地址指针。
 
成功时,返回0,
失败时,返回-1.
 
NOTE:
共享内存分离并未删除它,
只是使得该共享内存对当前进程不再可用。
 
4. shmctl
共享内存的控制函数
  1. int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
shmid_ds结构至少包含以下成员:
  1. struct shmid_ds {
  2.   uid_t shm_perm.uid;
  3.   uid_t shm_perm.gid;
  4.   mode_t shm_perm.mode;
  5. }
 
参数:
shm_id : 是shmget返回的共享内存标识符。
command: 是要采取的动作,
         它可以取3个值:
 
IPC_STAT  把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET   如果进程有足够的权限,
          就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID  删除共享内存段
 
buf    : 是一个指针,
         包含共享内存模式和访问权限的结构。
 
返回值:
成功时,返回0,
失败时,返回-1.
 
四、示例
典型的消费者-生产者程序,
第一个程序(消费者)将创建一个共享内存段,
然后把写到它里面的数据都显示出来。
第二个程序(生产者)将连接一个已有的共享内存段,
并允许我们向其中输入数据。
 
shm_com.h
  1. #define TEXT_SZ 2048
  2. struct shared_use_st {
  3.   int written_by_you;
  4.   char some_text[TEXT_SZ];
  5. };
当有数据写入这个结构中时,
我们用结构中的written_by_you标志来通知消费者。
需要传输的文本长度2K是随意定的。
 
shm1.c 消费者程序
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5. #include <sys/shm.h>
  6. #include "shm_com.h"
  7. int main()
  8. {
  9.   int running = 1;
  10.   void *shared_memory = (void *)0;
  11.   struct shared_use_st *shared_stuff;
  12.   int shmid;
  13.   srand((unsigned int)getpid());
  14.   shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
  15.   if (shmid == -1) {
  16.     fprintf(stderr, "shmget failed\n");
  17.     exit(EXIT_FAILURE);
  18.   }
 
现在,让程序可以访问这个共享内存:
  1.   shared_memory = shmat(shmid, (void *)0, 0);
  2.   if (shared_memory == (void *)-1) {
  3.     fprintf(stderr, "shmat failed\n");
  4.     exit(EXIT_FAILURE);
  5.   }
  6.   printf("Memory attached at %X\n", (int)shared_memory);
 
程序的下一部分将shared_memory分配给shared_stuff,
然后它输出written_by_you中的文本。
循环将一直执行到在written_by_you中找到end字符串为止。
sleep调用强迫消费者程序在临界区域多待一会,
让生产者程序等待:

  1.   shared_stuff = (struct shared_use_st *)shared_memory;
  2.   shared_stuff->written_by_you = 0;
  3.   while(running) 
  4.   {
  5.     if (shared_stuff->written_by_you) 
  6.     {
  7.       printf("You wrote: %s", shared_stuff->some_text);
  8.       sleep( rand() % 4 ); /* make the other process wait for us ! */
  9.       shared_stuff->written_by_you = 0;
  10.       if (strncmp(shared_stuff->some_text, “end”, 3) == 0) {
  11.         running = 0;
  12.       }
  13.     }
  14.   }
 
最后,共享内存被分离,然后被删除:
  1.   if (shmdt(shared_memory) == -1) 
  2.   {
  3.     fprintf(stderr, "shmdt failed\n");
  4.     exit(EXIT_FAILURE);
  5.   }
  6.   if (shmctl(shmid, IPC_RMID, 0) == -1) 
  7.   {
  8.     fprintf(stderr, "shmctl(IPC_RMID) failed\n");
  9.     exit(EXIT_FAILURE);
  10.   }
  11.   exit(EXIT_SUCCESS);
  12. }
shm2.c 生产者程序
通过它向消费者程序输入数据。
  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <string.h>
  5. #include <sys/shm.h>
  6. #include "shm_com.h"
  7. int main()
  8. {
  9.   int running = 1;
  10.   void *shared_memory = (void *)0;
  11.   struct shared_use_st *shared_stuff;
  12.   char buffer[BUFSIZ];
  13.   int shmid;
  14.   shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
  15.   if (shmid == -1) 
  16.   {
  17.     fprintf(stderr, "shmget failed\n");
  18.     exit(EXIT_FAILURE);
  19.   }
  20.   shared_memory = shmat(shmid, (void *)0, 0);
  21.   if (shared_memory == (void *)-1) 
  22.   {
  23.     fprintf(stderr, "shmat failed\n");
  24.     exit(EXIT_FAILURE);
  25.   }
  26.   printf("Memory attached at %X\n", (int)shared_memory);
  27.   shared_stuff = (struct shared_use_st *)shared_memory;
  28.   while(running) 
  29.   {
  30.     while(shared_stuff->written_by_you == 1) 
  31.     {
  32.       sleep(1);
  33.       printf("waiting for client...\n");
  34.     }
  35.     printf("Enter some text: ");
  36.     fgets(buffer, BUFSIZ, stdin);
  37.     strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
  38.     shared_stuff->written_by_you = 1;
  39.     if (strncmp(buffer, "end", 3) == 0) {
  40.       running = 0;
  41.     }
  42.   }
  43.   if (shmdt(shared_memory) == -1) {
  44.     fprintf(stderr, "shmdt failed\n");
  45.     exit(EXIT_FAILURE);
  46.   }
  47.   exit(EXIT_SUCCESS);
  48. }
运行程序,
将看到如下所示的样本输出:
  1. $ ./shm1 &
  2. [1] 294
  3. Memory attached at 40017000
  4. $ ./shm2
  5. Memory attached at 40017000
  6. Enter some text: hello
  7. You wrote: hello
  8. waiting for client...
  9. waiting for client...
  10. Enter some text: 
  11. You wrote: 
  12. waiting for client...
  13. waiting for client...
  14. waiting for client...
  15. Enter some text: end
  16. You wrote: end
  17. $
程序解析:
消费者程序
创建共享内存段,
然后将它连接到它自己的地址空间中,
并且,
我们在共享内存的开始处使用了一个结构shared_use_st.
该结构中有个标志written_by_you,
当共享内存中有数据写入时,就设置这个标志。
 
这个标志被设置时,
程序就从共享内存中读取文本,
将它打印出来,
然后清除这个标志,表示已经读完数据。
我们用一个特殊字符串end来退出循环。
 
接下来,
程序分离共享内存段并删除它。
 
生产者程序
使用相同的键值1234来取得并连接同一个共享内存段,
然后提示用户输入一些文本。
如果标志written_by_you被设置,
生产者就知道消费都进程还未读完上一次的数据,
因此就继续等待。
当其它进程清除了这个标志后,
生产者写入新的数据并设置这个标志。
它还使用字符串end来终止并分离共享内存段。
 
这里提供的同步标志written_by_you,
它是一个非常缺乏效率的忙等待(不停地循环)。
 
但在实际编程中,
应该使用信号量,
或通过传递消息(使用管道或IPC消息),
或生成信号
的方法来提供读写之间的更有效的同步机制。
 

linux进程间的通信(C): 消息队列

 
一、消息队列(message queue)
消息队列也是System V IPC机制之一。
消息队列与命名管道类似,
但少了打开和关闭管道方面的复杂性。
但使用消息队列并未解决我们在使用命名管道时遇到的一些问题,
如管道满时的阻塞问题。
 
消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。
与命名管道相比,
消息队列的优势在于,它独立于发送和接收进程而存在,
这消除了在同步命名管道的打开和关闭时可能产生的一些困难。
 
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。
而且,每个数据块被认为含有一个类型,
接收进程可以独立地接收含有不同类型值的数据块。
 
好消息是,
A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。
而且,
B. 我们可以用一些方法来提前查看紧急消息。
 
坏消息是,
A. 与管道一样,每个数据块有一个最大长度的限制,
而且,
B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。
 
Linux系统中有两个宏定义:
 MSGMAX, 以字节为单位,定义了一条消息的最大长度。
 MSGMNB, 以字节为单位,定义了一个队列的最大长度。
 
二、函数定义
消息队列的函数定义如下:
  1. #include <sys/msg.h>
  2. int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  3. int msgget(key_t key, int msgflg);
  4. int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
  5. int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);
 
1. msgget函数
我们用msgget函数来创建和访问一个消息队列:
  1. int msgget(key_t key, int msgflg);
参数:
key : 键值,
      和其他IPC机制一样,
      程序必须提供一个键值来命名某个特定的消息队列。
 
      特殊键值IPC_PRIVATE用于创建私有队列,
      从理论上说,它应该只能被当前进程访问。
msgflag: 由9个权限标志组成。
         由IPC_CREAT定义的一个特殊位必须和权限标志按位或才能创建一个新的消息队列。
 
返回值:
成功时,返回一个正整数,即队列标识;
失败时,返回-1.
 
2. msgsnd函数
该函数用来把消息添加到消息队列中:
  1. int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);
消息的结构受两方面约束:
A. 它的长度必须小于系统规定的上限;
B. 它必须以一个长整形成员变量开始,
   接收函数将用这个成员变量来确定消息的类型。
 
当使用消息时,最好把消息结构定义如下:

  1. struct my_message {
  2.   long int message_type;
  3.   /* The data you wish to transfer */
  4. }
由于消息的接收要用到message_type,
所以不能忽略它,
且必须在声明自己的数据结构时包含它,
并且将它初始化为一个已知值。
 
参数:
msqid  : 是由msgget函数返回的消息队列标识符。
msg_ptr: 一个指向准备发达消息的指针,
         这个消息必须以前面说的以一个长整形成员变量开始。
msg_sz : 是msg_ptr指向的消息的长度,
         这个长度不能包括长整形消息类型成员变量的长度。
msgflg : 控制在当前消息队列满或队列消息到达系统范围的限制时,
         将要发生的事情。
 
         IPC_NOWAIT, 函数将立即返回,不发送消息并且返回值为-1.
         如果IPC_NOWAIT标志被消除,则发送进程将挂起,
         以等待队列中腾出可用空间。
 
返回值:
成功时,返回0,消息数据的一份副本将被放到消息队列中;
失败时,返回-1.
 
3. msgrcv函数
该函数从一个消息队列中获取消息:
  1. int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
参数:
msqid  : 由msgget函数返回的消息队列标识符;
msg_ptr: 一个指向准备接收消息的指针,
         和前面介绍的一样,
         要以一个长整形成员变量开始。
msg_sz : 是msg_ptr指向的消息的长度,
         它不包括长整形消息成员变量的长度。
msgtype: 是一个长整形,
         它可以实现一个简单形式的接收优先级。
         == 0, 将获取队列中的第一个可用消息;
         >  0, 将获取具有相同消息类型的第一个消息;
         <  0, 将获取消息类型 <= msgtype的绝对值的第一个消息。
 
如果 只想按消息发送的顺序来接收它们,
     就把msgtype设置为0.
如果 只想获取某一特定类型的消息,
     就把msgtype设置为相应的类型值。
如果 只想接收类型 <= n的消息,
     就把msgtype设置为-n.
 
msgflg: 用于控制当队列中没有相应类型的消息可以接收时将发送的事情。
        如果 msgflg中的IPC_NOWAIT标志被设置,
             函数立即返回-1.
        如果 msgflg中的IPC_NOWAIT标志被消除,
             进程将会挂起以等待一条相应类型的消息到达。
 
返回值:
成功时, 函数返回放到接收缓存区中的字节数,
         消息被复制到由msg_ptr指向的用户分配的缓存中。
         然后删除消息队列中对应的消息。
失败时, 返回-1.
 
4. msgctl函数
该函数用来控制消息队列,
  1. int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid_ds结构至少包含以下成员:
  1. struct msqid_ds {
  2.   uid_t msg_perm.uid;
  3.   uid_t msg_perm.gid
  4.   mode_t msg_perm.mode;
  5. }
参数:
msqid  : 由msgget返回的消息队列标识符。
command: 将要采取的动作,
         它可以取3个值,
IPC_STAT  把msqid_ds结构中的数据设置为消息队列的当前关联值
IPC_SET   如果进程有足够的权限,
          就把消息队列的当着关联值设置为msqid_ds结构中给出的值。
IPC_RMID  删除消息队列
          如果删除消息队列时,
          某个进程正在msgsnd或msgrcv函数中等待,这两个函数将失败。
 
返回值:
成功时,返回0,
失败时,返回-1.
 
三、示例
msg1.c用于接收消息,
msg2.c用于发送消息。
允许两个程序都可以创建消息队列,
但只有接收者在接收完最后一个消息后可以删除它。
 
1. 接收者msg1.c
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include <errno.h>
  5. #include <unistd.h>
  6. #include <sys/msg.h>
  7. struct my_msg_st {
  8.   long int my_msg_type;
  9.   char some_text[BUFSIZ];
  10. };
  11. int main()
  12. {
  13.   int running = 1;
  14.   int msgid;
  15.   struct my_msg_st some_data;
  16.   long int msg_to_receive = 0;
 
首先创建消息队列:
  1.   msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
  2.   if (msgid == -1) 
  3.   {
  4.     fprintf(stderr, "msgget failed with error: %d\n", errno);
  5.     exit(EXIT_FAILURE);
  6.   }
 
然后从队列中获取消息,直到遇见end消息为止。
最后,删除消息队列:
  1.   while(running) 
  2.   {
  3.     if (msgrcv(msgid, (void *)&some_data, BUFSIZ, msg_to_receive, 0) == -1) 
  4.     {
  5.       fprintf(stderr, "msgrcv failed with error: %d\n", errno);
  6.       exit(EXIT_FAILURE);
  7.     }
  8.     printf("You wrote: %s", some_data.some_text);
  9.     if (strncmp(some_data.some_text, "end", 3) == 0) 
  10.     {
  11.       running = 0;
  12.     }
  13.   }
  14.   if (msgctl(msgid, IPC_RMID, 0) == -1) 
  15.   {
  16.     fprintf(stderr, “msgctl(IPC_RMID) failed\n”);
  17.     exit(EXIT_FAILURE);
  18.   }
  19.   exit(EXIT_SUCCESS);
  20. }
 
2. 发送者程序msg2.c
通过调用msgsnd来发送用户输入的文本到消息队列中。
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include <errno.h>
  5. #include <unistd.h>
  6. #include <sys/msg.h>
  7. #define MAX_TEXT 512
  8. struct my_msg_st 
  9. {
  10.   long int my_msg_type;
  11.   char some_text[MAX_TEXT];
  12. };
  13. int main()
  14. {
  15.   int running = 1;
  16.   struct my_msg_st some_data;
  17.   int msgid;
  18.   char buffer[BUFSIZ];
  19.   
  20.   msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
  21.   if (msgid == -1) 
  22.   {
  23.     fprintf(stderr, "msgget failed with error: %d\n", errno);
  24.     exit(EXIT_FAILURE);
  25.   }
  26.   while(running) 
  27.   {
  28.     printf("Enter some text: ");
  29.     fgets(buffer, BUFSIZ, stdin);
  30.     some_data.my_msg_type = 1;
  31.     strcpy(some_data.some_text, buffer);
  32.     if (msgsnd(msgid, (void *)&some_data, MAX_TEXT, 0) == -1) 
  33.     {
  34.       fprintf(stderr, "msgsnd failed\n");
  35.       exit(EXIT_FAILURE);
  36.     }
  37.     if (strncmp(buffer, "end", 3) == 0) 
  38.     {
  39.       running = 0;
  40.     }
  41.   }
  42.   exit(EXIT_SUCCESS);
  43. }
 
不同于管道,
这里不需要进程自己来提供同步方法
这是它的优势所在。
 
假如消息队列中有空间,
发送者可以创建队列,放一些数据到队列中,
然后在接收者启动之前退出。
我们将先行运行发送者msg2.
 
3. 下面是一些样本输出:
  1. $ ./msg2
  2. Enter some text: hello
  3. Enter some text: How are you today?
  4. Enter some text: end
  5. $ ./msg1
  6. You wrote: hello
  7. You wrote: How are you today?
  8. You wrote: end
  9. $
4. 解析:
发送者程序通过msgget来创建一个消息队列,
然后用msgsnd向队列中增加消息。
接收者程序用msgget获得消息队列标识符,
然后开始接收消息,
直到接收到特殊文件end为止。
然后它用msgctl来删除消息队列以完成清理工作。
 

linux进程间的通信(C): 信号量

 
一、信号量简介
信号量: 用于管理对资源的访问。
荷兰计算机科学家Edsger Dijkstra提出的信号量概念
是在并发编程领域迈出的重要一步。
 
信号量是一个特殊的变量,
它只取正数值,
并且程序对其访问都是原子操作。
 
二、信号量的定义
它是一个特殊变量,
只允许对它进行等待(wait)和发送信号(signal)这两种操作,
 P(信号量变量): 用于等待。
 V(信号量变量): 用于发送信号。
 
这两个字母分别源于荷兰语单词
passeren, 传递,      就好像进入临界区域之前的检查点,(或开信号标志, up);
vrijgeven,给予或释放,就好像放弃对临界区域的控制权,  (或关信号标志,down);
 
P(sv)  如果 sv的值 >  零, 就给它减去1;
       如果 sv的值 == 零, 就挂起该进程的执行.
V(sv)  如果 有其它进程因等待sv而被挂起, 就让它恢复运行;
       如果 没有进程因等待sv而被挂起,   就给它加1.
 
信号量分成:
二进制信号量, 取值只能是0和1;
通用信号量,   可以取多个正整数值;
 
三、工作原理
伪码如下:
  1. semaphore sv = 1;
  2. loop forever {
  3.   noncritical code section;
  4.   P(sv);
  5.   critical code section; 
  6.   V(sv);
  7.   noncritical code section;
  8. }
图示如下:
 
四、Linux的信号量机制
所有的Linux信号量函数都是针对成组的通用信号量进行操作,
而不是针对单个的二进制信号量。
 
信号量的函数定义如下所示:

  1. #include <sys/sem.h>
  2. int semget(key_t key, int num_sems, int sem_flags);
  3. int semctl(int sem_id, int sem_num, int command, ...);
  4. int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
1. semget函数
  1. int semget(key_t key, int num_sems, int sem_flags);
 
参数:
key  :    信号量键值,
          一个唯一的非零整数。
          不相关的进程可以通过它访问同一个信号量。
 
num_sems: 信号量数目,   
          它几乎总是取值为1.
sem_flags:一组标志。
          它低端的9个比特是该信号量的权限,其作用类似于文件的访问权限。
          此外,它还可以和值IPC_CREAT做按位或操作,来创建新的信号量。
          
          可以联合使用标志IPC_CREAT和IPC_EXCL
          来确保创建出的是一个新的、唯一的信号量。
          如果该信号量已存在,它将返回一个错误。 
          
返回值:
成功时, 返回一个正数(非零)值,
        它就是其他信号量函数将用到的信号量标识符.
失败进, 返回-1.
    
NOTE: 
它很类似于文件名, 代表程序可能要使用的某个资源,
如果多个程序使用相同的key值, 它将负责协调工作。
类似于文件的使用情况,
不同的进程可以用不同的信号量标识符(信号量变量名)来指向同一个信号量。
 
程序对所有的信号量访问都是间接的,
它先提供一个键,
再由系统生成一个相应的信号量标识符。
只有semget函数才直接使用信号量键,
所有其他的信号量函数都是使用由semget函数返回的信号量标识符。
 
特殊的信号量键值: IPC_PRIVATE,
它的作用是创建一个只有创建者进程才能才可以访问的信号量。
 
 
2. semop函数
用于改变信号量的值。
它的一切动作都是一次性完成的,
这是为了避免出现因使用多个信号量而可能发生的竞争现象。
  1. int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
参数:
sem_id : 由semget返回的信号量标识符。
sem_ops: 指向一个结构数组的指针,
 
每个数组元素至少包含以下成员:
struct sembuf {
  short sem_num;
  short sem_op;
  short sem_flag;
}
sem_num, 信号量编号,
         除非是使用一组信号量,
         否则它的取值一般为0, 表示第一个信号。
sem_op , 信号量在一次操作中需要改变是数值,
         可以用一个非1的数值来改变信号量的值,
         > 0, 这个值为加至semval。
         = 0, semop会等到semval降为0,
              除非sem_flag参数有设为IPC_NOWAIT。
         < 0, 如果 semval >= sem_op的绝对值,        
                   则semval的值会减去sem_op的绝对值。
              如果 semval < sem_op的绝对值 且 sem_flag等于IPC_NOWAIT,
                   则semop()会立即返回错误。
 
         通常只会用到两个值,
         -1, 也就是P操作, 它等待信号量变为可用。
         +1, 也就是V操作, 它发送信号表示信号量现在可用。
 
          
sem_flag,通常被设置成SEM_UNDO。
 
         它将使操作系统跟踪当前进程对这个信号量的修改情况,
         如果 这个进程在没有释放信号量的情况下终止,
              OS将自动释放该进程持有的信号量。
num_sem_ops: 表示结构数组sem_ops的个数。
 
若成功,返回0;
否则,  返回-1, 错误原因保存在errno中。
 
3. semctl函数
用来直接控制信号量信息。
  1. int semctl(int sem_id, int sem_num, int command, ...);
参数:
sem_id : 由semget返回的信号理标识符。
sem_num: 信号量编号,
         当需要用到成组的信号量时,就要用到这个参数,
         它一般取值为0,表示这是第一个也是唯一的一个信号量。
command: 将要采取的动作。
         最常用的有:
         SETVAL: 用来把信号量初始化为一个已知的值。
                 这个值通过union semun中的val成员设置。
                 其作用是在信号量第一次使用之前对它进行设置。
         IPC_RMID: 用于删除一个已经无需继续使用的信号量标识符。
第四个参数: 是一个union semun结构,
            至少包含以下成员:
  1. union semun 
  2. {
  3.   int val;
  4.   struct semid_ds *buf;
  5.   unsigned short *array;
  6. }
返回值:
将根据command参数的不同而返回不同的值。
对于SETVAL和IPC_RMID,
成功时,返回0;
失败进,返回-1;
 
五、示例
用两个不同字符的输出来表示进入和离开临界区域。
如果程序启动时带有一个参数,
它将在进入和退出临界区域时打印字符X; 
而程序的其它运行实例将在进入和退出临界区域时打印字符O;
 
因为在任一给定时刻,只能有一个进程可以进入临界区域,
所以字符X和O应该是成对出现的.
 
(1) 调用semget来创建一个信号量
该函数将返回一个信号量标识符;
如果程序是第一个被调用的(也就是说调用时带有一个参数,argc>1),
就调用set_semvalue初始化信号量
并将op_char设置为X:

  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <sys/sem.h>
  5. #include "semun.h"
  6. static int set_semvalue(void);
  7. static void del_semvalue(void);
  8. static int semaphore_p(void);
  9. static int semaphore_v(void);
  10. static int sem_id; // 信号量标识符
  11. int main(int argc, char *argv[])
  12. {
  13.   int i;
  14.   int pause_time;
  15.   char op_char = ‘O’;
  16.   
  17.   srand((unsigned int)getpid());
  18.   
  19.   /* 创建信号量 */
  20.   sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
  21.   if (argc > 1) 
  22.   {
  23.     if (!set_semvalue()) 
  24.     {
  25.       fprintf(stderr, “Failed to initialize semaphore\n”);
  26.       exit(EXIT_FAILURE);
  27.     }
  28.     op_char = ‘X’;
  29.     sleep(2);
  30.   }
(2) 接下来是一个循环,它进入和离开临界区10次。
在每次循环的开始时,
首先调用semaphore_p函数,
它在程序进入临界区域时设置信号量以等待进入:

  1.   for(i = 0; i < 10; i++) 
  2.   {
  3.     if (!semaphore_p()) // P操作, 等待 
  4.       exit(EXIT_FAILURE);
  5.     
  6.     /* 临界区域代码 */
  7.     printf(“%c”, op_char);fflush(stdout);
  8.     pause_time = rand() % 3;
  9.     sleep(pause_time);
  10.     printf(“%c”, op_char);fflush(stdout);
  11.     /* 临界区域结束 */
(3) 在临界区域之后,
调用semaphore_v来将信号量设置为可用,
然后等待一段随机时间,
再进入下一次循环。
在整个循环语句执行完毕后,
调用del_semvalue函数来清理信号量
  1.     if (!semaphore_v()) // V操作,发送信号
  2.       exit(EXIT_FAILURE);
  3.     pause_time = rand() % 2;
  4.     sleep(pause_time);
  5.   } // end of for
  6.   printf(“\n%d - finished\n”, getpid());
  7.   if (argc > 1) 
  8.   {
  9.     sleep(10);
  10.     del_semvalue(); // 清理信号量
  11.   }
  12.   exit(EXIT_SUCCESS);
  13. } // end of main
 
(4)函数set_semvalue通过调用semctl的command参数SETVAL,
来初始化信号量。
在使用信号量之前必须这样做。

  1. static int set_semvalue(void)
  2. {
  3.   union semun sem_union;
  4.   sem_union.val = 1;
  5.   if (semctl(sem_id, 0, SETVAL, sem_union) == -1) 
  6.     return(0);
  7.   return(1);
  8. }
 
(5)函数del_smvalue通过调用semctl的command参数IPC_RMID,
来删除信号量ID。
实际编程时一次要在执行结束前进行信号量删除,
以防导致下次程序引用时出错。

  1. static void del_semvalue(void)
  2. {
  3.   union semun sem_union;
  4.   if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
  5.     fprintf(stderr, “Failed to delete semaphore\n”);
  6. }
(6)函数semaphore_p对信号做减1操作(等待):

  1. static int semaphore_p(void)
  2. {
  3.   struct sembuf sem_b;
  4.   sem_b.sem_num = 0;
  5.   sem_b.sem_op = -1; /* P() */
  6.   sem_b.sem_flg = SEM_UNDO;
  7.   if (semop(sem_id, &sem_b, 1) == -1) {
  8.     fprintf(stderr, “semaphore_p failed\n”);
  9.     return(0);
  10.   }
  11.   return(1);
  12. }
 
(7)函数semaphore_v将sembuf结构中的sem_op设置为1(释放):

  1. static int semaphore_v(void)
  2. {
  3.   struct sembuf sem_b;
  4.   sem_b.sem_num = 0;
  5.   sem_b.sem_op = 1; /* V() */
  6.   sem_b.sem_flg = SEM_UNDO;
  7.   if (semop(sem_id, &sem_b, 1) == -1) {
  8.     fprintf(stderr, “semaphore_v failed\n”);
  9.     return(0);
  10.   }
  11.   return(1);
  12. }
 
下面是两个程序调用实例时的一些样本输出:

  1. $ cc sem1.c -o sem1
  2. $ ./sem1 1 &
  3. [1] 1082
  4. $ ./sem1
  5. OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
  6. 1083 - finished
  7. 1082 - finished
  8. $
 
NOTE:
如果程序在系统上执行不正常,
可能需要在程序执行之前执行命令
$stty -tostop
以确保产生tty输出后台程序不会引发系统生成一个信号。
 

posted on 2022-03-03 10:09  yipianchuyun  阅读(59)  评论(0编辑  收藏  举报

导航