ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Linux】21:进程间通信

【Linux】21:进程间通信 目录一、进程间通信介绍1.1 进程间通信的目的1.2 进程间通信的分类二、管道三、匿名管道3.1 背景3.2 原理3.3 测试接口的代码3.4 5种特性3.5 4种情况四、命名管道4.1 创建一个命名管道4.2 命名管道和匿名管道的区别4.3 测试代码一、进程间通信介绍我们之前说过进程之间是相互独立的。每一个进程都有自己的PCB,数据代码页表等等。一个进程出现Bug不会影响另外一个进程。即使时父子进程也是相互独立的。但是如果进程之间想要协助呢那怎么实现呢一个进程的数据如果给另一个进程呢1.1 进程间通信的目的数据传输一个进程需要将它的数据发送给另一个进程资源共享多个进程之间共享同样的资源。通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。1.2 进程间通信的分类1管道匿名管道pipe命名管道2System V IPCSystem V 消息队列System V 共享内存System V 信号量3POSIX IPC消息队列共享内存信号量互斥量条件变量读写锁进程之间通信的本质是让不同的进程先看到同一块资源内存然后才有通信的条件。这个资源内存不能由任何一个进程提供。即使是父子进程也做不到在父进程malloc一块空间交给子进程当子进程修改时会发生写时拷贝父进程时看不到子进程修改的。因此这个资源内存必须要由操作系统来提供。所以操作系统需要提供相应的系统调用。不管后面的进程通信的种类多么复杂。本质就是让不同的进程先看到同一块资源内存二、管道我们之前在命令行使用的 | 就是一种管道那么我们在命令行使用管道它肯定会在底层给我们转化成对应的操作管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”三、匿名管道3.1 背景在计算机早期人们是没有很强烈的进程通信需求的当谈到两个进程之间需要通信的时候我们首先想到的根本不是设计什么标准设计什么接口往往想到的是能不能基于当前操作系统技术来进行进程之间通信。所以就不需要专门在操作系统里修改源代码。我们能不能在计算机里面挑选出我们已经由的功能把这些功能稍微改一下来实现进程之间通信呢答案是有的。那个时代的工程师想到了文件技术。因为文件打开之后在操作系统内核结构中会生成一个file类型的对象而文件天然就有自己的缓冲区只不过我们之前写文件时是把文件缓冲区的内容刷新到磁盘上现在能不能把文件缓冲区里面的数据刷新或者交给另外一个进程呢不把文件缓冲区的数据刷新到磁盘上而交给另外一个进程那一瞬间不久有进程间通信技术了吗基于这一个技术实现了进程之间的通信还可以将之前的技术复用起来。3.2 原理我们在父进程里面打开一个文件图示如下当父进程想要创建一个子进程就需要把自己的PCB数据和代码拷贝给子进程。如下所示子进程和父进程的文件描述符3都能够找到父进程打开的文件这两个进程不就可以看到同一块缓冲区了吗就有了进程之间通信的前提条件了。上面的就是管道通信的原理。一个进程将数据写道文件的缓冲区里面另外一个进程从缓冲区里面读取所以两个进程就可以通信了。但是这里的文件缓冲区里面的数据不是还有刷新到磁盘上吗很明显父子通信的数据属于临时数据是不需要刷新到磁盘上的。所以真正的管道的实现并不是基于我们之前将的文件而真正的管道文件它是内存级的不需要将数据刷新到磁盘上甚至和磁盘都没有关系上面所画的图是理解过程采用上面的这一套原理然后再操作系统当中稍微修改一下。衍生出来了一个管道技术。因此需要将文件部分做一下修改形成管道技术如下所示父进程以读写的方式创建一个管道然后创建一个子进程。父进程和子进程就能看到同一块内存空间了。然后父子进程需要关闭不需要的文件标识符。父进程想要往管道里面写数据就需要关闭读的文件描述符子进程需要读数据就需要关闭写的文件描述符这就形成了一个单项通信的信道这个信道就被称作管道。所以看待管道就如同看待文件一样管道的使用和文件一致迎合了“Linux一切皆文件思想”这个管道是操作系统为了进程之间通信单独设计的所以需要配上单独的系统调用接口。如下所示这个参数是一个输出型参数需要传入一个int类型的数组。fd[0]表示读端的文件描述符,fd[1]表示写端的文件描述符。返回值成功创建管道返回0失败返回-1.创建管道的时候我们没有提供文件路径所以说这个管道是内存级别的没有文件名字所以也称为匿名管道。那我们是怎么保证两个进程打开的是同一个管道文件呢答案是子进程继承父进程的文件描述符表。3.3 测试接口的代码#include iostream #include cstring #include cstdio #include unistd.h #include sys/types.h #include sys/wait.h void ChildWrite(int wfd) { char buffer[1024]; int cnt 0; while (1) { snprintf(buffer, sizeof(buffer), i am child,pid: %d,cnt: %d, getpid(), cnt); write(wfd, buffer, strlen(buffer)); sleep(1); } } void FatherRead(int rfd) { char buffer[1024] {0}; while (1) { buffer[0] 0; ssize_t n read(rfd, buffer, sizeof(buffer) - 1); if (n 0) { buffer[n] 0; std::cout child say: buffer std::endl; } } } int main() { // 1创建管道 int fds[2] {0}; int n pipe(fds); if (n 0) { std::cerr pipe erorr std::endl; return 1; } std::cout fds[0]: fds[0] std::endl; std::cout fds[1]: fds[1] std::endl; // 2创建子进程 pid_t id fork(); if (id 0) { // 子进程code // 3关闭不需要的文件描述符,形成单向通信的信道 // 父进程读子进程写。 close(fds[0]); ChildWrite(fds[1]); close(fds[1]); exit(0); } // 3关闭不需要的文件描述符,形成单向通信的信道 // 父进程读子进程写。 close(fds[1]); FatherRead(fds[0]); waitpid(id, nullptr, 0); close(fds[0]); return 0; }3.4 5种特性1.匿名管道,可以允许具有血缘关系的进程之间进行进程间通信常用与父子,仅限于此2.匿名管道默认给读写端要提供同步机制3. 管道数据是面向字节流的4.管道的生命周期是随进程的5. 管道是单向通信的是半双工通信的一种特殊情况。3.5 4种情况1. 正常情况如果管道没有数据了读端必须等待直到有数据为止(写端写入数据了)2. 正常情况如果管道被写满了写端必须等待直到有空间为止(读端读走数据)3. 写端关闭读端一直读取, 读端会读到read()返回值为0时表示读到文件结尾4. 读端关闭写端一直写入OS会直接杀掉写端进程通过向目标进程发送SIGPIPE(13)信号终止目标进程四、命名管道管道应用的一个限制就是只能在具有共同祖先具有亲缘关系的进程间通信。如果我们想在不相关的进程之间交换数据可以使用FIFO文件来做这项工作它经常被称为命名管道。命名管道是一种特殊类型的文件进程A打开了一个a/b/c.txt文件进程B打开了一个a/b/c.txt文件。在操作系统内核中不会把这个文件的inode内容在内存中加载两次因为没有必要此时进程A和进程B就看到了同一个文件就满足了进程之间通信的前提条件了看到了同一块资源了。图示如下上面的匿名管道是通过子进程继承父进程的方式让两个进程看到了同一份资源。这里的方式是通过打开同一个路径下的同一个文件来让两个不同的进程看到了同一份资源。文件是有路径和名字的。路径是具有唯一性的所以能看到同一份资源。又因为AB进程打开文件是有名字的原理也是基于文件的通信方式。所以也被称为命名管道。又因为磁盘级文件的数据是需要刷新到磁盘之中的所以AB进程打开的文件不能是历史上所学的那种普通文件需要让linux内核支持一种特殊的文件叫做管道文件。管道文件有一个特点只会被打开不需要将数据刷新到磁盘之中。所以AB进程想要通信可以创建一个命名管道来通信。4.1 创建一个命名管道命名管道可以从命令行上创建命令行方法是使用下面这个命令pipe就是一个管道文件文件类型是p。命名管道也可以从程序里创建相关函数有删除管道文件可以使用unlink 管道名字或者rm -f 管道名字4.2 命名管道和匿名管道的区别4.3 测试代码如果我们想要让两个进程通信可以让一个进程把对应的管道文件创建出来后面的工作就是文件操作了一个进程把文件以写的方式打开一个进程把文件以读的方式打开就可以实现进程之间通信了。server.cc#include iostream #include string #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h int main() { umask(0); // 1:创建管道文件 int n mkfifo(FIFO_FILE, 0666); if (n ! 0) { std::cerr mkdir fifo error std::endl; return 1; } int fd open(FIFO_FILE, O_RDONLY); if (fd 0) { std::cerr open error std::endl; return 1; } int cnt 1; while (1) { char buffer[1024]; int n read(fd, buffer, sizeof(buffer) - 1); if (n 0) { break; } buffer[n] 0; std::cout client say: buffer [ cnt ] std::endl; } return 0; }client.cc#include iostream #include string #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h int main() { int fd open(FIFO_FILE, O_WRONLY); if (fd 0) { std::cerr open error std::endl; return 1; } while (1) { std::cout Please Enter#; std::string message; getline(std::cin, message); write(fd, message.c_str(), message.size()); } return 0; }运行如下所示当client按下ctrlc终止进程时client关闭写端server端read就会返回0结束server进程。我们修改一下上面的代码如下所示server.cc#include iostream #include string #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h int main() { umask(0); // 1:创建管道文件 int n mkfifo(FIFO_FILE, 0666); if (n 0) { std::cerr mkdir fifo error std::endl; return 1; } std::cout mkfifo sucess std::endl; // 2:打开管道文件 int fd open(FIFO_FILE, O_RDONLY); if (fd 0) { std::cerr open error std::endl; return 1; } std::cout open sucess std::endl; // 3:读取管道文件的数据 int cnt 1; while (1) { char buffer[1024]; int n read(fd, buffer, sizeof(buffer) - 1); if (n 0) { break; } buffer[n] 0; std::cout client say: buffer [ cnt ] std::endl; } // 4关闭fd文件描述符 close(fd); // 5:删除管道文件 n unlink(FIFO_FILE); if (n 0) { std::cout remove fifo suecess std::endl; } else { std::cerr remove fifo failed std::endl; } return 0; }client.cc#include iostream #include string #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h int main() { // 1:以写的方式打开文件 int fd open(FIFO_FILE, O_WRONLY); if (fd 0) { std::cerr open error std::endl; return 1; } std::cout open sucess std::endl; // 2:写数据给管道文件 std::string message; int cnt 1; pid_t id getpid(); while (1) { std::cout Please Enter#; getline(std::cin, message); message ,message number std::to_string(cnt) [ std::to_string(id) ]; write(fd, message.c_str(), message.size()); } // 3:关闭文件描述符 close(fd); return 0; }我们编译只会单独运行server程序如下所示我们发现成功创建了管道文件但是进程没有输出open success/failed这就说明代码阻塞到了open这里。这是为什么呢这是因为server在等待一个进程以写的方式打开这个管道文件如果没有没有进程以写的方式打开这个管道文件那么server以读的方式打开有什么意义呢现在我们运行client程序如下所示此时server端就会输出open success/failed。如下所示我们在客户端里面输入内容在server端就都会显示出来如下所示当client按下ctrlc终止进程时client关闭写端server端read就会返回0结束server进程。通过上面的代码我们可以得出一个结论当写入方没有执行open的时候读方就要在open内部进行阻塞直到写入方以写入的方式打开读方的open才会返回。上面的代码是面向过程的我们修改一下代码把代码修改成面向对象的如下所示comm.hpp#include iostream #include string #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h class NamedFifo { public: NamedFifo(const std::string path, const std::string name) : _path(path), _name(name) { fifoname _path / _name; umask(0); int n mkfifo(fifoname.c_str(), 0666); if (n 0) { std::cerr mkdir fifo error std::endl; } else { std::cout mkfifo sucess std::endl; } } ~NamedFifo() { int n unlink(fifoname.c_str()); if (n 0) { std::cout remove fifo suecess std::endl; } else { std::cerr remove fifo failed std::endl; } } private: std::string _path; std::string _name; std::string fifoname; }; class FileOper { public: FileOper(const std::string path, const std::string name) : _path(path), _name(name), _fd(-1) { _fifoname _path / _name; } void OpenForRead() { _fd open(_fifoname.c_str(), O_RDONLY); if (_fd 0) { std::cerr open error std::endl; } else { std::cout open sucess std::endl; } } void OpenForWrite() { _fd open(_fifoname.c_str(), O_WRONLY); if (_fd 0) { std::cerr open error std::endl; } else { std::cout open sucess std::endl; } } void Write() { std::string message; int cnt 1; pid_t id getpid(); while (1) { std::cout Please Enter#; getline(std::cin, message); message ,message number std::to_string(cnt) [ std::to_string(id) ]; write(_fd, message.c_str(), message.size()); } } void Read() { while (1) { char buffer[1024]; int n read(_fd, buffer, sizeof(buffer) - 1); if (n 0) { break; } buffer[n] 0; std::cout client say: buffer std::endl; } } void Close() { if (_fd 0) close(_fd); } ~FileOper() { } private: std::string _path; std::string _name; std::string _fifoname; int _fd; };server.cc#include comm.hpp int main() { // 创建管道文件 NamedFifo fifo(., fifo); // 文件操作 FileOper readerfile(., fifo); readerfile.OpenForRead(); readerfile.Read(); readerfile.Close(); return 0; }client.cc#include comm.hpp int main() { FileOper writerfile(., fifo); writerfile.OpenForWrite(); writerfile.Write(); writerfile.Close(); return 0; }
返回列表