进程间通信之共享内存

Semaphore 信号量类

==================================================================================================================================================================

Linux进程间通信之共享内存

一,共享内存
  内核管理一片物理内存,允许不同的进程同时映射,多个进程可以映射同一块内存,被多个进程同时映射的物理内存,即共享内存。
  映射物理内存叫挂接,用完以后解除映射叫脱接

1,共享内存的特点:

  优点:是最快的IPC。
  缺点:要编程者自己实现对共享内存互斥访问。如何实现?

2,编程模型:具体函数的用法可以用man手册查看(强力推荐)

进程A: writeshm.c
     1) 获得key, ftok()
     2) 使用key来创建一个共享内存 shmget()
     3) 映射共享内存(得到虚拟地址), shmat()
     4) 使用共享内存, 往共享内存中写入数据
     5) 解除映射 shmdt()
     6) 如果共享内存不再使用,可以使用shmctl()销毁共享内存

 

进程B: readshm.c     

  1) 获得key, ftok()     

  2) 使用key来获得一个共享内存 shmget()     

  3) 映射共享内存(得到虚拟地址), shmat()     

  4) 使用共享内存, 读取共享内存中的数据     

  5) 解除映射 shmdt()     

 

3,实例

进程A:

复制代码

// writeshm.c

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

int main()
{
    // 生成一个key
    key_t key = ftok("./", 66);

    // 创建共享内存,返回一个id
    int shmid = shmget(key, 8, IPC_CREAT|0666|IPC_EXCL);
    if(-1 == shmid)
    {
        perror("shmget failed");
        exit(1);
    }

    // 映射共享内存,得到虚拟地址
    void *p = shmat(shmid, 0, 0);
    if((void*)-1 == p)
    {
        perror("shmat failed");
        exit(2);
    }

    // 写共享内存
    int *pp = p;
    *pp = 0x12345678;
    *(pp + 1) = 0xffffffff;

    // 解除映射
    if(-1 == shmdt(p))
    {
        perror("shmdt failed");
        exit(3);
    }
    printf("解除映射成功,点击回车销毁共享内存\n");
    getchar();

    // 销毁共享内存
    if(-1 == shmctl(shmid, IPC_RMID, NULL))
    {
        perror("shmctl failed");
        exit(4);
    }

    return 0;
}

复制代码


进程B:

复制代码

// readshm.c

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

int main()
{
    // 生成一个key
    key_t key = ftok("./", 66);

    // 获取共享内存,返回一个id
    int shmid = shmget(key, 0, 0);
    if(-1 == shmid)
    {
        perror("shmget failed");
        exit(1);
    }

    // 映射共享内存,得到虚拟地址
    void *p = shmat(shmid, 0, 0);
    if((void*)-1 == p)
    {
        perror("shmat failed");
        exit(2);
    }

    // 读共享内存
    int x = *(int *)p;
    int y = *((int *)p + 1);
    printf("从共享内存中都取了:0x%x 和 0x%x \n", x, y);

    // 解除映射
    if(-1 == shmdt(p))
    {
        perror("shmdt failed");
        exit(3);
    }

    return 0;
}

复制代码

 运行结果:

writeshma:

readshma:

分类: linux进程间通信

 

==========================================================================================================================================================================================================================================================================

 

系统建立IPC通讯 (消息队列信号量共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

ftok

消息队列、信号灯、共享内存常用在Linux服务端编程的进程间通信环境中。而此三类编程函数在实际项目中都是用System V IPC函数实现的。System V IPC函数名称和说明如下表15-1所示。

表15-1 System V IPC函数

 

消息队列

信号灯

共享内存区

头文件

<sys/msg.h>

<sys/sem.h>

<sys/shm.h>

创建或打开IPC函数

msgget

semget

shmget

控制IPC操作的函数

msgctl

semctl

shmctl

IPC操作函数

msgsnd

msgrcv

semop

shmat

shmdt

1.   key_t键和ftok函数

函数ftok把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值(也称IPC key键值)。ftok函数原型及说明如下:

              ftok(把一个已存在的路径名和一个整数标识符转换成IPC键值)

 

 

 

所需头文件

#include <sys/types.h>

#include <sys/ipc.h>

函数说明

把从pathname导出的信息与id的低序8位组合成一个整数IPC键

函数原型

key_t ftok(const char *pathname, int proj_id)

函数传入值

pathname:指定的文件,此文件必须存在且可存取

proj_id:计划代号(project ID)

函数返回值

成功:返回key_t值(即IPC 键值)

出错:-1,错误原因存于error中

附加说明

key_t一般为32位的int型的重定义

 

ftok的典型实现是调用stat函数,然后组合以下三个值:
①    pathname所在的文件系统的信息(stat结构的st_dev成员)。
②    该文件在本文件系统内的索引节点号(stat结构的st_ino成员)。
③    proj_id的低序8位(不能为0)。
上述三个值的组合产生一个32位键。

2.   ftok函数代码举例

ftok.c源代码如下:

 
  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <sys/types.h>

  4. #include <sys/ipc.h>

  5. #include <sys/stat.h>

  6. #include <unistd.h>

  7. int main(int argc, char **argv)

  8. {

  9. struct stat stat1 ;

  10. if ( argc != 2 )

  11. {

  12. printf("usage: ftok < pathname >" ) ;

  13. exit(1) ;

  14. }

  15. stat( argv[1], &stat1 ) ;

  16. printf("st_dev:%lx, st_ino:%lx, key:%x\n", \

  17. (unsigned long)stat1.st_dev, (unsigned long)stat1.st_ino , ftok(argv[1],0x579 )) ;

  18. printf("st_dev:%lx, st_ino:%lx, key:%x\n", \

  19. (unsigned long)stat1.st_dev, (unsigned long)stat1.st_ino , ftok(argv[1],0x118 )) ;

  20. printf("st_dev:%lx, st_ino:%lx, key:%x\n", \

  21. (unsigned long)stat1.st_dev, (unsigned long)stat1.st_ino , ftok(argv[1],0x22 )) ;

  22. printf("st_dev:%lx, st_ino:%lx, key:%x\n", \

  23. (unsigned long)stat1.st_dev, (unsigned long)stat1.st_ino , ftok(argv[1],0x33 )) ;

  24. exit(0) ;

  25. }

编译 gcc ftok.c –o ftok
运行 ./ftok /tmp,执行结果如下:

st_dev:801, st_ino:4db21, key:7901db21
st_dev:801, st_ino:4db21, key:1801db21
st_dev:801, st_ino:4db21, key:2201db21
st_dev:801, st_ino:4db21, key:3301db21
st_dev:801, st_ino:4db21, key:4401db21

从上面程序可以看出,通过ftok返回的是根据文件(pathname)信息和计划编号(proj_id)合成的IPC key键值,从而避免用户使用key值的冲突。proj_id值的意义让一个文件也能生成多个IPC key键值。ftok利用同一文件最多可得到IPC key键值0xff(即256)个,因为ftok只取proj_id值二进制的后8位,即16进制的后两位与文件信息合成IPC key键值。

3.   IPC键值与IPC标识符

(1)key值选择方式

对于key值,应用程序有如下三种选择:
①    调用ftok,给它传递pathname和proj_id,操作系统根据两者合成key值。
②    指定key为IPC_PRIVATE,内核保证创建一个新的、唯一的IPC对象,IPC标识符与内存中的标识符不会冲突。IPC_PRIVATE为宏定义,其值等于0。
③    指定key为大于0的常数,这需要用户自行保证生成的IPC key值不与系统中存在的冲突,而前两种是操作系统保证的。

(2)IPC标识符

给semget、msgget、shmget传入key值,它们返回的都是相应的IPC对象标识符。注意IPC键值和IPC标识符是两个概念,后者是建立在前者之上。图15-1画出了从IPC键值生成IPC标识符图,其中key为IPC键值,由ftok函数生成;ipc_id为IPC标识符,由semget、msgget、shmget函数生成ipc_id在信号量函数中称为semid,在消息队列函数中称为msgid,在共享内存函数中称为shmid,它们表示的是各自IPC对象标识符。

图15-1 从IPC键值生成IPC标识符图

4. ipc_perm结构说明

系统为每一个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者,并确定了一个IPC操作是否可以访问该IPC对象。

 
  1. struct ipc_perm {

  2. key_t key ; /* 此IPC对象的key键 */

  3. uid_t uid ; /* 此IPC对象用户ID */

  4. gid_t gid ; /* 此IPC对象组ID */

  5. uid_t cuid ; /* IPC对象创建进程的有效用户ID */

  6. gid_t cgid ; /* IPC对象创建进程的有效组ID */

  7. mode_t mode ; /* 此IPC的读写权限 */

  8. ulong_t seq ; /* IPC对象的序列号 */

  9. } ;

表15-2列出了ipc_perm中mode的含义,其含义与文件访问权限相似。当调用IPC对象创建函数(semget、msgget、shmget)时,会对ipc_perm结构变量的每一个成员赋值,其中mode的值来源于IPC对象创建函数最右边的形参flag(msgget中为msgflg、semget中为semflg、shmget中shmflg)。如需修改这几个成员变量则需调用相应的控制函数(msgctl、semctl、shmctl)。        
表15-2 IPC对象存取权限表

ipc_perm中mode的含义

操作者

可读可写

用户

0400

0200

0600

0040

0020

0060

其他

0004

0002

0006

5.  IPC对象的创建权限

    msgget、semget、shmget函数最右边的形参flag(msgget中为msgflg、semget中为semflg、shmget中shmflg)为IPC对象创建权限,三种xxxget函数中flag的作用基本相同。
IPC对象创建权限(即flag)格式为0xxxxx,其中0表示8位制,低三位为用户、属组、其他的读、写、执行权限(执行位不使用),其含义与ipc_perm的mode相同,具体含义见表15-2。在这里姑且把IPC对象创建权限格式的低三位称为“IPC对象存取权限”。如0600代表只有此用户下的进程才有可读可写权限。IPC对象存取权限常与下面IPC_CREAT、IPC_EXCL两种标志进行或(|)运算完成对IPC对象创建的管理,在这里姑且把IPC_CREAT、IPC_EXCL两种标志称为IPC创建模式标志。下面是两种创建模式标志在<sys/ipc.h>头文件中的宏定义。

 
  1. #define IPC_CREAT 01000 /* Create key if key does not exist. */

  2. #define IPC_EXCL 02000 /* Fail if key exists. */

综上所述,flag标志由两部分组成,一为IPC对象存取权限(含义同ipc_perm中的mode),一为IPC对象创建模式标志(IPC_CREAT、IPC_EXCL),两者进行|运算合成IPC对象创建权限。

6. 创建或打开IPC对象流程图

semget、msgget、shmget函数的作用是创建一个新的IPC对象或者访问一个已存在的IPC对象。其创建或访问的规则如下:
①    指定key为IPC_PRIVATE操作系统保证创建一个唯一的IPC对象。
②    设置flag参数的IPC_CREAT位但不设置它的IPC_EXCL位时,如果所指定key键的IPC对象不存在,那就是创建一个新的对象;否则返回该对象。
③    同时设置flag的IPC_CREAT和IPC_EXCL位时,如果所指定key键的IPC对象不存在,那就创建一个新的对象;否则返回一个EEXIST错误,因为该对象已存在。
综上所述,flag创建模式标志的作用如下表15-3所示。
表15-3 三种xxxget函数flag的创建模式标志作用表

 

flag创建模式标志

      不存在

       已存在

无特殊标志

出错,errno=ENOENT

成功,引用已存在对象

IPC_CREAT

成功,创建新对象

成功,引用已存在对象

IPC_CREAT|IPC_EXCL

成功,创建新对象

出错,errno=EEXIST

下图15-2画出了semget、msgget、shmget创建或打开一个IPC对象的逻辑流程图,它说明了内核创建和访问IPC对象的流程。

图15-2 semget、msgget、shmget创建或打开一个IPC对象的逻辑流程图
     使用semget、msgget、shmget创建一个IPC对象时,需要指定flag标志,在key不等于IPC_PRIVATE情况下,flag标志决定了创建方式和创建后IPC对象的存取权限。在key等于IPC_PRIVATE情况下,flag标志决定了创建后IPC对象的存取权限。如果只是引用一个已经存在的IPC对象只需把flag标志设为0即可。

有关该函数的三个常见问题:

1.pathname是目录还是文件的具体路径,是否可以随便设置
2.pathname指定的目录或文件的权限是否有要求
3.proj_id是否可以随便设定,有什么限制条件

解答:

   1、ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key,该路径可以随便设置。
    2、该路径是必须存在的,ftok只是根据文件inode在系统内的唯一性来取一个数值,和文件的权限无关。
    3、proj_id是可以根据自己的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255;

关于ftok()函数的一个陷阱

     在使用ftok()函数时,里面有两个参数,即fname和id,fname为指定的文件名,而id为子序列号,这个函数的返回值就是key,它与指定的文件的索引节点号和子序列号id有关,这样就会给我们一个误解,即只要文件的路径,名称和子序列号不变,那么得到的key值永远就不会变。
     事实上,这种认识是错误的,想想一下,假如存在这样一种情况:在访问同一共享内存的多个进程先后调用ftok()时间段中,如果fname指向的文件或者目录被删除而且又重新创建,那么文件系统会赋予这个同名文件新的i节点信息,于是这些进程调用的ftok()都能正常返回,但键值key却不一定相同了。由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对象进行数据传输的目 的将无法实现。
      这是一个很重要的问题,希望能谨记!!!
     所以要确保key值不变,要么确保ftok()的文件不被删除,要么不用ftok(),指定一个固定的key值。

 

摘录自《深入浅出Linux工具与编程》

====================================================================================================================================================================================================================================================================================================================================================================

 

Linux进程间通信(五):信号量 semget()、semop()、semctl()

这篇文章将讲述别一种进程间通信的机制——信号量。注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物。有关信号的更多内容,可以阅读我的另一篇文章:Linux进程间通信 -- 信号。下面就进入信号量的讲解。

一、什么是信号量

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

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即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()函数

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

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()函数

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

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

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

1

2

3

4

5

6

7

struct sembuf{

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

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

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

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

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

};

3、semctl()函数

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

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

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

1

2

3

4

5

union semun {

    int val;

    struct semid_ds *buf;

    unsigned short *arry;

};

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

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

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

四、进程使用信号量通信

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <sys/sem.h>

 

//union semun

//{

//  int val;

//  struct semid_ds *buf;

//  unsigned short *arry;

//};

 

static int sem_id = 0;

static int set_semvalue();

static void del_semvalue();

static int semaphore_p();

static int semaphore_v();

 

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

{

    char message = 'X';

    int i = 0;

 

    // 创建信号量

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

 

    if (argc > 1)

    {

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

        if (!set_semvalue())

        {

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

            exit(EXIT_FAILURE);

        }

 

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

        message = argv[1][0];

        sleep(2);

    }

 

    for (i = 0; i < 10; ++i)

    {

        // 进入临界区

        if (!semaphore_p())

        {

            exit(EXIT_FAILURE);

        }

 

        // 向屏幕中输出数据

        printf("%c", message);

 

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

        fflush(stdout);

        sleep(rand() % 3);

 

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

        printf("%c", message);

        fflush(stdout);

 

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

        if (!semaphore_v())

        {

            exit(EXIT_FAILURE);

        }

        sleep(rand() % 2);

    }

 

    sleep(10);

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

 

    if (argc > 1)

    {

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

        sleep(3);

        del_semvalue();

    }

    exit(EXIT_SUCCESS);

}

 

static int set_semvalue()

{

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

    union semun sem_union;

 

    sem_union.val = 1;

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

    {

        return 0;

    }

    return 1;

}

 

static void del_semvalue()

{

    // 删除信号量

    union semun sem_union;

 

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

    {

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

    }

}

 

static int semaphore_p()

{

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

    struct sembuf sem_b;

    sem_b.sem_num = 0;

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

    sem_b.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sem_b, 1) == -1)

    {

        fprintf(stderr, "semaphore_p failed\n");

        return 0;

    }

 

    return 1;

}

 

static int semaphore_v()

{

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

    struct sembuf sem_b;

    sem_b.sem_num = 0;

    sem_b.sem_op = 1; // V()

    sem_b.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sem_b, 1) == -1)

    {

        fprintf(stderr, "semaphore_v failed\n");

        return 0;

    }

 

    return 1;

}

运行结果如下:

注:这个程序的临界区为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,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

#include <stdio.h>

#include <stdlib.h>

 

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

{

    char message = 'X';

    int i = 0;

    if (argc > 1)

    {

        message = argv[1][0];

    }

     

    for (i = 0; i < 10; ++i)

    {

        printf("%c", message);

        fflush(stdout);

        sleep(rand() % 3);

        printf("%c", message);

        fflush(stdout);

        sleep(rand() % 2);

    }

    sleep(10);

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

     

    exit(EXIT_SUCCESS);

}

运行结果如下:

例子分析:

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

六、信号量的总结

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

 


 

注意:

例子中的进程分为两种,一种是运行时带有命令行参数的进程,一种是运行时不带有参数的进程。前者负责信号量的创建、设置初值并销毁,而后者直接获得并使用前者创建并设置好的信号量。

 

 

参考:

http://blog.csdn.net/ljianhui/article/details/10243617

《Linux 高性能服务器编程》

 

===========================================================================================================================================================================================================================================================================

线程同步之semaphore信号量,代码实现

2016年07月08日 00:19:11 CV_ML_DP 阅读数:3298

版权声明:本文为博主原创文章,未经博主允许不得转载,转载请注明文章来源,联系方式:vipsummer@139.com https://blog.csdn.net/u012421852/article/details/51855267

一般在任务处理线程池中会有一个公共任务队列m_event_list,

任务监视线程有任务添加到m_event_list时,可以通过semaphore.post()增加信号量数来唤醒在semaphore信号量上的睡眠任务处理线程

简要代码为:

 
  1. void add_event(Event &e)

  2.  
  3. {

  4.  
  5.     m_event_list.push_back(e);

  6.  
  7.     m_semaphore.post(); //唤醒在信号量队列上处于阻塞状态的任务处理线程来处理event

  8.  
  9. }

在每个任务线程的线程执行体threadProc()中,通过semaphore.pend()消费信号量来接收任务并处理任务,如果任务队列中没有任务要处理,则当前线程会被阻塞

简要代码为:

 
  1. void threadProc()

  2.  
  3. {

  4.  
  5. while(looping())

  6.  
  7. {

  8.  
  9. if (m_event_list.empty() != false)

  10.  
  11. {

  12.  
  13. m_semaphore.pend();

  14.  
  15. Event e = m_event_list.front();

  16.  
  17. m_event_list.pop_front();

  18.  
  19. handle_event(e);

  20.  
  21. }//if

  22.  
  23. //do other thing

  24.  
  25. }//while

  26.  
  27. }

注:此处未做锁同步处理,在实际的使用过程当中一定要对m_event_list进行加锁同步。

注:还有一点,在实际项目中,主线程和子线程们之间用信号量进行同步,而用关键段来处理子线程们之间的互斥!

 

系统内核semaphore接口如下所示:

 

下面给出自定义的semaphore功能代码,如下所示,有不对地方请指教。

 

 
  1. //Semaphore.h

  2. #ifndef _SEMAPHORE_H__

  3. #define _SEMAPHORE_H__

  4.  
  5. #include <semaphore.h>

  6.  
  7. using namespace std;

  8. typedef unsigned int uint32_t;

  9.  
  10. struct internal

  11. {

  12. sem_t* m_sem;

  13. };

  14. /// \class CSemaphore 信号量类

  15.  
  16. class CSemaphore {

  17. private:

  18. CSemaphore(CSemaphore const&);

  19. CSemaphore& operator=(CSemaphore const&);

  20. public:

  21. /// 构造函数,创建系统信号量,initCnt表示信号量的初始计数

  22. explicit CSemaphore(int initCnt = 0);

  23.  
  24. /// 析构函数,销毁系统信号量

  25. ~CSemaphore();

  26.  
  27. /// 消费信号量,当信号量为0时线程再消费信号量,线程就会被阻塞,进入信号量等待队列的队尾

  28. /// \return pend操作后当前信号量计数

  29. int pend();

  30.  
  31. /// 生产信号量,如果信号量为0时有线程生产信号量,会唤醒信号量其等待队列的第一个线程

  32. /// \return post操作后当前信号量计数

  33. int post();

  34.  
  35. /// 消费信号量,当信号量为0时线程再消费信号量,线程就会被阻塞,直到超时(毫秒)

  36. /// \return >=0表示当前信号量计数,-1:表示超时

  37. int pend(uint32_t timeout);

  38.  
  39. /// 尝试减少信号量,如果信号量已经为0则马上返回

  40. /// \return 0:信号量减少成功,-1:信号量减少失败

  41. int tryPend();

  42.  
  43. private:

  44. struct internal* m_internal;

  45. };//class CSemaphore

  46.  
  47. #endif

 

 

 

 
  1. //Semaphore.cpp

  2. #include "semaphore.h"

  3. #include <stdio.h>

  4. #include <stdlib.h>

  5. #include <string.h>

  6. #include <errno.h>

  7. #include <unistd.h>

  8.  
  9. #define PRINT_LINE_INFO \

  10. do {\

  11. printf("%s:%d:%s\n", __FILE__, __LINE__, __FUNCTION__);\

  12. }while(0);

  13.  
  14. #define SEM m_internal->m_sem

  15.  
  16. CSemaphore::CSemaphore(int initCnt)

  17. :m_internal(new struct internal)

  18. {

  19. SEM = (sem_t*)malloc(sizeof(sem_t));

  20. memset (SEM, 0, sizeof(sem_t));

  21. sem_init(SEM, 0, initCnt);

  22. PRINT_LINE_INFO;

  23. }

  24.  
  25. CSemaphore::~CSemaphore()

  26. {

  27. sem_destroy(SEM);

  28. free(SEM);

  29. delete m_internal, m_internal = NULL;

  30. PRINT_LINE_INFO;

  31. }

  32.  
  33. int CSemaphore::pend()

  34. {

  35. int ret = 0;

  36. do {

  37. PRINT_LINE_INFO;

  38. ret = sem_wait(SEM);

  39. }while(ret != 0 && errno == EINTR);

  40.  
  41. return ret;

  42. }

  43.  
  44. int CSemaphore::post()

  45. {

  46. PRINT_LINE_INFO;

  47. return sem_post(SEM);

  48. }

  49.  
  50. int CSemaphore::pend(uint32_t timeout)

  51. {

  52. //此处不使用sem_timedwait函数的原因是sem_timedwait使用系统时间,

  53. //假如系统时间被修改可能导致sem_timeout的超时机制混乱

  54. int times = (timeout + 1) / 10;

  55. int ret = 0;

  56. while ((ret = tryPend() != 0) && (times-- > 0))

  57. {

  58. PRINT_LINE_INFO;

  59. ::usleep(1000);

  60. }

  61.  
  62. return ret;

  63. }

  64.  
  65. int CSemaphore::tryPend()

  66. {

  67. int ret = sem_trywait(SEM);

  68. if (ret == -1 && errno == EAGAIN)

  69. return -1;

  70. if (ret == 0)

  71. return 0;

  72.  
  73. return 1;

  74. }

 

 

 

 
  1. //Main.cpp

  2. #include "Semaphore.h"

  3. int main()

  4. {

  5. CSemaphore sem(0);

  6. sem.pend(10 * 1000);

  7. sem.pend();

  8.  
  9. return 0;

  10. }

 

 

运行结果如下所示:

 

(完)

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值