在现代计算系统中,多进程环境已经成为标准配置。随着计算需求的增长和应用复杂性的提升,单一进程往往无法独立完成所有任务。为了提高系统的灵活性、性能和可靠性,多个进程之间的协作成为了必然的选择。这就引出了一个关键问题:如何高效、安全地实现进程间的数据交换与通信?这就是进程间通信(Inter-Process Communication,IPC)的核心问题。
进程间通信的重要性:
进程间通信是指在不同进程之间传递信息的机制。在多进程系统中,各个进程可能需要共享数据、协调工作或交换状态信息。例如,在一个Web服务器中,工作进程可能需要与管理进程通信,以获取配置或报告状态;在数据处理系统中,生产者进程与消费者进程需要交换数据以完成任务。这些通信需求促使了IPC机制的设计与实现。
有效的IPC机制不仅能够提升系统的性能和响应速度,还能确保数据的一致性和系统的稳定性。在某些情况下,IPC机制甚至可以成为系统架构的核心组成部分,例如在分布式系统或微服务架构中,进程间通信的效率直接影响到整个系统的性能。
本指南旨在深入探讨进程间通信的各种机制,从基础知识到实战应用,帮助读者全面理解IPC的工作原理,并掌握如何在不同场景下选择和应用最合适的IPC方法。本文将涵盖以下几个方面:
进程间通信是一种通常由操作系统(或操作系统)提供的机制。该机制的主要目的或目标是在多个进程之间提供通信。简而言之,互通允许一个进程让另一个进程知道某些事件已经发生。
定义:进程间通信用于在一个或多个进程(或程序)中的众多线程之间交换有用的信息。由于进程之间拥有独立的地址空间和资源,直接访问对方的数据是不可能的。因此,IPC机制提供了一种通过操作系统提供的接口来进行数据交换的方法。IPC机制不仅涉及数据传输,还包括进程间的同步与协调。
IPC 机制的选择:
管道是一种单向的数据通道,即数据通道中的数据一次只能向一个方向移动。这是一种半双工方法,为了实现全双工,需要另一根管道,形成一组双通道,以便能够在两个进程中发送和接收数据。通常,它使用标准方法进行输入和输出。这些管道用于所有类型的 POSIX 系统以及不同版本的Windows操作系统。
在Unix和类Unix系统中,管道通常用于父子进程之间或者通过fork
创建的进程之间进行通信,因为在一个进程中使用管道是没有意义的。管道有两种类型:匿名管道和命名管道(FIFO)。
管道(Pipe)可能是本地使用最广泛的 IPC 方法之一。管道(Pipe)实际上是使用一段内核内存实现的。系统调用始终创建一个管道和两个关联的文件说明,用于从管道读取和写入管道。
优点:
缺点:
管道的工作原理:
pipe()
系统调用来创建一个管道。这个调用会返回两个文件描述符,一个用于读操作,一个用于写操作。例如:
int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); }
pipefd[0]
用于读取数据,而pipefd[1]
用于写入数据。两个文件描述符形成了一个单向的数据流通道。fork()
之后将管道的读写文件描述符分别传递给子进程和父进程。区分匿名管道与命名管道:
mkfifo()
函数创建命名管道,并通过文件路径进行读写操作:mkfifo("/tmp/myfifo", 0666);代码示例:
// 匿名管道
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipefd[2];
pid_t pid;
char buffer[128];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
if ((pid = fork()) == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) { // 子进程
close(pipefd[0]); // 关闭读端
const char *message = "Hello from child";
write(pipefd[1], message, strlen(message) + 1);
close(pipefd[1]);
exit(EXIT_SUCCESS);
} else { // 父进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], buffer, sizeof(buffer));
printf("Received message: %s\n", buffer);
close(pipefd[0]);
}
return 0;
}
// 命名管道(FIFO)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *fifo = "/tmp/myfifo";
char buffer[128];
// 创建命名管道
if (mkfifo(fifo, 0666) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
pid_t pid;
if ((pid = fork()) == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) { // 子进程
int fd = open(fifo, O_WRONLY);
const char *message = "Hello from FIFO";
write(fd, message, strlen(message) + 1);
close(fd);
exit(EXIT_SUCCESS);
} else { // 父进程
int fd = open(fifo, O_RDONLY);
read(fd, buffer, sizeof(buffer));
printf("Received message: %s\n", buffer);
close(fd);
}
// 删除命名管道
unlink(fifo);
return 0;
}
应用场景:
管道常用于实现简单的父子进程间的数据传递,或在管道的另一端读取进程的标准输出。在Shell脚本中,管道被广泛用于将一个命令的输出传递给另一个命令作为输入。例如:
ls | grep "txt"
这个命令将ls
命令的输出传递给grep
命令进行过滤,使用管道实现了两个命令之间的数据传递。
消息队列 (Message Queue) 允许进程在两个进程之间以消息的形式交换数据。它允许进程通过相互发送消息来异步通信,其中消息存储在队列中,等待处理,并在处理后删除。
消息队列是在非共享内存环境中使用的缓冲区,其中任务通过相互传递消息而不是通过访问共享变量进行通信。任务共享一个公共缓冲池。消息队列是一个无界 FIFO 队列,可防止不同线程的并发访问。
定义:消息队列提供异步通信协议,消息的发送方和接收方不需要同时与消息队列进行交互。
简单的说,消息队列的工作原理类似于邮箱:多个进程可以向消息队列发送邮件,接受者可以从队列中取回邮件。
事件是异步的。当一个类将事件发送到另一个类时,它不会将其直接发送到目标反应类,而是将事件传递到操作系统消息队列。当目标类准备好处理事件时,它从消息队列的头部检索该事件。可以改用触发的操作来传递同步事件。
许多任务可以将消息写入队列,但一次只能有一个任务从队列中读取消息。读取器在消息队列上等待,直到有消息要处理。消息可以是任意大小的。
消息队列是一种软件组件,可在微服务和无服务器基础架构中实现应用程序到应用程序的通信。消息使用异步通信协议进行传输和接收,该协议对消息进行排队,不需要收件人的立即响应。
优点:
缺点:
在程序中使用四个重要功能来实现使用消息队列的 IPC:
msgget(key_t key, int msgflg)
: 用来创建或打开一个消息队列。第一个参数是命名系统中消息队列的键,使用ftok
创建;第二个参数用于为消息队列分配权限。msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg)
: 用于发送消息到消息队列。最后一个参数控制在消息队列已满或达到排队消息的系统限制时会发生什么情况。msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg)
: 用于从消息队列接收消息。msgctl(int msqid, int command, struct msqid_ds *buf)
: 用于控制消息队列,例如修改权限、获取消息队列信息等等。第二个参数可以具有IPC_STAT
、IPC_SET
、IPC_RMID
中的一个。使用消息队列执行 IPC 的步骤:
msgget()
打开一个现有队列。msgsnd()
添加到队列末尾。每条消息都有一个正的长整型字段、一个非负长度和实际的数据字节(对应于长度),所有这些都在将消息添加到队列时指定给 msgsnd()
。msgrcv()
从队列中获取。我们不必按先进先出的顺序获取消息。相反,可以根据消息的类型字段获取消息。msgctl()
执行控制操作。示例,写消息队列:
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#define MAX_TEXT 512 //maximum length of the message that can be sent allowed
struct my_msg{
long int msg_type;
char some_text[MAX_TEXT];
};
int main()
{
int running=1;
int msgid;
struct my_msg some_data;
char buffer[50]; //array to store user input
msgid=msgget((key_t)14534,0666|IPC_CREAT);
if (msgid == -1) {
printf("Error in creating queue\n");
exit(0);
}
while(running) {
printf("Enter some text:\n");
fgets(buffer,50,stdin);
some_data.msg_type=1;
strcpy(some_data.some_text,buffer);
if(msgsnd(msgid,(void *)&some_data, MAX_TEXT,0)==-1) {
printf("Msg not sent\n");
}
if(strncmp(buffer,"end",3)==0) {
running=0;
}
}
}
读消息队列:
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
struct my_msg{
long int msg_type;
char some_text[BUFSIZ];
};
int main()
{
int running=1;
int msgid;
struct my_msg some_data;
long int msg_to_rec=0;
msgid=msgget((key_t)12345,0666|IPC_CREAT);
while(running) {
msgrcv(msgid,(void *)&some_data,BUFSIZ,msg_to_rec,0);
printf("Data received: %s\n",some_data.some_text);
if(strncmp(some_data.some_text,"end",3)==0)
running=0;
}
msgctl(msgid,IPC_RMID,0);
}
共享内存是两个或多个进程之间共享的内存,允许多个进程访问和共享相同内存块。每个进程都有自己的地址空间;如果任何进程想要将某些信息从其自己的地址空间与其他进程进行通信,则只能使用 IPC(进程间通信)共享内存技术。
共享内存是最快的进程间通信机制。操作系统将多个进程的地址空间中的内存段映射到该内存段中读取和写入,而无需调用操作系统函数。
对于交换大量数据的应用程序,共享内存远远优于消息队列技术,因为IPC消息队列需要对每次数据交换进行系统调用。
通常,使用管道或命名管道执行相互关联的进程通信。不相关的进程通信可以使用命名管道或通过共享内存和消息队列等。但是,管道、FIFO和消息队列的问题在于两个进程之间的信息交换要经过内核,总共需要 4 个数据副本(2 个读取和 2 个写入)。因此,共享内存提供了一种方法,让两个或多个进程共享一个内存段。使用共享内存时,数据仅复制两次,从输入文件复制到共享内存,从共享内存复制到输出文件。
在两个或多个进程中建立共享内存区域时,无法保证这些区域将放置在相同的基址上,当需要同步时,可以使用信号量。
有两个函数 shmget()
和 shmat()
用于使用共享内存的 IPC。shmget()
函数用于创建共享内存段,而 shmat()
函数用于将共享段与进程的地址空间附加。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
IPC使用共享内存如何工作?
进程使用 shmget()
创建共享内存段。共享内存段的原始所有者可以使用 shmctl()
将所有权分配给另一个用户。它还可以撤销此分配。具有适当权限的其他进程可以使用 shmctl()
在共享内存段上执行各种控制功能。
创建后,可以使用 shmat()
将共享段附加到进程地址空间。可以使用 shmdt()
将其分离。附加进程必须具有 shmat()
的适当权限。附加后,进程可以读取或写入段,因为附加操作中请求的权限允许。共享段可以通过同一进程多次附加。
共享内存段由具有唯一 ID 的控制结构描述,该 ID 指向物理内存区域。段的标识符称为 shmid
。共享内存段控制结构和原型的结构定义可以在 <sys/shm.h>
中找到。
使用示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/shm.h>
#include<string.h>
int main()
{
int i;
void *shared_memory;
char buff[100];
int shmid;
shmid=shmget((key_t)2345, 1024, 0666|IPC_CREAT);
//creates shared memory segment with key 2345, having size 1024 bytes.
printf("Key of shared memory is %d\n",shmid);
shared_memory=shmat(shmid,NULL,0);
//process attached to shared memory segment
printf("Process attached at %p\n",shared_memory);
//this prints the address where the segment is attached with this process
printf("Enter some data to write to shared memory\n");
read(0,buff,100); //get some input from user
strcpy(shared_memory,buff); //data written to shared memory
printf("You wrote : %s\n",(char *)shared_memory);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/shm.h>
#include<string.h>
int main()
{
int i;
void *shared_memory;
char buff[100];
int shmid;
shmid=shmget((key_t)2345, 1024, 0666);
printf("Key of shared memory is %d\n",shmid);
shared_memory=shmat(shmid,NULL,0); //process attached to shared memory segment
printf("Process attached at %p\n",shared_memory);
printf("Data read from shared memory is : %s\n",(char *)shared_memory);
}
在操作系统和进程间通信中,信号(Signals)是一种重要的机制,用于通知进程发生了某种事件或异常。
信号是一种异步通知机制,用于在软件层面向进程发送通知。它通常用于以下几种情况:
每种信号都由一个唯一的整数编号表示,这些编号通常以宏的形式定义在 <signal.h>
头文件中。一些常见的信号包括:
信号的发送与处理:
kill(pid, sig)
向指定的进程 pid
发送信号 sig
。signal(sig, handler)
或 sigaction(sig, &act, &oldact)
函数来指定信号处理函数。void handler(int sig)
这样的声明。信号处理的注意事项:
sigprocmask()
函数来屏蔽(阻止)或解除屏蔽特定的信号,以控制在什么时候接收某些信号。示例,使用 signal()
函数来捕获并处理 SIGINT
信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("Caught SIGINT, exiting...");
exit(0); // 或者执行一些清理工作后退出
}
int main() {
signal(SIGINT, sigint_handler);
printf("Waiting for SIGINT (Ctrl+C)...");
while (1) {
sleep(1); // 让程序持续运行
}
return 0;
}
信号虽然主要用于通知事件和处理异常,但也可以用于简单的进程间通信。
发送进程 (sender.c
)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#define SIG_CUSTOM SIGUSR1 // 自定义信号
void error_handling(char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
error_handling("Fork error");
} else if (pid == 0) {
// 子进程(接收进程)
execl("./receiver", "receiver", NULL); // 执行接收进程程序
error_handling("Exec error");
} else {
// 父进程(发送进程)
sleep(1); // 等待子进程初始化完毕
printf("Sending signal to child process (PID: %d)...\
", pid);
if (kill(pid, SIG_CUSTOM) == -1) {
error_handling("Kill error");
}
printf("Signal sent.\
");
}
return 0;
}
接收进程 (receiver.c
)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#define SIG_CUSTOM SIGUSR1 // 自定义信号
void sig_handler(int sig) {
if (sig == SIG_CUSTOM) {
printf("Received custom signal SIGUSR1.\
");
}
}
int main() {
struct sigaction act;
act.sa_handler = sig_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 设置自定义信号的处理函数
if (sigaction(SIG_CUSTOM, &act, NULL) == -1) {
perror("Sigaction error");
exit(EXIT_FAILURE);
}
printf("Waiting for signal...\
");
while (1) {
sleep(1); // 让程序持续运行
}
return 0;
}
编译和运行:
gcc sender.c -o sender
gcc receiver.c -o receiver
然后,分别在两个终端窗口中运行编译后的可执行文件:
./receiver
./sender
套接字(Socket)用于在不同主机或同一主机的不同进程之间进行通信。它是网络编程中最常用的一种方式,允许进程通过网络发送和接收数据。
套接字的基本概念:
本地套接字(Local Socket,也称为 Unix 域套接字)和网络套接字(Network Socket)是两种不同的套接字类型,它们主要在使用场景、实现方式和特性上有所区别。
本地套接字(Local Socket):
/tmp
目录或者系统指定的临时目录下。本地套接字使用文件系统的权限机制来控制访问权限。/tmp/mysocket
。网络套接字(Network Socket):
套接字主要可以根据使用的协议来分类,常见的包括:
SOCK_STREAM
,基于 TCP 协议。它提供面向连接的、可靠的数据传输,确保数据按顺序到达目的地,且不丢失、不重复。SOCK_DGRAM
,基于 UDP 协议。它提供无连接的数据传输服务,数据包可能会丢失或重复,不保证数据的顺序。在 UNIX 和类 UNIX 系统中,套接字通常使用以下系统调用进行创建、绑定、监听、连接、发送和接收数据等操作:
socket()
: 创建套接字,返回一个文件描述符。bind()
: 将套接字绑定到一个地址,如 IP 地址和端口号。listen()
: 仅用于流套接字,将套接字标记为被动套接字,等待连接请求。accept()
: 仅用于流套接字,接受客户端的连接请求,返回一个新的文件描述符用于与客户端通信。connect()
: 仅用于流套接字,连接到远程套接字(客户端)。send()
和 recv()
: 发送和接收数据。sendto()
和 recvfrom()
: 用于数据报套接字,发送和接收数据报。示例代码,使用套接字进行基本的客户端-服务器通信:
服务器端 (server.c
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
const char *hello = "Hello from server";
// 创建 TCP 套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 将套接字绑定到指定地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
// 监听传入的连接请求
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
// 接受连接,并处理数据
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
exit(EXIT_FAILURE);
}
// 从客户端接收数据,并发送响应
int valread = read(new_socket, buffer, 1024);
printf("Received message from client: %s
", buffer);
send(new_socket, hello, strlen(hello), 0);
printf("Hello message sent
");
return 0;
}
客户端 (client.c
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[1024] = {0};
// 创建 TCP 套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IPv4 地址从文本转换为二进制格式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
exit(EXIT_FAILURE);
}
// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
exit(EXIT_FAILURE);
}
// 发送消息给服务器
send(sock, hello, strlen(hello), 0);
printf("Hello message sent to server
");
// 接收服务器的响应
int valread = read(sock, buffer, 1024);
printf("Message from server: %s", buffer);
return 0;
}
内存映射文件(Memory-Mapped Files)是一种高效的文件访问方式,它允许将一个文件的内容直接映射到进程的虚拟内存空间中,使得文件的读取和写入可以像访问内存一样高效。
工作机制:
mmap()
系统调用,请求将一个文件的一部分或整个内容映射到自己的虚拟地址空间。mmap()
函数的参数包括文件描述符、映射长度、权限(读、写、执行)、映射标志等。msync()
函数来同步内存映射区域的修改到文件中,或者在不同进程间共享修改后的数据。munmap()
函数释放映射,操作系统会取消虚拟地址空间中的映射关系,并根据需要更新文件的修改到磁盘上。关键特点:
应用场景:
进程间通信(IPC)作为现代计算系统中重要的组成部分,扮演着确保多进程协作顺利进行的关键角色。本文从IPC的基本概念出发,深入探讨了多种经典和高级IPC机制的原理、优缺点及实际应用场景。
IPC是在多进程环境中实现进程间通信的关键技术,涉及数据共享、任务协调和状态更新等多个方面。有效的IPC机制可以提高系统性能和响应速度,确保数据的一致性和安全性,是现代计算系统中不可或缺的部分。
学习书籍:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。