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

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

無錫網(wǎng)絡(luò)營(yíng)銷推廣軟件蘭州seo

無錫網(wǎng)絡(luò)營(yíng)銷推廣軟件,蘭州seo,wordpress解決大型訪問,建論壇網(wǎng)站目錄 一、進(jìn)程間通信介紹 二、管道 1.什么是管道(pipe) 2.重定向和管道 (1)為什么要有管道的存在 (2)重定向和管道的區(qū)別 3.匿名管道 (1)匿名管道原理 (2&…

目錄

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

二、管道

1.什么是管道(pipe)

2.重定向和管道

(1)為什么要有管道的存在

(2)重定向和管道的區(qū)別

3.匿名管道

(1)匿名管道原理?

(2)站在文件描述符角度理解匿名管道

(3)創(chuàng)建匿名管道

(4)匿名管道讀寫規(guī)則

(5)匿名管道特點(diǎn)

(6)匿名管道4種特殊情況

(7)匿名管道大小

4.命名管道

(1)命名管道原理?

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

(3)命名管道的數(shù)據(jù)不會(huì)刷新到磁盤

5.匿名管道和命名管道的區(qū)別

三、System V IPC

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

2.共享內(nèi)存

(1)原理?

(2)步驟

(3)函數(shù)?

shmget

shmctl

shmat

shmdt

(4)使用

3.共享內(nèi)存和管道區(qū)別?

四、消息隊(duì)列?

1.原理?

2.數(shù)據(jù)結(jié)構(gòu)

3.步驟

4.函數(shù)?

(1)msgget

(2)msgctl

(3)msgsnd

(4)msgrcv

五、信號(hào)量

1.原理?

2.數(shù)據(jù)結(jié)構(gòu)

3.函數(shù)

(1)semget

(2)semctl

(3)semop

六、System V IPC總結(jié)


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

? ? ? ? 之前學(xué)習(xí)的進(jìn)程,都是各自運(yùn)行,互不干擾,進(jìn)程之間沒有協(xié)同。然而有許多場(chǎng)景下是需要進(jìn)程之間相互協(xié)同的,由于進(jìn)程是程序員寫的,因此進(jìn)程之間的協(xié)同本質(zhì)上就是程序員之間的協(xié)同,比如一個(gè)程序員從數(shù)據(jù)庫(kù)里面拿數(shù)據(jù),另一個(gè)程序員要把從數(shù)據(jù)庫(kù)里面拿到的數(shù)據(jù)進(jìn)行格式化,寫成特定格式,還有一個(gè)程序員根據(jù)格式化的數(shù)據(jù)進(jìn)行統(tǒng)計(jì),如果把這些工作量當(dāng)成意見工作去處理的話,如果其中這三個(gè)環(huán)節(jié)有任何一個(gè)環(huán)節(jié)出錯(cuò)了,那么這個(gè)工作就進(jìn)行不下去了,需要逐一去排查到底是哪個(gè)環(huán)節(jié)出錯(cuò)了,耗時(shí)久且效率低。

? ? ? ? 因此把這個(gè)工作可以分為3個(gè)部分,分別讓3個(gè)不同的進(jìn)程去做:1個(gè)進(jìn)程從數(shù)據(jù)庫(kù)拿數(shù)據(jù),1個(gè)進(jìn)程做數(shù)據(jù)格式化,1個(gè)進(jìn)程做數(shù)據(jù)分析。這就做到了在業(yè)務(wù)層面上用進(jìn)程進(jìn)行解耦。一旦拿數(shù)據(jù)有問題就去找拿數(shù)據(jù)的進(jìn)程,一旦格式化有問題就去找格式化的進(jìn)程,一旦數(shù)據(jù)分析有問題就去找數(shù)據(jù)分析的進(jìn)程。業(yè)務(wù)層面上的解耦能夠增加代碼的可維護(hù)性,這就是進(jìn)程之間的協(xié)同。比如過濾出文件中含字母'i'的行:

cat fdProcess.c運(yùn)行起來就是一個(gè)進(jìn)程,核心工作只是打印數(shù)據(jù),用grep來過濾含有字母'i'的行,數(shù)據(jù)源是從上一個(gè)進(jìn)程cat fdProcess.c通過管道來交給grep的。這就叫做協(xié)同。

就算是父子進(jìn)程,共享了進(jìn)程的代碼和數(shù)據(jù),寫的時(shí)候都必須分開,用寫時(shí)拷貝來寫。兩個(gè)相互獨(dú)立的進(jìn)程,交互數(shù)據(jù),成本很高,各自連對(duì)方保存數(shù)據(jù)的地址空間都看不到,因?yàn)楠?dú)立的進(jìn)程使用獨(dú)立的進(jìn)程地址空間,頁(yè)表映射到不同的物理內(nèi)存,所以看不到對(duì)方的數(shù)據(jù),因此要完成進(jìn)程間通信,不能只在應(yīng)用層解決,必須也要操作系統(tǒng)參與進(jìn)來,要讓操作系統(tǒng)設(shè)計(jì)通信方式。

通信的本質(zhì)就是傳遞數(shù)據(jù),這些數(shù)據(jù)需要一個(gè)進(jìn)程向公共資源里面去放,另一個(gè)進(jìn)程從公共資源向外拿,而公共資源還需要有暫存數(shù)據(jù)的能力。這個(gè)公共資源肯定不屬于這兩個(gè)進(jìn)程,因?yàn)檫M(jìn)程具有獨(dú)立性,如果這個(gè)公共資源是進(jìn)程A的,那么進(jìn)程B是看不到的:

從上圖可以看出進(jìn)程間通信有以下3種方式,目的是為了讓不同的進(jìn)程看到同一份資源:

  • 管道
  • System V進(jìn)程間通信
  • POSIX進(jìn)程間通信?

同時(shí)需要先了解以下概念:

數(shù)據(jù)傳輸:一個(gè)進(jìn)程需要將它的數(shù)據(jù)發(fā)送給另一個(gè)進(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)改變

二、管道

1.什么是管道(pipe)

管道是Unix最古老的進(jìn)程間通信的形式。把從一個(gè)進(jìn)程連接到另一個(gè)進(jìn)程的數(shù)據(jù)流稱為管道,Linux 管道使用豎線'|'連接多個(gè)命令,這被豎線'|'稱為管道符。

當(dāng)在兩個(gè)命令之間設(shè)置管道時(shí),管道符'|'左邊命令的輸出就變成了右邊命令的輸入。只要第一個(gè)命令向標(biāo)準(zhǔn)輸出寫入,而第二個(gè)命令是從標(biāo)準(zhǔn)輸入讀取,那么這兩個(gè)命令就可以形成一個(gè)管道。大部分的 Linux 命令都可以用來形成管道。

如下所示,對(duì)于命令cat fdProcess.c|grep -i 'i',管道符'|'之前的進(jìn)程cat fdProcess.c是標(biāo)準(zhǔn)輸入進(jìn)程,管道符'|'之后的進(jìn)程grep -i 'i'是標(biāo)準(zhǔn)輸出進(jìn)程,第一個(gè)命令向標(biāo)準(zhǔn)輸出寫入,第二個(gè)命令是從標(biāo)準(zhǔn)輸入讀取,這兩個(gè)命令形成了管道,管道作用于內(nèi)核。

如果沒有管道,那么這兩條命令就得分兩次執(zhí)行。因此用管道執(zhí)行也能達(dá)到同樣的效果。對(duì)于一些備份壓縮復(fù)制需求的命令就可以避免創(chuàng)建臨時(shí)文件。?

管道特點(diǎn):

  • 命令的語(yǔ)法緊湊并且使用簡(jiǎn)單。
  • 管道將多個(gè)命令串聯(lián)到一起完成復(fù)雜任務(wù)。
  • 從管道輸出的標(biāo)準(zhǔn)錯(cuò)誤會(huì)混合到一起。

2.重定向和管道

(1)為什么要有管道的存在

既然有重定向,為什么還要有管道呢?比如如下命令使用重定向?qū)⒖蓤?zhí)行程序process1的輸出都放入file中:

process1 > file

但是如果想讓可執(zhí)行程序process1 的輸出傳遞到可執(zhí)行程序process2呢?需要:

process1 > temp && process2 < temp

這個(gè)命令做了3步:

  • 運(yùn)行名為process1?
  • 將輸出保存到名為temp的文件中
  • 運(yùn)行名為的程序process2,假裝用戶在鍵盤上輸入temp的內(nèi)容。

有沒有發(fā)現(xiàn)這樣做很麻煩,既要?jiǎng)?chuàng)建臨時(shí)文件,又要用戶在鍵盤上輸入呢,但是管道就很簡(jiǎn)單呀:

process1 | process2

的效果和命令process1 > temp && process2 < temp的作用是一樣的。

(2)重定向和管道的區(qū)別

管道也有重定向的作用,因?yàn)樗淖兞藬?shù)據(jù)的輸入輸出方向。沖重定向使用">"將文件和命令連接起來,用文件來接收命令的輸出,而管道使用"I"將命令和命令連接起來,用第二個(gè)命令來接收第一個(gè)命令的輸出。

使用重定向一定要小心一些,如果連續(xù)鍵入如下兩條命令:

cd /usr/bin
ls > less

第一條命令將當(dāng)前目錄切換到了大多數(shù)程序所存放的目錄,第二條命令是告訴 Shell 用 ls 命令的輸出重寫文件 less。因?yàn)?/usr/bin 目錄已經(jīng)包含了名稱為 less的文件,第二條命令用 ls 輸出的文本重寫了 less 程序,因此破壞了文件系統(tǒng)中的 less 程序,這就破壞了less文件。這是使用重定向操作符錯(cuò)誤重寫文件的一個(gè)教訓(xùn),所以在使用重定向時(shí)要謹(jǐn)慎。

管道分為匿名管道和命名管道。

3.匿名管道

(1)匿名管道原理?

匿名管道僅限于本地父子進(jìn)程之間通信,不支持跨網(wǎng)絡(luò)之間的兩個(gè)進(jìn)程之間的通信。

進(jìn)程在操作文件時(shí),通過文件描述符找到文件,如果需要讀文件就直接執(zhí)行讀方法。使用fork創(chuàng)建子進(jìn)程之后,那么子進(jìn)程就擁有了自己的PCB,父進(jìn)程指向的struct file文件描述符表結(jié)構(gòu)也需要給子進(jìn)程拷貝一份。

?這是因?yàn)?#xff1a;

  • file_struct結(jié)構(gòu)是屬于進(jìn)程的,因?yàn)閒ile_struct能夠讓進(jìn)程看到已經(jīng)打開了多少個(gè)文件以及文件之間的關(guān)系,因此file_struct是屬于進(jìn)程的。file_struct屬于進(jìn)程,那么它一定屬于父進(jìn)程,在創(chuàng)建子進(jìn)程的時(shí)候,也必須為子進(jìn)程復(fù)制這份file_struct結(jié)構(gòu)。因?yàn)檫M(jìn)程具有獨(dú)立性,所以內(nèi)核數(shù)據(jù)結(jié)構(gòu)也必須保持獨(dú)立。
  • 如果讓子進(jìn)程也看到了父進(jìn)程的文件了,那么父進(jìn)程的文件進(jìn)行讀寫時(shí),緩沖區(qū)也被子進(jìn)程看到了,這就沒有做好進(jìn)程獨(dú)立性。

因此操作系統(tǒng)會(huì)將這個(gè)結(jié)構(gòu)給子進(jìn)程也拷貝一份:

基于文件的通信方式就叫做管道。進(jìn)程、struct_file、緩沖區(qū)、操作方法等都是操作系統(tǒng)提供的,文件不屬于進(jìn)程,屬于操作系統(tǒng)。父進(jìn)程先打開文件,讓子進(jìn)程繼承,雖然結(jié)構(gòu)上互相獨(dú)立,但它們指向同一個(gè)文件,一個(gè)向文件寫,另一個(gè)從文件讀,兩個(gè)進(jìn)程看到了同一份公共資源,這就滿足了進(jìn)程通信的前提。

(2)站在文件描述符角度理解匿名管道

  • 父進(jìn)程創(chuàng)建管道

管道可以看做文件的內(nèi)核緩沖區(qū),父進(jìn)程創(chuàng)建管道時(shí),分別以讀方式和寫方式打開同一文件:

  • ?父進(jìn)程fork出子進(jìn)程

?當(dāng)父進(jìn)程創(chuàng)建出子進(jìn)程后,父進(jìn)程的所有文件描述符表信息會(huì)被子進(jìn)程繼承,雖然父子進(jìn)程各自擁有獨(dú)立的文件描述符,但是內(nèi)容是一樣的,所以父子進(jìn)程都可以看到曾經(jīng)打開的讀端和寫端進(jìn)行讀寫,不過管道只能單向通信,只能有一個(gè)讀端,一個(gè)寫端。

所以父進(jìn)程一開始就有兩個(gè)文件描述符,一個(gè)讀端,一個(gè)寫端,這樣子進(jìn)程繼承復(fù)制了父進(jìn)程的文件描述符后,也有讀端和寫端。否則如果父進(jìn)程一開始只有讀端,沒有寫端,那么子進(jìn)程也只有讀端,沒有寫端,那么兩個(gè)讀端是不能進(jìn)行讀寫的。

  • ?父進(jìn)程關(guān)閉讀端(寫端),子進(jìn)程關(guān)閉寫端(讀端)

?至于父子進(jìn)程誰(shuí)關(guān)閉讀端,誰(shuí)關(guān)閉寫端,取決于父進(jìn)程讀還是子進(jìn)程讀,現(xiàn)在來看一下父進(jìn)程寫,子進(jìn)程讀的情況,現(xiàn)在關(guān)閉父進(jìn)程的讀端和子進(jìn)程的寫端:

(3)創(chuàng)建匿名管道

第一步:父進(jìn)程使用pipe函數(shù)來創(chuàng)建管道

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

參數(shù):pipefd文件描述符數(shù)組,元素個(gè)數(shù)為2,是輸出型參數(shù),通過這個(gè)參數(shù)讀取到打開的兩個(gè)文件描述符。其中pipefd[0]為讀操作,pipefd[1]為寫操作,且順序不能顛倒。

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

現(xiàn)在來創(chuàng)建一個(gè)管道:

#include<stdio.h>
#include<unistd.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗{perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);return 0;
}

執(zhí)行結(jié)果如下:

?可以看到文件描述符分別為3和4,因?yàn)?、1、2都被標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤占用了:

第二步:父進(jìn)程fork出子進(jìn)程

#include<stdio.h>
#include<unistd.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{}//父進(jìn)程return 0;
}

第3步:創(chuàng)建單向信道

現(xiàn)在如果想讓父進(jìn)程讀,子進(jìn)程寫,那么就要關(guān)閉父進(jìn)程的寫端和子進(jìn)程的讀端,即關(guān)閉父進(jìn)程的寫文件描述符和子進(jìn)程的讀文件描述符。為了讓子進(jìn)程關(guān)閉讀文件描述符后不要繼續(xù)向后執(zhí)行,使用eixt函數(shù)來終止。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符return 0;
}

現(xiàn)在已經(jīng)建立了父子進(jìn)程,并且父子進(jìn)程都看到了同一份資源?,F(xiàn)在讓子進(jìn)程寫入,需要調(diào)用write方法,讓父進(jìn)程讀取,需要調(diào)用read方法,write和read方法的使用請(qǐng)參考文章【Linux】-- 基礎(chǔ)IO和動(dòng)靜態(tài)庫(kù)第一章節(jié)第1節(jié)的內(nèi)容?第一章節(jié)第1節(jié)的內(nèi)容。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符const char *string_write = "lunch ";while(1){write(pipefd[1],string_write,strlen(string_write));//子進(jìn)程向文件緩沖區(qū)寫,pipe只要有緩沖區(qū)就一直寫入}close(pipefd[1]);exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符while(1){sleep(1);char string_read[64] ={0};size_t readLength = read(pipefd[0],string_read,sizeof(string_read));//父進(jìn)程從文件緩沖區(qū)讀,pipe只要有緩沖區(qū)就一直讀if(readLength == 0)//讀到內(nèi)容為空{(diào)printf("child quit...\n");break;}else if(readLength > 0)//讀到了正常內(nèi)容{string_read[readLength] = 0;printf("child write# %s\n",string_read);}else//讀出錯(cuò){printf("read error...\n");break;}close(pipefd[0]);}return 0;
}

?可以看到執(zhí)行結(jié)果如下,子進(jìn)程寫入,父進(jìn)程讀取:

?對(duì)于字節(jié)流,只要緩沖區(qū)有數(shù)據(jù),就把緩沖區(qū)的所有數(shù)據(jù)全部讀出來,一次讀取一個(gè)字節(jié)。

(4)匿名管道讀寫規(guī)則

pipe2函數(shù)與pipe函數(shù)類似,也是用于創(chuàng)建匿名管道,其函數(shù)原型如下:

int pipe2(int pipefd[2], int flags);

對(duì)于flags:?

  • 當(dāng)沒有數(shù)據(jù)可讀時(shí)

O_NONBLOCK disable:read調(diào)用阻塞,即進(jìn)程暫停執(zhí)行,一直等到有數(shù)據(jù)來到為止。
O_NONBLOCK enable:read調(diào)用返回-1,errno值為EAGAIN。

  • ?當(dāng)管道滿的時(shí)候

O_NONBLOCK disable: write調(diào)用阻塞,直到有進(jìn)程讀走數(shù)據(jù)
O_NONBLOCK enable:調(diào)用返回-1,errno值為EAGAIN

  • 如果所有管道寫端對(duì)應(yīng)的文件描述符被關(guān)閉,則read返回0
  • 如果所有管道讀端對(duì)應(yīng)的文件描述符被關(guān)閉,則write操作會(huì)產(chǎn)生信號(hào)SIGPIPE,進(jìn)而可能導(dǎo)致write進(jìn)程退出
  • 當(dāng)要寫入的數(shù)據(jù)量<=PIPE_BUF時(shí),linux將保證寫入的原子性。
  • 當(dāng)要寫入的數(shù)據(jù)量>PIPE_BUF時(shí),linux將不再保證寫入的原子性。

(5)匿名管道特點(diǎn)

  • 只能用于具有共同祖先的進(jìn)程之間進(jìn)行通信;通常,一個(gè)管道由一個(gè)進(jìn)程創(chuàng)建,然后該進(jìn)程調(diào)用fork,父、子進(jìn)程之間就可用該管道通信(具有親緣關(guān)系的進(jìn)程,祖孫進(jìn)程也可以)
  • 管道提供流式服務(wù),原子性寫入(讀端讀取的數(shù)據(jù)是任意的,底層沒有對(duì)數(shù)據(jù)做明確分割,報(bào)文段不定,因此是流式服務(wù))
  • 父子進(jìn)程退出,管道文件釋放,所以管道的生命周期隨進(jìn)程
  • 內(nèi)核會(huì)對(duì)管道操作進(jìn)行同步與互斥
  • 管道是半雙工的,數(shù)據(jù)只能向一個(gè)方向流動(dòng);需要雙方通信時(shí),需要建立起兩個(gè)管道

(6)匿名管道4種特殊情況

  • 讀端不讀或者讀的慢,寫端要等讀端
  • 讀端關(guān)閉,寫端收到SIGPIPE信號(hào)直接終止
  • 寫端不寫或?qū)懙穆?#xff0c;讀端要等寫端
  • 寫端關(guān)閉,讀端讀完pipe內(nèi)部的數(shù)據(jù)然后再讀,會(huì)讀到0,表明讀到文件結(jié)尾

(7)匿名管道大小

?如果讓子進(jìn)程無限循環(huán)每次往管道里寫一個(gè)字符,并且計(jì)數(shù),父進(jìn)程從管道里面不讀取數(shù)據(jù),當(dāng)計(jì)數(shù)不再增長(zhǎng)時(shí),計(jì)數(shù)值就為管道的大小:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符int count = 0;while(1){write(pipefd[1],"a",1);count++;printf("count:%d\n",count);}close(pipefd[1]);exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符while(1)//父進(jìn)程不讀取{sleep(1);}return 0;
}

?運(yùn)行結(jié)果如下,從1打印到65536:

?這說明管道大小為65536B=64KB。這也說明了如果寫端向管道寫滿數(shù)據(jù)以后,那么寫端就不寫了,等待讀端讀;同理,如果讀端把管道數(shù)據(jù)讀完了,管道沒數(shù)據(jù),那么讀端就不讀了,等待寫端寫。

?管道在被寫端寫滿以后,讀端要拿走數(shù)據(jù),如果一次拿走4KB,寫端才會(huì)寫,否則不會(huì)觸發(fā)寫端去寫,為什么是4KB呢?讓父進(jìn)程讀取的時(shí)候,存放數(shù)據(jù)的數(shù)組大小從1KB開始向上遞增到4KB的時(shí)候,寫端才寫:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符const char *string_write = "lunch ";int count = 0;while(1){//write(pipefd[1],string_write,strlen(string_write));//子進(jìn)程向文件緩沖區(qū)寫,pipe只要有緩沖區(qū)就一直寫入write(pipefd[1],"a",1);count++;printf("count:%d\n",count);}close(pipefd[1]);exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符while(1){sleep(3);char string_read[1024*4+1] ={0};//按照1024*1   1024*2   1024*3   1024*4向上遞增size_t readLength = read(pipefd[0],string_read,sizeof(string_read));//父進(jìn)程從文件緩沖區(qū)讀,pipe只要有緩沖區(qū)就一直讀printf("readLength = %d\n",readLength);string_read[readLength] = 0;printf("father take:%c\n",string_read[0]);}return 0;
}

可以看到,管道寫入字符的計(jì)數(shù)一開始增加到了65536B,父進(jìn)程讀走4KB之后,子進(jìn)程繼續(xù)寫,每寫一次,count計(jì)數(shù)就會(huì)++ :

?為什么讀走4KB的時(shí)候,寫端才寫,而讀走1KB 2KB 3KB時(shí)不寫呢?這是因?yàn)橐WC寫入和讀取的原子性:假如還沒讀夠4KB,就把寫端喚醒了,那么寫端就要來寫了,這就變成了,寫端在寫的同時(shí),讀端要來讀,這就違背了管道半雙工通信,不能同時(shí)讀寫的原則。同理,如果寫端寫的特別慢,讀端讀的特別快,當(dāng)緩沖區(qū)沒有數(shù)據(jù)時(shí),會(huì)等待數(shù)據(jù)寫入進(jìn)去后,讀端再讀 。因此要保證同步。

4.命名管道

(1)命名管道原理?

?匿名管道用于有血緣關(guān)系的進(jìn)程間通信,那么對(duì)于沒有血緣關(guān)系的進(jìn)程,他們之間如何通信呢?這就要用到命名管道,命名管道是一種特殊的文件,使用FIFO(First In First Out)來進(jìn)行通信。

?如何讓兩個(gè)沒有血緣關(guān)系的不相干的進(jìn)程看到操作系統(tǒng)提供的同一份資源?對(duì)于文件系統(tǒng)來說當(dāng)進(jìn)程A把磁盤文件打開,向磁盤里面寫數(shù)據(jù),寫完之后關(guān)閉這個(gè)磁盤文件,進(jìn)程B再把這個(gè)磁盤文件打開并讀取數(shù)據(jù):

但是這樣做有點(diǎn)慢,因?yàn)檫M(jìn)程A再內(nèi)存中打開這個(gè)文件,為這個(gè)文件建立內(nèi)存相關(guān)的數(shù)據(jù)結(jié)構(gòu)和緩沖區(qū),進(jìn)程B也在內(nèi)存中打開同一個(gè)文件,這樣就是一個(gè)通過讀的方式打開,一個(gè)通過寫的方式打開,進(jìn)程可以向這個(gè)內(nèi)存文件寫,進(jìn)程B可以從這個(gè)內(nèi)存文件讀,暫時(shí)先不把數(shù)據(jù)刷新到磁盤,否則效率會(huì)降低,這是基于內(nèi)存進(jìn)行數(shù)據(jù)之間的通信,那么A進(jìn)程和B進(jìn)程就可以通過這個(gè)內(nèi)存文件進(jìn)行不相關(guān)的進(jìn)程間的通信。

不相關(guān)的A進(jìn)程和B進(jìn)程是如何看到同一份資源的呢?路徑+文件名能唯一指定一個(gè)文件,這樣就能讓進(jìn)程A和進(jìn)程B打開同一份文件?,F(xiàn)在需要1個(gè)文件,同時(shí)滿足:

  • 文件被打開時(shí),數(shù)據(jù)不要被刷新到磁盤上,而是保存臨時(shí)數(shù)據(jù)
  • 這個(gè)文件也必須在磁盤上也有對(duì)應(yīng)的文件名

符合這些條件的只有命名管道。而且這個(gè)文件是有名字的,通過路徑+文件名確定唯一性來做到的:

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

命名管道有兩種創(chuàng)建方式:?

  • 通過mkfifo命令創(chuàng)建
mkfifo name

如創(chuàng)建一個(gè)名為testFifo的管道文件:

?可以看到文件類型為p,p表明這是一個(gè)管道文件。創(chuàng)建了命名管道文件后,就可以通信了:

?echo和cat是兩個(gè)不同的指令,但是運(yùn)行起來是兩個(gè)進(jìn)程,左側(cè)的消息打印到了右側(cè)的屏幕上,一個(gè)進(jìn)程把自己的內(nèi)容寫入到了命名管道文件中,通過命名管道文件把數(shù)據(jù)傳遞給另一個(gè)進(jìn)程。

  • 通過mkfifo函數(shù)創(chuàng)建

mkfifo函數(shù)的作用是生成一個(gè)FIFO的特殊文件,即命名管道?

 #include <sys/types.h>#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);

pathname:文件名

mode:管道的默認(rèn)權(quán)限,可用過umask來設(shè)置

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

現(xiàn)在使用mkfifo函數(shù)創(chuàng)建命名管道,server.c創(chuàng)建管道文件,并給管道文件分配權(quán)限:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>#define fifo_file ./fifo_fileint main()
{if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}return 0;
}

client.c暫時(shí)什么都不做:

#include<stdio.h>int main()
{return 0;
}

Makefile一次生成兩個(gè)可執(zhí)行文件:

.PHONY:all
all:client serverclient:client.cgcc -o $@ $^server:server.cgcc -o $@ $^.PHONY:clean
clean:rm -rf client server fifo_file

編譯后,生成兩個(gè)可執(zhí)行程序:

現(xiàn)在通信想讓client和server可執(zhí)行程序互相傳遞詳細(xì),那么?client和server可執(zhí)行程序運(yùn)行起來就是兩個(gè)進(jìn)程,而且是兩個(gè)毫不相干的進(jìn)程,沒有血緣關(guān)系。

執(zhí)行srver課執(zhí)行程序后,生成fifo_file命名管道,文件類型是p,但是權(quán)限是644,并不是666:

?這是因?yàn)閒ifo文件的參數(shù)mode受系統(tǒng)umask影響,可以查看到Umask的值是2:

?那么可以看出mode = mode & ~umask(666&~002),如果修改umask的值,比如創(chuàng)建命名管道文件時(shí)將umask清0:

server.c

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#define fifo_file "./fifo_file"int main()
{umask(0);//將umask清0if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}int fd = open(fifo_file,O_RDONLY);if(fd < 0){perror("open");return 2;}while(1){char buffer[64] = {0};ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);if(read_length > 0)//讀取成功{buffer[read_length-1] = 0;printf("client # %s\n",buffer);}else if(read_length == 0){printf("client quit\n");}else{perror("read");break;}}close(fd);return 0;
}

這時(shí)可以看到命名管道文件的權(quán)限變成了666:

?對(duì)于client和server進(jìn)程,想讓server讀,client寫,不推薦用c/c++接口,有緩沖區(qū),而系統(tǒng)調(diào)用沒有緩沖區(qū),推薦使用系統(tǒng)調(diào)用接口,client使用系統(tǒng)調(diào)用接收標(biāo)準(zhǔn)輸入并寫入到命名管道文件中:

client.c?

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>#define fifo_file "./fifo_file"//client不需要?jiǎng)?chuàng)建命名管道文件,只需要獲取就可以了
int main()
{int fd = open(fifo_file,O_WRONLY);if(fd < 0){perror("open");return 1;}while(1){printf("請(qǐng)輸入# ");//client的輸入提示fflush(stdout);//刷新一下標(biāo)準(zhǔn)輸出char buffer[64] = {0};//先把數(shù)據(jù)從標(biāo)準(zhǔn)輸入拿到client進(jìn)程內(nèi)部ssize_t read_length = read(0,buffer,sizeof(buffer)-1);if(read_length > 0){buffer[read_length-1] = 0;//拿到了數(shù)據(jù)write(fd,buffer,strlen(buffer));}}close(fd);return 0;
}

?現(xiàn)在運(yùn)行,得先讓server跑起來創(chuàng)建一個(gè)命名管道,然后再運(yùn)行client端,就可以再client端寫入數(shù)據(jù)了:

?從以上就可以看出,對(duì)于兩個(gè)不想管的進(jìn)程,通過命名管道,一個(gè)進(jìn)程把消息發(fā)給了另外一個(gè)進(jìn)程。因此一旦有了命名管道,只需要讓通信雙方進(jìn)程按照文件操作即可。由于命名管道也是基于字節(jié)流的,因此實(shí)際上,信息傳遞的時(shí)候,需要通信雙方定制“協(xié)議”。

現(xiàn)在讓client控制server,讓server去執(zhí)行任務(wù)??梢宰宻erver執(zhí)行程序替換,比如當(dāng)client接收標(biāo)準(zhǔn)輸入寫入到命名管道文件中的字符串為"show"時(shí),就會(huì)執(zhí)行l(wèi)s命令:

server.c

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>
#include<string.h>
#include<sys/wait.h>#define fifo_file "./fifo_file"int main()
{umask(0);if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}int fd = open(fifo_file,O_RDONLY);if(fd < 0){perror("open");return 2;}//業(yè)務(wù)邏輯,進(jìn)行讀寫while(1){char buffer[64] = {0};ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);if(read_length > 0)//讀取成功{buffer[read_length] = 0;if(strcmp(buffer,"show") == 0){printf("the string is show\n");if(fork() == 0){execl("/usr/bin/ls","ls","-l",NULL);//程序替換exit(1);}waitpid(-1,NULL,0);}else{printf("client # %s\n",buffer);}}else if(read_length == 0){printf("client quit\n");}else{perror("read");break;}}close(fd);return 0;
}

client.c

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>#define fifo_file "./fifo_file"//client不需要?jiǎng)?chuàng)建命名管道文件,只需要獲取就可以了
int main()
{int fd = open(fifo_file,O_WRONLY);if(fd < 0){perror("open");return 1;}while(1){printf("請(qǐng)輸入# ");//client的輸入提示fflush(stdout);//刷新一下標(biāo)準(zhǔn)輸出char buffer[64] = {0};//先把數(shù)據(jù)從標(biāo)準(zhǔn)輸入拿到client進(jìn)程內(nèi)部ssize_t read_length = read(0,buffer,sizeof(buffer)-1);if(read_length > 0){buffer[read_length - 1] = 0;//拿到了數(shù)據(jù)write(fd,buffer,strlen(buffer));}}close(fd);return 0;
}

現(xiàn)在運(yùn)行,得先讓server跑起來創(chuàng)建一個(gè)命名管道,然后再運(yùn)行client端,就可以再client端寫入數(shù)據(jù)了,在client輸入"show"之后,server就將ls的內(nèi)容展示出來了:

?可以看到通過命名管道把數(shù)據(jù)從一個(gè)進(jìn)程傳遞給另外一個(gè)進(jìn)程,并且也實(shí)現(xiàn)了讓一個(gè)進(jìn)程控制了另外一個(gè)進(jìn)程去執(zhí)行任務(wù),達(dá)到了進(jìn)程間通信的目的。

(3)命名管道的數(shù)據(jù)不會(huì)刷新到磁盤

假如讓server進(jìn)程每隔20秒讀一次,而client不斷往管道發(fā)消息,那么數(shù)據(jù)只能在管道文件:

server.c

#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>
#include<string.h>
#include<sys/wait.h>#define fifo_file "./fifo_file"int main()
{umask(0);if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}int fd = open(fifo_file,O_RDONLY);if(fd < 0){perror("open");return 2;}//業(yè)務(wù)邏輯,進(jìn)行讀寫while(1){char buffer[64] = {0};sleep(20);//等待20秒再讀ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);if(read_length > 0)//讀取成功{buffer[read_length] = 0;if(strcmp(buffer,"show") == 0){printf("the string is show\n");if(fork() == 0){execl("/usr/bin/ls","ls","-l",NULL);//程序替換exit(1);}waitpid(-1,NULL,0);}else{printf("client # %s\n",buffer);}}else if(read_length == 0){printf("client quit\n");}else{perror("read");break;}}close(fd);return 0;
}

client.c不用修改:

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>#define fifo_file "./fifo_file"//client不需要?jiǎng)?chuàng)建命名管道文件,只需要獲取就可以了
int main()
{int fd = open(fifo_file,O_WRONLY);if(fd < 0){perror("open");return 1;}while(1){printf("請(qǐng)輸入# ");//client的輸入提示fflush(stdout);//刷新一下標(biāo)準(zhǔn)輸出char buffer[64] = {0};//先把數(shù)據(jù)從標(biāo)準(zhǔn)輸入拿到client進(jìn)程內(nèi)部ssize_t read_length = read(0,buffer,sizeof(buffer)-1);if(read_length > 0){buffer[read_length - 1] = 0;//拿到了數(shù)據(jù)write(fd,buffer,strlen(buffer));}}close(fd);return 0;
}

這20秒內(nèi),client向命名管道寫,但是server沒有從命名管道讀,按理來說,命名管道里面有內(nèi)容,大小不為0,但是在這20秒之內(nèi)發(fā)現(xiàn)命名管道的fifo_file的大小為0,這就說明了命名管道的數(shù)據(jù),由于效率問題,不會(huì)刷新到磁盤。

5.匿名管道和命名管道的區(qū)別

創(chuàng)建與打開的方式不同:

  • 匿名管道由pipe函數(shù)創(chuàng)建并打開
  • 命名管道由mkfifo函數(shù)創(chuàng)建,由open函數(shù)打開

后面就有相同的語(yǔ)義了

三、System V IPC

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

System V是一種用于在操作系統(tǒng)層面上進(jìn)行進(jìn)程間通信的標(biāo)準(zhǔn),system V標(biāo)準(zhǔn)給用戶提供了系統(tǒng)調(diào)用接口,只要用戶使用它所提供的系統(tǒng)調(diào)用就可以完成進(jìn)程間通信。IPC(Inter-Process Communication)是進(jìn)程間通信。System V IPC不用基于文件進(jìn)行通信。

如何把系統(tǒng)調(diào)用接口提供給用戶使用呢?System V是操作系統(tǒng)內(nèi)核的一部分,是為操作系統(tǒng)中多進(jìn)程提供的一種通信方案。但是操作系統(tǒng)不相信任何用戶,采用系統(tǒng)調(diào)用為用戶提供功能。所以System V進(jìn)程間通信,存在專門用來通信的接口:System call(系統(tǒng)調(diào)用)

這就需要制定一套標(biāo)準(zhǔn)用來在同一主機(jī)內(nèi)進(jìn)行進(jìn)程間通信:System V。System V進(jìn)程間通信分為3種:

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

消息隊(duì)列模型通過在協(xié)作進(jìn)程間交換消息來實(shí)現(xiàn)通信。共享內(nèi)存模型會(huì)建立起一塊供協(xié)作進(jìn)程共享的內(nèi)存區(qū)域,進(jìn)程通過向此共享區(qū)域讀出或?qū)懭霐?shù)據(jù)來交換信息。以下是消息隊(duì)列和共享內(nèi)存的通信模型:

?消息隊(duì)列的實(shí)現(xiàn)經(jīng)常采用系統(tǒng)調(diào)用,因此需要消耗更多時(shí)間使內(nèi)核介入,但是共享內(nèi)存只在建立共享內(nèi)存區(qū)域時(shí)需要系統(tǒng)調(diào)用,一旦建立共享內(nèi)存,所有訪問都是常規(guī)內(nèi)存訪問,不需要借助內(nèi)核。

由于消息隊(duì)列和共享內(nèi)存用來傳遞消息,信號(hào)量用來實(shí)現(xiàn)進(jìn)程間同步和互斥。因此主要來看看進(jìn)程間通信方式中效率較高的共享內(nèi)存。

2.共享內(nèi)存

(1)原理?

把申請(qǐng)的共享內(nèi)存映射到不同進(jìn)程的地址空間當(dāng)中。有進(jìn)程A和進(jìn)程B,進(jìn)程A通過頁(yè)表映射找到進(jìn)程A的代碼和數(shù)據(jù),同樣,進(jìn)程B也通過頁(yè)表映射找到進(jìn)程B的代碼和數(shù)據(jù),由于兩個(gè)進(jìn)程的數(shù)據(jù)結(jié)構(gòu)相互獨(dú)立,且物理內(nèi)存當(dāng)中的代碼和數(shù)據(jù)也相互獨(dú)立,因此兩個(gè)進(jìn)程不會(huì)互相干擾。

在物理內(nèi)存開辟一塊共享內(nèi)存空間后,需要通過系統(tǒng)調(diào)用把開辟的內(nèi)存空間經(jīng)過頁(yè)表映射到進(jìn)程地址空間,那么共享內(nèi)存在進(jìn)程地址空間也有了虛擬地址,叫做共享存儲(chǔ)器映射區(qū),再把共享存儲(chǔ)器映射區(qū)的虛擬地址填到頁(yè)表當(dāng)中,這樣共享內(nèi)存的虛擬地址和物理地址就建立起了對(duì)應(yīng)關(guān)系,而且各個(gè)進(jìn)程也就看到了共享內(nèi)存同一份資源。

?以上的過程也是讓進(jìn)程掛接到共享內(nèi)存空間上的過程。操作系統(tǒng)內(nèi)可能存在多個(gè)共享內(nèi)存,那么操作系統(tǒng)需要管理這些共享內(nèi)存,管理還是先描述再組織。

?如何保證能夠讓多個(gè)進(jìn)程看到同一個(gè)共享內(nèi)存呢?

?共享內(nèi)存一定要有唯一標(biāo)識(shí)ID,就能讓不同進(jìn)程識(shí)別到同一個(gè)共享內(nèi)存資源。那么這個(gè)ID一定在描述共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)中。

(2)步驟

可以總結(jié)出使用共享內(nèi)存的過程:

  • 創(chuàng)建共享內(nèi)存
  • 關(guān)聯(lián)(掛接)
  • 去關(guān)聯(lián)(去掛接)
  • 釋放共享內(nèi)存

(3)函數(shù)?

shmget

使用shmget函數(shù)創(chuàng)建共享內(nèi)存,來申請(qǐng)一塊共享內(nèi)存空間:

#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
key通過ftok函數(shù)生成
size建議為4KB的整數(shù)倍,操作系統(tǒng)為了提高內(nèi)存和硬盤的數(shù)據(jù)交換的速度,以4KB為單位
shmflghmflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了
返回值成功就返回共享內(nèi)存地址,失敗就返回-1

?其中,shmget第一個(gè)參數(shù)key是通過ftok函數(shù)生成的:

#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
pathname自定義的文件路徑名
proj_id序號(hào),低8位被使用,非0
返回值返回key,會(huì)被設(shè)置進(jìn)共享內(nèi)存在內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里面

shmget第三個(gè)參數(shù)shmflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了:

?創(chuàng)建共享內(nèi)存后,如何查看共享內(nèi)存呢?ipcs命令用于報(bào)告進(jìn)程間通信設(shè)施狀況,其中:

ipcs -m //查看共享內(nèi)存(Shared Memory Segments)
ipcs -q //查看消息隊(duì)列(Message Queue)
ipcs -s //查看信號(hào)量(Semaphore Arrays)

shmctl

使用完共享內(nèi)存后,如果不刪除的話,共享內(nèi)存會(huì)一直存在,直到系統(tǒng)重啟。如何刪除呢?有兩種刪除方式,一種是命令刪除:

ipcrm -m shmid

key只是用來在系統(tǒng)層面進(jìn)行唯一標(biāo)識(shí),不能用來管理共享內(nèi)存。而shmid是操作系統(tǒng)給用戶返回的id,用來在用戶層進(jìn)行共享內(nèi)存管理,所以ipcrm是用戶層的命令。 以上是命令刪除,那么如何在代碼中刪除共享內(nèi)存呢?

?因此另外一種刪除共享內(nèi)存的方式就是使用shmctl函數(shù)控制共享內(nèi)存:

#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid操作系統(tǒng)給用戶返回的id
cmd選項(xiàng),有多個(gè)
bufdata structure數(shù)據(jù)結(jié)構(gòu)類型指針
返回值刪除成功返回0,失敗返回-1

其中cmd選項(xiàng)有多個(gè):

IPC_STAT將shmid的內(nèi)核數(shù)據(jù)結(jié)構(gòu)拷貝到buf指向的shmid_ds結(jié)構(gòu)中
IPC_SET將buf指向的shmid_ds結(jié)構(gòu)的一些成員的值寫入與此共享內(nèi)存段相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),同時(shí)更新其shm_ctime成員
IPC_RMID刪除共享內(nèi)存

第三個(gè)參數(shù)???

?其中,shmid_ds數(shù)據(jù)結(jié)構(gòu)如下:

struct shmid_ds
{struct ipc_perm shm_perm;    /* Ownership and permissions */size_t          shm_segsz;   /* Size of segment (bytes) */time_t          shm_atime;   /* Last attach time */time_t          shm_dtime;   /* Last detach time */time_t          shm_ctime;   /* Last change time */pid_t           shm_cpid;    /* PID of creator */pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */shmatt_t        shm_nattch;  /* No. of current attaches */...
};

shmat

?使用shmat把共享內(nèi)存映射到調(diào)用進(jìn)程的地址空間(關(guān)聯(lián):增加共享內(nèi)存和進(jìn)程地址空間映射關(guān)系的頁(yè)表項(xiàng))

#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid操作系統(tǒng)給用戶返回的id
shmaddr表明把共享內(nèi)存掛接到進(jìn)程地址空間的哪些范圍中?
shmflg有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了,同shmget函數(shù)的shmflg標(biāo)志,這里設(shè)置為0就可以了
返回值返回共享內(nèi)存掛接到進(jìn)程地址空間的虛擬地址,同申請(qǐng)堆空間的malloc返回值是一樣的

shmdt

?shmdt用來斷開共享內(nèi)存和進(jìn)程地址空間的映射(去關(guān)聯(lián):刪除共享內(nèi)存和進(jìn)程地址空間映射關(guān)系的頁(yè)表項(xiàng),而不是釋放共享內(nèi)存)

#include <sys/types.h>
#include <sys/shm.h>int shmdt(const void *shmaddr);
shmaddr要斷開映射的共享內(nèi)存地址,且必須和shmat的參數(shù)shmaddr相同
返回值成功斷開返回0,失敗返回-1

(4)使用

兩個(gè)進(jìn)程使用共享內(nèi)存通信,需要進(jìn)行創(chuàng)建、關(guān)聯(lián)、去關(guān)聯(lián)、刪除的步驟,現(xiàn)在使用上面的函數(shù)來進(jìn)行server和client兩個(gè)進(jìn)程間的通信。

comm.h來包含頭文件

#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>#define PATH_NAME "/home/delia/linux/20230627-sharedMemory/shared/server.c"  //ftok的路徑
#define PROJ_ID 0x6666
#define SIZE 4097

server端需要生成唯一ID,創(chuàng)建共享內(nèi)存,關(guān)聯(lián)共享內(nèi)存,去關(guān)聯(lián)共享內(nèi)存,刪除共享內(nèi)存:

server.c

#include "comm.h"int main()
{key_t key = ftok(PATH_NAME,PROJ_ID);//生成唯一ID保證在統(tǒng)一系統(tǒng)當(dāng)中
找到共享內(nèi)存if(key < 0){perror("fork");return 1;}//1.創(chuàng)建共享內(nèi)存int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//共享內(nèi)存不存在就創(chuàng)建,權(quán)限為666,共享內(nèi)存可以用文件權(quán)限來約束if(shmid < 0){perror("shmget");return 2;}printf("key = %u,shmid = %d\n",key,shmid);sleep(1);//2.關(guān)聯(lián)char *mem = shmat(shmid,NULL,0);printf("attaches shm success\n");sleep(15);//通信邏輯while(1){sleep(1);//printf("%s\n",mem);}//3.去關(guān)聯(lián)shmdt(mem);printf("detaches shm success\n");//4.刪除共享內(nèi)存shmctl(shmid,IPC_RMID,NULL);sleep(5);printf("key = %u,shmid = %d after shmctl\n",key,shmid);return 0;
}

client端需要和客戶端一樣生成同一個(gè)唯一ID,創(chuàng)建使用同一個(gè)共享內(nèi)存,關(guān)聯(lián)共享內(nèi)存,去關(guān)聯(lián)共享內(nèi)存,不需要?jiǎng)h除共享內(nèi)存,因?yàn)閟erver端已經(jīng)刪除了:

client.c

#include "comm.h"int main()
{key_t key = ftok(PATH_NAME,PROJ_ID);//生成唯一ID保證在統(tǒng)一系統(tǒng)當(dāng)中
找到共享內(nèi)存if(key < 0){perror("ftok");return 1;}printf("%u\n",key);//1.創(chuàng)建共享內(nèi)存int shmid = shmget(key,SIZE,IPC_CREAT);//共享內(nèi)存已存在就返回已存在共享內(nèi)存if(shmid < 0){perror("shmget");return 2;}//2.關(guān)聯(lián)char *mem = shmat(shmid,NULL,0);sleep(5);printf("client process attaches success\n");//通信邏輯char c = 'A';while(c <= 'G'){mem[c - 'A'] = c;c++;mem[c - 'A'] = 0;sleep(2);}//3.去關(guān)聯(lián)shmdt(mem);printf("client process detaches success\n");return 0;
}

?Makefile

.PHONY:all
all:server clientserver:server.cgcc -o $@ $^
client:client.cgcc -o $@ $^.PHONY:clean
clean:rm -f server client

make之后,使用命令

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

來查看共享內(nèi)存的掛接進(jìn)程的數(shù)量變化:當(dāng)server端和client端進(jìn)程都沒有開啟時(shí),看到共享內(nèi)存信息的nattch的個(gè)數(shù)為0,當(dāng)server和client端都運(yùn)行起來之后,發(fā)現(xiàn)nattch的個(gè)數(shù)變成了2,client所寫的消息就會(huì)被server讀取,當(dāng)client端去關(guān)聯(lián)之后,nattch變成了1,最后當(dāng)server端退出時(shí),共享內(nèi)存被刪除,nattch又變成了0:

key系統(tǒng)區(qū)別各個(gè)共享內(nèi)存的唯一標(biāo)識(shí)
shmid共享內(nèi)存的用戶層id(句柄)
owner共享內(nèi)存的擁有者
perms共享內(nèi)存的權(quán)限
bytes共享內(nèi)存的大小
nattch關(guān)聯(lián)共享內(nèi)存的進(jìn)程數(shù)
status共享內(nèi)存的狀態(tài)

從以上可以看出,共享內(nèi)存有以下特點(diǎn):

  • 共享內(nèi)存一旦建立好并映射進(jìn)自己進(jìn)程的地址空間,該進(jìn)程就可以看到該共享內(nèi)存,就像malloc的空間一樣,不需要任何系統(tǒng)調(diào)用接口(比如read、write會(huì)將數(shù)據(jù)從內(nèi)核拷貝到用戶或從用戶拷貝到內(nèi)核)。
  • 共享內(nèi)存是所有進(jìn)程間通信中速度最快的,這是因?yàn)閷⒁粔K共享內(nèi)存映射到不同的進(jìn)程地址空間,共享內(nèi)存地址對(duì)應(yīng)在內(nèi)存上的空間就拿到了,所以server和Client有任何一方寫了,另一方馬上就看到了。
  • 生命周期隨內(nèi)核,而且不提供同步互斥機(jī)制,需要程序員自行保證數(shù)據(jù)的安全。

3.共享內(nèi)存和管道區(qū)別?

從共享內(nèi)存的特點(diǎn)可以看出:

(1)創(chuàng)建好共享內(nèi)存后,就不需要再調(diào)用系統(tǒng)接口進(jìn)行通信了, 而管道創(chuàng)建好后還需要調(diào)用read、write等系統(tǒng)接口進(jìn)行通信。

(2) 共享內(nèi)存沒有同步互斥機(jī)制,但是管道有同步互斥機(jī)制。

(3)共享內(nèi)存是所有進(jìn)程間通信方式中速度最快的,將數(shù)據(jù)從一個(gè)進(jìn)程傳輸帶另一個(gè)進(jìn)程,管道需要進(jìn)行4次拷貝,共享內(nèi)存需要進(jìn)行2次拷貝,共享內(nèi)存需要的拷貝次數(shù)少。

使用管道,將文件從一個(gè)進(jìn)程傳到另一個(gè)進(jìn)程需要4次拷貝:

  • 服務(wù)端把信息從輸入文件復(fù)制到服務(wù)端的臨時(shí)緩沖區(qū)
  • 把服務(wù)端的臨時(shí)緩沖區(qū)信息復(fù)制到管道中
  • 客戶端把信息從管道復(fù)制到客戶端的緩沖區(qū)
  • 把客戶端臨時(shí)緩沖區(qū)的信息復(fù)制到輸出文件中

?使用共享內(nèi)存,將文件從一個(gè)進(jìn)程傳到另一個(gè)進(jìn)程需要2次拷貝:

  • 將信息從輸入文件拷貝到共享內(nèi)存
  • 將信息從共享內(nèi)存拷貝到輸出文件

四、消息隊(duì)列?

1.原理?

消息隊(duì)列是一個(gè)消息的鏈表,可以把消息看作一個(gè)記錄,具有特定的格式以及特定的優(yōu)先級(jí)。對(duì)消息隊(duì)列有寫權(quán)限的進(jìn)程可以向消息隊(duì)列中按照一定的規(guī)則添加新消息,對(duì)消息隊(duì)列有讀權(quán)限的進(jìn)程則可以從消息隊(duì)列中讀走消息。消息隊(duì)列的生命周期是隨內(nèi)核的。?

隊(duì)列的每個(gè)成員都是數(shù)據(jù)塊,每個(gè)數(shù)據(jù)塊包含類型和信息兩部分。這個(gè)隊(duì)列也遵循先進(jìn)先出,即從隊(duì)頭讀取消息,向隊(duì)尾寫入消息:

每個(gè)數(shù)據(jù)塊都有類型,這就說明,各個(gè)數(shù)據(jù)塊的類型可以不同,因此,接收者進(jìn)程接收的數(shù)據(jù)塊可以有不同的類型值。消息隊(duì)列的資源必須手動(dòng)刪除,因?yàn)閟ystem V IPC資源的生命周期是隨內(nèi)核的。

2.數(shù)據(jù)結(jié)構(gòu)

?消息對(duì)中的數(shù)據(jù)塊如何管理呢?還是先描述,再組織。使用命令:

cat /usr/include/linux/msg.h

就能夠看到消息隊(duì)列的數(shù)據(jù)結(jié)構(gòu)如下:?

struct msqid_ds 
{ struct ipc_perm msg_perm;     /* Ownership and permissions */time_t          msg_stime;    /* Time of last msgsnd(2) */time_t          msg_rtime;    /* Time of last msgrcv(2) */time_t          msg_ctime;    /* Time of last change */unsigned long   __msg_cbytes; /* Current number of bytes inqueue (nonstandard) */msgqnum_t       msg_qnum;     /* Current number of messagesin queue */msglen_t        msg_qbytes;   /* Maximum number of bytesallowed in queue */pid_t           msg_lspid;    /* PID of last msgsnd(2) */pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

?第一個(gè)ipc_perm?結(jié)構(gòu)體是不是有點(diǎn)熟悉呢?它和shm_perm是同類型的結(jié)構(gòu)體,使用命令:

cat /usr/include/linux/ipc.h

就能夠看到ipc_perm 的結(jié)構(gòu)體定義如下:

struct ipc_perm 
{key_t          __key;       /* Key supplied to msgget(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 */unsigned short mode;        /* Permissions */unsigned short __seq;       /* Sequence number */
};

3.步驟

消息隊(duì)列使用過程如下:?

  • 創(chuàng)建
  • 發(fā)送
  • 接收
  • 釋放

4.函數(shù)?

(1)msgget

使用msgget來創(chuàng)建消息隊(duì)列:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);
key通過ftok函數(shù)生成
msgflgmsgflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了
返回值創(chuàng)建成功就返回消息隊(duì)列標(biāo)識(shí)符,失敗就返回-1

同shmget一樣,msgget第一個(gè)參數(shù)key是通過ftok函數(shù)生成的:

#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
pathname自定義的文件路徑名
proj_id序號(hào),低8位被使用,非0
返回值返回key,會(huì)被設(shè)置進(jìn)共享內(nèi)存在內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里面

msgget第三個(gè)參數(shù)msgflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了:

(2)msgctl

使用msgctl來釋放消息隊(duì)列:

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

使用完消息隊(duì)列后,如果不刪除的話,消息隊(duì)列會(huì)一直存在,直到系統(tǒng)重啟。如何刪除呢?有兩種刪除方式,一種是命令刪除:

ipcrm -q msqid

那么如何在代碼中刪除共享內(nèi)存呢?因此另外一種刪除消息隊(duì)列的方式就是使用msgctl函數(shù)控制消息隊(duì)列:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid消息隊(duì)列的用戶層id
cmd選項(xiàng),有多個(gè)
bufdata structure數(shù)據(jù)結(jié)構(gòu)類型指針
返回值刪除成功返回0,失敗返回-1

其中cmd選項(xiàng)有多個(gè):?

IPC_STAT將msqid的內(nèi)核數(shù)據(jù)結(jié)構(gòu)拷貝到buf指向的msqid_ds結(jié)構(gòu)中
IPC_SET將buf指向的msqid_ds結(jié)構(gòu)的一些成員的值寫入與此共享內(nèi)存段相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),同時(shí)更新其msq_ctime成員
IPC_RMID刪除共享內(nèi)存

(3)msgsnd

使用msgsnd向消息隊(duì)列發(fā)送數(shù)據(jù):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid操作系統(tǒng)給用戶返回的id
msgp待發(fā)送數(shù)據(jù)塊
msgsz待發(fā)送數(shù)據(jù)塊大小
msgflg發(fā)送數(shù)據(jù)塊的方式,一般為0
返回值0表示調(diào)用成功,-1表示調(diào)用失敗

其中第二個(gè)參數(shù)msgp的結(jié)構(gòu)為:

struct msgbuf{long mtype;       /* message type, must be > 0 */char mtext[1];    /* message data */
};

其中mutex為待發(fā)送的信息,mutex大小可以由我們自己指定。

(4)msgrcv

使用msgrcv從消息隊(duì)列獲取消息:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msqid操作系統(tǒng)給用戶返回的id
msgp獲取到的數(shù)據(jù)塊
msgsz獲取到的數(shù)據(jù)塊大小
msgtyp獲取到的數(shù)據(jù)塊的類型
msgflg獲取數(shù)據(jù)塊的方式,一般為0
返回值>0表示實(shí)際獲取的字節(jié)數(shù),-1表示調(diào)用失敗


?

五、信號(hào)量

1.原理?

前面的管道、共享內(nèi)存、消息隊(duì)列都以傳輸數(shù)據(jù)為目的,但是信號(hào)量不以傳輸數(shù)據(jù)為目的,通過共享資源的方式,來達(dá)到多個(gè)進(jìn)程同步互斥的目的。?

信號(hào)量,有時(shí)被稱為信號(hào)燈,是在多線程環(huán)境下使用的一種設(shè)施,是可以用來保證兩個(gè)或多個(gè)關(guān)鍵代碼段不被并發(fā)調(diào)用。在進(jìn)入一個(gè)關(guān)鍵代碼段之前,線程必須獲取一個(gè)信號(hào)量;一旦該關(guān)鍵代碼段完成了,那么該線程必須釋放信號(hào)量。其它想進(jìn)入該關(guān)鍵代碼段的線程必須等待直到第一個(gè)線程釋放信號(hào)量。本質(zhì)就是一個(gè)計(jì)數(shù)器,衡量臨界資源中的資源數(shù)。

這就像坐火車一樣,并不是因?yàn)樽谧簧?#xff0c;這個(gè)作為才屬于某一個(gè)人,而是買了票的時(shí)候,這個(gè)作為就已經(jīng)屬于買票的人了,因此買票的本質(zhì)就是對(duì)臨界資源的預(yù)訂,票的數(shù)量就是信號(hào)量。如以下代碼:

?

信號(hào)量相關(guān)概念:

  • 臨界資源:被多個(gè)執(zhí)行流同時(shí)訪問的資源,一次只允許一個(gè)進(jìn)程使用。比如管道、共享內(nèi)存、消息隊(duì)列、信號(hào)量。
  • 臨界區(qū):進(jìn)程中訪問臨界資源的代碼(和臨界資源配套)為了保護(hù)數(shù)據(jù)安全,就要把臨界區(qū)保護(hù)起來,就有了信號(hào)量。
  • 原子性:一件事情要么做完,要么不做,沒有中間狀態(tài)。
  • IPC資源必須刪除,否則不會(huì)自動(dòng)清除,除非重啟,所以system V IPC資源的生命周期隨內(nèi)核。

?信號(hào)量本質(zhì)是對(duì)臨界資源的統(tǒng)計(jì),更是操作系統(tǒng)對(duì)臨界資源的預(yù)定機(jī)制,信號(hào)量要誒預(yù)訂,所有線程要訪問臨界資源,得先申請(qǐng)信號(hào)量,那么所有的進(jìn)程就得先看到信號(hào)量,信號(hào)量就是臨界資源,要保護(hù)信號(hào)量這個(gè)臨界資源,信號(hào)量的常見操作即PV操作就必須保證原子性。

2.數(shù)據(jù)結(jié)構(gòu)

使用命令:

cat /usr/include/linux/sem.h

就能夠看到信號(hào)量的數(shù)據(jù)結(jié)構(gòu)如下:?

struct semid_ds 
{struct ipc_perm sem_perm;               /* permissions .. see ipc.h */__kernel_time_t sem_otime;              /* last semop time */__kernel_time_t sem_ctime;              /* last change time */struct sem      *sem_base;              /* ptr to first semaphore in array */struct sem_queue *sem_pending;          /* pending operations to be processed */struct sem_queue **sem_pending_last;    /* last pending operation */struct sem_undo *undo;                  /* undo requests on this array */unsigned short  sem_nsems;              /* no. of semaphores in array */
};

?第一個(gè)ipc_perm?結(jié)構(gòu)體是不是有點(diǎn)熟悉呢?它和shm_perm、msg_perm是同類型的結(jié)構(gòu)體,使用命令:

cat /usr/include/linux/ipc.h

就能夠看到ipc_perm 的結(jié)構(gòu)體定義如下:

struct ipc_perm 
{key_t          __key;       /* Key supplied to msgget(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 */unsigned short mode;        /* Permissions */unsigned short __seq;       /* Sequence number */
};

3.函數(shù)

(1)semget

?使用semget創(chuàng)建信號(hào)量:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semget(key_t key, int nsems, int semflg);
key操作系統(tǒng)給用戶返回的id
nsems創(chuàng)建的信號(hào)量的個(gè)數(shù)
semflgsemflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了
返回值

創(chuàng)建成功就返回信號(hào)量標(biāo)識(shí)符,-1表示創(chuàng)建失敗

(2)semctl

?使用semctl刪除信號(hào)量:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ...);
semid信號(hào)量的用戶層id
semnum信號(hào)量序號(hào)
cmd信號(hào)量的控制操作標(biāo)識(shí)
返回值

創(chuàng)建成功就返回信號(hào)量標(biāo)識(shí)符,-1表示創(chuàng)建失敗

(3)semop

使用semop來進(jìn)行信號(hào)量的PV操作:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semop(int semid, struct sembuf *sops, unsigned nsops);
semid信號(hào)量的用戶層id
sops是sembuf類型的操作指針
nsops單個(gè)信號(hào)量的操作
返回值

創(chuàng)建成功就返回信號(hào)量標(biāo)識(shí)符,-1表示創(chuàng)建失敗

使用命令:

cat /usr/include/linux/sem.h

可以看到sembuf結(jié)構(gòu)體:

struct sembuf
{unsigned short  sem_num;        /* semaphore index in array */short           sem_op;         /* semaphore operation */short           sem_flg;        /* operation flags */
};
sem_num指定要操作的信號(hào)量,0表示第一個(gè)信號(hào)量,1表示第二個(gè)信號(hào)量,……
sem_op信號(hào)量操作
sem_flg操作標(biāo)識(shí)

六、System V IPC總結(jié)

?從以上內(nèi)容可以看出,共享內(nèi)存、消息隊(duì)列、信號(hào)量,雖然屬性和實(shí)現(xiàn)起來有差別,但是他們維護(hù)的數(shù)據(jù)結(jié)構(gòu)的成員卻是一樣的,即ipc_perm結(jié)構(gòu)體,這樣每次要申請(qǐng)System V IPC時(shí),無論是共享內(nèi)存、消息隊(duì)列、信號(hào)量,都會(huì)在數(shù)組中開辟ipc_perm這樣的結(jié)構(gòu):

?那么內(nèi)核可以分配一個(gè)ipc_perm數(shù)組,用來指向每一個(gè)IPC資源。

?

?

?

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

相關(guān)文章:

  • 網(wǎng)站加載慢圖片做延時(shí)加載有用百度關(guān)鍵詞怎么設(shè)置
  • 哪家專門做特賣的網(wǎng)站?杭州seo整站優(yōu)化
  • 網(wǎng)站做百度競(jìng)價(jià)利于百度優(yōu)化aso應(yīng)用商店優(yōu)化原因
  • 西安網(wǎng)站建設(shè)公司排名seo百度刷排名
  • 做網(wǎng)站需要公司資料嗎關(guān)鍵詞優(yōu)化排名軟件s
  • 模板做的網(wǎng)站不好優(yōu)化北京關(guān)鍵詞seo
  • 美國(guó)做空機(jī)構(gòu)渾水網(wǎng)站百度營(yíng)銷推廣官網(wǎng)
  • 北京網(wǎng)站設(shè)計(jì)公司有哪些摘抄一篇新聞
  • 大連開發(fā)區(qū)網(wǎng)站制作建設(shè)公司游戲推廣合作
  • 網(wǎng)站建設(shè)產(chǎn)品圖片尺寸要求百度貼吧網(wǎng)頁(yè)版
  • 德陽(yáng)城鄉(xiāng)建設(shè)部網(wǎng)站首頁(yè)網(wǎng)站創(chuàng)建
  • 網(wǎng)站下要加個(gè)備案號(hào) 怎么做上海推廣系統(tǒng)
  • 寧波市有哪些網(wǎng)站建設(shè)公司湖北網(wǎng)絡(luò)推廣公司
  • 怎樣做醫(yī)療保健網(wǎng)站網(wǎng)絡(luò)營(yíng)銷常用的工具和方法
  • 源碼怎樣做網(wǎng)站深圳推廣公司哪家正規(guī)
  • 男女做羞羞事網(wǎng)站現(xiàn)在學(xué)seo課程多少錢
  • 觸屏版手機(jī)網(wǎng)站鄭州網(wǎng)站運(yùn)營(yíng)實(shí)力樂云seo
  • 免費(fèi)網(wǎng)站app軟件億驅(qū)動(dòng)力競(jìng)價(jià)托管
  • 多個(gè)織夢(mèng)dedecms網(wǎng)站怎么做站群抖音搜索引擎優(yōu)化
  • wordpress 無法登錄寧波seo快速優(yōu)化教程
  • 沈陽(yáng)奇搜建站廣東seo快速排名
  • 網(wǎng)站面包屑導(dǎo)航代碼網(wǎng)站seo專員招聘
  • 廣州市政府門戶網(wǎng)站本地服務(wù)推廣平臺(tái)哪個(gè)好
  • cpanel wordpressseo sem是指什么意思
  • 如何做視頻網(wǎng)站的廣告推廣網(wǎng)站平臺(tái)做推廣
  • 公司網(wǎng)站設(shè)計(jì)意見百度搜索排名購(gòu)買
  • 申請(qǐng)自助網(wǎng)站深圳網(wǎng)站設(shè)計(jì)專業(yè)樂云seo
  • 四川省的建設(shè)廳注冊(cè)中心網(wǎng)站首頁(yè)怎么創(chuàng)建自己的網(wǎng)站平臺(tái)
  • 網(wǎng)站建設(shè)費(fèi)應(yīng)計(jì)入什么科目網(wǎng)站優(yōu)化排名軟件哪些最好
  • 羅湖網(wǎng)站建設(shè)公司上海高端網(wǎng)站建設(shè)