• 进程间通信


    进程间通信介绍

    进程间通信(IPC -> Inter Process Communication)是指不同进程之间交换信息或同步其行为的机制。它的主要目的是使进程之间能够相互协作、共享数据,以实现更加复杂的功能。

    进程间通信目的

    • 数据传输:一个进行需要向另外一个进程发送数据。
    • 数据共享:不同进程之间共享数据,以避免数据冗余和数据不一致的问题。
    • 任务协助:多个进程需要相互协作,共同完成某项任务,以提高系统的性能和效率。
    • 通知和事件处理:一个进程需要通知另一个进程某个事件的发送,以便进行相应的处理。
    • 进程控制:某些进行需要完全控制另一个进程的执行,例如 Debug 进程。进程控制需要能够拦截另一个进程的所有陷入和异常,并及时知道其状态的变化。

    进程间通信的本质

    进程间通信的本质是运行不同进程之间进行数据交换和共享,从而协作完成特定任务。由于每个进程都是独立的执行体,其运行环境和数据空间都是相互隔离的,因此进程间通信需要借助操作系统提供的通信机制。

    为了实现进程间通信,操作系统提供了一些机制,这些机制允许进程在共享的内存区域或是操作系统内核中的数据结构上进行读写操作,以便实现数据的传输和共享。在使用这些机制时,需要注意进程同步和互斥的问题,以保证数据的正确性和完整性。

    在这里插入图片描述

    因此,IPC 的本质是通过操作系统提供的通信机制,让不同的进程之间进行数据交换和共享,从而协同完成任务。

    进程间通信的机制

    管道

    • 匿名管道
    • 命名管道

    System V IPC

    • System V 消息队列
    • System V 共享内存
    • System V 信号量

    POSIX IPC

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

    常见的 IPC 机制如上所示,它们有各自不同的特点和适用场景,根据实际的需求选择合适的机制可以有效的提高进程间通信的效率和可靠性。

    管道

    什么是管道

    在 IPC 中,管道是一种半双工的通信方式,用于在两个进程之间传递数据。管道哦通常用于在父子进程之间进行数据传输。

    • 管道是 Unix 中最古老的进程间通信的形式。
    • 我们将一个进程连接到另外一个进程的一个数据流称为一个 ‘‘管道’’。
    • 管道实际上是一个内核缓冲区,用于存放一个进程向另一个进程传输的数据。它是一个单项通道,且是一个先进先出(FIFO)的数据结构。

    例如,统计我们当前使用的云服务器上登录的用户个数:

    在这里插入图片描述

    其中,who 命令和 wc 命令是两个不同的程序,当他们运行起来后就变成了两个进程。who 进程通过标准输出将数据写入 ‘‘管道’’ 中,wc 进行通过标准输出从 ‘‘管道’’ 中读取数据,命令中的 | 就表示管道,它们以此来实现两个进程之间的数据共享。

    在这里插入图片描述

    匿名管道

    匿名管道是一种进程间通信方式,它是一种半双工通信方式,即数据只能单向流动。匿名管道只能在具有情缘关系的进程之间使用,如父子进程之间的通信。

    匿名管道在创建时,操作系统会在内存中开辟一个缓冲区作为数据传输的通道,这个缓冲区的大小是有限制的。管道只能顺序进行读写,先写入的数据先被读出,因此匿名管道具有一定的局限性,仅适用于较小的数据量传输。

    pipe函数

    pipe 函数用于创建匿名管道,pipe 函数的语法如下:

    #include 
    
    int pipe(int pipefd[2]);
    
    • 1
    • 2
    • 3

    返回值:成功则返回 0。失败则返回 -1,并设置对应的错误码。

    说明:

    • pipe() 创建一个管道,一个可用于进程间通信的单向数据通道。数组 pipefd 用于返回两个指向管道末端的文件描述符。pipefd[0] 是管道的读端,pipefd[1] 是管道的写端。写入管道的写端数据由内核进行缓冲,直到从管道的读端读取为止。
    • 管道创建成功之后,父子进程分别关闭自己不需要的文件描述符。若父进程写入数据,子进程读取,则父进程关闭 pipefd[0],子进程关闭 pipefd[1],这样就可以实现父子之间的通信了。

    在这里插入图片描述

    匿名管道的创建与使用

    创建匿名管道实现父子进程间通信的过程中,需要 pipe 函数和 fork 函数配合使用,如下所示:父进程是写端,子进程是读端。

    step1: 父进程调用 pipe 函数创建管道。

    在这里插入图片描述
    step2:父进程调用 frok 创建子进程。

    在这里插入图片描述
    step3:父进程关闭读端,子进程关闭写端。

    在这里插入图片描述

    站在文件描述符角度 - 深度理解管道

    step1:父进程创建管道。
    在这里插入图片描述
    step2:父进程调用 fork 创建子进程。

    在这里插入图片描述

    step3:父进程关闭 fd[0],子进程关闭 fd[1]。

    在这里插入图片描述

    示例:该代码使用匿名管道实现父子进程之间的通信。父进程向管道中写入数据,子进程从管道中读取数据。代码主要过程如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    // 演示pipe通信的基本过程 -- 匿名管道
    #define NUM 1024
    
    int main()
    {
        // 1.创建管道
        int pipefd[2] = {0};
        if (pipe(pipefd) != 0)
        {
            cerr << "pipe error" << endl;
            return 1;
        }
    
        // 2.创建子进程
        pid_t id = fork();
        if (id < 0)
        {
            cerr << "fork error" << endl;
            return 1;
        }
        else if (id == 0)
        {
            // child process
            // 子进程用来进行读取,因此,子进程应该关闭写端 -> pipefd[1]
            close(pipefd[1]);
            char buffer[NUM] = {0};
    
            ssize_t s;
            while ((s = read(pipefd[0], buffer, sizeof(buffer) - 1)) > 0)
            {
                cout << "时间戳:" << (uint64_t)time(nullptr) << endl; // 打印子进程读取数据的时间
                // 读取成功
                buffer[s] = '\0';
                cout << "子进程读取数据成功,数据是:" << buffer << endl;
            }
    
            close(pipefd[0]);
            exit(0);
        }
        else
        {
            // parent process
            // 父进程用来写入数据,因此,父进程应该关闭读端
            close(pipefd[0]);
            const char *msg = "这是一条由父进程发出的数据->";
            int cnt = 1;
            while (cnt <= 5)
            {
                char sendBuffer[NUM];
                sprintf(sendBuffer, "%s : %d", msg, cnt);
                write(pipefd[1], sendBuffer, strlen(sendBuffer));
                cnt++;
                sleep(1);
            }
            close(pipefd[1]);
            cout << "父进程数据已写完!!!" << endl;
        }
    
        pid_t ret = waitpid(id, nullptr, 0);
        if (ret > 0)
            cout << "等待子进程成功!" << endl;
        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

    运行结果如下所示:

    在这里插入图片描述

    匿名管道实现进程任务调度

    以下代码实现了一个父子进程之间通过匿名管道进行通信,实现了父进程向子进程派发任务的功能。具体实现过程如下:

    1. 定义了 3 个函数(f1、f2、f3),分别代表三个不同任务,用一个 vector 容器保存了这三个函数的函数指针。
    2. loadFunctor() 函数用于将这三个函数指针储存在 functors 这个 vector 中,并使用一个 unordered_map 容器保存每个任务的编号和名称。
    3. 程序通过 pipe 函数创建一个管道,使用 fork 函数创建一个子进程。
    4. 子进程通过管道读取父进程派发的任务编号,根据编号在 functors 容器中找到对应的任务并执行。
    5. 父进程通过 write 函数将任务编号写入管道中。为了模拟分发场景,父进程通过 rand 函数随机生成任务编号。
    6. 最后,程序通过 waitpid 函数等待子进程退出,防止出现僵尸进程。
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    typedef void (*functor)(); // 定义一个名为functor的函数指针
    
    vector<functor> functors; // 方法集合
    
    // for debug
    unordered_map<uint32_t, string> info;
    
    void f1()
    {
        cout << "执行处理日志的任务,进程ID -> " << getpid() << " , "
             << "执行时间:" << (uint64_t)time(nullptr) << endl;
    }
    
    void f2()
    {
        cout << "执行备份数据的任务,进程ID -> " << getpid() << " , "
             << "执行时间:" << (uint64_t)time(nullptr) << endl;
    }
    
    void f3()
    {
        cout << "处理网路连接的任务,进程ID -> " << getpid() << " , "
             << "执行时间:" << (uint64_t)time(nullptr) << endl;
    }
    
    void loadFunctor()
    {
        info.insert({functors.size(), "执行处理日志的任务"});
        functors.push_back(f1);
    
        info.insert({functors.size(), "执行备份数据的任务"});
        functors.push_back(f2);
    
        info.insert({functors.size(), "处理网路连接的任务"});
        functors.push_back(f3);
    }
    
    void errno_exit(const char *msg)
    {
        perror(msg);
        exit(EXIT_FAILURE);
    }
    
    // 父进程控制子进程
    int main()
    {
        // 1.加载任务列表
        loadFunctor();
    
        // 2.创建管道
        int pipefd[2] = {0};
        if (pipe(pipefd) != 0)
            errno_exit("pipe");
    
        // 3.创建子进程
        pid_t id = fork();
        if (id < 0)
            errno_exit("fork");
        else if (id == 0)
        {
            // 子进程用于执行父进程派发的任务
    
            // 3.关闭不需要的文件描述符
            close(pipefd[1]);
    
            // 4.处理任务
            uint32_t operatorType = 0;
            ssize_t s;
            while ((s = read(pipefd[0], &operatorType, sizeof(uint32_t))) > 0)
            {
                if (operatorType < functors.size())
                    functors[operatorType]();
                else
                    cerr << "bug? operatorType = " << operatorType << endl;
            }
            if (s == 0)
                cout << "子进程退出了..." << endl;
            else
                errno_exit("read");
    
            // 关闭文件描述符
            close(pipefd[0]);
            exit(0);
        }
        else
        {
            // 父进程用于向派发任务
    
            // 3.关闭不需要的文件描述符
            close(pipefd[0]);
    
            // 4.指派任务
            srand((long long)time(nullptr));
            int num = functors.size();
            int cnt = 7;
            while (cnt--)
            {
                // 5.形成任务码
                uint32_t commandCode = rand() % num;
                cout << "父进程指派任务:" << info[commandCode] << " 任务编号为:" << cnt << endl;
                // 向指定的进程下达执行任务的操作
                ssize_t s = write(pipefd[1], &commandCode, sizeof(uint32_t));
                if (s != sizeof(uint32_t))
                    errno_exit("write");
                sleep(1);
            }
            close(pipefd[1]);
    
            pid_t res = waitpid(id, nullptr, 0);
            if (res)
                cout << "wait success!" << endl;
        }
        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
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128

    运行结果如下:

    在这里插入图片描述

    匿名管道实现多进程任务分发和执行

    以下代码实现了一个基于进程间通信的任务调度器。其中,父进程负责派发任务,子进程负责处理任务。子进程通过管道从父进程获取任务,然后执行。具体实现过程如下:

    1. 定义了三个函数指针 f1、f2、f3,分别表示三种不同的任务,将它们保存在一个 vector 中。
    2. 创建了 processNum 个子进程,并将它们的 pid 和对应的管道写端 fd 保存在 assignMap 中。
    3. 在父进程中,不断地随机选择一个进程和一个任务,将任务通过管道发送给子进程。
    4. 在子进程中,不断地从管道获取任务,执行相应的函数指针。
    5. 任务派发完成后,回收子进程资源,结束程序。

    代码如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    using functor = void (*)(); // 定义一个名为functor的函数指针
    
    vector<functor> functors; // 方法集合
    
    // for debug -> 将编号与任务对应
    unordered_map<uint32_t, string> info;
    
    void f1()
    {
        cout << "执行处理日志的任务,进程ID -> " << getpid() << " , "
             << "执行时间:" << chrono::system_clock::now().time_since_epoch().count() << endl;
    }
    
    void f2()
    {
        cout << "执行备份数据的任务,进程ID -> " << getpid() << " , "
             << "执行时间:" << chrono::system_clock::now().time_since_epoch().count() << endl;
    }
    
    void f3()
    {
        cout << "处理网路连接的任务,进程ID -> " << getpid() << " , "
             << "执行时间:" << chrono::system_clock::now().time_since_epoch().count() << endl;
    }
    
    void loadFunctor()
    {
        // 插入函数时使用 emplace_back() 函数,效率更高
        info.emplace(functors.size(), "执行处理日志的任务");
        functors.emplace_back(f1);
    
        info.emplace(functors.size(), "执行备份数据的任务");
        functors.emplace_back(f2);
    
        info.emplace(functors.size(), "处理网路连接的任务");
        functors.emplace_back(f3);
    }
    
    void errno_exit(const char *msg)
    {
        perror(msg);
        exit(EXIT_FAILURE);
    }
    
    // int32_t:进程pid,该进程对应的管道写端fd
    typedef pair<int32_t, int32_t> elem;
    const int32_t processNum = 5;
    
    void work(int32_t blockFd)
    {
        cout << "进程 [" << getpid() << "] 开始工作" << endl;
        // 子进程核心工作代码
        while (true)
        {
            // a.阻塞等待  b.获取任务
            uint32_t operatorCode = 0;
            ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
            if (s == 0)
                break;
            assert(s == sizeof(uint32_t));
            (void)s;
    
            // c.处理任务
            if (operatorCode < functors.size())
                functors[operatorCode]();
            sleep(1);
        }
        cout << "进程 [" << getpid() << "] 结束工作" << endl;
    }
    
    // [子进程的pid,子进程的管道fd] -> 父进程负责派发任务
    void dispatchTask(const vector<elem> &processFds)
    {
        random_device rand;
        mt19937 gen(rand());
    
        while (true)
        {
            // 随机选择一个进程
            uniform_int_distribution<int32_t> dist(0, processFds.size() - 1);
            int32_t pick = dist(gen);
    
            // 选择一个任务
            uniform_int_distribution<int32_t> taskDist(0, functors.size() - 1);
            uint32_t task = taskDist(gen);
    
            // 将任务派发给一个指定的进程
            ssize_t s = write(processFds[pick].second, &task, sizeof(task));
    
            // 打印对应的提示信息
            cout << "父进程指派任务:" << info[task] << "到进程:" << processFds[pick].first << " 编号:" << pick << endl;
            (void)s;
            sleep(1);
        }
    }
    
    int main()
    {
        // 加载任务列表
        loadFunctor();
    
        vector<elem> assignMap;
        // 创建processNum个进程
        for (int i = 0; i < processNum; ++i)
        {
            // 定义保存管道fd的对象
            int pipefd[2] = {0};
            // 创建管道
            if (pipe(pipefd) != 0)
                errno_exit("pipe");
            // 创建子进程
            pid_t id = fork();
            if (id < 0)
                errno_exit("fork");
            else if (id == 0)
            {
                // 子进程读取
                close(pipefd[1]);
                // 子进程执行
                work(pipefd[0]);
                close(pipefd[0]);
                exit(0);
            }
    
            close(pipefd[0]);
            elem e(id, pipefd[1]);
            assignMap.push_back(e);
        }
        cout << "create all process success!" << endl;
    
        // 父进程派发任务
        dispatchTask(assignMap);
    
        // 回收资源
        for (int i = 0; i < processNum; ++i)
        {
            if (waitpid(assignMap[i].first, nullptr, 0) > 0)
                cout << "wait for: " << assignMap[i].first << " success!"
                     << "number:" << i << endl;
    
            close(assignMap[i].second);
        }
        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
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159

    运行结果如下:

    在这里插入图片描述

    管道的读写规则

    pipe2 与 pipe 函数的功能类似,与 pipe 函数不同的是,pipe2 函数提供了额外的选项参数。

    #include 
    #include 
    
    int pipe2(int pipefd[2],int flags);
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    pipe2 函数可以设置管道的阻塞或非阻塞模式,或者可以指定管道在进程终止时是否自动关闭。

    1️⃣ 当没有数据可读时:

    • O_NONBLOCK disable:read 调用阻塞,即进程暂停执行,一直等到有数据读为止。
    • O_NONBLOCK enable:read 调用返回 -1,erron 值为 EAGAIN。

    2️⃣ 当管道满的时候:

    • O_NONBLOCK disable:write 调用阻塞,直到有进程读走数据。
    • O_NONBLOCK enable:调用返回 -1,errno 值为 EAGAIN。

    3️⃣ 如果所有管道写端对应的文件描述符被关闭,read 调用会返回 0,表示管道已被关闭。

    4️⃣ 如果所有管道读端对应的文件描述符被关闭,write 调用会产生 SIGPIPE 信号,进而可能导致 write 进程退出。

    5️⃣ 当要写入的数据量不大于 PIPE_BUF 时,Linux 会保证写入操作的原子性,即写入操作要么完整地成功执行,要么完全不执行。

    6️⃣ 当要写入的数据量大于 PIPE_BUF 时,Linux 不再保证写入操作的原子性,即写入操作可能会被中断或分成多个部分执行。

    管道的特点

    管道是一种常用的 IPC 机制,它的特点如下:

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

    管道的生命周期随进程。 🎯
    当一个进程创建了一个管道后,该管道就存在了,并可以被多个相关进程使用。这些进程中,只要有一个进程仍在使用管道,管道就会一直存在,直到所有相关进程都关闭了管道的读端和写端,管道才会被系统回收和释放。需要注意的是,若管道的创建进程异常终止或被终止,管道也会被自动关闭和回收。

    管道是一种基于字节流的通信机制,进程无法控制消息的大小和边界。 🎯

    进程 A 写入管道的数据,进程 B 每次从管道中读取的数据多少是任意的 ,这种被称为流式服务,与之相对应的是数据报服务。

    • 流式服务:数据没有明确的分割,不分一定的报文段。
    • 数据报服务:数据有明确的分割,按报文段分割数据。

    注意:由于进程无法控制数据的大小和边界,使用管道进行通信时需要注意缓冲区的大小和数据的完整性。若管道的缓冲区大小不足以容纳将要发送的数据,写入进程会被阻塞,直到有足够的空间可用为止。若管道中的数据被拆分成多个字节流,进程可能需要自己处理字节流的拆分和组合,以确保数据的完整性。

    管道内部自带同步互斥机制。 🎯

    管道内部自带同步机制,是因为管道在内部实现了一个缓冲区,当进程向管道写入数据时,数据会被先存在管道的缓冲区中,直到缓冲区满或者达到一定的阈值才会被发送到管道的读端,这个过程中,若有多个进程同时向管道写入数据,管道会自动对写入的数据进行排队和同步操作,避免了进程间的竞争条件。

    管道内部的互斥机制,是因为管道本身只有一个读端和一个写端,进程需要先获得管道的读写权限,才能进行读写操作。若多个进程同时向管道写入数据,写入的数据顺序会被自动排队,避免了数据冲突和互相覆盖的情况。同样的,若多个进程同时从管道中读取数据,读取的顺序也会被自动排队,避免了数据的丢失和混淆。

    管道是一种半双工的通信机制,即同一时间只能有一个进程进行读操作,另一个进程进行写操作。 🎯

    在数据通信中,数据在线路上的传送方式分为以下三种:

    • 单工传输(Simplex Transmission):单工传输是指数据只能在一个方向上进行传输,而不能在另一个方向上传输。例如:广播电台的无线电传播就是单工传输。
    • 半双工传输(Half Duplex Transmission):半双工传输是指数据可以在两个方向上进行传输,但是不能同时进行。例如:对讲机通信就是半双工传输,同一时间一个人说,另一个人必须等待。
    • 全双工传输(Full Duplex Transmission):全双工传输是指数据可以在两个方向上同时进行传输。例如:电话通信和互联网传输就是全双工传输。

    管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。

    在这里插入图片描述

    管道的大小

    在 IPC 中,管道的大小一般是指管道缓冲区的大小,也可以称为管道的容量。管道缓冲区是用来暂存数据的区域,当管道写满时,继续写入数据会阻塞写入进程;当管道读空时,继续读取数据会阻塞读取进程。

    那么我们怎么才能直到管道的最大容量呢?

    方法一:man 手册

    使用命令 man 7 pipe ,可以查看 pipe 的容量信息,如下所示:在 2.6.11 之前的 Linux 中,管道的最大容量与系统页面大小相同;在 2.6.11 之后的 Linux 中,管道的最大容量为 65536 字节。

    在这里插入图片描述

    可以使用 uname -r 查看当前的 Linux 版本:

    在这里插入图片描述

    方法二:ulimit 命令

    可以使用 ulimit -a 来查看当前资源的限制设置。

    在这里插入图片描述

    管道缓冲区的大小由内核自动分配,一般为一页大小(4KB 或 8KB),也可以在创建管道时通过 fcntl() 函数来设置缓冲区大小。在实际使用中,可以通过减小缓冲区大小来降低管道的延迟,但也可能导致管道写满时频繁阻塞的问题。

    命名管道

    管道的应用限制是由于管道只存在于父子进程之间的共享内存中,而不是存在于文件系统中,因此只能在具有共同祖先的进程之间通信。若想要在没有亲缘关系的进程之间交换数据,可以使用 FIFO 文件,也被称为命名管道。

    命名管道是一种特殊类型的文件,它可以被多个进程同时打开,从而实现不相关进程之间的通信。与管道不同的是,命名管道存在于文件系统中,具有文件名,多个进程可以通过打开相同的文件名来访问同一个命名管道,从而实现数据的交换。因此,命名管道不仅可以用于不相关的进程之间的通信,还可以用于相对独立的进程之间的通信,例如:在不同的终端或远程系统之间交换数据。

    命令行上创建命令管道

    命名管道可以从命令行上创建,可以使用以下命令:

    $ mkfifo filename
    
    • 1

    其中,filename 是创建的命名管道的名称。这个命令用于创建一个新的 FIFO 文件,即命名管道。并将其添加到文件系统中。

    在这里插入图片描述

    从创建出的 fifo 文件可看出,其文件类型为 p ,表示该文件是一个管道文件。

    如下所示:在一个进程中向使用 shell 脚本向命名管道每隔 1 秒写入一个字符串,在另外一个进程中通过 fifo 就可以进程读取管道中的数据了。

    在这里插入图片描述

    当管道的读端进程退出之后,写端的进程再向管道中写入数据就没有意义了,因此读端退出,写端就会被操作系统杀掉。如下,我们可以进行验证:因为写端执行的循环脚本是由命令行解释器 bash 执行的,当终止读端进程时,bash 就会被操作系统杀掉。

    在这里插入图片描述

    使用函数创建命名管道

    在程序中可以通过 mkfifo 函数在文件系统中创建一个命名管道,它通过文件路径名来标识一种特殊类型的文件。

    函数如下:

    #include 
    #include 
    
    int mkfifo(const char *pathname, mode_t mode);
    
    • 1
    • 2
    • 3
    • 4

    说明:

    • mkfifo() 创建一个名为 pathname 的 FIFO 特殊文件。mode 指定 FIFO 的权限。它由进程的 umask 以通常的方式修改:创建的文件权限是(mode & ~umask)。
    • FIFO 特殊文件类似于管道,只是创建方式不同。FIFO 不是匿名通信管道,而是通过 mkfifo() 将 FIFO 特殊文件输入到文件系统中。
    • 一旦你以这种方式创建了一个 FIFO 特殊文件,任何进程都可以打开它进行读写,就像普通文件一样。但是,在对其进行任何输入输出操作之前,它必须同时在两端打开。打开文件 FIFO 为了读取通常会被阻塞,直到其它进程打开相同的 FIFO 文件进行写入。

    返回值:

    如果调用成功,mkfifo() 返回 0 。如果出现错误,将返回 -1,对应的错误码将被设置。

    示例:使用 mkfifo() 在代码所在的路径下,创建一个名为 myfifo 的命名管道。

    在这里插入图片描述

    // 使用mkfifo函数创建一个名为myfifo的命令管道
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    #define FILE_NAME "myfifo"
    
    int main()
    {
        // 判断是否已经存在同名的命名管道
        if (access(FILE_NAME, F_OK) != -1)
        {
            cout << "Named pipe " << FILE_NAME << " already exists!" << endl;
            return 0;
        }
    
        // 将文件默认的权限掩码设置为0,使得创建的文件权限与给出的权限对应
        umask(0);
        if (mkfifo(FILE_NAME, 0666) < 0)
        {
            perror("mkfifo");
            return 1;
        }
    
        cout << "Named pipe created successfully!" << endl;
    
        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

    运行程序,myfifo 命令管道创建成功:

    在这里插入图片描述

    命名管道的打开规则

    命名管道的打开规则会根据打开操作的类型不同而有不同的行为。

    如果当前打开操作是为读而打开 FIFO 时:

    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO。
    • O_NONBLOCK enable:立刻返回成功。

    如果当前打开操作是为写而打开 FIFO 时:

    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该 FIFO。
    • O_NONBLOCK enable:立刻返回失败,错误码为 ENXIO。

    使用命名管道实现文件拷贝

    以下两份代码实现了一个基于管道的数据传输功能。第一份代码 read.cc 打开了一个名为 “test” 的文件,将其读入到缓冲区,然后将数据写入一个名为 “tp” 的管道文件。第二份代码 write.cc 从 “tp” 管道文件读入数据,并将数据写入到 “test.bak” 文件中,最后删除 “tp” 管道文件。

    read.cc 代码如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void error_exit(const char *msg)
    {
        perror(msg);
        exit(EXIT_FAILURE);
    }
    
    #define NUM 1024
    
    int main()
    {
        // 对FIFO文件是否存在进行检测,如果存在则删除
        if (access("tp", F_OK) == 0)
        {
            if (unlink("tp") == -1)
                error_exit("unlink");
        }
    
        mkfifo("tp", 0664);
        int infd = open("test", O_RDONLY);
        if (infd == -1)
            error_exit("open");
    
        int outfd = open("tp", O_WRONLY);
        if (outfd == -1)
            error_exit("open");
    
        char buffer[NUM];
        int n;
        // 从test中读取数据到buffer中
        while ((n = read(infd, buffer, sizeof(buffer)-1)) > 0)
        {
            // 将读到的buffer中的数据写入管道tp中
            buffer[n]='\0';
            write(outfd, buffer, n);
        }
    
        close(infd);
        close(outfd);
        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

    write.cc 代码如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void errno_exit(const char *msg)
    {
        perror(msg);
        exit(EXIT_FAILURE);
    }
    
    #define NUM 1024
    
    int main()
    {
        // 检测拷贝的目标文件是否存在,存在则删除
        if (access("test.bak", F_OK) == 0)
        {
            if (unlink("test.bak") == -1)
                errno_exit("unlink");
        }
    
        int outfd = open("test.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (outfd == -1)
            errno_exit("open");
    
        int infd = open("tp", O_RDONLY);
        if (infd == -1)
            errno_exit("open");
    
        char buffer[NUM];
        int n, m;
        while ((n = read(infd, buffer, sizeof(buffer))) > 0)
        {
            m = write(outfd, buffer, n);
            if (m != n)
            {
                errno_exit("write");
            }
        }
        if (n == -1)
            errno_exit("read");
    
        close(infd);
        close(outfd);
    
        if (unlink("tp") == -1)
            errno_exit("unlink");
        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

    运行结果如下:

    在这里插入图片描述

    使用管道实现的文件拷贝有什么意义?

    在这里使用管道在本地进行文件拷贝,似乎没有什么意义。但是你可以将管道比作 ”网络“,将客户端比作 ”Windows Xshell“ ,将服务端看作 ”centos 服务器“。那么实现的就是文件的上传过程,反过来则是文件的下载功能。

    在这里插入图片描述

    使用命名管道实现server&client通信

    以下代码实现了基于命名管道的进程间通信,即一个进程作为服务端,另一个进程作为客户端,客户端可以向服务端发送消息,服务端可以接收客户端发送的消息,并将其打印到输出终端上。

    公共头文件 comm.h 代码:

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    #define IPC_PATH "./.fifo" // 命名管道路径
    #define NUM 1024           // 缓冲区大小
    
    // 错误处理函数,输出错误信息并退出程序
    void errno_exit(const char *msg)
    {
        perror(msg);
        exit(EXIT_FAILURE);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    server.cpp 代码如下:

    #include "comm.h"
    
    int main()
    {
        // 设置当前进程的文件模式创建屏蔽字,即将屏蔽掉文件权限位中的某些权限
        umask(0);
        // 创建命名管道
        if (mkfifo(IPC_PATH, 0666) != 0)
            errno_exit("mkfifo");
    
        // 打开管道,获取文件描述符
        int pipefd = open(IPC_PATH, O_RDONLY);
        if (pipefd < 0)
            errno_exit("open");
    
        // 正常通信过程
        char buffer[NUM];
        ssize_t s;
        while ((s = read(pipefd, buffer, sizeof(buffer) - 1)) > 0)
        {
            // 在读取的数据后加上字符串结束符
            buffer[s] = '\0';
            cout << "client -> server# " << buffer << endl;
        }
        if (s == 0)
            cout << "client exit!" << endl;
        else
            cout << "read:" << strerror(errno) << endl;
    
        // 关闭文件描述符
        close(pipefd);
        cout << "server exit!" << endl;
        // 删除命名管道
        unlink(IPC_PATH);
        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

    client.cpp 代码如下:

    #include "comm.h"
    
    int main()
    {
        int pipefd = open(IPC_PATH, O_WRONLY);
        if (pipefd < 0)
            errno_exit("open");
    
        char line[NUM] = {0};
        while (true)
        {
            printf("请输入你的消息# ");
            fflush(stdout);
            // fgets -> c -> line结尾自动添加\0
            if (fgets(line, sizeof(line), stdin) != nullptr)
            {
                line[strlen(line) - 1] = '\0'; //去掉fgets读入的换行符
                write(pipefd, line, strlen(line));
            }
            else
                break;
        }
        close(pipefd);
        cout << "client exit!" << endl;
        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

    代码编写完毕之后,先将 server.cpp 生成的可执行程序运行起来,接下来在客户端就可以看见服务端创建的命名管道:

    在这里插入图片描述

    接下来运行 client.cpp 生成的可执行程序,此时,我们在客户端输入信息,服务端就会将输入的信息打印到终端上:

    在这里插入图片描述

    命名管道与匿名管道的区别

    匿名管道和命名管道都是进程间通信的一种方式,它们的主要区别就在于创建和打开的方式不同。

    匿名管道通过 pipe 函数创建,只能在有亲缘关系的进程间使用,并且生命周期也是在进程间通信期间,通信结束后自动销毁。

    命名管道(FIFO)通过 mkfifo 函数创建,需要指定管道的名字,并且可以被不相关的进程所共享。命名管道在创建之后,可以被不同的进程打开,并通过读写管道中的数据进行通信。

    命名行中的管道 “|”

    在命令行中输入以下命令:

    $ ls -l | wc -l
    
    • 1

    该命令将 ls 命令的输出通过管道传递给 wc 命令。ls -l 命令的输出是当前目录下所有文件和文件夹的列表,wc -l 命令将其作为输入并计算行数。如果管道工作正常,则输出结果应该是当前目录下文件和文件夹的总数。

    在这里插入图片描述

    那么命令行中的管道 “|” 属于什么类型的管道呢?

    匿名管道只能用于具有亲缘关系的进程之间通信,而命名管道可以用于两个不相关的进程之间的通信。因此,我们可以查看命令行中用管道 “|” 连接起来的进程之间是否具有亲缘关系来判断 “|” 管道的类型。

    如下所示:使用管道 “|” 连接了三个进程,通过 ps 命令查看这三个进程发现,这三个进程的 PPID 都是相同的,即它们是由同一个父进程创建的子进程。

    在这里插入图片描述

    查看它们的父进程 bash

    在这里插入图片描述

    由此看出,管道 “|” 连接起来的各个进程之间是具有亲缘关系的。若两个进程之间使用的是命名管道,那么在磁盘上一定有一个对应的命名管道文件名,但实际上,我们在命令行使用管道 “|” 时不存在类似的命名管道文件名,因此命令行中的管道实际上是匿名管道。

    system V 共享内存

    共享内存是 Linux 和其他类 Unix 系统中可用三种进程间通信机制之一。对于共享内存,内核创建共享内存段,并将其映射到请求进程的地址空间的数据段。进程可以像使用其他地址空间的任何其它全局变量一样使用共享内存。

    共享内存区是最快的 IPC 形式,一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再指向进入内核的系统调用来传递彼此的数据。

    共享内存的基本原理

    共享内存其基本原理是让多个进程共享同一块物理内存,这样这些进程就可以在这块共享内存中读写数据,从而实现数据共享。为了实现共享内存,操作系统会在物理内存中分配一块连续的内存空间,并在各个进程的虚拟地址空间中开辟与之对应的地址空间。各个进程可以通过这些虚拟地址访问共享内存,操作系统负责将虚拟地址翻译成物理地址,从而实现共享内存的访问。

    在这里插入图片描述

    共享内存数据结构

    在操作系统中,可能存在大量的进程进行通信,因此也会有大量的共享内存。为了对这些共享内存进行管理,操作系统会为每个共享内存维护一个共享内存数据结构。在这个数据结构中,可以获取到该共享内存的各种元信息,并进行管理。

    共享内存的数据结构如下:

    struct shmid_ds {
    	struct ipc_perm shm_perm; /* operation perms */
    	int shm_segsz; /* size of segment (bytes) */
    	__kernel_time_t shm_atime; /* last attach time */
    	__kernel_time_t shm_dtime; /* last detach time */
    	__kernel_time_t shm_ctime; /* last change time */
    	__kernel_ipc_pid_t shm_cpid; /* pid of creator */
    	__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    	unsigned short shm_nattch; /* no. of current attaches */
    	unsigned short shm_unused; /* compatibility */
    	void *shm_unused2; /* ditto - used by DIPC */
    	void *shm_unused3; /* unused */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    共享内存是用于进程间通信的一种方式,它需要一个唯一的标识符来标识系统中的共享内存。这个标识符就是我们在创建共享内存时传递的 key 值,它的作用类似于文件系统中的文件名。不同的进程可以通过这个 key 值来访问同一块共享内存,实现进程间通信。

    在共享内存的数据结构中,shm_perm 成员变量储存了共享内存的权限信息,包括创建者的 UID 和 GID,访问权限等信息。同时,shm_perm 中包含了用于标识共享内存的 IPC 键值(即 key),其中 ipc_perm 结构体的定义如下:

    struct ipc_perm {
        key_t key;          /* Key supplied to shmget(2) */
        uid_t uid;          /* Effective UID of owner */
        gid_t gid;          /* Effective GID of owner */
        uid_t cuid;         /* Effective UID of creator */
        gid_t cgid;         /* Effective GID of creator */
        mode_t mode;        /* Permissions */
        unsigned short seq; /* Sequence number */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    System V IPC key

    💫 想要使用 System V IPC 机制,我们需要创建一个 System V IPC key。 我们使用 ftok 函数来进行创建。

    ftok() : 将路径名和项目标识符转换为 System V IPC key。

    函数定义:

    #include 
    #include 
    
    key_t ftok(const char *pathname, int proj_id);
    
    • 1
    • 2
    • 3
    • 4

    说明:

    fork() 函数使用给定的路径名命名的文件的标识(必须引用一个现有的、可访问的文件)和 proj_id 的最低有效 8 位(必须是非零的)来生成一个 key_t 类型的 System V IPC key。

    返回值:

    调用成功时,返回生成的 key 值。如果失败,返回 -1,errno 标识 stat(2) 系统调用的错误。

    示例:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PATH_NAME "/home/hyr/linux_code/linux19"
    #define PROJ_ID 0x16
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if (key < 0)
        {
            std::cerr << "ftok:" << strerror(errno) << std::endl;
            exit(-1);
        }
        return key;
    }
    
    int main()
    {
        std::cout << CreateKey() << std::endl;
        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

    运行结果:

    在这里插入图片描述

    共享内存函数

    系统提供了一些函数用于创建和操作共享内存,如:shmget、shmat、shmflg、shmctl 。下面进行介绍。

    共享内存创建 - shmget

    shmget - 分配一个 System V 共享内存。

    函数定义:

    #include 
    #include 
    
    int shmget(key_t key, size_t size, int shmflg);
    
    • 1
    • 2
    • 3
    • 4

    参数说明:

    • key:共享内存段的标识符,可以是任意整数值,通常使用 ftok 函数将一个路径名和项目 ID 转换为一个唯一个 key 值。
    • size:共享内存段的大小,以字节为单位。
    • shmflg:共享内存段的访问权限和其它选项的标志,由九个权限标志构成,包括:IPC_CREAT、IPC_EXCL、mode_flags 和 SHM_HUGETLB 等。

    说明:

    shmget() 函数返回一个标识符,该标识符与参数 key 的值相关联,表示 System V 共享内存段。如果 key 的值为 IPC_PRIVATE 或 key 不是IPC_PRIVATE,但是 shmflg 指定了 IPC_CREAT 并且没有与 key 相对应的共享内存段,则将创建一个新的共享内存段,其大小等于 size 的值向上舍入到 PAGE_SIZE 的倍数。

    如果 shmflg 同时指定了 IPC_CREAT 和 IPC_EXCL ,并且 key 已经存在一个共享内存段,则 shmget() 会失败,并将 errno 设置为 EEXIST。

    返回值: 调用成功,返回共享内存的标识符。调用失败,则返回 -1。

    变量 shmflg 常用的值如下

    • IPC_CREAT:用于创建一个新的共享内存段。如果不使用此标志,则 shmget() 将查找与关键字 key 相关联的段,并检查用户是否有访问该段的权限。

    • IPC_EXCL:与 IPC_CREAT 一起使用,以确保如果段已经存在,则操作失败。

    接下来,我们使用 ftok 函数和 shmget 函数来创建一块共享内存,然后打印出共享内存的相关值,以便于进行观察:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    #define PATH_NAME "/home/hyr/linux_code/linux19" // 路径名
    #define PROJ_ID 0x16                             // 整数标识符
    #define SIZE 4096                                // 共享内存大小
    
    // 创建一个全新的共享内存
    const int flag = IPC_CREAT | IPC_EXCL | 0666;
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if (key < 0)
        {
            std::cerr << "ftok:" << strerror(errno) << std::endl;
            exit(-1);
        }
        return key;
    }
    
    int main()
    {
        key_t key = CreateKey();
    
        int shmid = shmget(key, SIZE, flag);
        if (shmid < 0)
        {
            perror("shmget");
            return 1;
        }
    
        printf("key: %x\n", key);
        printf("shmid: %d\n", shmid);
        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

    运行结果如下所示:

    在这里插入图片描述

    ipcs 是一个 Unix/Linux 下的命名行工具,用于查询当前系统中的 IPC 资源的使用情况,以及列出系统上 IPC 对象的信息。

    在这里插入图片描述

    使用 ipcs 命令时,若只想查看特定类型的 IPC 相关信息,可以携带以下选项:

    • -q:列出消息队列的相关信息。
    • -m:列出共享内存相关信息。
    • -s:列出信号量相关信息。

    ipcs -m 命令输出的每列信息的含义如下:

    key共享内存对应的键值
    shmid共享内存标识符
    owner创建共享内存的用户的用户名
    perms共享内存的访问权限
    bytes共享内存的大小,以字节为单位
    nattch当前连接到该共享内存的进程数
    status共享内存状态,通常 0 表示共享内存已被删除,1 表示共享内存还存在

    共享内存关联 - shmat

    shmat() - 将共享内存段连接到进程地址空间

    函数定义:

    #include 
    #include 
    
    void *shmat(int shmid,const void *shmaddr, int shmflg);
    
    • 1
    • 2
    • 3
    • 4

    参数说明:

    • shmid:共享内存的标识符。
    • shmaddr:表示共享内存的地址,如果为 0 表示让操作系统自动选择一个地址。
    • shmflg:它的两个取值可能是 SHM_RND 和 SHM_RDONLY。

    说明:

    • shmaddr 为 NULL 且 shmflg 无 SHM_RND 标记,则连接的地址会自动向下调整为 SHMLBA 的整数倍。公式:shmaddr - (shmaddr % SHMLBA)。
    • shmaddr 不为 NULL且 shmflg 无 SHM_RND 标记,则以 shmaddr 为连接地址。
    • shmaddr 不为 NULL且 shmflg 设置了 SHM_RND 标记,则连接的地址会自动向下调整为 SHMLBA 的整数倍。公式:shmaddr - (shmaddr % SHMLBA)。
    • shmflg = SHM_RDONLY,表示连接操作用来只读共享内存

    返回值: 函数执行成功返回共享内存段的首地址,执行失败返回 -1 并设置 errno 变量。

    使用 shmat 函数对共享内存进行连接:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    #define PATH_NAME "/home/hyr/linux_code/linux19" // 路径名
    #define PROJ_ID 0x16                             // 整数标识符
    #define SIZE 4096                                // 共享内存大小
    
    // 创建一个全新的共享内存
    const int flag = IPC_CREAT | IPC_EXCL | 0666;
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if (key < 0)
        {
            std::cerr << "ftok:" << strerror(errno) << std::endl;
            exit(-1);
        }
        return key;
    }
    
    int main()
    {
        key_t key = CreateKey();
    
        // 创建共享内存
        int shmid = shmget(key, SIZE, flag);
        if (shmid < 0)
        {
            perror("shmget");
            return 1;
        }
    
        printf("key: %x\n", key);
        printf("shmid: %d\n", shmid);
    
        cout << "attach begin!" << endl;
        sleep(2);
    
        // 将共享内和自己的进程产生关联(attach)
        char *str = (char *)shmat(shmid, nullptr, 0);
        if (str == (char *)-1)
        {
            perror("shmat");
            return 1;
        }
    
    	sleep(2);
        cout << "attach end!" << endl;
    
        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

    运行程序,使用监控脚本观测发现共享内存挂接成功。

    在这里插入图片描述

    共享内存去关联 - shmdt

    shmdt() - 用于解除当前进程与共享内存之间的映射关系,将共享内存从当前进程的虚拟地址空间中分离出来。

    函数定义:

    #include 
    #include 
    
    int shmdt(const void *shmaddr);
    
    • 1
    • 2
    • 3
    • 4

    说明: 其中,参数 shmaddr 是共享内存区域的起始地址。调用 shmdt 函数后,进程就不再拥有该共享区域的访问权限,若后续需要再次访问该共享内存区域,需要通过 shmat 函数重新将其附加到进程的地址空间。

    返回值: shmdt 函数调用成功则返回 0 ,调用失败则返回 -1 ,并设置 errno。

    如下,我们调用 shmdt 函数来取消进程与共享内存之间的关联:

    在这里插入图片描述

    注意:shmdt 函数只是将共享内存区域与进程的地址空间分离,并没有释放该共享内存区域的物理空间。

    共享内存释放 - shmctl

    通过上面创建共享内存来看,当进程运行完毕之后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道的生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存依旧存在。

    因此,若进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启。因此 IPC 资源是由内核提供并维护的。

    这里我们介绍两种方法来释放共享内存:1、命令行释放 2、调用函数进行释放。

    命令行释放

    可以使用命令 ipcs -m shmid 来释放指定 shmid 的共享内存。

    # ipcs -m shmid
    
    • 1

    在这里插入图片描述

    shmctl 函数
    shmctl() - 用于控制共享内存。

    #include 
    #include 
    
    int shmctl(int shmid,int cmd, struct shmid_ds *buf);
    
    • 1
    • 2
    • 3
    • 4

    说明: shmctl() 函数使用 shmid 标识符标识的共享内存段执行 cmd 指定的控制操作。

    参数说明:

    • shmid:由 shmget 返回的共享内存标识符。
    • cmd:命令参数,指示 shmctl 将要进行的操作类型。
    • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。

    返回值: 执行成功时,返回 0;执行失败时,返回 -1,并设置 errno 错误码。

    函数第二个参数 cmd 的常用选项如下:

    命令说明
    IPC_STAT读取共享内存的状态信息,并将其存储在 shmid_ds 结构体中
    IPC_SET在进程权限足够的情况下,修改共享内存的状态信息,修改的内存包含在 shmid_ds 结构体中
    IPC_RMID删除共享内存段
    SHM_LOCK锁定共享内存
    SHM_UNLOCK解除共享内存锁定

    示例代码如下:在进程退出前,将创建的共享内存调用函数删除。

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    #define PATH_NAME "/home/hyr/linux_code/linux19" // 路径名
    #define PROJ_ID 0x16                             // 整数标识符
    #define SIZE 4096                                // 共享内存大小
    
    // 创建一个全新的共享内存
    const int flag = IPC_CREAT | IPC_EXCL | 0666;
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if (key < 0)
        {
            std::cerr << "ftok:" << strerror(errno) << std::endl;
            exit(-1);
        }
        return key;
    }
    
    int main()
    {
        key_t key = CreateKey();
    
        // 创建共享内存
        int shmid = shmget(key, SIZE, flag);
        if (shmid < 0)
        {
            perror("shmget");
            return 1;
        }
    
        printf("key: %x\n", key);
        printf("shmid: %d\n", shmid);
    
        cout << "attach begin!" << endl;
        sleep(1);
    
        // 将共享内和自己的进程产生关联(attach)
        char *str = (char *)shmat(shmid, nullptr, 0);
        if (str == (char *)-1)
        {
            perror("shmat");
            return 1;
        }
    
        sleep(1);
        cout << "attach end!" << endl;
    
        // 将共享内存与进程去关联
        cout << "detach begin!" << endl;
        sleep(1);
        shmdt(str);
        cout << "detach end!" << endl;
        sleep(1);
    
        // 释放共享内存
        sleep(1);
        shmctl(shmid,IPC_RMID,nullptr);
        sleep(1);
    
        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

    当程序运行时,我们可以通过监控脚本 while :; do ipcs -m;echo "-----------------------------"; sleep 1; done 来实时观测共享内存的资源分配情况:

    在这里插入图片描述

    使用共享内存实现 serve&client通信

    以下两端代码实现了一个简单的共享内存通信机制,server.cc 代码用于读取共享内存的内容。client.cc 代码用于向共享内存中写入数据。通过共享内存,这两个程序实现了进程间的数据共享和通信。

    两个程序的头文件和共享函数定义在 comm.h,如下:

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    #define PATH_NAME "/home/hyr/linux_code"
    #define PROJ_ID 0x14
    #define MEM_SIZE 4096
    #define FIFO_FIEL ".fifo"
    
    // 创建一个全新的共享内存
    const int flag = IPC_CREAT | IPC_EXCL | 0666;
    
    key_t CreateKey()
    {
        key_t key = ftok(PATH_NAME, PROJ_ID);
        if (key < 0)
        {
            cerr << "ftok : " << strerror(errno) << endl;
            exit(-1);
        }
        return key;
    }
    
    • 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

    server.cc 代码如下:

    #include "comm.hpp"
    using namespace std;
    
    int main()
    {
        // 创建共享内存
        key_t key = CreateKey();
        int shmid = shmget(key, MEM_SIZE, flag);
        if (shmid == -1)
        {
            perror("shmget");
            return 1;
        }
    
        // 使共享内存与进程进行关联
        char *str = static_cast<char *>(shmat(shmid, nullptr, 0));
        if (str == reinterpret_cast<char *>(-1))
        {
            perror("shmat");
            return 1;
        }
    
        // 使用共享内存
        while (true)
        {
            printf("%s\n", str);
            sleep(1);
        }
    
        // 解除共享内存关联
        if (shmdt(str) == -1)
        {
            perror("shmdt");
            return 1;
        }
    
        // 删除共享内存
        if (shmctl(shmid, IPC_RMID, nullptr) == -1)
        {
            perror("shmctl");
            return 1;
        }
        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

    client.cc 代码如下:

    #include "comm.hpp"
    
    // 使用共享内存
    int main()
    {
        // 创建相同的key值
        key_t key = CreateKey();
    
        // 获取共享内存
        int shmid = shmget(key, MEM_SIZE, IPC_CREAT);
        if (shmid <= 0)
        {
            perror("shmid");
            return 1;
        }
    
        // 关联共享内存
        char *str = static_cast<char *>(shmat(shmid, nullptr, 0));
        if (str == reinterpret_cast<char *>(-1))
        {
            perror("shmat");
            return 1;
        }
    
        // 使用共享内存
        while (true)
        {
            printf("Please Enter# ");
            fflush(stdout);
            ssize_t s = read(0, str, MEM_SIZE);
            if (s > 0)
                str[s] = '\0';
        }
    
        // 解除共享内存关联
        if (shmdt(str) == -1)
        {
            perror("shmdt");
            return 1;
        }
        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

    先后运行服务端和客户端,通过监控脚本 while :; do ipcs -m;echo "-----------------------------"; sleep 1; done 来验证它们是否关联到同一块内存,并且该共享内存当前关联的进程数为 2 ,则表名服务端和客户端成功地建立了与共享内存的连接。

    在这里插入图片描述

    管道与共享内存的区别

    共享内存和管道是两种不同的进行间通信机制,它们的区别和特点如下:

    数据交互方式:

    • 共享内存:进程可以直接读写共享内存区域,数据交互更加灵活高效。
    • 管道:数据通过管道的读端和写端进行顺序读写,是一种基于字节流的通信方式。

    进程关系:

    • 共享内存:适用于具有父子关系或共享同一块内存的进行之间的通信,可以实现高效的数据共享。
    • 管道:通常用于父子进程之间的单向通信。

    通信方式:

    • 共享内存:进程通过读写共享内存中的数据进行通信,可实现实时的数据共享和同步。
    • 管道:进程通过管道进行数据的顺序传递,读写方会相互影响。

    适用场景:

    • 共享内存:适用频繁且大量的数据交换,特别是对实时性要求较高的场景。
    • 管道:适用于父子进程之间的简单通信,数据量较小且不需要实时交换的场景。

    因此,共享内存适用于需要高效数据共享和实时交互的场景,管道适用于简单的单向通信需求。

  • 相关阅读:
    怎么写一个可以鼠标控制旋转的div?
    Java通过反射注解赋值
    使用ExLlamaV2量化并运行EXL2模型
    大型项目开发设计文档规范
    Rabbitmq安装-docker版
    MVP-2:使用MVP分页加载数据
    pytest进阶之conftest.py
    非零基础自学Java (老师:韩顺平) 第7章 面向对象编程(基础部分) 7.8 构造方法构造器
    一文揭开,测试外包公司的真 相
    初次接触Sharding-JDBC并实际应用
  • 原文地址:https://blog.csdn.net/qq_61939403/article/details/130384349