国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Linux進(jìn)程間通信【匿名管道】

這篇具有很好參考價(jià)值的文章主要介紹了Linux進(jìn)程間通信【匿名管道】。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

?個(gè)人主頁(yè): 北 海
??所屬專欄: Linux學(xué)習(xí)之旅
??操作環(huán)境: CentOS 7.6 阿里云遠(yuǎn)程服務(wù)器

Linux進(jìn)程間通信【匿名管道】



??前言

進(jìn)程間通信簡(jiǎn)稱為 IPC(Interprocess communication),是兩個(gè)不同進(jìn)程間進(jìn)行任務(wù)協(xié)同的必要基礎(chǔ)。進(jìn)行通信時(shí),首先需要確保不同進(jìn)程之間構(gòu)建聯(lián)系,其次再根據(jù)不同的使用場(chǎng)景選擇不同的通信解決方案,本文主要介紹的通信解決方案為 匿名管道

Linux進(jìn)程間通信【匿名管道】


???正文

1、進(jìn)程間通信相關(guān)概念

在正式學(xué)習(xí) 匿名管道 之前,需要簡(jiǎn)單了解一下通信的相關(guān)概念

1.1、目的

進(jìn)程間通信主要有以下四個(gè)目的:

  1. 數(shù)據(jù)傳輸不同進(jìn)程間進(jìn)行數(shù)據(jù)傳輸,比如此時(shí)我寫的博客數(shù)據(jù)正在源源不斷的上傳至 CSDN 服務(wù)器中
  2. 資源共享多個(gè)進(jìn)程之間需要共享資源,假設(shè)每個(gè)用戶都是獨(dú)立的進(jìn)程,那么整個(gè) C 站就是一個(gè)被共享的資源,用戶之前可以共享其技術(shù)資源
  3. 事件通知一個(gè)進(jìn)程向其他進(jìn)程發(fā)送消息,通知處理相關(guān)事宜,比如 子進(jìn)程終止時(shí),需要通知父進(jìn)程,回收其資源
  4. 進(jìn)程控制有些進(jìn)程需要起到 管理者 的作用,于是需要與被管理進(jìn)程之間構(gòu)建通信關(guān)系,進(jìn)程任務(wù)下達(dá)及進(jìn)程控制,并對(duì)進(jìn)程狀態(tài)進(jìn)行實(shí)時(shí)監(jiān)視

其實(shí)進(jìn)程間通信的最終目的就是 打破各個(gè)獨(dú)立進(jìn)程之前的壁壘,進(jìn)行任務(wù)協(xié)同

就好比 大航海時(shí)期 的冒險(xiǎn)家們,克服困難、勇于開拓,打破了不同大陸間的隔離狀態(tài),最終將世界連為一個(gè) “整體”,人類文明迎來了高速發(fā)展時(shí)期

  • 讓世界連成一整體,開始了全球化發(fā)展
  • 有利于講文明傳播到世界各地,促進(jìn)了世界各地的交流
  • 幫助不發(fā)達(dá)的地方引進(jìn)新的文明成果

Linux進(jìn)程間通信【匿名管道】

進(jìn)程間具有獨(dú)立性,這是原則
讓進(jìn)程間可以更好的協(xié)同工作,這是目的

因此進(jìn)程間通信的本質(zhì)就是 讓不同的進(jìn)程看到同一份 “資源”

  • 其中的 資源OS 直接或間接提供

無論后續(xù)的哪種進(jìn)程間通信的解決方案,都要解決以下兩個(gè)問題:

  1. 想辦法讓不同的進(jìn)程看到同一份資源
  2. 讓其中一方寫入,另一方讀取,完成通信;至于通信的目的及后續(xù)工作,需要結(jié)合具體場(chǎng)景分析

1.2、發(fā)展

進(jìn)程間通信的發(fā)展可以簡(jiǎn)單概況為以下三個(gè)時(shí)期:

  • 管道時(shí)期(古老的通信方式)
  • System V 標(biāo)準(zhǔn)時(shí)期(本地化進(jìn)程間通信)
  • POSIX 標(biāo)準(zhǔn)時(shí)期(網(wǎng)絡(luò)中進(jìn)程間通信)

管道可以說是十分古老且簡(jiǎn)單了,后來新出的 System V 標(biāo)準(zhǔn)豐富了進(jìn)程間通信的方式,但奈何無法滿足網(wǎng)絡(luò)中的進(jìn)程間通信需求,于是誕生了更好的 POSIX 標(biāo)準(zhǔn)

管道適合深入學(xué)習(xí),探究進(jìn)程間通信時(shí)的原理及執(zhí)行流程

System V 標(biāo)準(zhǔn)如今比較少用了,但其通信速度極快的共享內(nèi)存還是值得深入學(xué)習(xí)的

POSIXUnix 系統(tǒng)的一個(gè)設(shè)計(jì)標(biāo)準(zhǔn),很多類 Unix 系統(tǒng)也在支持兼容這個(gè)標(biāo)準(zhǔn),如 Linux , POSIX 標(biāo)準(zhǔn)具有跨平臺(tái)性,就連 Windows 也對(duì)其進(jìn)行了支持,后續(xù)學(xué)習(xí) 同步與互斥 時(shí),所使用的信號(hào)量等都是出自 POSIX 標(biāo)準(zhǔn),這是進(jìn)程間通信的學(xué)習(xí)重點(diǎn)

POSIX 標(biāo)準(zhǔn)支持網(wǎng)絡(luò)中通信,比如 套接字(socket 就在此標(biāo)準(zhǔn)中

1.3、分類

根據(jù)不同發(fā)展時(shí)期的標(biāo)準(zhǔn),可以將進(jìn)程間通信的解決方案劃分為以下幾種:

管道:

  • 匿名管道
  • 命名管道

System V 標(biāo)準(zhǔn):

  • System V 消息隊(duì)列
  • System V 共享內(nèi)存
  • System V 信號(hào)量

POSIX 標(biāo)準(zhǔn):

  • POSIX 消息隊(duì)列
  • POSIX 共享內(nèi)存
  • POSIX 信號(hào)量
  • POSIX 互斥量(互斥鎖)
  • POSIX 條件變量
  • POSIX 讀寫鎖

顯然,隨著時(shí)代進(jìn)步,技術(shù)也在不斷迭代發(fā)展,新標(biāo)準(zhǔn)在替代舊標(biāo)準(zhǔn)時(shí),也必然新增時(shí)代特色需求,比如 上世紀(jì)末的網(wǎng)絡(luò);如今人工智能的迅猛發(fā)展也給傳統(tǒng)從業(yè)者敲響了警鐘,只有在自我發(fā)展中尋求變革,才能 hold 住新時(shí)代發(fā)展的浪潮


2、什么是管道?

管道是 Unix 系統(tǒng) IPC (進(jìn)程間通信)中 最古老 的方式,其歷史最早可追溯至 1964年10月11日

Linux進(jìn)程間通信【匿名管道】
出自 《UNIX PIPES 管道原稿》 — 陳皓

在命令行中輸入 | 即可使用管道

創(chuàng)建兩個(gè)睡眠時(shí)間較長(zhǎng)的 后臺(tái)進(jìn)程

sleep 10000 | sleep 20000 &

注:& 表示令當(dāng)前進(jìn)程變?yōu)楹笈_(tái)進(jìn)程

Linux進(jìn)程間通信【匿名管道】

可以看出,兩個(gè) sleep 進(jìn)程的 PPID 一致,同時(shí) PID 連續(xù),因此這兩個(gè)進(jìn)程是兄弟關(guān)系

管道分為 匿名管道命名管道,兩者絕大部分原理、特點(diǎn)都一致,本文主要介紹 匿名管道,同時(shí)適用于 命名管道 的知識(shí)點(diǎn)統(tǒng)一稱為 管道

Linux 中一切皆文件,所以管道本質(zhì)上就是一個(gè)文件


3、管道的工作原理

管道的工作原理其實(shí)很簡(jiǎn)單:打開一個(gè)文件,讓兩個(gè)進(jìn)程分別享有讀端與寫端 fd,對(duì)文件進(jìn)行操作即可

Linux進(jìn)程間通信【匿名管道】

命名管道和匿名管道基本原理都差不多,但命名管道更強(qiáng)大,能實(shí)現(xiàn)兩個(gè)毫不相干的進(jìn)程間通信

具體在 OS 中的體現(xiàn):在文件的結(jié)構(gòu)體 files_struct 中,存在一個(gè)特殊的成員 struct file *fd_array[],這是一個(gè)指針數(shù)組,其中存儲(chǔ)的是指向不同文件的指針

//Linux內(nèi)核源碼(部分)
struct files_struct {
  /*
   * read mostly part
   */
	atomic_t count;
	struct fdtable __rcu *fdt;
	struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
	spinlock_t file_lock ____cacheline_aligned_in_smp;
	int next_fd;
	unsigned long close_on_exec_init[1];
	unsigned long open_fds_init[1];
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];	//文件指針數(shù)組
};

此時(shí)父進(jìn)程可以打開匿名管道文件,fork 子進(jìn)程后,子進(jìn)程繼承原有的 文件系統(tǒng) 關(guān)系,與父進(jìn)程共享同一份文件資源,然后父子進(jìn)程分別關(guān)閉 讀端與寫端,實(shí)現(xiàn)匿名管道的單向關(guān)系,即可正常進(jìn)行通信

具體流程:

  1. 父進(jìn)程創(chuàng)建匿名管道,同時(shí)以讀、寫的方式打開匿名管道,此時(shí)會(huì)分配兩個(gè) fd
  2. fork 創(chuàng)建子進(jìn)程,子進(jìn)程擁有自己的進(jìn)程系統(tǒng)信息,同時(shí)會(huì)繼承原父進(jìn)程中的文件系統(tǒng)信息,此時(shí)子進(jìn)程和父進(jìn)程可以看到同一份資源:匿名管道 pipe
  3. 因?yàn)樽舆M(jìn)程繼承了原有關(guān)系,因此此時(shí)父子進(jìn)程對(duì)于 pipe 都有讀寫權(quán)限,需要確定數(shù)據(jù)流向,關(guān)閉不必要的 fd,比如父進(jìn)程寫、子進(jìn)程讀,或者父進(jìn)程讀、子進(jìn)程寫都可以

Linux進(jìn)程間通信【匿名管道】

注意:

  • fork 創(chuàng)建子進(jìn)程后,子進(jìn)程會(huì)繼承原父進(jìn)程中的文件系統(tǒng)信息,這也就是父子進(jìn)程都會(huì)同時(shí)向屏幕打印信息的原理,因?yàn)榇藭r(shí)它們操作的是同一個(gè)文件!
  • 父進(jìn)程需要以讀寫的方式打開匿名管道 pipe,這樣子進(jìn)程在繼承時(shí),才不會(huì)發(fā)生權(quán)限丟失
  • 創(chuàng)建出的匿名管道文件 pipe 雖然屬于文件系統(tǒng),但它是一個(gè)特殊文件,一個(gè)由 OS 提供的純純的內(nèi)存文件,不需要將數(shù)據(jù)沖刷至磁盤中,只需要承擔(dān)進(jìn)程間通信任務(wù)即可
  • 管道是一種半雙工、單流向的通信方式,因?yàn)?pipe 只有一個(gè)緩沖區(qū),所以這種方式才被叫做 管道通信

4、匿名管道的創(chuàng)建與使用

4.1、pipe 函數(shù)

匿名管道是通過 pipe 函數(shù)創(chuàng)建的,其函數(shù)原型如下所示

Linux進(jìn)程間通信【匿名管道】

#include <unistd.h>

int pipe(int pipefd[2]);

關(guān)于 pipefd 數(shù)組

數(shù)組元素 含義
pipefd[0] 表示 匿名管道的 讀端
pipefd[1] 表示 匿名管道的 寫端

巧記:

  • pipefd[0] -> 0 -> 嘴巴 -> 讀書 -> 讀端
  • pipefd[1] -> 1 -> 鋼筆 -> 寫字 -> 寫端

關(guān)于返回值:創(chuàng)建匿名管道成功,返回 0,失敗返回 -1,并設(shè)置錯(cuò)誤碼

實(shí)際在使用此函數(shù)時(shí),需要先創(chuàng)建好大小為 2pipefd 數(shù)組,然后將其傳入函數(shù),成功創(chuàng)建匿名管道后,pipefd 數(shù)組中存儲(chǔ)的就是 匿名管道的讀端和寫端 fd

4.2、實(shí)例代碼演示

下面通過一個(gè)簡(jiǎn)單的程序,演示 匿名管道函數(shù) pipe 的使用

使用匿名管道步驟

  • 創(chuàng)建匿名管道
  • 創(chuàng)建子進(jìn)程
  • 關(guān)閉不需要的 fd
  • 開始通信
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    // 1、創(chuàng)建匿名管道
    int pipefd[2]; // 數(shù)組
    int ret = pipe(pipefd);
    assert(ret == 0);
    (void)ret; // 防止 release 模式中報(bào)警告

    // 2、創(chuàng)建子進(jìn)程
    pid_t id = fork();
    if (id == 0)
    {
        // 子進(jìn)程內(nèi)
        close(pipefd[1]); // 3、子進(jìn)程關(guān)閉寫端

        // 4、開始通信
        char buff[64]; // 緩沖區(qū)
        while (true)
        {
            int n = read(pipefd[0], buff, sizeof(buff) - 1);    //注意預(yù)留一個(gè)位置存儲(chǔ) '\0'
            buff[n] = '\0';

            if (n >= 5 && n < 64)
            {
                // 讀取到了信息
                cout << "子進(jìn)程成功讀取到信息:" << buff << endl;
            }
            else
            {
                // 未讀取到信息
                if (n == 0)
                    cout << "子進(jìn)程沒有讀取到信息,通信結(jié)束!" << endl;
                // 讀取異常(消息過短)
                else
                    cout << "子進(jìn)程讀取數(shù)據(jù)量為:" << n << " 消息過短,通信結(jié)束!" << endl;
                break;
            }
        }

        close(pipefd[0]); // 關(guān)閉剩下的讀端
        exit(0);          // 子進(jìn)程退出
    }

    // 父進(jìn)程內(nèi)
    close(pipefd[0]); // 3、父進(jìn)程關(guān)閉讀端

    char buff[64];

    // 4、開始通信
    srand((size_t)time(NULL)); // 隨機(jī)數(shù)種子
    while (true)
    {
        int n = rand() % 26;
        for (int i = 0; i < n; i++)
            buff[i] = (rand() % 26) + 'A'; // 形成隨機(jī)消息
        buff[n] = '\0';                    // 結(jié)束標(biāo)志

        cout << "=============================" << endl;
        cout << "父進(jìn)程想對(duì)子進(jìn)程說: " << buff << endl;
        write(pipefd[1], buff, strlen(buff)); // 寫入數(shù)據(jù)

        if (n < 5)
            break; // 消息過短時(shí),不寫入

        sleep(1);
    }

    close(pipefd[1]); // 關(guān)閉剩下的寫端

    // 父進(jìn)程等待子進(jìn)程結(jié)束
    int status = 0;
    waitpid(id, &status, 0);

    // 通過 status 判斷子進(jìn)程運(yùn)行情況
    if ((status & 0x7F))
    {
        printf("子進(jìn)程異常退出,core dump: %d   退出信號(hào):%d\n", (status >> 7) & 1, (status & 0x7F));
    }
    else
    {
        printf("子進(jìn)程正常退出,退出碼:%d\n", (status >> 8) & 0xFF);
    }

    return 0;
}

Linux進(jìn)程間通信【匿名管道】

站在 文件描述符 的角度理解上述代碼:

Linux進(jìn)程間通信【匿名管道】

站在 內(nèi)核(管道本質(zhì)) 的角度理解上述代碼:

Linux進(jìn)程間通信【匿名管道】

所以,看待 管道 ,就如同看待 文件 一樣!管道 的使用和 文件 一致,迎合 Linux一切皆文件思想

4.3、管道讀寫規(guī)則

管道是一種 半雙工、單向流 的通信方式,因此在成功創(chuàng)建匿名管道后,需要兩個(gè)待通信的進(jìn)程都能獲得同一個(gè) pipefd 數(shù)組

這就是匿名管道比較特殊的地方了:匿名管道只支持具有血緣關(guān)系的進(jìn)程通信,如 父子進(jìn)程、兄弟進(jìn)程等,因?yàn)橹挥?繼承 了,才能共享到 同一個(gè) pipefd 數(shù)組

當(dāng)通信雙方都獲得 pipefd 數(shù)組后,需要根據(jù)情況關(guān)閉不需要的 fd,確保 單流向 的原則

注:命名管道可以支持不具有血緣關(guān)系進(jìn)程間通信

關(guān)于匿名管道還有一個(gè)函數(shù):pipe2 (了解),比 pipe 函數(shù)多一個(gè)參數(shù)2 flags,可以使匿名管道在發(fā)生特殊情況時(shí),作出不同的動(dòng)作,當(dāng) flags0 時(shí),pipe2 等價(jià)于 pipe

管道的讀寫規(guī)則:

PIPE_BUF 為管道大小,Linux 中為 4096 字節(jié)

  • 當(dāng)要寫入的數(shù)據(jù)量不大于 PIPE_BUF 時(shí),Linux 將保證寫入的原子性
  • 當(dāng)要寫入的數(shù)據(jù)量大于 PIPE_BUF 時(shí),Linux 將不再保證寫入的原子性

原子性:不存在中間狀態(tài),確保數(shù)據(jù)的安全性


5、管道的特點(diǎn)

管道 主要有以下幾個(gè)特點(diǎn):

1.單向通信,管道是半雙工的一種特殊情況

  • 管道就像單行道,只允許數(shù)據(jù)單向流通,即通知,如果想要實(shí)現(xiàn)兩個(gè)進(jìn)程間相互進(jìn)行通信,需要?jiǎng)?chuàng)建兩條管道,管道1:父進(jìn)程寫,子進(jìn)程讀;管道2:子進(jìn)程寫,父進(jìn)程讀

Linux進(jìn)程間通信【匿名管道】

2.管道的本質(zhì)是文件,因?yàn)?fd 的生命周期隨進(jìn)程而終止,所以管道的生命周期也是隨著進(jìn)程而結(jié)束的

  • 當(dāng)進(jìn)程終止運(yùn)行時(shí),管道資源會(huì)被 OS 回收

3.匿名管道常用來進(jìn)行具有 “血緣” 關(guān)系的進(jìn)程,進(jìn)行進(jìn)程間通信(常用于父子進(jìn)程間通信)

  • pipe 打開管道,并不清楚管道的名字等信息,這種管道稱為 匿名管道,因此 匿名管道 只能用于有血緣關(guān)系的進(jìn)程 IPC,因?yàn)?需要通過 fork 繼承匿名管道信息

4.在管道中,寫入讀取 的次數(shù)并不是嚴(yán)格匹配的,此時(shí)讀寫次數(shù)沒有強(qiáng)相關(guān)關(guān)系,管道是面向字節(jié)流讀寫的

  • 面向字節(jié)流讀寫又稱為 流式服務(wù):數(shù)據(jù)沒有明確的分割,不分一定的報(bào)文段;與之相對(duì)應(yīng)的是 數(shù)據(jù)報(bào)服務(wù):數(shù)據(jù)有明確的分割,拿數(shù)據(jù)按報(bào)文段拿
  • 不論寫端寫入了多少數(shù)據(jù),只要寫端停止寫入,讀端都可以將數(shù)據(jù)讀取

5.具有一定的協(xié)同能力,讓 讀端寫端 能夠按照一定的步驟進(jìn)行通信(自帶同步機(jī)制)

  • 當(dāng)讀端進(jìn)行從管道中讀取數(shù)據(jù)時(shí),如果沒有數(shù)據(jù),則會(huì)阻塞,等待寫端寫入數(shù)據(jù);如果讀端正在讀取,那么寫端將會(huì)阻塞等待讀端,因此 管道自帶 同步與互斥 機(jī)制

可以簡(jiǎn)單總結(jié)為:

  1. 管道是半雙工通信
  2. 管道生命隨進(jìn)程而終止
  3. 匿名管道只支持具有血緣關(guān)系的進(jìn)程間通信,而命名管道無所謂
  4. 管道提供的是流式數(shù)據(jù)傳輸服務(wù)
  5. 管道自帶 同步與互斥 機(jī)制

6、管道的四種特殊場(chǎng)景

管道還存在四種特殊場(chǎng)景:管道為空、管道為滿、寫端關(guān)閉、讀端關(guān)閉,四種場(chǎng)景對(duì)應(yīng)四種不同的特殊情況,都可以通過代碼進(jìn)行演示

注意:當(dāng)前大部分場(chǎng)景中,子進(jìn)程為讀端,父進(jìn)程為寫端

6.1、場(chǎng)景一

父進(jìn)程不寫,此時(shí)管道為空,子進(jìn)程嘗試讀取

偽代碼段
// 父進(jìn)程不寫(空),子進(jìn)程讀

//子進(jìn)程(嘗試讀?。?/span>
int cnt = 1;
while (true)
{
    char ch;
    read(pipefd[0], &ch, 1);
    cout << "已讀取 " << cnt++ << " 字節(jié)的數(shù)據(jù)" << endl;
}

//父進(jìn)程(不寫)
while (true) {}

Linux進(jìn)程間通信【匿名管道】
結(jié)果:因?yàn)楣艿罏榭?,因此子進(jìn)程無法讀取,即 讀端阻塞

只有當(dāng)寫端寫入數(shù)據(jù)后,讀端才能正常讀取

6.2、場(chǎng)景二

父進(jìn)程不斷寫入,直到管道寫滿,子進(jìn)程不讀取

偽代碼段
//父進(jìn)程寫(滿),子進(jìn)程不讀

//子進(jìn)程(不讀)
while (true) {}

//父進(jìn)程(不斷寫入)
int cnt = 1; // 計(jì)數(shù)器
while (true)
{
    char ch = 'x';
    write(pipefd[1], &ch, 1); // 寫入數(shù)據(jù)
    cout << "已寫入 " << cnt++ << " 字節(jié)的數(shù)據(jù)" << endl;
}

Linux進(jìn)程間通信【匿名管道】
結(jié)果:在一段時(shí)間后,管道被寫滿,寫端無法寫入數(shù)據(jù),進(jìn)入阻塞狀態(tài)

只有當(dāng)讀端嘗試將管道中的數(shù)據(jù)讀走一部分后,寫端才能繼續(xù)寫入

形象化理解
管道為空:垃圾桶為空時(shí),你不會(huì)去倒垃圾(讀端阻塞),因?yàn)闆]有垃圾,需要等有垃圾了(寫入數(shù)據(jù))才去倒
管道為滿:垃圾桶中的垃圾裝滿時(shí),無法再繼續(xù)扔垃圾(寫端阻塞),需要等把垃圾倒了(讀取數(shù)據(jù)),才能繼續(xù)扔垃圾

Linux進(jìn)程間通信【匿名管道】

6.3、場(chǎng)景三

在通信的過程中,關(guān)閉寫端,只保留讀端

偽代碼段
//寫端寫入一段信息后,就關(guān)閉

//子進(jìn)程正常讀取,并且對(duì)讀取到的數(shù)據(jù)量進(jìn)行判斷
char buff[64];
while(true)
{
    int n = read(pipefd[0], buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if(n == 0)
        cout << "寫端已關(guān)閉,讀取數(shù)據(jù)量為: " << n << " 字節(jié)" << endl;
    else
        cout << "成功讀取到信息: " << buff << endl;

    sleep(1);
}

//父進(jìn)程只寫入一次數(shù)據(jù),然后關(guān)閉寫端
char buff[] = "Hello pipe!";
write(pipefd[1], buff, strlen(buff)); // 寫入數(shù)據(jù)

close(pipefd[1]); // 關(guān)閉剩下的寫端

Linux進(jìn)程間通信【匿名管道】

結(jié)果:關(guān)閉寫端后,讀端會(huì)將匿名管道中的數(shù)據(jù)讀取完后,再讀,會(huì)讀到 0,表示已讀到文件末尾

如何理解?

  • 因?yàn)楣艿朗菃瘟飨蛲ㄐ?,寫端都關(guān)閉了,證明不會(huì)再有數(shù)據(jù)寫入,因此當(dāng)讀端把剩余數(shù)據(jù)都讀取后,每次都是讀取 0 字節(jié)數(shù)據(jù),表明此時(shí)已經(jīng)讀到了結(jié)尾,讀端也可以結(jié)束讀取了

6.4、場(chǎng)景四

在通信過程中,關(guān)閉讀端,只保留寫端

注:這里將角色變換一下,方便父進(jìn)程捕捉到子進(jìn)程的退出信號(hào)

切換:父進(jìn)程 -> 讀端,子進(jìn)程 -> 寫端

偽代碼段
//讀端讀取一段時(shí)間后,就關(guān)閉

//子進(jìn)程不斷寫入
while (true)
{
    char buff[] = "Hello pipe!";
    write(pipefd[1], buff, strlen(buff)); // 寫入數(shù)據(jù)
    sleep(1);
}

//父進(jìn)程在讀取五次信息后,就終止讀取,關(guān)閉讀端
char buff[64];
int cnt = 1;
while (true)
{
    int n = read(pipefd[0], buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if (n == 0)
        cout << "寫端已關(guān)閉,讀取數(shù)據(jù)量為: " << n << " 字節(jié)" << endl;
    else
        cout << "成功讀取到信息: " << buff << endl;

    // 讀取五次后,關(guān)閉讀端
    if (cnt++ == 5)
        break;
}

close(pipefd[0]);

// 父進(jìn)程等待子進(jìn)程結(jié)束
int status = 0;
waitpid(id, &status, 0);
printf("子進(jìn)程異常退出,core dump: %d   退出信號(hào):%d\n", (status >> 7) & 1, (status & 0x7F));

Linux進(jìn)程間通信【匿名管道】

結(jié)果:OS 不允許任何浪費(fèi)資源的行為存在,如果關(guān)閉了讀端,那么證明寫端寫了也沒有,即沒有存在的意義,于是 OS 會(huì)發(fā)出 13 號(hào)信號(hào),終止寫端進(jìn)程

通過指令查看信號(hào)表

kill -l

Linux進(jìn)程間通信【匿名管道】

以上就是管道的四種特殊場(chǎng)景,不僅適用于匿名管道,同時(shí)也適用于命名管道


7、匿名管道的大小

既然管道能被寫滿,那么管道的大小究竟是多少?

一、通過 man 手冊(cè)查詢相關(guān)信息

man 7 pipe

接著輸入 /pipe capacity 即可搜索出管道的大小

Linux進(jìn)程間通信【匿名管道】

文檔解釋:Linux 2.6.11 之前,管道大小為一個(gè)系統(tǒng)頁(yè)的大?。ū热缭?i386 平臺(tái)中,管道大小為 4096 字節(jié),即 4kb),從 Linux 2.6.11 開始,管道大小的容量統(tǒng)一為 65536 字節(jié),即 64kb

因?yàn)樵?Linux 2.6.11 版本中,對(duì)管道進(jìn)行更新,采取了新的解決方案

Linux進(jìn)程間通信【匿名管道】
原文鏈接:Circular pipes

可以通過指令查看當(dāng)前系統(tǒng)的內(nèi)核版本號(hào)

uname -a

Linux進(jìn)程間通信【匿名管道】

二、通過指令查看當(dāng)前系統(tǒng)資源的限制情況

ulimit -a

Linux進(jìn)程間通信【匿名管道】
當(dāng)前系統(tǒng)中,限制單條管道大小為 512 * 8 = 4096 字節(jié)

可以前往 /usr/src/kernels/內(nèi)核版本信息/include/linux/pipe_fs_i.h 這個(gè)文件中,查看當(dāng)前系統(tǒng)的 管道條目數(shù),比如我當(dāng)前的系統(tǒng)中,管道條目數(shù)為 16,因此管道的大小上限為 4096 * 16 = 65536 字節(jié)

Linux進(jìn)程間通信【匿名管道】

此時(shí)可以猜測(cè):新的管道解決方案中,為所有的管道分配了一塊定額空間,可用的 16 條管道中,可以根據(jù)自己的需要,獲取大小,極大提高了效率

三、通過程序驗(yàn)證

這個(gè)前面就已經(jīng)驗(yàn)證過了,不斷往管道中寫數(shù)據(jù),直到管道被寫滿

每次寫入 1 字節(jié)的數(shù)據(jù),可以看到最終寫了 65536 字節(jié)的數(shù)據(jù)

Linux進(jìn)程間通信【匿名管道】

總之,從 Linux 2.6.11 版本開始,管道大小上限為 64kb


8、匿名管道實(shí)操-進(jìn)程控制

匿名管道作為 IPC 的其中一種解決方案,那么肯定有它的實(shí)戰(zhàn)價(jià)值

場(chǎng)景:父進(jìn)程創(chuàng)建了一批子進(jìn)程,并通過多條匿名管道與它們鏈接,父進(jìn)程選擇某個(gè)子進(jìn)程,并通過匿名管道與子進(jìn)程通信,并下達(dá)指定的任務(wù)讓其執(zhí)行

8.1、邏輯設(shè)計(jì)

首先創(chuàng)建一批子進(jìn)程及匿名管道 -> 子進(jìn)程(讀端)阻塞,等待寫端寫入數(shù)據(jù) -> 選擇相應(yīng)的進(jìn)程,并對(duì)其寫入任務(wù)編號(hào)(數(shù)據(jù))-> 子進(jìn)程拿到數(shù)據(jù)后,執(zhí)行相應(yīng)任務(wù)

Linux進(jìn)程間通信【匿名管道】

8.2、具體功能實(shí)現(xiàn)

下面來看看具體功能實(shí)現(xiàn)(部分細(xì)節(jié)可能未展示,詳細(xì)實(shí)現(xiàn)可以看源碼)

1、創(chuàng)建一批進(jìn)程及管道

創(chuàng)建一批進(jìn)程及管道

  • 首先需要先創(chuàng)建一個(gè)包含進(jìn)程信息的類,最主要的就是子進(jìn)程的寫端 fd,這樣父進(jìn)程才能通過此 fd 進(jìn)行數(shù)據(jù)寫入
  • 循環(huán)創(chuàng)建管道、子進(jìn)程,進(jìn)行相應(yīng)的管道鏈接操作,然后子進(jìn)程進(jìn)入任務(wù)等待狀態(tài),父進(jìn)程將創(chuàng)建好的子進(jìn)程信息注冊(cè)
  • 假設(shè)子進(jìn)程獲取了任務(wù)代號(hào),那么應(yīng)該根據(jù)任務(wù)代號(hào),去執(zhí)行相應(yīng)的任務(wù),否則阻塞等待

注意:因?yàn)槭莿?chuàng)建子進(jìn)程,所以存在關(guān)系重復(fù)繼承的情況,此時(shí)應(yīng)該統(tǒng)計(jì)當(dāng)前子進(jìn)程的寫端 fd,在創(chuàng)建下一個(gè)進(jìn)程時(shí),關(guān)閉無關(guān)的 fd

具體體現(xiàn)為:每次都把 寫端 fd 存儲(chǔ)起來,在確定關(guān)系前 “清理” 干凈

Linux進(jìn)程間通信【匿名管道】

關(guān)于上述操作的危害,需要在編寫完進(jìn)程等待函數(shù)后,才能演示其作用

#define NAME_SIZE 64
// 封裝一個(gè)包含各種必備信息的類

class ProcInfo
{
public:
    ProcInfo(pid_t id = pid_t(), int fd = int())
        : _childID(id), _wfd(fd), _num(++_count)
    {
        char buff[NAME_SIZE];
        snprintf(buff, sizeof buff, "Process %d [%d:%d]", _num, _childID, _wfd);
        _name = string(buff);
    }

    ~ProcInfo()
    {
        _childID = _wfd = 0;
    }

    pid_t _childID;  // pid
    int _wfd;        // 寫端 fd
    string _name;    // 進(jìn)程名
    int _num;   //編號(hào)
    static int _count; // 計(jì)數(shù)
};

int ProcInfo::_count = 0; // 靜態(tài)成員初始化

void CreateProcessAndPipe(vector<ProcInfo> &PP, int ppNum = 3)
{
    vector<int> fds;    //存儲(chǔ)繼承中不需要的寫端 fd
    for(int i = 0; i < ppNum; i++)
    {
        //首先創(chuàng)建管道
        int pipefd[2];
        int ret = pipe(pipefd);
        assert(ret != -1);
        (void)ret;

        //然后創(chuàng)建子進(jìn)程
        int id = fork();
        assert(id != -1);
        (void)id;

        if(id == 0)
        {
            //子進(jìn)程內(nèi)

            //需要先關(guān)閉之前子進(jìn)程遺留的寫端fd
            for(auto e : fds)
                close(e);

            close(pipefd[1]);   //子進(jìn)程關(guān)閉寫端
            waitCommand(pipefd[0]);  //子進(jìn)程等待命令

            close(pipefd[0]);
            exit(0);
        }

        //父進(jìn)程內(nèi)
        close(pipefd[0]);   //父進(jìn)程關(guān)閉讀端
        PP.push_back(ProcInfo(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}
2、任務(wù)類創(chuàng)建及任務(wù)等待

子進(jìn)程在創(chuàng)建完成后,需要進(jìn)入一個(gè) 等待階段 -> 讀端阻塞,同時(shí)當(dāng)子進(jìn)程讀取到相應(yīng)的 指令 時(shí),需要執(zhí)行相應(yīng)任務(wù),這里將封裝成了一個(gè)類,并通過對(duì)象調(diào)用函數(shù)

ctrlProc.cc

void waitCommand(int rfd)
{
    while(true)
    {
        //讀端嘗試讀取信息
        int command = 0; 
        int n = read(rfd, &command, sizeof(command));
        if(n != 0)
        {
            TaskPools().Execute(command);
        }
        else
        {
            cout << "當(dāng)前子進(jìn)程讀取任務(wù)失敗,已退出!" << endl;
            break;
        }
    }
}

Task.hpp

#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>

using namespace std;

void PrintLOG()
{
  cout << "PID: " << getpid() << " 正在執(zhí)行打印日志的任務(wù)…" << endl;
}

void InsertSQL()
{
  cout << "PID: " << getpid() << " 正在執(zhí)行數(shù)據(jù)庫(kù)插入的任務(wù)…" << endl;
}

void NetRequst()
{
  cout << "PID: " << getpid() << " 正在執(zhí)行網(wǎng)絡(luò)請(qǐng)求的任務(wù)…" << endl;
}

typedef void(*func_t)();

//任務(wù)池
class TaskPools
{
public:
    TaskPools()
    {
    	//裝載任務(wù)
        _vft.push_back(PrintLOG);
        _vft.push_back(InsertSQL);
        _vft.push_back(NetRequst);
    }

    ~TaskPools()
    {}

    void Execute(int num)
    {
        //根據(jù)編號(hào),執(zhí)行任務(wù)
        if(num < 0 || num > _vft.size())
            cout << "沒有這個(gè)任務(wù)" << endl;
        else
            _vft[num]();
    }

private:
    vector<func_t> _vft;    //可用任務(wù)表
}; 
3、子進(jìn)程控制

當(dāng)所有子進(jìn)程都完成注冊(cè)后(統(tǒng)計(jì)至 PP),可以讓用戶輸入下標(biāo)選擇程序、輸入任務(wù)編號(hào)選擇任務(wù)、或者輸入程序退出

注意:因?yàn)楫?dāng)前子進(jìn)程編號(hào)從 1 開始,所以在進(jìn)行下標(biāo)訪問時(shí),需要 -1 避免越界

void showTask()
{
    cout << "**************************" << endl;
    cout << "* 0.日志打印  1.數(shù)據(jù)插入 *" << endl;
    cout << "* 2.網(wǎng)絡(luò)請(qǐng)求  3.退出程序 *" << endl;
    cout << "**************************" << endl;
}

void CtrlProcess(vector<ProcInfo> &PP)
{
    while (true)
    {
        // 展示當(dāng)前可用的進(jìn)程
        int index = 0;
        do
        {
            cout << "當(dāng)前可選擇進(jìn)程:";
            for (int i = 1; i <= PP.size(); i++)
                cout << i << " ";
            cout << endl;

            cout << "請(qǐng)選擇進(jìn)程: ";
            cin >> index;
        } while (index < 1 || index > PP.size());

        int taskNum = 0;
        do
        {
            showTask(); // 展示可選任務(wù)
            cout << "請(qǐng)選擇任務(wù): ";
            cin >> taskNum;
        } while (taskNum < 0 || taskNum > 3);

        // 分配任務(wù)
        if(taskNum == 3)
            break;

        cout << "已選擇: " << PP[index - 1]._num << " 號(hào)進(jìn)程 | " << PP[index - 1]._name << endl;
        write(PP[index - 1]._wfd, &taskNum, sizeof(taskNum));
        sleep(1);   //執(zhí)行完任務(wù)后,睡一會(huì)
    }
}

此時(shí)已經(jīng)可以把任務(wù)跑起來了

Linux進(jìn)程間通信【匿名管道】

現(xiàn)在就是萬事俱備,只欠回收

4、子進(jìn)程回收

子進(jìn)程回收十分簡(jiǎn)單,因?yàn)橐呀?jīng)在 PP 中存儲(chǔ)了各個(gè)子進(jìn)程的 PID,只需要遍歷等待回收即可

void WaitProcess(vector<ProcInfo> &PP)
{
    // 遍歷回收就好了
    for (auto e : PP)
    {
        close(e._wfd);  //關(guān)閉寫端,讀端讀取到 0 自動(dòng)結(jié)束阻塞
        int status = 0;
        waitpid(e._childID, &status, 0);

        // 通過 status 判斷子進(jìn)程運(yùn)行情況
        if ((status & 0x7F))
        {
            printf("子進(jìn)程異常退出,core dump: %d   退出信號(hào):%d\n", (status >> 7) & 1, (status & 0x7F));
        }
        else
        {
            printf("子進(jìn)程 %s 正常退出,退出碼:%d\n", e._name.c_str(), (status >> 8) & 0xFF);
        }
    }

    cout << "所有子進(jìn)程都已回收" << endl;
}

此時(shí)可以驗(yàn)證一下之前的 存在多個(gè)寫端的問題

首先正常跑(有解決方案的前提下)

Linux進(jìn)程間通信【匿名管道】

然后刪除原來的解決方案:vector<int> fds

Linux進(jìn)程間通信【匿名管道】

所以 關(guān)閉不必要的 fd 還是很重要的,尤其是在這種涉及 繼承 的場(chǎng)景中

8.3、效果演示

下面通過一個(gè)動(dòng)圖看看整個(gè)程序的運(yùn)行情況

Linux進(jìn)程間通信【匿名管道】

8.4、注意事項(xiàng)

總體來說,在使用這個(gè)小程序時(shí),以下關(guān)鍵點(diǎn)還是值得多注意的

  • 注冊(cè)子進(jìn)程信息時(shí),存儲(chǔ)的是 寫端 fd,目的是為了通過此 fd 向?qū)?yīng)的子進(jìn)程寫數(shù)據(jù),即使用不同的匿名管道
  • 創(chuàng)建管道后,需要關(guān)閉父、子進(jìn)程中不必要的 fd
  • 需要特別注意父進(jìn)程寫端 fd 被多次繼承的問題,避免因?qū)懚藳]有關(guān)干凈,而導(dǎo)致讀端持續(xù)阻塞
  • 關(guān)閉讀端對(duì)應(yīng)的寫端后,讀端會(huì)讀到 0,可以借助此特性結(jié)束子進(jìn)程的運(yùn)行
  • 在選擇進(jìn)程 / 任務(wù) 時(shí),要做好越界檢查
  • 等待子進(jìn)程退出時(shí),需要先關(guān)閉寫端,子進(jìn)程才會(huì)退出,然后才能正常等待

8.5、完整源碼

整個(gè)程序的完成源碼如下所示:

ctrlProc.cc

#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp" //任務(wù)所需頭文件

using namespace std;

#define NAME_SIZE 64

// 封裝一個(gè)包含各種必備信息的類
class ProcInfo
{
public:
    ProcInfo(pid_t id = pid_t(), int fd = int())
        : _childID(id), _wfd(fd), _num(++_count)
    {
        char buff[NAME_SIZE];
        snprintf(buff, sizeof buff, "Process %d [%d:%d]", _num, _childID, _wfd);
        _name = string(buff);
    }

    ~ProcInfo()
    {
        _childID = _wfd = 0;
    }

    pid_t _childID;    // pid
    int _wfd;          // 寫端 fd
    string _name;      // 進(jìn)程名
    int _num;          // 編號(hào)
    static int _count; // 計(jì)數(shù)
};

int ProcInfo::_count = 0; // 靜態(tài)成員初始化

void waitCommand(int rfd)
{
    while (true)
    {
        // 讀端嘗試讀取信息
        int command = 0;
        int n = read(rfd, &command, sizeof(command));
        if (n != 0)
        {
            TaskPools().Execute(command);
        }
        else
        {
            cout << "當(dāng)前子進(jìn)程讀取任務(wù)失敗,已退出!" << endl;
            break;
        }
    }
}

void CreateProcessAndPipe(vector<ProcInfo> &PP, int ppNum = 3)
{
    vector<int> fds; // 存儲(chǔ)繼承中不需要的寫端 fd
    for (int i = 0; i < ppNum; i++)
    {
        // 首先創(chuàng)建管道
        int pipefd[2];
        int ret = pipe(pipefd);
        assert(ret != -1);
        (void)ret;

        // 然后創(chuàng)建子進(jìn)程
        int id = fork();
        assert(id != -1);
        (void)id;

        if (id == 0)
        {
            // 子進(jìn)程內(nèi)

            // 需要先關(guān)閉之前子進(jìn)程遺留的寫端fd
            for (auto e : fds)
                close(e);

            close(pipefd[1]);       // 子進(jìn)程關(guān)閉寫端
            waitCommand(pipefd[0]); // 子進(jìn)程等待命令

            close(pipefd[0]);
            exit(0);
        }

        // 父進(jìn)程內(nèi)
        close(pipefd[0]); // 父進(jìn)程關(guān)閉讀端
        PP.push_back(ProcInfo(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}

void showTask()
{
    cout << "**************************" << endl;
    cout << "* 0.日志打印  1.數(shù)據(jù)插入 *" << endl;
    cout << "* 2.網(wǎng)絡(luò)請(qǐng)求  3.退出程序 *" << endl;
    cout << "**************************" << endl;
}

void CtrlProcess(vector<ProcInfo> &PP)
{
    while (true)
    {
        // 展示當(dāng)前可用的進(jìn)程
        int index = 0;
        do
        {
            cout << "當(dāng)前可選擇進(jìn)程:";
            for (int i = 1; i <= PP.size(); i++)
                cout << i << " ";
            cout << endl;

            cout << "請(qǐng)選擇進(jìn)程: ";
            cin >> index;
        } while (index < 1 || index > PP.size());

        int taskNum = 0;
        do
        {
            showTask(); // 展示可選任務(wù)
            cout << "請(qǐng)選擇任務(wù): ";
            cin >> taskNum;
        } while (taskNum < 0 || taskNum > 3);

        // 分配任務(wù)
        if (taskNum == 3)
            break;

        cout << "已選擇: " << PP[index - 1]._num << " 號(hào)進(jìn)程 | " << PP[index - 1]._name << endl;
        write(PP[index - 1]._wfd, &taskNum, sizeof(taskNum));
        sleep(1); // 執(zhí)行完任務(wù)后,睡一會(huì)
    }
}

void WaitProcess(vector<ProcInfo> &PP)
{
    // 遍歷回收就好了
    for (auto e : PP)
    {
        close(e._wfd);  //關(guān)閉寫端,讀端讀取到 0 自動(dòng)結(jié)束阻塞
        int status = 0;
        waitpid(e._childID, &status, 0);

        // 通過 status 判斷子進(jìn)程運(yùn)行情況
        if ((status & 0x7F))
        {
            printf("子進(jìn)程異常退出,core dump: %d   退出信號(hào):%d\n", (status >> 7) & 1, (status & 0x7F));
        }
        else
        {
            printf("子進(jìn)程 %s 正常退出,退出碼:%d\n", e._name.c_str(), (status >> 8) & 0xFF);
        }
    }

    cout << "所有子進(jìn)程都已回收" << endl;
}

int main()
{
    // 1、創(chuàng)建一批進(jìn)程及匿名管道
    vector<ProcInfo> PP;
    CreateProcessAndPipe(PP);

    // 2、進(jìn)程控制
    CtrlProcess(PP);

    // 3、進(jìn)程回收
    WaitProcess(PP);

    return 0;
}

Task.hpp

#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>

using namespace std;

void PrintLOG()
{
  cout << "PID: " << getpid() << " 正在執(zhí)行打印日志的任務(wù)…" << endl;
}

void InsertSQL()
{
  cout << "PID: " << getpid() << " 正在執(zhí)行數(shù)據(jù)庫(kù)插入的任務(wù)…" << endl;
}

void NetRequst()
{
  cout << "PID: " << getpid() << " 正在執(zhí)行網(wǎng)絡(luò)請(qǐng)求的任務(wù)…" << endl;
}

typedef void(*func_t)();

//任務(wù)池
class TaskPools
{
public:
    TaskPools()
    {
        _vft.push_back(PrintLOG);
        _vft.push_back(InsertSQL);
        _vft.push_back(NetRequst);
    }

    ~TaskPools()
    {}

    void Execute(int num)
    {
        //根據(jù)編號(hào),執(zhí)行任務(wù)
        if(num < 0 || num > _vft.size())
            cout << "沒有這個(gè)任務(wù)" << endl;
        else
            _vft[num]();
    }

private:
    vector<func_t> _vft;    //可用任務(wù)表
}; 

Makefile

ctrlProc:ctrlProc.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -r ctrlProc

??總結(jié)

以上就是本次關(guān)于 Linux 進(jìn)程間通信之匿名管道的全部?jī)?nèi)容了,在本文中,我們首先學(xué)習(xí)了什么是 IPC,以及 IPC 的發(fā)展歷史及分類;然后從 管道 中的 匿名管道 入手,介紹了 管道 的各種特性、場(chǎng)景及 匿名管道 的使用;最后通過一個(gè)簡(jiǎn)單的 匿名管道 進(jìn)程控制程序,將 匿名管道 IPC 這種方法的知識(shí)整體運(yùn)用了一遍,第一次接觸這種多進(jìn)程程序,還是值得一寫的


Linux進(jìn)程間通信【匿名管道】

文章來源地址http://www.zghlxwxcb.cn/news/detail-472660.html

相關(guān)文章推薦

Linux基礎(chǔ)IO【軟硬鏈接與動(dòng)靜態(tài)庫(kù)】

Linux基礎(chǔ)IO【深入理解文件系統(tǒng)】

Linux【模擬實(shí)現(xiàn)C語言文件流】

Linux基礎(chǔ)IO【重定向及緩沖區(qū)理解】

Linux基礎(chǔ)IO【文件理解與操作】
===============

Linux【模擬實(shí)現(xiàn)簡(jiǎn)易版bash】

Linux進(jìn)程控制【進(jìn)程程序替換】

Linux進(jìn)程控制【創(chuàng)建、終止、等待】

到了這里,關(guān)于Linux進(jìn)程間通信【匿名管道】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包