1. 進(jìn)程間通信
進(jìn)程間通信(Inter-Process Communication,簡稱IPC)是指不同進(jìn)程之間進(jìn)行數(shù)據(jù)交換和共享信息的機(jī)制和技術(shù)。在操作系統(tǒng)中,每個進(jìn)程都是獨立運行的,有自己的地址空間和數(shù)據(jù),因此進(jìn)程之間需要一種機(jī)制來進(jìn)行通信,以便彼此協(xié)調(diào)工作、共享數(shù)據(jù)或者進(jìn)行同步操作。
進(jìn)程間通信的前提,也是重中之重,是讓不同的進(jìn)程看到同一份資源。 由于進(jìn)程的獨立性,只有先讓不同進(jìn)程看到同一份資源,有了通信的平臺,才能實現(xiàn)通信。本文重點在于如何搭建進(jìn)程間通信的平臺,使得不同進(jìn)程看到同一份資源。
2. 管道
管道,是一種傳統(tǒng)的進(jìn)程間通信方法。管道的本質(zhì)是一個特殊文件,一個進(jìn)程作為寫入端,一個進(jìn)程作為讀取段,通過寫入和讀取管道實現(xiàn)通信。
??管道分為匿名管道和命名管道,它們的使用場景不同。
匿名管道
??匿名管道(pipe)應(yīng)用于有親緣關(guān)系的進(jìn)程之間通信(如:父子進(jìn)程、兄弟進(jìn)程)。以父子進(jìn)程為例,原理:
-
父進(jìn)程創(chuàng)建管道,并分別以寫方式和讀方式打開管道,此時父進(jìn)程就擁有了兩個新的文件描述符,以寫方式打開管道的文件描述符稱為寫端fd,以讀方式打開管道的文件描述符稱為讀端fd。
-
接著創(chuàng)建子進(jìn)程,子進(jìn)程繼承了父進(jìn)程的文件描述符表,二者有了相同的寫端fd和讀端fd。
-
然后根據(jù)需求關(guān)閉不要的文件描述符,如:父進(jìn)程寫數(shù)據(jù)給子進(jìn)程,即父進(jìn)程作為寫入端,子進(jìn)程作為讀取端,那就關(guān)閉父進(jìn)程的讀端fd和子進(jìn)程的寫端fd。
-
此時父子進(jìn)程已經(jīng)能看到同一份資源了,通信開始,父進(jìn)程調(diào)用
write
寫入管道,子進(jìn)程調(diào)用read
讀取管道,和文件操作相同。
在這個過程中創(chuàng)建的管道,稱之為匿名管道。之所以是匿名管道,是因為整個過程中用戶都無法獲知管道的名稱等具體信息,該管道由OS維護(hù)。
?上述過程的邏輯演繹如下:
??補充
-
管道是一種特殊的文件,它在內(nèi)存中以緩沖區(qū)的形式存在。因此打開管道就和打開文件一樣,OS也會在內(nèi)存中創(chuàng)建一個打開文件句柄來維護(hù)管道。通過打開文件句柄,我們可以引用到管道的緩沖區(qū),從而對其進(jìn)行讀寫操作。
-
匿名管道的生命周期隨進(jìn)程。當(dāng)引用該管道的所有進(jìn)程退出,OS自動關(guān)閉并刪除匿名管道。(打開文件句柄和inode的引用計數(shù)問題)
-
因為管道是一種臨時的通信機(jī)制,不像普通文件具有持久性的存儲需求,所以管道是沒有磁盤文件的。那么管道是否像文件一樣擁有一個inode呢?是的。管道文件的inode主要用于標(biāo)識和管理管道,記錄與管道相關(guān)的元數(shù)據(jù)信息,并跟蹤管道的引用計數(shù)。管道文件的inode并不鏈接實際數(shù)據(jù),數(shù)據(jù)是通過內(nèi)核的緩沖區(qū)進(jìn)行傳遞和管理的。
-
管道是一種半雙工的通信方式,即一端寫一端讀,單向數(shù)據(jù)流動。
- 下面是代碼分析。
??首先是創(chuàng)建匿名管道的接口
int pipe(int pipefd[2]);
pipe是一個系統(tǒng)調(diào)用接口。當(dāng)前進(jìn)程創(chuàng)建匿名管道,傳入?yún)?shù)pipefd是一個能夠存放2個元素的整型數(shù)組,調(diào)用成功后,管道的寫端fd和讀端fd存入
pipefd
中,pipefd[0]
是讀端fd,pipefd[1]
是寫端fd。
下面是pipe在2號手冊中的介紹。
NAME
pipe, pipe2 - create pipe
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]);
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
下面是使用匿名管道實現(xiàn)進(jìn)程間通信的一段代碼
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
const int NUM = 1024;
// 先創(chuàng)建管道,進(jìn)而創(chuàng)建子進(jìn)程,父子進(jìn)程使用管道進(jìn)行通信
// 父進(jìn)程向管道當(dāng)中寫“i am father”,
// 子進(jìn)程從管道當(dāng)中讀出內(nèi)容, 并且打印到標(biāo)準(zhǔn)輸出
int main()
{
// 1.創(chuàng)建管道
int pipefd[2] = {0};
int ret = pipe(pipefd);
if (ret < 0)
{
cerr << errno << ":" << strerror(errno) << endl;
return 1;
}
// 2.創(chuàng)建子進(jìn)程
pid_t id = fork();
assert(id >= 0);
if (id == 0)
{
// 子進(jìn)程讀
// 3.關(guān)閉不要的fd
close(pipefd[1]);
// 4.通信
char buf[NUM] = {0};
int n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = '\0';
cout << buf << endl;
}
else if (n == 0)
{
cout << "讀取到文件末尾" << endl;
}
else
{
exit(1);
}
close(pipefd[0]);
exit(0);
}
// 父進(jìn)程寫
// 3.關(guān)閉不要的fd
close(pipefd[0]);
// 4.通信
const char *msg = "I am father";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]);
// 5.等待子進(jìn)程退出
int n = waitpid(id, nullptr, 0);
if (n == -1)
{
cerr << errno << ":" << strerror(errno) << endl;
return 1;
}
return 0;
}
?執(zhí)行結(jié)果
[ckf@VM-8-3-centos Testpipe]$ ./a.out
I am father #子進(jìn)程成功讀取并輸出父進(jìn)程發(fā)送的信息
命名管道
??命名管道(named pipe)應(yīng)用于無親緣關(guān)系的進(jìn)程之間通信。無親緣關(guān)系的兩個進(jìn)程,無法通過繼承文件描述符表來獲得同一個匿名管道,因此就需要命名管道。命名管道有特定的文件名,多個進(jìn)程可以通過相同的文件名找到相同的管道,進(jìn)而實現(xiàn)通信。使用命名管道的步驟如下:
-
創(chuàng)建命名管道
創(chuàng)建命名管道的方式有兩種,通過指令或系統(tǒng)調(diào)用。
指令:
mkfifo [選項] [name] OPTION: -m MODE #設(shè)置管道的權(quán)限
系統(tǒng)調(diào)用:
NAME mkfifo - make a FIFO special file (a named pipe) SYNOPSIS #include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); RETURN VALUE On success mkfifo() returns 0. In the case of an error, -1 is returned (in which case, errno is set appropriately).
-
進(jìn)程打開命名管道
進(jìn)程可以調(diào)用
open
接口,以讀或?qū)懛绞酱蜷_命名管道,此時必須保證命名管道是存在的。注意:進(jìn)程要有命名管道對應(yīng)的權(quán)限才能正確地讀取或?qū)懭霐?shù)據(jù),權(quán)限在創(chuàng)建管道時設(shè)定。 -
通信
-
關(guān)閉管道,刪除管道
進(jìn)程調(diào)用
close
關(guān)閉管道,退出程序。命名管道的生命周期不隨進(jìn)程,進(jìn)程退出命名管道依舊存在。因此需要用戶自行刪除,可以通過指令rm
刪除命名管道文件,也可以在進(jìn)程中調(diào)用unlink
接口。NAME unlink - delete a name and possibly the file it refers to SYNOPSIS #include <unistd.h> int unlink(const char *pathname); RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
??下面是兩個進(jìn)程使用命名管道實現(xiàn)進(jìn)程間通信,client是寫進(jìn)程,負(fù)責(zé)創(chuàng)建namedpipe和刪除namedpipe,并向server發(fā)送數(shù)據(jù),數(shù)據(jù)由用戶交互傳遞。server是讀進(jìn)程,只負(fù)責(zé)讀取client發(fā)送的數(shù)據(jù)。
注意: 對于打開命名管道的寫端,調(diào)用open
時,若此時該命名管道沒有讀端,則寫端會阻塞等待至少一個讀端打開該管道,寫端才會打開。同理,若想打開讀端但是沒有寫端,也會阻塞等待。
//client
#include "common.hpp"
int main()
{
// 1.創(chuàng)建命名管道
umask(0);
int ret = mkfifo(pipename.c_str(), 0666);
if (ret < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
// 2.以寫方式打開命名管道
int wfd = open(pipename.c_str(), O_WRONLY);
if (wfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
//3.向管道中寫入數(shù)據(jù)
char buf[NUM] = {0};
std::cout << "請輸入您想要發(fā)送給服務(wù)端的信息: " << std::endl;
while (true)
{
char *str = fgets(buf, sizeof(buf), stdin);
assert(str);
(void)str;
int n = strlen(buf);
buf[n - 1] = '\0'; // 消除'\n'
if (strcasecmp(buf, "quit") == 0)
break;
int ret = write(wfd, buf, sizeof(buf));
assert(ret > 0);
(void)ret;
}
// 4.退出,關(guān)閉寫端
close(wfd);
unlink(pipename.c_str());
return 0;
}
//server
#include "common.hpp"
int main()
{
// 1.以讀方式打開命名管道
int rfd = open(pipename.c_str(), O_RDONLY);
if (rfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
//2.讀取管道中的數(shù)據(jù)
char buf[NUM] = {0};
while (true)
{
int cnt = read(rfd, buf, sizeof(buf));
if (cnt > 0)
{
buf[cnt] = '\0';
std::cout << "message from client: " << buf << std::endl;
}
else if (cnt == 0)
{
std::cout << "通信結(jié)束" << std::endl;
break;
}
else
{
return 1;
}
}
// 3.關(guān)閉讀端
close(rfd);
return 0;
}
//common.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <cassert>
const std::string pipename = "fifo";
const int NUM = 1024;
?實操演示
管道的特性
??作為特殊的文件,管道具有一些特性(匿名管道和命名管道同時具備)
- 當(dāng)管道為空時或讀進(jìn)程讀完數(shù)據(jù)時,讀進(jìn)程再次讀取時會阻塞等待寫進(jìn)程寫入數(shù)據(jù)后才開始讀取。
- 當(dāng)管道為滿時,讀進(jìn)程沒有讀取數(shù)據(jù),寫進(jìn)程會阻塞等待讀進(jìn)程讀取出一些數(shù)據(jù)后再寫入數(shù)據(jù),否則未被讀取的數(shù)據(jù)可能會被覆蓋。
- 若所有寫進(jìn)程被關(guān)閉,讀進(jìn)程仍在讀取,此時讀進(jìn)程調(diào)用的read函數(shù)會返回0,表示讀取到文件末尾,即讀取結(jié)束
- 若所有讀進(jìn)程被關(guān)閉,寫進(jìn)程再寫入數(shù)據(jù)就無意義了,因此OS會發(fā)送信號SIGPIPE,終止寫進(jìn)程
??這種特性也被稱為“管道的阻塞機(jī)制”。管道的阻塞機(jī)制確保了數(shù)據(jù)在寫進(jìn)程和讀進(jìn)程之間的可靠傳遞和同步處理,提高了數(shù)據(jù)處理的準(zhǔn)確性和效率,為進(jìn)程之間的通信和數(shù)據(jù)交換提供了便利和可靠性。
管道的應(yīng)用:簡易的進(jìn)程池
使用匿名管道制作一個簡易的進(jìn)程池,大概思路:先創(chuàng)建一個父進(jìn)程,然后讓這個父進(jìn)程創(chuàng)建多個子進(jìn)程,通過用戶交互的模式,讓父進(jìn)程下發(fā)指定的任務(wù)給不同的子進(jìn)程。其中,”下發(fā)任務(wù)“這個過程,就是利用管道來實現(xiàn),父進(jìn)程對于每個子進(jìn)程都有唯一一個管道用以傳輸“任務(wù)”數(shù)據(jù)。
-
管理子進(jìn)程
一個父進(jìn)程對多個子進(jìn)程,且每個子進(jìn)程對應(yīng)一個管道,那么肯定要先將多個子進(jìn)程管理起來。根據(jù)“先描述,再組織”的管理思想,我的設(shè)計如下:先將子進(jìn)程描述為一個結(jié)構(gòu)體,該結(jié)構(gòu)體中包含子進(jìn)程pid、子進(jìn)程對應(yīng)管道在父進(jìn)程中的寫端fd、以及一個子進(jìn)程名稱(自定義格式,為了后續(xù)方便調(diào)試觀察)。然后在父進(jìn)程中定義一個容器,用以組織這些創(chuàng)建出來的子進(jìn)程結(jié)構(gòu)體,方便后續(xù)管理。
//描述子進(jìn)程結(jié)構(gòu)體 struct ChildProc { ChildProc(int pid, int write_fd) : _pid(pid), _write_fd(write_fd) { _proc_name = "proc->" + to_string(_pid) + ":" + to_string(_write_fd); } int _pid; int _write_fd; string _proc_name; };
//父進(jìn)程主函數(shù),即整個進(jìn)程池的框架 int main() { //定義一個vector容器,用以組織ChildProc vector<ChildProc> child_processes; // 1.創(chuàng)建子進(jìn)程 CreatProcess(child_processes); // 2.父進(jìn)程下發(fā)命令(用戶交互式) OrderProcess(child_processes); // 3.進(jìn)程退出 WaitProcess(child_processes); cout << "子進(jìn)程已全部成功退出,并被回收!" << endl; return 0; }
-
創(chuàng)建子進(jìn)程
父進(jìn)程循環(huán)創(chuàng)建子進(jìn)程。每次子進(jìn)程創(chuàng)建完畢后,由于父進(jìn)程尚且沒有向管道寫入數(shù)據(jù),當(dāng)前子進(jìn)程read阻塞等待,父進(jìn)程繼續(xù)創(chuàng)建下一個子進(jìn)程。父進(jìn)程每次fork創(chuàng)建完一個子進(jìn)程,要將其描述為ChildProc結(jié)構(gòu)體,再插入管理的容器中。
const int child_process_num = 3; void CreatProcess(vector<ChildProc> &cps) { for (int i = 0; i < child_process_num; i++) { // 1.創(chuàng)建管道 int pipefd[2] = {0}; int ret = pipe(pipefd); if (ret < 0) { perror("The following error happen:"); } // 父進(jìn)程寫,子進(jìn)程讀(父進(jìn)程向子進(jìn)程發(fā)送命令) // 2.創(chuàng)建子進(jìn)程,一個子進(jìn)程在父進(jìn)程中對應(yīng)一個寫端 int id = fork(); assert(id >= 0); // 子進(jìn)程 if (id == 0) { // 3.關(guān)閉不要的fd close(pipefd[1]); // 子進(jìn)程接收并執(zhí)行命令 while (true) { int n = 0; // 此時管道為空時,子進(jìn)程read阻塞等待父進(jìn)程下發(fā)命令 int cnt = read(pipefd[0], &n, sizeof(int)); if (cnt > 0) { //FuncArray在Tasks.hpp中實現(xiàn) FuncArray[n](); cout << endl; } else if (cnt == 0) { //父進(jìn)程退出,即寫端關(guān)閉,read返回值為0,子進(jìn)程也隨之退出 cout << "讀取結(jié)束,子進(jìn)程退出" << " pid: " << getpid() << endl; break; } else { exit(1); } } close(pipefd[0]); exit(0); } // 父進(jìn)程 // 將子進(jìn)程(子進(jìn)程pid和寫端fd)管理起來,父進(jìn)程才方便下發(fā)命令 cps.push_back(ChildProc(id, pipefd[1])); close(pipefd[0]); } }
在common.hpp頭文件中,簡單寫幾個子進(jìn)程可執(zhí)行的任務(wù),這里沒有定義實際任務(wù),只是打印語句以表示任務(wù)成功執(zhí)行。后續(xù)這塊可完善。
#pragma once #include <iostream> #include <functional> using namespace std; void TaskWeChat() { cout << "wechat is running..." << endl; } void TaskChrome() { cout << "chrome is running..." << endl; } void TaskSteam() { cout << "steam is running.." << endl; } const function<void()> FuncArray[] = {TaskWeChat,TaskChrome,TaskSteam};
-
父進(jìn)程下發(fā)命令給子進(jìn)程
int SelectBoard() { //用戶選擇面板 cout << "#########################" << endl; cout << "# 0.wechat 1.chrome #" << endl; cout << "# 2.steam 3.quit #" << endl; cout << "#########################" << endl; cout << "請選擇你將下發(fā)的命令: "; int command = 0; cin >> command; return command; } void OrderProcess(vector<ChildProc> &cps) { int num = -1; while (true) { // 用戶交互, 下發(fā)命令 int command = SelectBoard(); if (command == 3) break; if (command < 0 || command > 2) continue; // 輪詢調(diào)用子進(jìn)程 num = (num + 1) % cps.size(); printf("調(diào)用了子進(jìn)程%d號, ", num); cout << cps[num]._proc_name << endl; // 將命令寫入對應(yīng)子進(jìn)程的管道中 write(cps[num]._write_fd, &command, sizeof(command)); sleep(1); } }
-
等待子進(jìn)程進(jìn)程退出并回收
void WaitProcess(vector<ChildProc> &cps) { // 先關(guān)閉父進(jìn)程的所有寫端,根據(jù)管道的特性(關(guān)閉管道所有寫端,讀端退出),關(guān)閉寫端讓對應(yīng)的子進(jìn)程退出 // 隨后,父進(jìn)程要回收所有的子進(jìn)程 for (auto &cp : cps) { close(cp._write_fd); waitpid(cp._pid, nullptr, 0); } }
?運行程序,并進(jìn)行測試。發(fā)現(xiàn)讓父進(jìn)程發(fā)送0、1、2命令都正常,可當(dāng)發(fā)送3號退出命令,讓父進(jìn)程等待并回收子進(jìn)程時,程序卡住了。
這里有一個隱藏的bug。匿名管道,我們運用了子進(jìn)程繼承父進(jìn)程文件描述符表的機(jī)制,但在進(jìn)程池中,由于利用了這個繼承機(jī)制,又會產(chǎn)生bug。父進(jìn)程創(chuàng)建0號子進(jìn)程時是沒問題的,如我們預(yù)期。當(dāng)創(chuàng)建1號子進(jìn)程時,由于此時父進(jìn)程文件描述符表有了0號子進(jìn)程的寫端fd,被1號子進(jìn)程繼承了,所以此時0號子進(jìn)程的管道有了兩個寫端fd,這并不符合我們的預(yù)期,我們的設(shè)計是讓父進(jìn)程和每個子進(jìn)程之間有一個獨立的管道。若創(chuàng)建三個子進(jìn)程,最后進(jìn)程池的結(jié)構(gòu)如下:
再看看我們剛才寫的WaitProcess函數(shù)。造成阻塞的原因是:close
關(guān)閉第一個子進(jìn)程管道的寫端時,并沒有關(guān)閉全部寫端,因此該子進(jìn)程并沒有退出,waitpid
阻塞等待。
void WaitProcess(vector<ChildProc> &cps)
{
for (auto &cp : cps)
{
close(cp._write_fd);
waitpid(cp._pid, nullptr, 0);
}
}
??解決方法:
-
因為最后一個子進(jìn)程只有父進(jìn)程一個寫端,因此可以先關(guān)閉最后一個子進(jìn)程的寫端fd,此時該子進(jìn)程成功退出,OS自動關(guān)閉其所有文件描述符,因此它由于bug鏈接到其它子進(jìn)程的管道上的寫端fd會被關(guān)閉。如此逆向
close
即可完成。 -
這種進(jìn)程池結(jié)構(gòu)并不是我們想要的,因此直接在創(chuàng)建子進(jìn)程時關(guān)閉對應(yīng)管道錯誤的寫端fd,形成我們期望的進(jìn)程池結(jié)構(gòu),才是上策。修改代碼如下:
void CreatProcess(vector<ChildProc> &cps) { //創(chuàng)建一個容器wfds,用以存放父進(jìn)程創(chuàng)建一個子進(jìn)程時,已經(jīng)擁有的寫端fd vector<int> wfds; for (int i = 0; i < child_process_num; i++) { int pipefd[2] = {0}; int ret = pipe(pipefd); if (ret < 0) { perror("The following error happen:"); } // 每次創(chuàng)建管道后,將寫端fd存入wfds wfds.push_back(pipefd[1]); int id = fork(); assert(id >= 0); if (id == 0) { // 子進(jìn)程關(guān)閉從父進(jìn)程繼承的所有寫端(包括子進(jìn)程自己管道的和其它管道的寫端fd)!! for (auto &wfd : wfds) { close(wfd); } // 錯誤寫法,在當(dāng)前子進(jìn)程push寫端fd,其它子進(jìn)程看不到?。?!寫時拷貝問題 // wfds.push_back(pipefd[1]); // for (auto& wfd : wfds) // { // close(wfd); // cout << "關(guān)閉fd: " << wfd << endl; // } while (true) { int n = 0; int cnt = read(pipefd[0], &n, sizeof(int)); if (cnt > 0) { FuncArray[n](); cout << endl; } else if (cnt == 0) { cout << "讀取結(jié)束,子進(jìn)程退出" << " pid: " << getpid() << endl; break; } else { exit(1); } } close(pipefd[0]); exit(0); } cps.push_back(ChildProc(id, pipefd[1])); close(pipefd[0]); } }
此時再次發(fā)送quit指令,觀察到子進(jìn)程成功退出并被父進(jìn)程回收。
3. System V共享內(nèi)存
另一種進(jìn)程間通信的方式是共享內(nèi)存。共享內(nèi)存是最快的進(jìn)程間通信(IPC)形式。因為其通信過程中,傳輸數(shù)據(jù)時,不再需要經(jīng)過內(nèi)核的“中轉(zhuǎn)”,而是直接通過地址的映射獲得共享資源。
共享內(nèi)存的概念
??在進(jìn)程間通信(IPC)中,共享內(nèi)存是一種特殊的通信機(jī)制,允許多個進(jìn)程共享同一塊物理內(nèi)存區(qū)域,從而實現(xiàn)高效的數(shù)據(jù)交換和共享。與其他IPC方式相比,共享內(nèi)存的主要優(yōu)勢是數(shù)據(jù)直接存儲在內(nèi)存中,避免了數(shù)據(jù)在進(jìn)程之間的復(fù)制,從而提高了通信的速度和效率。缺點是無法保證數(shù)據(jù)的安全性。
共享內(nèi)存的結(jié)構(gòu)
共享內(nèi)存(Shared Memory Segment,簡稱shm),是一段由多個進(jìn)程共享的物理內(nèi)存空間,各個進(jìn)程將其通過頁表映射到自己的地址空間共享區(qū)中。使得多個進(jìn)程可以訪問相同的空間,實現(xiàn)交換數(shù)據(jù),完成IPC。圖中,struct_shm(在真正的內(nèi)核中并非這個名字)是內(nèi)核中用于管理共享內(nèi)存的一個結(jié)構(gòu)體,每個共享內(nèi)存對應(yīng)一個該結(jié)構(gòu)體,該結(jié)構(gòu)體中包含了共享內(nèi)存區(qū)的各種屬性和元數(shù)據(jù),如共享內(nèi)存的大小、權(quán)限、關(guān)聯(lián)進(jìn)程等信息,這些結(jié)構(gòu)體也會被OS組織并管理起來。
共享內(nèi)存 = 管理共享內(nèi)存信息的數(shù)據(jù)結(jié)構(gòu) + 真正的共享內(nèi)存空間
共享內(nèi)存的使用
??以下假設(shè)使用共享內(nèi)存通信的只有兩個進(jìn)程,實際上一個共享內(nèi)存可以連接多個進(jìn)程。
-
共享內(nèi)存的獲取
通信雙方,必須先能看到同一份共享資源,才能進(jìn)行通信。獲取的方式是,一方負(fù)責(zé)創(chuàng)建共享內(nèi)存,另一方查找對方創(chuàng)建的共享內(nèi)存,用到的接口是
shmget
。NAME shmget - allocates a System V shared memory segment SYNOPSIS #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); RETURN VALUE On success, a valid shared memory identifier is returned. On error, -1 is returned, and errno is set to indicate the error.
??參數(shù)
-
key
用于標(biāo)識唯一的一個共享內(nèi)存段。多個進(jìn)程約定同一個key,可獲取同一份共享內(nèi)存。key是一個整型,可以通過
ftok
函數(shù)獲取key_t ftok(const char *pathname, int proj_id);
ftok
的參數(shù)是一個路徑字符串pathname
和一個整型值項目idproj_id
。內(nèi)含特定的算法,通過這兩個參數(shù)生成一個重復(fù)率較低的key值,并作為返回值。只要參數(shù)相同,生成的key值就相同。 -
size
共享內(nèi)存的大小,單位是字節(jié)byte
-
shmflg
標(biāo)記位。主要的標(biāo)記有
IPC_CREAT
和IPC_EXCL
,若shmflg==IPC_CREAT
,表示若以key為鍵值的共享內(nèi)存不存在,創(chuàng)建之。若存在,用之即可。若shmflg==IPC_CREAT|IPC_EXCL
,表示若以key為鍵值的共享內(nèi)存不存在,創(chuàng)建之。若存在,報錯。(IPC_EXCL
不能單獨使用,只與IPC_CREAT
一起使用)。另外,標(biāo)記位還包含mode_flags
,它用于定義共享內(nèi)存的權(quán)限,格式與open
的參數(shù)mode
相同 ,指明onwer、group、world(運行進(jìn)程者)對于共享內(nèi)存的權(quán)限。
??返回值
? 共享內(nèi)存描述符(shared memory identifier,簡稱shmid),用于標(biāo)識唯一的一段共享內(nèi)存。
??參數(shù)key和返回值shmid的區(qū)別?
key在函數(shù)調(diào)用時使用,意味著共享內(nèi)存可能尚未存在。key的作用是在進(jìn)程獲取共享內(nèi)存之前(此時共享內(nèi)存可能還沒創(chuàng)建),唯一標(biāo)識一個共享內(nèi)存段,使通信雙方能夠約定同一個共享內(nèi)存段。這樣,一個進(jìn)程創(chuàng)建以key為鍵值的shm,另一個進(jìn)程查找以key為鍵值的shm,并獲取相同的shmid。shmid用于進(jìn)程獲取共享內(nèi)存后,唯一標(biāo)識一個共享內(nèi)存段,這個標(biāo)識符可以用于后續(xù)的共享內(nèi)存操作 。
二者作用大致相同,但作用的時間節(jié)點不同。
-
-
進(jìn)程與共享內(nèi)存建立聯(lián)系
上一步做的事,只是讓通信雙方獲知了用哪一塊共享內(nèi)存(獲取相同的shmid),但并沒有真正與共享內(nèi)存建立聯(lián)系。那么現(xiàn)在就要把進(jìn)程和共享內(nèi)存鏈接起來,即在各自的地址空間中映射共享內(nèi)存段。需要用到的接口是
shmat
。(shm attach)SYNOPSIS #include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); RETURN VALUE On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate the cause of the error.
??參數(shù)
-
shmid
就是第一步中獲得的shmid。
-
shmaddr
指定共享內(nèi)存映射到當(dāng)前進(jìn)程的地址。一般設(shè)置為NULL,由OS自動選擇映射的地址,較為安全可靠。
-
shmflg
指明鏈接共享內(nèi)存的讀寫模式。設(shè)置
SHM_RDONLY
為只讀, 否則是即讀又寫(一般設(shè)置為0)。沒有只寫的選項。注意,進(jìn)程必須有對應(yīng)權(quán)限才能設(shè)定對應(yīng)的shmflg,如:設(shè)置SHM_RDONLY
,進(jìn)程對該共享內(nèi)存必須有讀權(quán)限。設(shè)置為0,進(jìn)程對該共享內(nèi)存必須有讀權(quán)限和寫權(quán)限。權(quán)限在shmget
函數(shù)中設(shè)定。
??返回值
? 一個void*類型的指針,指向當(dāng)前進(jìn)程地址空間中映射共享內(nèi)存段的起始地址,后續(xù)該地址為shmaddr。
-
-
開始通信,交換數(shù)據(jù)
不像管道需要調(diào)用系統(tǒng)接口寫入和讀取數(shù)據(jù),共享內(nèi)存只需要在映射的地址空間中讀寫數(shù)據(jù),這段空間的起始地址在第二步已經(jīng)獲得,直接當(dāng)成數(shù)組的起始地址用就行。注意,獲得的指針shmaddr是void*類型,不同場景下可能需要強(qiáng)轉(zhuǎn)成其它類型來使用。
-
進(jìn)程與共享內(nèi)存解除聯(lián)系
通信結(jié)束后,通信雙方無需再引用共享內(nèi)存,即可先解除與共享內(nèi)存的聯(lián)系。因為一個共享內(nèi)存可能會被多對進(jìn)程引用,而不止一個,所以只有當(dāng)引用該共享內(nèi)存的進(jìn)程數(shù)量為0時,才會刪除這個共享內(nèi)存。解除進(jìn)程與共享內(nèi)存的聯(lián)系,用到接口
shmdt
(shm detach)SYNOPSIS #include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr); RETURN VALUE On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
傳入shmaddr即可,返回值無意義,只是用作判斷函數(shù)調(diào)用成功與否。
-
刪除共享內(nèi)存
NAME shmctl - System V shared memory control SYNOPSIS #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
??參數(shù)
-
shmid
要刪除的共享內(nèi)存描述符
-
cmd
控制指令。刪除的指令是
IPC_RMID
。 -
buf
用于接收其它指令的返回值。刪除時傳入NULL即可。
-
?注意:進(jìn)程間通信時,創(chuàng)建和刪除共享內(nèi)存的工作最好由一個進(jìn)程來完成,其它進(jìn)程只是與已創(chuàng)建的共享內(nèi)存進(jìn)行連接和斷連即可。
除了系統(tǒng)調(diào)用,還有一些關(guān)于共享內(nèi)存的指令:
ipcs -m #查看共享內(nèi)存信息
ipcrm [OPTION] [...] #刪除共享內(nèi)存
OPTION:
-M 按key刪除
-m 按shmid刪除
代碼實現(xiàn)
由于利用共享內(nèi)存實現(xiàn)IPC時,總是有相似的前置工作(創(chuàng)建和連接)和后置工作(斷連和刪除),因此可以將其封裝在一個類中,將前置工作封裝在類的構(gòu)造函數(shù)中,后置工作封裝在類的析構(gòu)函數(shù)中,實現(xiàn)共享內(nèi)存自動化搭建和銷毀。如下代碼:文章來源:http://www.zghlxwxcb.cn/news/detail-591065.html
//頭文件common.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <stdlib.h>
#include <cassert>
const std::string pathname = ".";
const int proj_id = 666;
const int shm_size = 4096;
#define CREATER 0
#define USER 1
class smart_init
{
public:
smart_init(int type)
{
// 獲取共享內(nèi)存
assert(type == CREATER || type == USER);
if (type == CREATER)
_shmid = creatShm(getKey());
else if (type == USER)
_shmid = searchShm(getKey());
_type = type;
// 與共享內(nèi)存建立聯(lián)系
_shm_addr = attachShm(_shmid);
}
~smart_init()
{
// 與共享內(nèi)存斷開聯(lián)系
detachShm(_shm_addr);
if (_type == CREATER)
{
remoteShm(_shmid);
}
}
void *get_shmaddr()
{
return _shm_addr;
}
private:
key_t getKey();
int creatShm(key_t k);
int searchShm(key_t k);
int getShm(key_t k, int flag);
void *attachShm(int shmid);
void detachShm(const void *shmaddr);
void remoteShm(int shmid);
private:
int _type;
int _shmid;
void *_shm_addr;
};
std::string toHex(int n)
{
char buf[64] = {0};
snprintf(buf, sizeof(buf), "0x%x", n);
return std::string(buf);
}
key_t smart_init::getKey()
{
key_t k = ftok(pathname.c_str(), proj_id);
if (k == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(1);
}
return k;
}
int smart_init::getShm(key_t k, int flag)
{
int shmid = shmget(k, shm_size, flag);
if (shmid == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
int smart_init::creatShm(key_t k)
{
umask(0);
return getShm(k, IPC_CREAT | IPC_EXCL | 0666);
}
int smart_init::searchShm(key_t k)
{
umask(0);
return getShm(k, 0666);
}
void *smart_init::attachShm(int shmid)
{
void *shm_ptr = shmat(shmid, nullptr, 0);
if (shm_ptr == (void *)-1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(3);
}
return shm_ptr;
}
void smart_init::detachShm(const void *shmaddr)
{
int ret = shmdt(shmaddr);
if (ret == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(4);
}
}
void smart_init::remoteShm(int shmid)
{
int ret = shmctl(shmid, IPC_RMID, nullptr);
if (ret == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
exit(5);
}
}
//進(jìn)程A
#include "common.hpp"
int main()
{
smart_init si(CREATER);
char* shm_ptr = (char*)si.get_shmaddr();
//通信
int cnt = 0;
const char* msg = "i am process A";
strcpy(shm_ptr,msg);
sleep(10);
return 0;
}
//進(jìn)程B
#include "common.hpp"
int main()
{
smart_init si(USER);
//通信
char* shm_ptr = (char*)si.get_shmaddr();
printf("message from A: %s\n",shm_ptr);
return 0;
}
ENDING…文章來源地址http://www.zghlxwxcb.cn/news/detail-591065.html
到了這里,關(guān)于【Linux】進(jìn)程間通信——管道/共享內(nèi)存的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!