linux:命名管道与共享内存

发布时间:2026/6/13 5:10:10
linux:命名管道与共享内存 1.命名管道匿名管道只能有血缘关系的进程进行通信没有对应的实体文件在进程之中也就是在内核空间中的一个内存缓冲区不会在任何目录下显示。而命名管道以一个特殊的管道文件类型为p存在于文件系统中但只用了它的“名字空间”和“元数据管理功能”而数据的实际存储完全不用文件系统。但文件大小始终显示为0。我们可以使用mkfifo创建一个 p 属性的命名管道文件有文件名和文件目录就可以找到这个文件我这里创建一个名字为 fifo 的文件mkfifo fifo一个终端输入另一个终端输入这样第一个终端就会结束echo 和 cat 通过命名管道fifo 进行了通信该管道文件大小一直时0。它实际上是创建符号在被打开进行数据通信的时候所以它的内容也不会向磁盘刷新。如何做到两个进程看到同一份资源宏观不同路径为什么可以看到同一份资源因为路径是唯一的所以使用路径文件名然后inode相同来确定同一份资源。分别以读写方式打开微观操作系统同一个文件数据不会加载两次即属性和缓冲区内容是不用再加载的 struct file 需要再次创建因为读写位置可能不同。普通文件也是这样的原理只不过管道文件不需要向磁盘刷新跟匿名管道也是相同原理相同的缓冲区但是会创建不同的struct file命名管道也是管道也是是符合单向通信的。要需要不相关的管道之间进行通信需要两个可执行程序但他们实际还是 shell 的子进程兄弟关系但我们不使用。这里我们实现两个进程client写server读取。makefile 一次只能形成一个可执行我们已这种方式设计makefile :.PHONY:all all:server client client:client.cc g -o $ $^ -stdc11 server:server.cc g -o $ $^ -stdc11 .PHONY:clean clean: rm -f server client fifoLinux一切皆文件命名管道也是文件实现两个进程的通信和文件操作是相似的即打开文件操作关闭文件在进行通信之前我们先使用mkfifo 创建好命名管道文件对于两个进程谁创建文件都可以然后需要都打开文件只有一端打开会阻塞只有两个进程都打开命名管道里各自的程序才会继续执行。server.cc实现读操作#define FILENAME fifo //.隐藏文件不想显示出来 int Mkfifo() { int n mkfifo(FILENAME,0666);//不写路径直接使用当前路径 if(n -1) { std::cerr errno: errno ,errstring: strerror(errno)std::endl; return 0; } std::coutmkfifo success... std::endl; return 1; } int main() { //只有一端打开这里会阻塞需要读写都打开 Start: //文件操作读方式 打开文件 int rfd open(FILENAME,O_RDONLY);//只读 if(rfd -1) { std::cerr errno: errno ,errstring: strerror(errno)std::endl; if(Mkfifo()) goto Start;//创建好管道还需要打开 else return 1; } std::coutopen fifo success... readstd::endl; char buffer[2048]; while (true) { ssize_t s read(rfd, buffer, sizeof(buffer) - 1); // 写入的时候要求不写入\0, -1为了防止越界最后一位填上\0 if (s 0) { buffer[s] 0; // 表示是字符串 std::cout client say# buffer endl; } else if(s 0)//写端关闭读端也关闭 { std::cout client quit,server quit too!endl; break; } } close(rfd); std::coutclose fifo success...readstd::endl; return 0; }client.cc实现写操作#define FILENAME fifo //.隐藏文件不想显示出来 int main() { //打开文件 int wfd open(FILENAME,O_WRONLY); if(wfd -1) { std::cerr errno: errno ,errstring: strerror(errno)std::endl; return 1; } std::coutopen fifo success...writestd::endl; //写 std::string message; while(true) { std::coutPlease Enter# ; std::getline(std::cin,message); ssize_t s write(wfd,message.c_str(),message.size()); if(s -1) { std::cerr errno: errno ,errstring: strerror(errno)std::endl; break; } } close(wfd); std::coutclose fifo success...writestd::endl; return 0; }2.System V IPCSystem V 就是 Linux 里一套古老、经典、跨进程通信的 IPC 标准。管道通信是复用了系统文件代码不属于system vsystem v是一个单独模块用来专门负责进程间通信。只有匿名管道仅支持父子进程之间的通信而其他通信方式都支持非父子关系的通信。2.1 shm 原理进程间通信的原理必须让不同的进程看到同一份资源资源由操作系统提供管道中打开文件使用的文件缓存区就是操作系统提供的。管道文件属于内核数据结构在内核空间访问需要调用系统调用。操作系统一定会允许系统中同时存在多个共享内存操作系统为了管理要创建一个struct shm包含各种属性比如谁创建的什么时候创建。对共享内存的管理就变成了对所有共享内存数据结构对象的增删查改。共享内存也要被操作系统管理 先描述后组织。如何保证第二个之后参入共享内存通信的进程看到的就是同一个共享内存呢就要求共享内存必须有唯一的标识如何做到并给另一个进程呢2.2 认识系统接口shmget返回共享内存的shmidkey 通信双方约定的数字标识一份共享内存保证看到同一份共享内存不建议手动设置因为可能与系统中的其他共享内存起冲突我们使用一个frok()算法函数生成可以降低冲突概率proj_id就是自己捏造的数字了。通信双方使用同一个pathname和proj_id就可以形成相同key就可以找到同样的共享内存。当一个进程使用shmget和key在物理内存中创建一份空间后另外的通信进程就可以通过相同的key找到这份空间实现共享内存。通过shmflg选项shmget既能创建也可以获取IPC_CREAT如果不存在就创建存在就获取并返回。IPC_EXCL不单独使用单独使用没有任何意义一般要和IPC_CREAT按位或。IPC_EXCL | IPC_CREATshm不存在就创建存在就出错返回可以保证创建的共享内存是全新的。shmget成功返回标识符失败返回-1key_t GetKey() { key_t key ftok(pathname.c_str(),proj_id); if(key 0)//失败返回-1路径不存在路径无访问权限会返回失败 { std::cerr errno: errno ,errstring: strerror(errno) std::endl; exit(1); } return key; } const int size 4096 int main() { int shmid shmget(key,size,IPC_CREAT|IPC_EXCL); if(shmid 0) { std::cerr errno: errno ,errstring: strerror(errno) std::endl; exit(1); } return shmid; }结果可以看到第一次是创建后面因为共享内存存在出错返回使用ipcs -m可以查看共享内存的状态perms是权限nattch是与它挂接的进程数量。还可以发现进程退出后共享内存还存在与文件不同必须用户自己主动释放。可以使用-ipcs -m shmid释放。key vs shmid。key:是共享内存的属性不要在应用层使用只用来在内核中表示共享内存的唯一性。shmid应用这个共享内存的时候我们使用shmid来进行操作共享内存。 key 偏底层像 fdshmid 像 FILE*我们使用时一般应用使用 shmid。shmflg 也可以与权限直接按位或就可以改变permsint shmid shmget(key, size, IPC_CREAT|IPC_ECL|0644);shmatat-attach把进程挂接到共享内存shmid表示要挂接的共享内存id,shmaddr一般为nullptr, 它的作用是将共享内存挂接到进程指定的虚拟地址中而我们对虚拟地址空间的使用情况又不了解所以一般不使用手动让操作系统操作。shmflg代表共享内存挂接到共享内存时采用的读写方式因为我们创建共享内存空间时已经设置过权限所以直接设为默认0。成功返回进程虚拟地址空间挂接的其实地址失败返回 void*) -1 。char* s (char*)shmat(shmid,nullptr,0);当进程退出时共享内存的nattch就会减一。shmdt如何在进程不退出的情况下去掉与共享内存的关联呢使用shmdt函数也就是取消页表的映射关系。dt-detachshmdt(s);shmctl如何在进程中清除共享内存使用shmctl控制可以实现改和查找第一个操作表示要执行的操作IPC_RMID表示删除RM-remove ID-immidiately 或 shmid。shmid_ds 可以存放很多共享内存的属性它其实就是共享内存结构体的一个子集专门提供给上层用户读取和修改的数据类型我们不修改它的内容直接写nullptr就行。shmctl(shmid,IPC_RMID,nullptr);为什么不让操作系统来帮我们来形成 key而让用户层形成再到内核中使用在操作系统中唯一值key只有操作系统和创建该共享内存的进程知道想获取该共享内存的进程是不知道的所以我们通过用户层让想获取共享内存的一方知道。不同进程通过挂接相同的共享内存可以看到相同的资源。挂载之后就可以进行通信了直接读取和写入coutcin不用像管道使用 read 和 write 了也不需要等代对方。这里我的系统内核进行了初始化如果没有也可以根据申请时共享内存的大小初始化。//server 读取--------- while(true) { //直接读取 std::cout 共享内存的内容: s std::endl; sleep(1); } //client 写入------------- char c a; for(;cz;c) { s[c-a] c;//写入 sleep(5); std::cout write: c donestd::endl; sleep(6); }读端读了很多次可以说明共享内存是没有任何同步机制的。共享内存是需要同步的我们可以通过创建一个命名管道。//server 读取--------- int fd open(filename.c_str(),O_RDONLY);//双方都打开才会继续进行 //TODO while (true) { int code 0; ssize_t n read(fd, code, sizeof(code)); if (n 0)//通过命名管道收到指令后再读取 // 直接读取 { std::cout 共享内存的内容: s std::endl; sleep(1); } else if (s 0) { break; } } //client 写入------------- int fd open(filename.c_str(),O_WRONLY);//双方都等对方打开 char c a; for(;cz;c) { s[c-a] c;//写入 sleep(5); std::cout write: c donestd::endl; int code 1;//发送的指令不重要 write(fd,code,sizeof(code)); sleep(6); }所以说共享内存是直接裸露给所有的使用者的一定要注意共享内存的使用安全问题。我们每次都是拷贝全部内容如何移除内容管理起来内我们可以在这个空间约定一些管理数据就行。共享内存的特点不提供同步机制共享内存提供给所有的使用者一定要注意共享内存的使用安全问题共享内存是所有进层间通信速度最快的。管道通信要通过文件操作系统调用将数据从自己进程空间的数据拷贝到管道内核再拷贝到要通信的进程空间。但凡是数据迁移都是拷贝例键盘-A进程-管道-B进程-显示器 共享内存键盘-共享内存-显示器 通过共享内存可以不用再通过用户空间。共享内存可以提供较大的空间2.3 编写代码unlink也可以删掉一个文件。读端代码通过 Init 类自动实现共享内存的创建和清除class Init { public: Init() { bool r MakeFifo(); if(!r) return ; //创建共享内存 key_t key GetKey(); cout key: ToHex(key) endl; int shmid CreateShm(key); coutshmid:shmidendl; sleep(5); std::cout 开始将shm映射到进程的地址空间中std::endl; s (char*)shmat(shmid,nullptr,0); fd open(filename.c_str(),O_RDONLY);//双方都打开才会继续进行 } ~Init() { //去除挂接 sleep(5); shmdt(s); std::cout 开始将shm从进程虚拟地址空间中移除 endl; //删除共享内存 sleep(5); shmctl(shmid,IPC_RMID,nullptr); std::cout 开始将shm从操纵系统物理内存中移除 endl; close(fd);//关闭文件 unlink(filename.c_str());//把文件删除 } int shmid; int fd; char *s; }; int main() { Init init; //TODO while (true) { int code 0; ssize_t n read(init.fd, code, sizeof(code)); if (n 0)//通过命名管道收到指令后再读取 // 直接读取 { std::cout 共享内存的内容: init.s std::endl; sleep(1); } else if (n 0) { break; } } return 0; }写端代码int main() { key_t key GetKey(); int shmid GetShm(key);//约定相同的key与proj_id,获取相同的shmid char* s (char*)shmat(shmid,nullptr,0);//挂接到自己进程地址空间 std::cout attach shm done std::endl; sleep(5); int fd open(filename.c_str(),O_WRONLY);//双方都等对方打开 char c a; for(;cz;c) { s[c-a] c;//写入 std::cout write: c donestd::endl; int code 1;//发送的指令不重要 write(fd,code,sizeof(code)); sleep(1); } shmdt(s); std::cout detach shm done std::endl; close(fd);//写端关闭读端自动关闭 return 0;调用的函数const std::string pathname/home/wdz/linux_-tencent/26/test/5/15/shm; const std::string filename fifo; //但用 . 有一个大坑如果你在不同目录运行程序生成的 key 会不一样 const int proj_id 0x11223344; const int size 4096;//共享内存的大小强烈建议设置成n*4096 因为底层是以4096 开辟的 // key_t GetKey() { key_t key ftok(pathname.c_str(),proj_id); if(key 0)//失败返回-1路径不存在路径无访问权限会返回失败 { std::cerr errno: errno ,errstring: strerror(errno) std::endl; exit(1); } return key; } std::string ToHex(int id) { char buffer[1024]; snprintf(buffer,sizeof(buffer),0x%x,id); return buffer; } int CreateShmHeler(key_t key, int flag) { int shmid shmget(key,size,flag); if(shmid 0) { std::cerr errno: errno ,errstring: strerror(errno) std::endl; exit(1); } return shmid; } int CreateShm(key_t key) { return CreateShmHeler(key,IPC_CREAT|IPC_EXCL|0644); } int GetShm(key_t key)//获取共享内存也是通过key { return CreateShmHeler(key,IPC_CREAT/*0也可以*/); } int MakeFifo() { int n mkfifo(filename.c_str(),0666);//不写路径直接使用当前路径然后加上文件名称 if(n -1) { std::cerr errno: errno ,errstring: strerror(errno)std::endl; return 0; } std::coutmkfifo success... std::endl; return 1; }通过上面的内容我们可以知道共享内存是内核中用数据结构管理起来的一块内存内核中存在很多那我们如何查看数据结构的内容呢stmctl函数功能用于控制共享内存原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数shmid:由shmget返回的共享内存标识码cmd:将要采取的动作有三个可取值buf:指向一个保存着共享内存的模式状态和访问权限的数据结构返回值成功返回0失败返回-1命令说明TPC_STAT把shmid_ds结构中的数据设置为共享内存的当前关联值IPC_SET在进程有足够权限的前提下把共享内存的当前关联值设置为shmid_ds数据结构中给出的值IPC_RMID删除共享内存段可以执行下面代码验证int main() { Init init; struct shmid_ds ds; shmctl(init.shmid,IPC_STAT,ds); std::cout ToHex(ds.shm_perm.__key) std::endl; std::cout ds.shm_segsz std::endl;//大小 std::cout ds.shm_nattch std::endl;//连接值 }能获取属性意味着操作系统帮我们维护了属性shmget 通过 key 值进行查找返回 shmid。2.4 消息队列msg了解消息队列提供一个进程给另一个进程发送数据块的能力之前通信没有块的概念想发多少发多少每个数据块都被认为是有一个类型接收者进程接收的数据块可以有不同的类型值特性方面IPC资源必须删除否则不会自动清除除非重启所以system V IPC资源的生命周期随内核获取消息队列msgflg选项与shmflg相同key 的含义也相同要同时被通信双方知道。它的表现更像链表程序员自己定义msgbuf 数据放mtext数据是 A 发的还是 B 发的可以通过 mtype 识别。msgsnd用来发数据msgrvc用来接收数据msgp是接收缓冲区地址msgsz接收缓冲区大小msgtype是判断要接受的对象。消息队列可以通过ipcs -q 看见的。消息队列系统中可以同时存在多个消息队列消息队列也要在内核中把他管理起来先描述再组织 消息队列 队列 队列的属性。这个结构第一个字段与共享内存中相同int main() { key_t key GetKey(); cout key endl; //创建消息队列 int msgid msgget(key,IPC_CREAT|IPC_EXCL); cout msgid:msgidendl; struct msqid_ds ds; msgctl(msgid,IPC_STAT,ds); cout ds.msg_perm.__keyendl;//__key 是保留字段不向用户空间公开,读到的是0 //自动删除 msgctl(msgid,IPC_RMID,nullptr); }2.5 信号量(sem)了解信号量主要用于同步和互斥的下面先来看看什么是同步和互斥。进程互斥由于各进程要求共享资源而且有些资源需要互斥使用因此各进程间竞争使用这些资源进程的这种关系为进程的互斥系统中某些资源一次只允许一个进程使用称这样的资源为临界资源或互斥资源。在进程中涉及到互斥资源的程序段叫临界区特性方面IPC资源必须删除否则不会自动清除除非重启所以system V IPC资源的生命周期随内核信号量本质就是计数器semaphore信号量通过下面semget函数创建nsems表示创建一个信号量集里面有多个信号量。使用ipcs -s查看当前系统的信号量semctl中semnum如果要删除的话代表要删除第几个信号量集合下标从0开始。int main() { key_t key GetKey(); cout key endl; //创建 int mesid semget(key,3,IPC_CREAT|IPC_EXCL);//一个信号量集里有3个信号量 cout mesid: mesid endl; semctl(mesid,0,IPC_RMID);//IPC_RMID 是删除整个信号量集合不是删单个 }操作系统也要管理多个信号量集合semid_ds。通过上面三种通信方式他们都是使用XXXid_id组织起来管理的第一个字段都是ipc_perm但这些都是用户级别的也就是OS给你暴露出来的。那么内核是如何看待 IPC 资源的呢IPC资源返回的 key 唯一是操作系统中单独设计的模块系统如何管理起来的呢这三种通信方式第一个字段类型相同可以使用柔性数组第一个参数是要管理的通信模块数量第二个参数使用指针指向他们共用的ipc_perm就可以同一管理和查找key如果要访问他们各自内部的信息可以通过强制指针转换实现。类似于C中的多态基类是kern_ipc_perm派生类是不同的semid_dsshmid_dsseggid_ds。信号量相关概念信号量的本质是一把计数器多个执行看到的同一分资源公共资源会发生并发访问数据不一致问题回归需要保护需要护持和同步。如何对公共资源保护一种是用户保护例如共享内存。 一种是OS管道消息队列。互斥任何一个时刻只允许一个执行流访问公共资源加锁完成。同步多个执行流执行时按照一定的顺序执行。被保护起来的资源临界资源。不被保护是非临界资源。访问该临界资源的代码我们叫临界区。不访问的是非临界区。维护临界资源其实就是在维护临界区原子性只有两态要么不做要么做完。信号量表示资源数目的计数器每一个执行流向访问公共资源内的某一份资源不应该让执行流直接访问而是先申请信号量资源其实就是先对信号量计数器进行减减操作本质上只要减减 成功完成了对资源的预定机制。所以访问公共资源要先访问信号量成功就继续访问资源不成功执行流被挂起等待。int sem 1 二元信号量 -- 互斥锁 -- 完成互斥功能结论要使进程能够使用同一个临界资源意味着每个进程都得先看到同一个信号量资源那只能有OS提供了可以通过IPC体系实现。信号量本质也是公共资源因为进程访问临界资源要先访问信号量。对信号量的操作只需要做减减和加加就行是原子操作。申请是P操作申请是V操作。所以信号量也有PV操作。System V 中通过 semop() 函数实现PV操作是原子的。不能用全局变量替代信号量因为它的操作不是原子性的也不能被所有进程同时看到。单个信号量struct sem{ int count; sask_struct* wait_queue;};申请成功count-- 失败进程链接入等待队列本篇结束