Linux进程同步之POSIX信号量(非原创)

来源:互联网 发布:数据库系统由什么构成 编辑:程序博客网 时间:2024/04/30 11:26

POSIX 信号量是属于 POSIX 标准系统接口定义的实时扩展部分。在 SUS Single UNIX Specification )单一规范中,定义的 XSI IPC 中也同样定义了人们通常称为 System V 信号量的系统接口。信号量作为进程间同步的工具是很常用的一种同步IPC 类型。

在《 UNIX 网络编程 卷 2 :进程间通信》的前言第二页与第 1 版的区别中作者提到“ POSIX IPC 函数时大势所趋,因为他们比 System V 中的相应部分更具有优势”,这里所说的优势我还得慢慢领会呀。。。 <T_T>

信号量是一种用于不同进程间进行同步的工具,当然对于进程安全的对于线程也肯定是安全的,所以信号量也理所当然可以用于同一进程内的不同线程的同步。

有了互斥量和条件变量还提供信号量的原因 是:信号量的主要目的是提供一种进程间同步的方式。这种同步的进程可以共享也可以不共享内存区。虽然信号量的意图在于进程间的同步,互斥量和条件变量的意图在于线程间同步,但信号量也可用于线程间同步,互斥量和条件变量也可通过共享内存区进行进程间同步。 但应该根据具体应用考虑到效率和易用性进行具体的选择。

1 POSIX 信号量的操作

POSIX 信号量有两种: 有名信号量 和 无名信号量 ,无名信号量也被称作基于内存的信号量。有名信号量通过 IPC 名字进行进程间的同步,而无名信号量如果不是放在进程间的共享内存区中,是不能用来进行进程间同步的,只能用来进行线程同步。

POSIX 信号量有三种操作:

(1) 创建一个信号量 。创建的过程还要求初始化信号量的值。

根据信号量取值(代表可用资源的数目)的不同, POSIX 信号量还可以分为:

  • 二值信号量 :信号量的值只有 0  1 ,这和互斥量很类型,若资源被锁住,信号量的值为 0 ,若资源可用,则信号量的值为 1 
  • 计数信号量 :信号量的值在 0 到一个大于 1 的限制值( POSIX 指出系统的最大限制值至少要为 32767 )。该计数表示可用的资源的个数。

(2)等待一个信号量( wait  。该操作会检查信号量的值,如果其值小于或等于0 ,那就阻塞,知道该值变成大于 0 ,然后等待进程将信号量的值减 1 ,进程获得共享资源的访问权限。这整个操作必须是一个原子操作。该操作还经常被称为 P 操作(荷兰语 Proberen ,意为:尝试)。

(3)挂出一个信号量( post  。该操作将信号量的值加 1 ,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。该操作也必须是一个原子操作。该操作还经常被称为 V 操作(荷兰语 Verhogen ,意为:增加) 

下面演示经典的生产者消费者问题,单个生产者和消费者共享一个缓冲区;

下面是生产者和消费者同步的伪代码:


//信号量的初始化get = 0;//表示可读资源的数目put = 1;//表示可写资源的数目//生产者进程                               //消费者进程for(; ;){                                    for(; ;){Sem_wait(put);                                 Sem_wait(get);写共享缓冲区;                               读共享缓冲区;Sem_post(get);                                 Sem_post(put);}                                           }
上面的代码大致流程如下:当生产者和消费者开始都运行时,生产者获取put 信号量,此时 put  1 表示有资源可用,生产者进入共享缓冲区,进行修改。而消费者获取 get信号量,而此时 get  0 ,表示没有资源可读,于是消费者进入等待序列,直到生产者生产出一个数据,然后生产者通过挂出 get 信号量来通知等待的消费者,有数据可以读。

很多时候信号量和互斥量,条件变量三者都可以再某种应用中使用,那这三者的差异有哪些呢,下面列出了这三者之间的差异 :

  • 互斥量必须由给它上锁的线程解锁。而信号量不需要由等待它的线程进行挂出,可以在其他进程进行挂出操作。
  • 互斥量要么被锁住,要么是解开状态,只有这两种状态。而信号量的值可以支持多个进程成功进行 wait 操作。
  • 信号量的挂出操作总是被记住,因为信号量有一个计数值,挂出操作总会将该计数值加 1 ,然而当向条件变量发送一个信号时,如果没有线程等待在条件变量,那么该信号会丢失。

2 POSIX 信号量函数接口

POSIX 信号量的函数接口如下图所示:


2.1 有名信号量的创建和删除
#include <semaphore.h>sem_t *sem_open(const char *name, int oflag);sem_t *sem_open(const char *name, int oflag,                  mode_t mode, unsigned int value);                              //成功返回信号量指针,失败返回SEM_FAILED

sem_open 用于创建或打开一个信号量,信号量是通过 name 参数即信号量的名字来进行标识的。关于 POSIC IPC 的名字可以参考《 UNIX 网络编程 卷 2 :进程间通信》 P14 

oflag 参数可以为: 0  O_CREAT  O_EXCL 。如果为 0 表示打开一个已存在的信号量,如果为 O_CREAT ,表示如果信号量不存在就创建一个信号量,如果存在则打开被返回。此时 mode  value 需要指定。如果为 O_CREAT | O_EXCL ,表示如果信号量已存在会返回错误。

mode 参数用于创建信号量时,表示信号量的权限位,和 open 函数一样包括:S_IRUSR  S_IWUSR  S_IRGRP  S_IWGRP  S_IROTH  S_IWOTH 

value 表示创建信号量时,信号量的初始值。

#include <semaphore.h>int sem_close(sem_t *sem);int sem_unlink(const char *name);                              //成功返回0,失败返回-1

sem_close 用于关闭打开的信号量。当一个进程终止时,内核对其上仍然打开的所有有名信号量自动执行这个操作。调用 sem_close 关闭信号量并没有把它从系统中删除它, POSIX 有名信号量是随内核持续的。即使当前没有进程打开某个信号量它的值依然保持。直到内核重新自举或调用 sem_unlink() 删除该信号量。

sem_unlink 用于将有名信号量立刻从系统中删除,但信号量的销毁是在所有进程都关闭信号量的时候。

2.2 信号量的 P 操作
#include <semaphore.h>int sem_wait (sem_t *sem);#ifdef __USE_XOPEN2Kint sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);#endifint sem_trywait (sem_t * sem);                              //成功返回0,失败返回-1
sem_wait() 用于获取信号量,首先会测试指定信号量的值,如果大于 0 ,就会将它减1 并立即返回,如果等于 0 ,那么调用线程会进入睡眠,指定信号量的值大于 0.

sem_trywait  sem_wait 的差别是,当信号量的值等于 0 的,调用线程不会阻塞,直接返回,并标识 EAGAIN 错误。

sem_timedwait  sem_wait 的差别是当信号量的值等于 0 时,调用线程会限时等待。当等待时间到后,信号量的值还是 0 ,那么就会返回错误。其中 struct timespec *abs_timeout 是一个绝对时间,具体可以参考 条件变量关于等待时间的使用

2.3 信号量的 V 操作
#include <semaphore.h>int sem_post(sem_t *sem);                            //成功返回0,失败返回-1

当一个线程使用完某个信号量后,调用 sem_post ,使该信号量的值加 1 ,如果有等待的线程,那么会唤醒等待的一个线程。

2.4 获取当前信号量的值
#include <semaphore.h>int sem_getvalue(sem_t *sem,  int *sval);                            //成功返回0,失败返回-1

该函数返回当前信号量的值,通过 sval 输出参数返回,如果当前信号量已经上锁,那么返回值为 0 ,或为负数,其绝对值就是等待该信号量解锁的线程数。

下面测试在 Linux 下的信号量是否会出现负值:

#include <iostream>#include <unistd.h>#include <semaphore.h>#include <fcntl.h>using namespace std;#define SEM_NAME "/sem_name"sem_t *pSem;void * testThread (void *ptr){    sem_wait(pSem);    sleep(10);    sem_close(pSem);}int main(){    pSem = sem_open(SEM_NAME, O_CREAT, 0666, 5);    pthread_t pid;    int semVal;    for (int i = 0; i < 7; ++i)    {        pthread_create(&pid, NULL, testThread, NULL);        sleep(1);        sem_getvalue(pSem, &semVal);         cout<<"semaphore value:"<<semVal<<endl;    }    sem_close(pSem);    sem_unlink(SEM_NAME);}

执行结果如下:

semaphore value:4semaphore value:3semaphore value:2semaphore value:1semaphore value:0semaphore value:0semaphore value:0

这说明在 Linux  2.6.18中POSIX 信号量是不会出现负值的。

2.5 无名信号量的创建和销毁
#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);                            //若出错则返回-1int sem_destroy(sem_t *sem);                            //成功返回0,失败返回-1

sem_init() 用于无名信号量的初始化。无名信号量在初始化前一定要子啊内存中分配一个 sem_t 信号量类型,这就是无名信号量又称为基于内存的信号量的原因。

sem_init() 第一个参数是指向一个已经分配的 sem_t 变量。第二个参数 pshared 表示该信号量是否由于进程间通步,当 pshared  = 0 ,那么表示该信号量只能用于进程内部的线程间的同步。当 pshared  != 0 ,表示该信号量存放在共享内存区中,使使用它的进程能够访问该共享内存区进行进程同步。第三个参数 value 表示信号量的初始值。

这里需要注意的是, 无名信号量不使用任何类似 O_CREAT 的标志,这表示sem_init() 总是会初始化信号量的值,所以对于特定的一个信号量,我们必须保证只调用 sem_init() 进行初始化一次,对于一个已初始化过的信号量调用 sem_init() 的行为是未定义的 。 如果信号量还没有被某个线程调用还好,否则基本上会出现问题。

使用完一个无名信号量后,调用 sem_destroy 摧毁它。这里要注意的是: 摧毁一个有线程阻塞在其上的信号量的行为是未定义的 

2.6 有名和无名信号量的持续性

有名信号量是随内核持续的 。当有名信号量创建后,即使当前没有进程打开某个信号量它的值依然保持。直到内核重新自举或调用 sem_unlink() 删除该信号量。

无名信号量的持续性要根据信号量在内存中的位置:

  • 如果无名信号量是在 单个进程内部的数据空间 中,即信号量只能在进程内部的各个线程间共享,那么信号量是 随进程的持续性 ,当进程终止时它也就消失了。
  • 如果无名信号量位于 不同进程的共享内存区 ,因此只要该共享内存区仍然存在,该信号量就会一直存在。所以此时无名信号量是 随内核的持续性 

2.7 信号量的继承和销毁

( 1 )继承

对于有名信号量在父进程中打开的任何有名信号量在子进程中仍是打开的。即下面代码是正确的:

sem_t *pSem;pSem = sem_open(SEM_NAME, O_CREAT, 0666, 5);if(fork() == 0){    //...    sem_wait(pSem);    //...}

对于无名信号量的继承要根据信号量在内存中的位置:

  • 如果无名信号量是在 单个进程内部的数据空间 中,那么信号量就是进程数据段或者是堆栈上,当 fork 产生子进程后,该信号量只是原来的一个拷贝,和之前的信号量是独立的。下面是测试代码:
int main(){    sem_t mSem;    sem_init(&mSem, 0, 3);    int val;    sem_getvalue(&mSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;    sem_wait(&mSem);    sem_getvalue(&mSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;    if(fork() == 0)    {           sem_getvalue(&mSem, &val);        cout<<"child:semaphore value:"<<val<<endl;          sem_wait(&mSem);        sem_getvalue(&mSem, &val);        cout<<"child:semaphore value:"<<val<<endl;        exit(0);    }    sleep(1);    sem_getvalue(&mSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;}

测试结果如下:

parent:semaphore value:3parent:semaphore value:2child:semaphore value:2child:semaphore value:1parent:semaphore value:2
  • 如果无名信号量位于 不同进程的共享内存区 ,那么 fork 产生的子进程中的信号量仍然会存在该共享内存区,所以该信号量仍然保持着之前的状态。

( 2 )销毁

对于 有名信号量 ,当某个持有该信号量的进程没有解锁该信号量就终止了,内核并不会将该信号量解锁。这跟记录锁不一样。

对于 无名信号量 ,如果信号量位于进程内部的内存空间中,当进程终止后,信号量也就不存在了,无所谓解锁了。如果信号量位于进程间的共享内存区中,当进程终止后,内核也不会将该信号量解锁。

下面是测试代码:

int main(){    sem_t *pSem;    pSem = sem_open(SEM_NAME, O_CREAT, 0666, 5);    int val;    sem_getvalue(pSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;       if(fork() == 0)    {           sem_wait(pSem);        sem_getvalue(pSem, &val);        cout<<"child:semaphore value:"<<val<<endl;        exit(0);    }    sleep(1);    sem_getvalue(pSem, &val);    cout<<"parent:semaphore value:"<<val<<endl;    sem_unlink(SEM_NAME);}

下面是测试结果:

parent:semaphore value:5child:semaphore value:4parent:semaphore value:4

2.8 信号量代码测试

对于有名信号量在父进程中打开的任何有名信号量在子进程中仍是打开的。即下面代码是正确的:

对于信号量用于进程间同步的代码的测试,我没有采用经典的生产者和消费者问题,原因是这里会涉及到共享内存的操作。我只是简单的用一个同步文件操作的例子进行描述。 在下面的测试代码中, POSIX 有名信号量初始值为 2 ,允许两个进程获得文件的操作权限。代码如下:

#include <iostream>#include <fstream>#include <cstdlib>#include <unistd.h>#include <semaphore.h>#include <fcntl.h>using namespace std;#define SEM_NAME "/sem_name"void semTest(int flag){     sem_t *pSem;    pSem = sem_open(SEM_NAME, O_CREAT, 0666, 2);    sem_wait(pSem);    ofstream fileStream("./test.txt", ios_base::app);      for (int i = 0; i < 5; ++i)      {          sleep(1);          fileStream<<flag;          fileStream<<' '<<flush;      }      sem_post(pSem);    sem_close(pSem);}int main(){   for (int i = 1; i <= 3; ++i)   {       if (fork() == 0)       {           semTest(i);           sleep(1);           exit(0);       }   }}



程序的运行结果,“ ./test.txt ”文件的内容如下:

//./test.txt

1 2 1 2 1 2 1 2 1 2 3 3 3 3 3




程序的运行结果,“ ./test.txt ”文件的内容如下:

//./test.txt

1 2 1 2 1 2 1 2 1 2 3 3 3 3 3


1、概述

  信号量(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。Posix信号量分为有名信号量和无名信号量(也叫基于内存的信号量)。

2、Posix有名信号量

  有名信号量既可以用于线程间的同步也可以用于进程间的同步。

1)由sem_open来创建一个新的信号量或打开一个已存在的信号量。其格式为:

sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value); 
返回:若成功则为指向信号量的指针,若出错则为SEM_FAILED 其中,第三、四个参数可以没有,主要看第二个参数如何选取。
oflag参数:可以是0、O_CREAT或O_CREAT|O_EXCL。如果指定O_CREAT标志而没有指定O_EXCL,那么只有当所需的信号量尚未存在时才初始化它。但是如果所需的信号量已经存在也不会出错。 但是如果在所需的信号量存在的情况下指定O_CREAT|O_EXCL却会报错。
mode参数:指定权限位。
value参数:指定信号量的初始值。该初始值不能超过SEM_VALUE_MAX(这个常值必须至少为32767).二值信号量的初始值通常为1,计数信号量的初始值则往往大于1。
用sem_close来关闭该信号量。

创建一个新的信号量程序如下:

复制代码
 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <errno.h> 6 #include <fcntl.h> 7  8 //创建模式权限 9 #define FILE_MODE  (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)10 11 int main(int argc,char *argv[])12 {13     int     c, flags;14     sem_t   *sem;15     unsigned int value;16     flags = O_RDWR | O_CREAT;17     value = 1; //初始化信号量的值为1,即二元信号量18     while((c = getopt(argc,argv,"ei:"))!= -1)19     {20         switch(c)21         {22             case 'e':23                 flags |= O_EXCL;24                 break;25             case 'i':26                 value = atoi(optarg);  //获取信号量的值27                 break;28         }29     }30     if(optind != argc -1)31     {32         printf("usage: semcreate [-e] [-i initalvalue] <name>");33         exit(0);34     }35     //创建信号量,返回sem_t类型指针36     if((sem = sem_open(argv[optind],flags,FILE_MODE,value)) == SEM_FAILED)37     {38         perror("sem_open() error");39         exit(-1);40     }41     //关闭打开的信号量42     sem_close(sem);43     exit(0);44 }
复制代码

2)使用sem_unlink删除信号量:
int sem_unlink(const char *name); 返回:成功返回0,出错返回-1

删除信号量程序如下:

复制代码
 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <errno.h> 6 #include <fcntl.h> 7  8 int main(int argc,char *argv[]) 9 {10     if(argc != 2)11     {12         printf("usage: semunlink<name>");13         exit(0);14     }15         //从系统中删除信号量16     if(sem_unlink(argv[1]) == -1)17     {18         perror("sem_unlink() error");19         exit(-1);20     }21     exit(0);22 }
复制代码

3)获取信号量的当前值:
int sem_getvalue(sem_t *sem,int *valp); 返回:成功返回0,出错返回-1
sem_getvalue在由valp指向的整数中返回所指定信号量的当前值。如果信号量当前已上锁,那么返回值或为0,或为某个负数,绝对值即为等待等待该信号量解锁的线程数。

获取信号量的值程序如下:

复制代码
 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <errno.h> 6 #include <fcntl.h> 7  8 int main(int argc,char *argv[]) 9 {10     sem_t  *sem;11     int  val;12     if(argc != 2)13     {14         printf("usage: semgetvalue<name>");15         exit(0);16     }17     //打开一个已经存在的有名信号量18     sem = sem_open(argv[1],0);19      //获取信号量的值20     sem_getvalue(sem,&val);21     printf("value = %d\n",val);22     exit(0);23 }
复制代码

4)信号量的等待:(P操作,也称为递减down 或 上锁lock)
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem); 
返回:成功返回0,出错返回-1
sem_wait函数测试所指定信号量的值,如果该值大于0,就将它的值减1并立即返回;如果该值等于0,调用线程就被投入睡眠中,直到该值变为大于0,这时再将它减1,函数随后返回。“测试并减1”操作必须是原子的。sem_wait和sem_trywait的差别是:当所指定信号量的值已经是0时,后者并不将调用的进程投入睡眠。相反,它返回一个EAGAIN错误。如果被某个信号中断,sem_wait就可能过早的返回,返回的错误为EINTR。

等待信号量程序如下:

复制代码
 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <errno.h> 6 #include <fcntl.h> 7  8 int main(int argc,char *argv[]) 9 {10     sem_t *sem;11     int val;12     if(argc != 2)13     {14         printf("usage: semwait<name>");15         exit(0);16     }17         //打开已经存在的信号量18     sem = sem_open(argv[1],0);19         //等待20     sem_wait(sem);21         //获取信号量的值22     sem_getvalue(sem,&val);23     printf("pid %ld has semaphore,value = %d\n",(long) getpid(),val);24     pause();25     exit(0);26 }
复制代码

5)信号量挂出(V操作,也称为递增up 或解锁unlock)
int sem_post(sem_t *sem);返回:成功返回0,出错返回-1 将所指定的信号量值加1

信号量挂出程序如下:

复制代码
 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <errno.h> 6 #include <fcntl.h> 7  8 int main(int argc,char *argv[]) 9 {10     sem_t     *sem;11     int     val;12     if(argc != 2)13     {14         printf("usage: semopt <name>");15         exit(0);16     }17     //打开已经存在的信号量18     sem = sem_open(argv[1],0);19     //信号量挂出20     sem_post(sem); 21     //获取挂出后的信号量值22     sem_getvalue(sem,&val);23     printf("value = %ld\n",val);24     exit(0);25 }
复制代码

在Centos上测试Posix信号量如下:

3、采用Posix信号量实现生产者-消费者问题

  对生产者-消费者问题进行扩展,把共享缓冲区用作一个环绕缓冲区,即生产者填写最后一项后回头来填写第一项,消费者也这么操作。此时需要维持三个条件:

(1)当缓冲区为空时,消费者不能试图从其中去除一个条目

(2)当缓冲区填满时,生产者不能试图往其中放置一个条目

(3)共享变量可能描述缓冲区的当前状态(下标、计数和链表指针),因此生产者和消费者的所有缓冲区操作都必须保护起来,以避免竞争。

给出使用信号量的方案展示三种不同类型的信号量:

(1)定义mutex二元信号量保护两个临界区。

(2)定义nempty的计数信号量统计共享缓冲区中的空槽位数。

(3)定义nstored的计数信号量统计共享缓冲区中已填写的槽位数。

实现单个生产者和单个消费者的情况,程序如下所示:

View Code

程序执行结果如下:

result文件内容如下:生产者和消费者公用缓冲区。

4、Posix基于内存的信号量

  Posix有名信号量创建时候是用一个name参数标识,它通常指代文件系统中的某个文件。而基于内存的信号量是由应用程序分配信号量的内存空间,即分配一个sem_t数据类型的内存空间,然后由系统初始化它们的值。操作函数如下:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);   //初始化内存信号量
int sem_destroy(sem_t *sem);   //摧毁信号量

如果shared=0,那么待初始化的信号量是在同一进程的各个线程间共享的,否则该信号量是在进程间共享的,此时该信号量必须存放在某种类型的共享内存区中,使得用它的进程能够访问该共享内存区。value是该信号量的初始值。

现在采用基于内存的信号量实现生产者-消费者问题,单个生产者和单个消费者,程序如下所示:

View Code

程序执行结果与上面一致。

5、多个生产者、单个消费者

  针对这种情况,不仅要考虑生产者与消费者之间的同步,而且还要考虑多个生产者之间的互斥。生产者中同时获取nempty信号量可以有多个,但是每个时刻只能有一个生产者能获取mutex信号量。修改缓冲区结构如下:

复制代码
struct{    int         buff[NBUFF];  //缓冲区    int         nput;      //待存入缓冲区下标    int         nputval;    // 待存入的值    sem_t     mutex,nempy,nstored;  //基于内存的信号量}shared;
复制代码

添加nput和nputval用于同步多个生产者线程。实现程序如下:

View Code

程序测试结果如下:

6、多个生产者、多个消费者

  这种情况需要考虑多个生产者之间的同步和多个消费者之间的同步,修改缓冲区结构如下所示:

复制代码
struct{    int buff[NBUFF];    int nput;     //生产者产生新条目的下标    int nputval;    int nget;      //消费者移除条目的下标    int ngetval;    sem_t mutex,nempty,nstored;}shared;
复制代码

实现程序如下:

View Code

测试结果如下:













程序的运行结果,“ ./test.txt ”文件的内容如下:

//./test.txt

1 2 1 2 1 2 1 2 1 2 3 3 3 3 3

0 0
原创粉丝点击