进程间通信

进程通信的应用场景

  • 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

  • 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信方式

进程间通信的方式主要有:管道(有名管道和无名管道)、信号、消息队列、共享内存、内存映射、信号量和套接字。

1、管道

2、信号

3、消息队列

4、共享内存

5、存储映射I/O

6、信号量

7、套接字


1、管道

管道是一种两个进程间进行单向通信的机制。因为管道传递数据的单向性,管道又称为半双工管道。管道可以分为无名管道和命名管道。

(1)无名管道

  • 无名管道特点:

    • 无名管道只能用于父子进程或兄弟进程之间,必须用于具有亲缘关系的进程间的通信。

    • 无名管道只能单向传输,是半双工方式,如果双方需要同时收发数据需要两个管道。

    • 无名管道是一种特殊的文件,这种文件只存在于内存中。

  • 相关接口:无名管道可以用pipe()函数来创建

    • int pipe(int fd[2]);

      • fd[2]:管道两端用fd[0]和fd[1]来描述,读的一端用fd[0]表示,写的一端用fd[1]表示。通信双方的进程中写数据的一方需要把fd[0]先close掉,读的一方需要先把fd[1]给close掉。

(2)命名管道(也称为FIFO):

  • 命名管道特点:

    • 命名管道是FIFO文件,存在于文件系统中,可以通过文件路径名来指出。

    • 命名管道可以在不具有亲缘关系的进程间进行通信,命名管道由于给管道起了名字,因此可以用于同一系统上任意两个进程间通信。

  • 相关接口:

    • 系统调用方式:int mkfifo(const char *pathname, mode_t mode);例如mkfifo("testNamedPipe","rw"),会在当前目录下生成名为testNamedPipe的有名管道,创建好后就可以用普通的I/O操作对管道进行操作了,如read、write、close等。

      • mode:和open()中的参数相同。

      • pathname:即将创建的FIFO文件路径,如果文件存在需要先删除。

    • 命令行调用方式:mknod testNamedPipe p

  • FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。

(3)管道的主要缺点:

  • 只支持单向数据流
  • 只支持无格式的字节流
  • 管道的缓存大小是有限的

2、信号

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

(1)信号事件的来源:

  • 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
  • 系统调用产生,如:kill、raise、abort
  • 软件条件产生,如:定时器alarm
  • 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
  • 命令产生,如:kill命令

(2)信号的处理方式:

  • 执行默认操作,linux对每种信号都规定了默认操作。
  • 捕捉信号(调用户处理函数),定义信号处理函数,当信号发生时,执行相应的处理函数。
  • 忽略信号,当不希望接收到的信号对进程的执行产生影响,而让进程继续执行时,可以忽略该信号,即不对信号进程作任何处理。

有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SEGSTOP,这是为了使系统管理员能在任何时候中断或结束某一特定的进程。

(3)信号阻塞

有时候既不希望进程在接收到信号时立刻中断进程的执行,也不希望此信号完全被忽略掉,而是希望延迟一段时间再去调用信号处理函数,这个时候就需要信号阻塞来完成。

3、消息队列

消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。

消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

相比于 FIFO,消息队列具有以下优点:

  • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
  • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
  • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。

消息队列的常用函数如下表:

进程间通过消息队列通信,主要是:创建或打开消息队列,添加消息,读取消息和控制消息队列。

可以用函数msget创建消息队列,调用msgsnd函数,把输入的字符串添加到消息队列中,然后调用msgrcv函数,读取消息队列中的消息并打印输出,最后再调用msgctl函数,删除系统内核中的消息队列。

4、共享内存

共享内存可以提供给服务器进程和客户进程之间进行通信,不需要进行数据的复制,所以速度最快,只需要让两个进程通过页表映射到同一块物理内存即可,这样,这块物理内存是两个进程都能看到的,这样当一个进程进行写操作,另外的一个进程也就可以做读操作。所以问题关键也就是给出一个特定的存储区。通常情况下,我们需要确保一个进程在写的时候,另外一个进程不能去读,所以我们可以使用信号量进行共享内存访问。

 

5、存储映射I/O

Linux提供了存储映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改,mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。普通文件映射到进程地址空间后,进程可以向访问内存的方式对文件进行访问,不需要其他系统调用(read,write)去操作。

存储映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<—->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。

共享内存和存储映射I/O的对比:

  • mmap是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间进行映射。而对于shm而言,shm每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大。
  • 相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。
  • 另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget就会丢失。

6、信号量

  • 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 为了获得共享资源,进程需要执行下列操作: 
    • 测试控制该资源的信号量。 
    • 若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。 
    • 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。 
    • 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。 
  • 为了正确的实现信号量,信号量值的测试及减1操作应当是原子操作,为此,信号量通常是在内核中实现的。
  • 最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二元信号量,它控制单个资源,初始值为1。但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用。
  • 信号量只能进行两种操作等待和发送信号,即P(sem)和V(sem),他们的行为是这样的:
    • P(sem)(获得信号量):如果sem的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
    • V(sem)(释放信号量):如果有其他进程因等待sem而被挂起,就让它恢复运行,如果没有进程因等待sem而挂起,就给它加1.
      • P原语操作中,每来一个进程需要获得资源,会对sem减1,减1后,如果sem>=0,说明可以使用该资源,否则会进入等待队列;
      • V原语操作中,当一个进程使用完资源要进行释放时,会对sem加1,此时它会根据sem的值判断是否有进程在等待队列中等待使用,如果sem<=0,则说明有进程在等待(因为:当资源正好用完的时候,sem=0,此后,再有进程需要使用,会对sem减1再去等待,所以sem就小于0了),那么就会唤醒等待队列中的一个进程,否则直接释放资源。
  • 相关接口
    • 创建信号量:int semget(key_t key, int nsems, int semflag);

      创建成功返回信号量标识符,失败返回-1。

      • key:进程pid。

      • nsems:创建信号量的个数。

      • semflag:指定信号量读写权限。

    • 改变信号量值:int semop(int semid, struct sembuf *sops, unsigned nsops);

      我们所需要做的主要工作就是串讲sembuf变量并设置其值,然后调用semop,把设置好的sembuf变量传递进去。

      struct sembuf结构体定义如下:

      struct sembuf{
          short sem_num;
          short sem_op;
          short sem_flg;
      };

      成功返回信号量标识符,失败返回-1。

      • semid:信号量集标识符,由semget()函数返回。

      • sops:指向struct sembuf结构的指针,先设置好sembuf值再通过指针传递。

      • nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作。

    • 直接控制信号量信息:int semctl(int semid, int semnum, int cmd, union semun arg);

      • semid:信号量集标识符。

      • semnum:信号量集数组上的下标,表示某一个信号量。

      • arg:union semun类型。

7、套接字

一种广泛使用的通信方式,与其他进程间通信方式不同,套接字不仅可以用于不同进程与线程之间的通信,还可以用于不同机器上进程之间的通信。

 

 

 

  • 0
    点赞
  • 3
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

qq_36132127

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值