• [Linux]进程间通信--管道


    [Linux]进程间通信–管道

    进程间通信的目的

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

    实现进程间通信的原理

    进程是具有独立性的,一个进程是无法看到另一个进程的代码和数据的,为了让进程间通信,要做的工作就是让不同的进程看到同一份“资源”。

    任何进程通信手段需要解决的问题如下:

    • 让不同的进程看到同一份“资源”
    • 让一方进行读取,另一方进行写入

    不同的进程间通信手段本质的区别就是让不同的进程看到同一份“资源”的方式不同。

    匿名管道

    匿名管道是一种以文件为媒介的通信方式,匿名管道是一个内存级别的文件,拥有和普通文件一样的缓冲区,但是操作系统不会将缓冲区刷新至外设,匿名管道虽然是文件,但是由于没有文件路径,进程是无法通过系统文件接口来操作的,因此匿名管道通常用于父子进程之间使用。

    匿名管道的通信原理

    由于匿名管道没有文件路径,进程是无法通过系统文件接口来操作的特性,匿名管道必须通过父进程创建,子进程继承父进程文件描述符表的方式,使得不同的进程看到同一个文件:

    image-20230909104154011

    由于匿名管道只支持单向通信,在使用匿名管道进行通信时,父进程必须分别以读方式和写方式打开管道文件,子进程继承了文件描述符表后,一方关闭读端,一方关闭写端进行通信。

    注意: 如果父进程只以读方式或者写方式打开,子进程继承文件描述符表后,也是同样的方式,子进程自身无法打开该管道,因此导致无法通信。

    系统接口

    Linux系统提供了创建匿名管道的系统接口pipe:

    //pipe所在的头文件和声明
    #include 
    
    int pipe(int pipefd[2]);
    
    • 1
    • 2
    • 3
    • 4
    • pipefd为输出型参数,用于接收以读方式和写方式打开管道的文件描述符。
    • pipefd[0]获取读端文件描述符,pipefd[1]获取写端文件描述符。
    • 成功返回0,失败返回-1,错误码被设置。

    编写如下代码测试pipe接口:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
        //创建管道
        int pipefd[2] = { 0 };
        int n = pipe(pipefd);
        if (n < 0)//出错判断
        {
            cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
            exit(1); 
        }
        //创建子进程
        pid_t id = fork();
        assert(id != -1);//出错判断
    
        //进行通信 -- 父进程进行读取,子进程进行写入
        if (id == 0)
        {
            //子进程
            close(pipefd[0]);
            
            const string str = "hello world";
            int cnt = 1;
            char buffer[1024];
            while(1)
            {
                snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
                write(pipefd[1], buffer, strlen(buffer));//向管道写入数据
                sleep(1);
            }
            close(pipefd[1]);
            exit(0);
        }
    
        //父进程
        close(pipefd[1]);
        char buffer[1024];
        while(1)
        {
            read(pipefd[0], buffer, sizeof(buffer) - 1);//从管道读取数据
            cout << "我是父进程," << "child give me: " << buffer << endl;
        }
        close(pipefd[0]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    编译代码运行查看结果:

    pipe1演示

    从运行结果可以看出,建立管道后,父子进程就能够进行数据通信。

    管道特性

    1. 单向通信,半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    2. 管道的本质是文件,因此管道的生命周期随进程
    3. 管道通信,通常适用于具有“血缘关系的进程”,诸如父子进程、兄弟进程等
    4. 管道的数据是以字节流的形式传输的,读写次数的多数不是强相关的
    5. 具有一定的协同机制

    管道的协同场景

    场景一: 如果管道内部的数据被读端读取完了,写端不写入,读端就只能等待

    编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
        //创建管道
        int pipefd[2] = { 0 };
        int n = pipe(pipefd);
        if (n < 0)
        {
            cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
            exit(1); 
        }
        //创建子进程
        pid_t id = fork();
        assert(id != -1);
    
        //进行通信 -- 父进程进行读取,子进程进行写入
        if (id == 0)
        {
            //子进程
            close(pipefd[0]);
            
            const string str = "hello world";
            int cnt = 1;
            char buffer[1024];
            while(1)
            {
                snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
                write(pipefd[1], buffer, strlen(buffer));
                sleep(100); // ---------  模拟写入暂停  --------- 
            }
            close(pipefd[1]);
            exit(0);
        }
    
        //父进程
        close(pipefd[1]);
        char buffer[1024];
        while(1)
        {
            read(pipefd[0], buffer, sizeof(buffer) - 1);
            cout << "我是父进程," << "child give me: " << buffer << endl;
        }
        close(pipefd[0]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    编译代码运行查看结果:

    pipe2演示

    场景二: 如果管道内部的数据被写端写满了,读端不读取,写端无法继续写入

    编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
        //创建管道
        int pipefd[2] = { 0 };
        int n = pipe(pipefd);
        if (n < 0)
        {
            cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
            exit(1); 
        }
        //创建子进程
        pid_t id = fork();
        assert(id != -1);
    
        //进行通信 -- 父进程进行读取,子进程进行写入
        if (id == 0)
        {
            //子进程
            close(pipefd[0]);
            
            const string str = "hello world";
            int cnt = 1;
            char buffer[1024];
            while(1)
            {
                snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
                write(pipefd[1], buffer, strlen(buffer));
                printf("cnt: %d\n", cnt); // ---------  显示写入过程  --------- 
                //sleep(100);
            }
            close(pipefd[1]);
            exit(0);
        }
    
        //父进程
        close(pipefd[1]);
        char buffer[1024];
        while(1)
        {
            sleep(100); // ---------  模拟读取暂停  --------- 
            read(pipefd[0], buffer, sizeof(buffer) - 1);
            cout << "我是父进程," << "child give me: " << buffer << endl;
        }
        close(pipefd[0]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    编译代码运行查看结果:

    pipe3演示

    场景三: 写端关闭,读端读完了管道内部的数据时,再读就读到了文件的结尾。

    编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
        //创建管道
        int pipefd[2] = { 0 };
        int n = pipe(pipefd);
        if (n < 0)
        {
            cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
            exit(1); 
        }
        //创建子进程
        pid_t id = fork();
        assert(id != -1);
    
        //进行通信 -- 父进程进行读取,子进程进行写入
        if (id == 0)
        {
            //子进程
            close(pipefd[0]);
            
            const string str = "hello world";
            int cnt = 1;
            char buffer[1024];
            while(1)
            {
                snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
                write(pipefd[1], buffer, strlen(buffer));
                printf("cnt: %d\n", cnt);
                sleep(1);
                if (cnt == 5) break; // ---------  写端关闭  --------- 
            }
            close(pipefd[1]);
            exit(0);
        }
    
        //父进程
        close(pipefd[1]);
        char buffer[1024];
        while(1)
        {
            int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                cout << "我是父进程," << "child give me: " << buffer << endl;
            }
            else if (n == 0)// ---------  判断读取到文件末尾  --------- 
            {
                cout << "读取完毕, 读到文件结尾" << endl;
                break;
            }
            else
            {
                cout << "读取出错" << endl;
                break;
            }
        }
        close(pipefd[0]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    编译代码运行查看结果:

    pipe4演示

    **场景四:**写端一直写,读端关闭,操作系统会给写端发送13号信号终止进程。

    编写如下代码(如下代码只是在前文测试pipe接口的代码上做略微改动,主要改动已用-----标识)进行验证:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
        //创建管道
        int pipefd[2] = { 0 };
        int n = pipe(pipefd);
        if (n < 0)
        {
            cout << "errno: " << errno << "strerror: " << strerror(errno) << endl;
            exit(1); 
        }
        //创建子进程
        pid_t id = fork();
        assert(id != -1);
    
        //进行通信 -- 父进程进行读取,子进程进行写入
        if (id == 0)
        {
            //子进程
            close(pipefd[0]);
            
            const string str = "hello world";
            int cnt = 1;
            char buffer[1024];
            while(1)
            {
                snprintf(buffer, sizeof(buffer), "%s, 我是子进程, 我的pid:%d, 计数器:%d", str.c_str(), getpid(), cnt++);
                write(pipefd[1], buffer, strlen(buffer));
                printf("cnt: %d\n", cnt);
                sleep(1);
            }
            close(pipefd[1]);
            exit(0);
        }
    
        //父进程
        close(pipefd[1]);
        char buffer[1024];
        while(1)
        {
            int cnt = 0;
            //sleep(100);
            int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                cout << "我是父进程," << "child give me: " << buffer << endl;
            }
            else if (n == 0)
            {
                cout << "读取完毕, 读到文件结尾" << endl;
                break;
            }
            else
            {
                cout << "读取出错" << endl;
                break;
            }
            //sleep(100);
            sleep(5);
            break;// ---------  读端关闭  --------- 
        }
        close(pipefd[0]);
        int status = 0;
        waitpid(id, &status, 0);
        cout << "signal: " << (status & 0x7F) << endl;// --------- 回收子进程获取退出信号  --------- 
        sleep(3);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    编译代码运行查看结果:

    pipe5演示

    管道的大小

    在Linux下,管道(Pipe)的大小受到操作系统的限制。具体来说,管道的大小由内核参数PIPE_BUF定义,通常是4096个字节。

    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
    • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

    命名管道

    命名管道同样是内存级的文件,和匿名管道的区别就是命名管道可以在指定路径下创建,并且命名可以指定,因此命名管道可以给任何两个不同的进程用于通信。

    使用指令创建命名管道

    Linux下使用mkfifo 指令就可以在指定路径下创建命名管道。

    image-20230910162001046

    命名管道同样和匿名管道一样满足管道的协同场景:

    namepipe2演示

    写端尝试打开管道文件,没有读端,写端就会卡在打开文件这一步骤。

    namepipe1演示

    右侧读端开始会等待写端写入,后续关闭右侧读端,左侧写端进程直接被终止。

    使用系统调用创建命名管道

    //mkfifo所在的头文件和声明
    #include 
    #include 
    
    int mkfifo(const char *pathname, mode_t mode);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • pathname参数 – 创建命名管道的路径
    • mode参数 – 创建命名管道的文件权限
    • 成功返回0,失败返回-1,错误码被设置。

    为了测试mkfifo接口编写代码进行测试,首先设置文件结构如下:

    image-20230910164247561

    makefile文件内容如下:

    .PHONY:all
    all:client server
    
    client:client.cc
    	g++ -o $@ $^ -std=c++11
    
    server:server.cc
    	g++ -o $@ $^ -std=c++11
    
    .PHONY:clean
    clean:
    	rm -rf client server
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    common.hpp主要用于让两个进程获取管道路径,具体内容如下:

    #include 
    #include 
    
    #define NUM 1024
    
    const std::string pipename = "./namepipe"; //管道的路径和管道名
    
    mode_t mode = 0666; //创建管道的文件权限
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    client.cc作为写端输入数据,具体内容如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "commn.hpp"
    
    int main()
    {
        // 打开管道文件
        int wfd = open(pipename.c_str(), O_WRONLY);
        if (wfd < 0)
        {
            std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
            exit(1);
        }
    
        //进行通信
        while(true)
        {
            char buffer[NUM];
            std::cout << "请输入内容:";
            fgets(buffer, sizeof(buffer), stdin);//获取用户输入
            buffer[strlen(buffer) - 1] = 0;
    
            if (strcasecmp(buffer, "quit") == 0) break;//用户输入quit退出进程
    
            ssize_t size = write(wfd, buffer, strlen(buffer));
            assert(size >= 0);
            (void)size;
        }
    
        close(wfd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    server.cc作为读端用于接收写端的输入并打印,具体内容如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "commn.hpp"
    
    int main()
    {
        umask(0);
        // 创建管道文件
        int n = mkfifo(pipename.c_str(), mode);
        if (n < 0)
        {
            std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
            exit(1);
        }
        std::cout << "create fifo file success" << std::endl;
    
        // 以读方式打开管道文件
        int rfd = open(pipename.c_str(), O_RDONLY);
        if (rfd < 0)
        {
            std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
            exit(2);
        }
    
        // 进行通信
        while (true)
        {
            char buffer[NUM];
    
            ssize_t size = read(rfd, buffer, sizeof(buffer) - 1);
            buffer[size] = 0;
            if (size > 0)
            {
                std::cout << "client send me :" << buffer << std::endl;//输出接收的信息
            }
            else if (size == 0)
            {
                std::cout << "client quit, me too!" << std::endl;
                break;
            }
            else
            {
                std::cerr << "errno : " << errno << "strerror : " << strerror(errno) << std::endl;
                break;
            }
        }
    
        close(rfd);
        unlink(pipename.c_str()); // 删除文件
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    编译代码运行查看结果:

    namepipe3演示

  • 相关阅读:
    Java项目:ssm实验室设备管理系统
    详解:MySQL自增ID与UUID的优缺点及选择建议,MySQL有序uuid与自定义函数实现
    人工智能与智能系统3-> 机器人学3 | 移动机器人平台
    阿里云杨皓然:Serverless或将引领云的下一个时代
    82-FastDFS详解
    2022运营版开发代驾小程序/仿滴滴代驾小程序/打车/网约车/顺风车/快车/代驾/货运/Thinkphp+Uniapp开源版
    如何从零开始解读产品经理需求分析-需求挖掘
    这些嵌入式系统安全性的知识你需要了解
    《Java核心知识点》+《Java面试宝典》+《1000道互联网面试专题》+《350道Java面试》,总共1045页
    Axios、SASS学习笔记
  • 原文地址:https://blog.csdn.net/csdn_myhome/article/details/132793660