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
  2. #include
  3. #include
  4. #include
  5. #include
  6. #include
  7. #include
  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字节\n",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\n",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
  2. #include
  3. #include
  4. #include
  5. #include
  6. #include
  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("请输入:\n");
  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...\n",getpid(),getppid());
  39. read(fd[ 0],buf,20);
  40. printf("我读到了%s\n",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
  2. #include
  3. #include
  4. #include
  5. #include
  6. char* buf=NULL;
  7.  
  8. void sigint(int num)
  9. {
  10. printf("\r接收到数据:%s\n",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\n",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的内容:\n",pid);
  38. gets(buf);
  39. kill(pid,SIGINT);
  40. }
  41. shmdt(buf);
  42. }

程序B:

  1. #include
  2. #include
  3. #include
  4. #include
  5. #include
  6. char* buf=NULL;
  7.  
  8. void sigint(int num)
  9. {
  10. printf("\r接收到数据:%s\n",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\n",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的内容:\n",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
  2. #include
  3. #include
  4. #include
  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("请输入发送到消息队列中的内容:\n");
  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\n",msg.buf);
  32. //当接收消息首字母为q退出
  33. if('q'==msg.buf[0]) break;
  34.  
  35.  
  36. }
  37. }

B程序

  1. #include
  2. #include
  3. #include
  4. #include
  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\n",msg.buf);
  26. //当接收消息首字母为q退出
  27. if('q'==msg.buf[0]) break;
  28. printf("请输入发送到消息队列中的内容:\n");
  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\n", 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\n", getpid());
  110. 110 sem_v(sem_id); /*释放资源*/
  111. 111 del_sem(sem_id); /*删除信号量集*/
  112. 112 }
  113. 113 return 0;
  114. 114 }

复制代码

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

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

你可能感兴趣的:(Unix进程小结(三)进程间通信方式总结)