zoukankan      html  css  js  c++  java
  • Unix进程小结(三)进程间通信方式总结

    进程间通信(IPC)介绍

    进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

    IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

    一、一些基本概念

    进程间通信(IPC):进程之间交换数据的过程叫进程间通信。
        进程间通信的方式:
            简单的进程间通信:
                命令行:父进程通过exec函数创建子进程时可以附加一些数据。
                环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表。
                信号:父子进程之间可以根据进程号相互发送信号,进程简单通信。
                文件:一个进程向文件中写入数据,另一个进程从文件中读取出来。
                命令行、环境变量只能单身传递,信号太过于简单,文件通信不能实时。
            
            XSI通信方式:X/open 计算机制造商组织。
                共享内存、消息队列、信号量
            网络进程间通信方式:网络通信就是不同机器的进程间通信方式。
            传统的进程间通信方式:管道

    二、管道
        1、管道是一种古老的通信的方式(基本上不再使用)
        2、早期的管道是一种半双工,现在大多数是全双工。
        3、有名管道(这种管道是以文件方式存在的)。
        int mkfifo(const char *pathname, mode_t mode);

    例子:

    1. #include <stdio.h>
    2. #include <sys/types.h>
    3. #include <sys/stat.h>
    4. #include <fcntl.h>
    5. #include <unistd.h>
    6. #include <string.h>
    7. #include <strings.h>
    8.  
    9. int main()
    10. {
    11. // 创建管道文件
    12. if(0 > mkfifo("test.txt",0644))
    13. {
    14. perror("mkfifo");
    15. return -1;
    16. }
    17.  
    18. // 打开
    19. int fd = open("test.txt",O_RDWR);
    20. if(0 > fd)
    21. {
    22. perror("open");
    23. return -1;
    24. }
    25.  
    26. // 准备缓冲区
    27. char buf[255] = {};
    28. // 写/读
    29. while(1)
    30. {
    31. printf(">");
    32. gets(buf);
    33. int ret = write(fd,buf,strlen(buf));
    34. printf("写入数据%d字节 ",ret);
    35. if('q' == buf[0])break;
    36. getchar();
    37. bzero(buf,sizeof(buf));
    38. ret = read(fd,buf,sizeof(buf));
    39. printf("读取数据%d字节,内容:%s ",ret,buf);
    40. if('q' == buf[0])break;
    41. }
    42. // 关闭
    43. close(fd);
    44. }


                
        管道通信的编程模式:
            进程A                进程B
            创建管道mkfifo
            打开管道open            打开管道
            写/读数据read/write    读/写数据
            关闭管道close            关闭管道
                
        4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,但这种管道只能用在fork创建的父子进程之间。
            int pipe(int pipefd[2]);
            pipefd[0] 用来读数据
            pipefd[1] 用来写数据

    以Linux中的C语言编程为例。

    1. #include<stdio.h>
    2. #include<unistd.h>
    3. #include<stdlib.h>
    4. #include<strings.h>
    5. #include<sys/types.h>
    6. #include<signal.h>
    7. int main()
    8. {
    9. int fd[2];
    10. int pid;
    11. char buf[255]={};
    12.  
    13. if(pipe(fd)<0)
    14. {
    15. perror("pipe");
    16. return -1;
    17. }
    18. if((pid=fork())<0)
    19. {
    20. perror("fork");
    21. }
    22. else if(pid>0)
    23. {
    24.  
    25. printf("我是进程%d...",getpid());
    26. close(fd[0]);
    27. printf("请输入: ");
    28. gets(buf);
    29. write(fd[1],buf,sizeof(buf));
    30. pause();
    31.  
    32. }
    33. else
    34. {
    35. getchar();
    36. close(fd[1]);
    37. bzero(buf,sizeof(buf));
    38. printf("我是子进程%d,我的父进程是%d... ",getpid(),getppid());
    39. read(fd[0],buf,20);
    40. printf("我读到了%s ",buf);
    41. kill(getppid(),2);
    42.  
    43. }
    44. }

    此程序是一个简单的通过无名管道实现进程间的通信的程序!

    三、共享内存
        1、由内存维护一个共享的内存区域,其它进程把自己的虚拟地址映射到这块内存,然后多个进程之间就可以共享这块内存了。
        2、这种进程间通信的好处是不需要信息复制,是进程间通信最快的一种方式。
        3、但这种通信方式会面临同步的问题,需要与其它通信方式配合,最合适的就是信号。
        
        共享内存的编程模式:
            1、进程之间要约定一个键值
            进程A        进程B    
            创建共享内存        
            加载共享内存    加载共享内存
            卸载共享内存    卸载共享内存
            销毁共享内存
        
        int shmget(key_t key, size_t size, int shmflg);
        功能:创建共享内存
        size:共享的大小,尽量是4096的位数
        shmflg:IPC_CREAT|IPC_EXCL
        返回值:IPC对象标识符(类似文件描述符)
        
        void *shmat(int shmid, const void *shmaddr, int shmflg);
        功能:加载共享内存(进程的虚拟地址与共享的内存映射)
        shmid:shmget的返回值
        shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射。
        shmflg:
            SHM_RDONLY:限制内存的权限为只读
            SHM_REMAP:映射已经存的共享内存。
            SHM_RND:当shmaddr为空时自动分配
            SHMLBA:shmaddr的值不能为空,否则出错
        返回值:映射后的虚拟内存地址
            
        int shmdt(const void *shmaddr);
        功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)
        
        int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        功能:控制/销毁共享内存
        cmd:
            IPC_STAT:获取共享内存的属性
            IPC_SET:设置共享内存的属性
              IPC_RMID:删除共享内存
        buf:
            记录共享内存属性的对象

    例子:

    程序A

    1. #include<stdio.h>
    2. #include<sys/ipc.h>
    3. #include<sys/shm.h>
    4. #include<unistd.h>
    5. #include<signal.h>
    6. char* buf=NULL;
    7.  
    8. void sigint(int num)
    9. {
    10. printf(" 接收到数据:%s ",buf);
    11. printf(">");
    12. fflush(stdout);
    13. }
    14.  
    15. int main()
    16. {
    17. signal(SIGINT,sigint);
    18.  
    19. key_t key=39242236;
    20.  
    21. int pid=0;
    22. printf("我是进程:%d ",getpid());
    23. printf("与我通信的进程是:");
    24. scanf("%d",&pid);
    25. getchar();
    26.  
    27. int shmid=shmget(key,4096,IPC_CREAT|0744);
    28. if(0>shmid)
    29. {
    30. perror("shmget");
    31. return -1;
    32. }
    33.  
    34. buf = shmat(shmid,NULL,SHM_RND);
    35. while(1)
    36. {
    37. printf("请输入要发送给进程%d的内容: ",pid);
    38. gets(buf);
    39. kill(pid,SIGINT);
    40. }
    41. shmdt(buf);
    42. }

    程序B:

    1. #include<stdio.h>
    2. #include<sys/ipc.h>
    3. #include<sys/shm.h>
    4. #include<unistd.h>
    5. #include<signal.h>
    6. char* buf=NULL;
    7.  
    8. void sigint(int num)
    9. {
    10. printf(" 接收到数据:%s ",buf);
    11. printf(">");
    12. fflush(stdout);
    13. }
    14.  
    15. int main()
    16. {
    17. signal(SIGINT,sigint);
    18.  
    19. key_t key=39242236;
    20.  
    21. int pid=0;
    22. printf("我是进程:%d ",getpid());
    23. printf("与我通信的进程是:");
    24. scanf("%d",&pid);
    25. getchar();
    26.  
    27. int shmid=shmget(key,4096,0);
    28. if(0>shmid)
    29. {
    30. perror("shmget");
    31. return -1;
    32. }
    33.  
    34. buf = shmat(shmid,NULL,SHM_RND);
    35. while(1)
    36. {
    37. printf("请输入要发送给进程%d的内容: ",pid);
    38. gets(buf);
    39. kill(pid,SIGINT);
    40. }
    41. shmdt(buf);
    42. }

    上面两个程序分别运行得到进程A和进程B

    通过获取进程id用kill函数来发送信号,从而实现A和B的通信,两个程序通过共享内存通信       
    四、消息队列
        1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表。
        
        int msgget(key_t key, int msgflg);
        功能:创建或获取消息队列
        msgflg:
            创建:IPC_CREAT|IPC_EXEC
            获取:0
            
        int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
        功能:向消息队列发送消息
        msqid:msgget的返回人值
        msgp:消息(消息类型+消息内容)的首地址
        msgsz:消息内存的长度(不包括消息类型)
        msgflg:
            MSG_NOERROR:当消息的实际长比msgsz还要长的话,
                则按照msgsz长度截取再发送,否则产生错误。
                
        ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
        功能:从消息队列接收消息
        msgp:存储消息的缓冲区
        msgsz:要接收的消息长度
        msgtyp:消息的的类型(它包含消息的前4个字节)
        msgflg:
            MSG_NOWAIT:如果要接收的消息不存在,直接返回。
                否则阻塞等待。
            MSG_EXCEPT:从消息队列中接收第一个不msgtyp类型的第一个消息。
            
        int msgctl(int msqid, int cmd, struct msqid_ds *buf);
        功能:控制/销毁消息队列
        cmd:
            IPC_STAT:获取消息队的属性
            IPC_SET:设置消息队列的属性
            IPC_RMID:删除消息队列

    例子:

    A程序

    1. #include<stdio.h>
    2. #include<sys/types.h>
    3. #include<sys/ipc.h>
    4. #include<sys/msg.h>
    5. //定义消息
    6. typedef struct Msg
    7. {
    8. long type;
    9. char buf[255];
    10. }Msg;
    11.  
    12. int main()
    13. {
    14. key_t key=ftok(".",1);
    15. int msgid=msgget(key,0777|IPC_CREAT);
    16. if(0>msgid)
    17. {
    18. perror("msgget");
    19. return -1;
    20. }
    21. while(1)
    22. {
    23. Msg msg={};
    24. msg.type=1;
    25. printf("请输入发送到消息队列中的内容: ");
    26. gets(msg.buf);
    27. msgsnd(msgid,&msg,sizeof(msg.buf),0);
    28. //当发送消息首字母为q退出
    29. if('q'==msg.buf[0]) break;
    30. msgrcv(msgid,&msg,sizeof(msg.buf),2,0);
    31. printf("接收到:%s ",msg.buf);
    32. //当接收消息首字母为q退出
    33. if('q'==msg.buf[0]) break;
    34.  
    35.  
    36. }
    37. }

    B程序

    1. #include<stdio.h>
    2. #include<sys/types.h>
    3. #include<sys/ipc.h>
    4. #include<sys/msg.h>
    5. //定义消息
    6. typedef struct Msg
    7. {
    8. long type;
    9. char buf[255];
    10. }Msg;
    11.  
    12. int main()
    13. {
    14. key_t key=ftok(".",1);
    15. int msgid=msgget(key,0);
    16. if(0>msgid)
    17. {
    18. perror("msgget");
    19. return -1;
    20. }
    21. while(1)
    22. {
    23. Msg msg={};
    24. msgrcv(msgid,&msg,sizeof(msg.buf),1,0);
    25. printf("接收到:%s ",msg.buf);
    26. //当接收消息首字母为q退出
    27. if('q'==msg.buf[0]) break;
    28. printf("请输入发送到消息队列中的内容: ");
    29. gets(msg.buf);
    30. msg.type=2;
    31. msgsnd(msgid,&msg,sizeof(msg.buf),0);
    32. //当发送消息首字母为q退出
    33. if('q'==msg.buf[0]) break;
    34.  
    35.  
    36. }
    37. }

    进程A和B通过消息队列完成IPC

    五、信号量

    信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

    1、特点

    1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

    2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

    3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

    4. 支持信号量组。

    2、原型

    最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

    Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

    复制代码

    1. 1 #include <sys/sem.h>
    2. 2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
    3. 3 int semget(key_t key, int num_sems, int sem_flags);
    4. 4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
    5. 5 int semop(int semid, struct sembuf semoparray[], size_t numops);
    6. 6 // 控制信号量的相关信息
    7. 7 int semctl(int semid, int sem_num, int cmd, ...);

    复制代码

    semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

    semop函数中,sembuf结构的定义如下:

    复制代码

    1. 1 struct sembuf
    2. 2 {
    3. 3 short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    4. 4 short sem_op; // 信号量值在一次操作中的改变量
    5. 5 short sem_flg; // IPC_NOWAIT, SEM_UNDO
    6. 6 }

    复制代码

    其中 sem_op 是一次操作中的信号量的改变量:

    • sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。

    • sem_op < 0,请求 sem_op 的绝对值的资源。

      • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
      • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。
        • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN
        • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
          1. 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
          2. 此信号量被删除,函数smeop出错返回EIDRM;
          3. 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
    • sem_op == 0,进程阻塞直到信号量的相应值为0:

      • 当信号量已经为0,函数立即返回。
      • 如果信号量的值不为0,则依据sem_flg决定函数动作:
        • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN
        • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
          1. 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
          2. 此信号量被删除,函数smeop出错返回EIDRM;
          3. 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

    semctl函数中的命令有多种,这里就说两个常用的:

    • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
    • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

    3、例子

    复制代码

    1. 1 #include<stdio.h>
    2. 2 #include<stdlib.h>
    3. 3 #include<sys/sem.h>
    4. 4
    5. 5 // 联合体,用于semctl初始化
    6. 6 union semun
    7. 7 {
    8. 8 int val; /*for SETVAL*/
    9. 9 struct semid_ds *buf;
    10. 10 unsigned short *array;
    11. 11 };
    12. 12
    13. 13 // 初始化信号量
    14. 14 int init_sem(int sem_id, int value)
    15. 15 {
    16. 16 union semun tmp;
    17. 17 tmp.val = value;
    18. 18 if(semctl(sem_id, 0, SETVAL, tmp) == -1)
    19. 19 {
    20. 20 perror("Init Semaphore Error");
    21. 21 return -1;
    22. 22 }
    23. 23 return 0;
    24. 24 }
    25. 25
    26. 26 // P操作:
    27. 27 // 若信号量值为1,获取资源并将信号量值-1
    28. 28 // 若信号量值为0,进程挂起等待
    29. 29 int sem_p(int sem_id)
    30. 30 {
    31. 31 struct sembuf sbuf;
    32. 32 sbuf.sem_num = 0; /*序号*/
    33. 33 sbuf.sem_op = -1; /*P操作*/
    34. 34 sbuf.sem_flg = SEM_UNDO;
    35. 35
    36. 36 if(semop(sem_id, &sbuf, 1) == -1)
    37. 37 {
    38. 38 perror("P operation Error");
    39. 39 return -1;
    40. 40 }
    41. 41 return 0;
    42. 42 }
    43. 43
    44. 44 // V操作:
    45. 45 // 释放资源并将信号量值+1
    46. 46 // 如果有进程正在挂起等待,则唤醒它们
    47. 47 int sem_v(int sem_id)
    48. 48 {
    49. 49 struct sembuf sbuf;
    50. 50 sbuf.sem_num = 0; /*序号*/
    51. 51 sbuf.sem_op = 1; /*V操作*/
    52. 52 sbuf.sem_flg = SEM_UNDO;
    53. 53
    54. 54 if(semop(sem_id, &sbuf, 1) == -1)
    55. 55 {
    56. 56 perror("V operation Error");
    57. 57 return -1;
    58. 58 }
    59. 59 return 0;
    60. 60 }
    61. 61
    62. 62 // 删除信号量集
    63. 63 int del_sem(int sem_id)
    64. 64 {
    65. 65 union semun tmp;
    66. 66 if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
    67. 67 {
    68. 68 perror("Delete Semaphore Error");
    69. 69 return -1;
    70. 70 }
    71. 71 return 0;
    72. 72 }
    73. 73
    74. 74
    75. 75 int main()
    76. 76 {
    77. 77 int sem_id; // 信号量集ID
    78. 78 key_t key;
    79. 79 pid_t pid;
    80. 80
    81. 81 // 获取key值
    82. 82 if((key = ftok(".", 'z')) < 0)
    83. 83 {
    84. 84 perror("ftok error");
    85. 85 exit(1);
    86. 86 }
    87. 87
    88. 88 // 创建信号量集,其中只有一个信号量
    89. 89 if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
    90. 90 {
    91. 91 perror("semget error");
    92. 92 exit(1);
    93. 93 }
    94. 94
    95. 95 // 初始化:初值设为0资源被占用
    96. 96 init_sem(sem_id, 0);
    97. 97
    98. 98 if((pid = fork()) == -1)
    99. 99 perror("Fork Error");
    100. 100 else if(pid == 0) /*子进程*/
    101. 101 {
    102. 102 sleep(2);
    103. 103 printf("Process child: pid=%d ", getpid());
    104. 104 sem_v(sem_id); /*释放资源*/
    105. 105 }
    106. 106 else /*父进程*/
    107. 107 {
    108. 108 sem_p(sem_id); /*等待资源*/
    109. 109 printf("Process father: pid=%d ", getpid());
    110. 110 sem_v(sem_id); /*释放资源*/
    111. 111 del_sem(sem_id); /*删除信号量集*/
    112. 112 }
    113. 113 return 0;
    114. 114 }

    复制代码

    上面的例子如果不加信号量,则父进程会先执行完毕。这里加了信号量让父进程等待子进程执行完以后再执行。

    总结:进程间通信是实现两个程序传输数据的重要手段,非常值得学习和掌握

  • 相关阅读:
    Java JMX 监管
    Spring Boot REST(一)核心接口
    JSR 规范目录
    【平衡树】宠物收养所 HNOI 2004
    【树型DP】叶子的颜色 OUROJ 1698
    【匈牙利匹配】无题II HDU2236
    【贪心】Communication System POJ 1018
    【贪心】Moving Tables POJ 1083
    Calling Extraterrestrial Intelligence Again POJ 1411
    【贪心】Allowance POJ 3040
  • 原文地址:https://www.cnblogs.com/dachao0426/p/9373947.html
Copyright © 2011-2022 走看看