引言:进程的孤岛与通信的桥梁
在Linux的进程世界中,每个进程都是一座孤岛——拥有独立的地址空间、堆栈、数据段和代码段。操作系统精心设计的这种隔离机制保证了系统的稳定性和安全性,但同时也提出了一个根本性的挑战:当这些孤岛进程需要协作完成更复杂的任务时,它们如何交流通信?
想象一下这样的场景:Web服务器需要将用户请求分发给工作进程处理,数据库系统需要协调多个客户端进程的并发访问,现代浏览器需要让渲染进程、网络进程、插件进程协同工作……如果没有进程间通信,这些复杂的系统将无法运转。因此,Linux系统的程间通信不是Linux的\”缺陷\”,而是其强大能力的体现。正是因为有了可靠的IPC机制,Linux才能构建从微小的嵌入式设备到庞大的云计算集群的各种系统。
进程间通信(IPC)方式及编程实现
1.匿名管道通信方式
管道为半双工通信,本质上是文件,允许一个进程将数据写入管道,另一个进程从管道读取数据,并且数据以先进先出(FIFO)的顺序传输,常用于具有亲缘关系的进程间通信,如父子进程,管道属于系统资源,进程结束时会被系统回收。
实现步骤:
int pipe(int* pipes);
管道读端调用read函数会默认进行阻塞读取,直到管道缓冲区被写端写入数据或写端被关闭时,解除阻塞,在不读入数据时要及时关闭读端。 管道写端调用write函数会默认不会阻塞写入,直到管道缓冲区满时,写端会阻塞等待读端读取数据或读端被关闭时,解除阻塞,在不写入数据时要及时关闭写端。 匿名管道默认是半双工通信方式,可以采用创建两条管道实现全双工通信方式。
示例代码:
# 在父进程fork之前创建,以便父子进程都可以继承管道文件描述符
int pfdF_S[2] = { -1, -1 }; // 定义管道文件双端描述符数组,父进程给子进程通信
int pfdS_F[2] = { -1, -1 }; // 定义管道文件双端描述符数组,子进程给父进程通信
pipe(pfdF_S); // 创建管道
pipe(pfdS_F); // 创建管道
# 在父进程中创建子进程,模拟匿名管道在父子进程中通信
pid_t pid = fork();
if (pid > 0) {
// 执行父进程相关逻辑
std::cout << \”这里是父进程:\” << getpid() << \”,优先级:\” << getpriority(PRIO_PROCESS, getpid()) << \”,创建子进程ID:\” << pid << \”,优先级:\” << getpriority(PRIO_PROCESS, pid) << std::endl;
// 两条匿名管道实现父子进程的全双工通信
// 管道通信时,关闭不需要的管道读写端
close(pfdF_S[0]); // pfdF_S管道用于父进程给子进程发送数据,关闭父进程不需要使用的读端管道文件描述符
close(pfdS_F[1]); // pfdF_S管道用于父进程接收子进程发送数据,关闭父进程不需要使用的写端管道文件描述符
// 父进程向子进程发送的管道读取数据,调用read函数
char rbuf[256]; // 读取数据缓冲区
int rdbytes = read(pfdS_F[0], rbuf, sizeof(rbuf));
if (rdbytes < 0)
std::cerr << \”父进程从管道读取数据失败!\” << std::endl;
else if (rdbytes == 0) {
std::cout << \”子进程关闭写端,父进程读端检测到EOF,管道读取结束!\” << std::endl;
close(pfdS_F[0]); // 写端关闭,读端立马关闭,防止再次读取
}
else
std::cout << \”pipe: 父进程从管道中成功读取\” << rdbytes << \”个数据:\” << rbuf << std::endl;
// 读取完数据直接关闭父进程的读端文件描述符
close(pfdS_F[0]);
// 父进程向发送给子进程的管道写入数据,调用write函数
char wbuf[] = \”pipe: Hello Son Process! 这是父进程发送给子进程的数据。\”;
int wrbytes = write(pfdF_S[1], wbuf, sizeof(wbuf));
// 管道写端写完数据,立马关闭父进程写端文件描述符,防止出现读端无法检测到EOF一直阻塞
close(pfdF_S[1]);
if (wrbytes < 0)
std::cerr << \”父进程向管道写入数据失败!\” << std::endl;
else
std::cout << \”pipe: 父进程成功向管道写入\” << wrbytes << \”个数据:\” << wbuf << std::endl;
}
else if(pid == 0){
// 执行子进程相关逻辑
std::cout << \”这里是子进程:\” << getpid() << \”,修改后的子进程优先级:\” << nice(-10) << \”, 父进程ID:\” << getppid() << std::endl;
// 关闭不需要的管道端口
close(pfdF_S[1]); // pfdF_S父进程给子进程发送数据管道,子进程不需要写端文件描述符,直接关闭
close(pfdS_F[0]); // pfdS_F子进程给父进程发送数据管道,子进程不需要读端文件描述符,直接关闭
// 子进程向发送给父进程的管道写入数据,调用write函数
char wbuf[] = \”pipe: Hello Father Process! 这是子进程发送给父进程的数据。\”;
int wrbytes = write(pfdS_F[1], wbuf, sizeof(wbuf));
// 子进程发送完后立即关闭写端管道文件描述符
close(pfdS_F[1]);
if (wrbytes <= 0)
std::cerr << \”子进程向管道写入数据失败!\” << std::endl;
else
std::cout << \”pipe: 子进程成功向管道写入\” << wrbytes << \”个数据:\” << wbuf << std::endl;
// 子进程从父进程发送给子进程的管道读取数据,调用read函数
char rbuf[256]; // 读取数据缓冲区
int rdbytes = read(pfdF_S[0], rbuf, sizeof(rbuf));
if (rdbytes <= 0)
std::cerr << \”子进程从管道读取数据失败!\” << std::endl;
else if (rdbytes == 0) {
std::cout << \”子进程检测到父进程关闭写端,管道读取结束!\” << std::endl;
close(pfdF_S[0]); // 写端关闭, 读端读不到数据,立马关闭
}
else
std::cout << \”pipe: 子进程从管道中成功读取\” << rdbytes << \”个数据:\” << rbuf << std::endl;
// 子进程读写完管道数据后,直接关闭读端管道文件描述符
close(pfdF_S[0]);
}
else
std::cout << \”创建子进程失败!\” << std::endl;
2.命名管道通信方式
命名管道也是一种特殊的文件,允许不具有亲缘关系的进程间进行通信,命名管道在文件系统中有一个路径名,可以通过该路径名进行访问命名管道文件。本质上和匿名管道相同,命名管道只提供一个命名管道文件描述符代替匿名管道的读端和写端,命名管道也是半双工通信方式,可以通过创建两条命名管道实现全双工通信方式。
实现步骤:
int mkfifo(const char *pathname, mode_t mode);
int unlink(const char *pathname);
示例代码:
// 在父进程创建fifo文件,使用完后在父进程中删除fifo文件
char sffifo[] = \”S_F.fifo\”;
char fsfifo[] = \”F_S.fifo\”;
mkfifo(sffifo, 0666); // 创建从子进程到父进程的fifo文件
mkfifo(fsfifo, 0666); // 创建从父进程到子进程的fifo文件
atexit([]() { unlink(\”S_F.fifo\”); unlink(\”F_S.fifo\”); }); //// 注册退出函数删除fifo文件
pid_t pid = fork(); // 在父进程中创建子进程
if (pid > 0) {
// 执行父进程相关逻辑
std::cout << \”这里是父进程:\” << getpid() << \”,优先级:\” << getpriority(PRIO_PROCESS, getpid()) << \”,创建子进程ID:\” << pid << \”,优先级:\” << getpriority(PRIO_PROCESS, pid) << std::endl;
// 两条命名管道fifo实现父子进程的全双工通信
// 父进程以只读方式打开子进程到父进程的fifo文件
int sffd = open(sffifo, O_RDONLY);
if (sffd == -1)
std::cerr << \”父进程打开fifo文件:\” << sffd << \”失败\” << std::endl;
else {
char rbuf[256];
int rdbytes = read(sffd, rbuf, sizeof(rbuf));
if (rdbytes < 0)
std::cerr << \”父进程从fifo文件读取数据失败!\” << std::endl;
else if (rdbytes == 0) {
std::cout << \”子进程关闭写端,父进程读端检测到EOF,fifo读取结束!\” << std::endl;
// 直接关闭fifo文件
close(sffd);
unlink(sffifo);
}
else
std::cout << \”fifo: 父进程从fifo文件中成功读取\” << rdbytes << \”个数据:\” << rbuf << std::endl;
}
// 父进程以只写方式打开父进程到子进程的fifo文件
int fsfd = open(fsfifo, O_WRONLY);
if (fsfd == -1)
std::cerr << \”父进程打开fifo文件:\” << fsfd << \”失败\” << std::endl;
else {
char wbuf[256] = \”fifo: Hello Son Process! 这是父进程发送给子进程的fifo数据。\”;
int wrbytes = write(fsfd, wbuf, sizeof(wbuf));
if (wrbytes <= 0)
std::cerr << \”父进程向fifo文件写入数据失败!\” << std::endl;
else
std::cout << \”fifo: 父进程成功向fifo文件写入\” << wrbytes << \”个数据:\” << wbuf << std::endl;
}
// 关闭文件描述符和硬链接等待内核回收
close(fsfd);
unlink(fsfifo);
close(sffd);
unlink(sffifo);
}
else if(pid == 0){
// 执行子进程相关逻辑
std::cout << \”这里是子进程:\” << getpid() << \”,修改后的子进程优先级:\” << nice(-10) << \”, 父进程ID:\” << getppid() << std::endl;
// 子进程以只写方式打开子进程到父进程的fifo文件
int sffd = open(sffifo, O_WRONLY);
if (sffd == -1)
std::cerr << \”子进程打开fifo文件:\” << sffd << \”失败\” << std::endl;
else {
char wbuf[256] = \”fifo: Hello Father Process! 这是子进程发送给父进程的fifo数据。\”;
int wrbytes = write(sffd, wbuf, sizeof(wbuf));
if (wrbytes <= 0)
std::cerr << \”子进程向fifo文件写入数据失败!\” << std::endl;
else
std::cout << \”fifo: 子进程成功向fifo文件写入\” << wrbytes << \”个数据:\” << wbuf << std::endl;
}
// 子进程以只读方式打开父进程到子进程的fifo文件
int fsfd = open(fsfifo, O_RDONLY);
if (fsfd == -1)
std::cerr << \”子进程打开fifo文件:\” << fsfd << \”失败\” << std::endl;
else {
char rbuf[256];
int rdbytes = read(fsfd, rbuf, sizeof(rbuf));
if (rdbytes < 0)
std::cerr << \”子进程从fifo文件读取数据失败!\” << std::endl;
else if (rdbytes == 0) {
std::cout << \”父进程关闭写端,子进程读端检测到EOF,fifo读取结束!\” << std::endl;
// 写端关闭,读端读不到数据,立即关闭fifo文件
close(fsfd);
}
else
std::cout << \”fifo: 子进程从fifo文件中成功读取\” << rdbytes << \”个数据:\” << rbuf << std::endl;
}
// 命名管道文件需要手动显示删除
close(fsfd);
close(sffd);
}
else
std::cout << \”创建子进程失败!\” << std::endl;
3.信号量通信方式
进程间通信的信号量机制是一种用于多进程同步和互斥的核心技术,其核心是通过原子操作控制共享资源的访问,以保证多进程访问共享数据的一致性,类似于多线程的信号量机制,被信号量阻塞的进程会被挂起,直到被信号量唤醒。
①创建信号量集
创建包含nsems个信号量的信号量集,key一般使用ftok函数生成用于唯一标识信号量集以便进程可以访问,semflg指定3位8进制表示权限和创建IPC_CREAT属性,成功返回信号量集标识符semid,失败返回-1。
# 成功返回信号量标识符semid,失败返回-1
int semget( key_t key, int nsems, int semflg );
创建系统唯一int类型标识符ftok函数,一般用于共享内存或信号量集等生成全局唯一标识符。
# 将指定路径pathname和整数值proj_id映射为系统唯一的int值返回
key_t ftok(const char* pathname, int proj_id);
②信号量集的初始化或删除等操作
包括对创建好后的信号量进行初始化信号量集或删除等操作,信号量集被某个进程使用semget函数创建,必须在使用结束后退出前采用semctl函数进行删除,释放资源。
# 对指定信号量





