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

网友投稿 716 2022-05-29

一、信号 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

1

管道操作符的内部实现其实就是 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() 映射同一个普通文件,来实现共享内存。

系统 V 则是通过映射特殊文件系统 shm 中的文件实现进程间的共享内存,通过 shmget 可以创建或获得共享内存的标识符,取得共享内存标识符后,通过 shmat 将这个内存区映射到本进程的虚拟地址空间。

有关 mmap() 系统调用、系统 V 共享内存的详细介绍,以及两者的对比,可以参考:

Linux环境进程间通信 - 共享内存(上);

Linux环境进程间通信 - 共享内存(下)。

六、消息队列 Message Queue

消息队列是一个消息的链表,保存在内核中。消息队列中的每个消息都是一个数据块,具有特定的格式。操作系统中可以存在多个消息队列,每个消息队列有唯一的 key,称为消息队列标识符。

消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。和信号相比,消息队列能够传递更多的信息。与管道相比,消息队列提供了有格式的数据,但消息队列仍然有大小限制。

消息队列允许一个或多个进程向它写入与读取消息。消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。也就是说,消息队列是异步的,但这也造成了一个缺点,就是接收者必须轮询消息队列,才能收到最近的消息。

操作系统提供创建消息队列、取消息、发消息等系统调用。操作系统负责读写同步:若消息队列已满,则写消息进程排队等待;若取消息进程没有找到需要的消息,则在等待队列中寻找。

消息队列和管道相比,相同点在于二者都是通过发送-接收的方式进行通信,并且数据都有最大长度限制。不同点在于消息队列的数据是有格式的,并且取消息进程可以选择接收特定类型的消息,而不是像管道中那样默认全部接收。

七、套接字 Socket

不同的计算机的进程之间通过 socket 通信,也可用于同一台计算机的不同进程。

需要通信的进程之间首先要各自创建一个 socket,内容包括主机地址与端口号,声明自己接收来自某端口地址的数据。

进程通过 socket 把消息发送到网络层中,网络层通过主机地址将其发到目的主机,目的主机通过端口号发给对应进程。

操作系统提供创建 socket、发送、接收的系统调用,为每个 socket 设置发送缓冲区、接收缓冲区。

八、总结

任务调度 网络

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:现代富文本编辑器Quill的模块化机制
下一篇:金三银四马上到了,找工作需要准备什么?
相关文章