【Linux】进程间通信(IPC)之信号量详解与测试用例

2017年03月22日 17:28:50

阅读数:2255

学习环境centos6.5 Linux内核2.6


进程间通信概述

1. 进程通信机制

一般情况下,系统中运行着大量的进程,而每个进程之间并不是相互独立的,有些进程之间经常需要互相传递消息。但是每个进程在系统中都有自己的地址空间,操作系统通过页表和实际物理内存所关联,不允许其他进程随意进入。因此,就必须有一种机制既能保证进程之间的通信,又能保证系统的安全,即进程间通信机制——I P C (Inter_Process Communication)

Linux中的内存空间分为系统空间用户空间。在系统空间中,由于各个线程的地址空间都是共享的,即一个线程能够随意访问kernel中的任意地址,所以无需进程通信机制的保护。而在用户空间中,每个进程都有自己的地址空间,一个进程为了与其他进程通信,必须陷入到有足够权限访问其他进程空间的kernel中,从而与其他进程进行通信。在Linux中支持System V 进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。

2. 进程通信对象标示符和键

在kernel中,对每一类I P C 对象,都由一个非负整数来索引。为了识别并唯一标识各个进程通信的对象,需要一个标识符(即IPC标示符)来标识各个通信对象。而为了获取一个独一无二的通信对象,必须使用(可使用ftok( )函数生成,返回值key)。这里的键是用来定位I P C 对象的标识符的。


背景知识

1. 原子操作(atomic operation)

原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。

2. 同步与互斥

同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。

  1. 同步与互斥是保证在高效率运行的同时,可以正确运行。大部分情况下同步是在互斥的基础上进行的。
  • 1
  • 2

3. 临界资源

不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源

4. 临界区

临界区是一段代码,在这段代码中进程将访问临界资源(例如:公用的设备或是存储器),当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。

5. 相关命令

  • ipcs -s 显示已存在的信号量
  • ipcrm -s 删除指定信号量

注意:有时候因为权限问题需要在root下查看与删除。


什么是信号量(Semaphore)

信号量(Semaphore)可以被看做是一种具有原子操作的计数器,它控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数目,常常被当做(lock)来使用,防止一个进程访问另外一个进程正在使用的资源。

信号量本身不具有数据交换的功能,而是控制其他资源来实现进程间通信,在此过程中负责数据操作操作的互斥同步等功能。

  1. 简言之:信号量的主要目的是为了保护临界资源。
  • 1
  • 2

1. 为什么要使用信号量

为了防止出现因多个进程同时访问一个共享资源而引发的问题,我们需要一种方法,可以通过生成并使用令牌来授权,在任一时刻只能有一个执行流访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时刻只有一个执行流在访问它。

2. 信号量的工作原理

  1. 测试控制该资源的信号量。

  2. 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,,表示一个资源被使用。

  3. 若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒,从新进入第1步。

  4. 当进程不再使用由一个信号控制的共享资源时,该信号量值增1,如果有进程正在休眠等待该信号量,则会被唤醒。

  1. 为了正确地实现信号量,信号量的操作应是原子操作,所以信号量通常是在内核中实现的。
  • 1

3. Linux的信号量机制

  1. 在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。

  2. 创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中各个信号量赋初值。

  3. 即使没有进程在使用I P C资源,它们仍然是存在的,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以使用关键字(SEM_UNDO )在退出时恢复信号量值为初始值。


相关函数

1、ftok函数

  1. #include <sys/ipc.h>
  2. #include <sys/types.h>
  3. key_t ftok(const char* path, int id);
  • ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字

  • path 参数就是你指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。

  • id 是子序号, 只使用8bit (1-255)

  • 返回值:若成功返回键值,若出错返回(key_t)-1

在一般的UNIX实现中,是将文件的索引节点号取出(inode),前面加上子序号的到key_t的返回值

2、semget函数

  1. #include <sys/types.h>
  2. #include <sys/ipc.h>
  3. #include <sys/sem.h>
  4. int semget( key_t key, int nsems, int semflg);
  • 用来创建一个信号集,或者获取已存在的信号集。

  • key: 所创建或打开信号量集的键值(ftok成果执行的返回值)。

  • nsems:创建的信号量集中的信号量个数,该参数只在创建信号量时有效。

  • semflg :调用函数的操作类型,也可用于设置信号量集的访问权限,通过or运算使用。

    • IPC_CREAT | IPC _EXCL | 0666 :一般用于创建,可保证返回一个新的ID,同时制定权限为666
    • IPC_CREAT : 用于获取一个已经存在的ID
  • 返回值:成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:

    • EACESS : 没有访问该信号量集的权限。
    • EEXIST:信号量集已经存在,无法创建。
    • EINVAL:参数nsems的值小于0,或者大于该信号量集的限制,或者是该key关联的信号量以存在,并且nsems的值大于该信号量集的信号量数。
    • ENOENT:信号量集不存在,同时没有使用,IPC_CREAT。
    • ENOMEM:没有足够的内存创建新的信号量集。

3、semctl函数

  1. #include <sys/types.h>
  2. #include <sys/ipc.h>
  3. #include <sys/sem.h>
  4. int semctl(int semid, int semun, int cmd, ...);
  • 用来初始化信号集,或者删除信号集。

  • semid:信号量集I P C 标识符。

  • semun:操作信号在信号集中的编号,第一个信号的号是0.

  • cmd:在semid指定的信号量集合上执行此命令。

  • 第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):

  1. union semun
  2. {
  3. int val;
  4. struct semid_ds * buf;
  5. unsigned short * array;
  6. struct seminfo * __buf;
  7. };
  • 第三个参数cmd常用命令:

    • IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中。
    • IPC_RMID:从系统中删除该信号量集合。
    • SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数。
  • 返回值:成功返回一个正数,失败返回-1。

4、 semop函数

  1. #include <sys/types.h>
  2. #include <sys/ipc.h>
  3. #include <sys/sem.h>
  4. int semop(int semid, struct sembuf * sops, unsigned nsops);
  • 功能:操作一个或一组信号。也可以叫PV操作

  • semid:信号集的识别码,可以通过semget获取。

  • sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:

  1. struct sembuf
  2. {
  3. unsigned short sem_num // 在信号集中的编码 0 , 1, ... nsems-1
  4. short sem_op; //操作 负值或正值
  5. short sem_flg; // IPC_NOWAIT, SEM_UNDO
  6. };
  • sembuf结构体参数说明:

    1. sem_num:操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。

    2. sem_op:操作信号量

      • 若sem_op 为负(P操作), 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权。

      • 若sem_op 为正(V操作), 该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。

      • sem_op的值为0:如果没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠,直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
    3. sem_flg: 信号操作标识,有如下两种选择:

      • IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。

      • SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避免程序在异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁。

  • nsops:信号操作结构的数量,恒大于或等于1.

  • 返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。


代码演示

(1)、目的阐述:使用P操作和V操作控制临界区,实现两个进程(父进程与子进程)打印不同的文字,在一个进程进入临界区时,另一进程等待。下面使用的测试是,让子进程在它的临界区打印出(你好:),让父进程打印出(在吗?)。

(2)、在没有使用信号量控制时,打印出来的汉字顺序不是通顺的话。在加入PV操作后,可以保证一个进程打印完,另外一个进程继续打印剩下的汉字,而不会互相交叉。

PV操作成功打印截图示例

无PV操作时打印截图

注:
1. 对于第一次进入临界区时父进程先进入还子进程先进入,与操作系统的进程调度算法有关。
2. 没有在程序中设置跳出循环条件,可以ctrl+c 结束后,用命令去删除信号量集。

代码:

mysem_h


  1. #define _MYSEM_H_
  2. #include <stdio.h>
  3. #include <sys/types.h>
  4. #include <sys/ipc.h> // ftok
  5. #include <sys/sem.h>
  6. #include <sys//wait.h>
  7. #define PATHNAME "." // ftok 中生成key值 . 表示当前路径
  8. #define PROJ_ID 56 // ftok 中配合PATHNAME 生成唯一key值
  9. int create_sems(int nums); // 创建含有nums个信号量的集合
  10. int get_sems(); // 获取信号量
  11. // 初始化semid对应的信号量集中编号为which的信号量值为value
  12. int init_sems(int semid , int which, int value);
  13. int destroy_sems(int semid); // 释放该信号量集
  14. int P(int semid, int which); // 表示分配 信号量值-1
  15. int V(int semid, int which); // 表示释放 信号量值+1
  16. #endif /* _MYSEM_H_ */

mysem_c

  1. // 创建信号量和获取信号量公用函数
  2. static int comm_sem ( int nums , int semflag)
  3. {
  4. // 获取key
  5. key_t key = ftok(PATHNAME, PROJ_ID);
  6. if(key < 0)
  7. {
  8. perror("ftok");
  9. return -1;
  10. }
  11. int semid = semget(key,nums, semflag );
  12. if( semid < 0)
  13. {
  14. perror("semget");
  15. return -1;
  16. }
  17. return semid;
  18. }
  19. int create_sems(int nums) // 创建含有nums个信号量的集合
  20. {
  21. return comm_sem(nums, IPC_CREAT|IPC_EXCL|0666);
  22. }
  23. int get_sems() // 获取信号量
  24. {
  25. return comm_sem(0, IPC_CREAT);
  26. }
  27. union semun
  28. {
  29. int val; // value for SETVAL
  30. struct semid_ds *buf; // buffer for IPC_STAT & IPC_SET
  31. unsigned short *array; // buffer for GETALL & SELALL
  32. struct seminfo * __buf; // buffer for IPC_INFO
  33. };
  34. // 初始化semid对应的信号量集中编号为which的信号量值为value
  35. int init_sems(int semid , int which, int value)
  36. {
  37. union semun _semun;
  38. _semun.val = value;
  39. int ret = semctl(semid, which, SETVAL,_semun);
  40. if(ret < 0)
  41. {
  42. perror("inin_sem");
  43. return -1;
  44. }
  45. return 0;
  46. }
  47. int destroy_sems(int semid) // 释放该信号量集
  48. {
  49. int ret = semctl(semid, 0, IPC_RMID, NULL);
  50. if(ret < 0)
  51. {
  52. perror("rm_sem");
  53. return -1;
  54. }
  55. return 0;
  56. }
  57. static int comm_sem_op(int semid, int which, int op)
  58. {
  59. struct sembuf _sembuf;
  60. _sembuf.sem_num = which;
  61. _sembuf.sem_op = op;
  62. _sembuf.sem_flg = 0; // IPC_NOWAIT SEM_UNDO
  63. return semop(semid, &_sembuf, 1);
  64. }
  65. int P(int semid, int which) // 表示通过 信号量值-1
  66. {
  67. return comm_sem_op(semid, which , -1);
  68. }
  69. int V(int semid, int which) // 表示释放 信号量值+1
  70. {
  71. return comm_sem_op(semid, which, 1);
  72. }

test_mysem_c

  1. // 加入信号量操作后的程序
  2. #include "mysem.h"
    #include "mysem.c"
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. int main()
  6. {
  7. int semid = create_sems(10); // 创建一个包含10个信号量的信号集
  8. init_sems(semid, 0, 1); // 初始化编号为 0 的信号量值为1
  9. pid_t id = fork(); // 创建子进程
  10. if( id < 0)
  11. {
  12. perror("fork");
  13. return -1;
  14. }
  15. else if (0 == id)
  16. {// child
  17. int sem_id = get_sems();
  18. while(1)
  19. {
  20. P(sem_id, 0); // 对该信号量集中的0号信号 做P操作
  21. printf("你");
  22. fflush(stdout);
  23. sleep(1);
  24. printf("好");
  25. printf(":");
  26. fflush(stdout);
  27. sleep(1);
  28. V(sem_id, 0);
  29. }
  30. }
  31. else
  32. {// father
  33. while(1)
  34. {
  35. P(semid,0);
  36. printf("在");
  37. sleep(1);
  38. printf("吗");
  39. printf("?");
  40. fflush(stdout);
  41. V(semid, 0);
  42. }
  43. wait(NULL);
  44. }
  45. destroy_sems(semid);
  46. return 0;
  47. }
  1. // 未加信号量的测试代码
  2. #include "mysem.h"
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. int main()
  6. {
  7. pid_t id = fork(); // 创建子进程
  8. if( id < 0)
  9. {
  10. perror("fork");
  11. return -1;
  12. }
  13. else if (0 == id)
  14. {// child
  15. int sem_id = get_sems();
  16. while(1)
  17. {
  18. printf("你");
  19. fflush(stdout);
  20. sleep(1);
  21. printf("好");
  22. printf(":");
  23. fflush(stdout);
  24. sleep(1);
  25. }
  26. }
  27. else
  28. {// father
  29. while(1)
  30. {
  31. printf("在");
  32. sleep(1);
  33. printf("吗");
  34. printf("?");
  35. fflush(stdout);
  36. }
  37. wait(NULL);
  38. }
  39. return 0;
  40. }
  41. 附上我的测试代码和截图:
  1. #include"mylib.h"
  2. #include "mylib.c"
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. #include <string.h>
  7. #include <stdlib.h>
  8. #include <sys/types.h>
  9. #include <sys/ipc.h>
  10. #include <sys/msg.h>
  11. #include <sys/shm.h>
  12.  
  13. typedef struct msgbuf {
  14. char mtext[1024];
  15. } msgbuf_t;
  16.  
  17. int main()
  18. {
  19. msgbuf_t *shmptr;
  20. int shmid = shmget(10001, sizeof(msgbuf_t), IPC_CREAT | 0644);
  21. shmptr = (msgbuf_t *)shmat(shmid, 0, 0);
  22. int semid = create_sems(10); // 创建一个包含10个信号量的信号集
  23. init_sems(semid, 0, 1); // 初始化编号为 0 的信号量值为1
  24.  
  25. if(fork() == 0) {
  26. printf("I am process P1:");
  27. int count = 0;
  28. int sem_id = get_sems();
  29. while(1) {
  30.  
  31. P(sem_id, 0); // 对该信号量集中的0号信号 做P操作
  32. if(count >= 10)
  33. break;
  34. else{
  35. printf("P1 sending %d\n", count);
  36. memset(shmptr->mtext, 0, sizeof(shmptr->mtext));
  37. sprintf(shmptr->mtext, "%d", count++);
  38. }
  39. V(sem_id, 0);
  40. sleep(1);
  41. }
  42. V(sem_id, 0);
  43.  
  44. shmdt(shmptr);
  45. exit(0);
  46. } else if(fork() == 0) {
  47. int count=0;
  48. printf("I am process P2:");
  49. int sem_id = get_sems();
  50. while(1){
  51. P(sem_id, 0);
  52.  
  53. if(count>=10)
  54. break;
  55. else
  56. {
  57. printf("received from P1: %s\n", shmptr->mtext);
  58. count++;
  59. }
  60. V(sem_id, 0);
  61. sleep(1);
  62. }
  63. V(semid, 0);
  64. shmdt(shmptr);
  65. exit(0);
  66. }
  67.  
  68. printf("I am father process");
  69. destroy_sems(semid);
  70.  
  71. shmdt(shmptr);
  72. // while(1)
  73. sleep(12);
  74. }

  

Linux进程间通信(IPC)之信号量的更多相关文章

  1. Linux进程间通信IPC学习笔记之同步二(SVR4 信号量)

    Linux进程间通信IPC学习笔记之同步二(SVR4 信号量)

  2. Linux进程间通信IPC学习笔记之同步二(Posix 信号量)

    Linux进程间通信IPC学习笔记之同步二(Posix 信号量)

  3. Linux进程间通信IPC学习笔记之消息队列(SVR4)

    Linux进程间通信IPC学习笔记之消息队列(SVR4)

  4. Linux 进程间通信(IPC)

    Linux 进程间通信(IPC): Linux系统中除了进程和进程之间通信,我想大家也应该关注用户空间与内核空间是怎样通信的.例如说netlink等等. 除了传统进程间通信外像Socket通信也须要掌 ...

  5. Linux进程间通信(IPC)

    序言 linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的. 而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心) ...

  6. Linux进程间通信(System V) --- 信号量

    信号量 IPC 原理 信号量通信机制主要用来实现进程间同步,避免并发访问共享资源.信号量可以标识系统可用资源的个数.最简单的信号量为二元信号量 下图为 Linux 信号量通信机制的概念图.在实际应用中 ...

  7. Linux进程间通信(IPC)机制总览

    Linux进程间通信 Ø  管道与消息队列 ü  匿名管道,命名管道 ü  消息队列 Ø  信号 ü  信号基础 ü  信号应用 Ø  锁与信号灯 ü  记录锁 ü  有名信号灯 ü  无名信号灯(基 ...

  8. Linux进程间通信IPC学习笔记之有名管道

    基础知识: 有名管道,FIFO先进先出,它是一个单向(半双工)的数据流,不同于管道的是:是最初的Unix IPC形式,可追溯到1973年的Unix第3版.使用其应注意两点: 1)有一个与路径名关联的名 ...

  9. Linux进程间通信IPC学习笔记之管道

    基础知识: 管道是最初的Unix IPC形式,可追溯到1973年的Unix第3版.使用其应注意两点: 1)没有名字: 2)用于共同祖先间的进程通信: 3)读写操作用read和write函数 #incl ...

  10. Linux进程间通信IPC学习笔记

    linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的.而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间 ...

随机推荐

  1. KMP算法详解&&P3375 【模板】KMP字符串匹配题解

    KMP算法详解: KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt(雾)提出的. 对于字符串匹配问题(such as 问你在abababb中有多少个 ...

  2. 第八章 watch监听 85 computed-计算属性的使用和3个特点

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8&quo ...

  3. numpy的ndarray数组如何reshape成固定大小

    在做肺结节检测的时候,遇到dicom文件reshape之后尺寸大小不一.因为大下不一,numpy.reshape又无法重塑成指定大小的.最后还是在一个大牛的代码中找到了解决方法. VL = np.lo ...

  4. Google Protocol Buffer入门

    简介 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 ...

  5. JS自带的map()方法

    1. map()方法返回一个由原数组的每个元素调用一个指定方法后返回值组成的新数组. 2. 例子: 2.1 在字符串中使用map 在一个String上使用map方法获取字符串中每个字符所对应的ASCI ...

  6. Acwing-169-数独2(搜索, 剪枝)

    链接: https://www.acwing.com/problem/content/171/ 题意: 请你将一个16x16的数独填写完整,使得每行.每列.每个4x4十六宫格内字母A~P均恰好出现一次 ...

  7. 钉钉报警-prometheus-alertmanager

    alertmanager alertmanager可以放在远程服务器上 报警机制 在 prometheus 中定义你的监控规则,即配置一个触发器,某个值超过了设置的阈值就触发告警, prometheu ...

  8. JavaScript中undefined和not defined 的区别

    参考:某个大佬的博客 以下原创: <script type="text/javascript"> console.log(a); a = 100; </scrip ...

  9. [bx]和loop

    1.关于[bx] 1)[bx]用来表示取寄存器bx中的值作为偏移地址: 段地址保存在段寄存器ds中: 例如:将 2000:1000 处的数据保存到寄存器ax mov ax,2000 mov ds,ax ...

  10. MessagePack Java 0.6.X List, Map 对象的序列化和反序列化

    为了序列化原生的容器对象例如  List 和 Map 对象,你必须使用 Template. Template 对象是 serializer 和 deserializer 的配对.例如,为了序列化一个  ...