Linux进程间通信-共享内存 &信号量

时间:2024-10-26 16:18:12

一、共享内存

1、简单介绍

(1)共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递不再涉及内核,即进程不再通过执行进入内核的系统调用来传递彼此的数据。

(2)共享内存的生命周期随内核。

(3)注意:共享内存未提供任何保护资源,即共享内存自身没有同步与互斥机制,但它是临界资源,所以我们需要利用其它机制来保证数据的正确性,Linux下就可以用信号量达到同步的目的。

(4)linux共享内存有两种方式(本文主要介绍shmget方式)

                1)mmap方式,适用于父子进程之间,创建的内存非常大时;

                2)shmget方式,适用于同一台电脑上不同进程之间,创建的内存相对较小。

(5)进程间利用共享内存实现消息队列的基本原理如下图

2、相关函数介绍

(1)shmget函数

1)函数原型:

2)函数功能:创建共享内存

3)参数:

key:共享内存段名字

size:共享内存大小

shmflg:九个权限标志构成,用法与创建文件时用的mode一致

4)返回值:成功返回一个非负整数,即该共享内存段标识码;失败返回-1

(2)shmat函数

1)函数原型:

2)函数功能:将共享内存段连接到进程地址空间

3)参数:

shmid:共享内存标识码

shmaddr:指定连接的地址

shmflg:两个可能取值SHM_RND和SHM_RDONLY

4)返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

5)说明:

shmaddr为NULL时,核心自动选择一个地址;

shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址;

shmaddr不为NULL时,且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍,相应公式为:shmadrr - (shmadrr % SHMLBA);

shmflg = SHM_RDONLY,表示连接操作用来只读共享内存

(3)shmdt函数

1)函数原型:

2)函数功能:将共享内存段与当前进程脱离

3)参数:

            shmadrr:由shmat函数返回的指针

4)返回值:成功返回0;失败返回-1

5)注意:将共享内存段与当前进程脱离不等于删除共享内存段

(4)shmctl函数

1)函数原型:

2)函数功能:用于控制共享内存

3)参数:

shmid:由shmget函数返回的共享内存标识码

cmd:将要采取的动作,有三个可取值

buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

4)返回值:成功返回0;失败返回-1

3、共享内存实现进程间的单向通信

(1)创建一个文件用于实现创建、销毁共享内存,向共享内存里发消息收消息等函数,文件为它相应的头文件等内容

  1. #include <sys/>
  2. #include <sys/>
  3. #include <sys/>
  4. #define PATHNAME "."
  5. #define PROJ_ID 0x6666
  1. #include ""
  2. static int _CreateShm(int size, int flags)
  3. {
  4. key_t key = ftok(PATHNAME, PROJ_ID);
  5. if(key < 0)
  6. {
  7. perror("ftok");
  8. return -1;
  9. }
  10. int shmid;//共享内存标识码(非负整数)
  11. if((shmid = shmget(key, size, flags)) < 0)//创建共享内存失败时返回-1
  12. {
  13. perror("shmget");
  14. return -2;
  15. }
  16. return shmid;//将创建的共享内存的标识码返回,以实现后续代码
  17. }
  18. int CreateShm(int size)//创建一个新的共享内存
  19. {
  20. return _CreateShm(size, IPC_CREAT | IPC_EXCL | 0666);//已有该共享内存,返回-1;否则创建再返回标识码
  21. }
  22. int GetShm(int size)//获得共享内存标识码
  23. {
  24. return _CreateShm(size, IPC_CREAT);//已有该共享内存,返回标识码即可;没有则创建则创建
  25. }
  26. int DestroyShm(int shmid)
  27. {
  28. if((shmctl(shmid, IPC_RMID, NULL)) < 0)
  29. {
  30. perror("shmctl");
  31. return -1;
  32. }
  33. return 0;
  34. }

(2)创建一个文件,用于创建、销毁共享内存,接收消息,实现代码如下:

  1. include ""
  2. #include<>
  3. int main()
  4. {
  5. int shmid = CreateShm(4096);//创建一个共享内存
  6. char* addr = shmat(shmid, NULL, 0);//将共享内存段连接到进程的地址空间,成功返回指向共享内存的第一个字节
  7. sleep(6);//为了能够等到client进程向共享内存放消息,具体等几秒自己把握,如不等,接收的消息会少一点
  8. int i = 0;
  9. while(i++ < 26)
  10. {
  11. printf("client# %s\n",addr);
  12. sleep(1);//这里不等的话,26次循环很快就执行完了,你还没等到client向共享内存里放消息,它就运行完了
  13. }
  14. shmdt(addr);//将共享内存段与当前进程脱离
  15. sleep(2);
  16. DestroyShm(shmid);
  17. return 0;
  18. }

(3)创建一个文件,用于向共享内存里发消息

  1. #include ""
  2. #include<>
  3. int main()
  4. {
  5. int shmid = GetShm(4096);//获得一个共享内存的标识码
  6. sleep(1);
  7. char* addr = shmat(shmid, NULL, 0);
  8. sleep(2);
  9. char x = 'A';
  10. for(x='A'; x<='Z'; x++)//依次发送字母A-Z
  11. {
  12. addr[x-'A'] = x;
  13. addr[x-'A'+1] = '\0';
  14. sleep(1);
  15. }
  16. shmdt(addr);
  17. sleep(2);
  18. return 0;
  19. }

(4)运行结果如下:

        同样在运行时,要先运行server.c文件,因为它要先创建消息队列,才能实现进程间通信。可以看到,当server打印26次消息后,就会结束此次通信。我们的预想是,server接到client发送的A-Z的26条消息,并依次打印,但在代码中sleep的时间不同,会有不一样的结果,这个得根据自身在运行时的速度以及等待的时间决定。

4、删除消息队列IPC资源

        之前在另一文章我提到过一句话:IPC资源在用完后必须删除。如以上的代码,若是正常跑完IPC资源会被代码删除,若是我们用ctrl+c终止进程,则代码不能删除IPC资源。这样在下次运行代码时,就会出现以下问题:

这个问题产生的原因就是IPC资源未删除,所以我们要学两条命令用来删除消息队列的IPC资源:

删除后,该IPC资源就没有了

二、信号量

     信号量主要用于同步与互斥。本质上是一个计数器,里面记录了临界资源的数目。信号量的生命周期也随内核。

1、进程互斥

(1)由于各进程要求共享资源,而且有些资源需要互斥使用。因此各进程间竞争使用这些资源,进程的这种关系即为进程的互斥;

(2)系统中某些资源一次只能让一个进程使用,这样的资源叫做临界资源或互斥资源;

(3)在进程中涉及到互斥资源的程序段叫做临界区。

2、进程同步

进程同步是指多个进程需要相互配合共同完成同一项任务

3、信号量和P、V原语

(1)信号量和P、V原语由迪杰斯特拉(Dijkstra)提出,信号量值为1的为二元信号量,又称为互斥锁。

(2)信号量

同步:P、V在不同进程中

互斥:P、V在同一进程中

(3)信号量值含义:

S>0:S表示可用资源的数目

S=0:表示无可用资源,无等待进程

S<0:ISI表示等待队列中的进程数

(4)信号量结构体伪代码

        信号量本质上其实是一个计数器(整型变量),它维护等待队列。

  1. struct semaphore
  2. {
  3. int value;
  4. pointer_PCB queue;
  5. };

(5)P原语

  1. //减1操作
  2. P(s)
  3. {
  4. = --;
  5. if( < 0)
  6. {
  7. 该进程状态置为等待状态
  8. 将该进程的PCB插入到相应的等待队列队尾
  9. }
  10. }

(6)V原语

  1. //加1操作
  2. V(s)
  3. {
  4. = ++;
  5. if( >= 0)
  6. {
  7. 唤醒相应等待队列中等待的一个进程
  8. 改变其状态为就绪态
  9. 并将其插入到就绪队列
  10. }
  11. }

注意:P、V原语都是原子操作

4、信号量集相关函数

        信号量是以多个即集申请的,而不是单个申请。维护一种临界资源需要一个信号量,所以维护多种临界资源就需要多个信号量。多种信号量组成一个信号量集。信号量集可以看做是计数器的个数,信号量值可看作计数器的个数。信号量集是以数组形式进行组织的,以下标来提取各个信号,数组元素表示信号量的值即临界资源的数目。

(1)semget函数

        1)函数原型

        2)函数功能:创建和访问一个信号量集

        3)参数

            key:信号集的名字

            nsems:信号集中信号量的个数

            semflg:九个权限标志构成,用法与创建文件的mode模式标志一致

        4)返回值:成功返回一个非负整数即该信号集的标识码;失败返回-1

(2)semctl函数

        1)函数原型

        2)函数功能:控制信号量集

        3)参数:        

            semid:由semget函数返回的信号量集标识码

            semnum:信号量集中信号量的序号

            cmd:将要采取的动作(有三个可取值)

        4)返回值:成功返回0,失败返回-1

(3)semop函数

        1)函数原型

        2)函数功能:创建和访问一个信号量集

        3)参数

            semid:由semget函数返回的信号量的标识码

            sops:是个指向一个结构数值的指针

            nsops:信号量的个数

        4)返回值:成功返回0;失败返回-1

        5)说明:

  1. struct sembuf
  2. {
  3. short sem_num;//信号量的编号
  4. short sem_op;//信号量一次PV操作时加减的数值,一般只会用到两个值:
  5. //一个是“-1”,即P操作,等待信号量变得可用
  6. //另一个是“+1”,即V操作,发出信号量已经变得可用
  7. short sem_flg;//默认设为0,另外两个取值是IPC_NOWAIT或SEM_UNDO                                                                     };

5、程序实现信号量的作用(采用二元信号量来测试)

        创建一个文件,以封装信号量相关操作的函数,代码如下

  1. //实现信号量的相关操作的函数
  2. #include ""
  3. static int _CreateSem(int nsems, int flags)//创建一个信号量
  4. {
  5. key_t key = ftok(PATHNAME, PROJ_ID);//产生key值
  6. if(key < 0)
  7. {
  8. perror("ftok");
  9. return -1;
  10. }
  11. int semid = semget(key, nsems, flags);//创建一个信号量
  12. if(semid < 0)
  13. {
  14. perror("semget");
  15. return -2;
  16. }
  17. return semid;
  18. }
  19. int CreateSem(int nsems)//获得一个新的信号量集
  20. {
  21. return _CreateSem(nsems, IPC_CREAT | IPC_EXCL | 0666);
  22. }
  23. int GetSem(int nsems)//获得一个信号量集的标识码
  24. {
  25. return _CreateSem(nsems, IPC_CREAT);
  26. }
  27. int InitSem(int semid, int semnum, int initval)//对信号量集进行初始化
  28. {
  29. union semun _un;
  30. _un.val = initval;
  31. if(semctl(semid, semnum, SETVAL, _un ) < 0)//设置信号量集中信号量的计数值
  32. {
  33. perror("semctl");
  34. return -1;
  35. }
  36. return 0;
  37. }
  38. static int SemPV(int semid, int who, int op)//PV操作实现
  39. {
  40. struct sembuf _sf;
  41. _sf.sem_num = who;//通过信号量的编号确定对哪个信号量进行操作
  42. _sf.sem_op = op;//信号量一次PV操作时加减的数值
  43. _sf.sem_flg = 0;
  44. if(semop(semid, &_sf, 1) < 0)
  45. {
  46. perror("semop");
  47. return -1;
  48. }
  49. return 0;
  50. }
  51. int P(int semid, int who)//对信号量进行P操作
  52. {
  53. return SemPV(semid, who, -1);//减1操作
  54. }
  55. int V(int semid, int who)//对信号量进行V操作
  56. {
  57. return SemPV(semid, who, +1);//即加1操作
  58. }
  59. int DestroySem(int semid)//销毁信号量集
  60. {
  61. if(semctl(semid, 0, IPC_RMID) < 0)//删除信号量集中序号为0的信号量
  62. {
  63. perror("semctl");
  64. return -1;
  65. }
  66. }

它对应的头文件,代码如下:

  1. #pragma once
  2. #include <>
  3. #include <sys/>
  4. #include <sys/>
  5. #include <sys/>
  6. #include <>
  7. #include <>
  8. #define PATHNAME "."
  9. #define PROJ_ID 0x6666
  10. union semun
  11. {
  12. int val;//SETVAL用的值
  13. struct semid_ds* buf;//IPC_STAT、IPC_SET用的
  14. unsigned short* array;//GETALL、SETALL用的数组值
  15. struct seminfo* _buf;//为IPC_INFO提供的缓存
  16. };

测试代码如下:

运行结果如下:

        可以看到,程序输出的结果很乱。因为程序中的父子进程都要向显示器打印数据,所以此时显示器就是一个临界资源。我们希望父子进程互斥使用它,即输出AA、BB而不会出现AB混合的情况,这种情况是父子进程在竞争的使用它,我们不知道它在什么时刻就会被会切换进程,造成这样的输出结果。所以我们要让它们互斥的访问以输出我们想要的结果,这时就可以用到信号量以实现互斥与同步,所以我们修改代码如下:

  1. #include ""
  2. int main()
  3. {
  4. int semid = CreateSem(1);//申请信号量为1的信号量集
  5. InitSem(semid, 0 , 1);//将信号量的计数值初始化为1
  6. pid_t id = fork();
  7. if(id == 0)//child
  8. {
  9. int _semid = GetSem(0);
  10. while(1)
  11. {
  12. P(_semid, 0);
  13. printf("A");
  14. fflush(stdout);
  15. usleep(123456);
  16. printf("A ");
  17. fflush(stdout);
  18. usleep(321456);
  19. V(_semid, 0);
  20. }
  21. }
  22. else//father
  23. {
  24. while(1)
  25. {
  26. P(semid, 0);
  27. printf("B");
  28. fflush(stdout);
  29. usleep(223456);
  30. printf("B ");
  31. fflush(stdout);
  32. usleep(121456);
  33. V(semid, 0);
  34. }
  35. wait(NULL);//不关心子进程的退出状态
  36. }
  37. DestroySem(semid);
  38. return 0;
  39. }

此时,运行结果如下:

        其实,我们只是在原有代码的基础上,对父子进程的每一次打印加了一个信号量的PV操作。当子进程开始打印时先进行P操作,将资源减去1,此时没有资源了,父进程只能等待,知道子进程打印一次之后进行V操作,此时资源数目为1。父进程也是同样的原理,我们不能控制让谁申请到临界资源,但是我们可以保证在当前进程使用资源时,不被其他进程切换进来,从而造成的数据不正确。

6、信号量资源的释放

以上的代码,因为我们设置的是死循环,所以代码不能执行到删除创建的信号量资源就被我们终止。这样在下次运行时会出现以下情况:

所以我们要学两条命令手动删除信号量资源:

再说一遍:IPC资源必须删除,否则不会自动清除,除非重启,所以System V IPC资源随内核。