首页 > C/C++ > 线程 > Linux多线程编程(三)信号量
2016
11-09

Linux多线程编程(三)信号量

三、 信号量

  1、 基本概念

  2、 API

    1)、 创建一个新的命名信号量或者打开已经存在的命名信号量

    2)、 关闭指定的命名信号量

    3)、 删除指定的命名信号量

    4)、 增加信号量(V操作)

    5)、 基于信号量的计数值阻塞

    6)、 尝试基于信号量的计数值阻塞

    7)、 返回指定信号量的当前计数值

    8)、 初始化未命名信号

    9)、 销毁未命名信号

 

 

三、信号量

1、基本概念

信号量是用来在进程间或者一个进程的多个线程之间进行同步/互斥的原语。
信号量一般用整数表示,经典的信号量上的操作是P操作(wait,down,lock)和V操作(post,up,unlock)。其使用方法为:

  1. 线程发起P操作尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则P操作成功返回,并且信号量的值被减1;否则线程阻塞直到信号量的值变为正值
  2. 线程完成自己需要在信号量保护下进行的操作
  3. 线程调用V操作将信号量的值加1

P、V操作必须以原子方式执行,不能再将其划分成子操作,即,在这些子操作之间不能对信号量执行其他操作。在P操作中,信号量的值在减小之前必须为正,从而确保生成的信号量的值不为负,并且比该值减小之前小 1。信号量通常用来协调对资源的访问,其中信号量计数会初始化为可用资源的数目。

有两种基本信号量:二进制信号量和计数信号量。二进制信号量的值只能是 0 或 1,计数信号量可以是任意非负值。二进制信号量在逻辑上相当于一个互斥锁。因而二进制的信号量可以像互斥锁那样来使用:

sem_wait(…)
临界区
sem_post(…)
但是二者存在一些区别,互斥锁一般应当仅由持有该锁的线程来解除锁定,但是由于不存在“持有信号量的线程”这一概念,所以,任何线程都可以执行V操作。
另外注意到由于信号量的V操作不一定要由发起P操作的线程来执行,因而V操作就可以由其它线程来执行,这就类似于条件变量的功能了:一个线程A等待某个条件被满足,另一个线程B通知线程A“你所等待的条件被满足了”。进一步的说信号量可用于异步事件通知,如用于信号处理程序中,并且不用象条件变量那样要求获取互斥锁。但是,信号量的效率不如互斥锁高。
缺省情况下,如果有多个线程正在等待信号量,则解除阻塞的顺序是不确定的。信号量在使用前必须先初始化,但是信号量没有属性。
总体上来说,信号量和互斥锁、条件变量之间有类似之处,但是也有区别,它们之间的区别:

  1. 互斥锁一般由锁定该锁的线程(即持有者)解锁,但是信号量的V操作不一定要由发起P操作的线程来执行,条件变量的唤醒一般由其它线程来执行
  2. 互斥锁要么是锁定的,要么是非锁定的,它类似于二进制信号量
  3. 只有条件变量会因为被信号打断而返回
  4. 由于信号量有一个相关的状态即其计数值,因而信号量上的V操作是被记住了的;但是对于条件变量来说如果发出signal时没有线程在等待该signal,则这个通知就会丢失
  5. 信号量不用像条件变量那样和互斥锁配合使用,但是其效率不如条件变量高
  6. 只有sem_post可以在信号处理程序中调用

POSIX定义了两种信号量:

  1. 命名信号量:命名信号量由符合Posix定义的IPC命名规则的名字标识,可以被用于进程之间或线程之间的同步互斥。命名信号具有属主用户 ID、组 ID 和保护模式。
  2. 未命名信号:未命名信号量在进程内存中分配,需要进行初始化,它可以被用于进程之间或线程之间的同步互斥,是否可用于多进程环境取决于信号的分配和初始化方式。

2、API

与信号量相关的数据结构类型是sem_t

  1. #include <semaphore.h>  
  2. int sem_init(sem_t *sem, int pshared, unsigned int value);成功返回0,其它返回值表示出错  
  3. int sem_destroy(sem_t *sem); 成功返回0,其它返回值表示出错  
  4. int sem_post(sem_t *sem); 成功返回0,其它返回值表示出错  
  5. int sem_wait(sem_t *sem); 成功返回0,其它返回值表示出错  
  6. int sem_trywait(sem_t *sem); 成功返回0,其它返回值表示出错  
  7. int sem_getvalue(sem_t *sem, int *value);成功返回0,其它返回值表示出错  
  8. sem_t *sem_open(const char *name, int oflag, …/* mode_t mode, unsigned int value */);成功时返回指向信号量的指针,否则返回SEM_FAILED.  
  9. int sem_close(sem_t *sem);成功返回0,其它返回值表示出错  
  10. int sem_unlink(const char *sem);成功返回0,其它返回值表示出错  

需要说明的是
sem_wait,sem_trywait,sem_post,sem_getvalue四个API对于命名信号和未命名信号都适用
sem_init和sem_destroy仅适用于未命名信号
sem_open,sem_close和sem_unlink仅适用于命名信号

 

1)、创建一个新的命名信号量或者打开已经存在的命名信号量

  1. #include <semaphore.h>  
  2. sem_t *sem_open(const char *name, int oflag, …/* mode_t mode, unsigned int value */);成功时返回指向信号量的指针,否则返回SEM_FAILED.  

 

sem_open用于创建一个新的命名信号量或者打开已经存在的命名信号量,命名信号总是可用于进程间或线程间同步、互斥。
oflag可以为0,O_CREAT或O_CREAT|O_EXCL的取值及其含义
如果oflag包含O_CREAT,则需要指定第3和第4个参数,其中第三个参数的含义类似于文件的模式;第4个参数即为信号量的初始值
如果oflag未包含O_EXCL而是仅包含O_CREAT,则其含义是如果指定名字的信号量不存在就用指定的模式和初始值创建并初始化它,因而即使信号量已经存在也不算是错误
如果oflag同时包含O_EXCL和含O_CREAT,则如果指定名字的信号量已经存在就会返回错误

 

实例:

/*sem_open*/

  1. #include <semaphore.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <fcntl.h>
  6.  
  7. int main(int argc,char **argv)
  8. {
  9.     sem_t *sem;
  10.     
  11.     if(argc!=2)
  12.     {
  13.         printf("please input a file name!\n");
  14.         exit(1);
  15.     }
  16.     sem=sem_open(argv[1],O_CREAT,0644,1);
  17.     exit(0);
  18. }

 

 

运行结果为

  1.  vteyga@root:/root>./sem_open abc

 

 

 

 

2)、关闭指定的命名信号量

  1. #include <semaphore.h>   
  2. sem_close(sem_t *sem);成功返回0,其它返回值表示出错

 

sem_close用来关闭用sem_open打开的命名信号量。
在进程终止时,所有由该进程打开的命名信号量都会自动被关闭,不管进程是主动退出的还是被“杀死的”。
但是关闭命名信号量并不会从系统删除该命名信号量。命名信号量至少具有内核持久性。
内核持久性是和资源的存在时间相关的一个概念,其相关的概念:

  1. 内核持久性:指的是只要系统没有重启并且资源没有被显式的删除它,它就会一直存在。
  2. 进程持久性:指的是只要还有任何一个打开该资源的进程还没有关闭它,它就会一直存在
  3. 文件持久性:只要没有显式的删除资源,它就一直存在,即便系统重启也如此

3)、删除指定的命名信号量

  1. #include <semaphore.h>  
  2. int sem_unlink(const char *sem);成功返回0,其它返回值表示出错  

 

sem_unlink用于从系统删除指定的命名信号量。它类似于文件系统的unliink函数,真正的删除只有在所有打开该命名信号量的线程都关闭了它之后才会发生。

 

实例:

/*sem_unlink*/

  1. #include <semaphore.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <fcntl.h>
  6.  
  7. int main(int argc,char **argv)
  8. {
  9.     sem_t *sem;
  10.     
  11.     if(argc!=2)
  12.     {
  13.         printf("please input a file name!\n");
  14.         exit(1);
  15.     }
  16.     sem=sem_open(argv[1],O_CREAT,0644,1);
  17.     exit(0);
  18. }

 

 

运行结果为

  1.  vteyga@root:/root>./sem_unlink abc
  2.  

 

 

4)、增加信号量(V操作)

  1. #include <semaphore.h>  
  2. int sem_post(sem_t *sem); 成功返回0,其它返回值表示出错  

 

sem_post实现V操作,用来以原子方式增加 sem 所指示的信号量额值。

当一个线程使用完某个信号灯时,它应该调用sem_post来告诉系统申请的资源已经用完。本函数和sem_wait函数的功能正好相反,它把所指定的信号灯的值加1,然后唤醒正在等待该信号灯值变为正数的任意线程。

 

实例:

/*sem_post*/

  1. #include <semaphore.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <fcntl.h>
  6.  
  7. int main(int argc,char **argv)
  8. {
  9.     sem_t *sem;
  10.     int val;
  11.     
  12.     if(argc!=2)
  13.     {
  14.         printf("please input a file name!\n");
  15.         exit(1);
  16.     }
  17.     sem=sem_open(argv[1],0);
  18.     sem_post(sem);
  19.     sem_getvalue(sem,&val);
  20.     printf("value=%d\n", val);
  21.     exit(0);
  22. }

 

 

运行结果为

  1.  vteyga@root:/root>./sem_post abc

 

 

 

 

5)、基于信号量的计数值阻塞  5)、基于信号量的计数值阻塞

  1. #include <semaphore.h>  
  2. int sem_wait(sem_t *sem); 成功返回0,其它返回值表示出错

 

sem_wait实现P操作,它尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则立即返回,并且信号量的值被减1;否则将阻塞直到信号量的值变为正值。

 

实例:

/*sem_wait*/

  1. #include <semaphore.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <fcntl.h>
  6.  
  7. int main(int argc,char **argv)
  8. {
  9.     sem_t *sem;
  10.     int val;
  11.     
  12.     if(argc!=2)
  13.     {
  14.         printf("please input a file name!\n");
  15.         exit(1);
  16.     }
  17.     sem=sem_open(argv[1],0);
  18.     sem_wait(sem);
  19.     sem_getvalue(sem,&val);
  20.     printf("pid %ld has semaphore,value=%d\n",(long)getpid(),val);
  21.     pause();
  22.     exit(0);
  23. }

 

 

运行结果为

  1.  vteyga@root:/root>./sem_wait abc

 

 

 

 

6)、尝试基于信号量的计数值阻塞

  1. #include <semaphore.h>  
  2. int sem_trywait(sem_t *sem); 成功返回0,其它返回值表示出错

 

sem_wait实现P操作,它尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则立即返回,并且信号量的值被减1;否则将返回EAGAIN错误。

sem_wait和sem_trywait的差别是:当所指定信号灯的值已经是0时,后者并不将调用线程投入睡眠。相反,它返回一个EAGAIN错误。

7)、返回指定信号量的当前计数值

  1. #include <semaphore.h>  
  2. int sem_getvalue(sem_t *sem, int *value);成功返回0,其它返回值表示出错 

 

sem_getvalue返回指定信号量的当前计数值。
如果指定的信号量当前处于阻塞状态,则返回值为0或者负值;如果是负值,则该值的绝对值等于当前阻塞在该信号量上的线程的数目。

 

关于posix有名信号灯使用要注意以下几点:

1.Posix有名信号灯的值是随内核持续的。也就是说,一个进程创建了一个信号灯,这个进程结束后,这个信号灯还存在,并且信号灯的值也不会改变。

下面我们利用上面的几个程序来证明这点

#./semopen abc

#./sempost abc

value=1  信号的值仍然是1

 

2。当持有某个信号灯锁的进程没有释放它就终止时,内核并不给该信号灯解锁。

#./semopen abc

#./semwait abc &

pid 9772 has semaphore,value=0

#./sempost abc

value=0   信号量的值变为0了

 

实例:

/**/

  1. #include <semaphore.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <fcntl.h>
  5. #include <pthread.h>
  6.  
  7. void *thread_function(void *arg); /*线程入口函数*/
  8. void print(); /*共享资源函数*/
  9. sem_t *sem; /*定义Posix有名信号灯*/
  10. int val; /*定义信号灯当前值*/
  11.  
  12. int main(int argc,char *argv[])
  13. {
  14.     int n=0;
  15.     pthread_t a_thread[6];
  16.  
  17.     if(argc!=2)
  18.     {
  19.         printf("please input a file name!\n");
  20.         exit(1);
  21.     }
  22.     sem=sem_open(argv[1],O_CREAT,0644,3); /*打开一个信号灯*/
  23.  
  24.     while(n<5) /*循环创建5个子线程,使它们同步运行*/
  25.     {
  26.         if((pthread_create(&a_thread[n],NULL,thread_function,NULL))!=0)
  27.         {
  28.              perror("Thread creation failed");
  29.              exit(1);
  30.         }
  31.         n++;
  32.     }
  33.     n–;
  34.     while(n>0)
  35.     {
  36.         pthread_join(a_thread[n],NULL);
  37.         n–;
  38.     }
  39.     
  40.     sem_close(sem);
  41.     sem_unlink(argv[1]);
  42. }
  43.  
  44. void *thread_function(void *arg)
  45. {
  46.     printf("I am begin,my tid is %u\n",pthread_self());
  47.     sem_wait(sem); /*申请信号灯*/
  48.     print(); /*调用共享代码段*/
  49.     sleep(1);
  50.     sem_post(sem); /*释放信号灯*/
  51.     printf("I am finished,my tid is %u\n",pthread_self());
  52. }
  53.  
  54. void print()
  55. {
  56.     printf("I get it,my tid is %u\n",pthread_self());
  57.     sem_getvalue(sem,&val);
  58.     printf("Now the value have %d\n",val);
  59. }

 

 

运行结果为

  1.  vteyga@root:/root>./pthread_sem_open abc
  2. I am begin,my tid is 4088395520
  3. I get it,my tid is 4088395520
  4. Now the value have 2
  5. I am begin,my tid is 4098885376
  6. I get it,my tid is 4098885376
  7. Now the value have 1
  8. I am begin,my tid is 4067415808
  9. I get it,my tid is 4067415808
  10. Now the value have 0
  11. I am begin,my tid is 4109375232
  12. I am begin,my tid is 4077905664
  13. I am finished,my tid is 4088395520
  14. I get it,my tid is 4077905664
  15. Now the value have 0
  16. I am finished,my tid is 4098885376
  17. I am finished,my tid is 4067415808
  18. I get it,my tid is 4109375232
  19. Now the value have 1
  20. I am finished,my tid is 4109375232
  21. I am finished,my tid is 4077905664

 

 

 

 

 

 

8)、初始化未命名信号

  1. #include <semaphore.h>  
  2. int sem_init(sem_t *sem, int pshared, unsigned int value);成功返回0,其它返回值表示出错  

 

sem_init用于将指定的信号量的初始值设置为value。
如果pshared的值为零,则该信号量只能由创建它的线程所在的进程内的线程使用。如果pshared的值不为零,则该信号量必须位于进程的共享内存内,信号量可以由所有可以访问这片共享内存的进程内的线程所使用。
多个线程不能初始化同一个信号量。
不能重新初始化其它线程正在使用的信号量。

跟sem_open一样,value参数是该信号灯的初始值。

使用完一个基于内存的信号灯后,我们调用sem_destroy关闭它

9)、销毁未命名信号

  1. #include <semaphore.h>  
  2. int sem_destroy(sem_t *sem); 成功返回0,其它返回值表示出错

 

sem_destroy用于销毁与 sem 所指示的未命名信号相关联的任何状态(但是不会释放存储信号量的内存)。

 

除了sem_open和sem_close外,其它的poisx有名信号灯函数都可以用于基于内存的信号灯。

注意:posix基于内存的信号灯和posix有名信号灯有一些区别,我们必须注意到这些。

1.sem_open不需要类型与shared的参数,有名信号灯总是可以在不同进程间共享的。

2.sem_init不使用任何类似于O_CREAT标志的东西,也就是说,sem_init总是初始化信号灯的值。因此,对于一个给定的信号灯,我们必须小心保证只调用一次sem_init。

3.sem_open返回一个指向某个sem_t变量的指针,该变量由函数本身分配并初始化。但sem_init的第一个参数是一个指向某个sem_t变量的指针,该变量由调用者分配,然后由sem_init函数初始化。

4.posix有名信号灯是通过内核持续的,一个进程创建一个信号灯,另外的进程可以通过该信号灯的外部名(创建信号灯使用的文件名)来访问它。posix基于内存的信号灯的持续性却是不定的,如果基于内存的信号灯是由单个进程内的各个线程共享的,那么该信号灯就是随进程持续的,当该进程终止时它也会消失。如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。

5.基于内存的信号灯应用于线程很麻烦(待会你会知道为什么),而有名信号灯却很方便,基于内存的信号灯比较适合应用于一个进程的多个线程。

下面是posix基于内存的信号灯实现一个进程的各个线程间的互次。

实例:

/**/

  1. #include <semaphore.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <fcntl.h>
  5. #include <pthread.h>
  6. #include <stdlib.h>
  7.  
  8. void *thread_function(void *arg); /*线程入口函数*/
  9. void print(void); /*共享资源函数*/
  10. sem_t bin_sem; /*定义信号灯*/
  11. int value; /*定义信号量的灯*/
  12.  
  13. int main()
  14. {
  15.     int n=0;
  16.     pthread_t a_thread[6];
  17.  
  18.     if((sem_init(&bin_sem,0,2))!=0) /*初始化信号灯,初始值为2*/
  19.     {
  20.         perror("sem_init");
  21.         exit(1);
  22.     }
  23.  
  24.     while(n<5) /*循环创建5个子线程,使它们同步运行*/
  25.     {
  26.         if((pthread_create(&a_thread[n],NULL,thread_function,NULL))!=0)
  27.         {
  28.              perror("Thread creation failed");
  29.              exit(1);
  30.         }
  31.         n++;
  32.     }
  33.     n–;
  34.     while(n>0)
  35.     {
  36.         pthread_join(a_thread[n],NULL);
  37.         n–;
  38.     }
  39.     sem_destroy(&bin_sem);
  40. }
  41. void *thread_function(void *arg)
  42. {
  43.     printf("I begin,my pid is %u\n",pthread_self());
  44.     sem_wait(&bin_sem); /*等待信号灯*/
  45.     print();
  46.     sleep(1);
  47.     sem_post(&bin_sem); /*挂出信号灯*/
  48.     printf("I finished,my pid is %u\n",pthread_self());
  49.     pthread_exit(arg);
  50. }
  51. void print()
  52. {
  53.     printf("I get it,my tid is %u\n",pthread_self());
  54.     sem_getvalue(&bin_sem,&value); /*获取信号灯的值*/
  55.     printf("Now the value have %d\n",value);
  56. }

 

 

运行结果为

  1.  vteyga@root:/root>./pthread_sem_init
  2. I begin,my pid is 3937576704
  3. I get it,my tid is 3937576704
  4. Now the value have 1
  5. I begin,my pid is 3958556416
  6. I get it,my tid is 3958556416
  7. Now the value have 0
  8. I begin,my pid is 3927086848
  9. I begin,my pid is 3948066560
  10. I begin,my pid is 3969046272
  11. I finished,my pid is 3937576704
  12. I get it,my tid is 3927086848
  13. Now the value have 0
  14. I finished,my pid is 3958556416
  15. I get it,my tid is 3948066560
  16. Now the value have 0
  17. I finished,my pid is 3927086848
  18. I get it,my tid is 3969046272
  19. Now the value have 0
  20. I finished,my pid is 3948066560

 

 

 

 

最后编辑:
作者:liujg
真实-不弄虚,不做假,做自己,不违心; 踏实-不浮躁,不盲从,不急功,不近利; 实学-不投机,不取巧,勤于学,精于业。