Linux C语言进程间通信机制对比
2023-07-094.3k 阅读
管道(Pipe)
匿名管道(Anonymous Pipe)
- 原理
- 匿名管道是一种半双工的通信方式,数据只能单向流动,且只能在具有亲缘关系(通常是父子进程)的进程间使用。它在内核中创建了一个缓冲区,就像一个先进先出(FIFO)的队列。一个进程写入管道的数据,会被另一个进程从管道中读出。
- 匿名管道基于文件描述符来实现通信。当创建匿名管道时,内核会创建两个文件描述符,一个用于读(通常是
fd[0]
),另一个用于写(通常是fd[1]
)。
- 代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 256
int main() {
int pipe_fds[2];
pid_t child_pid;
char buffer[BUFFER_SIZE];
// 创建管道
if (pipe(pipe_fds) == -1) {
perror("pipe creation failed");
return 1;
}
// 创建子进程
child_pid = fork();
if (child_pid == -1) {
perror("fork failed");
return 1;
} else if (child_pid == 0) {
// 子进程关闭写端
close(pipe_fds[1]);
// 从管道读数据
ssize_t bytes_read = read(pipe_fds[0], buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read failed");
return 1;
}
buffer[bytes_read] = '\0';
printf("Child process received: %s\n", buffer);
// 关闭读端
close(pipe_fds[0]);
} else {
// 父进程关闭读端
close(pipe_fds[0]);
// 向管道写数据
const char* message = "Hello from parent!";
ssize_t bytes_written = write(pipe_fds[1], message, strlen(message));
if (bytes_written == -1) {
perror("write failed");
return 1;
}
// 关闭写端
close(pipe_fds[1]);
// 等待子进程结束
wait(NULL);
}
return 0;
}
- 特点
- 简单易用:对于具有亲缘关系的进程间通信,匿名管道提供了一种简单直接的方式。
- 半双工限制:数据只能单向流动,如果需要双向通信,需要创建两个管道。
- 生命周期短:管道的生命周期与创建它的进程相关,当所有使用管道的进程关闭时,管道自动消失。
命名管道(Named Pipe,FIFO)
- 原理
- 命名管道是一种特殊类型的文件(FIFO 文件),它在文件系统中有一个路径名,因此可以在不相关的进程间进行通信。与匿名管道不同,命名管道不依赖于进程的亲缘关系。
- 命名管道同样基于内核缓冲区实现 FIFO 机制,不同进程通过打开同一个命名管道文件进行读写操作来实现通信。
- 代码示例 写端(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#define FIFO_PATH "/tmp/my_fifo"
#define BUFFER_SIZE 256
int main() {
int fd;
char buffer[BUFFER_SIZE];
// 创建命名管道
if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {
perror("mkfifo failed");
return 1;
}
// 打开命名管道用于写
fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open for write failed");
return 1;
}
// 向命名管道写数据
const char* message = "Hello from writer!";
ssize_t bytes_written = write(fd, message, strlen(message));
if (bytes_written == -1) {
perror("write failed");
return 1;
}
// 关闭文件描述符
close(fd);
return 0;
}
读端(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#define FIFO_PATH "/tmp/my_fifo"
#define BUFFER_SIZE 256
int main() {
int fd;
char buffer[BUFFER_SIZE];
// 打开命名管道用于读
fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open for read failed");
return 1;
}
// 从命名管道读数据
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read failed");
return 1;
}
buffer[bytes_read] = '\0';
printf("Reader received: %s\n", buffer);
// 关闭文件描述符
close(fd);
// 删除命名管道
unlink(FIFO_PATH);
return 0;
}
- 特点
- 跨进程通信:能在无亲缘关系的进程间通信,扩展了通信的范围。
- 文件系统关联:依赖文件系统,创建和管理相对复杂,需要处理文件路径和权限等问题。
- 数据持久化:只要命名管道文件存在,数据就可以被不同进程按顺序读取,有一定的数据持久化特性。
信号(Signal)
原理
- 信号的概念
- 信号是一种软中断,用于通知进程发生了某种特定事件。它是一种异步通信机制,内核可以向进程发送信号,进程也可以向其他进程发送信号。每个信号都有一个唯一的编号(例如
SIGINT
对应编号 2,SIGTERM
对应编号 15 等)和一个默认的处理动作,如终止进程、忽略信号、暂停进程等。
- 信号是一种软中断,用于通知进程发生了某种特定事件。它是一种异步通信机制,内核可以向进程发送信号,进程也可以向其他进程发送信号。每个信号都有一个唯一的编号(例如
- 信号的处理
- 进程可以通过
signal()
函数或sigaction()
函数来设置对特定信号的处理方式。signal()
函数相对简单,但在一些系统中存在可移植性问题,sigaction()
函数则提供了更丰富的功能和更好的可移植性。 - 当进程接收到一个信号时,会暂停当前执行的任务,转而执行信号处理函数(如果设置了的话),处理完成后再返回原来被中断的地方继续执行。
- 进程可以通过
代码示例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void signal_handler(int signum) {
printf("Received signal %d\n", signum);
// 处理信号的逻辑,这里简单打印信息
exit(0);
}
int main() {
// 设置信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal() error");
return 1;
}
printf("Waiting for signal...\n");
while (1) {
sleep(1);
}
return 0;
}
在这个示例中,程序通过 signal()
函数设置了对 SIGINT
信号(通常由用户按下 Ctrl + C
产生)的处理函数 signal_handler
。当接收到 SIGINT
信号时,程序会打印接收到信号的信息并退出。
特点
- 异步通知:信号是异步的,进程不需要一直等待信号的到来,这对于处理一些紧急事件(如用户终止程序请求)非常有用。
- 简单但功能有限:信号机制简单,主要用于通知进程事件的发生,但信号携带的信息有限,一般只能传递信号编号,难以传递复杂的数据。
- 不可靠性:在某些情况下,信号可能会丢失或被延迟处理,特别是在进程处于忙碌状态或系统负载较高时。
消息队列(Message Queue)
原理
- 消息队列的概念
- 消息队列是内核中一个存放消息的链表,每个消息都有一个特定的类型。进程可以向消息队列中发送消息,也可以从消息队列中接收消息。消息队列提供了一种有格式的、异步的通信方式。
- 消息队列的操作
- 创建或获取消息队列使用
msgget()
函数,它返回一个消息队列标识符。发送消息使用msgsnd()
函数,接收消息使用msgrcv()
函数。进程可以根据消息的类型来接收特定类型的消息,这使得消息队列在通信的灵活性上优于管道。
- 创建或获取消息队列使用
代码示例
发送端(sender.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#define MSG_TYPE 1
#define BUFFER_SIZE 256
// 消息结构
typedef struct {
long mtype;
char mtext[BUFFER_SIZE];
} message_t;
int main() {
key_t key;
int msgid;
message_t msg;
// 生成唯一的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok failed");
return 1;
}
// 创建消息队列
msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget failed");
return 1;
}
// 设置消息类型和内容
msg.mtype = MSG_TYPE;
strcpy(msg.mtext, "Hello from sender!");
// 发送消息
if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd failed");
return 1;
}
printf("Message sent successfully.\n");
return 0;
}
接收端(receiver.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#define MSG_TYPE 1
#define BUFFER_SIZE 256
// 消息结构
typedef struct {
long mtype;
char mtext[BUFFER_SIZE];
} message_t;
int main() {
key_t key;
int msgid;
message_t msg;
// 生成唯一的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok failed");
return 1;
}
// 获取消息队列
msgid = msgget(key, 0666);
if (msgid == -1) {
perror("msgget failed");
return 1;
}
// 接收消息
if (msgrcv(msgid, &msg, sizeof(msg.mtext), MSG_TYPE, 0) == -1) {
perror("msgrcv failed");
return 1;
}
printf("Received message: %s\n", msg.mtext);
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl failed");
return 1;
}
return 0;
}
特点
- 有格式通信:消息队列可以传递有格式的消息,每个消息都有类型,进程可以根据类型有选择地接收消息,提高了通信的灵活性。
- 异步通信:进程发送消息后不需要等待接收方立即处理,消息会在队列中等待,实现了异步通信。
- 内核维护:消息队列由内核维护,提供了一定的可靠性,但需要注意消息队列的大小限制,超过限制可能导致发送失败。
共享内存(Shared Memory)
原理
- 共享内存的概念
- 共享内存是最快的一种进程间通信机制。它允许不同的进程访问同一块物理内存区域,这样进程之间可以直接读写共享内存中的数据,而不需要像管道或消息队列那样进行数据的拷贝。
- 共享内存的操作
- 创建或获取共享内存使用
shmget()
函数,它返回一个共享内存标识符。将共享内存附加到进程地址空间使用shmat()
函数,这样进程就可以像访问普通内存一样访问共享内存。使用完共享内存后,通过shmdt()
函数将其从进程地址空间分离,最后可以使用shmctl()
函数删除共享内存。
- 创建或获取共享内存使用
代码示例
写入端(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#define SHM_SIZE 256
int main() {
key_t key;
int shmid;
char* shm_ptr;
// 生成唯一的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok failed");
return 1;
}
// 创建共享内存
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget failed");
return 1;
}
// 附加共享内存到进程地址空间
shm_ptr = (char*)shmat(shmid, NULL, 0);
if (shm_ptr == (void*)-1) {
perror("shmat failed");
return 1;
}
// 向共享内存写入数据
const char* message = "Hello from writer!";
strcpy(shm_ptr, message);
// 分离共享内存
if (shmdt(shm_ptr) == -1) {
perror("shmdt failed");
return 1;
}
return 0;
}
读取端(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#define SHM_SIZE 256
int main() {
key_t key;
int shmid;
char* shm_ptr;
// 生成唯一的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok failed");
return 1;
}
// 获取共享内存
shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget failed");
return 1;
}
// 附加共享内存到进程地址空间
shm_ptr = (char*)shmat(shmid, NULL, 0);
if (shm_ptr == (void*)-1) {
perror("shmat failed");
return 1;
}
// 从共享内存读取数据
printf("Received message: %s\n", shm_ptr);
// 分离共享内存
if (shmdt(shm_ptr) == -1) {
perror("shmdt failed");
return 1;
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl failed");
return 1;
}
return 0;
}
特点
- 速度快:由于直接在共享内存中读写数据,避免了数据拷贝,因此共享内存是进程间通信中速度最快的方式。
- 复杂的同步问题:因为多个进程可以同时访问共享内存,所以需要额外的同步机制(如信号量)来保证数据的一致性和避免竞争条件。
- 内存管理:共享内存的大小需要提前确定,并且对共享内存的操作需要谨慎,不正确的内存访问可能导致段错误等问题。
信号量(Semaphore)
原理
- 信号量的概念
- 信号量是一个整型变量,它用于控制对共享资源的访问。信号量的值表示当前可用的共享资源数量。当一个进程想要访问共享资源时,它需要先获取信号量(将信号量的值减 1),如果信号量的值为 0,表示没有可用资源,进程会被阻塞,直到有其他进程释放信号量(将信号量的值加 1)。
- 信号量的操作
- 创建或获取信号量使用
semget()
函数,它返回一个信号量标识符。对信号量进行操作使用semop()
函数,该函数可以进行 P 操作(获取信号量,将信号量值减 1)和 V 操作(释放信号量,将信号量值加 1)。semctl()
函数用于控制信号量,如初始化信号量的值等。
- 创建或获取信号量使用
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
// 信号量操作结构
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P操作
void semaphore_P(int semid) {
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = -1;
sem_op.sem_flg = 0;
if (semop(semid, &sem_op, 1) == -1) {
perror("semaphore P operation failed");
exit(1);
}
}
// V操作
void semaphore_V(int semid) {
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = 1;
sem_op.sem_flg = 0;
if (semop(semid, &sem_op, 1) == -1) {
perror("semaphore V operation failed");
exit(1);
}
}
int main() {
key_t key;
int semid;
union semun sem_union;
// 生成唯一的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok failed");
return 1;
}
// 创建信号量
semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget failed");
return 1;
}
// 初始化信号量值为 1
sem_union.val = 1;
if (semctl(semid, 0, SETVAL, sem_union) == -1) {
perror("semctl SETVAL failed");
return 1;
}
// 模拟进程使用共享资源
semaphore_P(semid);
printf("Process is using the shared resource.\n");
sleep(2);
printf("Process finished using the shared resource.\n");
semaphore_V(semid);
// 删除信号量
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID failed");
return 1;
}
return 0;
}
特点
- 同步控制:主要用于进程间的同步,保证对共享资源的有序访问,避免竞争条件。
- 计数功能:信号量的值可以表示可用资源的数量,这使得它在管理多个共享资源时非常方便。
- 复杂操作:信号量的操作相对复杂,需要仔细处理 P、V 操作的顺序和时机,否则容易导致死锁等问题。
对比总结
- 通信方式和数据传递
- 管道:匿名管道用于亲缘关系进程间半双工通信,数据单向流动;命名管道可用于无亲缘关系进程间,数据按 FIFO 顺序传递。它们都以字节流形式传递数据。
- 信号:主要用于异步通知,传递的信息有限,一般只有信号编号。
- 消息队列:传递有格式的消息,进程可按消息类型接收,实现异步通信。
- 共享内存:直接共享物理内存区域,数据传递最快,但需要额外同步机制。
- 同步机制
- 管道和消息队列:本身有一定的同步机制,如管道的 FIFO 特性,消息队列按顺序接收消息。但对于复杂同步需求,可能需要额外同步手段。
- 信号量:专门用于同步控制,通过 P、V 操作管理共享资源访问。
- 共享内存:必须配合同步机制(如信号量)使用,否则易出现数据不一致问题。
- 适用场景
- 管道:适合简单的、具有亲缘关系进程间的数据流传输,如 shell 命令的管道。命名管道适用于无亲缘关系进程间简单的数据交换。
- 信号:处理异步事件,如程序终止、暂停等信号的处理。
- 消息队列:适用于需要传递有格式消息且异步处理的场景,如分布式系统中的消息传递。
- 共享内存:适用于对速度要求极高、数据量较大且能妥善处理同步问题的场景,如数据库系统中的共享缓冲区。
在实际应用中,需要根据具体的需求和场景来选择合适的进程间通信机制,以实现高效、稳定的进程间通信。