进程间通信的七种方法实战演示!值得收藏!
文章目录
- 前言
- 一、管道(Pipes)
- 管道示例代码:
- 管道示例流程解读:
- 管道示例运行效果:
- 二、消息队列(Message Queues)
- 消息队列示例代码
- 消息队列示例流程解读:
- 消息队列示例执行效果
- 三、共享内存(Shared Memory)
- 共享内存示例代码
- 共享内存示例流程解读:
- 共享内存示例执行效果
- 四、 信号(Signals)
- 信号示例代码:
- 信号示例流程解读:
- 信号示例执行效果
- 五、套接字(Sockets)
- 套接字示例代码:
- 套接字示例流程解读:
- 套接字示例执行结果
- 六、信号量(Semaphores)
- 信号量示例代码:
- 信号量示例流程解读:
- 信号量示例执行结果
- 七、文件映射(Memory-Mapped Files)
- 文件映射示例代码:
- 文件映射示例流程解读:
- 文件映射示例执行结果
- 总结
前言
线程和进程间的通讯(Inter-Process Communication, IPC)是操作系统中的一个重要概念,用于实现不同进程或同一进程中的不同线程之间的数据交换和协调。
但是这些概念太多了,容易搞混了,所有特地写了这篇博客来记录这些方式的使用实例,建立大家收藏起来,将来需要复习的时候,再来看一看,能够快速的带你复习一遍!
以下是几个常见的进程间通信的方式:管道、消息队列、共享内存、信号、套接字、信号量、文件映射
提示:以下是本篇文章正文内容,下面案例可供参考
一、管道(Pipes)
管道是一种半双工的通信方式,数据只能单向流动。管道分为两种类型:
匿名管道:仅限于具有亲缘关系的进程之间使用,通常用于父子进程之间的通信。
命名管道(FIFO):可以在任意两个进程之间使用,通过文件系统中的一个特殊文件来标识。
管道示例代码:
以下是一个简单的 C 语言示例,演示如何使用管道实现父子进程间的通信。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h> // 用于 memset
int main() {
int pipefd[2];
pid_t pid;
char buf[100];
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 子进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello from child", 18);
close(pipefd[1]);
} else { // 父进程
close(pipefd[1]); // 关闭写端
memset(buf, 0, sizeof(buf)); // 清空缓冲区
ssize_t bytes_read = read(pipefd[0], buf, sizeof(buf) - 1); // 读取数据,留一个字节给终止符
if (bytes_read > 0) {
printf("Received: %s\n", buf);
} else {
perror("read");
}
close(pipefd[0]);
wait(NULL);
}
return 0;
}
管道示例流程解读:
1、创建管道:使用 pipe 函数创建一个管道,该函数返回两个文件描述符,分别用于读取和写入。
2、创建子进程:使用 fork 函数创建一个子进程。fork 返回子进程的 PID,如果返回值为 -1 表示出错,0 表示当前是子进程,其他值表示当前是父进程。
3、关闭不必要的管道端:
在父进程中,关闭管道的端(pipefd[1]),因为子进程只读取数据。
在子进程中,关闭管道的读端(pipefd[0]),因为父进程只写入数据。
4、数据传输:
子进程使用 write 函数向管道中写入数据。
父进程使用 read 函数从管道中读取数据。
5、关闭管道:在完成数据传输后,关闭管道的相应端。
6、等待子进程结束:父进程使用 wait 函数等待子进程结束。
管道示例运行效果:
二、消息队列(Message Queues)
消息队列是一种更高级的通信方式,允许进程发送和接收消息。消息队列可以存储多个消息,并且可以设置消息的优先级。
消息队列示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_MSG_SIZE 256
// 定义消息结构体
struct msg_buffer {
long msg_type;
char msg_text[MAX_MSG_SIZE];
};
int main() {
key_t key;
int msgid;
struct msg_buffer message;
pid_t cpid;
// 生成一个唯一的键值
key = ftok("/etc/passwd", 65); // 使用现有的文件 /etc/passwd
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建消息队列
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
// 创建子进程
cpid = fork();
if (cpid == -1) { // fork 失败
perror("fork");
exit(EXIT_FAILURE);
} else if (cpid == 0) { // 子进程
// 子进程接收消息
if (msgrcv(msgid, &message, sizeof(message.msg_text), 1, 0) == -1) {
perror("msgrcv");
exit(EXIT_FAILURE);
}
printf("Child process received: %s\n", message.msg_text);
// 子进程发送消息
message.msg_type = 2;
strcpy(message.msg_text, "Hello from child");
if (msgsnd(msgid, &message, sizeof(message.msg_text), 0) == -1) {
perror("msgsnd");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
} else { // 父进程
// 父进程发送消息
message.msg_type = 1;
strcpy(message.msg_text, "Hello from parent");
if (msgsnd(msgid, &message, sizeof(message.msg_text), 0) == -1) {
perror("msgsnd");
exit(EXIT_FAILURE);
}
// 父进程接收消息
if (msgrcv(msgid, &message, sizeof(message.msg_text), 2, 0) == -1) {
perror("msgrcv");
exit(EXIT_FAILURE);
}
printf("Parent process received: %s\n", message.msg_text);
// 等待子进程结束
wait(NULL);
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
exit(EXIT_FAILURE);
}
}
return 0;
}
消息队列示例流程解读:
1、生成唯一键值:使用 ftok 函数生成一个唯一的键值,这个键值用于创建消息队列。这里需要指定一个文件
2、创建消息队列:使用 msgget 函数创建一个消息队列。0666 | IPC_CREAT 表示创建一个新的消息队列,并设置权限。
3、创建子进程:使用 fork 函数创建一个子进程。
4、父进程发送消息:
设置消息类型为 1。
使用 msgsnd 函数将消息发送到消息队列。
5、子进程接收消息:
使用 msgrcv 函数从消息队列中接收类型为 1 的消息。
打印接收到的消息。
设置消息类型为 2。
使用 msgsnd 函数将消息发送回父进程。
6、父进程接收子进程的消息:
使用 msgrcv 函数从消息队列中接收类型为 2 的消息。
打印接收到的消息。
7、删除消息队列:使用 msgctl 函数删除消息队列。
消息队列示例执行效果
三、共享内存(Shared Memory)
共享内存允许多个进程共享同一块内存区域,是最快速的进程间通信方式。但是需要同步机制(如信号量)来防止竞态条件。
共享内存示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SHM_SIZE 1024 // 共享内存大小
int main() {
key_t key;
int shmid;
char *shm;
pid_t cpid;
// 生成一个唯一的键值
key = ftok("progfile", 65);
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建共享内存段
shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 将共享内存段连接到进程地址空间
shm = shmat(shmid, NULL, 0);
if (shm == (char *) -1) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 创建子进程
cpid = fork();
if (cpid == -1) { // fork 失败
perror("fork");
exit(EXIT_FAILURE);
} else if (cpid == 0) { // 子进程
// 子进程读取共享内存中的数据
printf("Child process reads: %s\n", shm);
// 子进程写入共享内存
strcpy(shm, "Hello from child");
// 子进程断开共享内存
if (shmdt(shm) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
} else { // 父进程
// 父进程写入共享内存
strcpy(shm, "Hello from parent");
// 父进程等待子进程结束
wait(NULL);
// 父进程读取共享内存中的数据
printf("Parent process reads: %s\n", shm);
// 父进程断开共享内存
if (shmdt(shm) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(EXIT_FAILURE);
}
}
return 0;
}
共享内存示例流程解读:
1、生成唯一键值:使用 ftok 函数生成一个唯一的键值,这个键值用于创建共享内存段。
2、创建共享内存段:使用 shmget 函数创建一个共享内存段。0666 | IPC_CREAT 表示创建一个新的共享内存段,并设置权限。
3、将共享内存段连接到进程地址空间:使用 shmat 函数将共享内存段连接到进程的地址空间。
4、创建子进程:使用 fork 函数创建一个子进程。
5、父进程写入共享内存:
父进程将消息 “Hello from parent” 写入共享内存。
6、子进程读取和写入共享内存:
子进程读取共享内存中的消息并打印。
子进程将消息 “Hello from child” 写入共享内存。
7、父进程读取子进程写入的消息:
父进程等待子进程结束。
父进程读取共享内存中的消息并打印。
8、断开共享内存:使用 shmdt 函数断开共享内存段。
9、删除共享内存段:使用 shmctl 函数删除共享内存段。
共享内存示例执行效果
四、 信号(Signals)
信号是一种异步通信方式,用于通知进程发生了某些事件。信号可以由操作系统或进程自身发送。
信号示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
// 信号处理函数
void signal_handler(int signum) {
if (signum == SIGUSR1) {
printf("Received SIGUSR1 signal\n");
} else if (signum == SIGUSR2) {
printf("Received SIGUSR2 signal\n");
}
}
int main() {
pid_t cpid;
struct sigaction sa;
// 设置信号处理函数
sa.sa_handler = signal_handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction SIGUSR1");
exit(EXIT_FAILURE);
}
if (sigaction(SIGUSR2, &sa, NULL) == -1) {
perror("sigaction SIGUSR2");
exit(EXIT_FAILURE);
}
// 创建子进程
cpid = fork();
if (cpid == -1) { // fork 失败
perror("fork");
exit(EXIT_FAILURE);
} else if (cpid == 0) { // 子进程
// 子进程等待一段时间,让父进程先发送信号
sleep(1);
// 子进程发送 SIGUSR1 信号给父进程
kill(getppid(), SIGUSR1);
// 子进程等待一段时间,让父进程处理信号
sleep(1);
// 子进程发送 SIGUSR2 信号给父进程
kill(getppid(), SIGUSR2);
exit(EXIT_SUCCESS);
} else { // 父进程
// 父进程发送 SIGUSR1 信号给子进程
kill(cpid, SIGUSR1);
// 父进程等待一段时间,让子进程处理信号
sleep(1);
// 父进程发送 SIGUSR2 信号给子进程
kill(cpid, SIGUSR2);
// 父进程等待子进程结束
wait(NULL);
}
return 0;
}
信号示例流程解读:
设置信号处理函数:
使用 sigaction 函数设置信号处理函数 signal_handler,处理 SIGUSR1 和 SIGUSR2 信号。
创建子进程:
使用 fork 函数创建一个子进程。
父进程发送信号:
父进程使用 kill 函数发送 SIGUSR1 信号给子进程。
父进程等待一段时间,让子进程处理信号。
父进程再次使用 kill 函数发送 SIGUSR2 信号给子进程。
子进程发送信号:
子进程等待一段时间,让父进程先发送信号。
子进程使用 kill 函数发送 SIGUSR1 信号给父进程。
子进程等待一段时间,让父进程处理信号。
子进程再次使用 kill 函数发送 SIGUSR2 信号给父进程。
父进程等待子进程结束:
使用 wait 函数等待子进程结束。
信号示例执行效果
五、套接字(Sockets)
套接字是一种网络通信方式,允许不同主机上的进程进行通信。套接字可以用于本地进程间通信(通过Unix域套接字)和远程进程间通信(通过Internet域套接字)。
套接字示例代码:
以下是一个使用套接字在父进程和子进程之间进行通信的示例。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket, valread;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char *hello = "Hello from server";
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket 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");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(server_fd, 3) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
// 创建子进程
pid_t cpid = fork();
if (cpid == -1) { // fork 失败
perror("fork");
close(server_fd);
exit(EXIT_FAILURE);
} else if (cpid == 0) { // 子进程
// 子进程作为客户端
struct sockaddr_in serv_addr;
int sock = 0;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); // 显式指定服务器 IP 地址
// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect");
close(sock);
exit(EXIT_FAILURE);
}
// 发送消息
send(sock, "Hello from client", strlen("Hello from client"), 0);
// 接收消息
valread = read(sock, buffer, BUFFER_SIZE);
printf("Message received from server: %s\n", buffer);
// 关闭套接字
close(sock);
exit(EXIT_SUCCESS);
} else { // 父进程
// 父进程作为服务器
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
close(server_fd);
exit(EXIT_FAILURE);
}
// 接收消息
valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Message received from client: %s\n", buffer);
// 发送消息
send(new_socket, hello, strlen(hello), 0);
// 关闭套接字
close(new_socket);
close(server_fd);
// 等待子进程结束
wait(NULL);
}
return 0;
}
套接字示例流程解读:
1、创建套接字:父进程创建一个 TCP 套接字。
2、绑定套接字:父进程将套接字绑定到本地地址和端口。
3、监听套接字:父进程将套接字设置为监听状态,等待客户端连接。
4、创建子进程:父进程创建一个子进程。
5、子进程作为客户端:
子进程创建一个新的 TCP 套接字。
子进程连接到父进程的服务器。
子进程发送消息 “Hello from client” 到服务器。
子进程接收服务器的消息并打印。
子进程关闭套接字并退出。
6、父进程作为服务器:
父进程接受客户端的连接请求。
父进程接收客户端的消息并打印。
父进程发送消息 “Hello from server” 到客户端。
父进程关闭套接字并等待子进程结束。
套接字示例执行结果
六、信号量(Semaphores)
信号量是一种用于控制对共享资源访问的同步机制。它可以用于进程间的同步和互斥。
信号量示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define SEM_NAME "/my_semaphore"
#define SHARED_FILE "shared_file.txt"
int main() {
sem_t *sem;
int fd;
pid_t pid;
// 创建信号量
sem = sem_open(SEM_NAME, O_CREAT, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}
// 创建共享文件
fd = open(SHARED_FILE, O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("open");
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
close(fd);
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
} else if (pid == 0) { // 子进程
// 子进程写入共享文件
fd = open(SHARED_FILE, O_WRONLY);
if (fd == -1) {
perror("open");
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
// 等待信号量
if (sem_wait(sem) == -1) {
perror("sem_wait");
close(fd);
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
// 写入消息
const char *msg = "Hello from child";
write(fd, msg, strlen(msg));
printf("Child: Wrote message to shared file\n");
// 释放信号量
if (sem_post(sem) == -1) {
perror("sem_post");
close(fd);
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
close(fd);
exit(EXIT_SUCCESS);
} else { // 父进程
// 父进程读取共享文件
fd = open(SHARED_FILE, O_RDONLY);
if (fd == -1) {
perror("open");
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
// 等待信号量
if (sem_wait(sem) == -1) {
perror("sem_wait");
close(fd);
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
// 重置文件偏移量到文件开头
lseek(fd, 0, SEEK_SET);
// 读取消息
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("read");
close(fd);
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
buffer[bytes_read] = '\0';
printf("Parent: Read message from shared file: %s\n", buffer);
// 释放信号量
if (sem_post(sem) == -1) {
perror("sem_post");
close(fd);
sem_close(sem);
sem_unlink(SEM_NAME);
exit(EXIT_FAILURE);
}
close(fd);
// 等待子进程结束
wait(NULL);
// 关闭信号量
sem_close(sem);
sem_unlink(SEM_NAME);
}
return 0;
}
信号量示例流程解读:
1、创建信号量:使用 sem_open 函数创建一个命名信号量 SEM_NAME,初始值为 1。
2、创建共享文件:使用 open 函数创建一个共享文件 SHARED_FILE,如果文件已存在则打开它。
3、创建子进程:使用 fork 函数创建一个子进程。
4、子进程写入共享文件:
子进程打开共享文件,准备写入。
使用 sem_wait 函数等待信号量,确保只有一个进程可以访问共享文件。
写入消息 “Hello from child” 到共享文件。
使用 sem_post 函数释放信号量。
关闭文件并退出子进程。
5、父进程读取共享文件:
父进程打开共享文件,准备读取。
使用 sem_wait 函数等待信号量,确保只有一个进程可以访问共享文件。
重置文件偏移量到文件开头。
读取共享文件中的消息并打印。
使用 sem_post 函数释放信号量。
关闭文件,等待子进程结束,关闭信号量并删除信号量。
信号量示例执行结果
七、文件映射(Memory-Mapped Files)
文件映射允许将文件或文件的一部分映射到内存中,从而允许多个进程共享同一块内存区域。这在处理大文件时特别有用。
文件映射示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define SHARED_FILE "shared_file.txt"
#define MMAP_SIZE 1024
int main() {
int fd;
pid_t pid;
char *mapped_memory;
// 创建共享文件
fd = open(SHARED_FILE, O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 设置文件大小
if (ftruncate(fd, MMAP_SIZE) == -1) {
perror("ftruncate");
close(fd);
exit(EXIT_FAILURE);
}
// 映射文件到内存
mapped_memory = mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
munmap(mapped_memory, MMAP_SIZE);
close(fd);
exit(EXIT_FAILURE);
} else if (pid == 0) { // 子进程
// 子进程写入共享内存
const char *msg = "Hello from child";
strcpy(mapped_memory, msg);
printf("Child: Wrote message to shared memory: %s\n", mapped_memory);
// 等待一段时间,让父进程有时间读取
sleep(1);
exit(EXIT_SUCCESS);
} else { // 父进程
// 父进程读取共享内存
sleep(1); // 等待子进程写入
printf("Parent: Read message from shared memory: %s\n", mapped_memory);
// 等待子进程结束
wait(NULL);
// 取消内存映射
if (munmap(mapped_memory, MMAP_SIZE) == -1) {
perror("munmap");
close(fd);
exit(EXIT_FAILURE);
}
// 关闭文件
close(fd);
// 删除共享文件
unlink(SHARED_FILE);
}
return 0;
}
文件映射示例流程解读:
1、创建共享文件:使用 open 和 ftruncate 函数创建并设置共享文件的大小。
2、映射文件到内存:使用 mmap 函数将文件映射到内存区域。
3、创建子进程:使用 fork 函数创建一个子进程。
4、子进程写入共享内存:子进程将消息写入共享内存区域,并打印写入的消息。
5、父进程读取共享内存:父进程读取共享内存区域中的消息并打印,等待子进程结束,取消内存映射,关闭文件并删除共享文件。
文件映射示例执行结果
总结
管道:适用于父子进程间的简单通信。
消息队列:适用于需要发送和接收消息的场景。
共享内存:适用于需要高效共享大量数据的场景。
信号:适用于异步事件的通知。
套接字:适用于网络通信和本地进程间通信。
信号量:适用于进程间的同步和互斥。
文件映射:适用于处理大文件和共享内存。