ARTICLE DETAIL

资讯详情

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

进程间通信:原理与实践

进程间通信:原理与实践 进程间通信进程间通信介绍进程间通信的目的进程是具有独立性的但优点具有局限性进程是独立的要是进程之间想协作呢一个数据可能需要多个进程依次处理才可以这样进程间的通信就比较困难就比如计算目录下文件数量我们使用ls -l | wc -l我们file查看可知ls和wc都是ELF可执行文件使用调用指令要把ls和wc都作为进程执行ls进程结果通过管道传输给wc进程完成任务数据传输一个进程需要把自己的数据传输给另一个进程进程间通信可能跨网络想手机程序之间数据的传输资源共享多个进程之间共享同样的资源通知事件一个进程需要向另一个或一组进程发送信息通知它(它们)发生了某种事件(如进程终止时要通知父进程)进程控制有些进程希望完全控制另一个进程的执行(如Debug进程)此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变什么是进程间通信怎么进程间通信进程间通信的本质是先让不同的进程先看到同一份资源[“内存”](然后才有通信的条件)这份资源绝对不能是任何一个提供的进程间关系最亲密的就是父子进程父子进程间父进程向子进程传输信息做不到的用户层所有的操作父子进程都会对对应的数据进行写时拷贝所有这个部分的内存资源全部都是由OS提供的两个陌生人认识需要中间人搭桥进程间通信同样如此而OS要为这些资源做一系列操作那就必须提供这方面的系统调用以此完成对这块资源的操作而系统调用是OS提供的接口这个通信接口就需要统一的设计这个设计的方案就会由不同的种类了进程间通信发展通信设计的方案也是需要标准因为需要很多人使用标准的设定尤为重要进程间通信的标准就有System V 和 POSIX 标准而标准设定一般由这个通信领域强的一方来定标准比如2G3G的标准是由美国的高通来订的而3G4G由诺基亚和华为来定到5G就是华为来定管道---System V进程间通信---POSIX进程间通信发展的历史AI来了解标准某个领域的标准的设定是这个领域中能力遥遥领先的组织或公司来定的因为公司实力在这个领域中以及是垄断的实力这个标准就不得不由这个公司来定其他公司想尽方法超越最终失败别人无法反驳只能承认这个标准环境问题Ubuntu C vscode进程间通信分类管道匿名管道pipe命名管道System V IPCSystem V 消息队列System V 共享内存System V 信息量POSIX IPC消息队列共享内存信息量互斥量条件变量读写锁匿名管道背景基于已有的技术直接进行通信——文件文件有自己的缓冲区文件操作就是将缓冲区内的数据和内容加载到内存上那如果把缓冲区的内容交给其他进程呢这样进程就可以通信了文件间共享这不就是指向同一个文件描述符吗所以匿名管道是基于文件来通信的什么是管道管道是Unix中最古老的进程间通信的形式我们把从一个进程连接到另一个进程的一个数据流成为一个管道原理进程打开文件根据路径打开并创建struct file将struct file的地址写入文件描述符表上层就根据文件描述符找到来strcut file根据其中的inode和dentry找到文件以及操作方法来访问底层硬件以及缓冲区而磁盘中存储文件的属性和数据块而匿名管道通常用来做父子通信创建子进程拷贝父进程PCB等内核数据结构包括文件描述符表struct file也会拷贝但是最终指向的内容都是统一部分磁盘部分是文件系统相关不会拷贝子进程并不是新文件所以依旧是原文件的内容因为拷贝了文件描述符之前打开的标准文件的地址都拷贝下来可以找到而struct file会额外拷贝一份这样两个进程就可以看到同一个缓冲区了进程间通信的本质是先让不同的进程先看到同一份资源然后才有通信的条件但是不能在缓冲区因为缓冲区会加载刷新数据回磁盘是文件系统的而真正的管道文件是内存级别的文件不会刷新到磁盘的与磁盘没有关系管道文件是内存级别的通过OS的系统调用来范围以读写方式打开文件在文件描述符中占两个元素而子进程拷贝文件描述符表以此子进程获取了父进程指向的管道文件的的地址而通讯的方向通过文件描述符表是修改来实现比如父-子单向通信关闭父进程对管道文件的读权限关闭子进程对管道文件的写权限这个就管道管道是被OS单独设计的需要配上单独的系统调用pipe而管道文件是内存级别文件不需要提供路径没有路径也就是匿名文件的即匿名管道因为子进程继承文件描述符表两个进程都可以打开同一个管道文件demo代码测试接口创建管道可以看到管道的读写fd为3和4创建子进程创建通信信道此时还并没有通信我们在让不同的进程看到同一份资源在做前期的准备工作正是因为进程的独立性所以做通信比较麻烦接下来就做进程间通信注意父进程读数据要在waitpid之前因为此时子进程也正在活跃中若waitpid之后读子进程都结束了还读什么//注意C类函数方法字符串操作结尾默认会加\0而系统调用不关心这个要手动加5种特性 4种情况5种特性1、匿名管道只能用来进行具有血缘关系的进程进行进程间通信2、管道文件自带同步机制两个进程同时向显示器文件写入这个操作双方进程没有影响各写各的而在管道文件中子进程写入数据到管道文件父进程才可以读取管道文件的信息如果管道文件没有输入新的数据父进程读取数据的一方就要阻塞等待这就是同步机制写数据的一方若是间隔3秒输出一次数据那么读数据一方也要等3s读取一次数据3、管道是面向字节流的面对写的快读的慢的情况而读取的速度取决于怎么读(取决于缓冲区的大小)这种情况读数据的快慢取决于缓冲区的大小文件操作学习打开文件我们叫打开文件流这些输入输出流这些都是字节流C语言学习时通讯录项目就有用到类似的字节流用法我们将一个链表的数据以结点数量写入文件而读取时根据链表数量就可以全部读出缓冲区的设计刚刚好可以获取目标数据所以字节流操作也可以按行写入行读取4、管道是单向通信的是先有文件间做单向的通信以此用管道来为这个理论命名任何一个时刻一个发一个收 --- 半双工任何一个时候可以同时收发 --- 全双工管道工作模式属于半双工的一种特殊情况人正常交流就是半双工模式而人开始吵架的时候就是全双工模式5、管道文件的生命周期是随进程的4种情况1、写慢读快写慢所以读也慢甚至写休眠管道文件为空读也要阻塞等待(进程阻塞)2、写快读慢写快容易把管道文件写满写满写就阻塞等待(阻塞等待读端读取数据)3、写关继续读read 就会读到返回值为0表示文件结尾写关读端就直接把管道文件读到结尾0处4、读关继续写写端写入无意义传出的数据并不会响应OS不会做没有意义的事情本质是浪费资源OS会发送异常信号13)SIGPIPE杀掉写端进程管道的大小写端一直写读端一直不读看看写多少会停下可以知道有65536字节除于1024等于64KB则管道的大小为64KB管道的写入原子性在对管道文件写入的时候单词写入字节数小于PIPE_BUF(4KB)这个写入过程是原子的原子性表示做一件事情只有做或不做的两态写端写入hello world管道文件中写入了hello读端不能读取这个hello只有在管道文件中hello world都写入了读端才可以读取基于匿名管道 ---进程池父进程创建多个管道都是父进程写子进程们读这里就用5个子进程父进程就可以通过管道向任意一个子进程进行通信如果父进程不写数据那么子进程们就都阻塞在read处等待父进程写消息此时父进程不就相当于暂停和唤醒子进程通过规定父进程只输出int code整型数据(这样不会有之前面向字节流读取不完整的问题了)这个code可以认为是任务码(code为0是让子进程进行打印消息code为1子进程完成网络通信...)通过规定任务码发送任务码以此唤醒子进程并根据任务码安排子进程完成对于任务这样提前创建一批子进程的技术叫做进程池其实池化技术很早就遇见过vector动态扩容通过扩容1.5倍容量提前安排空间之后可以直接使用可能会牺牲一些空间资源但是会提高时间效率池化技术可以减少创建某种资源的成本提高访问的效率就是现有子进程等有任务了通过父进程唤醒并安排任务给子进程先有了子进程在调用不需要在调用的时候在处理调用子进程接下来是代码实践实现一个进程池cpp、cc、cxx都是c的文件后缀之后要使用的代码文件个数会变多所以采用hpp文件即h和cpp混用合用hpp可以直接将函数写道hpp文件不需要在h文件声明cpp文件定义这样方便就跟平常定义函数一样.hpp文件内部定义有两个一个是上面正在用的另一个是被注释的我们在设计进程池的时候管道要处理一下父进程有很多个管道我们要管理这管道管道要命名并且要知道父进程向那个进程写入的即对其中这弓信道进行管理以上就成功建立了进程池需要的信道了而对子进程下发任务不能一直向一个信道派发任务这样进程池创建的意义不大信道忙的忙死闲的都在read阻塞会造成负载不均衡我们要让每个子进程较为均等的 任务量做到负载均衡负载均衡的方法1、轮询2、随机3、channel添加负载指标在channel中设置int变量记录channel完成的任务通过channelmanager对vectorchannel做指标最小值查找将任务给找到的那个channel使用轮询方式实现了进程池代码的复刻以上是进程池的创建思路具体细节看ProcessPool.hpp以上代码有一个隐藏的问题如果写端关闭和等待一起做会出现什么问题父进程创建管道创建子进程对端口做操作创建第二个管道和子进程就会发现问题创建第二个子进程的时候子进程把指向第一个管道的地址也复制了这样管道写端被多个指向我们管道退出一个管道文件也没有关闭引用计数没有为0子进程会在read阻塞解决方法1解决方法2真的让父进程一人指向所有管道的写端子进程可以拿到父进程在这之前的写端fd那把这些写端fd关闭即关闭当前子进程的哥进程写端即可而channelmanager中又管理着当前子进程的所有哥进程的管道而channel中有维护wfd通过这个wfd来删除即可命名管道匿名管道只能用来进行具有血缘关系的进程进行进程间通信常用于父子如果有两个进程不相关该如何进行通信呢进程A打开一个文件/a/b/c.txt进程B打开一个文件/a/b/c.txt内核中操作系统会不会把这个/a/b/c.txt文件(inode/文件内容等)在内存中加载两次不会因为没必要两个进程打开同一个文件打开一个文件通过inode值在磁盘找到对应文件inode在内存创建struct file以及inode和文件级缓冲区当第二个进程要打开这个文件OS要为这个进程创建这个文件的struct file因为不同进程读写文件会有不同的读写位置进程与文件的关联也需要各自一份而真正的文件内容是inode和文件内核缓冲区OS发现访问的是同一个文件根据inode值知道OS看到要打开的文件以及被打开被加载内存上了所以OS没必要把这个文件内容再加载一份了这也是为什么父子进程可以向同一个显示器文件进行打印这不就是让不同的进程看到了同一份资源通过打开同一个路径下的同一个文件文件有路径就代表着有名字因为路径具有唯一性所以进程间可以看到同一份资源所以是命名管道要做通信命名管道就不能以普通文件打开因为会刷新数据的到磁盘所以Linux内核会要使用特殊的文件管道文件管道文件是只会被打开不需要刷新做内存级的操作创建命名管道指令mkfifo XXX创建后ll查看看到文件类型为p管道文件做一些操作mkfifo fifi echo hello fifo fifo cat fifo上面操作是通过进程间通信实现的echo命令指向成为进程向命名管道文件fifo写入之后cat命令指向成为进程向命名管道文件fifo读取删除命名管道除了rm删除还有unlink XXX方式删除命名管道实现的代码实际思路就是让其中的一个进程创建命名管道之后就是文件操作的内容了进程之间确定好读写端以此实现通信说到进程创建管道文件那就提到对应的系统调用这个系统调用就可以在进程内创建命名管道其中的mode和open的mode使用一样对创建文件权限进行设置server.cppclient.cpp公共文件comm.hpp实现不相关进程间的通信实现了客户端向服务端的输出只有读取端打开管道文件会阻塞在这里命名管道文件检测有读写端的两个进程才会打开成功缺少其中一个都要阻塞等待知道对应的读写端的进程打开管道文件才会成功此时客户端退出服务端是无响应的状态客户端关闭其打开的写端文件会自动退出写端退出读端read返回为0而我们此处对读端读取为0没有做处理所以一直在死循环小总结匿名管道和命名管道基本操作与内容相似最大的区别就是命名管道可以和毫无相关的进程进行通信命名管道通过可以实现进程池无联系的进程池小技巧总结在打开文件失败的时候要perror(XXX)exit(1)这个操作会多次使用不如直接宏定义#define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXOT_FALIURE) \ } while(0)上面是固定的写法本来是这样的#define ERR_EXIT(m) do { perror(m); exit(EXOT_FALIURE) } while(0)\目的是续航这样就可以直接ERR_EXIT(mkfifo);System V共享内存System V 是一种标准Linux内核支持了这种标准专门设计了一个IPC通信模块通信的接口设计原理和接口有相似性IPC本质让不同进程先看到同一份资源(之前的学习一个是继承一个是打开同一个文件)共享内存原理共享内存的原理和动态库的映射有关进程地址空间中在共享区内开辟一块区域与内存中的一块区域进程映射这样就可以将进程数据传输到内存而这块内存区域也可以和其他的进程的共享区中的区域进行映射以此可以获取内存中这个区域的数据这样就内存区域其实就是共享内存上面说的所有工作都是OS自己完成的我们可以通过系统调用完成上面的工作现在A、B进程可以通信了已经让A、B进程看到了同一份数据了通过不同的映射让不同的进程看到了同一份数据了关闭通信 进程A、B把对应地址空间和页表映射都释放共享内存空间的映射没了共享空间就自然释放了退出通信的方法取消关联关系OS释放内存可能同时存在由多组进程都在使用不同的共享内存进行通信---- OS内多个共享内存同时存在OS就需要对这个共享内存进行管理先描述再组织共享内存一定要有对应的描述共享内存的内核结构体对象 物理内存所以进程和共享内存的关系 ---- 内核数据结构之间的关系每创建一个共享内存就需要创建一个共享内存结构体对象将对象通过指针连接形成链表这样就把OS对共享内存的管理转化为对链表的增删查改的操作而上面讲到的退出通信与共享内存取消关联关系通过再结构体对象中设置引用计数来决定是否示释放内存共享内存最基本的原理就搞定了使用共享内存的接口System V通信的特性创建创建共享内存的接口shmgetshmget为系统调用 会在内存创建一个共享内存size为创建共享内存的大小shmflg为int类型的标记位以上是shmflg最常见的标记位这里标记位的用法和文件操作类似可以用|一起使用还有一个标记位IPC_PRIVATE不常用IPC_CREAT:创建共享内存如果目标共享内存不存在就创建否则打开这个已经存在的共享内存并返回和之前命名管道类似IPC_CREAT | IPC_EXCL(IPC_EXCL单独使用无意义!) :如果需要创建的shm不存在就创建它如果已经存在shmget就会出错返回(出错返回的意义只要shmget成功返回一定是一个全新的共享内存)所以IPC_CREAT一般是用来获取的IPC_CREAT | IPC_EXCL一般是用来创建的如何评估共享内存是否存在如何确定两个不同的进程拿到的就是同一个共享内存呢通过参数key来实现不同进程shm来进行通信标识共享内存的唯一性key用来区分不是内核直接形成的而是在用户层构建并传入给OS的为什么现在遇到的问题是进程A创建一个共享内存获取到共享内存的ID但是进程B要如何获取进程A创建的共享内存以此达到通信呢可以让A、B进程约定一个key通过key创建共享内存之后的进程通过key区共享内存中不对找到key值相等的共享内存以此获取对应的共享内存达成通信如何实现这个及时呢其实之前就已经做过类似的了命名管道demo部分两个进程要到开同一路径下的同一个fifo管道我们在comm.hpp中就约定了统一路径下的文件就可以看到同一个key我们在用户层创建key不建议随便设置希望使用算法来设置降低冲突的风险引入一个算法的接口ftok推荐使用这个函数接口来形成唯一的key值ftok并不是系统调用而是纯用户级别的算法ftok根据传入的pathname路径和id拼接形成key需要通信的两个进程就可以约定这两个数据这样就可以形成相同的key值了虽然还有冲突的可能但是很小冲突了就修改这两个参数shmget创建成功会返回共享内存合法的标识符这个标示符是要给内核使用的进程就通过key来获取标示符这有点像文件操作我们通过fd找到文件但是内核实际上还是使用struct file来访问真实的文件共享内存的demo实现创建共享文件的代码查看创建的共享内存使用指令ipcs -m删除共享内存的指令ipcrm -m XXX(shmid)问题创建了一次更新内存再次运行可执行程序就会报错:文件存在主要是文件第一次创建共享内存已经创建了之后调用程序还是会用同样的key值去创建共享内存肯定会报错进程结束如果没有进行删除共享内存共享内存资源会一直存在 --- 共享内存的资源生命周期随内核--- 如果没有显示的删除即使进程退出了IPC资源依旧被占用(像文件操作这样的被打开文件的生命周期就是随进程进程结束打开文件也自动关闭)删除方法1.ipcs/ipcrm2.代码删除为什么通过指令删除不能通过key来删除只能用shmid来删除删除控制共享内存在用户层不能使用keykey只未来只给内核进行区分唯一性用户层需要是只用shmid进行管理共享内存指令本身是运行在用户空间的指令本身也是系统调用的封装所以在bash命令行只能用shmid来操作共享空间删除共享内存的接口shmctlshmget可以对共享内存进行统一的管理第一个参数为需要管理的共享内存的shmid这也表明这个接口是面向用户的第二个参数是使用内置的命令来实现管理操作这里有一系列操作具体查手册第三个参数和共享内存的属性有关现在没了解其属性先不了解实现了创建共享内存的简单周期接下来要考虑的是如何映射如何让当前进程和共享内存关联起来认识接口shmat将共享内存挂接到进程的地址空间中shmatatattach关联、挂接shmat接口会调用系统调用的进程把进程地址空间的堆栈间的映射区和共享内存建立映射返回值是映射成功后的起始虚拟地址线程再了解页表细节存储为内存块所以给进程使用的连续的空间映射的地址也是连续的地址根据共享内存的大小以及获取的起始虚拟地址以线性遍历的方式就可以访问到全部的数据映射成功后就可以访问共享内存的数据了shmid需要确定要和那个共享内存进行映射shmaddr虚拟地址固定地址进行挂接像动态库连接可能就需要这样的操作但是这个操作往往是对自定义库和进程关联时使用我们往往使用null即可不需要处理shmflg是标记位对共享文件的权限进行设置我们往往使用0即可即默认的设置默认设置是读写权限之后再设置即可其实动态库的加载也是这样只是使用的不是通信用的共享内存通过malloc进一步了解shmatmalloc申请虚拟地址空间填充页表的虚拟地址等到程序执行的时候再申请物理空间然后进行映射malloc返回也是起始地址连续的空间和shmat类似只是malloc是程序和虚拟地址之间而shmat是虚拟地址和内存之间shmat映射的数据的使用与malloc申请空间的使用没有任何区别比特实验的时候映射失败错误表示权限不对再加上之前文件已存在我们可以知道OS把共享内存也当作一个文件毕竟一切皆文件我们关联共享内存但是权限为0那直接不关联我们在创建的时候需要处理文件权限shmget兼容对文件权限的设置这里的权限问题与文件操作一样直接设置文件权限_shmid shmget(k,_size,IPC_CREAT | IPC_EXCL | 0666);这里我一开始把文件权限记混成了文件访问标准位了权限我们查看共享内存属性信息可以看到属性被设置设置后才可以继续进行关联并且nattach也从0到1因为有与进程进行关联客户端同样可以关联共享内存的关联计数会变成2目前做的工作都是在建立进程间通信的信道还没有进行通信也就是做让不同进程看到同一份资源的工作以上就是优化后的shm类而使用共享内存进行通信其实就是通过起始地址进行写入或读取可以发现使用共享内存进行通信读写并没有使用系统调用共享内存映射成功其映射虚拟地址在虚拟地址空间的堆栈之间即共享内存映射的共享区是属于用户空间的可以让用户直接使用进程向共享内存写入另一个进程就可以立马从共享内存读取到所以共享内存是进程间通信中速度最快的方式1、映射之后读写直接被对方看到2、不需要进行系统调用获取或写入内容如果其中一个进程是磁盘呢磁盘数据加载到共享内存中进程与共享内存进行映射此时进程虚拟空间就可以直接使用共享空间的数据对程序中数据代码做地址重定向以用户地址方式访问库方法动态库加载到进程的方法就是这个原理只是不是用共享内存这个方法而是用mmap这个方法共享内存也会有自己的缺点没有绝对的优点只有相对的优点通信双方没有所谓的同步机制server会提前读取如果client中的数据是废弃的无用数据server就读取了无用数据而管道就不一样写端没有写读端就要阻塞等待这样就会有一些情况要求是要么不读取要么就要读取完整的数据如hello world使用共享内存就有可能只读取了hello这样就会出错上面的现象叫做数据不一致共享内存的缺点是没有保护机制对共享内存中数据的保护 如何保护呢此时有可以使用到之前学习的命名管道命名管道不是用来传数据而是用来传输消息读端不允许直接读取共享内存读端要读取命名管道通过写端向命名管道写入信息唤醒读端读端才可以去读取共享内存的数据以此可以保护数据保证数据一致性共享内存小知识点共享内存大小问题在内核中共享内存在创建的时候其大小必须是4KB4096的整数倍我们在用户层开辟4097的共享内存实际上在OS内核创建40962的空间进行向上4KB取整我们查出来大小是4097这个我们规定的4097的内存OS创建了40962 的内存但是我们只能用其中的4097的内存我们申请的系统与用户是又区别的共享内存去关联共享内存正确的删除方式应该是先去关联再删除就像先创建再关联这个共享内存去关联接口shmdt去掉共享内存的引用计数再删除获取共享内存的属性使用shmctl接口其中的IPC_STAT标志位通过shmid_ds结构体将共享内存的属性获取这个结构体中有共享内存的大小最近被挂起的时间被去关联的时间 被改变的时间创建者那个进程最后关联 进程关联数这些属性都在其中其中有一个成员是ipc_perm这个结构体其中存放着mode文件权限还有uid用户名以及key值我们shmget(key..)就会被设置到共享内存的描述结构体中一般获取到key到shmid_ds找到ipc_perm,比对key以此知道是访问的同样的共享内存System V消息队列初识消息队列IPC看到同一份资源(方式上可以是文件也可以是内存)也可以维护成一个队列在OS内核中进程可以将传输的数据以队列结点的方式将队列结点插入在OS中的队列并且也可以实现双向传输但是进程无法识别队列结点的归属进程向队列放数据不仅仅是放数据这么简单还需要对数据进行类型标签设置本质是int类型type以后在结点内通过判断type变量就可以识别结点是属于哪个进程了进程获取信息直接识别对方进程的type值的结点数据以此通过一个队列实现数据交换的目的结论1消息队列提供了一种一个进程给另一个进程发送有类型数据块的方式消息队列在内核中同样会有多个OS需要管理消息队列依旧先描述再组织和共享内存一样要有自己的结构体消息队列结构体为struct msgid_ds设置结构体node内部属性进行设置这样实际上就创建了一个数据结构一个带结点的消息队列如何保证进程间看到的是同一个消息队列与共享内存同理进程上层同样约定一个key创建者设置key到消息队列结构体表里消息队列的调度接口创建消息队列msgget其中擦参数同样有key和flg标志位key值设置和以往同理ftok提供pathname和projid即可设置而flg标志位也与上面同理有IPC_CREAT和IPC_EXCL其返回值为int返回的是消息队列的标识符删除消息队列通过使用msgctl()使用对应的IPC_RMIDcmd命令来删除而msqid_ds消息队列也有自己的属性通过这个结构体对象获取消息队列的属性其中有消息队列一系列的属性获取消息队列属性:使用msgctl的IPC_STAT参数来获取这也是System V标准同一个标准下的知识有一定的泛用性在消息队列收发数据就和共享内存的操作不一样了收发的方法如上消息队列发送消息使用msgsnd其中参数msqid为创建或打开返回的数据而msgp是发送的数据块msgsz是发送数据的大小写数据要在文件公共区域用户设置这样的结构体结构体内部数据需要重定义但是结构体名字一定要一样这个结构体一般都设置在comm.hpp这类共享文件通过设置类型在传输的数据类型中设置这个类型之后进程就可以对指定的类型进行查找传输数据结点类型填自己的类型接收操作和上面类似唯独要注意的是需要传入要读取的队列结点类型消息队列生命周期也是随内核不删除一直存在内核查找消息队列的方法ipcs这样直接查是查找System V三种通信资源查找出来查找消息队列是ipcs -q删除是ipcrm -q msqid小细节在传输数据的时候msgsz的大小是struct msgbuf中插入mtext[1]的大小并不需要整个结构体的大小直接sizeof结构体也不会报错但要严谨一些System V信号量 (铺垫)并发编程概念铺垫做铺垫为后续多线程编程打基础共享内存看到同一份资源这是通信提供的前提如果没有保护机制数据不一致解决方案信号量一种具体的方案多个执行流(进程)能看到的同一份公共资源共享资源管道的文件共享内存和消息队列的内存都是共享资源被保护起来的共享资源叫做临界资源公共管道保护的共享内存此时共享内存就是共享资源在进程中涉及到互斥资源的程序段叫临界区通信数据不一致的原因1、是有未保护共享资源2、各自的代码访问了这个没有被保护的共享资源临界区是访问资源对应的代码非临界区一个程序中只有一小部分代码是访问共享资源其余代码并没有访问共享资源与数据不一致问题无关这些区域就成为非临界区数据不一致需要保护机制这个保护机制是保护临界区代码也就变相的保护临界资源这里保护也可以是约束电影院座位常常说需要被保护电影院座位处被留下垃圾是座位的错吗不是是人的问题不可能对座位进行调整而是要约束人的行为这里座位就是公共资源人就是程序如何保护(互斥与同步)任何时刻只允许一个执法流访问资源叫做互斥进程在运行到临界区之前要先竞争加锁这个锁竞争到锁的进程就在加锁到解锁之间一个进程使用临界区进程用完临界区解锁之后其他进程才可以继续竞争锁以此访问临界区这样就可以保证只有一个进程可以单独访问这个共享资源这个方式称为互斥使用ATM机就是互斥这个原理多个执行流访问临界资源的时候具有一定的顺序性叫做同步用上面的例子就是现在的人都是好人不需要将ATM机围起来人们使用ATM会自觉的排队依次使用上节课通过命名管道来控制进程间用共享内存通信其实就是同步做完一个操作通过管道通知进程依次类推系统中某些资源一次只允许一个进程使用称这样的资源未临界资源或互斥资源在进程中涉及互斥资源的程序段叫做临界区代码 访问临界资源的代码(临界区)不访问临界资源的代码(非临界区)所谓的对共享资源进行保护本质是对访问共享资源的代码进行保护原子性要么做要么不做人去ATM机取钱对于外面的人来说只关心有人和没人这两个状态里面人的操作不关系里面人的操作是原子的父母对你学习的要求学就学好不学就算了父母对学习的过程是原子的A、B进程访问临界区都需要申请锁申请锁这个操作也是一个代码而锁也会被两个进程同时看到毕竟两个进程要竞争同一个锁呀所以锁也是共享的锁保护共享资源那谁来保护锁呢需要想办法把锁设计为申请锁的时候必须是原子的锁对于进程来说要么不做要做就是做完了谁申请到了就是谁的其他人不能影响正是因为共享内存并没有原子性所以需要被保护共享内存有写前写中写后三种状态并不能达成原子性有在写中被影响的可能如果写入过程只有写前和写后那么对数据的写入肯定没有影响写前读取不到数据写后读到完整数据但是访问临界区的代码很难保证对于资源写入是原子的所以需要锁来保护所以锁必须是原子的临界区、临界资源、原子性、同步、互斥这个5个概念面对并发编程是必聊的问题信号量信号量是什么在OS中也叫信号灯本质是一个计数器用来表明临界资源中资源的数量多少理解信号量例子理解电影院售票最怕的两个问题1、票卖多了2、票号重读电影院买票本质是对资源的预定机制想要访问临界资源就得先买票电影院中放映厅就是一个临界资源这个临界资源内有多个子资源也就是放映厅内的座位此时就认为放映室是共享内存进程对共享内存进行分批分块使用比如这里就看16个小块来访问这样可以多进程使用共享内存保证效率又保证多进程通信的数据一致性问题对共享内存的操作把共享内存按照不同的区域部分使用1、不要访问同一个位置2、不要放入过多的进程进来这样操作并发访问就不会出问题此时就可以实现多进程的并发访问所以信号量其实就是计数器描述的是临界资源中资源数量的多少所有进程访问临界资源中的一小块就必须先申请信号量进程访问共享资源就要申请信号量信号量要sem--if(sem 0)通过了才可以访问共享内存未通过说明现在共享内存全都被占用需要阻塞等待访问完了退出共享内存就要将信号量sem!进程访问资源前先申请信号量这个操作的本质是对资源的预定机制(资源一旦预定给你了可以随时去访问资源没人和你抢)细节1、信号量本身就是临界资源信号量如何被保护申请-- 操作退出操作 --操作不是原子的需要原子性的操作这个操作是---操作是P操作操作是V操作PV操作完成对资源的预定机制2、信号量只有1或者0两态的信号量叫做二元信号量电影院有超级VIP放映厅这个放映厅只有一个座位也就是只有一个进程可以访问这个临界资源这个临界资源的信号量值为1这种临界资源的信号量叫二元信号量也就是互斥复盘共享资源使用的问题访问临界资源只有两种场景第一种是把资源整体使用的使用二元信号量第二种是把资源分区块使用使用多元信号量信号量的伪代码信号量不能是一个整数整数不是原子的这个话题要在多线程讲解整数无法做到数据交换的进程间就不能看到同一个信号量struct sem { //锁 //int count; //task_struct * waitqueue; }进程创建信号量都上锁对计数器--若大于零就可以操作临界资源之后解锁离开若小于等于零就将进程链入信号量的阻塞队列中阻塞挂起直到计数器大于零。信号量和通信有什么关系1、先访问信号量P每个进程都得先看到同一个信号量System V解决的就是这个问题2、不是传递数据才是通信IPC通知、同步互斥也算同步互斥本质还是属于信息的传递通信的本质不是数据的传递而是特殊信息的传递要么传递信息要么传递事件进程对信号量计数器或--难道不是在访问公共资源在做通信吗所以信号量属于通信范畴信号量并不是直接传递数据信号量是做临界资源保护的方式本质上还是传递控制信息熟悉 信号量接口 和 系统调用创建信号量: semgetkey不需要解释semflg常用的依旧是以下两个功能和之前一样返回值为int整数返回的是信号量集标示符semid创建信号量可以创建一个信号量也可以创建多个信号量这多个信号量就是信号量集信号量支持一次创建一组信号量查看与删除信号量ipcs -sipcrm -s semid删除信号量接口: semctl()在cmd参数使用IPC_RMID操作信号量:semopstruct sembufstruct sembuf结构体里面存储着三个属性其中sem_num是记录要操作的sem编号而sem_op记录要操作什么一般是1或-1会自动调用VP操作信号量提供一次对多个信号量做op操作通过传递一个struct sembuf结构体数组到semop中通过遍历批量化的对信号量操作细节信号量的初始值是多少sem在创建的时候不能设置初始值要在创建之后使用semctl才能初始化信号量初始值初始化的方法就是semctl的semnum参数使用semctl做删除操作的时候semnum这个参数是无效的而要对信号量做初始化就需要使用SETVAL选项使用semctl(_id,0,SETVAL,...),这个操作就是要对信号量集的第0个信号量做值初始化其中参数的...是可变参数要传入一个结构体这个结构体是一个联合体其中的val就是设置信号量的初始值OS需要对信号量进行管理先描述再组织信号量的属性同样可以通过semctl的IPC_STAT标志位来查看用户级内核属性也是使用key作为唯一性准则所以System V的通信方法都是用key来保证唯一性的每一个方法都提供了XXXid_ds的结构都用key做标示符各个方法的使用要注意key值冲突共享内存、消息队列、信号量都是用key区分唯一也就是OS把共享内存、消息队列、信号量当成同一种资源了这样为什么是System V IPC了IPC资源是如何组织的呢OS中存在全局的ipc_ids内部包含了一个柔性数组这个数组的类型是kern_ipc_perm * 通过数组元素来指向其他通信方法的kern_ipc_perm结构因为共享内存、消息队列、信号量结构的第一个元素也是kern_ipc_perm所以可以使用数组把所有ipc资源组织在一起对应的ipc通信方法的结构体对象地址强转为kern_ipc_perm类型结构体指针添加进ipc_ids内的柔性数组之后OS就可以通过柔性数组访问到对应的权限这之外的权限就要强转为对应类型的通信方式结构体才行这不就是C语言版本的多态吗对于资源的判断这个不用担心三个通信方式的接口都是独立的OS调用接口访问id_aryOS可以通过接口来确认类型查内核代码max_id最大id值struct mutex锁ipc_id_ary *上面的柔性数组entries的赋值kmalloc(sizeof(ipc_id_ary) n * sizeof(struct kern_ipc_perm *))扩展内存而msg_queue读一个元素就是kern_ipc_perm所有柔性数组指向的不是msg_queue而是第一个元素而这两者的地址在数组层面是相等的但是范围是不一样的这个元素之后的数据就是消息队列自己个性化的东西信号量结构也是同理sem_nsems是信号量数量而sem_base是对信号量进行初始化毕竟是信号量集一个就直接malloc一个多个就malloc(n * )创建共享内存第一个元素同理内部有nattach与进程关联的数量shm_segsz共享内存的大小shm_file是共享内存的内存提供方共享内存是以文件为载体向内存开辟空间的共享内存shmget是创建了一个文件对象 而shmat是做内存与进程的映射也就是在虚拟内存上创建一块vm_area_struct虚拟地址空间通过文件信息在页表上映射内存中创建共享内存实际上是在柔性数组中开辟特定下标对应的内容返回的shmid、msgid、semid都是数组下标数组下标到最大值后会回绕
返回列表