美文网首页基础知识转载部分
进程间通信与线程间通信

进程间通信与线程间通信

作者: 杰伦哎呦哎呦 | 来源:发表于2018-07-05 19:49 被阅读96次

    操作系统的主要任务是管理计算机的软件、硬件资源。现代操作系统的主要特点是多用户和多任务,也就是程序的并行执行,windows如此linux也是如此。所以操作系统就借助于进程来管理计算机的软、硬件资源,支持多任务的并行执行。要并行执行就需要多进程、多线程。因此多进程和多线程间为了完成一定的任务,就需要进行一定的通信。而线程间通信又和进程间的通信不同。由于进程的数据空间相对独立而线程是共享数据空间的,彼此通信机制也很不同。

    一、进程间通信

    进程间的通信,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。

    进程的通信机制主要有:管道、有名管道、消息队列、信号量、共享映射区、共享内存、信号、套接字。

    1.信号

    信号是在软件层次上对中断机制的一种模拟(是由系统内核 发出,由于错误内存冲突等原因引起产生的),在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX(一个针对操作系统(准确地说是针对类Unix操作系统)的标准化协议)实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源。

    信号分为可靠信号和不可靠信号,实时信号和非实时信号。

    进程有三种方式响应信号: 

    忽略信号

    捕捉信号

    执行默认操作

    2.信号量(在进程中是有名信号量)

    信号量也可以说是一个计数器,常用来处理进程或线程同步的问题,特别是对临界资源的访问同步问题;信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识可以用来控制多个进程对共享资源的访问,不是用于交换大批数据,而用于多线程之间的同步,常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源。 

    信号量分为有名与无名

              信号量在进程是以有名信号量进行通信的,在线程是以无名信号进行通信的,因为线程linux还没有实现进程间的通信,所以在sem_init的第二个参数要为0,而且在多线程间的同步是可以通过有名信号量也可通过无名信号,但是一般情况线程的同步是无名信号量,无名信号量使用简单,而且sem_t存储在进程空间中,有名信号量必须LINUX内核管理,由内核结构struct ipc_ids 存储,是随内核持续的,系统关闭,信号量则删除,当然也可以显示删除,通过系统调用删除

    (临界资源:为某一时刻只能由一个进程操作的资源)当信号量的值大于或等于0时,表示可以供并发进程访问的临界资源数,当小于0时,表示正在等待使用临界资源的进程数。更重要的是,信号量的值仅能由PV操作来改变。(每个进程中访问临界资源的那段程序称为临界区临界资源是一次仅允许一个进程使用的共享资源))

    PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思。PV操作(原语也叫原子操作Atomic

    Operation,是不可中断的过程)是典型的同步机制之一,用一个信号量与一个消息联系起来,当信号量的值为0时,表示期望的消息尚未产生;当信号量的值非0时,表示期望的消息已经存在。用P V操作实现进程同步时,调用P操作测试消息是否到达,调用V操作发送消息)

    3.消息队列

         消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符标识,于管道不同的是,消息队列存放在内核中,只有在内核重启时才能删除一个消息队列, 同样消息队列的大小也是受限制的。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为是一个管道,接收进程可以独立地接收含有不同管道的数据结构。

          消息队列与命名管道一样,每个数据块都有一个最大长度的限制。我们可以将每个数据块当作是一种消息类型,发送和接收的内容就是这个类型对应的消息,每个类型相当于一个独立的管道,相互之间互不影响

    消息队列进行通信的一些操作:

    1)、使用msgget()函数创建打开队列;

    2)、使用msgrcv()函数从队列中读数据;

    3)、使用msgsnd()函数写数据到队列中;

    4)、使用msgctl()函数控制消息队列。

    4. 共享内存映射区

    通过mmap函数将磁盘文件的数据映射到内存,用户修改内存就能修改磁盘文件(数据实际存储在磁盘上)。

    (1)有血缘关系的进程间通信:因为子进程和父进程永远共享的有两个部分,第一是文件描述符,第二就是内存映射区。所以只需要父进程先用open()函数打开一个文件得到fd,再利用mmap()函数创建一个内存映射区,然后fork()一个子进程出来,子进程就可以直接通过mmap()返回的内存映射区首地址对共享部分进行读写了,相当于就是有血缘关系的进程间通信机制。

    (2)没有血缘关系的进程间通信:还是让一个进程先通过open()函数打开一个文件得到fd,再利用mmap()创建一个内存映射区并得到首地址ptr,然后另外一个无血缘关系的进程通过打开这个文件得到fd1,也利用mmap()函数将fd1传参进去,就会得到那个进程创建的内存映射区首地址ptr1了,这样两个互不相关的进程通过 ptr 和 ptr1 各自进行读写即可。(因为这两个内存映射区指向的磁盘存储区都是同一个)

    注意:打开哪个文件其实无所谓的,用不到。

    5.共享内存(没有血缘关系的进程间通信机制)

    共享内存就是分配一块能被其他进程访问的内存。共享内存可以说是最有用的进程间通信方式,也是最快的IPC(Inter-Process Communication,进程间通信)形式。

    首先说下在使用共享内存区前,必须通过mmap将其附加到进程的地址空间或说为映射到进程空间。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到 进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。采用共享内存通信的一个显而易 见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而 共享内存则只拷贝两次数据一次 从输入文件到共享内存区,另一次 从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就 解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存 中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

    由于每个进程都有3G的私有进程空间,所以系统的物理内存无法对这些地址空间进行一一映射,因此kernel(内核)需要一种机制,把进程地址空间映射到物理内存上。当一个进程请求访问内存时,操作系统通过存储在kernel中的进程页表把这个虚拟地址映射到物理地址,如果还没有为这个地址建立页表项,那么操作系统就为这个访问的地址建立页表项。最基本的映射单位是page,对应的是页表项PTE

    深入理解linux内存管理之 页表管理 - CSDN博客

    实现共享内存的步骤如下:

    (1) 创建内存共享区

    进程1通过操作系统提供的api从内存中申请一块共享区域,linux系统中可以通过shmget函数实现,生成的共享内存块与某个特定的key进行绑定。

    (2) 映射共享内存到进程1中

    在linux环境中,可以通过shmat实现。

    (3)映射共享内存到进程2中

    进程2通过进程1的shmget函数和同一个key值,然后执行shmat,将这个内存映射到进程2中。

    (4)进程1与进程2中相互通信

    共享内存实现两个映射后,可以利用该区域进行信息交换,由于没有同步机制,需要参与通信的进程自己协商处理。

    (5)撤销内存映射关系

    完成通信之后,需要撤销之前的映射操作,通过shmdt函数实现。

    (6)删除共享内存区

    在linux中通过shctl函数来实现。

    6.管道

    管道传递数据是单向性的,只能从一方流向另一方,也就是一种半双工的通信方式;只用于有亲缘关系的进程间的通信,亲缘关系也就是父子进程或兄弟进程;没有名字并且大小受限,传输的是无格式的流,所以两进程通信时必须约定好数据通信的格式。管道它就像一个特殊的文件,但这个文件只存在于内存中,在创建管道时,系统为管道分配了一个页面作为数据缓冲区,进程对这个数据缓冲区进行读写,以此来完成通信。其中一个进程只能读一个只能写,所以叫半双工通信,为什么一个只能读一个只能写呢?因为写进程是在缓冲区的末尾写入,读进程是在缓冲区的头部读取,他们各自 的数据结构不同,所以功能不同。

    管道如何实现进程间的通信

    (1)父进程创建管道,得到两个⽂件描述符指向管道的两端

    (2)父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。

    (3)父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。   (也可以父进程读,子进程写)

    创建匿名管道,#include是调用fork,如 fork、pipe 以及各种 I/O 原语(read、write、close 等等);  

    7.命名管道(无血缘间进程通信机制)

    命名管道(NamedPipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是:命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。

    匿名管道只能在具有亲缘关系的进程间通信

    命名管道与它不同,它提供了一个路径名与之关联,有了自己的传输格式。

    命名管道和管道的不同之处还有一点是,命名管道是个设备文件,存储在文件系统中,没有亲缘关系的进程也可以访问,但是它要按照先进先出的原则读取数据。同样也是半双工的。

    8.套接字

    Socket套接字 - 简书

    套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。

    二、线程间通信

    线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。只是要注意的是线程间须要做好同步

    1.锁机制

    包括互斥锁、条件变量、读写锁;

    互斥锁提供了以排他方式防止数据结构被并发修改的方法。

    使用条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

    读写锁允许多个线程同时读共享数据,而对写操作是互斥的。

    2.信号量机制(Semaphore)

    包括无名线程信号量和命名线程信号量

    在POSIX标准中,信号量分两种,一种是无名信号量,一种是有名信号量。无名信号量只用于线程间的同步有名信号量只用于进程间通信。信号量是属于POSIX:SEM的,不是属于POSIX:THR的,需要的文件头是。两者的共同点都是相当于计数器,用于限制多个进程对有限共享资源的访问

    3.信号机制(Signal)

    类似进程间的信号处理

    线程间的通信目的主要是用于线程同步。所以线程没有像进程通信中的用于数据交换的通信机制。

    三、linux中进程间通信和线程间通信的区别

    linux中的进程,是有fork()系统调用创建的,进程间都有独立的地址空间,他们之间不能直接通信,必须通过一些IPC进程进程间通信机制来完成。常见的IPC有:PIPE,命名管道,信号,共享内存以及socket等;

    linux中的线程,是clone()系统调用创建的,一个进程下的线程间是共享内存空间的,故线程A可以之间访问线程B中定义的变量,但是必须注意并发的情况;

    另:“线程上下文”的规模要远远小于进程上下文

    四、进程/线程间同步机制

    线程:锁机制(互斥锁,读写锁等),条件变量,信号量

    临界区(Critical Section)、互斥量(Mutex)(锁)、信号量(Semaphore)、事件(Event)四种方式

    1.临界区

    通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

    2.互斥量(锁)

    采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享 .互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

    3.信号量在线程中是一般是无名信号量

    它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目 .信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

    PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。

    P操作申请资源:

    (1)S减1;

    (2)若S减1后仍大于等于零,则进程继续执行;

    (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。

    V操作 释放资源:

    (1)S加1;

    (2)若相加结果大于零,则进程继续执行;

    (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。

    4.事 件

    通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作 .事件是用来在进程和线程间同步的。就像我们人之间的交流。我把一件事做完了,告诉别人,别人收到这个消息才能接着往下做。或者他还要等第三个人把事情做完才能开       始做他的事

    5.总结

     互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。

    互斥量(Mutex),信号量(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。

    通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。

    多线程编程之进程、线程、进程间通信、线程间通信简介 - 小西红柿 - 博客园

    相关文章

      网友评论

        本文标题:进程间通信与线程间通信

        本文链接:https://www.haomeiwen.com/subject/qjcmuftx.html