Linux —— 进程间通信(1)

简介: Linux —— 进程间通信(1)

一、进程间通信的介绍

1.进程间通信的概念

进程通信(Interprocess communication),简称:IPC;


       本来进程之间是相互独立的。但是由于不同的进程之间可能要共享某些信息,所以就必须要有通讯来实现进程间的互斥和同步。比如说共享同一块内存、管道、消息队列、信号量等等就是实现这一过程的手段,相当于移动公司在打电话的作用。

2.进程间通信的目的


    数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

3.进程间通信的前提

        进程间通信的前提本质:由操作系统参与,提供一份所有通信进行都能看到的公共资源;两个或多个进程相互通信,必须先看到一份公共的资源,这里的所谓的资源是属于操作系统的,就是一段内存(可能以文件的方式提供、可能以队列的方式提供,也有可能提供的就是原始内存块),这也就是通信方式有很多种的原因;

4.进程间通信的分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存(重点介绍)
  • System V 信号量

POSIX IPC(本次不做介绍)

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

二、管道

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

60a6bcefe26f4b118e50f46e4d0afd1d.png

       通过管道我们查看test.c文件写了多少行代码。其中cat和wc是两个命令,运行起来也就是进程,cat test.c 进程将查看内容通过管道交给了下一个进程wc -l 来计算代码行数;

60a6bcefe26f4b118e50f46e4d0afd1d.png

三、匿名管道

1.基本原理

匿名管道用于进程间通信,且仅限于父子进程之间的通信。

60a6bcefe26f4b118e50f46e4d0afd1d.png

 我们知道进程的PCB中包含了一个指针数组 struct file_struct,它是用来描述并组织文件的。父进程和子进程均有这个指针数组,因为子进程是父进程的模板,其代码和数据是一样的;


       打开一个文件时,其实是将文件加载到内核中,内核将会以结构体(struct  file)的形式将文件的相关属性、文件操作的指针集合(即对应的底层IO设备的调用方法)等;


       当父进程进行数据写入时(例如:写入“hello Linux”),数据是先被写入到用户级缓冲区,经由系统调用函数,又写入到了内核缓冲区,在进程结束或其他的操作下才被写到了对应的设备中;


       如果数据在写入设备之前,“hello Linux”是在内核缓冲区的,因为子进程和父进程是同时指向这个文件的,所以子进程是能够看到这个数据的,并且可以对其操作;


       简单来说,父进程向文件写入数据时,不直接写入对应的设备中,而是将数据暂存在内核缓冲区中,交给子进程来处理;


       所以这种基于文件的方式就叫做管道;

2.管道的创建步骤

       在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

60a6bcefe26f4b118e50f46e4d0afd1d.png

       匿名管道属于单向通信,意味着父子进程只有一个端是打开的,实现父子通信的时候就需要根据自己的想要实现的情况,关闭对应的文件描述符;

1.pipe函数

#include <unistd.h>
int pipe(int pipefd[2]);

函数的参数是两个文件的描述符,是输出型参数:

  • pipefd[0]:读管道 --- 对应的文件描述符是3
  • pipefd[1]:写管道 --- 对应的文件描述符是4

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

#include <stdio.h>
#include <unistd.h>                                                                                                          
#include <stdlib.h>
#include <string.h>
int main()
{
    int pipefd[2] = {0};
    if(pipe(pipefd) != 0){
        perror("pipe error!");
        return 1;
    }
    //pipefd[0]:读取段  pipefd[1]:写入端
    printf("pipefd[0]:%d\n",pipefd[0]);//3
    printf("pipefd[1]:%d\n",pipefd[1]);//4
    return 0;
}

60a6bcefe26f4b118e50f46e4d0afd1d.png

2.代码实战

接下来我们来实现子进程写入数据,父进程读取数据;那么我们就需要针对父子进程关闭对应的文件描述符fd,子进程关闭读端,父进程关闭写端;

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>                                                                                                          
#include <string.h>
//让子进程sleep
int main()
{
    int pipefd[2] = {0};
    if(pipe(pipefd) != 0){ //创建匿名管道
        perror("pipe error!");
        return 1;
    }
    //pipefd[0]:读取端  pipefd[1]:写入端
    printf("pipefd[0]:%d\n",pipefd[0]);//3
    printf("pipefd[1]:%d\n",pipefd[1]);//4
    if(fork() == 0){
        //子进程---写入
        close(pipefd[0]); //关闭子进程的读取端
        const char* msg = "hello-linux!";
        while(1){
            write(pipefd[1], msg, strlen(msg)); //子进程不断的写数据
            sleep(1);
        }
    exit(0);
    }
    //父进程---读取
    close(pipefd[1]); //关闭父进程的写入端
    char buffer[64] = {0};
    while(1){
        //如果read返回值是0,就意味着子进程关闭文件描述符了
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程不断的读数据
        if(s == 0){
            break;
        }
        else if(s > 0){
            buffer[s] = 0;
            printf("child say to father:%s\n",buffer);
        }
        else{
            break;
        }
    }
return 0;
}

60a6bcefe26f4b118e50f46e4d0afd1d.png

3.管道的五个特点和四种情况

五个特点:

  1. 管道是一个只能单向通信的通信信道,仅限于父子间通信
  2. 管道提供流式服务
  3. 管道操作自带同步与互斥机制
  4. 进程退出,管道释放,所以管道的生命周期随进程
  5. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

四种情况:

  1. 读端不读或者读的慢,写端要等待读端;
  2. 读端关闭,写端收到SIGPIPE信号直接终止;
  3. 写端不写或者写的慢,读端要等待写端;
  4. 写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾;

接下来我们通过下面的程序进行验证 :管道是单向通信和面向字节流

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>                                                                                                          
#include <string.h>
int main()
{
    int pipefd[2] = {0};
    if(pipe(pipefd) != 0){ //创建匿名管道
        perror("pipe error!");
        return 1;
    }
    //pipefd[0]:读取端  pipefd[1]:写入端
    printf("pipefd[0]:%d\n",pipefd[0]);//3
    printf("pipefd[1]:%d\n",pipefd[1]);//4
    if(fork() == 0){
        //子进程---写入
        close(pipefd[0]); //关闭子进程的读取端
        const char* msg = "hello-linux!";
        while(1){
            write(pipefd[1], msg, strlen(msg)); //子进程写数据
            sleep(1);
        }
    exit(0);
    }
    //父进程---读取
    close(pipefd[1]); //关闭父进程的写入端
    char buffer[64] = {0};
    while(1){
        sleep(1);
        ssize_t s = read(pipefd[0], buffer, sizeof(buffer)); //父进程读数据
        if(s == 0){
            break;
        }
        else if(s > 0){
            buffer[s] = 0;
            printf("child say to father:%s\n",buffer);
        }
        else{
            break;
        }
    }
return 0;
}

上述代码中,在父子进程中都有sleep函数:(我们切换使用)

1.当子进程sleep时,父进程没有sleep,运行结果如下:

60a6bcefe26f4b118e50f46e4d0afd1d.png

我们可以发现,子进程在写入数据后经由管道交给父进程处理,这就验证了管道是单向通信的信道

2.当父进程sleep时,子进程没有sleep,运行结果如下:

60a6bcefe26f4b118e50f46e4d0afd1d.png

       我们发现打印出来的数据并不想像刚才那样一条一条的打印,这是因为子进程在写入数据时,只要pipe内部有缓冲区,就不断的写入;当父进程在读取的时候,只要管道内有数据就会一直读;这就是所谓的字节流;即管道是面向字节流的(提供流式服务)

通过下面的程序来验证:同步机制

int main()
{
     int pipefd[2] = {0};
     if(pipe(pipefd) != 0){
         perror("pipe error!");
         return 1;                                                                                                            
     }
     //pipefd[0]:读取端  pipefd[1]:写入端
     printf("pipefd[0]:%d\n",pipefd[0]);//3
     printf("pipefd[1]:%d\n",pipefd[1]);//4
     if(fork() == 0){
         //子进程---写入
         close(pipefd[0]);
         int count = 0;
         while(1){
             write(pipefd[1], "a", 1);
             count++;
             printf("count: %d\n",count);
         }
         exit(0);
     }
     //父进程---读取
     close(pipefd[1]);
     while(1){
         sleep(1);
     }
     return 0;
}

       上面的代码中,子进程在不断的写入数据,而父进程一直不读取数据,运行结果如下:

60a6bcefe26f4b118e50f46e4d0afd1d.png

 我们运行起来后,就会一直刷屏,直到count为65536的时候停下来。这里为什么子进程不继续写了呢?这首先说明管道是有大小的,在我的云服务器下Linux的管道容量是65536(64Kb),其次子进程不继续写了,表明写端写满后要等待读端读取,才可以继续写入;


       我们对上面的代码进行修改,让父进程一次读取一个字符,检验一下子进程会不会继续写入。

//这里简写了,其他内容和上面的代码一样
//父进程---读取
close(pipefd[1]);
while(1){
    sleep(10);
    char c = 0;
    read(pipefd[0], &c, 1);
    printf("father taken:%c\n", c);                                                                                     
}

60a6bcefe26f4b118e50f46e4d0afd1d.png

       我们发现父进程每过10秒读取一个字符,但是子进程并没有写入,我们试着将读取字符大小调整到4096个字节时,会发现读端读走数据后,写端就进行写入了;这表明管道自带同步机制(当然管道肯定也是有互斥机制的,这里不做讲解)。

通过下面的程序验证:写端不写或者写的慢,读端会等待写端;(读端不写同理)

int main()
{
     int pipefd[2] = {0};                                                                                                     
     if(pipe(pipefd) != 0){
         perror("pipe error!");
         return 1;
     }
     //pipefd[0]:读取端  pipefd[1]:写入端
     printf("pipefd[0]:%d\n",pipefd[0]);//3                                                                                  
     printf("pipefd[1]:%d\n",pipefd[1]);//4
     //子进程写的慢
     if(fork() == 0){
         //子进程---写入
         close(pipefd[0]);
         const char* msg = "hello-linux!";
         while(1){
             write(pipefd[1], msg, strlen(msg));
             sleep(10);    
         }
         exit(0);
     }
     //父进程---读取
     close(pipefd[1]);
     while(1){
         sleep(10);
         char c[64] = {0};
         ssize_t s = read(pipefd[0], &c, sizeof(c)-1);
         c[s] = 0;
         printf("father taken:%s\n", c);
     }
     return 0;
}

运行结果如下:

60a6bcefe26f4b118e50f46e4d0afd1d.png

       从运行结果可以看出,读端是在等待写端的,这也就是所谓的同步机制,当我们对写端不在进行写入时,读端也会一直在的等待写端的数据写入

通过下面的程序验证:写端关闭,读端读完pipe内部的数据然后再读,会读到0为止,表明读到文件结尾

int main()
{
     int pipefd[2] = {0};                                                                                                     
     if(pipe(pipefd) != 0){
         perror("pipe error!");
         return 1;
     }
     //pipefd[0]:读取端  pipefd[1]:写入端
     printf("pipefd[0]:%d\n",pipefd[0]);//3                                                                                  
     printf("pipefd[1]:%d\n",pipefd[1]);//4
     //子进程写的慢
     if(fork() == 0){
        //子进程---写入
        close(pipefd[0]);
        const char* msg = "hello-linux!";
        while(1){
            write(pipefd[1], msg, strlen(msg));
            sleep(10); 
            break;   
        }
        close(pipefd[1]);
        exit(0);
     }
     //父进程---读取
     close(pipefd[1]);
     while(1){
        sleep(10);
        char c[64] = {0};
        ssize_t s = read(pipefd[0], &c, sizeof(c)-1);
        if(s > 0){
            c[s] = 0;
            printf("father taken:%s\n", c);
        }
        else if(s ==0){
            printf("write quit...\n");
            break;
        }
        else{
            break;
        }
     }
     return 0;
}

       在上面的程序中,我们让写端写入一条数据后,10秒直接退出,然后关闭读端,运行结果如下:

60a6bcefe26f4b118e50f46e4d0afd1d.png

当写端写入数据后关闭了写端,读端会从管道内读取到文件的末尾,接收到写端关闭后,就自行退出了。

通过下面的程序验证: 读端关闭,写端收到SIGPIPE信号直接终止

int main()
{
     int pipefd[2] = {0};                                                                                                     
     if(pipe(pipefd) != 0){
         perror("pipe error!");
         return 1;
     }
     //pipefd[0]:读取端  pipefd[1]:写入端
     printf("pipefd[0]:%d\n",pipefd[0]);//3                                                                                  
     printf("pipefd[1]:%d\n",pipefd[1]);//4
     //子进程写的慢
     if(fork() == 0){
        //子进程---写入
        close(pipefd[0]);
        const char* msg = "hello-linux!";
        while(1){
            write(pipefd[1], msg, strlen(msg));  
        }
        exit(0);
     }
     //父进程---读取
     close(pipefd[1]);
     while(1){
        sleep(10);
        char c[64] = {0};
        ssize_t s = read(pipefd[0], &c, sizeof(c)-1);
        if(s > 0){
            c[s] = 0;
            printf("father taken:%s\n", c);
        }
        else if(s ==0){
            printf("write quit...\n");
            break;
        }
        else{
            break;
        }
        break;
     }
     close(pipefd[0]);
     return 0;
}

60a6bcefe26f4b118e50f46e4d0afd1d.png

     首先我们对程序进行分析,子进程处于一直写的状态,父进程读取一次数据后就break了,然后将读端关闭了(文件描述符0);


       当我们的读端关闭,写端还在写入,在操作系统的层面上,严重不合理;这本质上就是在浪费操作系统的资源,所以操作系统在遇到这样的情况下,会将子进程杀掉(发送13号信号---SIGPIPE);

close(pipefd[0]);
//在源程序的基础上加上,用来获取子进程退出信号
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d\n",(status >> 8)& 0xFF);
printf("exit signal: %d\n",status& 0x7F);

60a6bcefe26f4b118e50f46e4d0afd1d.png

目录
相关文章
|
6天前
|
消息中间件 算法 Linux
【Linux】详解如何利用共享内存实现进程间通信
【Linux】详解如何利用共享内存实现进程间通信
|
6天前
|
Linux
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
|
6天前
|
Linux
【Linux】匿名管道实现简单进程池
【Linux】匿名管道实现简单进程池
|
6天前
|
Linux
【Linux】进程通信之匿名管道通信
【Linux】进程通信之匿名管道通信
|
6天前
|
存储 Linux Shell
Linux:进程等待 & 进程替换
Linux:进程等待 & 进程替换
30 9
|
6天前
|
存储 Linux C语言
Linux:进程创建 & 进程终止
Linux:进程创建 & 进程终止
29 6
|
6天前
|
Linux 数据库
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
|
6天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
6天前
|
Linux 调度 C语言