Linux这片充满无限可能的操作系统天地里,进程间通信犹如一条隐秘而强大的纽带,将一个个独立运行的进程紧密相连,编织出复杂而精妙的软件生态网络。想象一下,在一个庞大的 Linux 系统中,众多进程如同忙碌的小工匠,各自专注于特定的任务。有的进程负责处理用户输入,有的在后台默默管理着系统资源,还有的在进行复杂的数据运算。然而,若没有进程间通信,这些进程就只是孤立的个体,无法协同工作,整个系统也将陷入混乱与低效。
进程间通信赋予了进程们相互交流、共享信息、协调行动的能力。它让数据能够在不同的进程空间中自由穿梭,使得一个进程的成果可以为其他进程所用,一个进程的需求能够被其他进程感知并响应。无论是简单的文本数据传递,还是复杂的共享资源同步与协作,进程间通信都提供了多样化的解决方案。
一、简介
Linux 进程间通信(Inter-Process Communication,IPC)是指在多道程序环境下,进程间进行数据交换和信息传递的一种机制或方法。在现代操作系统中,进程是系统资源分配的基本单位,不同进程之间需要相互合作和通信,才能完成各种任务。进程间通信是实现进程间协作的重要手段。
进程间通信在 Linux 系统中至关重要。每个进程在 Linux 环境下都有独立的用户地址空间,一般情况下,进程间的进程空间不能相互访问。但在很多实际应用场景中,进程与进程之间需要进行通信,以共同完成特定的功能需求。例如,一个进程需要将数据发送给另一个进程进行进一步处理,或者多个进程需要共享同一个资源等。
1.1 为什么要通信
在软件体系中,进程间通信的原因与人类通信有相似之处。首先,存在需求是关键因素。在软件系统中,多个进程协同完成任务、服务请求或提供消息等情况时有发生。例如,在一个复杂的分布式系统中,不同的进程可能分别负责数据采集、处理和存储等任务,它们之间需要进行通信以确保整个系统的正常运行。其次,进程间存在隔离。每个进程都有独立的用户空间,互相看不到对方的内容。这就如同人与人之间如果身处不同的房间,没有沟通渠道的话就无法交流信息。所以,为了实现信息的传递和任务的协同,进程间通信就显得尤为必要。
通信方式与人类类似,取决于需求、通信量大小和客观实现条件。在人类社会中,有烽火、送信鸽、写信、发电报、打电话、发微信等多种通信方式。在软件中,也对应着不同的进程间通信方式。比如,对于小量的即时信息传递,可以类比为打电话的方式,采用信号这种通信方式;对于大量的数据传输,可以类比为写信的方式,采用消息队列或共享内存等通信方式。
我们先拿人来做个类比,人与人之间为什么要通信,有两个原因。首先是因为你有和对方沟通的需求,如果你都不想搭理对方,那就肯定不用通信了。其次是因为有空间隔离,如果你俩在一起,对方就站在你面前,你有话直说就行了,不需要通信。此时你非要给对方打个电话或者发个微信,是不是显得非常奇怪、莫名其妙。如果你俩不在一块,还有事需要沟通,此时就需要通信了。通信的方式有点烽火、送信鸽、写信、发电报、打电话、发微信等。采取什么样的通信方式跟你的需求、通信量的大小、以及客观上能否实现有关。
同样的,软件体系中为什么会有进程间通信呢?首先是因为软件中有这个需求,比如有些任务是由多个进程一起协同来完成的,或者一个进程对另一个进程有服务请求,或者有消息要向另一方提供。其次是因为进程间有隔离,每个进程都有自己独立的用户空间,互相看不到对方,所以才需要通信。
1.2 为什么能通信
内核空间是共享的,虽然多个进程有多个用户空间,但内核空间只有一个。就像一个公共的资源库,虽然每个进程都有自己独立的 “房间”(用户空间),但它们都可以通过特定的通道访问这个公共资源库(内核空间)。
为什么能通信呢?那是因为内核空间是共享的,虽然N个进程都有N个用户空间,但是内核空间只有一个,虽然用户空间之间是完全隔离的,但是用户空间与内核空间并不是完全隔离的,他们之间有系统调用这个通道可以沟通。所以两个用户空间就可以通过内核空间这个桥梁进行沟通了。
虽然用户空间之间完全隔离,但用户空间与内核空间并非完全隔离,它们之间有系统调用这个通道可以沟通。Linux 使用两级保护机制:0 级供内核使用,3 级供用户程序使用。每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的 1GB 字节虚拟内核空间则为所有进程以及内核所共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。虽然内核空间占据了每个虚拟空间中的最高 1GB 字节,但映射到物理内存却总是从最低地址(0x00000000)开始。
通过一副图讲解进程间通信的原理,进程之间虽然有空间隔离,但都和内核连着,可以通过特殊的系统调用和内核沟通,从而达到和其它进程通信的目的。就像不同的房间虽然相互独立,但都通过管道与一个中央控制室相连。进程就如同各个房间,内核就如同中央控制室。进程虽然不能直接访问其他进程的用户空间,但可以通过系统调用与内核进行交互,内核再将信息传递给其他进程,从而实现进程间通信。例如,当一个进程需要向另一个进程发送数据时,它可以通过系统调用将数据写入内核空间的特定区域,内核再通知目标进程从该区域读取数据。
我们再借助一副图来讲解一下。
虽然这个图是讲进程调度的,但是大家从这个图里面也能看出来进程之间为什么要通信,因为进程之间都是有空间隔离的,它们之间要想交流信息是没有办法的。但是也不是完全没有办法,好在它们都和内核是连着的,虽然它们不能随意访问内核,但是还有系统调用这个大门,进程之间可以通过一些特殊的系统调用和内核沟通从而达到和其它进程通信的目的。
二、进程间通信的框架
2.1进程间通信机制的结构
进程间通信机制由存在于内核空间的通信中枢和存在于用户空间的通信接口组成,两者关系紧密。通信中枢就如同邮局或基站,为通信提供核心机制;通信接口则像信纸或手机,为用户提供使用通信机制的方法。
为了更直观地理解进程间通信机制的结构, 我们可以通过以下图示来展示:
用户通过通信接口让通信中枢建立通信信道或传递通信信息。例如,在使用共享内存进行进程间通信时,用户通过特定的系统调用接口(通信接口)请求内核空间的通信中枢为其分配一块共享内存区域,并建立起不同进程对该区域的访问路径。
2.2进程间通信机制的类型
⑴共享内存式
通信中枢建立好通信信道后,通信双方之后的通信不需要通信中枢的协助。这就如同两个房间之间打开了一扇门,双方可以直接通过这扇门进行交流,而不需要中间人的帮忙。
但是,由于通信信息的传递不需要通信中枢的协助,通信双方需要进程间同步,以保证数据读写的一致性。否则,就可能出现数据踩踏或者读到垃圾数据的情况。比如,多个进程同时对共享内存进行读写操作时,需要通过信号量等机制来确保在同一时间只有一个进程能够进行写操作,避免数据冲突。
⑵消息传递式
通信中枢建立好通信信道后,每次通信还都需要通信中枢的协助。这种方式就像一个中间人在两个房间之间传递信息,每次传递都需要经过中间人。
消息传递式又分为有边界消息和无边界消息。无边界消息是字节流,发过来是一个一个的字节,要靠进程自己设计如何区分消息的边界。有边界消息的发送和接收都是以消息为基本单位,类似于一封封完整的信件,接收方可以明确地知道每个消息的开始和结束位置。
2.3进程间通信机制的接口设计
按照通信双方的关系,可分为对称型通信和非对称型通信:
消息传递式进程间通信一般用于非对称型通信,例如在客户服务关系中,客户端向服务端发送请求消息,服务端接收消息并进行处理后返回响应消息,整个通信过程通过通信中枢进行消息的传递。
共享内存式进程间通信一般用于对称型通信,也可用于非对称型通信。在对称型通信中,通信双方关系对等,如同两个平等的伙伴共同使用一块共享内存进行数据交换。在非对称型通信中,也可以通过共享内存实现一方写入数据,另一方读取数据的模式。
进程间通信机制一般要实现三类接口:
如何建立通信信道,谁去建立通信信道。对于对称型通信来说,谁去建立通信信道无所谓,有一个人去建立就可以了,后者直接加入通信信道。对于非对称型通信,一般是由服务端、消费者建立通信信道,客户端、生产者则加入这个通信信道。不同的进程间通信机制,有不同的接口来创建信道。例如,在使用共享内存时,可以通过特定的系统调用(如 shmget)来创建共享内存区域,建立通信信道。
后者如何找到并加入这个通信信道。一般情况是,双方通过提前约定好的信道名称找到信道句柄,通过信道句柄加入通信信道。但是有的是通过继承把信道句柄传递给对方,有的是通过其它进程间通信机制传递信道句柄,有的则是通过信道名称直接找到信道,不需要信道句柄。
如何使用通信信道。一旦通信信道建立并加入成功,进程就需要知道如何正确地使用通信信道进行数据的读写操作。例如,在使用管道进行通信时,进程需要明确知道哪个文件描述符是用于读,哪个是用于写,以及在读写过程中的各种规则和特殊情况的处理。
三、Linux中的进程间通信机制
3.1管道(Pipe)
⑴匿名管道:匿名管道通常用于临时的、简单的数据传输,仅用于有亲缘关系的进程。当使用 fork 函数创建子进程时,子进程会继承父进程的文件描述符表。父进程通过 pipe 函数自动以读写的方式打开同一个管道文件,并将文件描述符返回给一个数组。其中,数组的一个元素存储以读的方式打开管道文件所返回的文件描述符,另一个元素存储以写的方式打开管道文件所返回的文件描述符。
站在文件描述符角度深度理解管道,子进程拷贝父进程后,就不需要再以读或者写的方式打开管道文件了。确保管道通信的单向性,父子进程要分别关闭读端和写端。例如,如果希望数据从父进程流向子进程,就关闭父进程的读端,子进程的写端;如果希望数据从子进程流向父进程,就关闭父进程的写端,子进程的读端。
下面是验证管道通信的代码示例:
#include<iostream>
#include<cassert>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstdlib.h>
#define MAX 100
int main() {
// 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
std::cout << "pipefd[0]:" << pipefd[0] << ",pipefd[1]:" << pipefd[1] << std::endl;
// 创建子进程
pid_t id = fork();
// 判断是否创建失败
if (id < 0) {
perror("fork");
return 1;
}
// 子进程
else if (id == 0) {
close(pipefd[0]); // 关闭读通道
int cnt = 10;
while (cnt) {
char message[MAX];
snprintf(message, sizeof(message), "hello father, I am child, Mypid:%d, cnt: %d", getpid(), cnt);
cnt--;
// 将字符串 message 写入到管道中
write(pipefd[1], message, strlen(message));
sleep(1); // 让子进程写慢些
}
exit(0);
}
// 父进程
close(pipefd[1]); // 关闭写通道
// TODO
char buffer[MAX];
while (true) {
sleep(1);
// 从文件描述符对应的管道里读取数据,并将数据存储到 buffer 中
size_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = 0;
std::cout << getpid() << " -> " << "child sends: " << buffer << " to me!" << std::endl;
} else if (n == 0) {
std::cout << "father return val(n):" << n << std::endl;
std::cout << "child quit, me too!!!" << std::endl;
sleep(1);
break;
}
}
std::cout << "finish reading..." << std::endl;
// 写端已经退出,读完后关闭读端
close(pipefd[0]);
pid_t rid = waitpid(id, nullptr, 0);
if (rid == id) {
std::cout << "wait success" << std::endl;
}
return 0;
}
⑵有名管道FIFO:有名管道允许不相关的进程通过文件系统中的一个路径名进行通信。创建有名管道可以使用 mkfifo 函数,判断有名管道是否已存在,若尚未创建,则以相应的权限创建。以下是创建有名管道和使用有名管道进行通信的代码示例:
发送端:
#include"name_fifo.hpp"
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<limits.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#define MYFIFO "/tmp/myfifo"/* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF/*常量PIPE_BUF 定义在于limits.h中*/
int main() {
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
/* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1) {
if ((mkfifo(MYFIFO, 0666) < 0) && (errno!= EEXIST)) {
printf("Cannot create fifo file\n");
exit(1);
}
/* 以只读阻塞方式打开有名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1) {
printf("Open fifo file error\n");
exit(1);
}
while (1) {
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) {
printf("Read '%s' from FIFO\n", buff);
}
close(fd);
exit(0);
}
}
}
接收端:
#include"name_fifo.hpp"
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<limits.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#define MYFIFO "/tmp/myfifo"/* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF/*常量PIPE_BUF 定义在于limits.h中*/
int main() {
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
/* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1) {
if ((mkfifo(MYFIFO, 0666) < 0) && (errno!= EEXIST)) {
printf("Cannot create fifo file\n");
exit(1);
}
/* 以只读阻塞方式打开有名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1) {
printf("Open fifo file error\n");
exit(1);
}
while (1) {
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) {
printf("Read '%s' from FIFO\n", buff);
}
close(fd);
exit(0);
}
}
}
3.2信号(Signals)
信号是一种软件中断,是操作系统用来通知进程某个事件已经发生的一种方式。可以使用 signal 函数注册信号处理函数。以下是注册信号处理函数的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数
void sighandler(int signo) {
printf("signo==[%d]\n", signo);
}
int main() {
// 注册信号处理函数
signal(SIGINT, sighandler);
while (1) {
sleep(10);
}
return 0;
}
3.3文件(Files)
文件是一种持久化存储机制,可用于进程间通信。写进程将数据写入文件,读进程从文件中读取数据。以下是写进程和读进程的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 写进程
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
perror("Error opening file for writing");
return 1;
}
char *data = "Hello from write process!";
fputs(data, fp);
fclose(fp);
// 读进程
fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("Error opening file for reading");
return 1;
}
char buffer[100];
fgets(buffer, sizeof(buffer), fp);
printf("Read from file: %s\n", buffer);
fclose(fp);
return 0;
}
3.4信号量(Semaphores)
信号量是一种计数器,用于控制对共享资源的访问。可以使用信号量控制对共享内存的访问。以下是使用信号量控制对共享内存访问的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int set_semvalue(int sem_id) {
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return 0;
return 1;
}
void del_semvalue(int sem_id) {
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\n");
}
int semaphore_p(int sem_id) {
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
int semaphore_v(int sem_id) {
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1) {
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
int main() {
int shmid, sem_id;
void *shm = NULL;
struct shared_use_st *shared;
// 创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if (shmid == -1) {
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
// 将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if (shm == (void *)-1) {
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
// 新建信号量
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
// 信号量初始化
if (!set_semvalue(sem_id)) {
fprintf(stderr, "init failed.\n");
exit(EXIT_FAILURE);
}
// 操作共享内存
shared = (struct shared_use_st *) shm;
// 写入数据
strcpy(shared->text, "Data to be shared");
semaphore_v(sem_id);
// 读取数据
semaphore_p(sem_id);
printf("Read from shared memory: %s\n", shared->text);
// 删除信号量
del_semvalue(sem_id);
// 把共享内存从当前进程中分离
if (shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1) {
fprintf(stderr, "shmctl(IPC_RMID) failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
3.5共享内存(Shared Memory)
共享内存允许多个进程访问同一内存区域,是一种高效的 IPC 机制。可以使用 shmget、shmat、shmdt 和 shmctl 函数来创建共享内存、写入数据和读取数据。以下是创建共享内存、写入数据和读取数据的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define TEXT_SZ 2048
struct shared_use_st {
char text[TEXT_SZ];
};
int main() {
int shmid;
void *shm = NULL;
struct shared_use_st *shared;
// 创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if (shmid == -1) {
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
// 将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if (shm == (void *)-1) {
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
// 设置共享内存
shared = (struct shared_use_st *) shm;
// 写入数据
strcpy(shared->text, "Data to be shared");
// 读取数据
printf("Read from shared memory: %s\n", shared->text);
// 把共享内存从当前进程中分离
if (shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1) {
fprintf(stderr, "shmctl(IPC_RMID) failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
3.6消息队列(Message Queues)
消息队列允许进程发送和接收消息。可以使用 msgget、msgsnd 和 msgrcv 函数来创建消息队列、发送消息和接收消息。以下是创建消息队列、发送消息和接收消息的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_SIZE 100
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
int msgid;
key_t key = ftok(".", 'm');
// 创建消息队列
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget");
exit(1);
}
// 发送消息
struct msgbuf sendbuf;
sendbuf.mtype = 1;
strcpy(sendbuf.mtext, "Hello from message queue!");
if (msgsnd(msgid, &sendbuf, strlen(sendbuf.mtext) + 1, 0) == -1) {
perror("msgsnd");
exit(1);
}
// 接收消息
struct msgbuf recvbuf;
if (msgrcv(msgid, &recvbuf, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv");
exit(1);
}
printf("Received message: %s\n", recvbuf.mtext);
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
exit(1);
}
return 0;
}
3.7套接字(Sockets)
套接字是一种网络通信机制,也可用于本地 IPC。可以使用 socket、bind、listen、accept 和 connect 函数来实现服务端和客户端的通信。以下是服务端和客户端的代码示例:
服务端:
#include <iostream>
#include <string.h>
#include <unistd.h> // for close()
#include <arpa/inet.h> // for sockaddr_in, inet_addr
#include <sys/types.h>
#include <sys/socket.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建 socket 文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 将套接字绑定到端口
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
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");
exit(EXIT_FAILURE);
}
std::cout << "Server is listening on port " << PORT << std::endl;
// 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
(socklen_t*)&addrlen))<0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 接收数据
read(new_socket , buffer, BUFFER_SIZE);
std::cout << "Message from client: " << buffer << std::endl;
const char *hello = "Hello from server";
send(new_socket , hello , strlen(hello) , 0 );
std::cout << "Hello message sent" << std::endl;
close(new_socket);
close(server_fd);
return 0;
}
客户端代码:
#include <iostream>
#include <string.h>
#include <unistd.h> // for close()
#include <arpa/inet.h> // for sockaddr_in, inet_addr
#define PORT 8080
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[1024] = {0};
// 创建套接字文件描述符
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "\n Socket creation error \n";
return -1;
}
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) {
std::cerr << "\nInvalid address/ Address not supported \n";
return -1;
}
// 发起连接请求
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
std::cerr << "\nConnection Failed \n";
return -1;
}
send(sock , hello , strlen(hello) , 0 );
std::cout << "Hello message sent from client" << std::endl;
read(sock , buffer, sizeof(buffer));
std::cout << "Message from server: " << buffer << std::endl;
close(sock);
return 0;
}
编译与运行
首先,编译服务端和客户端代码:g++ server.cpp -o server g++ client.cpp -o client
在一个终端中运行服务端:./server
在另一个终端中运行客户端:./client
四、应用场景
4.1数据传输与共享
文件处理与转换:管道常被用于将一个进程的输出作为另一个进程的输入,从而实现数据的传输与处理。比如在 Shell 脚本中,使用 ls | grep “txt” 命令,通过管道将 ls 命令列出的文件列表传输给 grep 命令,筛选出文件名包含 “txt” 的文件2。
数据库操作:多个进程可能需要共享数据库连接或对数据库进行协同操作。例如,一个进程负责向数据库写入数据,另一个进程负责从数据库读取数据并进行分析,它们之间可以通过共享内存或消息队列来传递数据库操作的指令和数据。
多媒体处理:在多媒体应用中,不同的进程可能负责音频、视频的采集、编码、解码、播放等不同环节。进程间通过共享内存或管道等方式传输音频视频数据,实现多媒体数据的流畅处理。
4.2资源共享与同步
打印机等设备共享:多个进程可能需要同时访问打印机、扫描仪等外部设备。通过信号量来控制对设备的访问权限,确保同一时刻只有一个进程能够使用设备,避免冲突134。
文件锁机制:当多个进程需要对同一个文件进行读写操作时,使用信号量或文件锁来实现互斥访问,防止数据损坏或不一致。例如,一个进程正在写入文件时,其他进程需要等待,直到写入操作完成。
4.3通知与事件传递
进程状态通知:父进程创建子进程后,子进程的终止、暂停等状态变化需要及时通知父进程。信号机制常用于这种场景,子进程可以通过发送特定信号告知父进程其状态14。
系统事件通知:当系统发生某些事件,如磁盘空间不足、网络连接变化等,内核会向相关进程发送信号,进程接收到信号后可以采取相应的处理措施。
4.4任务协作与并行处理
分布式计算:在分布式系统中,不同的计算机节点上的进程需要协同工作来完成复杂的计算任务。消息队列可用于在节点间传递任务指令和中间结果,实现任务的分发和结果的汇总。
多线程编程:在同一个进程中的多个线程之间也需要进行通信和协作。虽然线程共享进程的地址空间,但也需要通过信号量、互斥量等机制来实现同步和互斥,确保线程安全地访问共享资源 。
4.5构建复杂应用架构
客户端 / 服务器模型:服务器进程和多个客户端进程之间需要进行通信。套接字是实现这种通信的常用方式,它不仅可以用于本地进程间通信,还支持网络通信,使得客户端可以通过网络连接到服务器。
微服务架构:在微服务架构中,不同的微服务进程之间需要进行高效的通信和协作。可以根据具体需求选择合适的进程间通信方式,如消息队列用于异步通信、HTTP 接口基于套接字实现服务间的调用等。
五、案例分析
5.1管道通信案例
案例描述:父进程创建一个管道,然后创建子进程。父进程向管道写入数据,子进程从管道读取数据并打印。
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int pipe_fd[2];
char data[] = "Hello from parent";
char buffer[20];
// 创建管道
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) { // 子进程
// 关闭写端
close(pipe_fd[1]);
// 从管道读取数据
read(pipe_fd[0], buffer, sizeof(buffer));
printf("Child received data: %s\n", buffer);
// 关闭读端
close(pipe_fd[0]);
} else { // 父进程
// 关闭读端
close(pipe_fd[0]);
// 写入数据到管道
write(pipe_fd[1], data, sizeof(data));
// 关闭写端
close(pipe_fd[1]);
// 等待子进程结束
wait(NULL);
}
return 0;
}
通过管道实现了父子进程间的单向数据传输,父进程写入的数据被子进程成功读取,管道在这种亲缘关系进程间通信简单高效,但要注意及时关闭不需要的管道端,避免资源浪费和潜在的阻塞问题。
5.2消息队列通信案例
案例描述:创建一个消息队列,一个进程向消息队列发送消息,另一个进程从消息队列接收消息并打印。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msg_buffer {
long msg_type;
char msg_text[100];
};
int main() {
key_t key;
int msg_id;
struct msg_buffer msg;
// 创建唯一的key
key = ftok("/tmp", 65);
// 创建消息队列
msg_id = msgget(key, 0666 | IPC_CREAT);
// 设置消息类型
msg.msg_type = 1;
// 消息内容
strcpy(msg.msg_text, "Hello, Message Queue!");
// 发送消息
msgsnd(msg_id, &msg, sizeof(msg), 0);
// 接收消息
msgrcv(msg_id, &msg, sizeof(msg), 1, 0);
printf("Received message: %s\n", msg.msg_text);
// 删除消息队列
msgctl(msg_id, IPC_RMID, NULL);
return 0;
}
消息队列允许不同进程异步通信,发送和接收进程不需要同时运行。通过消息类型可以区分不同的消息,实现有针对性的消息处理,但要注意及时删除不再使用的消息队列,避免资源占用。
5.3共享内存通信案例
案例描述:创建一块共享内存,一个进程向共享内存写入数据,另一个进程从共享内存读取数据并打印。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {
key_t key;
int shm_id;
char *shm_data;
// 创建唯一的key
key = ftok("/tmp", 65);
// 创建共享内存
shm_id = shmget(key, 1024, 0666 | IPC_CREAT);
// 连接共享内存
shm_data = (char *)shmat(shm_id, NULL, 0);
// 写入数据到共享内存
strcpy(shm_data, "Hello, Shared Memory!");
printf("Data written to shared memory: %s\n", shm_data);
// 分离共享内存
shmdt(shm_data);
// 删除共享内存
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
共享内存允许多个进程直接访问同一块物理内存区域,数据传输效率高。但需要注意进程间的同步问题,避免同时读写导致数据冲突。
5.4信号通信案例
案例描述:一个进程可以向另一个进程发送自定义信号,接收进程根据接收到的信号执行相应的操作。例如,进程 A 向进程 B 发送 SIGUSR1 信号,进程 B 接收到信号后打印一条消息。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signal_num) {
if (signal_num == SIGUSR1) {
printf("Received SIGUSR1 signal\n");
}
}
int main() {
// 注册信号处理程序
signal(SIGUSR1, signal_handler);
printf("Waiting for SIGUSR1 signal...\n");
while (1) {
sleep(1);
}
return 0;
}
通过自定义信号和信号处理函数,进程间可以实现简单的交互和通知,适用于一些简单的事件驱动场景,但要注意信号处理函数的编写要尽可能简洁,避免长时间阻塞导致系统响应问题。
5.5套接字通信案例
案例描述:创建一个简单的服务器进程和客户端进程,服务器监听指定端口,客户端连接服务器后发送消息,服务器接收消息并回复。——代码示例:
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int server_socket, client_socket;
struct sockaddr_in server_address, client_address;
socklen_t client_address_length;
char buffer[1024];
// 创建套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
// 设置服务器地址
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
server_address.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字
bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));
// 监听套接字
listen(server_socket, 5);
// 接受客户端连接
client_address_length = sizeof(client_address);
client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_address_length);
// 接收客户端消息
recv(client_socket, buffer, sizeof(buffer), 0);
printf("Received message from client: %s\n", buffer);
// 发送回复消息
strcpy(buffer, "Message received successfully!");
send(client_socket, buffer, sizeof(buffer), 0);
// 关闭套接字
close(client_socket);
close(server_socket);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int client_socket;
struct sockaddr_in server_address;
char buffer[1024];
// 创建套接字
client_socket = socket(AF_INET, SOCK_STREAM, 0);
// 设置服务器地址
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接服务器
connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address));
// 发送消息
strcpy(buffer, "Hello, Server!");
send(client_socket, buffer, sizeof(buffer), 0);
// 接收服务器回复
recv(client_socket, buffer, sizeof(buffer), 0);
printf("Received reply from server: %s\n", buffer);
// 关闭套接字
close(client_socket);
return 0;
}
套接字通信不仅可以用于本地进程间通信,还支持网络通信。通过客户端 / 服务器模式,实现了不同主机或同一主机上不同进程间的可靠通信,但要注意网络编程中的错误处理和资源管理,如套接字的正确关闭等。