【操作系统】9.进程间通信(IPC)详解

📅 发布时间:2026/7/11 16:12:17 👁️ 浏览次数:
【操作系统】9.进程间通信(IPC)详解
目录1. 匿名管道1.1 为什么进程需要通讯1.2 管道原理1.3 创建管道1.4 创建进程实现通讯1.5 特点1.6 管道异常情况2. 匿名管道进程池2.1 创建子进程2.2 阻塞子进程2.3 问题3. 命名管道3.1 打开文件原理3.2 管道文件3.3 代码实现通讯4. SystemV共享内存4.1 管理机制4.2 创建共享内存4.3 key生成4.4 删除管道4.5 建立映射4.6 删除共享内存4.7 SystemV空间位置5. SystemV管道和命名管道结合通讯5.1 SystemV优缺点5.2 结合方案5.3 命名管道的创建封装5.4 命名管道链接封装5.5 SystemV封装5.6 SystemV的系统调用特性6. SystemV消息队列6.1 原理6.2 使用7. SystemV信号量7.1 保护机制7.2 保护方式7.3 原子性7.4 信号量7.5 信号量操作7.6 互斥操作7.7 信号量和通信关系7.8 信号量管理1. 匿名管道1.1 为什么进程需要通讯数据传输资源共享通知事件进程终止要通知父进程进程控制前提让不同进程先看到同一份资源内存才能用那份资源进行通讯因此需要操作系统统一设计接口1.2 管道原理基于已有的技术直接通讯子进程的files_struct为浅拷贝父进程拷贝时不需要将打开的文件粘贴一份而是使用引用计数。因此父进程结束子进程依旧能打印。可以在拷贝前父进程的files_struct创建一个文件接上2个位置然后父进程关闭一个子进程关闭另一个做到读写。1.3 创建管道int fds[2] {0}; int n pipe(fds); // fds[0]通常进行读取fds[1]写入1.4 创建进程实现通讯if(id 0) { // 子进程 - 写入 close(fds[0]); char buff[1024]; int cnt 0; while(1) { snprintf(buff, sizeof(buff)-1, 子进程pid%d%d, getpid(), cnt); cnt; write(fds[1], buff, strlen(buff)); sleep(1); } close(fds[0]); } else { // 父进程 - 读取 close(fds[1]); char buff[1024]; while(1) { int s read(fds[0], buff, sizeof(buff)-1); buff[s] \0; std::cout child say buff std::endl; } close(fds[1]); }子进程进行写入父进程进行读取C语言字符串的规定是最后一位为\0因此C语言接口要strlen-1预留空间。而操作系统规定不一样读取的字符串没有\0因此要加上因此也要留空间。1.5 特点匿名管道只能在有血缘关系的进程通讯管道文件自带同步机制如上面的代码中子进程1秒写一条父进程也只能1秒读一条面向字节流https://media/image1.png如子进程一直写父进程1秒读一次就会一次只将缓冲区读满为止单向通讯半双工一个发一个收全双工可以同时收发匿名管道就是半双工管道文件生命周期伴随进程父子进程都结束由于计数为0管道自动退出1.6 管道异常情况写慢读快读端阻塞写快读慢将管道占满后写阻塞https://media/image2.png一直向管道写入一个字符最后写入65559次差不多就是64k大小写关掉读继续读不到东西读关掉写继续操作系统对于没意义的行为会停止while(1) { int s read(fds[0], buff, sizeof(buff)-1); buff[s] \0; std::cout child say buff std::endl; sleep(1); break; } close(fds[0]); int pw 0; int ret waitpid(id, pw, 0); printf(进程退出码%d,指令%d\n, (pw8)0xff, pw0x7f);https://media/image3.png退出指令为13(SIGPIPE)2. 匿名管道进程池2.1 创建子进程bool buychannel() { for (int i 0; i _process; i) { int pipfd[2] {0}; int n pipe(pipfd); if (n 0) return 0; int sid fork(); if (sid 0) return 0; else if (sid 0) { // 子进程 close(pipfd[1]); work(pipfd[0]); close(pipfd[0]); exit(0); } else { // 父进程 close(pipfd[0]); _cm.insert(pipfd[1], sid); close(pipfd[1]); } } return 1; }work用于阻塞子进程2.2 阻塞子进程void work(int pid) { while (1) { int ret 0; int n read(pid, ret, sizeof(int)); if (n 0) { break; // 子进程退出 } else if (n 0) { if (n ! sizeof(int)) continue; else { std::cout 子进程 getpid() :任务码 ret std::endl; // 执行 _tm.excuate(n); } } } }read函数在管道读取到信息前就一直处于阻塞状态直到程序调用到这个进程2.3 问题正常情况1号管道父进程4号fd和1号子进程3号连接2号管道父进程5号fd和2号子进程3号连接......但是由于子进程会完全继承父进程的链接数组因此实际上是1号管道父进程4号fd和1号、2号、3号子进程3号fd连接2号管道父进程5号fd和2号、3号进程3号fd连接3号管道父进程6号fd和3号进程3号fd连接连接数量成一个三角形。因此先关掉1号进程1号管道文件引用计数还会为2关不掉。解决方案倒着关治标不治本for(int i _poolmanager.size()-1; i 0; i--) { channel c _poolmanager[i]; c._close(); }创建时就关掉void stopchannel2() { for(int i 1; i _poolmanager.size()-1; i) { channel c _poolmanager[i]; c._close(); } }使用数组里存的fd值即可3. 命名管道进程间不想管如何通讯3.1 打开文件原理操作系统中两个进程同时打开一个文件这个文件不会同时在内存里存在2次。过程A进程自己的fd数组指向struct file结构体指向文件inodeB进程自己的fd数组指向自己的struct file结构体指向文件inode由于inode相同就可以判断文件已经打开此时B会指向同一个文件因此struct file有2个但是inode只有1个。两进程可以同时向显示器文件打印也是同理。3.2 管道文件可以同时被2个进程写入不用刷新。创建mkfifo f1删除unlink3.3 代码实现通讯创建管道umask(0); int n mkfifo(fifo, 0666); if (n 0) { ERR_EXIT(mkfifo); } int fd open(fifo, O_RDONLY);异常处理宏#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0)读操作char buff[128]; while (1) { int n read(fd, buff, sizeof(buff) - 1); if (n 0) { buff[n] 0; std::cout user say: buff std::endl; } else if (n 0) { break; } else { ERR_EXIT(readree); } }写操作std::string s; while(1) { std::getline(std::cin, s); write(fd, s.c_str(), s.size()); }4. SystemV共享内存SystemV为一个标准设计了一个IPC通讯模块4.1 管理机制由于CPU可能存在多个共享内存块在通讯因此也需要进行管理。进程和共享内存关系就是内核数据集结构间的关系。4.2 创建共享内存shmget函数参数key一个随机值代表共享内存的编号size共享内存大小shmflag异或生成的模式选择IPC_CREAT不存在创建存在就打开IPC_EXCL跟IPC_CREAT一起使用存在就报错确保创建新内存返回值shmid用户通过返回值找到进程A进程和B进程约定key一个拿key创建一个拿key查找到对应内存。4.3 key生成ftok用路径和随机数生成随机值生成值碰撞概率较小。4.4 删除管道ipcrm -m后面跟shmid删除管道ipcs -m查询管道信息注意管道在进程结束后不会自动关闭因此下一次程序启动时由于路径名和随机数是一样的因此ftok生成值一样就会出现碰撞因此结束时要删掉。4.5 建立映射在shmget建立管道后还需要shmat函数建立进程与管道的映射。参数shmid上面返回的标识符shmaddr虚拟地址置为nullptr即可操作系统比你更懂地址shmflag权限0为默认权限返回值void*类似于malloc需要强转。4.6 删除共享内存shmctl用来控制管道IPC_RMID选项表示删除。4.7 SystemV空间位置开在栈区和堆区之间的共享区这个区可以让用户操作。5. SystemV管道和命名管道结合通讯5.1 SystemV优缺点优点速度快直接在内存调用不用经过操作系统调用缺点不会像命名管道一样阻塞等待对方导致数据不一致无法对共享内存的数据保护5.2 结合方案可以让命名管道发送读取信息当读取一方收到另一方的读取信息后再在SystemV进行读取。5.3 命名管道的创建封装class namedfifo { public: namedfifo(const std::string name, const std::string path.) : _path(path), _name(name) { _fifoname _path / _name; umask(0); int n mkfifo(_fifoname.c_str(), 0666); if (n 0 errno ! EEXIST) { ERR_EXIT(mkfifo); } else { std::cout mkfifo success std::endl; } } ~namedfifo() { int n unlink(_fifoname.c_str()); if (n 0) { ; } else { std::cout remove fifo failed std::endl; } } private: std::string _path; std::string _name; std::string _fifoname; };指定名称进行实例化就创建了管道。5.4 命名管道链接封装class pipe1 { public: pipe1(std::string name, const std::string path.) : _path(path) , _name(name) , _fd(-1) { _pipe_name path / name; } void pipe_write() { _fd open(_pipe_name.c_str(), O_WRONLY); if(_fd 0) ERR_EXIT(open_write); std::cout open std::endl; } void pipe_read() { _fd open(_pipe_name.c_str(), O_RDONLY); if(_fd 0) ERR_EXIT(open_read); std::cout open std::endl; } void weakup() { char c ; int n write(_fd, c, 1); } bool wait() { char rc; int n read(_fd, rc, 1); if(n 0) { std::cout wait std::endl; return 1; } else return 0; } void _close() { close(_fd); } ~pipe1() { ; } private: std::string _path; std::string _name; std::string _pipe_name; int _fd; };通讯双方实例化共2次并且指明名字。一方调用pipe_write并在合适时机调用weakup唤醒读取。读取一方调用pipe_read函数在函数不阻塞时就代表收到了读取指令开始读取SystemV中的信息。5.5 SystemV封装class shm { void _create(int mode) { _shmid shmget(_key, _size, mode); if (_shmid 0) { ERR_EXIT(shmget); } } void attach() { _start_space (char *)shmat(_shmid, nullptr, 0); if ((long long)_start_space 0) ERR_EXIT(shmat); printf(attach success); } public: shm(std::string who, const std::string path .) : _shmid(-1), _size(gsize), _start_space(nullptr), _path(path), _who(who) { _key ftok(path.c_str(), projid); if (_key 0) { ERR_EXIT(ftok); } if (who USER) { _create(IPC_CREAT); } else { _create(IPC_CREAT | IPC_EXCL | 0666); } attach(); } int size() { return _size; } char* virtualaddr() { return _start_space; } ~shm() { if (_who SERVE) { int n shmctl(_shmid, IPC_RMID, nullptr); if (n 0) ERR_EXIT(ctrl); } printf(del success); } private: int _shmid; int _size; char *_start_space; int _key; std::string _path; std::string _who; };由于在创建时读、写一方的区别在于权限的不同服务方的权限设置IPC_CREAT | IPC_EXCL | 0666需要确保之前管道没被创建写方只需要IPC_CREAT打开就行因此创建函数可以进行封装。5.6 SystemV的系统调用特性在程序中SystemV相关函数没有用系统调用因为它开在共享区上这个区可以让用户自己进行使用不需要系统调用。6. SystemV消息队列6.1 原理使用队列管理通讯的消息。同样用key确保通讯进程看到的消息一致。进程A和进程B通讯队列中消息的结构体里要标识消息来源的进程。进程B在队列中拿取标记为A的结构体即可实现通讯。6.2 使用由于和SystemV管道一个标准因此虽然原理不同但是使用接口基本相同。结构体包含mytype整型存储消息来源char数组存储消息发送msgsnd调用要标明正文即char数组的大小删除msgctl调用7. SystemV信号量7.1 保护机制为了确保进程的数据一致上面的SystemV管道命名管道的命名管道即为保护机制。临界区被保护的资源的位置保护临界区写的代码就是保护临界资源7.2 保护方式互斥任何时候只允许一个进程访问资源类比ATM机同步多个执行流顺序访问临界资源7.3 原子性为了确保互斥就需要锁一个进程执行时锁住临界区。但是由于锁要让其它进程看到因此要被共享。因此需要保护锁用锁的原子性保护申请锁不需要中间过程。7.4 信号量并发访问的要求不要访问同一个位置访问人数 ≤ 资源数。类比门票信号量描述临界资源数量以实现并发访问。访问临界资源要先买票即信号量--访问完位置腾出来信号量。信号量本质是对资源的预定机制。当信号量小于0时进程就挂起到大于0可申请了为止。7.5 信号量操作信号量操作就是原子性的申请P操作信号量--释放V操作信号量用PV操作完成预定机制。7.6 互斥操作即信号量只有01的操作。7.7 信号量和通信关系先访问信号P每个进程都得先看到一个信号量通信不仅仅只是传输数据通知、同步也算。如一个进程结束信号量大于1就让另一个进程访问也算通讯。7.8 信号量管理操作系统中存在多个信号量集合也要对信号量进行管理。之前命名管道、消息队列、以及这个信号量都是用key来访问不同种类但是同一个key也是会冲突的。这三者在OS内部都是同一个东西存放在ipc_id_ary可变数组中。Key即为他们的下标。他们的开头都为一个叫做kern_ipc_perm的结构体数组才可以指向他们。这就是用C语言实现多态。