网站建设服务器和空间费,wordpress 5.2,韶关市网站建设招标,长沙医院网站建设目录
一、进程间通信的背景
进程间通信方式
进程间通信目的#xff08;为什么要进程间通信#xff09;
二、管道
管道的特点
匿名管道
命名管道
匿名管道与命名管道的区别
三、System V共享内存
1.shmget函数
2.shmctl函数
3.shmat函数和shmdt函数
借助管道实现…目录一、进程间通信的背景进程间通信方式进程间通信目的为什么要进程间通信二、管道管道的特点匿名管道命名管道匿名管道与命名管道的区别三、System V共享内存1.shmget函数2.shmctl函数3.shmat函数和shmdt函数借助管道实现访问控制版的共享内存一、进程间通信的背景进程是具有独立性的即使是父子进程之间父进程的数据子进程能够看得见但这是在没有发生写时拷贝的前提下一旦发生了写时拷贝父子进程之间的数据是不能相互看见的这是因为进程是具有独立性的。正是因为进程具有独立性所以进程之间想要交互数据成本是非常高的。所以我们要有支持进程间通信的方法。除此之外如果我们想要多进程协同完成一件事情也需要实现进程间通信。进程之间实现通信的的前提是我们要让不同的进程看到同一份资源。这个同一份资源可以是文件、内存块等等只有不同的进程看到了同一份资源才可以实现一个进程向资源内写入内容其它进程可以从资源内读取内容获取信息从而完成通信。不同的资源种类决定了不同的进程间通信方式。进程间通信方式常见的方式有管道SystemV进程间通信POSIX进程间通信进程间通信目的为什么要进程间通信数据传输:一个进程需要将它的数据发送给另一个进程。资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息通知它(它们)发生了某种事件(如进程终止时要通知父进程)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。二、管道管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“。管道本质上就是一个内存级文件。当我们创建一个进程的时候操作系统会维护一个task_struct结构以及files_struct结构files_struct结构里有一个结构体数组struct file*fd_array[]其中保存着该进程打开的文件的地址。我们用该进程创建并打开一个管道文件然后创建一个子进程。当子进程被创建成功的时候会将父进程的task_struct结构和files_struct结构拷贝下来。因此父进程打开的文件子进程也可以找得到。此时的父进程打开的文件就是能够被子进程看到的同一份资源我们可以在管道文件中实现进程间的通信这就是管道的原理。管道的特点管道是用来传输数据的文件这份文件可以被多个进程同时看到。管道是半双工的数据只能从一个方向流动。即进行通信的两个进程只能由一个进程向管道写入内容由另一个进程从管道中读取内容。不能两个进程同时向管道写入和读取内容。一般而言进程退出管道释放所以管道的生命周期是随进程的。管道是自带同步机制的它会自带访问控制当管道满了的时候写端进程不能再写入数据必须阻塞式等待读端进程读取走数据才可以接着写入当管道空了的时候读端进程不能再读取数据必须阻塞式等待写端进程写入数据才可以接着读取。管道是面向字节流的。首先管道是一块固定大小的缓冲区管道中先写入的字符一定是先被读取的。其次管道内的内容是没有格式边界的需要我们使用管道的用户来规定内容的边界。//比如说如果我们没有规定格式边界写端进程一直在写入数据但读端进程暂时就是不读取等到写端进程写入完毕以后读端进程就会一次性地从头到尾将数据全部读取。但可能出现以下情况无消息边界比如你写100个字节是连续的但是由于读端有限制每30个字节进行读取。如果我们没有规定格式边界但是写端进程写入之后读端进程也在读取数据那么写端进程就会向管道内一个字节一个字节地写入数据读端进程就会从管道内一个字节一个字节地读取数据。所以我们可以规定管道的格式边界比如我们控制写端进程在写入的时候调用write接口的时候规定每次写入的大小是sizeof()多少比如每次写入sizeof(int)大小的数据那么读端进程每次也会读取sizeof(int)大小的数据。匿名管道#include unistd.h 功能:创建⼀⽆名管道 原型 int pipe(int fd[2]); 参数 fd⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端 返回值:成功返回0失败返回错误代码使用匿名管道进行进程间通信限制在具有亲缘关系的进程之间所以使用匿名管道的步骤一般分为下面几步首先由父进程创建匿名管道文件该文件的读端和写端文件都会被打开。父进程创建子进程子进程继承了父进程的匿名管道文件。根据需求在父子进程中分别将读端文件和写端文件关闭以此来满足只有一个进程进行写入操作另一个进程进行读取操作。接下来我们就来演示一下#include iostream #include string #include cstring #include unistd.h #include sys/types.h #include sys/wait.h #define NUM 1024 using namespace std; int main() { // 1.创建匿名管道 int pipefd[2]; // 用来获取匿名管道的读写端文件描述符 // 如果匿名管道文件创建失败 if (pipe(pipefd) ! 0) { cerr pipe error endl; return 1; } // 2.创建子进程 pid_t id fork(); if (id 0) { // 子进程 // 我们让子进程进行读取操作所以就要关闭写端文件 close(pipefd[1]); // 子进程开始执行读取操作 while (true) { char buf[NUM]; memset(buf, 0, sizeof(buf)); ssize_t readRes read(pipefd[0], buf, sizeof(buf) - 1); // 读取成功打印读取到的内容 if (readRes 0) { buf[readRes] \0; cout buf endl; } // 父进程关闭写端文件停止读取 else if (readRes 0) { cout 父进程退出了子进程也可以退出了 endl; break; } // 读取失败 else { cerr read error endl; return 4; } } // 子进程读取完毕关闭读端文件 close(pipefd[0]); cout 子进程读取完毕可以退出了 endl; exit(0); } else if (id 0) { // 父进程 // 我们让父进程进行写入操作所以就要关闭读端文件 close(pipefd[0]); // 父进程开始执行写入操作 string msg 你好子进程我是父进程; int cnt 0; while (cnt 5) { ssize_t writeRes write(pipefd[1], msg.c_str(), msg.size()); // 写入失败 if (writeRes 0) { cerr write error endl; return 3; } cnt; sleep(1); } // 父进程写入完毕关闭写端文件 close(pipefd[1]); cout 父进程写入完毕可以退出了 endl; } else { cerr fork error endl; return 2; } // 父进程最后需要回收子进程 pid_t waitRes waitpid(id, nullptr, 0); // 等待失败 if (waitRes ! id) { cerr wait error endl; return 5; } return 0; }命名管道命名管道和匿名管道的特征几乎一致不同的地方在于匿名管道只能够让父子进程或者是兄弟进程之间通信而命名管道是让两个毫无亲缘关系的进程通信。和匿名管道一样命名管道的使用首先得要创建一个命名管道文件。我们用 mkfifo 指令创建命名管道文件输入指令man mkfifo查看一下 mkfifo 指令的介绍我们在写代码的时候需要使用操作系统为我们提供的 mkfifo 接口来创建命名管道mkfifo形参1const char *pathname指定在什么路径下创建命名管道文件2mode_t mode指定命名管道文件的权限返回值如果创建命名管道文件成功则返回0否则返回-1所以创建一个命名管道:int main(int argc, char *argv[]) { mkfifo(p2, 0644); return 0; }匿名管道与命名管道的区别匿名管道由pipe函数创建并打开。命名管道由mkfifo函数创建打开用openFIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同一但这些工作完成之后它们具有相同的语义。三、System V共享内存System V是一套正常进行通信时的标准进程间通信的本质是要让不同的进程能够看到同一份资源共享内存的原理就是在物理内存上创建一个共享内存能让不同的进程都可以访问这块共享内存。每一个进程都有进程地址空间和页表页表维护的是进程地址空间和物理内存之间的映射关系。共享内存机制的通信方式首先要在物理内存上创建一块共享内存然后通过不同进程的页表将这块内存的地址映射到对应进程的共享区中这样每个进程都能拿到物理内存上的这一块共享内存也就是不同的进程可以看到同一份资源从而可以实现共享内存式的进程间通信。共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间这些进程间数据传递不再涉及到内核换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据我们再来看一下接口函数注意共享内存的接口函数使用成本很高注意辨别。1.shmget函数shmget函数是用来创建一个共享内存的它需要传入三个参数分别是 key_t key 、 size_t size 、 int shmflg。下面首先介绍一下这三个参数的含义以及使用方法。1.size_t size这个参数是用来设置共享内存的空间大小的这个参数建议设置为页的整数倍一页的大小是4KB也就是建议设置成4KB的整数倍。原因是假设我们的内存是4GB的大小一页的大小是4KB那么4GB的内存就等于1048576页也就是2^20页所以操作系统是将内存看作一个一个的页操作系统会为一个页维护一个数据结构 struct page{} 然后将这些页组织起来成为一个页数组 struct page mem[2^20]最终操作系统对内存的管理就变成了对页数组的管理。所以我们向物理内存中申请共享内存最好是以页为单位。2.int shmflg在物理内存中申请共享内存会有两种情况如果该共享内存不存在、如果该共享内存存在。shmflg这个参数需要用户传递选项由用户来规定在创建共享内存时如果该共享内存存在要怎么做如果该共享内存不存在又要怎么做。它的常见选项有以下两个IPC_CREAT 创建共享内存时如果该共享内存已经存在就获取如果该共享内存不存在就创建。IPC_EXECL 这个选项不单独使用必须和IPC_CREAT配合使用位图结构按位或即可配合使用创建共享内存时如果该共享内存不存在就创建如果该共享内存已经存在就出错返回。IPC_EXECL可以保证如果用shmget函数创建共享内存成功了那么该共享内存一定是一个全新的共享内存。3.key_t ket我们先来看共享内存的数据结构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 */ };在 struct shmid_ds{} 中有一个结构是 struct ipc_perm shm_perm 是共享内存里与权限相关的信息。我们在再看一下 struct ipc_perm{} 这个结构该结构里有一个变量 key_t _key 该变量后面的描述说这是由shmget函数提供的key值。#include sys/ipc.h struct ipc_perm { key_t __key; // 核心共享内存的键值ftok生成用户态可见为key uid_t uid; // 共享内存所有者的UID用户ID gid_t gid; // 共享内存所有者的GID组ID uid_t cuid; // 共享内存创建者的UID gid_t cgid; // 共享内存创建者的GID unsigned short mode; // 访问权限类似文件权限如0666 unsigned short __seq; // 内核内部序列号避免shmid重复 };这个key值就是shmget函数接口中需要传入的参数 key_t ket 它标定了共享内存在内核中的唯一值。这个key值是由用户提供的而不是由操作系统生成的原因是如果key值是由操作系统生成的那么我们一个进程调用shmget函数以后获取到了这个key值它是没有办法让其它进程也获得该key值得。由于key值是标识共享内存的唯一性的所以如果我们想让通信的两个进程看到同一份共享内存只需要让他们拥有同一个key值即可。理论上来说key值的设定我们可以自己给值但需要注意的是不能与操作系统中已有的共享内存的key值起冲突。所以方便起见操作系统为我们提供了ftok接口来生成key值我们只需要传递对应的文件路径和项目id项目id可以自定义设置一般在0-255之间就够了它会根据文件路径找到对应的文件拿到该文件的inode因为每个文件的inode具有唯一性和我们传入的项目id进行组合生成一个具有唯一性的key值。2.shmctl函数shmctl是一个控制共享内存的接口它可以控制删除共享内存就不用在命令行删除那么麻烦了可以直接写代码删除共享内存、设置共享内存属性以及获取共享内存的属性。它需要传递三个参数指定要操作的共享内存的shmidint cmd 和 struct shmid_ds * buf 。shmid:由shmget返回的共享内存标识码cmd:将要采取的动作有三个可取值buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构其中cmd可取IPC_STAT这个命令选项是用来获取共享内存信息的共享内存的信息保存在 struct shmid_ds {} 这个结构中shmctl函数的第三个参数 struct shmid_ds *buf 可以用来获取内核中的共享内存的信息。IPC_SET这个命令选项是用来设置共享内存信息的我们可以定义变量 struct shmid_ds *buf 来写入共享内存的信息再将这个变量通过shmctl函数传递进去设置内核中的共享内存的信息。IPC_RMID这个命令选项可以删除共享内存如果使用这个选项的话第三个参数可以设置为nullptr。3.shmat函数和shmdt函数功能将共享内存段连接到进程地址空间 原型 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)shmflgSHM_RDONLY表⽰连接操作⽤来只读共享内存功能将共享内存段与当前进程脱离 原型 int shmdt(const void *shmaddr); 参数 shmaddr: 由shmat所返回的指针 返回值成功返回0失败返回-1 注意将共享内存段与当前进程脱离不等于删除共享内存段借助管道实现访问控制版的共享内存共享内存由于本身的特性它是不具有访问控制的读端进程不会阻塞式地等待写端进程写入数据即使共享内存为空读端进程也会读取。管道是具有访问控制的所以我们可以实现一份代码利用管道的特性让共享内存具有访问控制。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/ipc.h #include sys/shm.h #include sys/wait.h #include errno.h // 共享内存大小存储字符串 #define SHM_SIZE 1024 // 管道控制令牌用单个字符表示访问权限 #define TOKEN 1 // 管道文件描述符全局父子进程共享 int ctrl_pipe[2]; // 初始化共享内存 int init_shm(key_t key) { int shmid shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); if (shmid -1) { // 若共享内存已存在直接获取 if (errno EEXIST) { shmid shmget(key, SHM_SIZE, 0666); } else { perror(shmget error); exit(1); } } return shmid; } // 获取共享内存访问权限从管道读令牌 void get_access() { char token; // 阻塞读取令牌无令牌则等待 if (read(ctrl_pipe[0], token, 1) ! 1) { perror(read pipe error (get access)); exit(1); } printf([%d] 获取到共享内存访问权限\n, getpid()); } // 释放共享内存访问权限写令牌回管道 void release_access() { char token TOKEN; // 写令牌回管道释放权限 if (write(ctrl_pipe[1], token, 1) ! 1) { perror(write pipe error (release access)); exit(1); } printf([%d] 释放共享内存访问权限\n, getpid()); } // 写进程逻辑向共享内存写入数据 void write_process(int shmid) { // 挂载共享内存到进程地址空间 char *shm_addr (char *)shmat(shmid, NULL, 0); if (shm_addr (char *)-1) { perror(shmat error (write)); exit(1); } // 循环写入3次数据每次先获取权限 for (int i 0; i 3; i) { // 1. 获取访问权限管道同步 get_access(); // 2. 操作共享内存写数据 char msg[SHM_SIZE]; snprintf(msg, SHM_SIZE, 这是第%d次写入的数据PID%d, i1, getpid()); strcpy(shm_addr, msg); printf([%d] 写入共享内存%s\n, getpid(), shm_addr); sleep(1); // 模拟写操作耗时 // 3. 释放访问权限 release_access(); sleep(1); // 给读进程留出读取时间 } // 解除共享内存挂载 if (shmdt(shm_addr) -1) { perror(shmdt error (write)); exit(1); } } // 读进程逻辑从共享内存读取数据 void read_process(int shmid) { // 挂载共享内存到进程地址空间 char *shm_addr (char *)shmat(shmid, NULL, 0); if (shm_addr (char *)-1) { perror(shmat error (read)); exit(1); } // 循环读取3次数据每次先获取权限 for (int i 0; i 3; i) { // 1. 获取访问权限管道同步 get_access(); // 2. 操作共享内存读数据 printf([%d] 读取共享内存%s\n, getpid(), shm_addr); sleep(1); // 模拟读操作耗时 // 3. 释放访问权限 release_access(); sleep(1); // 给写进程留出写入时间 } // 解除共享内存挂载 if (shmdt(shm_addr) -1) { perror(shmdt error (read)); exit(1); } } int main() { // 1. 创建控制管道用于访问权限控制 if (pipe(ctrl_pipe) -1) { perror(pipe error); exit(1); } // 2. 初始化管道令牌先写入1个令牌表示初始可访问 char init_token TOKEN; if (write(ctrl_pipe[1], init_token, 1) ! 1) { perror(init pipe token error); exit(1); } // 3. 创建共享内存key基于当前文件项目ID生成 key_t shm_key ftok(./shm_pipe_demo.c, 1); if (shm_key -1) { perror(ftok error); exit(1); } int shmid init_shm(shm_key); printf(共享内存创建成功shmid%d\n, shmid); // 4. 创建子进程读进程 pid_t pid fork(); if (pid -1) { perror(fork error); exit(1); } else if (pid 0) { // 子进程读共享内存 printf(子进程PID%d启动准备读取共享内存\n, getpid()); read_process(shmid); exit(0); } else { // 父进程写共享内存 printf(父进程PID%d启动准备写入共享内存\n, getpid()); write_process(shmid); // 等待子进程退出 waitpid(pid, NULL, 0); printf(子进程已退出\n); // 删除共享内存 if (shmctl(shmid, IPC_RMID, NULL) -1) { perror(shmctl delete error); exit(1); } printf(共享内存已删除\n); } // 关闭管道 close(ctrl_pipe[0]); close(ctrl_pipe[1]); return 0; }流程思路如下我们这里先创建命名管道文件父进程作为向共享内存写入的一端所以当向共享内存写入完毕以后再向命名管道写入一个信号代表写端进程写入完毕了子进程作为读端进程在读取共享内存的数据之前先读取命名管道的信号由于命名管道是有访问控制的所以如果写端进程没有发送信号过来就意味着写端进程还没有向共享内存写入数据此时读端进程就会阻塞式等待读取命名管道的内容当读取到信号以后再从共享内存读取信息这样就可以利用管道的访问控制从而实现共享内存的访问控制。