七、进程间通信
1. 进程间通信分类
systeam V共享内存
进程间通信的本质就是让不同进程看到同一份资源。而systeam V是通过让不同的进程经过页表映射到同一块内存空间(操作系统完成的)。
我们申请的共享内存,如果进程结束了,但共享内存并不会释放,需要我们手动释放。管道文件的生命周期是随进程的,但是共享内存的生命周期是随内核的。
使用 ipcs -m 命令可以查看系统中我们创建的共享内存数量。使用 ipcrm -m 和要删除的共享内存的 shmid 即可删除指定共享内存。
这里我们来实现一下共享内存模式的进程间通信:
Makefile:
.PHONY:all all:shm_server shm_client shm_server:ShmServer.cc g++ -o $@ $^ -std=c++11 shm_client:ShmClient.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f shm_server shm_client
Comm.hpp:
#pragma once #include <iostream> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <cerrno> #include <cstring> #include <cstdlib> #include <string> #include <unistd.h> using namespace std; const char* pathname = "/home/student"; const int proj_id = 0x66; const int defaultsize = 4096; // 将key转换为16进制 string ToHex(key_t k) { char buffer[1024]; snprintf(buffer, sizeof(buffer), "0x%x", k); return buffer; } // 生成共享内存的key key_t GetShmKeyOrDie() { key_t k = ftok(pathname, proj_id); if (k < 0) { cerr << "ftok error, errno : " << errno << ", errno string : " << strerror(errno) << endl; exit(1); } return k; } // 根据key创建共享内存 int CreateShmOrDie(key_t key, int size, int flag) { int shmid = shmget(key, size, flag); if (shmid < 0) { cerr << "shmget error, errno : " << errno << ", errno string : " << strerror(errno) << endl; exit(2); } return shmid; } int CreateShm(key_t key, int size) { return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666); } int GetShm(key_t key, int size) { return CreateShmOrDie(key, size, IPC_CREAT); } // 删除共享内存 void DeleteShm(int shmid) { int n = shmctl(shmid, IPC_RMID, nullptr); if (n < 0) { cerr << "shmctl error, errno : " << errno << ", errno string : " << strerror(errno) << endl; } else { cout << "shmctl delete shm success, shmid : " << shmid << endl; } } // 将共享内存附加到进程的地址空间,实现映射关系 void* ShmAttach(int shmid) { void* addr = shmat(shmid, nullptr, 0); if (addr == (void*)-1) { cerr << "shmat error, errno : " << errno << ", errno string : " << strerror(errno) << endl; return nullptr; } return addr; } // 解除共享内存的映射关系 void ShmDetach(void* addr) { int n = shmdt(addr); if (n < 0) { cerr << "shmdt error, errno : " << errno << ", errno string : " << strerror(errno) << endl; } }
Fifo.hpp:
#ifndef __COMM_HPP__ #define __COMM_HPP__ #include <iostream> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <cerrno> #include <cstring> #include <unistd.h> #include <fcntl.h> #include <cassert> using namespace std; #define Mode 0666 #define Path "./fifo" // 命名管道类 class Fifo { public: Fifo(const string& path = Path) : _path(path) { umask(0); // 创建命名管道 int n = mkfifo(_path.c_str(), Mode); if (n == 0) { cout << "mkfifo success" << endl; } else { cout << "mkfifo failed, error : " << errno << "errstring : " << strerror(errno) << endl; } } ~Fifo() { // 删除命名管道 int n = unlink(_path.c_str()); if (n == 0) { cout << "remove " << _path << " success" << endl; } else { cout << "remove failed, error : " << errno << "errstring : " << strerror(errno) << endl; } } private: // 文件路径 + 文件名 string _path; }; class Sync { public: Sync() :_rfd(-1) ,_wfd(-1) {} void OpenReadOrDie() { _rfd = open(Path, O_RDONLY); if (_rfd < 0) exit(1); } void OpenWriteOrDie() { _wfd = open(Path, O_WRONLY); if (_wfd < 0) exit(1); } bool Wait() { bool ret = true; uint32_t c = 0; ssize_t n = read(_rfd, &c, sizeof(uint32_t)); if (n == sizeof(uint32_t)) { cout << "wait for server" << endl; } else if (n == 0) { ret = false; } else { return false; } return ret; } void WakeUp() { uint32_t c = 0; ssize_t n = write(_wfd, &c, sizeof(uint32_t)); assert(n == sizeof(uint32_t)); cout << "wakeup server" << endl; } ~Sync() {} private: int _rfd; int _wfd; }; #endif
ShmServer:
#include "Comm.hpp" #include "Fifo.hpp" int main() { // 生成一个key key_t key = GetShmKeyOrDie(); // 生成共享内存 int shmid = CreateShm(key, defaultsize); // 将共享内存和进程进行关联(挂接) char *addr = (char*)ShmAttach(shmid); // 引入管道 Fifo fifo; Sync syn; syn.OpenReadOrDie(); // 循环读取共享内存 while(1) { if (!syn.Wait()) break; cout << "Shm content: " << addr << endl; } // 去关联共享内存 ShmDetach(addr); // 删除共享内存 DeleteShm(shmid); return 0; }
ShmClient:
#include "Comm.hpp" #include "Fifo.hpp" int main() { key_t key = GetShmKeyOrDie(); int shmid = GetShm(key, defaultsize); char *addr = (char*)ShmAttach(shmid); Sync syn; syn.OpenWriteOrDie(); memset(addr, 0, defaultsize); for (int c = 'A'; c <= 'Z'; c++) { addr[c - 'A'] = c; sleep(1); syn.WakeUp(); } ShmDetach(addr); return 0; }
结果:
消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。
消息队列的本质就是操作系统在内核维护了多个数据块队列,不同进程根据数据怪的标识来通信。
信号量
信号量本质是一个计数器,描述临界资源数量的计数器。进程访问临界资源本质就是申请信号量,申请成功才能访问临界资源,否者只能等待申请。所有的进程访问临界资源,都需要申请信号量,所有的进程都必须要看到同一个信号量,说明信号量本身就是一个共享资源。
信号量的申请操作称为 P操作 ,释放操作称为 V操作 。
在命令行使用 ipcs -s 即可查询信号量, ipcrm -s 加信号量标识符 即可删除指定信号量。