进程间的悄悄话:深入理解操作系统中的进程通信 (IPC) (Python 版)
在我们之前的博客中,我们学习了进程的概念——程序的一次执行实例,以及操作系统如何使用进程控制块 (PCB) 来管理进程。我们知道,操作系统要同时管理成百上千个进程,让它们井然有序地运行,并在它们之间切换。
然而,在许多情况下,不同的进程需要协同工作,交换数据或同步活动。例如:
- Shell 进程需要启动其他命令进程,并可能需要知道它们何时完成。
- 一个进程生成数据(生产者),另一个进程处理数据(消费者)。
- 一个进程需要向另一个进程发送一个事件通知。
- 客户端进程需要向服务器进程发送请求并接收响应。
为了满足这些需求,操作系统提供了各种机制,允许进程在受控和安全的环境下进行通信。这些机制统称为进程间通信 (Inter-Process Communication, IPC)。
IPC 的核心目的就是让相互独立的进程能够交换信息和进行同步。
不同的 IPC 机制在通信方式、性能、复杂度和适用场景上各有不同。下面我们来详细探讨一些常见的 IPC 方式,并使用 Python 语言来举例说明。
1. 共享内存 (Shared Memory)
核心思想: 允许多个进程访问同一块物理内存区域。
工作原理: 操作系统将一块特定的内存区域映射到参与通信的多个进程的各自的地址空间中。一旦映射完成,进程就可以像访问自己的普通内存一样直接对这块共享内存进行读写。
优点:
- 速度快: 数据无需在进程之间复制,直接在共享内存中读写,是所有 IPC 方式中速度最快的。
- 高效: 适用于需要传输大量数据的场景。
缺点:
- 同步问题: 多个进程同时读写共享内存可能会导致竞争条件(Race Condition)。需要额外的同步机制(如互斥锁 Mutex、信号量 Semaphore)来保证数据的一致性和正确性。
- 复杂性: 需要程序员自己管理共享内存中的数据结构和同步,相对复杂。
适用场景: 高性能数据交换、大量数据传输。
Python 举例 (使用 multiprocessing.shared_memory
):
Python 3.8+ 提供了 multiprocessing.shared_memory
模块,它是 POSIX 共享内存的 Python 封装。需要配合 multiprocessing
创建进程和同步机制。
shm_example.py
import multiprocessing as mp
import multiprocessing.shared_memory as sm
import time
import os
import threading # 演示同步,实际跨进程应使用 mp.Lock/Semaphore
# 生产者进程函数
def producer(shm_name, data_to_write, lock):
# 进程需要通过名字 attach 到已创建的共享内存
try:
# attach=True 表示连接到已存在的共享内存
shm = sm.SharedMemory(name=shm_name)
print(f"Producer (PID: {os.getpid()}) attached to shared memory '{shm_name}'.")
# 将数据写入共享内存的缓冲区 (bytes)
# 注意:这里为了简化,直接写入,没有考虑 buffer 大小和越界
# 在实际应用中,需要确保写入的数据不超过共享内存的大小
with lock: # 使用锁进行同步,保证写入的原子性
print("Producer acquired lock.")
shm.buf[:len(data_to_write)] = data_to_write
print(f"Producer wrote: {data_to_write}")
# 可以选择在写入后在缓冲区末尾添加一个 null 字节作为结束标记
if len(data_to_write) < shm.size:
shm.buf[len(data_to_write)] = 0
print("Producer released lock.")
# 模拟工作或等待消费者读取
time.sleep(0.5)
except FileNotFoundError:
print(f"Producer error: Shared memory '{shm_name}' not found. Is the creator process running?")
except Exception as e:
print(f"Producer error: {e}")
finally:
# 进程完成使用后,需要 dettach(关闭视图),但共享内存本身仍然存在
if 'shm' in locals() and shm:
shm.close()
print(f"Producer (PID: {os.getpid()}) detached from shared memory.")
# 消费者进程函数
def consumer(shm_name, lock):
try:
# 进程需要通过名字 attach 到已创建的共享内存
shm = sm.SharedMemory(name=shm_name)
print(f"Consumer (PID: {os.getpid()}) attached to shared memory '{shm_name}'.")
# 从共享内存的缓冲区读取数据 (bytes)
with lock: # 使用锁进行同步,保证读取到完整的数据
print("Consumer acquired lock.")
# 读取数据,可以查找 null 字节作为结束标记,或按固定大小读取
# bytes(shm.buf) 将缓冲区转为 bytes 对象
read_data = bytes(shm.buf).split(b'\0', 1)[0] # Read up to first null or buffer size
print(f"Consumer read: {read_data.decode()}") # 将 bytes 解码为字符串
print("Consumer released lock.")
# 模拟工作或等待生产者写入
time.sleep(0.5)
except FileNotFoundError:
print(f"Consumer error: Shared memory '{shm_name}' not found. Is the creator process running?")
except Exception as e:
print(f"Consumer error: {e}")
finally:
# 进程完成使用后,需要 dettach(关闭视图)
if 'shm' in locals() and shm:
shm.close()
print(f"Consumer (PID: {os.getpid()}) detached from shared memory.")
if __name__ == "__main__":
# 这部分代码在主进程中执行
shm_name = "my_python_shared_memory_demo" # 给共享内存起个名字
shm_size = 256 # 共享内存大小 (bytes)
shm = None
# 创建一个跨进程的锁对象,用于同步对共享内存的访问
lock = mp.Lock()
try:
# 1. 主进程创建共享内存段
# create=True 表示如果不存在则创建
shm = sm.SharedMemory(name=shm_name, create=True, size=shm_size)
print(f"Main process (PID: {os.getpid()}) created shared memory segment '{shm_name}' with size {shm_size} bytes.")
data = b"Hello from main process initially." # 写入一些初始数据
# 注意:在创建共享内存后,主进程也可以访问其 buf
# shm.buf[:len(data)] = data
# print(f"Main process wrote initial data: {data}")
# 2. 创建生产者和消费者进程,并将共享内存名字和锁传递给它们
producer_data = b"Data for shared memory communication!"
p = mp.Process(target=producer, args=(shm_name, producer_data, lock))
c = mp.Process(target=consumer, args=(shm_name, lock))
# 启动子进程
p.start()
c.start()
# 等待子进程结束
p.join()
c.join()
print("All processes finished.")
except FileExistsError:
print(f"Error: Shared memory segment '{shm_name}' already exists. Please run this script again after ensuring no other process is using it.")
except Exception as e:
print(f"An error occurred in main process: {e}")
finally:
# 3. 主进程在所有子进程使用完毕后,unlink 共享内存
# unlink=True 只有创建者才能执行,用于删除共享内存段
if shm:
shm.close() # 主进程也需要先 close
try:
shm.unlink()
print(f"Shared memory segment '{shm_name}' unlinked (removed).")
except FileNotFoundError:
print(f"Shared memory segment '{shm_name}' already unlinked.")
运行方式: 运行 python shm_example.py
。它会创建共享内存,启动生产者和消费者进程,它们会 attach 到共享内存进行读写,然后进程终止,主进程会 unlink 共享内存。
2. 消息传递 (Message Passing)
核心思想: 进程通过发送和接收格式化的消息来进行通信。数据在发送进程和接收进程之间复制。
工作原理: 操作系统提供发送消息 (send
) 和接收消息 (receive
) 的系统调用。发送进程调用 send
将消息放入一个由操作系统维护的队列或通道中,操作系统负责将消息传递给接收进程。接收进程调用 receive
从队列或通道中获取消息。
优点:
- 隔离性好: 进程不共享内存,数据通过操作系统复制传递,相对安全。
- 易于管理: 操作系统处理消息的缓冲、同步和排队,对程序员相对友好。
缺点:
- 速度较慢: 数据需要在用户空间和内核空间之间复制,比共享内存慢。
- 消息大小限制: 通常有消息的最大大小限制。
Python 举例 (使用 multiprocessing.Queue
):
multiprocessing.Queue
是 Python 中实现消息传递的一种简单且常用的方式,它实际上是基于管道和锁实现的。
mq_example.py
import multiprocessing as mp
import time
import os
# 生产者进程函数
def producer(queue):
message = "Hello from producer via message queue!"
print(f"Producer (PID: {os.getpid()}) sending message: '{message}'")
queue.put(message) # 将消息放入队列
print("Producer finished sending.")
# 消费者进程函数
def consumer(queue):
print(f"Consumer (PID: {os.getpid()}) waiting for message...")
message = queue.get() # 从队列获取消息 (如果队列为空会阻塞)
print(f"Consumer received message: '{message}'")
print("Consumer finished receiving.")
if __name__ == "__main__":
# 这部分代码在主进程中执行
# 创建一个跨进程的队列对象
message_queue = mp.Queue()
# 创建生产者和消费者进程,并将队列对象作为参数传递
p = mp.Process(target=producer, args=(message_queue,))
c = mp.Process(target=consumer, args=(message_queue,))
# 启动子进程
p.start()
c.start()
# 等待子进程结束
p.join()
c.join()
print("All processes finished.")
运行方式: 运行 python mq_example.py
。主进程创建队列和子进程,生产者将消息放入队列,消费者从队列取出消息。
3. 管道 (Pipes)
核心思想: 提供一个单向的字节流通信通道。
工作原理: 操作系统维护一个内核缓冲区,一端用于写入,另一端用于读取。写入的数据先进先出 (FIFO)。
优点:
- 简单易用: 特别是无名管道,非常适合在父子进程之间传递数据流。
- 自带同步: 管道满时写阻塞,管道空时读阻塞,实现了简单的流控制和同步。
缺点:
- 单向性: 数据只能在一个方向上传输。实现双向通信需要使用两个管道。
- 容量有限: 内核缓冲区大小有限制。
- 流式传输: 按字节流传输,没有消息边界概念(不像消息队列)。
分类:
-
无名管道 (Unnamed Pipes):
-
特点: 用于具有亲缘关系的进程之间(通常是父子进程或兄弟进程)。通过
pipe()
系统调用创建。只存在于内存中,没有文件系统实体。 -
Python 举例 (使用
subprocess
模拟 Shell 管道ls | grep
):
Python 的subprocess
模块是执行外部命令和处理其输入/输出的标准方式,它可以很方便地模拟 Shell 中的管道操作。unnamed_pipe_example.py
import subprocess import sys # 演示如何在 Python 中模拟 Shell 命令 ls -l | grep main try: print("Executing 'ls -l | grep main' using subprocess:") # 第一步:运行第一个命令 'ls -l' 并捕获其标准输出 # stdout=subprocess.PIPE 会创建一个管道,将子进程的标准输出连接到这个管道 ls_process = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE) # 第二步:运行第二个命令 'grep main' # stdin=ls_process.stdout 会将第一个进程的 stdout 作为当前进程的 stdin # text=True 方便处理文本,否则需要处理 bytes grep_process = subprocess.Popen(['grep', 'main'], stdin=ls_process.stdout, stdout=subprocess.PIPE, text=True) # 务必在父进程中关闭第一个进程的 stdout 文件句柄 # 这样当第一个进程退出时,管道的写端会被关闭,第二个进程会收到 EOF ls_process.stdout.close() # 第三步:等待第二个命令完成,并获取其标准输出和标准错误 stdout, stderr = grep_process.communicate() # 第四步:打印 grep 的输出结果 print("\n--- Output from grep ---") print(stdout) # 可选:等待第一个进程,确保它也已经结束 ls_process.wait() # 检查命令的退出状态 if ls_process.returncode != 0: print(f"Warning: 'ls -l' command exited with non-zero status code {ls_process.returncode}", file=sys.stderr) if grep_process.returncode != 0 and grep_process.returncode != 1: # grep exits 1 if no matches print(f"Warning: 'grep main' command exited with non-zero status code {grep_process.returncode}", file=sys.stderr) except FileNotFoundError: print("Error: Make sure 'ls' and 'grep' commands are available in your system's PATH.", file=sys.stderr) except Exception as e: print(f"An unexpected error occurred: {e}", file=sys.stderr)
运行方式: 运行
python unnamed_pipe_example.py
。它会执行ls -l
命令并将输出通过管道传递给grep main
命令进行过滤,最后打印过滤后的结果。
-
-
命名管道 (Named Pipes / FIFO):
-
特点: 用于无亲缘关系的进程之间。通过
mkfifo()
系统调用在文件系统中创建一个特殊的文件(类型为 p)。进程可以通过文件名打开 FIFO 进行读写,就像普通文件一样,但其行为是管道式的(数据读出后就消失)。 -
Python 举例 (需要运行两个独立的 Python 脚本):
fifo_writer.py
import os import time import sys FIFO_PATH = "/tmp/my_python_fifo" # FIFO 文件路径 def writer(): print(f"Writer process (PID: {os.getpid()})") try: # 创建 FIFO 文件 (如果不存在) if not os.path.exists(FIFO_PATH): os.mkfifo(FIFO_PATH, 0o666) # 0o666 是八进制权限 print(f"FIFO '{FIFO_PATH}' created.") else: print(f"FIFO '{FIFO_PATH}' already exists.") # 打开 FIFO 文件进行写入 # 注意:以写模式打开 FIFO 会阻塞,直到有进程以读模式打开它 print(f"Writer opening FIFO '{FIFO_PATH}' for writing...") # 使用 'w' 模式以文本模式写入,方便示例 with open(FIFO_PATH, 'w') as fifo: print("FIFO opened for writing. Starting to send messages.") for i in range(5): message = f"Message {i} from writer (PID: {os.getpid()})\n" fifo.write(message) fifo.flush() # 确保数据被写入到内核缓冲区 print(f"Writer sent: '{message.strip()}'") time.sleep(0.5) # 模拟发送间隔 print("Writer finished sending messages.") except FileExistsError: print(f"Writer error: FIFO '{FIFO_PATH}' already exists, cannot create. Check if another process is creating it.") except FileNotFoundError: print(f"Writer error: Directory for FIFO '{FIFO_PATH}' not found.") except Exception as e: print(f"Writer error: {e}", file=sys.stderr) finally: # 在某些场景下,你可能希望在写入器退出时删除 FIFO, # 但通常由一个管理进程或读取进程负责删除。 # 为了演示读取端能收到所有数据,这里不删除 pass # print(f"Writer finished.") # 重复打印,移除 if __name__ == "__main__": writer()
fifo_reader.py
import os import time import sys FIFO_PATH = "/tmp/my_python_fifo" # FIFO 文件路径 def reader(): print(f"Reader process (PID: {os.getpid()})") try: # 打开 FIFO 文件进行读取 # 注意:以读模式打开 FIFO 会阻塞,直到有进程以写模式打开它 print(f"Reader opening FIFO '{FIFO_PATH}' for reading...") # 使用 'r' 模式以文本模式读取 with open(FIFO_PATH, 'r') as fifo: print("FIFO opened for reading. Waiting for messages.") while True: line = fifo.readline() # 从 FIFO 读取一行 if not line: # 读取到 EOF (写入端已关闭) print("Reader detected writer closed the FIFO (EOF).") break print(f"Reader received: '{line.strip()}'") # time.sleep(0.1) # 模拟处理延迟 print("Reader finished reading.") except FileNotFoundError: print(f"Reader error: FIFO '{FIFO_PATH}' not found. Make sure the writer script is run first.", file=sys.stderr) except Exception as e: print(f"Reader error: {e}", file=sys.stderr) finally: # 在读取完毕后,通常由读取进程负责删除 FIFO 文件 # 只有当所有打开该 FIFO 的文件描述符都被关闭后,unlink 才会真正删除底层对象 if os.path.exists(FIFO_PATH): try: os.remove(FIFO_PATH) print(f"FIFO '{FIFO_PATH}' removed.") except OSError as e: print(f"Could not remove FIFO '{FIFO_PATH}': {e}", file=sys.stderr) if __name__ == "__main__": reader()
运行方式:
- 打开一个终端窗口,运行
python fifo_reader.py
。你会看到它打印“Reader opening FIFO … for reading…”然后阻塞住。 - 打开另一个终端窗口,运行
python fifo_writer.py
。它会创建 FIFO(如果不存在),然后打开它进行写入,发送几条消息,然后退出。 - 回到第一个终端窗口,你会看到
fifo_reader.py
收到了写入器发送的消息,当写入器退出关闭 FIFO 后,读取器会检测到 EOF 并退出,并删除 FIFO 文件。
- 打开一个终端窗口,运行
-
4. 信号 (Signals)
核心思想: 一种轻量级的进程间通信方式,用于通知进程发生了某个事件。
工作原理: 操作系统向目标进程发送一个信号(一个小的整数)。目标进程可以忽略信号、捕获信号并执行自定义的信号处理函数,或者执行信号的默认动作(通常是终止进程)。
优点:
- 简单快速: 传递的信息量小(只有信号类型),开销低。
- 异步: 可以在进程执行的任何时刻发送和接收。
缺点:
- 无法传递数据: 只能通知事件类型,不能传递具体的数据内容(某些特殊信号可以携带少量额外信息,但不是主要用途)。
- 处理限制: 有些信号无法被捕获或忽略(如
SIGKILL
)。
适用场景: 事件通知、异常处理、进程控制(如终止、暂停、继续)。
Python 举例 (使用 signal
模块):
Python 的 signal
模块提供了注册信号处理函数的功能。
signal_example.py
import signal # 导入信号模块
import os # 用于获取进程 PID
import time # 用于模拟工作和暂停
import sys # 用于优雅退出
# 信号处理函数
# signal.signal() 注册的函数需要接收两个参数: 信号编号 和 栈帧对象 (frame)
def signal_handler(signum, frame):
# 打印收到信号的信息
# signal.Signals(signum).name 可以将信号编号转换为信号名称 (Python 3.5+)
signal_name = signal.Signals(signum).name if hasattr(signal, 'Signals') else str(signum)
print(f"\n[PID {os.getpid()}] Received signal {signum} ({signal_name}).")
print("Performing cleanup...")
# 在这里执行清理操作,例如保存数据、关闭文件、释放资源
print("Cleanup finished. Exiting gracefully.")
# 调用 sys.exit() 可以确保 Python 解释器进行正常的退出流程
sys.exit(0)
def main():
print(f"Process PID: {os.getpid()}")
print("Registering signal handler for SIGINT (Interrupt Signal - typically Ctrl+C)...")
try:
# 注册 SIGINT 信号的处理函数为 signal_handler
# 当进程收到 SIGINT 信号时,操作系统会中断当前执行,转而执行 signal_handler 函数
signal.signal(signal.SIGINT, signal_handler)
print("Signal handler registered. Press Ctrl+C or use 'kill -INT [PID]' to send SIGINT.")
except ValueError as e:
print(f"Error registering signal handler: {e}")
print("This might happen in environments that do not support signals (like some IDEs).")
# 如果注册失败,可能无法通过 Ctrl+C 正常触发处理函数,进程可能会以默认方式终止
print("Process running. Looping indefinitely...")
try:
# 让进程保持运行,模拟一个长时间运行的任务
# signal.pause() # 暂停进程,直到收到一个信号 (可能会让程序更响应信号,但也完全阻塞)
while True:
print("Still running...")
time.sleep(2) # 模拟每隔2秒做一些工作
except KeyboardInterrupt:
# 在某些环境下 (比如没有正确注册信号处理函数),Ctrl+C 可能会触发 KeyboardInterrupt 异常
# 如果信号处理函数工作正常,这里通常不会被触发
print("\nKeyboardInterrupt exception caught.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during execution: {e}", file=sys.stderr)
sys.exit(1)
# 这行代码通常在信号处理函数调用 sys.exit() 后不会被执行到
print("Main function finished (this should not be reached).")
if __name__ == "__main__":
main()
运行方式:
- 运行
python signal_example.py
。它会打印出进程 PID 和注册信号处理器的信息,然后开始循环打印“Still running…”。 - 在同一个终端窗口中,按下
Ctrl+C
。你会看到程序没有立即终止,而是先打印出“Received signal … Performing cleanup…”等信息,然后才正常退出。 - 你也可以打开另一个终端,找到这个 Python 进程的 PID (使用
ps aux | grep signal_example.py
),然后使用命令kill -INT [PID]
来发送 SIGINT 信号,效果类似。使用kill -KILL [PID]
(发送 SIGKILL) 会强制终止进程,这个信号是无法被捕获或忽略的,进程会立即停止。
5. 套接字 (Sockets)
核心思想: 提供一种通用的通信机制,可以用于同一台机器上或不同机器上的进程通信。
工作原理: 套接字是通信的端点。它将进程与网络协议(如 TCP/IP)连接起来。进程通过套接字 API 进行通信,操作系统内核负责网络协议栈的处理、数据包的发送和接收。
优点:
- 通用性强: 既可以用于同一台机器上的进程通信 (
AF_UNIX
),也可以用于跨网络的进程通信 (AF_INET
,AF_INET6
)。 - 支持多种协议: 可以基于可靠的面向连接协议(TCP,
SOCK_STREAM
)或不可靠的无连接协议(UDP,SOCK_DGRAM
)。 - 全双工: 通常支持双向数据传输。
缺点:
- 相对复杂: API 函数较多,涉及网络概念。
- 开销相对较高: 数据需要在用户空间和内核空间之间复制,并通过网络协议栈处理。
适用场景: 网络应用(Web 服务器、客户端、文件传输)、同一机器上复杂进程间通信(如 X Window System)。
分类:
- 域套接字 (Domain Sockets / AF_UNIX): 用于同一台机器上的进程通信。数据通过文件系统路径标识的套接字文件进行传递。性能比网络套接字高,因为数据不需要经过网络协议栈。
- 网络套接字 (Network Sockets / AF_INET, AF_INET6): 用于不同机器之间的进程通信,使用 IP 地址和端口号标识通信端点。这是构建分布式系统的基础。
Python 举例 (使用 socket
模块实现简单的 TCP 客户端/服务器):
这需要运行两个独立的 Python 脚本,一个作为服务器,一个作为客户端。
socket_server.py
import socket
import threading # 使用线程来处理多个客户端连接 (可选,但更实用)
import sys
HOST = '127.0.0.1' # 服务器监听的 IP 地址 (这里使用本地回环地址)
PORT = 8080 # 服务器监听的端口号 (非特权端口 > 1023)
# 处理客户端连接的函数 (可以在单独的线程或进程中运行)
def handle_client(conn, addr):
print(f"[*] Accepted connection from {addr}")
try:
# 接收客户端发送的数据
# conn.recv(1024) 会读取最多 1024 字节的数据
data = conn.recv(1024)
if not data: # 如果没有数据,表示客户端可能关闭了连接
print(f"[*] Client {addr} closed connection.")
return
print(f"[*] Received from {addr}: {data.decode()}") # 将接收到的 bytes 解码为字符串
# 准备要发送的响应数据 (需要是 bytes)
response = b"Hello from server via socket!"
# 发送响应数据
# conn.sendall() 确保所有数据都被发送出去
conn.sendall(response)
print(f"[*] Sent '{response.decode()}' to {addr}")
except Exception as e:
print(f"[!] Error handling client {addr}: {e}", file=sys.stderr)
finally:
# 关闭与客户端的连接套接字
conn.close()
print(f"[*] Connection from {addr} closed.")
def server_main():
# 创建一个套接字对象
# socket.AF_INET 表示使用 IPv4 地址族
# socket.SOCK_STREAM 表示使用面向连接的 TCP 协议
# 使用 'with' 语句可以确保套接字在使用完毕后被正确关闭
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
print(f"[*] Server socket created.")
try:
# 绑定套接字到指定的 IP 地址和端口
server_socket.bind((HOST, PORT))
print(f"[*] Server bound to {HOST}:{PORT}")
# 开始监听连接请求
# listen() 的参数是允许的最大排队连接数
server_socket.listen(5) # 最多允许5个待处理的连接
print("[*] Server listening...")
# 主循环:持续接受客户端连接
while True:
try:
# 接受一个客户端连接
# server_socket.accept() 会阻塞,直到有客户端尝试连接
# 它返回一个新的套接字对象 (conn) 用于与客户端通信,以及客户端的地址 (addr)
conn, addr = server_socket.accept()
# 为了能够同时处理多个客户端,可以将每个连接交给一个新线程或进程来处理
# 这里使用线程作为简单示例
client_handler = threading.Thread(target=handle_client, args=(conn, addr))
client_handler.start()
# 在一个更复杂的服务器中,你可能需要管理这些线程或进程
except KeyboardInterrupt:
print("\n[*] Server received KeyboardInterrupt. Shutting down.")
break # 退出主循环
except Exception as e:
print(f"[!] Error accepting connection: {e}", file=sys.stderr)
# 发生错误时,可以选择继续监听或者退出,这里选择继续
except Exception as e:
print(f"[!] Server startup error: {e}", file=sys.stderr)
print("[*] Server main loop finished. Exiting.")
if __name__ == "__main__":
server_main()
socket_client.py
import socket
import sys
HOST = '127.0.0.1' # 服务器的 IP 地址或主机名
PORT = 8888 # 服务器监听的端口号
def client_main():
# 创建一个套接字对象
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
print(f"[*] Client socket created.")
try:
# 连接到服务器
# client_socket.connect() 会尝试与指定的地址和端口建立 TCP 连接
client_socket.connect((HOST, PORT))
print(f"[*] Connected to {HOST}:{PORT}")
# 准备要发送的数据 (需要是 bytes)
message = b"Hello from client via socket!"
# 发送数据
client_socket.sendall(message) # 使用 sendall 确保所有数据都发送
print(f"[*] Sent '{message.decode()}' to server.")
# 接收服务器发送的数据
# client_socket.recv(1024) 读取最多 1024 字节的响应
data = client_socket.recv(1024)
print(f"[*] Received from server: {data.decode()}") # 将接收到的 bytes 解码为字符串
except ConnectionRefusedError:
print(f"[!] Error: Connection refused. Is the server running on {HOST}:{PORT}?", file=sys.stderr)
except FileNotFoundError:
print(f"[!] Error: Host '{HOST}' not found or unreachable.", file=sys.stderr) # This might be raised for hostnames
except Exception as e:
print(f"[!] An unexpected error occurred: {e}", file=sys.stderr)
print("[*] Client finished. Connection closed.")
if __name__ == "__main__":
client_main()
运行方式:
- 打开第一个终端窗口,运行
python socket_server.py
。它会启动服务器并等待连接。 - 打开第二个终端窗口,运行
python socket_client.py
。客户端会连接到服务器,发送消息,接收响应,然后退出。 - 回到第一个终端窗口,你会看到服务器接收到连接,处理了客户端请求,然后客户端断开连接。服务器会继续监听下一个连接(如果使用了线程处理)。按下
Ctrl+C
可以优雅地关闭服务器。
IPC 方式对比总结
下表总结了上面介绍的几种 IPC 方式在 Python 中的常见实现以及它们的特点:
IPC 方式 | Python 实现示例 | 通信方式 | 数据传输速度 | 复杂性(对程序员) | 适用场景 | 亲缘关系要求 |
---|---|---|---|---|---|---|
共享内存 | multiprocessing.shared_memory | 直接内存读写 | 最快 | 高(需同步) | 大量数据传输、高性能 | 无特定要求 |
消息传递 | multiprocessing.Queue | 队列/通道 | 慢于共享内存 | 中等 | 结构化消息交换、解耦通信 | 无特定要求 |
无名管道 | subprocess.Popen(..., stdout/stdin=PIPE) | 单向字节流 | 中等 | 较低 | 父子/兄弟进程简单通信 | 有亲缘关系 |
命名管道 | os.mkfifo , open() | 单向字节流 (FIFO) | 中等 | 较低 | 无亲缘进程简单通信 | 无特定要求 |
信号 | signal 模块 | 事件通知 | 很快 | 较低(有限制) | 异步事件通知、异常、控制 | 无特定要求 |
套接字 | socket 模块 | 字节流或数据报 | 慢于管道 | 高 | 网络通信、复杂本地通信 | 无特定要求 |
结论
进程间通信是构建现代多任务和分布式系统的基础。不同的 IPC 机制各有优劣,适用于不同的场景。共享内存提供了最高的数据交换速度,但需要细致的同步管理;消息传递提供了良好的隔离性,但有数据复制开销;管道简单易用,适合流式数据传输;信号用于异步事件通知;套接字则提供了最强大的通用性,支持网络和本地通信。