Linux篇:进程间通信
- 其他
- 2025-07-21 19:19:17

一、进程间通信原理:
1、通信是有成本的:两个或者多个进程,实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高。
2、进程间通信的方式: ①基本数据 ②发送命令 ③某种协同 ④通知 ......
3、进程间通信的本质:必须让不同的进程,看到同一份资源——特定形式的内存空间。这个“资源”一般是操作系统提供(第三方空间)(为什么不是我们两个进程中的一个呢?因为这样会破坏进程独立性)。进程访问这个空间进行通信,本身就是访问操作系统!而进程代表的就是用户。“资源”从创建,使用(一般)到释放,都要使用系统调用接口。所以从底层设计,从接口设计,都要从操作系统独立设计。
4、一般操作系统会有一个独立的通信模块,它隶属于文件系统,称为IPC通信模块。进程间通信是有标准的——system V &&possix。
5、基于文件级别的通信方式——管道。
二、(匿名)管道(本质是文件):让不同的进程,看到同一份资源。
1. 原理: · 父进程fork创建出子进程,子进程会拷贝父进程的文件描述符表,此时,父进程与子进程都会有相应的读写端指向同一个文件。此时根据要求关闭父进程与子进程相应的读写端,来形成单向通信的信道。
· 同一个文件是内存级的,每个文件都存在自己的缓冲区,如果双方想向自己的缓冲区中写入,子进程就可以通过缓冲区读取,实现进程间通信。正因为其只能进行单向通信,故称其为管道。
· 若进行双向通信用多个管道即可。
· 必须有血缘关系的进程才能通信,常用于父子关系,兄弟关系和爷孙关系也可。
· 管道是有固定大小的,在不同内核里,大小可能有差别。
2、接口:
输出型参数:将文件的文件描述符数字带出来,让用户使用。 pipefd[0]:读下标。 pipefd[1]:写下标。
//testPipe.cc #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <cstdlib> // stdlib.h #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define N 2 #define NUM 1024 using namespace std; //child:用户级缓冲区拷贝到文件级缓冲区 void Writer(int wfd) { string s = "hello, I am child"; pid_t self = getpid(); int number = 0; char buffer[NUM]; while(true) { //构建发送字符串 buffer[0] = 0;//字符串清空,只是为了提醒阅读代码的人,我把这个数组当做字符串了 snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);//int snprintf(char* str, size_t size, const char *format, ...); cout << buffer << endl; //发送/写入给父进程,system call write(wfd, buffer, strlen(buffer));//ssize_t write(int fd, const void * buf, size_t count);//向文件写入不需要+1 sleep(1); } } //father:内核级缓冲区拷贝到应用层缓冲区 void Reader(int rfd) { char buffer[NUM]; while(true) { buffer[0] = 0; //system call ssize_t n = read(rfd, buffer, sizeof(buffer));//ssize_t read(int fd, void *buf, size_t count);//sizeof != strlen, 代表buffer缓冲区大小 if(n > 0) { buffer[n] = 0;// 0 == '\0' cout << "father get a message[" << getpid() << "]#" << buffer << endl; } //TODO } } int main() { int pipefd[N] = {0}; int n = pipe(pipefd); if(n < 0) return 1; //cout << "pipefd[0]:" << pipefd[0] << " , pipefd[1]: " << pipefd[1] << endl; //child -> w, father -> r pid_t id = fork(); if(id < 0) return 2; if(id == 0) { //child close(pipefd[0]); //IPC code Writer(pipefd[1]); close(pipefd[1]); exit(0); } //father close(pipefd[1]); //IPC code Reader(pipefd[0]); pid_t rid = waitpid(id, nullptr, 0); if(rid < 0) return 3; close(pipefd[0]); return 0; } //Makefile testPipe:testPipe.cc g++ $^ -o $@ -std=c++11 .PHONY:clean clean: rm -f testPipe3、编码实现:
管道的五大特征: ①具有血缘关系的进程进行进程间通信。 ②管道只能单向通信。 ③多执行流共享的难免出现访问冲突的问题。临界资源竞争的问题。所以父子进程是会进程协同,同步与互斥,保护管道文件的数据安全。 ④管道是面向字节流的 ⑤那是基于文件的,而对象的生命周期是随进程的
管道四种的情况: ①读写端正常管道,如果为空,读端就要堵塞 ②读写端正常,管道如果被写满,写端就要阻塞 ③读端正常读写端关闭,读端就会读到零,表明读到的文件(pipe)结尾不会被阻塞。 ④写端是正常写入,读端关闭了。操作系统就要通过信号杀掉正在写入的进程。转为③(所以子进程写入父进程读取)(操作系统是不会做低效浪费等类似的工作的,如果做了,就是操作系统的bug)。
4、管道的应用场景:
①自定义shell:指令的判断 a.分析输入的命令行字符串,获取有多少个|命令,打散多个子命令字符串。 b.malloc申请空间,pipe先申请多个管道。 c.循环创建多个子进程,每一个子进程的重定向情况。最开始:输出重定向,1->指定的一个管道的写端。中间:输入输出重定向,0标准输入重定向到上一个管道的读端,标准输出重定向到下一个管道的写端。最后一个:将输入重定向将标准输入重定向到最后一个管道的读端。 d.分别让不同的子进程执行不同的命令---exec*(exec*不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向)。
②简易版本的进程池:降低系统调用和成本。
ProcessPool.cc #include "Task.hpp" #include <string> #include <vector> #include <cstdlib> #include <cassert> #include <ctime> #include <unistd.h> #include <sys/wait.h> #include <sys/stat.h> const int processnum = 10; //void LoadTask(std::vector<task_t> *tasks); std::vector<task_t> tasks; // 先描述 class channel { public: channel(int cmdfd, pid_t slaverid, const std::string &processname) :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname) {} public: int _cmdfd; // 发送任务的文件描述符 pid_t _slaverid; // 子进程PID std::string _processname;// 子进程的名字————方便我们打印日志 }; void slaver() { // read(0) while(true) { int cmdcode = 0; int n = read(0, &cmdcode, sizeof(int)); // 如果父进程不给子进程发送数据呢?阻塞等待! if(n == sizeof(int)) { //执行cmdcode对应的任务列表 std::cout << "slaver say@ get a command: " << getpid() << " : cmdcode: " << cmdcode << std::endl; if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode](); } if(n == 0) break; } } //编码规范: //输入: const & //输出: * //输入输出:& void InitProcessPool(std::vector<channel> *channels) { // version 2 : 确保每一个子进程都只有一个写端 std::vector<int> oldfds; for(int i =0; i < processnum; i++) { int pipefd[2]; // 临时空间 int n = pipe(pipefd); assert(!n); (void)n; pid_t id = fork(); if(id == 0) // child { std::cout << " child: " << getpid() << " close history fd: "; for(auto fd : oldfds) { std::cout << fd << " "; close(fd); } std::cout << "\n"; close(pipefd[1]); dup2(pipefd[0], 0); close(pipefd[0]); slaver(); std::cout << "process : " << getpid() << "quit" << std::endl; // slaver(pipefd[0]); exit(0); } // father close(pipefd[0]); // 添加channel字段了 std::string name = "process-" + std::to_string(i); channels->push_back(channel(pipefd[1], id, name)); oldfds.push_back(pipefd[1]); sleep(1); } } void Debug(const std::vector<channel> &channels) { // test for(const auto &c : channels) { std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl; } } void Menu() { std::cout << "#####################################" << std::endl; std::cout << "##### 1、刷新日志 2、刷新野怪 #######" << std::endl; std::cout << "##3、检测软件是否更新 4、更新血量蓝量##" << std::endl; std::cout << "############## 0、退出 ###############" << std::endl; std::cout << "#####################################" << std::endl; } void ctrlSlaver(const std::vector<channel> &channels) { int which = 0; //int cnt = 5; while(true) { int select = 0; Menu(); std::cout << "Please Enter@ "; std::cin >> select; if(select <= 0 || select >= 5) break; //1、选择任务 //int cmdcode = rand()%tasks.size(); int cmdcode = select - 1; //2、选择进程:负载均衡(随机数,轮转) //int processpos = rand()%channels.size(); std::cout << " father say: " << " cmdcode: " << cmdcode << " already sendto " << channels[which]._slaverid << " process name: " << channels[which]._processname << std::endl; //3、发送任务 write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode)); which++; which %= channels.size(); //cnt--; //sleep(1); } } void QuitProcess(const std::vector<channel> &channels) { // version1 int last = channels.size()-1; for(int i = last; i >= 0; i--) { close(channels[i]._cmdfd); waitpid(channels[i]._slaverid, nullptr, 0); } // for(const auto &c : channels) close(c._cmdfd); // //sleep(5); // for(const auto &c : channels) waitpid(c._slaverid, nullptr, 0); // //sleep(5); } int main() { LoadTask(&tasks); srand(time(nullptr)^getpid()^1023); // 种一颗随机数种子 // 再组织 std::vector<channel> channels; // 1、初始化 InitProcessPool(&channels); Debug(channels); // 2、开始控制子进程 ctrlSlaver(channels); // 3、清理收尾 QuitProcess(channels); return 0; } //Task.hpp #pragma once #include <iostream> #include <vector> typedef void (*task_t)(); void task1() { std::cout << "lol 刷新日志" << std::endl; } void task2() { std::cout << "lol 更新野区,刷新出来野怪" << std::endl; } void task3() { std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl; } void task4() { std::cout << "lol 用户释放技能,更新血量蓝量" << std::endl; } void LoadTask(std::vector<task_t> *tasks) { tasks->push_back(task1); tasks->push_back(task2); tasks->push_back(task3); tasks->push_back(task4); } ##Makefile ProcessPool:ProcessPool.cc g++ $^ -o $@ -std=c++11 .PHONY:clean clean: rm -f ProcessPool以上为具有血缘关系的进程进行进程间通信 如果毫不相关的进程进行进程间通信呢?
三、命名管道:
1、 理解:如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开同一个文件。
· 进程间通信的前提:先让不同进程看到同一份资源。
· 管道文件不需要刷盘,只是内存级文件。
· 管道怎么打开同一个文件?为什么要这么做? 通过同路径下的同一个文件名(路径+文件名具有唯一性)的方式,让不同进程看到同一份资源,进而实现不同进程间通信,所以叫命名管道。
2、编码:
①模拟实现命名管道的应用场景:
//server.cc #include "comm.hpp" using namespace std; int main() { Init init; //打开管道 int fd = open(FIFO_FILE, O_RDONLY);// 等待写入方打开之后,自己才会打开文件,向后执行,open 阻塞了! if(fd < 0) { perror("open"); exit(FIFO_OPEN_ERR); } cout << "server open file done" << endl; //开始通信 while(true) { char buffer[1024] = {0}; int x = read(fd, buffer, sizeof(buffer)); if(x > 0) { buffer[x] = 0; cout << "client say# " << buffer << endl; } else if(x == 0) { cout << "client quit, me too!\n" << endl; break; } else break; } close(fd); return 0; } //client.cc #include <iostream> #include "comm.hpp" using namespace std; int main() { int fd = open(FIFO_FILE, O_WRONLY); if(fd < 0) { perror("open"); exit(FIFO_OPEN_ERR); } cout << "client open file done" << endl; string line; while(true) { cout << "Please Enter@ "; getline(cin, line); write(fd, line.c_str(), line.size()); } close(fd); return 0; } //comm.hpp #pragma once #include <iostream> #include <string> #include <cerrno> #include <cstring> #include <cstdlib> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #define FIFO_FILE "./myfifo" #define MODE 0664 enum{ FIFO_CREATE_ERR = 1, FIFO_DELETE_ERR, FIFO_OPEN_ERR }; class Init { public: Init() { //创建管道 int n = mkfifo(FIFO_FILE, MODE); if(n == -1) { perror("mkfifo"); exit(FIFO_CREATE_ERR); } } ~Init() { int m = unlink(FIFO_FILE); if(m == -1) { perror("unlink"); exit(FIFO_DELETE_ERR); } } }; ##Makefile .PHONY:all all:server client server:server.cc g++ -o $@ $^ -g -std=c++11 client:client.cc g++ -o $@ $^ -g -std=c++11 .PHONY:clean clean: rm -f server client②模拟实现日志:输出时间,日志的等级,日志内容,文件的名称和行号。
//server.cc #include "comm.hpp" #include "log.hpp" using namespace std; int main() { Init init; Log log; log.Enable(Classfile); //打开管道 int fd = open(FIFO_FILE, O_RDONLY);// 等待写入方打开之后,自己才会打开文件,向后执行,open 阻塞了! if(fd < 0) { // log.logmessage(Fatal, "error string: %s, error code: %d",strerror(errno), errno); log(Fatal, "error string: %s, error code: %d",strerror(errno), errno); exit(FIFO_OPEN_ERR); } // log.logmessage(Info, "server open file done, error string: %s, error code: %d",strerror(errno), errno); // log.logmessage(Warning, "server open file done, error string: %s, error code: %d",strerror(errno), errno); // log.logmessage(Fatal, "server open file done, error string: %s, error code: %d",strerror(errno), errno); // log.logmessage(Debug, "server open file done, error string: %s, error code: %d",strerror(errno), errno); log(Info, "server open file done, error string: %s, error code: %d",strerror(errno), errno); log(Warning, "server open file done, error string: %s, error code: %d",strerror(errno), errno); log(Fatal, "server open file done, error string: %s, error code: %d",strerror(errno), errno); log(Debug, "server open file done, error string: %s, error code: %d",strerror(errno), errno); //开始通信 while(true) { char buffer[1024] = {0}; int x = read(fd, buffer, sizeof(buffer)); if(x > 0) { buffer[x] = 0; cout << "client say# " << buffer << endl; } else if(x == 0) { // log.logmessage(Debug, "client quit, me too, error string: %s, error code: %d",strerror(errno), errno); log(Debug, "client quit, me too, error string: %s, error code: %d",strerror(errno), errno); break; } else break; } close(fd); return 0; } //client #include <iostream> #include "comm.hpp" using namespace std; int main() { int fd = open(FIFO_FILE, O_WRONLY); if(fd < 0) { perror("open"); exit(FIFO_OPEN_ERR); } cout << "client open file done" << endl; string line; while(true) { cout << "Please Enter@ "; getline(cin, line); write(fd, line.c_str(), line.size()); } close(fd); return 0; } //comm.hpp #pragma once #include <iostream> #include <string> #include <cerrno> #include <cstring> #include <cstdlib> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #define FIFO_FILE "./myfifo" #define MODE 0664 enum{ FIFO_CREATE_ERR = 1, FIFO_DELETE_ERR, FIFO_OPEN_ERR }; class Init { public: Init() { //创建管道 int n = mkfifo(FIFO_FILE, MODE); if(n == -1) { perror("mkfifo"); exit(FIFO_CREATE_ERR); } } ~Init() { int m = unlink(FIFO_FILE); if(m == -1) { perror("unlink"); exit(FIFO_DELETE_ERR); } } }; //log.hpp #pragma once #include <iostream> #include <time.h> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile "log.txt" class Log { public: Log() { printMethod = Screen; path = "./log/"; } void Enable(int method) { printMethod = method; } std::string levelToString(int level) { switch(level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } // void logmessage(int level, char *format, ...) // { // time_t t = time(nullptr); // struct tm *ctime = localtime(&t); // char leftbuffer[SIZE]; // snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), // ctime->tm_year+1900, ctime->tm_mon+1, ctime->tm_mday, // ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // // va_list s; // // va_start(s, format); // char rightbuffer[SIZE]; // vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // // va_end(s); // //格式:默认部分+自定义部分 // char logtxt[SIZE*2]; // snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer); // // printf("%s", logtxt);//暂时打印 // printLog(level, logtxt); // } void printLog(int level, const std::string &logtxt) { switch(printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } void printOneFile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666); // "log.txt" if(fd < 0) return; write(fd, logtxt.c_str(), logtxt.size()); close(fd); } void printClassFile(int level, const std::string logtxt) { std::string filename = LogFile; filename += "."; filename += levelToString(level); // "log.txt.Debug/Warning/Fatal" printOneFile(filename, logtxt); } ~Log() {} void operator()(int level, const char *format, ...) { time_t t = time(nullptr); struct tm *ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year+1900, ctime->tm_mon+1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); //格式:默认部分+自定义部分 char logtxt[SIZE*2]; snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer); // printf("%s", logtxt);//暂时打印 printLog(level, logtxt); } private: int printMethod; std::string path; }; // 拓展:可变参数(可变参数必须要至少一个实参) // int sum(int n, ...) // { // va_list s; // char* // va_start(s, n); // int sum = 0; // while(n) // { // sum += va_arg(s, int); // n--; // } // va_end(s); // return sum; // } //Makefile .PHONY:all all:server client server:server.cc g++ -o $@ $^ -g -std=c++11 client:client.cc g++ -o $@ $^ -g -std=c++11 .PHONY:clean clean: rm -f server client四、System共享内存
1、原理:每个进程都要有自己对应的地址空间,有自己对应的tast_struct,通过页表将自己地址空间中的内容映射到物理内存中。在物理内存中创建一块共享空间,共享空间通过页表映射到,进程的共享区当中,并给应用层返回一个所对应连续内存空间的起始虚拟地址。从此两个进程就可以通过各自页表访问到同一块物理内存了。
2、申请共享内存步骤: ①申请内存 ②挂接到进程地址空间 ③返回首地址
释放共享内存步骤:去关联,释放共享内存
3、上面的操作不是进程直接做的(进程具有独立性),而是直接由操作系统来做(系统调用)。操作系统中内核结构体描述共享内存,再组织,从而管理所有内存。
4、接口:
创建共享内存(返回消息队列标识符):
获取key:
让当前进程和指定共享内存链接起来和去关联:
删除共享内存:
问题1:共享内存标识符shmflg IPC_CREAT(单独):如果你申请的共享内存不存在就创建,存在,就获取并返回。 IPC_CREAT|IPC_EXCL:你申请的共享内存不存在,就创建,存在,就出错并返回。确保如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。(IPC_EXCL不单独使用)
问题2:不过你怎么保证让不同的进程看到同一个内存资源呢?你怎么知道这个内存资源存在还是不存在呢? 谈谈key ①key是一个数字,这个数字是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。 ②第一个进程可以通过key创建共享进程。第二个之后的进程,只要拿着同一个key,就可以和第一个进程看到同一个共享内存了。 ③对于一个已经创建好的共享内存,key在哪?key在共享内存的描述对象中。 ④第一次创建的时候,必须有一个key了。怎么有?ftok,它是一套算法——通过pathname和proj_id进行数值计算即可!(pathname和proj_id由用户自由指定。) ⑤key和路径都是唯一的。
问题三:key和shmid区别: key:操作系统的标定唯一型(只在创建管理内存时使用)。 shmid:只在进程内表示资源的唯一性。
问题四: ①共享内存的生命周期是随内核的!用户不主动关闭,共享内存会一直存在。除非内核重启(用户释放)。 ②接口: 查看所有的共享内存:ipcs -m 删除管理内存:ipcrm -m shmid
5、共享内存的特性: ①共享内存没有同步互斥之类的保护机制。 ②共享内存是所有的进程间通信中速度最快的(由地址空间映射的方式,拷贝少,速度快)。 ③共享内存内部的数据由用户自己维护。 ④共享内存没有同步机制。
6、共享内存的属性:
struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ __kernel_time_t shm_atime; /* last attach time */ __kernel_time_t shm_dtime; /* last detach time */ __kernel_time_t shm_ctime; /* last change time */ __kernel_ipc_pid_t shm_cpid; /* pid of creator */ __kernel_ipc_pid_t shm_lpid; /* pid of last operator */ unsigned short shm_nattch; /* no. of current attaches */ unsigned short shm_unused; /* compatibility */ void *shm_unused2; /* ditto - used by DIPC */ void *shm_unused3; /* unused */ };7、用共享内存实现进程间通信:
// comm.hpp #ifndef __COMM_HPP__ #define __COMM_HPP__ #include <iostream> #include <string> #include <cstdlib> #include <cstring> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include "log.hpp" using namespace std; Log log; //共享内存大小一般的大小一般建议是4096的整数倍 //4097,实际上操作系统给你的是4096*2的大小 const int size = 4096; const string pathname = "/home/zsx"; const int proj_id = 0x6666; key_t GetKey() { key_t k = ftok(pathname.c_str(), proj_id); if(k < 0) { log(Fatal, "ftok error: %s", strerror(errno)); exit(1); } log(Info, "ftok sucess, key is : 0x%x", k); return k; } int GetShareMemHelper(int flag) { key_t k = GetKey(); // int shmid = shmget(k, size, IPC_CREAT | IPC_EXCL | 0666); int shmid = shmget(k, size, flag); if(shmid < 0) { log(Fatal, "create share memory error: %s", strerror(errno)); exit(2); } log(Info, "create share memory success, shmid: %d", shmid); return shmid; } int CreateShm() { return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666); } int GetShm() { return GetShareMemHelper(IPC_CREAT); } #endif // log.hpp #pragma once #include <iostream> #include <time.h> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile "log.txt" class Log { public: Log() { printMethod = Screen; path = "./log/"; } void Enable(int method) { printMethod = method; } std::string levelToString(int level) { switch(level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } // void logmessage(int level, char *format, ...) // { // time_t t = time(nullptr); // struct tm *ctime = localtime(&t); // char leftbuffer[SIZE]; // snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), // ctime->tm_year+1900, ctime->tm_mon+1, ctime->tm_mday, // ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // // va_list s; // // va_start(s, format); // char rightbuffer[SIZE]; // vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // // va_end(s); // //格式:默认部分+自定义部分 // char logtxt[SIZE*2]; // snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer); // // printf("%s", logtxt);//暂时打印 // printLog(level, logtxt); // } void printLog(int level, const std::string &logtxt) { switch(printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } void printOneFile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666); // "log.txt" if(fd < 0) return; write(fd, logtxt.c_str(), logtxt.size()); close(fd); } void printClassFile(int level, const std::string logtxt) { std::string filename = LogFile; filename += "."; filename += levelToString(level); // "log.txt.Debug/Warning/Fatal" printOneFile(filename, logtxt); } ~Log() {} void operator()(int level, const char *format, ...) { time_t t = time(nullptr); struct tm *ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year+1900, ctime->tm_mon+1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); //格式:默认部分+自定义部分 char logtxt[SIZE*2]; snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer); // printf("%s", logtxt);//暂时打印 printLog(level, logtxt); } private: int printMethod; std::string path; }; // 拓展:可变参数 // int sum(int n, ...) // { // va_list s; // char* // va_start(s, n); // int sum = 0; // while(n) // { // sum += va_arg(s, int); // n--; // } // va_end(s); // return sum; // } //processa.cc #include "comm.hpp" extern Log log; int main() { // sleep(3); int shmid = CreateShm(); // log(Debug, "create shm done"); // sleep(5); char *shmaddr = (char*)shmat(shmid, nullptr, 0); // log(Debug, "attach shm done, shmaddr: 0x%x", shmaddr); // sleep(5); struct shmid_ds shmds; // ipc code // 一旦有人把数据写入共享内存,不需要经过系统调用,直接就能看到数据了。 while(true) { cout << "client say@ " << shmaddr << endl; // 直接访问共享内存 sleep(1); shmctl(shmid, IPC_STAT, &shmds); cout << "shm size: " << shmds.shm_segsz << endl; cout << "shm nattch: " << shmds.shm_nattch << endl; printf("0x%x\n", shmds.shm_perm.__key); cout << "shm mode: " << shmds.shm_perm.mode<< endl; } shmdt(shmaddr); // log(Debug, "detach shm done, shmaddr: 0x%x", shmaddr); // sleep(5); shmctl(shmid, IPC_RMID, nullptr); // log(Debug, "destory shm done, shmaddr: 0x%x", shmaddr); // sleep(5); return 0; } //processb.cc #include "comm.hpp" int main() { // sleep(3); int shmid = GetShm(); // log(Debug, "create shm done"); // sleep(5); char *shmaddr = (char*)shmat(shmid, nullptr, 0); // log(Debug, "attach shm done, shmaddr: 0x%x", shmaddr); // sleep(5); // ipc code //一旦有了共享内存,挂接到自己的地址空间中,直接把它当成自己的内存空间来用即可。 //不需要调用系统调用 while(true) { cout << "Please Enter@ "; // char buffer[1024]; // 缓冲区(此处没必要),因为有内存 // fgets(buffer, sizeof(buffer), stdin); // memcpy(shmaddr, buffer, strlen(buffer)+1); // 当作字符串 fgets(shmaddr, 4096, stdin); } shmdt(shmaddr); // log(Debug, "detach shm done, shmaddr: 0x%x", shmaddr); // sleep(5); return 0; } ##Makefile .PHONY:all all:processa processb processa:processa.cc g++ -o $@ $^ -g -std=c++11 processb:processb.cc g++ -o $@ $^ -g -std=c++11 .PHONY:clean clean: rm -f processa processb当然,我们也可以利用命名管道实现共享内存的同步,有兴趣的同学可以尝试一下!
8、mmap函数:也是共享内存的一种。(选学)
五、消息队列(了解):
1、原理: ①必须让不同的进程看到同一个队列。 ②允许不同的进程向内核中发送带类型的数据块。
2、接口:
①创建共享内存(返回消息队列标识符):
②释放共享内存:
③发送/接收消息队列:
④查看所有的共享内存:ipcs -q 删除管理内存:ipcrm -m msgid
六、IPC内核中的数据结构设计: ①在操作系统中,所有的IPC资源,都是整合进操作系统里的IPC模块中的! ②创建共享内存/消息队列就要创建对应的数据结构。这些数据结构的第一个字段类型都是ipc_perm,管理这些数据结构是通过数组(struct ipc_perm *array[])来管理的。创建共享内存/消息队列时,操作系统就要创建对应的数据结构,并将第一个字段的地址填入数组(结构体数据类型可以不一样,因为第一个字段的类型都一样)。 ③从此往后,管理操纵系统中所有的Ipc资源,只要先描述,然后对所有资源进行的增删查改转化成对该数组的增删查改。 ④当然,也可通过用户输入的key,找到每一个对应的ipc资源,通过比较第一个字段的ipc_perm中的key,确认进程是否已经被创建(新旧)。 ⑤其中该数组的数组下标为xxxid(shmid/msgid……)(是线性递增的) ⑥当用户未来尝试访问某种资源的时候,只要将对应的地址强转成指定的类型,就可以自由访问整个结构中的任意类型。 ⑦而操作系统为什么能区分指针指向的对象的类型呢? ipc_perm是操作系统在应用层上的结构体,而在内核结构中,它的数据结构为kern_ipc_perm。它在第一个字段中增加了一种类型标志位,来让代码区分它自己是哪种ipc资源。 ⑧这实际上是多态技术的一种体现。其中ipc_perm就是基类,shm_perm/msg_perm就是子类。
七、信号量:(了解)
1、原理概念
1、)当我们的进程a正在向共享内存写入时,写入了一部分就被进程b拿走了,就会导致双方发和收的数据不完整——数据不一致问题(因为共享内存没有保护机制)。 而管道不会(因为管道在通信过程中有原子性保证和同步互斥)。
2、) ①AB看到的同一份资源共享资源,如果不加保护,会导致数据不一致问题。 ②加锁--互斥访问--任何时刻,只允许一个执行流访问(就是执行访问)共享资源——互斥。 ③共享的,任何时刻只允许一个执行流访问的资源称为临界资源---一般是内存空间。 ④举例:100行代码,5到10行代码才在访问临界资源。那我们访问临界资源的代码称为临界区。
3、)解释一个现象: 多进程、多线程、并发打印时,显示器上的信息是错乱的、混乱的、和命令行混在一起的。这就是数据不一致问题。
4、)理解信号量:信号量/信号灯的本质是一把计数器,用来描述临界资源中资源数量的多少。 ①申请计数器成功就表示,我具有访问资源的权限了。 ②申请了计数器资源。我当前访问我要的资源了吗? 没有。申请了计数器资源是对资源的预定机制。 ③计数器可以有效保证进入共享资源的执行流的数量。 ④所以每一个,执行刘翔访问共享资源中的一部分的时候不是直接访问,而是先申请计数器资源。这个计数器就叫做信号量。 ⑤我们把值只能为01两态的计数器叫做二元信号量,本质就是一个锁。 ⑥让计数器为1,资源为1的本质,其实就是不将临界资源分成很多块了,而是当做一个整体整体申请,整体释放(比如管道)。 ⑦要访问临界资源,需要申请信号量计数器资源。而信号量计数器也是共享资源。它要先保证自己的安全。 cnt--:C语言一条语句变成汇编,多条(3)汇编语句。 a t变量的内容,内存->CPU寄存器 b.cpu内进行--操作 c.将计算结果写回cnt变量的内存位置。 进程在运行的时候可以随时被切换,多执行流都访问这个变量时,可能会出错。(线程部分详细解释) ⑧申请信号量本质是对计数器--(P操作)。 释放资源,释放信号量,本质是对计数器进行++操作(V操作)。 申请和释放PV操作——原子的:一件事情要么不做,要做就做完(两态的),没有“正在做”这样的概念。用一条汇编语句即可实现。
总结:信号量本质是一把计数器。对计数器匹配的操作叫pv操作---原子的。执行流申请资源,必须先申请信号量资源,得到信号量之后才能访问临界资源。信息量值10两态的二元信号量就是互斥功能。申请信号量的本质是:对临界资源的预定机制。
2、接口:
①申请信号量:
②控制信号量:
③设定信号量:
注:system v的接口是最难的:多线程部分来进行操作说明。
3、信号量凭什么是进程间通信的一种? ①通信不仅仅是通信数据,也在于互相协同。 ②要协同,本质也是通信信号量首先被所有的通信进程看到。
Linux篇:进程间通信由讯客互联其他栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Linux篇:进程间通信”