Linux进程间通信机制详谈

Linux进程间通信机制

Unix系统提供的进程间通信机制主要有:

  • 管道和FIFO(命名管道)
  • 套接字
  • 信号
  • 信号量
  • 消息队列
  • 共享内存区

管道pipe

在这里插入图片描述

管道机制思想是在内存中创建一个共享文件,从而使得通信双方利用该共享文件进行交互。需要注意的是管道数据流动是单向的,是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。管道只在具有公共祖先的两个进程之间使用,可以将管道视为一个独立的文件系统,管道在管道两侧的进程看来就是一个文件,只是这个文件只存在于内存中。

一个进程写入管道的所有数据由内核定向到另一个进程,另一个进程就可以从管道中读取数据。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。

在Unix的shell中,通过 “|” 创建管道(样子就像管道一样),将前面的进程的标准输出重定向到管道中,然后后面的进程从管道中读取输入。

管道的创建:应用程序调用pipe系统产生管道,该调用将返回两个文件描述符,分别用于管道的两侧。由于两个描述符位于同一个进程,因此进程自身通信,好像没太大作用,但是通过fork或者clone复制进程时管道也会被复制,因此管道通信就是利用该性质,实现了具有公共祖先的进程之间的通信。

常用的操作就是创建一个连接到另一个进程的管道,然后将其输出或向其输入端发送数据。标准IO库提供了两个函数,popen 和 pclose,这两个函数可以实现:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,等待命令终止。

管道具有局限性:只支持单向数据流,没有名字,缓冲区有限等

命名管道FIFO

管道很简单灵活有效,但是有一个比较大的缺点,那就是无法打开已经存在的管道,即两个进程不能共享同一个管道,同时匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。故Unix引入了命名管道,或称为FIFO(先进先出),因为这是一种最先写入文件的字节总是最先被读出的特殊文件类型。

FIFO文件包含在系统的文件树中,即拥有磁盘索引节点。因此哪怕没有亲缘关系的进程也可以通过该方式消息传递。

  • FIFO的索引节点出现在系统目录树上
  • FIFO是一种双向通信管道,即一个进程可以以读/写模式打开一个FIFO

通过mkfifo函数创建FIFO文件,之后可以用open打开它。命名管道在打开时需要确实该管道已经存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。
在这里插入图片描述

消息队列

在这里插入图片描述
消息传递机制通过消息队列实现,发送方产生消息并将消息写到队列,之后一个或多个其他进程从队列获取消息。消息包含消息正文还有一个数,该数用来使消息队列实现多种不同类型的消息,接收者通过该数字获取自己想要的消息。消息读取后将从队列中删除,即便是很多进程在同一个信道上监听,每一个消息仍然只能被一个进程读取。即进程产生的消息放入到 IPC 消息队列,直到一个进程将其读走然后再移除。

需要注意的是,发送者和接收者通过消息队列传递信息时,不需要同时运行。发送进程打开一个队列将消息放入后则结束工资,接收进程再发送方结束工作后仍可以访问该消息队列并获取需要的消息。这期间是由内核维护的。

  • 为了发送一条消息,进程调用msgsnd()函数,该函数的参数如下:
    • 目标消息队列的IPC标识符
    • 消息正文的大小
    • 用户态缓冲区的地址,缓冲区中包含有消息的类型,之后紧跟消息正文
  • 为了获得一条消息,进程调用msgrcv()函数,该函数参数如下:
    • IPC消息队列资源的IPC标识符
    • 指向用户态缓冲区的指针,消息类型和消息正文将被拷贝至该缓冲区
    • 缓冲区的大小
    • 一个值t,即应该获取消息的特征

消息队列的结构定义如下:

struct msg_queue{
    struct kern_ipc_perm q_perm;
    time_t q_stime; //上一次调用msgsnd发送消息的时间
    time_t q_rtime; //上一次调用msgrcv接收消息的时间
    time_t q_ctime; //上一次修改的时间
    unsigned long q_cbytes; //队列上当前的字节数目
    unsigned long q_qnum; //队列中的消息数目
    unsigned long q_qbytes; //队列上最大字节数目
    pid_t q_lspid; //上一次调用msgsnd的pid
    pid_t q_lrpid; //上一次接收消息的pid
    
    struct list_head q_messsages; //队列中的消息链表
    struct list_head q_receivers;  //接收消息的进程链表
    struct list_head q_senders;   //发送消息的进程链表
};

因为当消息队列满时(或到达了最大消息数,或到达了最大字节数),则试图令新消息入列的进程将被阻塞,将其插入到发送消息链表。当消息队列是空时(或者进程指定的消息类型当前队列中没有),接收进程将被阻塞,将其插入到接收消息链表。 被阻塞进入睡眠的进程,在满足条件后将被唤醒并自动尝试重新收发操作。

q_message各个消息都被封装在如下数据结构中:

struct msg_msg{
    struct list_head m_list; //连接各消息的链表元素
    long m_type; //消息类型
    int m_ts;  //消息正文长度
    struct msg_msgseg* next; 
    /*     消息内容       */
};

结构体中并没有指定存储消息自身的字段,因为每个消息至少分配一个内存页,msg_msg保存在该页的起始处,剩余空间即可存储消息正文,如果消息较长需要别的内存页,则由next指针连接。
在这里插入图片描述
消息队列的整体数据结构:(无阻塞睡眠的发送进程链表)
在这里插入图片描述
进程间通信可以通过调用原语 send() 和 receive() 来进行。实现这些原语有不同的设计方案。消息传递可以是阻塞或非阻塞,也称为同步或异步:

  • 阻塞发送:发送进程阻塞,直到消息由接收进程或邮箱所接收。
  • 非阻塞发送:发送进程发送消息,并且恢复操作。
  • 阻塞接收:接收进程阻塞,直到有消息可用。
  • 非阻塞接收:接收进程收到一个有效消息或空消息。

共享内存

采用共享内存的进程通信,就需要建立共享区域。共享内存允许两个或多个进程通过把共享数据结构放入到共享内存区来进行访问。如果进程要访问共享内存区,就必须在自己的地址空间增加一个新内存区,用来映射与这个共享内存区相关的页框。共享内存是进程间通信最快的方式,但是一定要保证数据的同步

  • 调用shmget()函数获得共享内存区的IPC标识符,如果该共享内存区不存在则创建它
  • 调用shmat()函数把一个共享内存区附加到一个进程上
  • 调用shmdt()函数把一个共享内存区从进程的地址空间删除
    在这里插入图片描述

在smd_ids全局变量的entries数组中保存了kern_ipc_perm和shmid_kernel的组合,以便管理IPC对象的访问权限。对每个共享内存对象都创建一个伪文件,通过shm_file连接到shmid_kernel的实例。内核使用了shm_file->f_mapping指针访问地址空间对象。完成这个过程还需要设置各进程的页表,使每个进程都能够访问该IPC相关的共享内存区域。

需要注意的是,由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

IPC信号量

IPC信号量类似于内核信号量,是一个计数器,用来为多个进程共享的数据结构提供受控访问。

如果受保护的资源是可用的,则信号量的值就是正数;如果受保护的资源现在不可用,则信号量的值为0。要访问资源的进程就是试图将对应的信号量减1,如果当前信号量值不为正则内核将阻塞该行为,直至该值为正再唤醒该进程。当一个进程释放该资源时,对应的信号量就加1。

为了获取该共享资源,进程的操作如下:

  1. 测试控制该资源的信号量
  2. 如果信号量为正,则使用该资源,信号量对应减1
  3. 否则,内核阻塞该过程,进程进入休眠状态。直至被唤醒后将进入步骤1
  4. 当进程不再使用该资源时将释放资源,对应信号量加1,如果有进程正在休眠等该信号量,则将其唤醒

为了正确完成操作,信号量的测试以及加1减1操作必须是原子操作

IPC信号量相较于内核信号量更加复杂,主要是每个IPC信号量可能是多个信号量值的集合,即一个IPC资源保护多个独立共享的资源,并且System V IPC信号量提供了失效安全机制,当进程死亡时可以令对应的IPC信号量恢复。

当进程想访问IPC信号量所保护的一个或多个资源时,步骤如下:

  • 调用semget()封装函数获得IPC信号量标识符
  • 调用semop()封装函数测试并递减所有原始信号量的值,如果不能访问这些资源则将进程挂起
  • 当放弃受保护的资源时,调用semop()函数原子增加所有相关的信号量
  • semct()封装函数可以用于删除IPC信号量

信号

信号是Linux系统中用于进程间互相通信的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态;如果该进程当前并未处于执行状态,则该信号由内核保存起来,直到该进程恢复执行并接收该信号为止;如果一个信号被进程设置为阻塞,则该信号的传递将被延迟,直到其阻塞被取消是才被传递给进程。

使用信号的主要目的:

  • 让进程知道某一特定事件已经发生
  • 强迫进程执行它自己代码中的信号处理程序

信号是软中断,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件。

kill命令通过PID向进程发送信号,可以通过kill -l 查看支持哪些信号量

在这里插入图片描述

一些信号的含义:

以下列出几个常用的信号:

信号名称 描述
SIGHUP 用户从终端注销,所有由该终端开启的进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程
SIGINT 程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号
SIGQUIT 程序退出信号,程序运行过程中,按Ctrl+\\键将产生该信号
SIGKILL 用户终止进程执行信号。shell下执行kill -9发送该信号。该信号不能被阻塞、处理和忽略
SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。
SIGSTOP 暂停(stopped)进程的执行. 该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

信号传递的流程:

  • 一个进程产生信号,并设置要发送的进程PID,然后传递给内核
  • 内核根据对应的进程的设置决定是否发送给接收进程,如果接收者阻塞该信号,则暂时保留该信号,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号
  • 接收进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(即保存当前的状态),然后开始执行中断服务程序,执行完成后恢复上下文,继续执行中断前的程序

套接字

进程通过套接字网络接口可以实现通信,既可以和本机内的进程通信,也可以和本机外的进程通信。

网络套接字需要底层协议的支持,例如 TCP(传输控制协议)或 UDP(用户数据报协议)。

IPC 套接字依赖于本地系统内核的支持来进行通信;特别的,IPC 通信使用一个本地的文件作为套接字地址
在这里插入图片描述

也就是通信双方基于套接字编程,基于提供的库函数进行网络通信。

这一部分具体应该详看网络的相关书籍了解。

猜你喜欢

转载自blog.csdn.net/dingdingdodo/article/details/106985681