文章目录
- Linux进程间通讯
- 1、进程间通信介绍
- 1.1、进程间通信目的
- 1.2、进程间通信发展
- 1.3、进程间通信分类
- 2、管道
- 2.1、什么是管道
- 2.2、匿名管道
- 2.2.1、标准输入stdin和标准输出stdout通信
- 2.2.2、父子进程通信
- 2.2.3、父子进程通信现象
- 2.2.4、父子进程通信特性
- 2.2.5、进程池
- 2.3、命名管道
- 2.3.1、命名管道的创建
- 2.3.2、命名管道和匿名管道的区别
- 2.3.3、用命名管道实现server和client间通信
- 3、System V 共享内存
- 3.1、共享内存的介绍
- 3.2、共享内存的使用
- 4、System V 消息队列(了解)
- 5、System V 信号量(了解)
Linux进程间通讯
1、进程间通信介绍
让不同进程看到同一份资源的行为通常称为进程间通信(Inter-Process Communication,IPC)。IPC 是在操作系统中用于实现不同进程之间数据传输和共享资源的机制。
1.1、进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程共享一份资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2、进程间通信发展
- 管道
- System V 进程间通信
- POSIX进程间通信
1.3、进程间通信分类
管道
- 匿名管道
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
2、管道
2.1、什么是管道
- 管道是Unix中最古老的进程间通信的形式
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“
2.2、匿名管道
使用系统调用接口
pipe
:我们可以看到,pipe接口需要传入一个大小为2的整型数组。并且在调用pipe后,这数组中的两个位置的值都会被设置,第一个值是作为读端,第二个值是作为写端。匿名管道通常是父子进程进行通信的管道。
2.2.1、标准输入stdin和标准输出stdout通信
// 从键盘读取数据,写入管道,读取管道,写到屏幕 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main() { int fds[2]; char buf[100]; int len; int n = pipe(fds); if (n == -1) perror("make pipe"), exit(1); // read from stdin while (fgets(buf, 100, stdin)) { len = strlen(buf); // write into pipe n = write(fds[1], buf, len); if (n != len) { perror("write to pipe"); break; } memset(buf, 0x00, sizeof(buf)); // 重置buff // read from pipe n = read(fds[0], buf, 100); if (n == -1) { perror("read from pipe"); break; } // write to stdout printf("%s\n", buf); // n = write(1, buf, len); // if (n != len) // { // perror("write to stdout"); // break; // } } }
2.2.2、父子进程通信
使用前面我们学到的fork来创建子进程,那么子进程会继承父进程的读写端,该父子进程的读写端是指向同一份文件的(匿名管道)。这时候只需要父子进程各关闭一个读端和写端就能实现父子通信了。比如父进程关闭写端,子进程关闭读端,那么子进程就可以往管道中写数据了!
站在文件描述符的角度理解匿名管道:
这里是父进程写,子进程读。
站在内核角度理解匿名管道:
可以看到父子进程指向的文件是同一个inode,也就是同一个文件。
所以,我们可以看到,管道其实和文件没有什么两样,后面我们将命名管道就更能理解Linux中一切皆文件的思想。
下面是父子进程通信的代码:
#include <stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<stdlib.h> #include<string.h> void writer(int wfd){ const char* msg = "hello father"; char buff[128]; pid_t pid = getpid(); int cnt = 0; while(1){ snprintf(buff,sizeof(buff),"sent message: %s :%d,pid: %d\n",msg,cnt,pid); write(wfd,buff,strlen(buff)+1); cnt++; sleep(1); } } void reader(int rfd){ char buff[1024]; while(1){ ssize_t n = read(rfd,buff,sizeof(buff)-1); printf("get message: %s",buff); } } int main(){ int pipefd[2]; int ret = pipe(pipefd); if(ret == -1) return -1; printf("pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]); pid_t id = fork(); if(id == 0){ close(pipefd[0]); // w writer(pipefd[1]); exit(10); } // 父进程 close(pipefd[1]); reader(pipefd[0]); wait(NULL); return 0; }
2.2.3、父子进程通信现象
这里以读端是父进程,写端是子进程为例。
现象一:管道内部没有数据并且子进程不关闭自己的写端文件描述符pipefd[1],读端(父进程)就要阻塞等待,直到pipe有数据。
代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> #include <string.h> void writer(int wfd) { const char *msg = "hello father"; char buff[128]; pid_t pid = getpid(); int cnt = 0; while (1) { snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt); write(wfd, buff, strlen(buff)); char c = 'A'; cnt++; // write(wfd, &c, 1); // printf("%d\n",cnt); sleep(10); // if (cnt == 10) // break; } close(wfd); } void reader(int rfd) { char buff[1024]; int cnt = 10; while (1) { sleep(1); ssize_t n = read(rfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = '\0'; // Add null terminator after reading printf("get message: %s ,n: %ld\n", buff, n); } else if (n == 0) { printf("read file done!\n"); break; } else { printf("read error!\n"); break; } --cnt; if (cnt == 0) break; } close(rfd); printf("read endpoint done!\n"); } int main() { int pipefd[2]; int ret = pipe(pipefd); if (ret == -1) return -1; printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]); pid_t id = fork(); if (id == 0) { close(pipefd[0]); // w writer(pipefd[1]); exit(10); } // 父进程 close(pipefd[1]); reader(pipefd[0]); // wait(NULL); int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) { printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F); } return 0; }
可以看到,子进程每10秒写一次数据,那么父进程在读完数据后(管道没有数据了)需要等10秒才能读到数据(等子进程写)。
现象二:管道内部被写满并且父进程不关闭自己的读端文件描述符pipefd[0],写端(子进程)写满之后就要阻塞等待,读端读了部分数据(这个每次读多少是看编译器)后写端才可以写。
代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> #include <string.h> void writer(int wfd) { const char *msg = "hello father"; char buff[128]; pid_t pid = getpid(); int cnt = 0; while (1) { // snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt); // write(wfd, buff, strlen(buff)); char c = 'A'; cnt++; write(wfd, &c, 1); printf("%d\n",cnt); // sleep(10); // if (cnt == 10) // break; } close(wfd); } void reader(int rfd) { char buff[1024]; int cnt = 10; while (1) { sleep(10); ssize_t n = read(rfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = '\0'; // Add null terminator after reading printf("get message: %s ,n: %ld\n", buff, n); } else if (n == 0) { printf("read file done!\n"); break; } else { printf("read error!\n"); break; } --cnt; if (cnt == 0) break; } close(rfd); printf("read endpoint done!\n"); } int main() { int pipefd[2]; int ret = pipe(pipefd); if (ret == -1) return -1; printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]); pid_t id = fork(); if (id == 0) { close(pipefd[0]); // w writer(pipefd[1]); exit(10); } // 父进程 close(pipefd[1]); reader(pipefd[0]); // wait(NULL); int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) { printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F); } return 0; }
可以看到,当前Linux内核的管道大小是4KB,当子进程写满后,子进程就阻塞了,在等父进程读一部分数据。
现象三:对于写端(子进程)而言,不写了并且关闭写文件描述符,读端将会把管道中的数据读完,最后就会读到返回值为0,表示读结束,类似读到了文件的结尾。
代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> #include <string.h> void writer(int wfd) { const char *msg = "hello father"; char buff[128]; pid_t pid = getpid(); int cnt = 0; while (1) { // snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt); // write(wfd, buff, strlen(buff)); char c = 'A'; cnt++; write(wfd, &c, 1); printf("%d\n",cnt); sleep(1); if (cnt == 10) break; } close(wfd); } void reader(int rfd) { char buff[1024]; int cnt = 10; while (1) { // sleep(10); ssize_t n = read(rfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = '\0'; // Add null terminator after reading printf("get message: %s ,n: %ld\n", buff, n); } else if (n == 0) { printf("read file done!\n"); break; } else { printf("read error!\n"); break; } --cnt; if (cnt == 0) break; } close(rfd); printf("read endpoint done!\n"); } int main() { int pipefd[2]; int ret = pipe(pipefd); if (ret == -1) return -1; printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]); pid_t id = fork(); if (id == 0) { close(pipefd[0]); // w writer(pipefd[1]); exit(10); } // 父进程 close(pipefd[1]); reader(pipefd[0]); // wait(NULL); int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) { printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F); } return 0; }
可以看到,子进程写了10次数据后就退出了,父进程在读完10次数据也退出了。
现象四:对于读端(父进程)而言,不读了并且关闭读文件描述符,写端在写,操作系统会终止写端(子进程)的写入,通过信号13(SIGPIPE)信号杀掉子进程。
代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> #include <string.h> void writer(int wfd) { const char *msg = "hello father"; char buff[128]; pid_t pid = getpid(); int cnt = 0; while (1) { // snprintf(buff, sizeof(buff), "sent message: %s:,pid: %d,cnt: %d\n", msg, pid, cnt); // write(wfd, buff, strlen(buff)); char c = 'A'; cnt++; write(wfd, &c, 1); printf("%d\n",cnt); sleep(1); if (cnt == 10) break; } close(wfd); } void reader(int rfd) { char buff[1024]; int cnt = 10; while (1) { // sleep(1); ssize_t n = read(rfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = '\0'; // Add null terminator after reading printf("get message: %s ,n: %ld\n", buff, n); } else if (n == 0) { printf("read file done!\n"); break; } else { printf("read error!\n"); break; } --cnt; if (cnt == 0) break; } close(rfd); printf("read endpoint done!\n"); } int main() { int pipefd[2]; int ret = pipe(pipefd); if (ret == -1) return -1; printf("pipefd[0]:%d,pipefd[1]:%d\n", pipefd[0], pipefd[1]); pid_t id = fork(); if (id == 0) { close(pipefd[0]); // w writer(pipefd[1]); exit(10); } // 父进程 close(pipefd[1]); reader(pipefd[0]); // wait(NULL); int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) { printf("exit code:%d ,exit signal:%d\n", WEXITSTATUS(status), status & 0x7F); } return 0; }
可以看到,父进程读了10次数据后就直接退出了,此时子进程本来还在写的,但是父进程退出了,子进程也被操作系统强制退出了!并且我们获取了子进程的退出信号为13!
2.2.4、父子进程通信特性
- 自带同步机制
- 血缘关系进程通信,常见于父子
- pipe是面向字节流的
- 父子进程退出,管道自动释放,文件的生命周期是随进程的
- 管道只能单向通信,半双工的一种特殊情况
2.2.5、进程池
创建进程池,并使用进程池来执行任务。
大概有以下几个步骤:
- 创建通信信道和子进程
- 控制子进程
- 回收子进程
整体代码:
ProcessPool.cc
文件#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <vector> #include <ctime> using namespace std; #include "task.hpp" enum { UsageErr = 1, Sub_processNumErr, PipeErr }; class Channel { public: Channel(int wfd, int sub_id, string name) : _wfd(wfd), _sub_process_id(sub_id), _name(name) {} ~Channel() {} void Debug() { cout << "_wfd :" << _wfd; cout << ",_sub_process_id :" << _sub_process_id; cout << ",_name :" << _name << endl; } int wfd() { return _wfd; } string name() { return _name; } pid_t pid() { return _sub_process_id; } void Close() { close(_wfd); } private: int _wfd; int _sub_process_id; string _name; }; class ProcessPool { public: ProcessPool(int sub_porcessNum) : _sub_processNum(sub_porcessNum) {} ~ProcessPool() {} int CreateProcess(work_t work) { vector<int> fds; // 创建一个空的文件描述符数组,用来记录每个进程需要关闭的描述符 // 因为子进程会继承父进程的文件描述符表,那么当父进程创建的越多,就越多子进程进程指向文件的写端,如果不一一关闭个子进程不属于自己的写文件描述符 // 会导致如果按顺序关闭文件描述符的时候,子进程会阻塞,因为有写文件描述符没关完全。 for (int i = 0; i < _sub_processNum; ++i) { int pipefd[2]{0}; int n = pipe(pipefd); if (n < 0) return PipeErr; pid_t id = fork(); if (id == 0) { if (!fds.empty()) { cout << "close fd: "; for (auto e : fds) { close(e); cout << e << " "; } cout << endl; } close(pipefd[1]); // child -- r dup2(pipefd[0], 0); work(pipefd[0]); // sleep(100); exit(0); // 正常退出 } sleep(2); string cname = "Channel - " + to_string(i); close(pipefd[0]); // father -- w _channels.push_back(Channel(pipefd[1], id, cname)); fds.push_back(pipefd[1]); // 记录文件描述符,给下一个子进程用来关闭该文件描述符 } return 0; } void Debug() { for (auto &e : _channels) { e.Debug(); } } int NextChannel() { static int next = 0; int c = next++; next %= _channels.size(); return c; } int NextTask() { return rand() % 3; } void SendTaskCode(int channel, int code) { cout << "send code :" << code << " to " << _channels[channel].name() << " sub process id: " << _channels[channel].pid() << endl; write(_channels[channel].wfd(), &code, sizeof(code)); // 把code写到_channels[channel].wfd()中,去给worker中的read读去code的值 } // 先关闭子进程 void KillAll() { for (auto &e : _channels) { e.Close(); cout << e.name() << " close done " << " sub process quit now:" << e.pid() << endl; } } // 再等待子进程,回收资源 void Wait() { for (auto &e : _channels) { pid_t id = e.pid(); pid_t rid = waitpid(id, nullptr, 0); if (id == rid) cout << "wait sub process:" << id << " sucess..." << endl; } } // 直接关闭加等待一步到位 void KillAndWait() { for (auto &e : _channels) { e.Close(); cout << e.name() << " close done " << " sub process quit now:" << e.pid() << endl; pid_t id = e.pid(); pid_t rid = waitpid(id, nullptr, 0); if (id == rid) cout << "wait sub process:" << id << " sucess..." << endl; } } private: vector<Channel> _channels; int _sub_processNum; }; void CtrlProcessPool(ProcessPool *processpool_ptr, int cnt) { while (cnt) { // a.选择一个进程和通道 int channel = processpool_ptr->NextChannel(); // b.选择一个任务 int code = processpool_ptr->NextTask(); // c.发送任务 processpool_ptr->SendTaskCode(channel, code); cnt--; sleep(1); } } void Usage_Command() { cout << "Usage: ./processpool number" << endl; } int main(int argc, char *argv[]) { if (argc != 2) { Usage_Command(); return UsageErr; } srand((uint32_t)time(nullptr)); int sub_processNum = stoi(argv[1]); if (sub_processNum <= 0) return Sub_processNumErr; // 1.创建通信信道和子进程 ProcessPool *processpool_ptr = new ProcessPool(sub_processNum); processpool_ptr->CreateProcess(worker); // processpool_ptr->Debug(); // 2.控制子进程 CtrlProcessPool(processpool_ptr, 10); cout << "task done ..." << endl; // sleep(100); // 3.回收子进程 // processpool_ptr->KillAll(); // processpool_ptr->Wait(); processpool_ptr->KillAndWait(); // sleep(100); cout << "wait sub process done ..." << endl; return 0; }
task.hpp
文件#pragma once typedef void (*work_t)(int); typedef void (*task_t)(int, pid_t); void PrintLog(int fd, pid_t id) { cout << "sub process:" << id << " fd : " << fd << " task is : " << "PrintLog task\n" << endl; } void ConnectMysql(int fd, pid_t id) { cout << "sub process:" << id << " fd : " << fd << " task is : " << "ConnectMysql task\n" << endl; } void FlushPerson(int fd, pid_t id) { cout << "sub process:" << id << " fd : " << fd << " task is : " << "FlushPerson task\n" << endl; } task_t task[3] = {PrintLog, ConnectMysql, ConnectMysql}; void worker(int fd) { // 这里为什么fd总是3? 因为每次父进程每创建一个子进程,关闭的读端都是3,那么下一次子进程的写端总是3 // 从wfd = 0读取就行 while (true) { uint32_t command_code = 0; ssize_t n = read(0, &command_code, sizeof(command_code)); // 从fd = 0读去放到command_code中 if (n == sizeof(command_code)) { if (command_code >= 3) continue; task[command_code](fd, getpid()); sleep(1); } else if (n == 0) { // 父进程没有任务了,管道里面没有数据了就直接退出 break; } // cout << "I am worker"<<endl; sleep(1); } }
需要注意的是:在
ProcessPool.cc
文件中创建了一个vector<int> fds
数组,原因是,创建一个空的文件描述符数组,用来记录每个进程需要关闭的描述符。因为子进程会继承父进程的文件描述符表,那么当父进程创建的越多,就越多子进程进程指向文件的写端,如果不一一关闭个子进程不属于自己的写文件描述符会导致如果按顺序关闭文件描述符的时候,子进程会阻塞,因为有写文件描述符没关完全。还有一种解决方案就是先关闭子进程,在等待子进程。
还有一种解决方案是从后往前等待子进程。
该进程池的运行情况
gif
图如下:
2.3、命名管道
匿名管道是一般在父子进程之间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件。
2.3.1、命名管道的创建
命名管道可以在命令行创建,使用命令:
mkfifo filename
。命名管道也可以在程序里创建,相关函数:
比如创建命名管道:
int main(){ umask(0); mkfifo("fifo",0666); return 0; }
2.3.2、命名管道和匿名管道的区别
匿名管道由pipe函数创建并打开
匿名管道由mkfifo函数创建,由open函数打开
但是两者在通讯上没有区别,都是使用管道
2.3.3、用命名管道实现server和client间通信
Comm.hpp
文件#pragma once #include <iostream> #include <cstring> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <error.h> #include <errno.h> using namespace std; #define PATH "fifo" #define MODE 0666 class Fifo { public: Fifo(const string &path) : _path(path) { // 创建管道文件 umask(0); int n = mkfifo(path.c_str(), MODE); if (n == 0) { cout << "make fifo file:" << path << " sucess .." << endl; } else { // 创建失败 cerr << "make fifo file " << "errorno:" << errno << " error info:" << strerror(errno) << endl; } } ~Fifo() { int n = unlink(PATH); if (n < 0) { cerr << "remove fifo file " << "errorno:" << errno << " error info:" << strerror(errno) << endl; } else { cout << "remove file:" << _path << " sucess ..." << endl; } } private: string _path; };
pipe_server.cc
文件#include "Comm.hpp" int main() { // 创建管道文件 Fifo fifo(PATH); int rfd = open(PATH, O_RDONLY); if (rfd < 0) { cerr << "open error, " << "errorno:" << errno << " error info:" << strerror(errno) << endl; } char buff[1024]; while (true) { ssize_t n = read(rfd, buff, sizeof(buff) - 1); if (n > 0) { buff[n] = 0; cout << "get message :" << buff <<endl; } else if (n == 0) { cout << "read done .." << endl; break; } else { cerr << "read error, " << "errorno:" << errno << " error info:" << strerror(errno) << endl; break; } } close(rfd); return 0; }
pipe.client.cc
文件#include "Comm.hpp" int main() { int wfd = open(PATH, O_WRONLY); if (wfd < 0) { cerr << "open error, " << "errorno:" << errno << " error info:" << strerror(errno) << endl; return 1; } string str; while (true) { cout << "please start write: "; getline(cin, str); if(str == "quit") break; ssize_t n = write(wfd, str.c_str(), str.size()); if (n < 0) { cerr << "write error, " << "errorno:" << errno << " error info:" << strerror(errno) << endl; break; } } close(wfd); return 0; }
运行结果:
3、System V 共享内存
3.1、共享内存的介绍
共享内存区是最快的IPC(进程间通信)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图:
共享内存数据结构:
共享内存函数:
shmget
函数功能:用来创建共享内存 原型 int shmget(key_t key, size_t size, int shmflg); 参数 key:这个共享内存段名字 size:共享内存大小 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmat
函数功能:将共享内存段连接到进程地址空间 原型 void *shmat(int shmid, const void *shmaddr, int shmflg); 参数 shmid: 共享内存标识 shmaddr:指定连接的地址 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY 返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1 注意: shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt
函数功能:将共享内存段与当前进程脱离 原型 int shmdt(const void *shmaddr); 参数 shmaddr: 由shmat所返回的指针 返回值:成功返回0;失败返回-1 注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl
函数功能:用于控制共享内存 原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 参数 shmid:由shmget返回的共享内存标识码 cmd:将要采取的动作(有三个可取值) buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0;失败返回-1
3.2、共享内存的使用
Comm.hpp
文件#include <iostream> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> #include <string.h> #include <unistd.h> #include "Fifo.hpp" using namespace std; const char *Path = "/home/xp2"; int proj_id = 0x168; const int defaultSize = 4096; string ToHex(key_t key) { char buff[1024]; snprintf(buff, sizeof(buff), "0x%x", key); return buff; } key_t GetshmKeyOrDie() { key_t k = ftok(Path, proj_id); if (k < 0) { cerr << "ftok error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; exit(1); } return k; } int CreatShmOrDie(key_t key, int size, int flag) { int shmid = shmget(key, size, flag); if (shmid < 0) { cerr << "shmget error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; exit(1); } return shmid; } int CreatShm(key_t key, int size) { return CreatShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666); } int GetShm(key_t key, int size) { return CreatShmOrDie(key, size, IPC_CREAT); } // void *shmat(int shmid, const void *shmaddr, int shmflg); // int shmdt(const void *shmaddr); void DeleteShm(int shmid) { int n = shmctl(shmid, IPC_RMID, nullptr); if (n < 0) { cerr << "shmctl error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; } else { cout << "shmctl delete shm sucess," << "shmid :" << shmid << endl; } } void *ShmAttach(int shmid) { void *addr = shmat(shmid, nullptr, 0); if ((long long int)addr == -1) { cerr << "shmat error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; return nullptr; } return addr; } void ShmDetach(void *addr) { int n = shmdt(addr); if (n < 0) { cerr << "shmdt error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; } } void DebugShm(int shmid) { struct shmid_ds shmds; int n = shmctl(shmid, IPC_STAT, &shmds); if (n < 0) { cerr << "Debug error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; return; } else { cout << "shmds.shm_segsz: " << shmds.shm_segsz << endl; cout << "shmds.shm_nattch: " << shmds.shm_nattch << endl; cout << "shmds.shm_ctime: " << shmds.shm_ctime << endl; cout << "shmds.shm_perm.__key: " << ToHex(shmds.shm_perm.__key) << endl; } }
Fifo.hpp
文件:用来实现两个进程对共享内存读写的同步#pragma once #include <iostream> #include <cstring> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <error.h> #include <errno.h> #include <assert.h> using namespace std; #define PATH "fifo" #define MODE 0666 class Fifo { public: Fifo(const string &path) : _path(path) { // 创建管道文件 umask(0); int n = mkfifo(path.c_str(), MODE); if (n == 0) { cout << "make fifo file:" << path << " sucess .." << endl; } else { // 创建失败 cerr << "make fifo file " << "errorno:" << errno << " error info:" << strerror(errno) << endl; } } ~Fifo() { int n = unlink(PATH); if (n < 0) { cerr << "remove fifo file " << "errorno:" << errno << " error info:" << strerror(errno) << endl; } else { cout << "remove file:" << _path << " sucess ..." << 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; } bool Wait() { bool ret = true; char c = 0; // 读一个字符 ssize_t n = read(rfd, &c, sizeof(char)); if (n == sizeof(char)) { cout << "server wakeup, begin read shm..." << endl; } else { return false; } return ret; } void WakeUp() { char c = 0; ssize_t n = write(wfd, &c, sizeof(char)); assert(n == sizeof(char)); cout << "wakeup server...." << endl; } ~Sync() {} private: int rfd; int wfd; };
shm_server.cpp
文件#include "Comm.hpp" int main() { cout << ToHex(GetshmKeyOrDie()) << endl; key_t key = GetshmKeyOrDie(); int shmid = CreatShm(key, defaultSize); if (shmid < 0) { cerr << "CreatShm error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; return 1; } else { cout << "shmid :" << shmid << endl; } cout << "server挂接前:" << endl; DebugShm(shmid); sleep(5); char *addr = (char *)ShmAttach(shmid); cout << "ShmAttach Address:" << ToHex((uint64_t)addr) << endl; cout << "server挂接后:" << endl; DebugShm(shmid); sleep(5); // 先创建管道 Fifo fifo(PATH); Sync sync; sync.OpenReadOrDie(); // 通信 while (true) { bool ret = sync.Wait(); if (ret == true) { cout << "shm content:" << addr << endl; sleep(1); } else break; } sleep(10); ShmDetach(addr); sleep(10); DeleteShm(shmid); sleep(5); return 0; }
shm_client.cpp
文件#include "Comm.hpp" int main() { cout << ToHex(GetshmKeyOrDie()) << endl; key_t key = GetshmKeyOrDie(); int shmid = GetShm(key, defaultSize); if (shmid < 0) { cerr << "GetShm error, " << "error code: " << errno << ", error string :" << strerror(errno) << endl; return 1; } else { cout << "shmid :" << shmid << endl; } char *addr = (char *)ShmAttach(shmid); // 页表映射的逻辑地址罢了 cout << "ShmAttach Address:" << ToHex((uint64_t)addr) << endl; cout << "client挂接后:" << endl; DebugShm(shmid); sleep(5); // 通信 memset(addr, 0, defaultSize); // 根据逻辑地址找到物理地址并初始化 Sync sync; sync.OpenWriteOrDie(); for (char c = 'A'; c <= 'Z'; ++c) { addr[c - 'A'] = c; sleep(1); sync.WakeUp(); } sleep(10); ShmDetach(addr); sleep(10); return 0; }
运行结果:
4、System V 消息队列(了解)
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
特性方面:
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
5、System V 信号量(了解)
信号量主要用于同步(同步我们在管道中有涉及,也就是两个进程之间是有序的进行通信)和互斥的,下面先来看看什么是互斥。
进程互斥:
由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
在进程中涉及到互斥资源的程序段叫临界区
特性方面:
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
那么好,Linux进程间通信就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。
Xpccccc的github主页