进程间通信
在计算机科学中,进程间通信(IPC)特指操作系统提供的允许进程共享数据的机制。进程间通信有多种方式,比如文件、信号、通道、共享内存和Socket等。这篇文章涉及到的进程间通信方式主要有信号,管道和Socket三种。
信号(Signal)
信号就是向一个进程或同一进程中的特定线程发送的异步通知,以通知它一个事件。大多数信号可以被忽略、阻止或处理(通过指定的代码),SIGSTOP
(暂停)和SIGKILL
(立即终止)是两个例外。符号常量是有整数值的,比如SIGKILL
的整数值为9。
用户的交互就可以产生信号。例如,用户可以在命令行中按Ctrl+C
来终止一个从命令行启动的程序;Ctrl+C
会产生一个SIGTERM
信号,与SIGKILL
不同,SIGTERM
可以被阻止或处理。
再比如我们经常用kill
命令来杀掉一个进程:
|
|
kill
命令的第二个参数就是一个标准的信号,如SIGTERM
或SIGKILL
。当然也可以指定其他信号,比如SIGUSR1
和 SIGUSR2
,这两个信号被用来告诉进程有一个用户定义的事件发生了,具体做什么取决于进程如何处理发送过来的信号,不一定是杀掉进程,比如在MongoDB中,运行下边的命令就是告诉MongoDB该进行log rotation了。
|
|
当信号发出时,操作系统会中断目标进程的正常执行流程进而完成信号传递。进程可以在任何非原子指令期间被中断,如果进程之前已经注册了一个对这个信号的处理程序,则执行该程序,否则就执行默认的信号处理程序。
信号处理程序可以通过signal(2)
或sigaction(2)
这两个系统调用来设置,现在sigaction()
函数取代了signal()
函数,应优先使用。另外要注意这两个系统调用不应该在同一个进程中控制同一个信号。在设置信号处理程序的时候还可以用这两个特殊的值:
- SIG_IGN: 忽略信号(ignore)
- SIG_DFL: 和使用默认信号处理程序(default)
简单来说,如果想忽略一个信号,可以这样:
|
|
其中sigaction
的第二个参数是一个sigaction struct
:
|
|
详细的用法就不赘述了,可以看这里 。
管道(pipe)
管道是基于消息传递实现的,将一组进程的标准流链在一起,这样每个进程的输出(stdout)直接作为输入(stdin)传递给下一个进程。它们是并发执行的,也就是说后边的进程可以在前一个进程running的时候被启动,直观来说:
|
|
我们经常用到管道,比如要列出当前目录下的文件(ls),只保留ls输出中包含字符串 “key “的行(grep),并在滚动页面中查看结果(less)
|
|
注意,这是匿名管道,命名管道还不太一样,关于命名管道可以看这里 ,它不是本文的重点。
套接字(Socket)
Socket是网络上运行的两个程序之间双向通信链接的一个端点,Socket机制提供了一种进程间通信的方式。管道是由pipe()
系统调用创建的,Socket则是使用socket()
系统调用来创建的。
Socket在网络上提供双向FIFO通信设施,在通信的两端各创建一个连接到网络的Socket,每个Socket都有一个特定的地址,这个地址由一个IP地址和一个端口号组成。
举个不恰当的例子,可以把Socket当作两个插座,中间用一根电线连接起来。
SIGPIPE
说了这么多前置知识,我们现在来看看本文的主角–SIGPIPE
信号:
The SIGPIPE signal is sent to a process when it attempts to write to a pipe without a process connected to the other end.
当一个进程试图向一个管道写入时,如果没有一个进程连接到另一端,则会向该尝试写入的进程发送SIGPIPE
信号。也就是说当一个进程试图向一个读端已经关闭的管道写入时,就会收到这个信号,而收到这个信号的默认操作是终止进程。
这很合理,比如有这样一个管道:
|
|
如果process2已经死掉了,理应通知process1一下,不能让他在那一直做无用功,至于收到信号process1怎么处理就是它自己的事了。
举个例子
|
|
yes
命令的作用是将无限的"y"
序列写入STDOUT,而head则将其从管道的另一端读为STDIN。head
读取第一行,然后退出,只要head
终止,管道的接收端就关闭了。因此,Linux内核会向yes
进程发送SIGPIPE
信号,表示没有reader了,然后yes
进程就会终止。
不只是对于管道,Socket也有这个机制:
When a process writes to a socket that has received an RST, the SIGPIPE signal is sent to the process. The default action of this signal is to terminate the process, so the process must catch the signal to avoid being involuntarily terminated.
如果向一个收到RST
的Socket继续发送数据,则会收到SIGPIPE
信号。
先来回忆一下RST
数据包,当一个意外的TCP数据包到达主机时,该主机通常会通过在同一连接上发送一个RST
数据包来进行响应。RST
数据包就是一个没有有效载荷,并且在TCP头标志中设置了RST位的数据包。常见的意外的TCP数据包有:
- SYN数据包,试图建立连接到一个没有进程监听的服务器端口
- 数据包到达之前建立的TCP连接上,但本地应用程序已经关闭了它的套接字或退出,操作系统关闭了套接字
我们可以用一个例子说明Socket上的SIGPIPE
:
Server:
|
|
Client:
|
|
运行上边的代码,你应该可以看到下边的错误
|
|
如果你没看到,就把100000改的再大一点。
在TCP中,当Server端调用close
时, Client端会收到FIN
数据包,从Client的角度来看,Server端不会再发送数据,但是仍可以接收数据。此时对Client端的Socket调用write
方法时, 如果发送缓冲没问题, 会返回正确写入,但发送的报文会导致Server端的TCP协议栈返回RST
数据包。此时再调用write
方法(假设在收到RST之后), 会生成SIGPIPE
信号, 导致进程退出。
如果不想进程退出,可以捕捉SIGPIPE
信号:
|
|
在很多网络工具中都会用到这种方法,比如openssl ,在网络编程中这是需要注意的。