珠海网站建设防,做网站的空间和服务器吗,软件项目实施流程八个阶段,优秀网站设计作品分析文章目录 前言一、管道通信1、进程间通信目的2、进程间通信分类3、匿名管道通信3.1 匿名管道通信介绍3.2 匿名管道通信3.3 匿名管道读写规则3.4 匿名管道特点3.5 站在文件描述符角度-深度理解管道3.6 站在内核角度-管道本质 4、进程池练习5、命名管道6、匿名管道与命名管道的区… 文章目录 前言一、管道通信1、进程间通信目的2、进程间通信分类3、匿名管道通信3.1 匿名管道通信介绍3.2 匿名管道通信3.3 匿名管道读写规则3.4 匿名管道特点3.5 站在文件描述符角度-深度理解管道3.6 站在内核角度-管道本质 4、进程池练习5、命名管道6、匿名管道与命名管道的区别 二、共享内存1、共享内存介绍2、共享内存使用 三、消息队列1、消息队列介绍2、消息队列操作 前言 一、管道通信
1、进程间通信目的
当程序为单进程时那么也就无法使用并发能力就更加无法实现多进程协同了。而要是想要程序变为多进程的话那么各个进程之间需要传输数据、同步执行流、消息通知等。所以进程间通信的目的就是解决这些问题。 数据传输一个进程需要将它的数据发送给另一个进程。 资源共享多个进程之间共享同样的资源。 通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。 进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。
因为进程是具有独立性的。虚拟地址空间页表保证进程运行的独立性(每个进程都有自己的进程内核数据结构进程的代码和数据)所以想要进行进程间通信的话成本会比较高因为进程间通信的前提首先需要让不同的进程看到同一块内存(特定的结构组织的)然后这样才能通过这块内存来互相交换数据。并且这块不同进程都能看到的内存不能隶属于任何一个进程而应该强调共享。
2、进程间通信分类
管道 (1). 匿名管道pipe (2). 命名管道
System V IPC (1). System V 消息队列 (2). System V 共享内存 (3). System V 信号量
POSIX IPC (1). 消息队列 (2). 共享内存 (3). 信号量 (4). 互斥量 (5). 条件变量 (6). 读写锁
3、匿名管道通信
3.1 匿名管道通信介绍
在现实世界中管道都有一个入口和一个出口管道都是单向传输内容的。而在计算机通信领域的设计者设计了一种单向通信的方式即单向传输数据的方式并且将这种通信方式命名为管道。 管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道” 下面是一个进程和它打开的内存文件的关系图此时如果这个进程使用fork创建一个子进程时操作系统会为子进程创建一份这些进程内核数据结构并且会这些内核数据结构里面的数据也会拷贝父进程中的。 但是fork之后并不会为子进程创建被打开文件相关的内核结构。所以父子进程都指向同一个被打开文件相关内核结构。所以子进程的文件描述符表中指向的struct file和父进程中指向的相同此时我们就看到父子进程都可以访问这些打开的内存文件。即这就解决了让不同的进程看到同一份内存的问题了。 此时父子进程的通信就可以通过双方进程各自关闭自己不需要的文件描述符来进行写入和读取的单向通信。即父进程写入就将读取文件描述符3关闭只保留写入的文件描述符4而子进程读取就将写入的文件描述符4关闭只保留读取的文件描述符3。这样就做到了让两个进程看到同一份空间并且父进程可以发数据给子进程。这个就叫做管道通信。是Linux天然支持的一种通信方式。并且管道通信不会将数据写到磁盘中它是一种纯内存级的通信方式。 所有的通信方式都是内存级别的。即不会将数据写到磁盘中因为进程通信的数据一般都是内存级别的临时数据所以不需要写到磁盘中进行永久保存。
3.2 匿名管道通信
匿名管道通信可以通过系统调用pipe来实现。调用pipe就可以创建一个匿名管道该函数的形参pipefd为一个输出型参数即pipefd[0]表示读端pipefd[1]表示写端。如果创建管道成功就会返回0失败就会返回-1。 我们在下面的代码中创建一个管道然后查看pipefd[0]和pipefd[1]发现pipefd[0]中存的是文件描述符3pipefd[1]中存的是文件描述符4。而pipefd[0]表示读端pipefd[1]表示写端所以此时通过pipefd[0]就可以读取管道中的数据通过pipefd[1]就可以向管道中写数据。 下面我们给这两句语句加上条件编译判断即如果我们想在运行时显示这两句打印时我们就可以定义DEBUG可以在执行编译语句时在后面定义DEBUG这个宏。
g -o $ $^ -stdc11 -DDEBUG如果我们不想让这两句语句被编译时就可以在执行编译语句时在后面注释掉DEBUG这个宏。
g -o $ $^ -stdc11 #-DDEBUG下面我们使用fork创建一个子进程然后构建单向通信的信道实现父进程向管道写入子进程从管道中读取数据。首先我们就需要先将子进程的写的文件描述符关闭将父进程的读的文件描述符关闭。 下面我们就在子进程中调用read系统调用来读取文件描述符为pipefd[0]的文件中的数据即管道中的数据。 下面我们在父进程中使用snprintf生成不同的字符串然后调用write系统调用每1秒向父进程中文件描述符为pipefd[1]的文件中写入数据。 然后我们编译并运行程序我们看到在子进程中成功读取到了父进程写到管道中的数据。管道是用来让具有血缘关系的进程之间进行通信的一种进程通信方式常用于父子通信。 然后我们将父进程每5s向管道中写入一次数据但是子进程中是一直循环从管道中读取数据的但是我们看到子进程中并没有一直打印管道中的数据而是也每5s打印一次数据。这是因为管道为了让进程间协同提供了访问控制。 当一个进程尝试从一个空的管道中读取数据时read接口会被阻塞直到管道内有数据为止当一个进程尝试向一个满的管道中写入时write接口会被阻塞直到足量的数据从管道中被读取走为止. 下面我们让父进程一直向管道中写数据子进程每5s从管道中读取一次数据我们看到会出现下面的情况这是因为当父进程向管道中一直写数据时如果管道中的数据满了此时父进程的write接口会被阻塞直到足量的数据从管道中被读取走为止即子进程从管道中将数据取走后父进程才能接着向管道中写数据。 下面我们让父进程每1s向通道中发送一次数据当发送5次数据后父进程将向管道文件写入数据的文件关闭。然后子进程一直向管道中读取数据。我们看到当管道的写的一方关闭了后读的一方最后一次读完管道中的数据后也会随之关闭。那么子进程是如何感知父进程关闭管道了呢这是因为每当一个进程打开一个文件的时候该文件的引用计数会加一每当一个进程关闭一个文件的时候该文件的引用计数会减一。当一个文件的引用计数减为0时表明没有进程打开这个文件那么这个文件才会被真正被关闭。当管道文件的引用计数为1时表明父进程已经关闭管道文件子进程读完当前消息就可以作为文件的结尾而退出了。因此子进程是可以感知父进程是否关闭写端的。
3.3 匿名管道读写规则
综上我们就知道了管道读写的规则。 a. 写快读慢。写满不能再写了。 b. 写慢读快。管道没有数据的时候读必须等待。 c. 写关读0标识读到了文件结尾。 d. 读关写继续写OS终止写进程。 当没有数据可读时 O_NONBLOCK disableread调用阻塞一直等到有数据来到为止 O_NONBLOCK enableread调用返回-1errno值为EAGAIN (使用 fcntl 函数设置非阻塞选项) 当管道满的时候 O_NONBLOCK disable write调用阻塞直到有进程读走数据 O_NONBLOCK enable调用返回-1errno值为EAGAIN 如果所有管道写端对应的文件描述符都被关闭则read返回0表示读到文件结尾 如果所有管道读端对应的文件描述符都被关闭则write操作会产生信号SIGPIPE进而可能导致write进程退出 当要写入的数据量不大于PIPE_BUFLinux下为 4096 字节时linux将保证写入的原子性。否则将不保证。
3.4 匿名管道特点
(1). 只能用于具有共同祖先的进程具有亲缘关系的进程之间进行通信通常一个管道由一个进程创建然后该进程调用fork此后父、子进程之间就可应用该管道。 (2). 管道提供流式服务。 (3). 管道为了让进程间协同提供了访问控制。 (4). 一般而言进程退出管道释放所以管道的生命周期随进程。 (5). 一般而言内核会对管道操作进行同步与互斥。 (6). 管道是半双工的数据只能向一个方向流动需要双方通信时需要建立起两个管道。
3.5 站在文件描述符角度-深度理解管道 3.6 站在内核角度-管道本质 4、进程池练习
下面我们写一个进程池练习用来更好的理解管道通信。 进程池的作用就是当父进程收到要执行的任务后会从进程池中挑选子进程来执行这个任务。 我们需要先创建多个管道文件和多个子进程每一个子进程通过一个管道与父进程通信然后父进程随机分配任务即指定哪个进程执行什么任务然后子进程处理任务。 下面我们使用for循环创建5个管道和5个子进程。 然后我们创建一个vector容器该容器里面的每一个元素都是pair类型的元素即存子进程的pid和对应的管道的写的文件描述符这样父进程才能分辨出和每个子进程对应的管道的写的文件描述符。
然后我们让子进程中调用waitCommand函数阻塞式等待命令为什么说是阻塞式等待呢这是因为在waitCommand函数中调用了read系统调用向管道中读取数据而当管道中父进程没有写入数据时此时管道就为空所以read就会一直阻塞在这里读取管道数据直到管道中被父进程写入数据为止。 下面我们再创建一个Task.hpp文件里面实现了一些函数用来模拟任务以后我们就可以将这些函数换成真正的业务代码。并且我们创建了一个存储函数类的vector容器callbacks里面存放了这些函数。 然后我们再创建一个unordered_map容器里面存对应的函数描述信息。 然后我们写一个showHandler函数用来打印各个函数的信息。写一个handlerSize函数用来返回有多少种任务。 然后再次回到ProcessPool.cc文件中引入Task.hpp头文件并且执行load函数即将我们实现的模拟任务的函数都加载到容器中。
然后我们判断子进程中调用waitCommand函数从管道中读取的数据如果从管道中读取的命令的编号正确我们就让子进程执行对应的命令。 然后在父进程中通过用户输入命令父进程随机选取一个子进程来执行这个命令。子进程通过sendAndWakeup方法来向子进程和对应的管道中写命令当管道中有数据后
然后我们再判断子进程读取管道数据时父进程是否已经关闭了写管道如果父进程关闭了写管道那么子进程也关闭读管道然后子进程退出。 下面让父进程将所有子进程都关闭并且回收所有子进程信息完成资源的回收工作。 我们可以看到当程序运行时父进程随机选取了子进程来执行命令。 如果我们不想手动输入执行第几个命令我们可以将代码改为随机选出命令编号随机选子进程执行命令。下面就为父进程随机选取任务随机选取子进程执行命令。我们的父进程采用随机数的方式来选择子进程完成任务即随机数方式的负载均衡。 我们可以看到用户不用输入此时父进程随机选取任务随机分配给子进程执行。
5、命名管道
匿名管道应用的一个限制就是只能在具有共同祖先具有亲缘关系的进程间通信。 如果我们想在不相关的进程之间交换数据可以使用FIFO文件来做这项工作它经常被称为命名管道。 命名管道是一种特殊类型的文件。 我们从上面介绍匿名管道时知道了当A进程打开一个文件后如果B进程也要打开这个文件那么操作系统并不会再重新打开一份这个文件并且创建file内核数据结构而是直接将B进程也指向这个file内核数据结构。这样两个进程就看到了同一个文件。但是我们知道普通的文件在磁盘中都有映射当缓冲区满时都会将数据更新到磁盘中这个过程是很慢的。但是命名管道文件是一个特殊的文件命名管道文件不会将内存中的数据刷新到磁盘中这样就不用和硬件磁盘建立IO了。 我们可以通过下面的命令在命令行中创建一个命名管道。
mkfifo filename下面为左边的进程循环向命名管道文件filename中写数据右边的进程一直从命名管道文件中读取数据。 命名管道文件就类似于一个文件当我们不使用这个文件时也可以删除这个文件我们可以使用rm命令或unlink命令来删除命名管道文件。
unlink filename
rm filename我们也可以在程序中通过mkfifo函数来创建一个命名管道文件。 mkfifo函数的第一个参数为命名管道文件创建的路径第二个参数为命名管道文件的起始权限如果创建命名管道文件成功就返回0如果创建命名管道文件失败就返回-1并且将错误码进行设置。 下面我们通过命名管道文件来实现两个没有血缘关系的进程进行通信。 我们需要让这两个程序都包含同一个comm.hpp头文件这个头文件中规定了命名管道文件的路径、权限等一些公共信息以便两个程序都能找到命名管道文件。
然后我们再来写好项目的makefile文件。 下面我们在server.cc中使用mkfifo函数创建一个命名管道文件。我们看到命名管道文件fifo.ipc被成功创建。 下面我们就可以进行一些正常的文件操作即打开命名管道文件从命名管道文件中读取数据或者写入数据等。我们将server进程实现从命名管道文件中读取数据并打印。 然后我们在client进程中打开管道文件然后调用write系统调用向命名管道文件中写数据。 这样就能实现client进程向命名管道文件中写数据server进程从命名管道文件中读取数据了我们需要注意的是因为是server进程创建的命名管道文件所以当server进程退出时不只是需要将命名管道文件关闭还需要将命名管道文件删除。下一次打开server进程时会重写创建一个新的命名管道文件。在程序中删除命名管道文件我们可以使用unlink系统调用。unlink系统调用的参数就是文件的路径返回值为0就表示删除文件成功返回值为-1就表示删除文件失败并且会设置errno错误码。 我们看到此时client进程向命名管道文件中写的数据都被server进程读取到了。 下面我们再来写一个文件用来打印进程的执行情况。我们在Log.hpp文件中实现了一个Log函数用来打印信息我们可以规定这个信息的级别为Debug或Notice或Warning或Error。这样就方便了程序的后期维护。 然后我们在comm.hpp头文件中引用这个Log.hpp这个头文件。 下面我们将server程序和client程序中都添加上程序运行状态的信息。 我们看到当先运行server进程时该进程会直接创建好命名管道文件但是并不会打开命名管道文件因为此时命名管道文件的引用计数为0所以server进程知道此时并没有进程向命名管道文件中写入数据即命名管道文件的写端没有打开所以不会打开命名管道文件的读端。 然后我们运行client进程client进程会以写的方式打开命名管道文件此时也可以看到server进程中打开了命名管道文件因为命名管道文件的写端已经打开所以server进程会打开命名管道文件的读端。 当我们将client进程关闭后即命名管道的写端关闭了此时可以看到server进程也关闭了命名管道的读端并且将命名管道文件也删除了。 下面我们在server进程中创建3个子进程然后每个子进程都调用getMessage函数阻塞在read中等待命名管道文件中写入数据如果命名管道文件中被client进程写入数据后此时server进程中创建的3个子进程都就会竞争式的从命名管道文件中读取数据如果一个子进程读取到数据就会将数据打印出来然后继续和其它两个进程还阻塞在read中等待命名管道文件中写入新的数据。 当我们运行server进程和client进程后我们看到server进程中成功创建了3个子进程。 我们看到在server进程中3个子进程竞争式的从命名管道文件中读取数据这样我们就使用一个管道实现了进程池。这样实现的话我们就不能指定哪一个子进程来执行任务了而是子进程竞争式的执行任务。
6、匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。 命名管道由mkfifo函数创建打开用open。 FIFO命名管道与pipe匿名管道之间唯一的区别在它们创建与打开的方式不同一但这些工作完成之后它们具有相同的语义。
二、共享内存
1、共享内存介绍
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间这些进程间数据传递不再涉及到内核换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。 我们通过前面的管道通信知道了管道其实就是特殊的文件操作系统管理管道文件和管理普通文件类似所以Linux中可以将管道当作普通文件来管理但是共享内存的通信方式是操作系统特意提供的进程通信方式所以操作系统创建出共享内存后就需要管理共享内存而管理共享内存的方式和管理进程、管理文件类似都是先建立内核数据结构然后通过管理这些内核数据结构来管理共享内存。 下面就是共享内存的内核数据结构每当创建一个共享内存时操作系统都会建立一个对应的共享内存内核数据结构。所以当使用共享内存的方式来进行进程间通信时我们首先需要先建立一个共享内存。
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 */
};共享内存是如何实现进程间通信的呢我们知道每个进程都有一个自己的地址空间即虚拟内存然后通过页表将虚拟内存映射到物理内存中。如果我们修改两个进程的页表即重新建立映射将两个进程的一部分虚拟内存映射到同一片物理内存那么就实现了进程间通信的第一个条件让两个进程看到同一片空间。
2、共享内存使用
我们可以通过shmget函数来创建共享内存。 通过上面的介绍我们知道了进程间通过共享内存的方式通信其实就是两个进程同时映射到了共享内存这片空间中但是操作系统中可能创建了很多这样的共享内存那么两个想要通信的进程是怎么确定它们之间映射的是同一个共享内存的呢这就需要通过shmget函数的第一个参数key了这个key值是多少不重要只要两个想要通信的进程使用同样的算法形成一个相同的并且唯一的key值就可以然后两个进程就可以通过这一个唯一的key值来映射同一片共享内存key值会存在共享内存内核数据结构中。我们可以通过ftok函数来生成key值ftok函数的第一个参数为一个路径第二个参数可以写一个整数然后ftok内部就会通过某种算法根据这两个形参来生成一个唯一的key_t类型的值key_t就是一个有符号整型。需要注意的是ftok的第一个参数的路径需要保证用户具有访问权限才可以。 这样我们就明白了两个进程想要进行共享内存通信需要两个进程有一个相同的key这样两个进程才可以链接到同一个共享内存。
下面我们就来使用共享内存来实现两个进程间的通信。 我们先写好项目的makefile文件。 然后我们在comm.hpp头文件中定义两个进程通信时需要遵守的规范即两个进程通过comm.hpp文件中定义的宏调用ftok函数时可以生成同样的key值这样才能保证两个进程链接到同一个共享内存。 然后我们在server程序和client程序中调用ftok函数传入comm.hpp头文件中定义好的参数可以看到两个进程中生成了同样的key值。 接下来我们就可以创建共享内存了。我们先在comm.hpp文件中规定共享内存的大小。 然后我们在server进程中创建一个全新的共享内存我们测试时发现第一次执行server程序没有问题也成功的创建了共享内存但是第二次运行server程序时报出了文件已存在的错误这其实是因为第二次运行server程序创建共享内存时此时共享内存已经在底层存在了而我们为了保证每一次创建的都是全新的共享内存我们调用shmget函数时使用了IPC_CREAT | IPC_EXCL选项所以当发现底层已经有共享内存存在时才会报错。 其实当我们的进程运行结束时共享内存并没有随着进程的结束而删除因为创建一个共享内存时操作系统会为这个共享内存创建一个内核数据结构所以共享内存的生命周期随内核。这样我们就需要每次手动来删除共享内存了。 我们可以通过下面的命令来查看系统中的共享内存。
//查看共享内存
ipcs -s我们也可以通过ipcrm -m 共享内存的shmid命令来删除共享内存。然后我们就可以再次运行server程序了当我们再次运行了server程序后查看共享内存时可以看到又新建立了一个共享内存。
//删除共享内存
ipcrm -m 1通过上面的分析我们知道了需要每次在使用完共享内存后将共享内存删除我们在代码中可以通过shmctl系统调用来删除共享内存。shmctl系统调用不只是可以删除共享内存还可以通过传入的第二个参数的选项不同来得到共享内存的信息等。第三个参数为共享内存内核数据结构即可以通过这个参数来获取或设置共享内存的属性。我们只使用删除功能就好所以我们给第二个参数传入IPC_RMID第三个参数传入nullptr即可。 我们看到此时每一次运行server进程就不会报错了。因为共享内存创建后当server进程结束时就删除创建的共享内存了这样就不会出现共享内存已存在的错误了。并且我们看到每一次生成的共享内存的shmid都不同。 我们在上面使用ipcs命令查看共享内存时可以看到共享内存的key但是我们发现server进程打印的key和ipcs命令查看到的共享内存的key不同这是因为我们使用server进程打印的是key的十进制形式而ipcs命名显示的是key的十六进制。所以下面我们将server中使用十六进制打印key。当共享内存被创建成功后我们将程序sleep10s然后我们再写一个每秒打印共享内存的信息的脚本然后我们看到两个打印中的key的值相同了。 我们在查看共享内存的信息时发现还会有一个perms选项的信息perms其实为共享内存的权限权限为0说明任何进程都不能读写共享内存。 所以在创建共享内存时也需要指定权限。这样其它进程访问共享内存就有读写权限了。我们可以通过shmget函数的第三个参数来为创建的共享内存设置权限。然后我们就看到共享内存的权限为666了。 我们创建好了共享内存后那么进程之间该怎么通过共享内存来通信呢我们前面说到了当一个进程需要共享内存时就将共享内存映射到该进程的页表中即将指定的共享内存挂接到自己的进程空间这个过程叫做attach。当一个进程不需要共享内存时就将共享内存从页表的映射中去掉这个过程叫detach。 我们可以通过shmat系统调用将指定的共享内存挂接到自己的进程空间。第一个参数为共享内存的shmid第二个参数为要将共享内存挂接到进程的虚拟地址(不推荐手动设置直接设置为0让操作系统自行选择)第三个参数为挂接方式。shmat返回值成功时返回共享内存挂接在进程中的虚拟地址。失败时返回-1并且错误码被设置。类似于malloc函数malloc函数申请空间成功就会将空间的地址返回。失败就返回NULL。 我们看到当server进程挂接到创建的共享内存上时此时共享内存的nattch从0变为1。其实nattch显示的就是当前共享内存被进程挂载的数量。并且我们看到删除共享内存时即使有进程和当前的共享内存正在挂接依旧会删除共享内存。这其实和调用shmctl的选项有关IPC_RMID选项就规定了即便是有进程和当下的shm挂接依旧删除共享内存。 上面我们将共享内存挂接到了自己的地址空间之中那么当我们不再使用共享内存时我们就需要将指定的共享内存从自己的地址空间中去关联。我们可以使用shmdt系统调用来进行进程和共享内存之间的去关联shmdt的参数就是成功调用shmat函数返回的共享内存挂接在进程中的虚拟地址。即将shmat的返回值传入shmdt中shmdt就会将这个共享内存与当前进程进行去关联。如果去关联成功shmdt会返回0如果失败就会返回-1并且设置错误码。 然后我们就完成了(1)共享内存的创建(2)共享内存的挂接(3)共享内存的去关联(4)共享内存的删除。我们可以在2、3步骤之中写进程间通信的逻辑。即要使用共享内存来进行进程间通信需要先创建共享内存 - 然后进程挂载共享内存 - 然后向共享内存中读写数据进行通信 - 然后进程和共享内存去掉关联 - 然后关闭共享内存。 下面就演示了创建共享内存、挂接共享内存、共享内存去关联、删除共享内存这几个步骤后共享内存的nattch的变化。 下面我们在client进程中挂载server进程创建好的共享内存然后使用完了共享内存之后再将共享内存去关联client进程不需要删除共享内存因为client只管用创建和删除共享内存是server进程维护的。 我们看到下面的测试中当server和client都挂接共享内存后此时共享内存的nattch为2此时两个进程就可以通过共享内存进行通信了。 到这我们就完成了进程间通过共享内存通信的准备工作通过上面的实验我们了解了共享内存的建立中出现了两个唯一值分别为key和shmid那么这两个唯一值有什么区别呢 我们知道当建立一个共享内存时操作系统就会在内核空间中建立一份共享内存的内核数据结构而key就是存在共享内存的内核数据结构中的是在内核层面上用来标定共享内存唯一性的想要让多个进程看到同一份共享内存就通过key。当使用shmget创建共享内存时使用同样的key并且选项为IPC_CREAT | IPC_EXCL那么第二次创建就会失败因为此时操作系统的内核空间中已经有一份共享内存内核数据结构的key为这个值了所以操作系统不会再创建一份key相同的共享内存内核数据结构了。 shmid是在用户层标定共享内存的唯一性后期挂接、去关联、删除共享内存都使用shmid。
并且我们也知道了匿名管道的生命周期是随进程的即使用匿名管道通信的进程当进程结束时匿名管道也会随之删除。而命名管道和共享内存的生命周期是随内核的即创建命名管道相当于创建了一个文件操作系统在内核空间中会为该文件创建一份文件内核数据结构创建共享内存时操作系统也会在内核空间创建一份共享内存内核数据结构所以这两种通信方式只有将内核空间中的内核数据结构删除了才算是真的删除了。
下面我们来接着实现两个进程间使用共享内存通信。 我们知道每一个进程都有一个虚拟地址空间在虚拟地址空间中又分为堆区、栈区、静态区、代码段、共享区等等在堆区和栈区之间有一片空间被称为共享区该进程的共享内存、动态库等的虚拟地址空间就在这片区域。我们看到下面的两个进程的虚拟地址空间中的堆区、栈区、代码段区域等都通过页表映射在各自的物理内存中而两个进程的虚拟地址空间中的共享区通过页表映射在了同一片物理内存中。这其实就是共享内存的实现原理让两个进程的共享区映射到同一片物理内存这样两个进程就看到了同一片内存空间。 我们知道当一个进程想要获取一个栈区中的变量的值时需要知道这个变量的虚拟地址空间然后通过这个虚拟地址空间根据页表的映射去物理内存中拿取数据那么一个进程想要获取共享区的变量时也是拿到共享区这个变量的虚拟地址空间然后通过这个虚拟地址空间根据页表的映射去物理内存中拿取数据那么我们从共享内存中获取数据不就和程序从变量中获取数据一样了嘛这就是为什么共享内存是最快的进程间通信方式的原因因为从共享内存中拿取数据和从进程自己的栈区、堆区中拿取数据一样。而我们之前讲的pipe、fifo需要调用read和write等系统调用这是因为管道属于文件操作系统通过为每个文件建立内核数据结构来管理文件而这些内核数据结构是在1G的内核空间中的用户无权直接访问所以访问文件必须要调用系统接口通过操作系统来进行访问因为修改的是内核空间中的内核数据结构只能通过操作系统来修改。而共享内存是创建在堆栈之中的属性3G用户空间所以用户可以直接访问所以访问共享内存中的数据不需要进行系统调用。 我们使用malloc函数在堆区申请空间时malloc函数会返回申请的空间的虚拟地址那么我们使用shmat函数挂接共享内存时shmat函数会返回挂接的共享内存的虚拟地址。这两个函数就有些类似了都是返回进程的虚拟地址空间中的空闲的虚拟地址所以我们使用共享内存的空间时就和使用malloc申请的堆区的空间一样。 下面我们在client进程中将共享内存这片区域当作一个大字符数组然后我们向这个字符数组中存入数据。我们在server进程中将共享内存这片区域当作存的一个大字符串我们直接将这个空间中的字符串打印出来。 我们测试时发现当还没有执行client进程向共享内存中写数据时执行server进程后该进程就已经开始打印共享内存中的数据了并且打印的是空字符串这是因为共享内存在被创建出来后默认会将这片空间清为全0。 当执行了client进程后client进程就开始向共享内存中存储数据了然后server进程就可以读取到共享内存中client进程存储的数据了。 下面我们将client进程每5秒向共享内存中存储一次数据最后向共享内存中存储quit然后server每1秒向共享内存中读取数据当server进程读取到quit就不再读取共享内存中的数据。可以看到向共享内存中写入和读取数据没有调用任何系统调用接口。 我们看到当client向共享内存中写入数据后server进程马上就从共享内存中看到了client进程写入的数据。并且我们看到当client进程每5秒向共享内存中写入一次数据时server每1秒都从共享内存中读取数据并打印出来而对比我们之前的管道通信当管道的写端每5秒写一次数据时管道的读端也是每5秒读取一次数据这是因为管道通信提供了访问控制当管道中没有数据时读端就会进行阻塞式的等待直到管道中有数据了再进行读取当管道中写满数据时写端就会进行阻塞式的等待直到管道中的数据被清空。但是共享内存的通信方式我们看到并没有访问控制。 下面我们将client进程从键盘中读数据然后直接将从键盘读取到的数据存入共享内存中server进程还是从共享内存中读取数据并进行打印。 我们看到此时client输入数据server就马上读取到数据。并且我们是直接将从键盘读取到的数据存入到了共享内存当中然后server从共享内存中读取数据显示到显示器中。 看了上面的演示后我们再从数据拷贝的方面分析为什么共享内存的进程间通信方式是最快的。 下面是从键盘中获取数据存到buffer中然后将数据从buffer中写入到管道文件然后将数据从管道文件读取到buffer数组中然后从buffer数组再到显示器的过程。可以看到数据经过了4次拷贝。 下面是从键盘中读取数据直接存到共享内存中然后从共享内存中读取数据直接显示到显示器中可以看到只发生了两次数据拷贝。所以共享内存是进程通信之中最快的。 我们通过分析知道了共享内存为什么是最快的进程间通信方式但是共享内存没有访问控制而缺乏访问控制会带来并发问题。读取一方只需要读写入一方只需要写入读取和写入都不知道对方的存在那么当写入一方退出时读取一方也不知道还是在读取共享内存中的数据。我们下面使用管道文件来进行一定程度上的访问控制管道文件提供了访问控制那么我们可以创建一个管道文件在server进程从共享内存中读取数据之前我们先进行管道文件的读取操作我们知道如果管道文件中没有数据那么进程server就会在这里阻塞式的等待管道文件的写端写入数据这样我们就实现了阻塞server进程从共享内存中读取数据。然后我们在client进程向共享内存中写入数据之后再进行一次向管道文件中写数据的操作此时因为管道文件中有数据了那么server进程就会被唤醒而读取管道文件和共享内存中的数据了当管道文件中的数据被读取后server进程又会阻塞式的等待管道文件中写入数据。这样我们就通过管道文件的访问控制让共享内存也具有了访问控制功能。 下面我们先写一个Init类该类的构造函数中创建一个命名管道文件。该类的析构函数中将命名管道文件删除。然后我们在server程序的最开始创建一个全局的Init类实例化的对象init那么在server进程刚运行时就会执行这一句语句然后就会实例化一个Init类类型的对象init实例化init对象时会自动调用Init类的构造函数所以就创建了命名管道文件。然后当server进程退出时init对象销毁之前会自动调用Init类的析构函数将命名管道文件删除。 然后我们再定义这四个方法以便在client进程和server进程中执行管道文件相关的操作。OpenFIFO函数就是打开命名管道文件Wait其实就是从命名管道文件中读取数据如果命名管道文件中没有数据则这个函数就会阻塞式的等待命名管道文件中的数据更新。Signal函数就是向命名管道文件中写数据。CloseFifo函数就是关闭命名管道文件。Signal函数中向命名管道文件中写的数据是随意的因为我们不是通过命名管道文件来进行进程间通信只要Signal函数向命名管道文件中写一次数据即可同理Wait函数中从管道文件中读取的数据也是没有用的我们并不会用命名管道文件中的数据只要读取命名管道文件中的数据让进程不再阻塞在Wait函数中就行。 然后我们在server进程的开始实例化一个init对象并且在从共享内存中读取数据之前先调用Wait函数将server进程先进行一次命名管道文件的读操作如果此时命名管道文件中没有数据那么进程就会阻塞在这里等待命名管道文件中的数据更新而client进程中要想向命名管道文件中写入数据必须先从键盘中获取数据到共享内存后才会调用Signal函数进行向命名管道文件中写数据的操作。 下面是我们在client进程中的操作即在向共享内存中存入数据之后再调用Signal函数向命名管道文件中写入数据即将server进程唤醒进行一次从命名管道文件中读取数据和从共享内存中读取数据的操作。 我们看到当运行server进程和client进程后server进程不会再自己打印空字符串而是进行阻塞式的等待。 我们看到只有当client进程输入数据后server进程才会被唤醒一次然后将共享内存中的数据打印一次后又进入阻塞式等待的状态。当client进程输入quit后两个进程都成功的退出并且共享内存和命名管道文件都被成功的删除了。
三、消息队列
1、消息队列介绍
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。 每个数据块都被认为是有一个类型接收者进程接收的数据块可以有不同的类型值。 特性方面 IPC资源必须删除否则不会自动清除除非重启所以system V IPC资源的生命周期随内核。
2、消息队列操作
当查看消息队列时使用下面的命令。
//查看消息队列
ipcs -q当删除消息队列时使用下面的命令。
//删除消息队列
ipcrm -q msqid //消息队列的msqid当查看信号量时使用下面的命令。
//查看信号量
ipcs -s当删除信号量时使用下面的命令。
//删除信号量
ipcrm -q semid //信号量的semid文章链接