国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁(yè) > news >正文

黃山網(wǎng)站建設(shè)推廣網(wǎng)絡(luò)輿情監(jiān)測(cè)系統(tǒng)

黃山網(wǎng)站建設(shè)推廣,網(wǎng)絡(luò)輿情監(jiān)測(cè)系統(tǒng),國(guó)外做設(shè)備網(wǎng)站,網(wǎng)站沒(méi)收錄可以做推廣嗎🌈歡迎來(lái)到Linux專(zhuān)欄~~進(jìn)程通信 (???(??? )🐣,我是Scort目前狀態(tài):大三非科班啃C中🌍博客主頁(yè):張小姐的貓~江湖背景快上車(chē)🚘,握好方向盤(pán)跟我有一起打天下嘞!送給自己的一句雞湯…

🌈歡迎來(lái)到Linux專(zhuān)欄~~進(jìn)程通信


  • (???(??? )🐣,我是Scort
  • 目前狀態(tài):大三非科班啃C++中
  • 🌍博客主頁(yè):張小姐的貓~江湖背景
  • 快上車(chē)🚘,握好方向盤(pán)跟我有一起打天下嘞!
  • 送給自己的一句雞湯🤔:
  • 🔥真正的大師永遠(yuǎn)懷著一顆學(xué)徒的心
  • 作者水平很有限,如果發(fā)現(xiàn)錯(cuò)誤,可在評(píng)論區(qū)指正,感謝🙏
  • 🎉🎉歡迎持續(xù)關(guān)注!
    在這里插入圖片描述

請(qǐng)?zhí)砑訄D片描述

文章目錄

  • 🌈歡迎來(lái)到Linux專(zhuān)欄~~進(jìn)程通信
    • 一. 進(jìn)程間通信介紹
    • 二. 管道
      • 🌍匿名管道
        • 😎匿名管道原理
        • 😎創(chuàng)建匿名管道pipe
        • 😎demo代碼
        • 😎匿名管道通信的4種情況
          • ?讀阻塞:寫(xiě)快,讀慢
          • ?寫(xiě)阻塞:寫(xiě)慢,讀快
          • ?寫(xiě)端關(guān)閉
          • ?讀端關(guān)閉
        • 😎管道的大小
      • 🌍命名管道
        • 🎨創(chuàng)建命名管道
        • 🎨基于命名管道通信
      • 🌍 pipe vs fifo
    • 三. System V標(biāo)準(zhǔn)下的進(jìn)程間通信方式
      • 🌈共享內(nèi)存
        • 💦共享內(nèi)存的建立
          • 💛 創(chuàng)建共享內(nèi)存
          • 💛 控制共享內(nèi)存
          • 💛 掛接和去關(guān)聯(lián)
          • 💛 shmid 和 key
        • 💦共享內(nèi)存的進(jìn)程間通信
        • 💦共享內(nèi)存與管道進(jìn)行對(duì)比
        • 💦共享內(nèi)存歸屬誰(shuí)
        • 💦共享內(nèi)存的特征
      • 🌈消息隊(duì)列(了解)
  • 📢寫(xiě)在最后

請(qǐng)?zhí)砑訄D片描述

一. 進(jìn)程間通信介紹

進(jìn)程之間會(huì)存在特定的協(xié)同工作的場(chǎng)景:

  • 數(shù)據(jù)傳輸:一個(gè)進(jìn)程要把自己的數(shù)據(jù)交給另一個(gè)進(jìn)程,讓其繼續(xù)進(jìn)行處理
  • 資源共享:多個(gè)進(jìn)程之間共享同樣的資源。
  • 通知事件:一個(gè)進(jìn)程需要向另一個(gè)或一組進(jìn)程發(fā)送消息,通知它(它們)發(fā)生了某種事件(如進(jìn)程終止時(shí)要通知父進(jìn)程)。
  • 進(jìn)程控制:有些進(jìn)程希望完全控制另一個(gè)進(jìn)程的執(zhí)行(如Debug進(jìn)程),此時(shí)控制進(jìn)程希望能夠攔截另一個(gè)進(jìn)程的所有陷入和異常,并能夠及時(shí)知道它的狀態(tài)改變

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

進(jìn)程是具有獨(dú)立性的。虛擬地址空間+頁(yè)表 保證了進(jìn)程運(yùn)行的獨(dú)立性(進(jìn)程內(nèi)核數(shù)據(jù)結(jié)構(gòu)+進(jìn)程代碼和數(shù)據(jù))

進(jìn)程通信的前提,首先需要讓不同的進(jìn)程看到同一份“內(nèi)存”(特定的結(jié)構(gòu)組織)

  • 這塊內(nèi)存應(yīng)該屬于誰(shuí)呢?為了維持進(jìn)程獨(dú)立性,它一定不屬于進(jìn)程A或B,它屬于操作系統(tǒng)。

綜上,進(jìn)程間通信的前提就是:由OS參與,提供一份所有通信進(jìn)程都能看到的公共資源

進(jìn)程間通信的發(fā)展

  • 管道
    • 匿名管道pipe
    • 命名管道pipe
  • System V標(biāo)準(zhǔn) 進(jìn)程間通信
    • System V 消息隊(duì)列
    • System V 共享內(nèi)存
    • System V 信號(hào)量
  • POSIX標(biāo)準(zhǔn) 進(jìn)程間通信(多線程詳談)
    • 消息隊(duì)列
    • 共享內(nèi)存
    • 信號(hào)量
    • 互斥量
    • 條件變量
    • 讀寫(xiě)鎖

二. 管道

什么是管道?

  • 有入口,有出口,都是單向傳輸資源的(數(shù)據(jù))

在這里插入圖片描述

所以計(jì)算機(jī)領(lǐng)域設(shè)計(jì)者,設(shè)計(jì)了一種單向通信的方式 —— 管道

🌍匿名管道

眾所周知,父子進(jìn)程是兩個(gè)獨(dú)立進(jìn)程,父子通信也是進(jìn)程間通信的一種,基于父子間進(jìn)程通信就是匿名管道。我們首先要對(duì)匿名管道有一個(gè)宏觀的認(rèn)識(shí)

父進(jìn)程創(chuàng)建子進(jìn)程,子進(jìn)程需要以父進(jìn)程為模板創(chuàng)建自己的files_struct ,而不是與父進(jìn)程共用;但是struct file這個(gè)結(jié)構(gòu)體就不會(huì)拷貝,因?yàn)榇蜷_(kāi)文件也與創(chuàng)建進(jìn)程無(wú)關(guān)(文件的數(shù)據(jù)不用拷貝)

  • 因?yàn)樽筮吺沁M(jìn)程相關(guān)數(shù)據(jù)結(jié)構(gòu),右邊是文件相關(guān)結(jié)構(gòu)

在這里插入圖片描述

😎匿名管道原理

  1. 父進(jìn)程創(chuàng)建管道,對(duì)同一文件分別以讀&寫(xiě)方式打開(kāi)

在這里插入圖片描述

  1. 父進(jìn)程fork創(chuàng)建子進(jìn)程
    在這里插入圖片描述

  2. 因?yàn)楣艿朗且粋€(gè)只能單向通信的信道,父子進(jìn)程需要關(guān)閉對(duì)應(yīng)讀寫(xiě)端,至于誰(shuí)關(guān)閉誰(shuí),取決于通信方向。
    在這里插入圖片描述

于是,通過(guò)子進(jìn)程繼承父進(jìn)程資源的特性,雙方進(jìn)程看到了同一份資源。

😎創(chuàng)建匿名管道pipe

pipe誰(shuí)調(diào)用就讓以讀寫(xiě)方式打開(kāi)一個(gè)文件(內(nèi)存級(jí)文件)

#include <unistd.h>
int pipe(int pipefd[2]);
  • 參數(shù)pipefd輸出型參數(shù)!通過(guò)這個(gè)參數(shù)拿到兩個(gè)打開(kāi)的fd
  • 返回值:成功返回0;失敗返回-1

數(shù)組pipefd用于返回兩個(gè)指向管道讀端和寫(xiě)端的文件描述符:

數(shù)組元素含義
pipefd[0]~嘴巴管道讀端的文件描述符
pipefd[1] ~ 鋼筆管道寫(xiě)端的文件描述符

此處提取查一下要用到的函數(shù)

  • man2是獲得系統(tǒng)(linux內(nèi)核)調(diào)用的用法; man 3 是獲得標(biāo)準(zhǔn)庫(kù)(標(biāo)準(zhǔn)C語(yǔ)言庫(kù)、glibc)函數(shù)的文檔
//linux中用man可以查哦
#include <unistd.h>
pid_t fork(void);#include <unistd.h>
int close(int fd);#include <stdlib.h>
void exit(int status);

下面按照之前講的原理進(jìn)行逐一操作:①創(chuàng)建管道 ②父進(jìn)程創(chuàng)建子進(jìn)程 ③關(guān)閉對(duì)應(yīng)的讀寫(xiě)端,形成單向信道

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>using namespace std;int main()
{//1.創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd);  //失敗返回-1assert(n != -1);  //只在debug下有效(void)n; //僅此證明n被使用過(guò)#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl;  //3cout<< "pipefd[1]" << pipefd[1] << endl;  //4
#endif//2.創(chuàng)建子進(jìn)程 pid_t id = fork();assert(id != -1);if(id == 0){//子進(jìn)程//3. 構(gòu)建單向通信的信道//3.1 子進(jìn)程關(guān)閉寫(xiě)端[1]close(pipefd[1]);exit(0);}//父進(jìn)程//父進(jìn)程關(guān)閉讀端[0]close(pipefd[0]);return 0;
}

在此基礎(chǔ)上,我們就要進(jìn)行通信了,實(shí)際上就是對(duì)某個(gè)文件進(jìn)行寫(xiě)入,因?yàn)楣艿酪彩俏募?#xff0c;下面提提前查看要用到的函數(shù)

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:
- 返回寫(xiě)入的字節(jié)數(shù)
- 零表示未寫(xiě)入任何內(nèi)容,這里意味著對(duì)端進(jìn)程關(guān)閉文件描述符#include <unistd.h>
unsigned int sleep(unsigned int seconds);

😎demo代碼

簡(jiǎn)單實(shí)現(xiàn)了管道通信的demo版本:

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;int main()
{//1.創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd);  //失敗返回-1assert(n != -1);  //只在debug下有效(void)n; //僅此證明n被使用過(guò)#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl;  //3cout<< "pipefd[1]" << pipefd[1] << endl;  //4
#endif//2.創(chuàng)建子進(jìn)程 pid_t id = fork();assert(id != -1);if(id == 0){//子進(jìn)程  - 讀//3. 構(gòu)建單向通信的信道//3.1 子進(jìn)程關(guān)閉寫(xiě)端[1]close(pipefd[1]);char buffer[1024];while(1){size_t s = read(pipefd[0], buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;//因?yàn)閞ead是系統(tǒng)調(diào)用,沒(méi)有/0,此處給加上cout<<"child get a message["<< getpid() << "] 爸爸對(duì)你說(shuō)" << buffer << endl;}}//close(pipefd[0]);exit(0);}//父進(jìn)程 - 寫(xiě)//父進(jìn)程關(guān)閉讀端[0]close(pipefd[0]);string message = "我是父進(jìn)程,我正在給你發(fā)消息";int count = 0; //計(jì)算發(fā)送次數(shù)char send_buffer[1024];while(true){//3.2構(gòu)建一個(gè)變化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count);count++;//3.3寫(xiě)入write(pipefd[1], send_buffer, strlen(send_buffer));//此處strlen不能+1//3.4 故意sleepsleep(1);}pid_t ret = waitpid(id, nullptr, 0);assert(ret != -1);(void)ret;return 0;
}

此處有個(gè)問(wèn)題:為什么不定義一個(gè)全局的buffer來(lái)進(jìn)行通信呢?

  • 因?yàn)橛?strong>寫(xiě)時(shí)拷貝的存在,無(wú)法更改通信!

上面的方法就是把數(shù)據(jù)交給管道,讓對(duì)方通過(guò)管道進(jìn)行讀取

😎匿名管道通信的4種情況

之前父子進(jìn)程同時(shí)向顯示器中寫(xiě)入的時(shí)候,二者會(huì)互斥 —— 缺乏訪問(wèn)控制

而對(duì)于管道進(jìn)行讀取的時(shí)候,父進(jìn)程如果寫(xiě)的慢,子進(jìn)程就會(huì)等待讀取 —— 這就是說(shuō)明管道具有訪問(wèn)控制

?讀阻塞:寫(xiě)快,讀慢

父進(jìn)程瘋狂的進(jìn)行寫(xiě)入,子進(jìn)程隔10秒才讀取,子進(jìn)程會(huì)把這10秒內(nèi)父進(jìn)程寫(xiě)入的所有數(shù)據(jù)都一次性的打印出來(lái)!

代碼如非就是在父進(jìn)程添加了打印conut,子進(jìn)程sleep(10),可以自行的在demo代碼上添加

在這里插入圖片描述

父進(jìn)程寫(xiě)了1220次,子進(jìn)程一次就給你讀完了,讀寫(xiě)之間沒(méi)有關(guān)系,這就叫做流式的服務(wù)。
也就是管道是面向字節(jié)流的,也就是只有字節(jié)的概念,究竟讀成什么樣也無(wú)法保證,甚至可能讀出亂碼,所以父子進(jìn)程通信也是需要制定協(xié)議的,但這個(gè)我們網(wǎng)絡(luò)再細(xì)說(shuō)。。

?寫(xiě)阻塞:寫(xiě)慢,讀快

管道沒(méi)有數(shù)據(jù)的時(shí)候,讀端必須等待:父進(jìn)程每隔2秒才進(jìn)行寫(xiě)入,子進(jìn)程瘋狂的讀取

請(qǐng)?zhí)砑訄D片描述

?寫(xiě)端關(guān)閉

父進(jìn)程寫(xiě)入10秒,后把寫(xiě)端fd關(guān)閉,讀端會(huì)怎么樣?

  • 寫(xiě)入的一方,fd沒(méi)有關(guān)閉,如果有數(shù)據(jù)就讀,沒(méi)有數(shù)據(jù)就等
  • 寫(xiě)入的一方,fd關(guān)閉了,讀取的一方,read會(huì)返回0,表示讀到了文件結(jié)尾,退出讀端
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <assert.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;int main()
{//1.創(chuàng)建管道int pipefd[2] = {0};int n = pipe(pipefd);  //失敗返回-1assert(n != -1);  //只在debug下有效(void)n; //僅此證明n被使用過(guò)#ifdef DEBUGcout<< "pipefd[0]" << pipefd[0] << endl;  //3cout<< "pipefd[1]" << pipefd[1] << endl;  //4
#endif//2.創(chuàng)建子進(jìn)程 pid_t id = fork();assert(id != -1);if(id == 0){//子進(jìn)程  - 讀//3. 構(gòu)建單向通信的信道//3.1 子進(jìn)程關(guān)閉寫(xiě)端[1]close(pipefd[1]);char buffer[1024*8];while(1){//sleep(10);//20秒讀一次//寫(xiě)入的一方,fd沒(méi)有關(guān)閉,如果有數(shù)據(jù)就讀,沒(méi)有數(shù)據(jù)就等//寫(xiě)入的一方,fd關(guān)閉了,讀取的一方,read會(huì)返回0,表示讀到了文件結(jié)尾size_t s = read(pipefd[0], buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;//因?yàn)閞ead是系統(tǒng)調(diào)用,沒(méi)有/0,此處給加上cout<<"child get a message["<< getpid() << "] 爸爸對(duì)你說(shuō)" << buffer << endl;}else if (s == 0){cout << "write quit(father), me quit!!!" <<endl;break;}}//close(pipefd[0]);exit(0);}//父進(jìn)程 - 寫(xiě)//父進(jìn)程關(guān)閉讀端[0]close(pipefd[0]);string message = "我是父進(jìn)程,我正在給你發(fā)消息";int count = 0; //計(jì)算發(fā)送次數(shù)char send_buffer[1024*8];while(true){//3.2構(gòu)建一個(gè)變化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",message.c_str(), getpid(), count);count++;//3.3寫(xiě)入write(pipefd[1], send_buffer, strlen(send_buffer));//此處strlen不能+1//3.4 故意sleepsleep(1);cout<< count <<endl;if(count == 5){cout<< "父進(jìn)程寫(xiě)端退出" << endl;break;}}close(pipefd[1]);//關(guān)閉讀端pid_t ret = waitpid(id, nullptr, 0);assert(ret != -1);(void)ret;return 0;
}

運(yùn)行結(jié)果如下:

請(qǐng)?zhí)砑訄D片描述

?讀端關(guān)閉

讀端關(guān)閉,寫(xiě)端繼續(xù)寫(xiě)入,直到OS終止寫(xiě)進(jìn)程

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe創(chuàng)建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork創(chuàng)建子進(jìn)程if (id == 0){//childclose(fd[0]); //子進(jìn)程關(guān)閉讀端//子進(jìn)程向管道寫(xiě)入數(shù)據(jù)const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子進(jìn)程寫(xiě)入完畢,關(guān)閉文件exit(0);}//fatherclose(fd[1]); //父進(jìn)程關(guān)閉寫(xiě)端close(fd[0]); //父進(jìn)程直接關(guān)閉讀端(導(dǎo)致子進(jìn)程被操作系統(tǒng)殺掉)int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F); //打印子進(jìn)程收到的信號(hào)return 0;
}

運(yùn)行結(jié)果顯示,子進(jìn)程退出時(shí)收到的是13號(hào)信號(hào)

在這里插入圖片描述
通過(guò)kill -l命令可以查看13對(duì)應(yīng)的具體信號(hào)

在這里插入圖片描述
由此可知,當(dāng)發(fā)生情況四時(shí),操作系統(tǒng)向子進(jìn)程發(fā)送的是SIGPIPE信號(hào)將子進(jìn)程終止的。

🐋總結(jié)上述的4中場(chǎng)景:

  • 寫(xiě)快,讀慢,寫(xiě)滿(mǎn)了不能再寫(xiě)了
  • 寫(xiě)慢,讀快,管道沒(méi)有數(shù)據(jù)的時(shí)候,讀端必須等待
  • 寫(xiě)關(guān),讀取的一方,read會(huì)返回0,表示讀到了文件結(jié)尾,退出讀端
  • 讀關(guān),寫(xiě)繼續(xù)寫(xiě),OS終止寫(xiě)進(jìn)程 ——

🧐由上總結(jié)出匿名管道的5個(gè)特點(diǎn) ——

  1. 管道是一個(gè)單向通信的通信管道,是半雙工通信的一種特殊情況
  2. 管道是用來(lái)進(jìn)行具有血緣關(guān)系的進(jìn)程進(jìn)行進(jìn)程間通信 —— 常用于父子通信
  3. 管道具有通過(guò)讓進(jìn)程間協(xié)同,提供了訪問(wèn)控制!
  4. 管道是 面向字節(jié)流 —— 協(xié)議(后面詳談)
  5. 管道是基于文件的,管道的聲明周期是隨進(jìn)程的

😎管道的大小

管道的容量是有限的,如果管道已滿(mǎn),那么寫(xiě)端將阻塞或失敗,那么管道的最大容量是多少呢?

ps:原子性:要么做了,要么不做,沒(méi)有中間狀態(tài)

方法1 :man手冊(cè)查詢(xún)

在這里插入圖片描述
然后我們可以使用uname -r命令,查看自己使用的Linux版本

在這里插入圖片描述
我使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字節(jié)

方法二:自行測(cè)試

也就是如果讀端一直不讀取,寫(xiě)端又不斷的寫(xiě)入,當(dāng)管道被寫(xiě)滿(mǎn)后,寫(xiě)端進(jìn)程就會(huì)被掛起。據(jù)此,我們可以寫(xiě)出以下代碼來(lái)測(cè)試管道的最大容量。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe創(chuàng)建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork創(chuàng)建子進(jìn)程if (id == 0){//child close(fd[0]); //子進(jìn)程關(guān)閉讀端char c = 'a';int count = 0;//子進(jìn)程一直進(jìn)行寫(xiě)入,一次寫(xiě)入一個(gè)字節(jié)while (1){write(fd[1], &c, 1);count++;printf("%d\n", count); //打印當(dāng)前寫(xiě)入的字節(jié)數(shù)}close(fd[1]);exit(0);}//fatherclose(fd[1]); //父進(jìn)程關(guān)閉寫(xiě)端//父進(jìn)程不進(jìn)行讀取waitpid(id, NULL, 0);close(fd[0]);return 0;
}

寫(xiě)端進(jìn)程最多寫(xiě)65536字節(jié)的數(shù)據(jù)就被操作系統(tǒng)掛起了,也就是說(shuō),我當(dāng)前Linux版本中管道的最大容量是65536字節(jié)

在這里插入圖片描述

🌍命名管道

為了解決匿名管道只能在父子之間通信,我們引入命名管道,可以在任意不相關(guān)進(jìn)程進(jìn)行通信

多個(gè)進(jìn)程打開(kāi)同一個(gè)文件,OS只會(huì)創(chuàng)建一個(gè)struct_file

在這里插入圖片描述

命名管道就是一種特殊類(lèi)型的文件(可以被打開(kāi),但不會(huì)將數(shù)據(jù)刷新進(jìn)磁盤(pán)),兩個(gè)進(jìn)程通過(guò)命名管道的文件名打開(kāi)同一個(gè)管道文件,此時(shí)這兩個(gè)進(jìn)程也就看到了同一份資源,進(jìn)而就可以進(jìn)行通信了。

命名管道就是通過(guò)唯一路徑/文件名的方式定位唯一磁盤(pán)文件的

ps:命名管道和匿名管道一樣,都是內(nèi)存文件,只不過(guò)命名管道在磁盤(pán)有一個(gè)簡(jiǎn)單的映像(所以有名字),但這個(gè)映像的大小永遠(yuǎn)為0,因?yàn)槊艿篮湍涿艿蓝疾粫?huì)將通信數(shù)據(jù)刷新到磁盤(pán)當(dāng)中。

🎨創(chuàng)建命名管道

💛 make FIFOs 在命令行上創(chuàng)建命名管道

mkfifo (named pipes)

FIFO:First In First Out 隊(duì)列呀

在這里插入圖片描述

來(lái)個(gè)小實(shí)驗(yàn):
命令行上執(zhí)行的命令echocat都是進(jìn)程,所以這就是通過(guò)管道文件進(jìn)行的進(jìn)程間通信 ——

在這里插入圖片描述
請(qǐng)?zhí)砑訄D片描述
💛 那么如何用代碼實(shí)現(xiàn)命名管道進(jìn)程間通信的呢?

//查手冊(cè):man 3 mkfifo
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
  • pathname:管道文件路徑
  • mode:管道文件權(quán)限
  • 返回值:創(chuàng)建成功返回0;創(chuàng)建失敗返回-1,并設(shè)置錯(cuò)誤碼

我touch了server.c和client.c,最終希望在serverclient兩個(gè)進(jìn)程之間相互通信,先寫(xiě)一個(gè)Makefile ——

.PHONY:all
all:client serverclient:client.cxxg++ -o $@ $^ -std=c++11
server:server.cxxg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server
  • Makefile自頂向下掃描,只會(huì)把第一個(gè)目標(biāo)文件作為最終的目標(biāo)文件。所以要一次性生成兩個(gè)可執(zhí)行程序,需要定義偽目標(biāo).PHONY: all,并添加依賴(lài)關(guān)系

🎨基于命名管道通信

comm.h

我們創(chuàng)建一個(gè)共用的頭文件,這只是為了兩個(gè)程序能有看到同一個(gè)資源的能力了

#ifndef _COMM_H_ //能避免頭文件的重定義
#define _COMM_H_//hpp和.h的區(qū)別:.h里面只有聲明,沒(méi)有實(shí)現(xiàn),而.hpp里聲明實(shí)現(xiàn)都有,后者可以減少.cpp的數(shù)量#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>using namespace std;#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc";#endif

server.c

  1. 創(chuàng)建命名管道
  2. 讀信息,并實(shí)現(xiàn)相應(yīng)業(yè)務(wù)邏輯
#include "comm.hpp"int main()
{//1.創(chuàng)建管道文件if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}//3.編寫(xiě)正常的通信代碼char buffer[SIZE];while(1){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout << "client say >" << buffer << endl;}else if(s == 0){//說(shuō)明寫(xiě)端關(guān)閉了cerr << "read end of file, client quit, server quit too" <<endl;}else{//讀取失敗perror("read");break;}}//4. 關(guān)閉文件close(fd);unlink(ipcPath.c_str());//通信完畢,刪除文件return 0;
}

client.c
此時(shí)不需要再創(chuàng)建命名管道,只需要獲取已打開(kāi)的命名管道文件

  • 從鍵盤(pán)拿到了待發(fā)送數(shù)據(jù)
  • 發(fā)送數(shù)據(jù),也就是向管道中寫(xiě)入
#include "comm.hpp"int main()
{//不需要?jiǎng)?chuàng)建fifo,只需獲取即可int fd = open(ipcPath.c_str(), O_WRONLY);if(fd < 0){perror("open");exit(1);}//2.ipc通信string buffer;while(1){cout << "Place Enter Message:";std::getline(std::cin, buffer);write(fd, buffer.c_str(), sizeof(buffer));}//3.關(guān)閉close(fd);return 0;
}

效果展示:
一定要先運(yùn)行服務(wù)端server創(chuàng)建命名管道,再運(yùn)行客戶(hù)端,實(shí)現(xiàn)了不相關(guān)進(jìn)程通信 ——

請(qǐng)?zhí)砑訄D片描述
如果我想讓多個(gè)子進(jìn)程來(lái)執(zhí)行打印任務(wù)
在這里插入圖片描述
當(dāng)然我們就要調(diào)整一下server.c的業(yè)務(wù)邏輯:

#include "comm.hpp"
#include <sys/wait.h>static void getMessage(int fd)
{//3.編寫(xiě)正常的通信代碼char buffer[SIZE];while(1){memset(buffer, '\0', sizeof(buffer));ssize_t s = read(fd, buffer, sizeof(buffer)-1);if(s > 0){cout << "[" << getpid() << "] " << "client say >" << buffer << endl;}else if(s == 0){//說(shuō)明寫(xiě)端關(guān)閉了cerr << "[" << getpid() << "] " << "read end of file, client quit, server quit too" <<endl;}else{//讀取失敗perror("read");break;}}
}int main()
{//1.創(chuàng)建管道文件if(mkfifo(ipcPath.c_str(), MODE) < 0){perror("mkfifo");exit(1);}//log("創(chuàng)建管道文件成功", Debug) << "step 1" <<endl;//2. 正常的文件操作int fd = open(ipcPath.c_str(), O_RDONLY);if(fd < 0){perror("open");exit(2);}//log("打開(kāi)管道文件成功", Debug) << "step 2" <<endl;int nums = 3;for(int i = 0; i < nums; i++){pid_t id = fork();if(id==0){//子進(jìn)程getMessage(fd);exit(2);}}for(int i = 0; i < nums; i++){waitpid(-1, nullptr, 0);}//4. 關(guān)閉文件close(fd);//log("關(guān)閉管道文件成功", Debug) << "step 3" <<endl;unlink(ipcPath.c_str());//通信完畢,刪除文件//log("刪除管道文件成功", Debug) << "step 4" <<endl;return 0;
}

🌍 pipe vs fifo

為什么pipe叫做匿名管道和和fifo叫做命名管道?

  • 匿名管道文件屬于內(nèi)存級(jí)的文件,不需要名字,因?yàn)樗峭ㄟ^(guò)父子繼承的方式看到同一份資源
  • 命名管道一定要有名字,從而使不相關(guān)進(jìn)程通過(guò)唯一路徑定位同一個(gè)文件

三. System V標(biāo)準(zhǔn)下的進(jìn)程間通信方式

下面我們要學(xué)習(xí)System V標(biāo)準(zhǔn),是在同一主機(jī)內(nèi)的進(jìn)程間通信方案,是站在OS層面,專(zhuān)門(mén)為進(jìn)程間通信設(shè)計(jì)的方案。

進(jìn)程通信的本質(zhì)是先讓不同進(jìn)程看到同一份資源,System V提供了這三個(gè)主流方案 ——

  • 共享內(nèi)存 - 傳遞數(shù)據(jù)
  • 消息隊(duì)列(有點(diǎn)落伍) - 傳遞數(shù)據(jù)
  • 信號(hào)量 (多線程講POSIX標(biāo)準(zhǔn)) - 實(shí)現(xiàn)進(jìn)程同步&控制詳談

🌈共享內(nèi)存

基于共享內(nèi)存進(jìn)行進(jìn)程間通信原理 ——

  1. 首先在物理內(nèi)存當(dāng)中申請(qǐng)一塊內(nèi)存空間,將這塊內(nèi)存空間分別與各個(gè)進(jìn)程各自的頁(yè)表之間建立映射
  2. 進(jìn)程虛擬地址空間當(dāng)中開(kāi)辟空間(共享內(nèi)存)并將虛擬地址填充到各自頁(yè)表的對(duì)應(yīng)位置,使得虛擬地址和物理地址之間建立起對(duì)應(yīng)關(guān)系
  3. 所以?xún)蓚€(gè)進(jìn)程便看到了同一份物理內(nèi)存,這塊物理內(nèi)存就叫做共享內(nèi)存

在這里插入圖片描述

💦共享內(nèi)存的建立

共享內(nèi)存提供者是操作系統(tǒng)OS,那么操作系統(tǒng)要不要管理共享內(nèi)存呢? -> 先描述再組織

共享內(nèi)存 = 共享內(nèi)存塊 + 對(duì)應(yīng)的共享內(nèi)存的內(nèi)核數(shù)據(jù)結(jié)構(gòu)來(lái)描述其屬性

💛 創(chuàng)建共享內(nèi)存
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);

參數(shù):

  • key:為了使不同進(jìn)程看到同一段共享內(nèi)存,即讓不同進(jìn)程拿到同一個(gè)ID,需要由用戶(hù)自己設(shè)定,但如何設(shè)定的與眾不同好難啊,就要借助下面這個(gè)函數(shù)。
    在這里插入圖片描述所以怎么樣保證兩個(gè)進(jìn)程拿到同一個(gè)key值呢?

    #include <sys/types.h>
    #include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
    
    • pathname:自定義路徑名
    • proj_id:自定義項(xiàng)目ID
    • 返回值:成功后,返回生成的key_t值。失敗時(shí)返回1
  • szie共享內(nèi)存的大小,建議是4KB的整數(shù)倍,因?yàn)楣蚕韮?nèi)存在內(nèi)核中申請(qǐng)的基本單位是頁(yè)(內(nèi)存頁(yè))。

  • shmflg標(biāo)記位,這一看就是宏,都是只有一個(gè)比特位是1且相互不重復(fù)的數(shù)據(jù),這樣|在一起,就能傳遞多個(gè)標(biāo)志位

    • IPC_CREAT:如果單獨(dú)使用IPC_CREAT或者flg為0,如果創(chuàng)建共享內(nèi)存時(shí),底層已經(jīng)存在,獲取之;如果不存在,就創(chuàng)建之
    • IPC_EXCL單獨(dú)使用沒(méi)有意義,通常要搭配起來(lái)IPC_CREAT | IPC_EXCL,如果底層不存在,就創(chuàng)建,并返回;如果底層存在就出錯(cuò)返回。這樣的意義在于 如果調(diào)用成功,得到的一定是一個(gè)全新的共享內(nèi)存。

返回值:成功后,將返回有效的共享內(nèi)存標(biāo)識(shí)符。失敗了,返回-1,并設(shè)置errno錯(cuò)誤碼。

💛 控制共享內(nèi)存

手動(dòng)查看與手動(dòng)刪除

ipcs -m 查看ipc資源,不帶選項(xiàng)默認(rèn)查看消息隊(duì)列(-q)、共享內(nèi)存(-m)、信號(hào)量(-s)
ipcrm -m + shmid //刪除共享內(nèi)存

system V IPC資源,生命周期隨內(nèi)核!所以我們要手動(dòng) / 自動(dòng)刪除,那怎么樣自動(dòng)刪除呢?

💛 控制共享內(nèi)存

#include <sys/ipc.h> 
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);

參數(shù):

  • cmd:設(shè)置IPC_RMID就行,IPC_RMID:即便是有進(jìn)程和當(dāng)下的shm掛接,依舊刪除共享內(nèi)存(強(qiáng)大)
  • buf:這就是描述共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)啊!
    在這里插入圖片描述
    返回值:失敗返回-1,成功返回0
💛 掛接和去關(guān)聯(lián)

attach 掛接 ——

#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmaddr:掛接到什么位置,我們也不知道,給NULL,讓操作系統(tǒng)來(lái)設(shè)置
  • shmflg: 給0

最重要的是返回值

  • 這個(gè)地址一定是虛擬地址,類(lèi)似malloc返回申請(qǐng)到的起始地址
  • 失敗返回-1,并設(shè)置錯(cuò)誤碼

detach 去關(guān)聯(lián) ——

int shmdt(const void *shmaddr);
  • shmaddr:shmat返回的地址

注意:去關(guān)聯(lián),不是釋放共性?xún)?nèi)存,而是取消當(dāng)前進(jìn)程和共享內(nèi)存的關(guān)系,本質(zhì)是去掉進(jìn)程和物理內(nèi)存構(gòu)建映射關(guān)系的頁(yè)表項(xiàng)去掉

返回值:成功返回0,失敗返回-1

💛 shmid 和 key

只有創(chuàng)建的時(shí)候用key,大部分用戶(hù)訪問(wèn)共享內(nèi)存,都用的是shmid(用戶(hù)層)

💦共享內(nèi)存的進(jìn)程間通信

comm.h

#pragma one#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "log.hpp"using namespace std;//不推薦#define PATH_NAME "/home/ljj"
#define PROJ_ID 0x66

server.c

  1. 創(chuàng)建公共的key

  2. 創(chuàng)建共享內(nèi)存 - 建議創(chuàng)建一個(gè)全新的共享內(nèi)存:因?yàn)槭峭ㄐ诺陌l(fā)起者
    帶選項(xiàng)IPC_CREAT | IPC_EXCL若和系統(tǒng)中已經(jīng)存在的ID沖突,則出錯(cuò)返回;
    注意到其中權(quán)限perm是0,那也可以設(shè)置一下

    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); 
    

    在這里插入圖片描述

  3. 將指定的共享內(nèi)存,掛接到自己的地址空間上

  4. 將指定的共享內(nèi)存,從自己的地址空間去關(guān)聯(lián)

  5. 刪除共享內(nèi)存

#include "comm.hpp"string TransToHex(key_t k)
{char buffer[32];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}int main()
{//1.創(chuàng)建公共的key值key_t k = ftok(PATH_NAME, PROJ_ID);assert(k != -1);Log("create key done", Debug) << "server key : " << TransToHex(k) << endl;//2. 創(chuàng)建共享內(nèi)存  - 建議創(chuàng)建一個(gè)全新的共享內(nèi)存:因?yàn)槭峭ㄐ诺陌l(fā)起者int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);if(shmid == -1){perror("shmget");exit(1);}Log("creat shm done", Debug) << "shmid : " << shmid << endl;sleep(10);//3.將指定的共享內(nèi)存,掛接到自己的地址空間上char *shmaddr = (char*)shmat(shmid, nullptr, 0);Log("attach shm done", Debug) << "shmid : " << shmid << endl;sleep(10); //這里就是通信的代碼//4.將指定的共享內(nèi)存,從自己的地址空間去關(guān)聯(lián)int n = shmdt(shmaddr);assert(n != -1);(void)n;Log("detach shm done", Debug) << "shmid : " << shmid << endl;sleep(10); //5.刪除共享內(nèi)存n = shmctl(shmid, IPC_RMID, nullptr);assert(n != -1);(void)n;Log("delete shm done", Debug) << "shmid : " << shmid << endl;return 0;
}

關(guān)于申請(qǐng)共享內(nèi)存的大小size,我們說(shuō)建議是4KB的整數(shù)倍,因?yàn)楣蚕韮?nèi)存在內(nèi)核中申請(qǐng)的基本單位是頁(yè)(內(nèi)存頁(yè)),4KB。如果我申請(qǐng)4097Byte大小的空間,內(nèi)核會(huì)向上取整給我4096* 2Byte,誒?那我監(jiān)視到的↑怎么還是4097啊!雖然在底層申請(qǐng)到的是4096*2,但不會(huì)多給你,這樣也可能引起錯(cuò)誤~

client.c

  • 只需獲取共享內(nèi)存;不用刪除
#include "comm.hpp"int main()
{key_t k = ftok(PATH_NAME, PROJ_ID);if(k < 0){Log("create key failed", Error) << "client key : " << k << endl;exit(1);}Log("create key done", Debug) << "client key : " << k << endl;//獲取共享內(nèi)存int shmid = shmget(k, SHM_SIZE, IPC_CREAT);if(shmid < 0){Log("create shm failed", Error) << "client key : " << k << endl;exit(2);}Log("attach shm success", Error) << "client key : " << k << endl;sleep(10);//掛接地址char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(shmaddr == nullptr){Log("attach shm failed", Error) << "client key : " << k << endl;exit(3);}Log("attach shm success", Error) << "client key : " << k << endl;sleep(10);//使用//去關(guān)聯(lián)int n = shmdt(shmaddr);assert(n != -1);Log("datach shm success", Error) << "client key : " << k << endl;sleep(10);//你只管用,不需要?jiǎng)h除共享內(nèi)存return 0;
}

效果展示:
寫(xiě)一個(gè)命令行腳本來(lái)監(jiān)視共享內(nèi)存 ——

while :; do ipcs -m; echo "_________________________________________________________________"; sleep 1; done

注意觀察nattch這個(gè)參數(shù)的變化:0->1->2->1->0

請(qǐng)?zhí)砑訄D片描述

上面的框架都搭建好了之后,接下來(lái)就是通信部分:
1??客戶(hù)端不斷向共享內(nèi)存寫(xiě)入數(shù)據(jù):

//client將共享內(nèi)存看成一個(gè)char類(lèi)型的buffer
char a = 'a';
for(; a <= 'z'; a++)
{//每一次都想共享內(nèi)存shmaddr的起始地址snprintf(shmaddr, SHM_SIZE - 1,\"hello server, 我是其他進(jìn)程, 我的pid: %d, inc: %c\n",\getpid(), a);sleep(2);
}

2??服務(wù)端不斷讀取共享內(nèi)存當(dāng)中的數(shù)據(jù)并輸出:

//將共享內(nèi)存當(dāng)成一個(gè)大字符串
for(;;)
{printf("%s\n", shmaddr);sleep(1);
}

結(jié)果如下:
在這里插入圖片描述

ps:我們發(fā)現(xiàn)即使我們沒(méi)有向server端發(fā)消息,server也是不斷的在讀取信息的

💦共享內(nèi)存與管道進(jìn)行對(duì)比

共享內(nèi)存是所有進(jìn)程間通信方式中最快的一種通信方式。

在這里插入圖片描述
將一個(gè)文件從一個(gè)進(jìn)程傳輸?shù)搅硪粋€(gè)進(jìn)程需要進(jìn)行四次拷貝操作:

我們?cè)賮?lái)看看共享內(nèi)存通信:

在這里插入圖片描述
鍵盤(pán)寫(xiě)入shm,另一端可以直接獲取到,哪里還需要什么拷貝?最多兩次拷貝(鍵盤(pán)輸入一次,輸出到外設(shè)一次)

💦共享內(nèi)存歸屬誰(shuí)

共享內(nèi)存的區(qū)域是在OS內(nèi)核?還是在用戶(hù)空間?

  • 用戶(hù)空間!

其中文本、初始化數(shù)據(jù)區(qū)、未初始化數(shù)據(jù)區(qū)、堆、棧、環(huán)境變量、命令行參數(shù)、再 往上就是1GOS內(nèi)核,其中剩余3G都是用戶(hù)自己支配的

用戶(hù)空間:不用經(jīng)過(guò)系統(tǒng)調(diào)用,直接進(jìn)行訪問(wèn)!

在這里插入圖片描述

  • 所以雙方進(jìn)程如果要進(jìn)行通信,直接進(jìn)行內(nèi)存級(jí)的讀和寫(xiě)(減少了許多拷貝)

那為什么之前將的pipe和fifo都要通過(guò)read、write進(jìn)行通信,為什么呢?

因?yàn)楣艿离p方看到的資源都屬于內(nèi)核級(jí)的文件,我們無(wú)權(quán)直接進(jìn)行訪問(wèn),必須調(diào)用系統(tǒng)接口

💦共享內(nèi)存的特征

  • 共享內(nèi)存的生命周期隨內(nèi)核
  • 共享內(nèi)存是所有進(jìn)程中速度最快的,只需要經(jīng)過(guò)頁(yè)表映射,不需來(lái)回拷貝(不經(jīng)過(guò)OS)
  • 共享內(nèi)存沒(méi)有提供訪問(wèn)控制,讀寫(xiě)雙方根本不知道對(duì)方的存在,會(huì)帶來(lái)并發(fā)問(wèn)題

🌈消息隊(duì)列(了解)

嚴(yán)重過(guò)時(shí):接口與文件不對(duì)應(yīng)

創(chuàng)建消息隊(duì)列,與創(chuàng)建共享內(nèi)存極其相似:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);

刪除消息隊(duì)列:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);

我們可以通過(guò)key找到同一個(gè)共享內(nèi)存。

我們發(fā)現(xiàn)共享內(nèi)存、消息隊(duì)列、信號(hào)量的 ——

  • 接口都類(lèi)似
  • 數(shù)據(jù)結(jié)構(gòu)的第一個(gè)結(jié)構(gòu)類(lèi)型struct ipc_perm是完全一致的!

我們由shmid申請(qǐng)到的都是01234… 大膽推測(cè),在內(nèi)核中,所有的ipc資源都是通過(guò)數(shù)組組織起來(lái)的。可是描述它們的結(jié)構(gòu)體類(lèi)型并不相同啊?但是~ System V標(biāo)準(zhǔn)的IPC資源,xxxid_ds結(jié)構(gòu)體的第一個(gè)成員都是ipc_perm都是一樣的。

📢寫(xiě)在最后

應(yīng)該是我寫(xiě)過(guò)最長(zhǎng)的一篇博客了
請(qǐng)?zhí)砑訄D片描述

http://m.aloenet.com.cn/news/30611.html

相關(guān)文章:

  • 現(xiàn)在pc網(wǎng)站的標(biāo)準(zhǔn)一般是做多大長(zhǎng)沙網(wǎng)站優(yōu)化推廣
  • 網(wǎng)站建設(shè)gzdlzgg北京網(wǎng)絡(luò)網(wǎng)站推廣
  • 贛州網(wǎng)站建設(shè)jxgzg3百度導(dǎo)航如何設(shè)置公司地址
  • 微網(wǎng)站 報(bào)價(jià)重慶百度seo
  • 做淘寶客網(wǎng)站用什么系統(tǒng)谷歌瀏覽器安卓版
  • 聊城手機(jī)網(wǎng)站建設(shè)公司seo技術(shù)306
  • 具有營(yíng)銷(xiāo)價(jià)值好的網(wǎng)站武漢seo優(yōu)化代理
  • 網(wǎng)站建設(shè)是好的競(jìng)價(jià)賬戶(hù)托管外包
  • 有沒(méi)有專(zhuān)業(yè)做二維碼連接網(wǎng)站在營(yíng)銷(xiāo)技巧第三季
  • 圖片制作視頻的appseo宣傳
  • 如何提高網(wǎng)站百度權(quán)重如何去除痘痘有效果
  • 鄭州官網(wǎng)網(wǎng)絡(luò)營(yíng)銷(xiāo)外包上海網(wǎng)站seo策劃
  • 平度疫情最新消息成都seo推廣
  • 品牌網(wǎng)站建設(shè)有哪些內(nèi)容吳中seo網(wǎng)站優(yōu)化軟件
  • 網(wǎng)站長(zhǎng)春網(wǎng)站建設(shè)惠州網(wǎng)絡(luò)推廣平臺(tái)
  • 網(wǎng)站制作怎樣做背景贛州seo唐三
  • 網(wǎng)站建設(shè)公司相關(guān)資質(zhì)精準(zhǔn)客源app
  • 做百度網(wǎng)上搜索引擎推廣最好網(wǎng)站杭州seo網(wǎng)站排名優(yōu)化
  • 攝影網(wǎng)站建設(shè)內(nèi)容網(wǎng)站運(yùn)營(yíng)一個(gè)月多少錢(qián)
  • 怎么做網(wǎng)站多少錢(qián)蘇州網(wǎng)站制作推廣
  • 做網(wǎng)站劃算還是做app劃算營(yíng)銷(xiāo)型網(wǎng)站和普通網(wǎng)站
  • 做模版網(wǎng)站打開(kāi)百度網(wǎng)站首頁(yè)
  • 優(yōu)化網(wǎng)站哪家好競(jìng)價(jià)排名是按照什么來(lái)計(jì)費(fèi)的
  • 視頻網(wǎng)站seo實(shí)戰(zhàn)免費(fèi)私人網(wǎng)站建設(shè)軟件
  • 中山建設(shè)監(jiān)理有限公司 網(wǎng)站如何提高網(wǎng)站的搜索排名
  • wordpress上傳函數(shù)四川seo哪里有
  • 很好用的炫酷WordPress主題上海seo顧問(wèn)
  • dede新聞網(wǎng)站源碼營(yíng)銷(xiāo)課程培訓(xùn)視頻
  • 晉州做網(wǎng)站響應(yīng)式模版移動(dòng)優(yōu)化
  • 建材公司網(wǎng)站建設(shè)方案點(diǎn)擊器 百度網(wǎng)盤(pán)