网络通信与信息安全之深入解析进程之间的通信方式

Posted Serendipity·y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络通信与信息安全之深入解析进程之间的通信方式相关的知识,希望对你有一定的参考价值。

一、信号 Signal

  • 信号是 Linux 系统响应某些条件而产生的一个事件,由操作系统事先定义,接收到该信号的进程可以采取自定义的行为,这是一种“订阅-发布”的模式。
  • 信号来源分为硬件来源和软件来源:
    • 硬件来源:如按下 CTRL+C、除 0、非法内存访问等;
    • 软件来源:如 Kill 命令、Alarm Clock 超时,当 Reader 中止之后又向管道写数据等。
  • 如下所示,Linux 系统上支持的 30 种不同类型的信号:

  • 一般的信号是都是由一个错误产生的。以除 0 为例,在 x86 机器上 DIV 或 IDIV 指令除数为 0 时,会引发 0 号中断,编号 #DE(Divide Error),即所谓除零异常。这是一个硬件级中断,会导致陷入内核,执行操作系统预定义在 IDT 中的中断处理程序,而操作系统处理这个异常的方法,就是向进程发送一个信号 SIGFPE。如果进程设置相应的 signal handler,就执行进程的处理方法。否则,执行操作系统的默认操作,一般这种信号的默认操作是杀死进程。
  • 同理,溢出、非法内存访问(越界)、非法指令等也都属于硬件中断,由操作系统处理。操作系统会将这些硬件异常包装成“信号”发送给进程。如果进程不处理这几个异常信号,那么默认的行为就是挂掉。
  • 但是,信号也可以作为进程间通信的一种方式,明确地由一个进程发送给另一个进程。
  • 进程如何发送信号?
    • 操作系统提供发送信号的系统调用;
    • 该系统调用会将信号放到目标进程的信号队列中;
    • 如果目标进程未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给它为止;
    • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
  • 进程如何接收信号?
    • 每个进程有一个信号队列,放其它进程发给它、等待它处理的信号;
    • 进程在执行过程中的特定时刻,检查并处理自己的信号队列,如从系统空间返回到用户空间之前;
    • 发送信号时,必须指明发送目标进程的号码。一般用在具有亲缘关系的进程之间。
  • 用户进程对信号的处理过程有三种:
    • 处理信号:定义信号处理函数,当信号发生时,执行相应的处理函数;
    • 忽略信号:当不希望接收到的信号对进程的执行产生影响,而让进程继续执行时,可以忽略该信号,即不对信号进程作任何处理;
    • 不处理也不忽略:执行默认操作,linux 对每种信号都规定了默认操作;
  • 有的信号,用户进程是无法处理也无法忽略的,比如 SIGSTOP、SIGKILL 等。信号处理程序是一个用户层函数,进程可以为某个信号指定一个信号处理程序,接收到信号后,进程会跳转执行信号处理程序,执行完成后再返回到中断位置的下一条指令继续执行:

二、管道 Pipe

  • 管道命令,在 Linux Shell 中经常使用,一般使用管道操作符 | 来表示两个命令之间的数据通信。比如:
ps -ef | grep java | xargs echo
  • 管道操作符的内部实现其实就是 Linux 的管道接口,由管道操作符 | 分割的每个命令是独立的进程,各个进程的标准输出 STDOUT,会作为下一个进程的标准输入 STDIN。

① 定义

  • 管道是一种半双工的通信方式,数据只能单向流动,上游进程往管道中写入数据,下游进程从管道中接收数据。如果想实现双方通信,那么需要建立两个管道。
  • 管道适合于传输大量信息。管道发送的内容是以字节为单位的,没有格式的字节流。

② 创建管道

  • 通过 pipe() 系统调用来创建并打开一个管道,当最后一个使用它的进程关闭对他的引用时,pipe 将自动撤销。
  • 通过 pipe() 创建的是匿名管道,只能用于具有亲缘关系的进程之间(父子进程或兄弟进程)。

③ 管道的实现

  • 管道就是一个文件,是一种只存在于内存中的特殊的文件系统。
  • 在 Linux 中,管道借助文件系统的 File 结构实现,父进程使用 File 结构保存向管道写入数据的例程地址,子进程保存从管道读出数据的例程地址,这解释了上文所说的:
    • 单向流动;
    • 只能用于具有亲缘关系的进程之间。
  • 管道是由内核管理的一个缓冲区,缓冲区被设计成为环形的数据结构,以便管道可以被循环利用(循环队列)。

④ 管道的同步

  • 管道是一个具有特定大小的缓冲区:
    • 操作系统会保证读写进程的同步;
    • 下游进程或者上游进程需要等另一方释放锁后才能操作管道,管道就相当于一个文件,同一时刻只能有一个进程访问;
    • 当管道为空时,下游进程读阻塞;当管道满时,上游进程写阻塞;
    • 管道不再被任何进程使用时,自动消失。

三、命名管道 FIFO

  • Linux 管道包含匿名管道和命名管道,上文的匿名管道,只能用在亲缘进程中,管道文件信息保存在内存里。
  • 命名管道(FIFO)可用于没有亲缘的进程间,Pipe 和 FIFO 除了建立、打开、删除的方式不同外,二者几乎一模一样。
  • 通过 mknode() 系统调用或者 mkfifo() 函数建立命名管道,一旦建立,任何有访问权的进程都可以通过文件名将其打开和进行读写,而不局限于父子进程。
  • 建立命名管道时,会在磁盘中创建一个索引节点,命名管道的名字就相当于索引节点的文件名。索引节点设置了进程的访问权限,但是没有数据块。
  • 命名管道实质上也是通过内核缓冲区来实现数据传输,有访问权限的进程,可以通过磁盘的索引节点来读写这块缓冲区。
  • 当不再被任何进程使用时,命名管道在内存中释放,但磁盘节点仍然存在。

四、信号量 Semaphore

  • 信号量是一种特殊的变量,对它的操作都是原子的,有两种操作:V(signal())和 P(wait())。
  • V 操作会增加信号量 S 的数值,P 操作会减少它:
    • V(S):如果有其他进程因等待 S 而被挂起,就让它恢复运行,否则 S 加 1;
    • P(S):如果 S 为 0,则挂起进程,否则 S 减 1;
    • P、V 来自于荷兰语:Probeer (try)、Verhoog (increment)。
  • 如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的 0 或 1,称为二进制信号量(binary semaphore)。在 Linux 系统中,二进制信号量又称互斥锁(Mutex),信号量可以用于实现进程或线程的互斥和同步。
  • 信号量在底层的实现是通过硬件提供的原子指令,如 Test And Set、Compare And Swap 等。比如 golang 实现互斥量就是使用了 Compare And Swap 指令(go)。

五、共享内存 Shared Memory

  • 共享内存顾名思义,允许两个或多个进程共享同一段物理内存,不同进程可以将同一段共享内存映射到自己的地址空间,然后像访问正常内存一样访问它,不同进程可以通过向共享内存端读写数据来交换信息。
  • 一个进程可以通过操作系统的系统调用,创建一块共享内存区;其他进程通过系统调用把这段内存映射到自己的用户地址空间中;之后各个进程向读写正常内存一样,读写共享内存。共享内存区只会驻留在创建它的进程地址空间内。
  • 共享内存的优点是简单且高效,访问共享内存区域和访问进程独有的内存区域一样快,原因是不需要系统调用,不涉及用户态到内核态的转换,也不需要对数据不必要的复制。
  • 比如管道和消息队列,需要在内核和用户空间进行四次的数据拷贝(读输入文件、写到管道;读管道、写到输出文件),而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。


  • 此外,消息传递的实现经常采用系统调用,也就经常需要用户态和内核态互相转换;而共享内存只在建立共享内存区域时需要系统调用;一旦建立共享内存,所有访问都可作为常规内存访问,无需借助内核。
  • 共享内存的缺点是存在并发问题,有可能出现多个进程修改同一块内存,因此共享内存一般与信号量结合使用。
  • Linux 的 2.2.x 内核支持多种共享内存方式,如 mmap() 系统调用,Posix 共享内存,以及系统 V 共享内存;
    • mmap() 系统调用的主要作用是将普通文件映射到进程的地址空间,然后可以像访问普通内存一样对文件进行访问,不必再调用 read(),write() 等操作;mmap() 不是专门用来共享内存的,但是多个进程可以通过 mmap() 映射同一个普通文件,来实现共享内存。



六、消息队列 Message Queue

  • 消息队列是一个消息的链表,保存在内核中。消息队列中的每个消息都是一个数据块,具有特定的格式。操作系统中可以存在多个消息队列,每个消息队列有唯一的 key,称为消息队列标识符。
  • 消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。和信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,但消息队列仍然有大小限制。
  • 消息队列允许一个或多个进程向它写入与读取消息。消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。也就是说,消息队列是异步的,但这也造成了一个缺点,就是接收者必须轮询消息队列,才能收到最近的消息。
  • 操作系统提供创建消息队列、取消息、发消息等系统调用。操作系统负责读写同步:若消息队列已满,则写消息进程排队等待;若取消息进程没有找到需要的消息,则在等待队列中寻找。
  • 消息队列和管道相比,相同点在于二者都是通过发送-接收的方式进行通信,并且数据都有最大长度限制。不同点在于消息队列的数据是有格式的,并且取消息进程可以选择接收特定类型的消息,而不是像管道中那样默认全部接收。

七、套接字 Socket

  • 不同的计算机的进程之间通过 socket 通信,也可用于同一台计算机的不同进程。
  • 需要通信的进程之间首先要各自创建一个 socket,内容包括主机地址与端口号,声明自己接收来自某端口地址的数据。
  • 进程通过 socket 把消息发送到网络层中,网络层通过主机地址将其发到目的主机,目的主机通过端口号发给对应进程。
  • 操作系统提供创建 socket、发送、接收的系统调用,为每个 socket 设置发送缓冲区、接收缓冲区。

八、总结

方式传输的信息量使用场景关键词
信号少量任何硬件来源、软件来源 / 信号队列
管道大量亲缘进程间单向流动 / 内核缓冲区 / 循环队列 / 没有格式的字节流 / 操作系统负责同步
命名管道大量任何磁盘文件 / 访问权限 / 无数据块 / 内核缓冲区 / 操作系统负责同步
信号量N任何互斥同步 / 原子性 / P 减 V 增
共享内存大量多个进程内存映射 / 简单快速 / 操作系统不保证同步
消息队列比信号多,但有限制任何有格式 / 按消息类型过滤 / 操作系统负责同步
套接字大量不同主机的进程读缓存区 / 写缓冲区 / 操作系统负责同步

以上是关于网络通信与信息安全之深入解析进程之间的通信方式的主要内容,如果未能解决你的问题,请参考以下文章

IPC之套接字

简述Linux进程间通信之命名管道FIFO

网络通信与信息安全之深入解析TCP与UDP传输协议

python之进程与线程

Socket通信

网络通信与信息安全之深入解析TCP连接中如何确定客户端的端口号