Linux知識(shí)點(diǎn) – 進(jìn)程間通信(二)
一、System V共享內(nèi)存
1.原理
先在內(nèi)存中申請(qǐng)空間,然后將這段空間映射到不同進(jìn)程的地址空間中,這就叫做共享內(nèi)存;
一般都是映射在進(jìn)程的堆棧之間的共享區(qū);
共享內(nèi)存不屬于任何一個(gè)進(jìn)程,它屬于操作系統(tǒng);
操作系統(tǒng)對(duì)共享內(nèi)存的管理,是先描述再組織,先通過內(nèi)核數(shù)據(jù)結(jié)構(gòu)描述共享內(nèi)存的屬性信息,再將它們組織起來;
共享內(nèi)存 = 共享內(nèi)存塊 + 對(duì)應(yīng)的共享內(nèi)存的內(nèi)核數(shù)據(jù)結(jié)構(gòu);
共享區(qū)屬于用戶空間,不用經(jīng)過系統(tǒng)調(diào)用,直接可以訪問;
雙方進(jìn)程如果要通信,直接進(jìn)行內(nèi)存級(jí)的讀寫即可;
之前的管道是一種文件。是OS中的一種數(shù)據(jù)結(jié)構(gòu),所以用戶無權(quán)直接訪問,需要進(jìn)行系統(tǒng)調(diào)用;
2.申請(qǐng)共享內(nèi)存
shmget接口能夠申請(qǐng)共享內(nèi)存;
-
參數(shù):
key:通信雙方的進(jìn)程,通過key值來保證是通信的雙方來創(chuàng)建的共享內(nèi)存,相當(dāng)于一個(gè)驗(yàn)證值,需要在系統(tǒng)內(nèi)是唯一的,通信雙方使用同一個(gè)key;
size:內(nèi)存大小,一般是頁(4byte)的整數(shù)倍;
shmflag:有兩個(gè)選項(xiàng):IPC_CREAT和IPC_EXCL;
IPC_CREAT能單獨(dú)出現(xiàn),代表如果共享內(nèi)存已存在,則獲取之;如果不存在,就創(chuàng)建之,并返回;
IPC_EXCL必須和IPC_CREAT組合使用,代表如果共享內(nèi)存不存在,就創(chuàng)建之,并返回;如果已存在,出錯(cuò)并返回;
0就代表IPC_CREAT;
返回值:成功會(huì)返回共享內(nèi)存id,失敗返回-1;
ftok函數(shù):生成唯一的key
-
參數(shù):
==pathname:==文件路徑,一定要保證用戶有權(quán)限;
==id:==項(xiàng)目id,隨便給,一般是0 - 255;
返回值:成功,返回key值;失敗,返回-1;
ftok會(huì)拿路徑文件的inode,和id形成一個(gè)唯一的key,生成結(jié)果是有可能重復(fù)的;
3.System V共享內(nèi)存的使用
- Makefile:
.PHONY:all
all:shmClient shmServer
shmServer:shmServer.cc
g++ -o $@ $^ -std=c++11
shmClient:shmClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
claen:
rm -f shmServer shmClient
- Log.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include<iostream>
#include<ctime>
#define DeBug 0
#define Notice 1
#define Waring 2
#define Error 3
const std::string msg[] = {
"DeBug",
"Notice",
"Waring",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
- comm.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cassert>
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/usr/lmx" //路徑,一定保證有權(quán)限
#define PROJ_ID 0X66
#define SHM_SIZE 4096 //共享內(nèi)存大小,最好是頁(4byte)的整數(shù)倍
#endif
-
shmServer.cc
#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 key = ftok(PATH_NAME, PROJ_ID);
if (key == -1)
{
perror(“ftok”);
exit(1);
}
Log("creat key done", DeBug) << "server key : " << TransToHex(key) << endl;
// 2.創(chuàng)建共享內(nèi)存 -- 建議創(chuàng)建一個(gè)全新的共享內(nèi)存 -- 通信的發(fā)起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(2);
}
Log("shm creat done", DeBug) << "shmid : " << shmid << endl;
//3.將指定的共享內(nèi)存,掛接到自己的地址空間
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
Log("attach shm done", DeBug) << "shmid : " << shmid << endl;
//這里就是通信邏輯了
//將共享內(nèi)存看作一個(gè)大字符串
//shmaddr就是這個(gè)字符串的起始地址
for(;;)
{
printf("%s\n", shmaddr);//不斷打印這個(gè)字符串的內(nèi)容
if(strcmp(shmaddr, "quit") == 0)
{
break;
}
sleep(1);
}
//4.將指定的共享內(nèi)存,從自己的地址空間中去關(guān)聯(lián)
int n = shmdt(shmaddr);
if(n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "shmid : " << shmid << endl;
//5.刪除共享內(nèi)存,IPC_RMID即便是有進(jìn)程和當(dāng)下的shm掛接,依舊刪除共享內(nèi)存
n = shmctl(shmid, IPC_RMID, nullptr);
if(n == -1)
{
perror("shmctl");
exit(4);
}
Log("delete shm done", DeBug) << "shmid : " << shmid << endl;
return 0;
}
注意:
(1)
要保證創(chuàng)建出唯一的key;*
(2)
創(chuàng)建全新的共享內(nèi)存,0666代表共享內(nèi)存的權(quán)限;
共享內(nèi)存的大小最好是頁的整數(shù)倍,否則會(huì)造成空間浪費(fèi),多開空間,但是沒有權(quán)限訪問;
第二次創(chuàng)建的時(shí)候,提示共享內(nèi)存已存在;
(3)ipcs -m:查看共享內(nèi)存信息;
ipcrm -m shmid:刪除共享內(nèi)存(不能用key刪除)
共享內(nèi)存的生命周期隨內(nèi)核;
與文件不一樣,文件的生命周期,如果進(jìn)程退出,沒有其他進(jìn)程再關(guān)聯(lián)這個(gè)文件,那么就會(huì)被回收;
perms屬性就是共享內(nèi)存的權(quán)限,
(4)因此,當(dāng)進(jìn)程結(jié)束后,共享內(nèi)存還存在,我們繼續(xù)要?jiǎng)h除它,使用系統(tǒng)接口:
shmctl:刪除共享內(nèi)存
(5)nattch屬性是掛接的共享內(nèi)存?zhèn)€數(shù),共享內(nèi)存創(chuàng)建好之后,需要掛接在自己的進(jìn)程地址空間;
shmat:掛接共享內(nèi)存
參數(shù):
shmid:共享內(nèi)存id
shmaddr:掛接虛擬地址,直接設(shè)為0,讓os掛接
shmflg:掛接方式
返回值:成功返回共享內(nèi)存addr虛擬地址,失敗返回-1
使用:
將返回值作為共享內(nèi)存的起始地址;
shmdt:去關(guān)聯(lián)
參數(shù):
shmaddr:共享內(nèi)存地址
返回值:成功返回0,失敗返回-1
- shmClient.cc
#include "comm.hpp"
int main()
{
// 客戶端也獲取key
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
Log("creat key failed", Error) << "client key : " << key << endl;
exit(1);
}
Log("creat key done", DeBug) << "client key : " << key << endl;
// 獲取共享內(nèi)存
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid == -1)
{
Log("creat shm failed", Error) << "client key : " << key << endl;
exit(2);
}
Log("creat shm done", DeBug) << "client key : " << key << endl;
// 掛接共享內(nèi)存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
Log("attach shm failed", Error) << "client key : " << key << endl;
}
Log("attach shm done", DeBug) << "client key : " << key << endl;
// 使用
//client將共享內(nèi)存看作一個(gè)char類型的buffer
//客戶端從鍵盤讀取消息,直接讀到共享內(nèi)存中
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if(s > 0)
{
shmaddr[s - 1] = 0;
if(strcmp(shmaddr, "quit") == 0)//讀到quit,客戶端退出
{
break;
}
}
}
// char a = 'a';
// for(; a <= 'z'; a++)
// {
// //每一次都向shmaddr(共享內(nèi)存的起始地址)寫入
// snprintf(shmaddr, SHM_SIZE - 1,
// "hello server, 我是其他進(jìn)程,我的pid: %d, inc: %c\n",
// getpid(), a);
// sleep(2);
// }
// 去關(guān)聯(lián)
int n = shmdt(shmaddr);
if (n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "client key : " << key << endl;
// client不需要?jiǎng)h除shm
return 0;
}
注意:
(1)共享內(nèi)存的使用,直接將共享內(nèi)存看作一個(gè)char類型的buffer,直接向里面寫入數(shù)據(jù)
從stdin中鍵盤讀取消息,直接讀取到shmaddr這個(gè)地址,即共享內(nèi)存的起始地址;
運(yùn)行結(jié)果:
服務(wù)端:
客戶端:
-
注:
(1)只要是通信雙方使用shm,一方直接向共享內(nèi)存中寫入數(shù)據(jù),另一方就可以立馬看到對(duì)方寫入的數(shù)據(jù);共享內(nèi)存是所有進(jìn)程間通信中最快的,不需要過多的拷貝;
(2)管道通信中,一次通信需要多次拷貝,用戶從鍵盤輸入數(shù)據(jù)到緩沖區(qū)是一次拷貝,從緩沖區(qū)向管道文件寫入數(shù)據(jù)又是一次拷貝,從管道文件向緩沖區(qū)讀取數(shù)據(jù)是一次拷貝,從緩沖區(qū)將數(shù)據(jù)打印又是一次拷貝;
(3)共享內(nèi)存只需要兩次拷貝,從鍵盤輸入的數(shù)據(jù)直接寫入shm,這是一次拷貝,直接將shm的數(shù)據(jù)打印出來,這是第二次拷貝;
4.為共享內(nèi)存添加訪問控制
從上面的結(jié)果可以看出,即便是客戶端還沒有掛接共享內(nèi)存,服務(wù)端就已經(jīng)開始不停讀取數(shù)據(jù)了,這就表明共享內(nèi)存是不帶訪問控制的,會(huì)帶來一定的并發(fā)問題;
然而,管道是自帶訪問控制的,我們可以利用管道通信來為共享內(nèi)存添加訪問控制;
comm.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/home/lmx" // 路徑,一定保證有權(quán)限
#define PROJ_ID 0X66
#define SHM_SIZE 4096 // 共享內(nèi)存大小,最好是頁(4byte)的整數(shù)倍
#define FIFO_NAME "./fifo"
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
(void)n;
Log("creat fifo succsee", Notice) << "\n";
}
~Init()
{
unlink(FIFO_NAME);
Log("remove fifo succsee", Notice) << "\n";
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int flags)
{
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
Log("waiting...", Notice) << "\n";
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
Log("aweaking...", Notice) << "\n";
}
void CloseFIFO(int fd)
{
close(fd);
}
#endif
注:
(1)創(chuàng)建了一個(gè)類,類的構(gòu)造函數(shù)有創(chuàng)建管道文件,一旦類實(shí)例化出對(duì)象,調(diào)用構(gòu)造函數(shù),就能夠創(chuàng)建一個(gè)管道文件,后面就是對(duì)管道文件的讀寫控制了;
shmServer.cc
#include "comm.hpp"
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
int main()
{
Init init;
// 對(duì)應(yīng)的程序在加載的時(shí)候,會(huì)自動(dòng)構(gòu)建全局變量,就要調(diào)用該類構(gòu)造函數(shù) -- 創(chuàng)建管道文件
// 程序退出的時(shí)候,全局變量會(huì)被析構(gòu),會(huì)自動(dòng)刪除管道文件
// 1.創(chuàng)建公共的key值
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key == -1)
{
perror("ftok");
exit(1);
}
Log("creat key done", DeBug) << "server key : " << TransToHex(key) << endl;
// 2.創(chuàng)建共享內(nèi)存 -- 建議創(chuàng)建一個(gè)全新的共享內(nèi)存 -- 通信的發(fā)起者
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(2);
}
Log("shm creat done", DeBug) << "shmid : " << shmid << endl;
// 3.將指定的共享內(nèi)存,掛接到自己的地址空間
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attach shm done", DeBug) << "shmid : " << shmid << endl;
// 這里就是通信邏輯了
// 將共享內(nèi)存看作一個(gè)大字符串
// shmaddr就是這個(gè)字符串的起始地址
//使用管道進(jìn)行訪問控制
int fd = OpenFIFO(FIFO_NAME, READ);
for (;;)
{
Wait(fd);//等待客戶端響應(yīng),
//使用管道文件的訪問控制,如果客戶端沒有向管道內(nèi)寫入數(shù)據(jù),那么該進(jìn)程會(huì)一直阻塞
printf("%s\n", shmaddr); // 不斷打印這個(gè)字符串的內(nèi)容
if (strcmp(shmaddr, "quit") == 0)
{
break;
}
sleep(1);
}
CloseFIFO(fd);
// 4.將指定的共享內(nèi)存,從自己的地址空間中去關(guān)聯(lián)
int n = shmdt(shmaddr);
if (n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "shmid : " << shmid << endl;
// 5.刪除共享內(nèi)存,IPC_RMID即便是有進(jìn)程和當(dāng)下的shm掛接,依舊刪除共享內(nèi)存
n = shmctl(shmid, IPC_RMID, nullptr);
if (n == -1)
{
perror("shmctl");
exit(4);
}
Log("delete shm done", DeBug) << "shmid : " << shmid << endl;
return 0;
}
注:
(1)在服務(wù)端先創(chuàng)建一個(gè)管道文件
(2)在讀取共享內(nèi)存中的數(shù)據(jù)前,先讀取管道數(shù)據(jù),看客戶端是否響應(yīng);
shmClient.cc
#include "comm.hpp"
int main()
{
// 客戶端也獲取key
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
Log("creat key failed", Error) << "client key : " << key << endl;
exit(1);
}
Log("creat key done", DeBug) << "client key : " << key << endl;
// 獲取共享內(nèi)存
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid == -1)
{
Log("creat shm failed", Error) << "client key : " << key << endl;
exit(2);
}
Log("creat shm done", DeBug) << "client key : " << key << endl;
// 掛接共享內(nèi)存
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
Log("attach shm failed", Error) << "client key : " << key << endl;
}
Log("attach shm done", DeBug) << "client key : " << key << endl;
// 使用
//client將共享內(nèi)存看作一個(gè)char類型的buffer
//客戶端從鍵盤讀取消息,直接讀到共享內(nèi)存中
//使用管道進(jìn)行訪問控制
int fd = OpenFIFO(FIFO_NAME, WRITE);
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
if(s > 0)
{
shmaddr[s - 1] = 0;
Signal(fd);//向管道寫入數(shù)據(jù)
if(strcmp(shmaddr, "quit") == 0)//讀到quit,客戶端退出
{
break;
}
}
}
CloseFIFO(fd);
// 去關(guān)聯(lián)
int n = shmdt(shmaddr);
if (n == -1)
{
perror("shmdt");
exit(3);
}
Log("detach shm done", DeBug) << "client key : " << key << endl;
// client不需要?jiǎng)h除shm
return 0;
}
注:
(1)在向共享內(nèi)存寫入數(shù)據(jù)前,先向管道寫入信號(hào),表明客戶端準(zhǔn)備寫入數(shù)據(jù),喚醒服務(wù)端:
運(yùn)行結(jié)果:
當(dāng)運(yùn)行服務(wù)端,但是客戶端未響應(yīng)時(shí),服務(wù)端會(huì)等待客戶端響應(yīng),進(jìn)程阻塞;
當(dāng)客戶端響應(yīng)時(shí),服務(wù)端會(huì)被喚醒,讀取共享內(nèi)存中的數(shù)據(jù):
退出:
二、信號(hào)量(概念理解)
1.概念
-
基于對(duì)共享內(nèi)存的理解:
為了讓進(jìn)程間通信,讓不同的進(jìn)程之間,看到同一份資源,我們之前講的所有的進(jìn)程間通信都是基于這種方式;
而讓不同的進(jìn)程看到同一份資源,比如共享內(nèi)存,也帶來了一些時(shí)序問題,會(huì)造成數(shù)據(jù)的不一致 -
概念
(1)臨界資源:多個(gè)進(jìn)程(執(zhí)行流)看到的公共的一份資源;
(2)臨界區(qū):自己的進(jìn)程,訪問臨界資源的代碼;
(3)互斥:為了更好的進(jìn)行臨界區(qū)的維護(hù),可以讓多執(zhí)行流在任何時(shí)刻,都只能有一個(gè)進(jìn)程進(jìn)入臨界區(qū);
(4)原子性:要么不做,要么做完,沒有中間狀態(tài);
2.信號(hào)量
我們平??措娪扒?,會(huì)先買票,電影院中的座位就相當(dāng)于資源,當(dāng)你買了票,這個(gè)座位就真正屬于你,買票的本質(zhì)就是對(duì)座位的預(yù)定機(jī)制;
對(duì)于進(jìn)程來說,訪問臨界資源中的一部分,不能讓進(jìn)程直接去使用臨界資源,需要先申請(qǐng)信號(hào)量;
信號(hào)量的本質(zhì)是一個(gè)計(jì)數(shù)器;
-
申請(qǐng)信號(hào)量:
(1)申請(qǐng)信號(hào)量的本質(zhì),就是讓信號(hào)量技術(shù)器 - -;
(2)申請(qǐng)信號(hào)量成功,臨界資源內(nèi)部,一定給進(jìn)程預(yù)留了需要的資源,申請(qǐng)信號(hào)量的本質(zhì)就是對(duì)臨界資源的一種預(yù)定機(jī)制; -
釋放信號(hào)量:
釋放信號(hào)量就是將計(jì)數(shù)器++;
如果將信號(hào)量計(jì)數(shù)器設(shè)為全局變量(整數(shù)n,存放在共享內(nèi)存),讓多個(gè)進(jìn)程看到同一個(gè)全局變量,大家都能夠進(jìn)行信號(hào)量的申請(qǐng),這樣是不行的;
因?yàn)镃PU在執(zhí)行n++這個(gè)指令的時(shí)候,其實(shí)執(zhí)行了三條語句:
(1)將內(nèi)存中的數(shù)據(jù)加載到CPU內(nèi)的寄存器(讀指令);
(2)n–(執(zhí)行指令);
(3)將CPU修改完畢的值再寫入內(nèi)存(寫指令);
而執(zhí)行流在執(zhí)行的時(shí)候,在任何時(shí)刻都是能被切換的;
例如:
如果信號(hào)量剛開始是5,client在申請(qǐng)信號(hào)量的時(shí)候,第一步就被切換了,寄存器里的數(shù)據(jù)保存為上下文數(shù)據(jù),
由server申請(qǐng)信號(hào)量,如果server將信號(hào)量減到2了,此時(shí)server被切換,client回來
client回來的時(shí)候就會(huì)將上下文數(shù)據(jù)恢復(fù),將信號(hào)量恢復(fù)為5,再申請(qǐng)信號(hào)量,這時(shí)信號(hào)量就變?yōu)榱?;文章來源:http://www.zghlxwxcb.cn/news/detail-631405.html
寄存器只有一套,被所有執(zhí)行流共享,但是寄存器內(nèi)的數(shù)據(jù),屬于每一個(gè)執(zhí)行流,屬于該執(zhí)行流的上下文數(shù)據(jù);
這樣設(shè)計(jì),會(huì)導(dǎo)致信號(hào)量是不安全的;
因此,申請(qǐng)和釋放信號(hào)量這兩個(gè)操作,必須是原子的;文章來源地址http://www.zghlxwxcb.cn/news/detail-631405.html
到了這里,關(guān)于Linux知識(shí)點(diǎn) -- 進(jìn)程間通信(二)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!