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 }

    复制代码

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

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

  • 相关阅读:
    csu1804
    uvalive4513
    poj3264(Sparse-Table 算法模板)
    uva11107(后缀数组)
    poj2774(最长公共子串)
    uvalive4108(线段树)
    hdu5306 Gorgeous Sequence
    bzoj2823: [AHOI2012]信号塔&&1336: [Balkan2002]Alien最小圆覆盖&&1337: 最小圆覆盖
    bzoj3330: [BeiJing2013]分数
    bzoj1283: 序列
  • 原文地址:https://www.cnblogs.com/dachao0426/p/9373947.html
Copyright © 2011-2022 走看看