进程间通信

Posted DR5200

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信相关的知识,希望对你有一定的参考价值。

一.进程间通信介绍

进程间通信目的

(1). 数据传输:一个进程需要将它的数据发送给另一个进程(如A进程要把数据传输给B进程,让B进程进行一些业务处理)
举例 :
log.txt 中有一些内容

cat log.txt | grep hello

cat 进程将log.txt中的数据通过管道交给grep进程

(2). 资源共享:多个进程之间共享同样的资源。
(3). 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
(4). 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

补充知识 :

(1). 前面的文章说过进程之间是具有独立性的,那就说明两个进程要想完成数据共享成本是比较高的,因为进程的独立性就体现在数据各自私有,所以进程间通信,一般一定要借助第三方(OS)资源

(2). 通信的本质就是"数据的拷贝"
进程A->数据拷贝给OS->OS把数据拷贝给进程B
OS一定要提供一段内存区域,能够被双方进程看到

(3). 进程间通信本质 : 让不同的进程,看到同一份资源(内存,文件内核缓冲等),资源由操作系统哪些模块提供,就有了不同的进程间通信方式

(4). 进程间通信是有标准的(system V标准/POSIX标准)

进程间通信发展

(1). 管道

(2). System V进程间通信
实现一台机器上的若干进程通信

(3). POSIX进程间通信
实现进程间可以跨网络通信(QQ聊天时就实现了跨主机通信)

进程间通信分类

管道 :
(1). 匿名管道pipe
(2). 命名管道

System V IPC :
(1). System V 共享内存
(2). System V 消息队列
(3). System V 信号量

POSIX IPC :
(1). 消息队列
(2). 共享内存
(3). 信号量
(4).互斥量
(5).条件变量
(6). 读写锁

二.管道

匿名管道

(1). 管道是Unix中最古老的进程间通信的形式。
(2). 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
(3). 管道只能进行单向通信

管道通信 : 管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。

pipe函数介绍

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
// 以读方式和写方式打开同一个文件,返回的两个文件描述符填入fd数组中
返回值:成功返回0,失败返回错误代码

匿名管道实现步骤 :
(1). 调用pipe函数创建匿名管道,传入的输出型参数fd[2]调用后fd[0]为读端,fd[1]为写端
(2). fork创建子进程,子进程fd[0]同样为读端,fd[1]为写端
(3). 关闭父进程读端,子进程写端(或关闭父进程写端,子进程读端)
(4). 父进程向管道写,子进程从管道读,从而实现进程间通信

// 父子进程通过匿名管道实现通信的测试代码

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\\n");
                return -1;
        }
        //printf("fd[0] : %d\\n",fd[0]); 3
        //printf("fd[1] : %d\\n",fd[1]); 4
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\\n";
                int count = 10;
                while(count)
                {
                        write(fd[1],msg,strlen(msg));
                        count--;
                        sleep(1);
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\\n");
                        break;
                }
                else
                {
                        perror("read\\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                                                                                                                             

关于这段代码的几点分析和扩展

(1). 为什么子进程休眠1秒,运行结果父进程也休眠1秒呢 ?

临界资源 : 被多个进程共享的资源
临界区 : 访问临界资源的代码
进程互斥 : 在使用系统资源时,一个进程正在使用,另一个进程必须阻塞等待,不能同时使用
管道自带同步与互斥机制,进程互斥可以解决数据混乱的问题,如果没有互斥机制,子进程在写入的时候,父进程就有可能来读取,就会发生意料之外的结果

管道内部已经自动提供了互斥与同步机制,当子进程向管道写入数据,然后sleep,父进程去管道读取数据打印,然后read 识别到管道为空,父进程不再进行读取,阻塞式的等待子进程写入,所以并非是父进程sleep了,而是因为子进程写的慢,导致父进程阻塞式等待

(2). 子进程一直写,父进程sleep不去读,写满缓冲区之后,子进程会等待父进程读取之后再写入,子进程被挂起

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\\n");
                return -1;
        }
        //printf("fd[0] : %d\\n",fd[0]);
        //printf("fd[1] : %d\\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\\n";
                while(1)
                {
                        write(fd[1],msg,strlen(msg));
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
        		sleep(100);
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\\n");
                        break;
                }
                else
                {
                        perror("read\\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                                  

(3). 父进程一直在读取,子进程写入5行后不再写入,父进程会等待子进程写入之后再读取,父进程被挂起

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\\n");
                return -1;
        }
        //printf("fd[0] : %d\\n",fd[0]);
        //printf("fd[1] : %d\\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\\n";
                int count = 10;
                while(count)
                {
                        if(count == 5)
                        {
                        	sleep(1000);
                        }
                        else
                        {
                        	sleep(1);
                         	write(fd[1],msg,strlen(msg));
                        	count--;
                        }
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\\n");
                        break;
                }
                else
                {
                        perror("read\\n");
                        break;
                }
        }
        waitpid(id,NULL,0);
        return 0;
}                      

(4). 父进程读端关闭,子进程写端再写入就无意义,操作系统会杀掉子进程,子进程会收到13号信号(SIGPIPE)

运行结果 :

child send to father : I am a child
child exit sigal : 13
// kill -l 查看信号
13) SIGPIPE
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\\n");
                return -1;
        }
        //printf("fd[0] : %d\\n",fd[0]);
        //printf("fd[1] : %d\\n",fd[1]);
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                const char* msg = "I am a child\\n";
                int count = 5;
                while(count)
                {
                        if(count == 2)
                        {
                        	sleep(1000);
                        }
                        else
                        {
                        	sleep(1);
                         	write(fd[1],msg,strlen(msg));
                        	count--;
                        }
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\\n");
                        break;
                }
                else
                {
                        perror("read\\n");
                        break;
                }
                close(fd[0]);
                break;
        }
        int status;
        waitpid(id,&status,0);
        
        printf("child exit,sign : %d\\n",status & 0x7F);
        return 0;
}                      

(5). 对挂起的理解 :
所谓挂起是将进程的PCB由R状态设置成非R,然后将进程的PCB链入等待队列.
进程被唤醒(将进程的PCB由非R状态设置为R状态,将PCB链入运行队列中)

(6). 为什么子进程退出,父进程也退出了呢 ?
如果子进程写端关闭,父进程读端read返回值为0,代表文件结束

(7). 如果打开文件的进程退出了,文件也会被释放掉,所以管道的生命周期是随进程的

(8). 管道是半双工,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
全双工 : 指可以同时(瞬时)进行信号的双向传输(A→B且B→A)
半双工 : 只允许甲方向乙方传送信息,而乙方不能向甲方传送

(9). 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道

(10). 管道的大小是多大呢?

ulimit 是一条查看系统资源的命令,可以看到管道的大小

ulimit -a

// 代码测试管道的大小

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
// child->write  father->read
int main()
{
        int fd[2] = {0};
        if(pipe(fd) < 0)
        {
                perror("pipe!\\n");
                return -1;
        }
        pid_t id = fork();
        if(id == 0)
        {
                close(fd[0]);
                char c = 'a';
                int count = 0;
                while(1)
                {
                        write(fd[1],&c,1);
                        count++;
                        printf("%d\\n",count);
                }
                exit(0);
        }
        close(fd[1]);
        char buf[64];
        while(1)
        {
                sleep(1000);
                ssize_t s = read(fd[0],buf,sizeof(buf));
                if(s > 0)
                {
                        buf[s] = '\\0';
                        printf("child send to father : %s",buf);
                }
                else if(s == 0)
                {
                        printf("read file end !\\n");
                        break;
                }
                else
                {
                        perror("read\\n");
                        break;
                }
                close(fd[0]);
                break;
        }

        waitpid(id,NULL,0);

        return 0;
}                                                                                                      

让父进程一直不读取,子进程一直在写入,就可以得出管道的大小为 65536 字节

命名管道

匿名管道可以让具有亲缘关系的进程完成通信,那毫无关系的进程怎么完成通信呢? 我们可以借助命名管道来完成,不同进程可以通过文件名打开同一个文件,就可以让不同进程看到同一份资源,但普通文件是很难做到通信的,实际上这个文件在磁盘上只是一个标识符,没有任何内容

命令创建命名管道

mkfifo fifo

举例 :
一个进程下执行如下命令

while :; do echo "hello world" ;sleep 1;done > fifo

一个进程下执行如下命令

cat < fifo

可以看到两个进程通过 fifo 命名管道完成了通信,当把读端关闭的时候,写端就无意义了,操作系统将写端杀掉了

函数创建命名管道

int mkfifo(const char *filename,mode_t mode);
创建成功返回0,失败返回-1

下面来写代码完成两个不相关的进程间的通信

// commit.h

#pragma once
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define FILE_NAME "myfifo"

// colient.c

#include"comm.h"
int main()
{
        int fd = open(FILE_NAME,O_WRONLY);
        if(fd < 0)
        {
                perror("open\\n");
                return 1;
        }
        char以上是关于进程间通信的主要内容,如果未能解决你的问题,请参考以下文章

进程间通信——管道

进程间通信

进程间通信

进程间通信

iOS进程间通信之CFMessagePort

操作系统实验3共享内存进程间通信实验