在Linux操作系统中,进程间通信(IPC,Inter-Process
Communication)是实现不同进程之间数据交换和协同工作的关键技术。本文将介绍Linux中常见的四种进程间通信方式:管道、命名管道、System V共享内存和System V消息队列
,并分析它们的优点和缺点。
管道是Linux中最简单的IPC机制,它允许两个进程之间进行单向数据传输。管道通常用于父子进程之间的通信。
优点:
- 简单易用:管道是Linux中最早的IPC机制,使用起来非常简单。
- 高效:管道的数据传输是直接在内存中进行,避免了系统调用的开销。
缺点:
- 半双工: 管道只能进行单向数据传输,不能进行双向通信。
- 只能用于具有亲缘关系的进程:管道通常只能用于父子进程之间的通信,对于其他进程间通信场景不够灵活。
who | wc -l #who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数
pipe()
函数int pipefd[2];
int ret = pipe(pipefd);
//pipe()函数接受一个整数数组作为参数,该数组包含两个整数值,分别表示管道的读端和写端的文件描述符。
如果成功,pipe()函数返回0,否则返回-1并设置errno。
通过管道的写端(
pipefd[1]
)写入数据。可以使用write()函数向管道写入数据。例如:
char buf[] = "Hello, world!";
ssize_t n = write(pipefd[1], buf, sizeof(buf));
write()函数将数据写入管道,并返回实际写入的字节数。如果写入成功,返回值大于0;如果写入失败,返回-1并设置errno
通过管道的读端(
pipefd[0]
)从管道读取数据。可以使用read()函数从管道读取数据。例如:
char buf[1024];
ssize_t n = read(pipefd[0], buf, sizeof(buf));
read()函数从管道读取数据,并返回实际读取的字节数。如果读取成功,返回值大于0;如果读取失败或没有数据可读,返回0或-1并设置errno。
可以将读看做嘴巴(0),写看做笔(1)方便记忆
在不再需要管道时,需要关闭管道的读端和写端。可以使用
close()
函数关闭管道。例如:
close(pipefd[0]); // 关闭读端
close(pipefd[1]); // 关闭写端
close()函数关闭文件描述符,释放相关资源。如果关闭成功,返回0;如果关闭失败,返回-1并设置errno。
以一段代码为例,父进程关闭读描述符,向管道文件中写入"--i am father--"
,子进程关闭写描述符,从管道文件中读取管道文件内容,打印在显示器上。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);
assert(n!=-1);
(void)n;
pid_t id=fork();
assert(id>=0);
if(id==0)
{
close(pipefd[1]); //关闭写描述符
char buffer[1024*8];
while(true)
{
ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"child read father => "<<buffer<<endl;
}
else if(s==0)
{
cout<<"write quit"<<endl;
break;
}
}
exit(0);
}
close(pipefd[0]); //关闭读描述符
string massage="i am father";
char send_buff[1024*8];
while(true)
{
snprintf(send_buff,sizeof send_buff,"--%s--",massage.c_str());
write(pipefd[1],send_buff,strlen(send_buff));
sleep(5);
break;
}
close(pipefd[1]);
pid_t ret=waitpid(id,nullptr,0);
cout<<"id: "<<id<<"ret "<<ret<<endl;
assert(ret>0);
(void)ret;
return 0;
}
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
命名管道通过文件系统中的路径名来标识,允许任何进程访问,因此可以在非亲缘关系的进程之间进行通信。
优点:
- 通用性:命名管道可以用于任何两个进程之间的通信,不受亲缘关系的限制。
- 文件系统支持:命名管道通过文件系统来管理,可以利用文件系统的权限控制机制来保护通信的安全性。
缺点:
- 相对复杂:使用命名管道需要更多的编程工作,包括创建、打开、读写等操作。
- 可能受到文件系统限制:例如,文件系统可能不支持跨文件系统的命名管道通信。
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式 不同,一但这些工作完 成之后,它们具有相同的语义。
$ mkfifo filename
#include
#include
#include
int mkfifo(const char *filename,mode_t mode);
参数说明:
函数返回值:
下面是一个简单的示例代码,演示如何使用mkfifo()函数创建命名管道,并进行读写操作:
#include
#include
#include
#include
#include
#include
int main() {
const char *fifo_path = "/tmp/my_fifo";
int ret;
char buffer[1024];
int bytes_written; // 修改变量名以更准确地反映其功能
int bytes_read;
int fd;
// 创建命名管道文件
ret = mkfifo(fifo_path, 0666);
if (ret != 0) {
perror("mkfifo error");
exit(EXIT_FAILURE);
}
// 打开管道文件进行写入操作
fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open error");
exit(EXIT_FAILURE);
}
// 向管道中写入数据
strcpy(buffer, "Hello, world!");
bytes_written = write(fd, buffer, strlen(buffer) + 1);
if (bytes_written == -1) {
perror("write error");
exit(EXIT_FAILURE);
} else {
printf("Sent: %s\n", buffer); // 打印发送的数据
}
// 关闭写入管道的文件描述符
close(fd);
// 打开管道文件进行读取操作
fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open error");
exit(EXIT_FAILURE);
}
// 从管道中读取数据并打印出来
memset(buffer, 0, sizeof(buffer)); // 清空缓冲区
bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("read error");
exit(EXIT_FAILURE);
} else if (bytes_read > 0) {
printf("Received: %s\n", buffer); // 打印接收到的数据
} else {
printf("No data available\n");
}
// 关闭读取管道的文件描述符
close(fd);
unlink(fifo_path); // 删除该管道文件
return 0; // 添加返回值,表明程序正常结束
}
既然可以读取和写入可以分开进行,那么就可以类比成
client & server
来进行通信
server
端进行管道文件创建,对需要服务的解析
#include"common.hpp"
int main()
{
umask(0);
if(mkfifo(ipcPATH.c_str(),MODE))
{
perror("mkfifo");
return 1;
}
int fd=open(ipcPATH.c_str(),O_RDONLY);
if(fd<0)
{
perror("open");
return 2;
}
char buffer[SIZE];
while(true)
{
buffer[0]='\0';
ssize_t s=read(fd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]='\0';
printf("server# %s\n",buffer);
char* res="+-*/%";
char* p=buffer;
int flag=0;
while(*p)
{
switch(*p)
{
case '+':
flag=0;
break;
case '-':
flag=1;
break;
case '*':
flag=2;
break;
case '/':
flag=3;
break;
case '%':
flag=4;
break;
}
p++;
}
char*num1=strtok(buffer,res);
char*num2=strtok(NULL,res);
int a=atoi(num1);
int b=atoi(num2);
int ret=0;
switch (flag)
{
case 0:
ret=a+b;
break;
case 1:
ret=a-b;
break;
case 2:
ret=a*b;
break;
case 3:
ret=a/b;
break;
case 4:
ret=a%b;
break;
}
printf("%d %c %d = %d\n",a,res[flag],b,ret);
}
else if(s==0)
{
cout<<"client quit--->server quit"<<endl;
break;
}
else
{
perror("server read");
break;
}
}
close(fd);
return 0;
}
client
端完成数据从外设的读取
#include"common.hpp"
int main()
{
int fd=open(ipcPATH.c_str(),O_WRONLY);
if(fd < 0)
{
perror("open");
return 2;
}
char buffer[SIZE];
while(true)
{
buffer[0]='\0';
cout<<"Please Enter#";
fflush(stdout);
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s-1]='\0';
write(fd,buffer,strlen(buffer));
}
}
close(fd);
return 0;
}
common
公共模块——头文件定义,文件创建路径,大小等宏的定义
#ifndef _COMM_H_
#define _COMM_H_
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define MODE 0664
#define SIZE 128
string ipcPATH="./fifo.ipc";
#endif
结果演示
(完成上述效果实现)
System V共享内存允许多个进程访问同一块物理内存区域,从而实现快速的数据共享和传输。
优点:
- 高效率:多个进程可以同时访问共享内存区域,数据传输速度非常快。
- 适用于大量数据传输:共享内存可以用于传输大量数据,而不会受到系统调用的开销限制。
缺点:
- 需要同步机制:多个进程同时访问共享内存时,需要使用同步机制来避免数据冲突和竞争条件。这会增加编程的复杂性。
- 系统资源消耗:创建和管理共享内存需要消耗一定的系统资源。如果使用不当,可能会导致系统资源紧张。
System V共享内存是一种进程间通信(IPC)机制,允许不同进程访问同一块内存区域,实现数据共享。在System V中,共享内存使用shmget()、shmat()、shmdt()和shmctl()等系统调用来实现。
ipcs #查看共享内存、消息队列和信号量
共享内存信息:
使用ipcs -m
命令可以查看系统使用的IPC共享内存资源,包括共享内存的大小、数量和使用情况等信息。消息队列信息:
使用ipcs -q
命令可以查看系统使用的IPC消息队列资源,包括消息队列的名称、类型、访问权限和进程ID等信息。信号量信息:
使用ipcs -s
命令可以查看系统使用的IPC信号量资源,包括信号量的名称、类型、访问权限和进程ID等信息。系统IPC参数:
使用ipcs -l
命令可以查看系统IPC参数,包括共享内存限制、信号量限制和消息队列限制等信息。int shmget(key_t key, size_t size, int flags);
参数:
key
:指定共享内存段的标识符,通常使用一个整数或字符串。size
:指定共享内存段的大小。flags
:指定共享内存段的权限,如读、写、执行等。返回值:成功时返回共享内存段的标识符(一个非负整数),失败时返回-1。- 解析:
shmget()
函数用于创建一个新的共享内存段或获取一个已存在的共享内存段的标识符。它需要指定一个唯一的键值来标识共享内存段,并指定大小和权限。如果成功,返回的标识符可以用于后续的shmat()、shmdt()和shmctl()调用。
void *shmat(int shmid, const void *shmaddr, int flags);
参数:
shmid
:指定要附加的共享内存段的标识符。shmaddr
:指定要附加的地址,通常设置为NULL,表示由系统自动选择一个地址。flags
:指定附加选项,如是否可以睡眠等。返回值:成功时返回一个指向附加地址的指针,失败时返回(void *)-1。- 解析:
shmat()
函数用于将共享内存段附加到调用进程的地址空间中。它需要指定要附加的共享内存段的标识符、附加地址和附加选项。如果成功,返回的指针指向附加的地址,进程可以通过该指针访问共享内存段中的数据。
int shmdt(const void *shmaddr);
参数:
shmaddr
:指定要分离的地址,通常由shmat()函数返回。返回值:成功时返回0,失败时返回-1。- 解析:
shmdt()
函数用于将共享内存段从调用进程的地址空间中分离。它需要指定要分离的地址,通常由shmat()函数返回。如果成功,调用进程将无法再通过该地址访问共享内存段中的数据。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid
:指定要控制的共享内存段的标识符。cmd
:指定要执行的控制命令,如IPC_RMID表示删除共享内存段。buf
:指向一个结构体的指针,用于传递命令相关的参数或返回命令执行结果。返回值:成功时返回0,失败时返回-1。- 解析:
shmctl()
函数用于控制共享内存段的属性或执行特定的命令。它需要指定要控制的共享内存段的标识符、控制命令和相关的参数或结果结构体。通过执行不同的控制命令,可以实现共享内存段的创建、删除、获取属性等操作。
创建一个共享内存段,将字符串写入共享内存,然后从中读取并最终删除共享内存段
#include
#include
#include
int main() {
key_t key = ftok("path/to/keyfile", 'R');
int shm_id = shmget(key, 1024, IPC_CREAT | 0666);
void *shm_ptr = shmat(shm_id, NULL, 0);
// 使用共享内存
sprintf((char*)shm_ptr, "Hello, shared memory!");
printf("Data read from shared memory: %s\n", (char*)shm_ptr);
shmdt(shm_ptr); // 分离共享内存
// 如果不再需要共享内存,可以删除共享内存段
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
创建一个简易版通讯,客户端发消息,服务端收消息并打印
server
端实现创建共享内存,挂接共享内存,并读取外设信息,判断是否有“quit”,然后进行释放
#include "comm.h"
#include "log.hpp"
Init init;
int main()
{
//1. 通过ftok算法获取唯一key值
key_t k=ftok(PATH_NAME,PROJ_ID);
if(k<0)
{
Log("创建key值失败",Error);
return -1;
}
//2. 获取共享内存
umask(0);
int shm=shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
if(shm<0)
{
Log("获取共享内存失败",Error);
return -2;
}
//可在终端用ipcs查看共享内存信息
// -q:列出消息队列相关信息。
// -m:列出共享内存相关信息。
// -s:列出信号量相关信息
//3. 挂接共享内存
char* shmaddr=(char*)shmat(shm,nullptr,0);
if(shmaddr==(void*)-1)
{
Log("挂接共享内存失败",Error);
return -3;
}
// 将共享内存当成一个大字符串
// char buffer[SHM_SIZE];
// 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
// 共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
/*****一个问题是,服务端会一直读,就算没有client端写入,此时需要访问控制*******/
// 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】(引入管道,管道天生具有)
int fd=OpenFifo(FIFO_PATH,READ);
Log("开始通信############",Info)<<std::endl;
for(;;)
{
Wait(fd);
printf("%s\n",shmaddr);
if(strcmp("quit",shmaddr)==0) break;
}
Log("结束通信############",Info)<<std::endl;
//4. 去关联共享内存
int n=shmdt(shmaddr);
if(n<0)
{
Log("去关联共享内存失败",Error);
return -4;
}
//sleep(5);
//5. 删除共享内存
int ctl=shmctl(shm,IPC_RMID,NULL);
if(ctl<0)
{
Log("释放共享内存失败",Error);
return -5;
}
CloseFifo(fd);
return 0;
}
client
端完成挂接共享内存并发送消息
#include "comm.h"
#include "log.hpp"
int main()
{
//创建key值
key_t k=ftok(PATH_NAME,PROJ_ID);
assert(k>=0);
Log("创建key成功",Info)<<std::endl;
int shmid=shmget(k,SHM_SIZE,0);
assert(shmid>=0);
Log("创建共享内存id成功",Info)<<std::endl;
char*shmaddr=(char*)shmat(shmid,nullptr,0);
assert(shmaddr!=nullptr);
Log("挂接共享内存成功",Info)<<std::endl;
int fd=OpenFifo(FIFO_PATH,WRITE);
for(;;)
{
ssize_t s = read(0, shmaddr, SHM_SIZE-1);
if(s > 0)
{
shmaddr[s-1] = 0;
Signal(fd); //只用来唤醒server端。当读取到的s>0时,调用此函数,唤醒server
if(strcmp(shmaddr,"quit") == 0) break;
}
}
int n=shmdt(shmaddr);
assert(n>=0);
Log("去关联共享内存成功",Info)<<std::endl;
CloseFifo(fd);
return 0;
}
comm
中包含Log打印并实现部分函数
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
#define PATH_NAME "/home/sun"
#define SHM_SIZE 4096
#define PROJ_ID 0x66
#define FIFO_PATH "./fifo"
class Init
{
public:
Init()
{
umask(0);
int n=mkfifo(FIFO_PATH,0666);
assert(n == 0);
(void)n;
Log("创建命名管道成功",Info)<<std::endl;
}
~Init()
{
unlink(FIFO_PATH);
Log("去关联管道成功",Info)<<std::endl;
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFifo(std::string pathname,int flag)
{
int fd=open(pathname.c_str(),flag);
assert(fd>=0);
Log("打开文件成功",Info)<<std::endl;
return fd;
}
void Wait(int fd)
{
Log("等待中.....",Info)<<std::endl;
uint32_t temp=0;
ssize_t s=read(fd,&temp,sizeof(uint32_t));
assert(s==sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t temp=1;
ssize_t s=write(fd,&temp,sizeof(uint32_t));
assert(s==sizeof(uint32_t));
(void)s;
Log("唤醒中.....",Info)<<std::endl;
}
void CloseFifo(int fd)
{
close(fd);
}
然后就开始通信
以上述案例为例子,如果我们向终端发送kill -2
信号(ctrl+c)那么就会出现下述问题
进程结束,但是创建的共享内存未释放
这是一个很严重后果的问题,代码中我们使用的是如果收到client端发送的quit及退出
但是难免会有进程退出,共享内存未正常释放的情况(kill -2、意外断连接…)我们再去创建是创建不了的,只能先用ipcrm -m +shmid
来释放共享内存
System V共享内存和管道是两种不同的进程间通信方式,它们在实现方式、通信速度和适用场景上有所不同。
实现方式:
- System V共享内存:通过调用系统接口(如shmget、shmat等)来创建和管理共享内存段。
- 管道:通过创建一个管道文件,并使用read和write系统接口进行通信。 通信速度:
System V共享内存:所有进程间通信方式中最快的一种
,因为共享内存不需要进行多次的拷贝操作,只需要将数据从输入文件复制到共享内存,再从共享内存复制到输出文件。
管道:管道通信需要进行四次拷贝操作
,服务端将信息从输入文件复制到服务端的临时缓冲区中,然后将服务端临时缓冲区的信息复制到管道中,客户端将信息从管道复制到客户端的缓冲区,最后将客户端临时缓冲区的信息复制到输出文件中。
适用场景:
- System V共享内存:适用于需要大量数据传输的进程间通信场景,因为共享内存可以避免多次的拷贝操作,提高通信效率。
- 管道:适用于需要小量数据传输的进程间通信场景,因为管道的创建和维护相对简单,且不需要额外的内存开销。
-- System V共享内存
+----------------+ +----------------+ +----------------+
| 输入文件 | --> | 共享内存 | --> | 输出文件 |
+----------------+ +----------------+ +----------------+
2次拷贝 2次拷贝 2次拷贝
-- 管道
+----------------+ +----------------+ +----------------+
| 服务端临时缓冲区 | --> | 管道 | --> | 客户端临时缓冲区 |
+----------------+ +----------------+ +----------------+
1次拷贝 2次拷贝 1次拷贝
这里的“拷贝”指的是将数据从一处复制到另一处的操作。因此,共享内存只需要进行两次拷贝操作,而管道需要进行四次拷贝操作。这导致了System V共享内存的通信速度比管道更快。
System V消息队列允许进程之间发送和接收消息。每个消息都有一个类型和一个长度。
优点:
- 消息传递:消息队列可以实现进程之间的消息传递,适用于需要异步通信的场景。
- 消息类型和长度控制:消息队列提供了消息类型和长度的控制机制,可以确保消息的完整性和准确性。
缺点:
- 相对复杂:使用消息队列需要更多的编程工作,包括创建、发送、接收等操作。
- 系统资源消耗:创建和管理消息队列需要消耗一定的系统资源。如果使用不当,可能会导致系统资源紧张。
使用 msgget 函数创建消息队列,该函数返回一个消息队列标识符。
#include
#include
#include
key_t key = ftok("path/to/keyfile", 'R');
int msg_id = msgget(key, IPC_CREAT | 0666);
定义一个结构体,用于存储消息的内容。
#include
struct msg_buffer {
long msg_type;
char msg_text[256];
};
使用 msgsnd 函数向消息队列发送消息。
struct msg_buffer message;
message.msg_type = 1;
strcpy(message.msg_text, "Hello, message queue!");
msgsnd(msg_id, &message, sizeof(message), 0);
使用 msgrcv 函数从消息队列接收消息。
struct msg_buffer received_message;
msgrcv(msg_id, &received_message, sizeof(received_message), 1, 0);
printf("Received message: %s\n", received_message.msg_text);
当不再需要消息队列时,使用 msgctl 函数删除它。
msgctl(msg_id, IPC_RMID, NULL);
创建一个消息队列,向队列发送一条消息,然后从队列中接收消息并打印出来。最后,删除消息队列。
#include
#include
#include
#include
#include
struct msg_buffer {
long msg_type;
char msg_text[256];
};
int main() {
key_t key = ftok("path/to/keyfile", 'R');
int msg_id = msgget(key, IPC_CREAT | 0666);
struct msg_buffer message;
message.msg_type = 1;
strcpy(message.msg_text, "Hello, message queue!");
// 发送消息
msgsnd(msg_id, &message, sizeof(message), 0);
// 接收消息
struct msg_buffer received_message;
msgrcv(msg_id, &received_message, sizeof(received_message), 1, 0);
printf("Received message: %s\n", received_message.msg_text);
// 删除消息队列
msgctl(msg_id, IPC_RMID, NULL);
return 0;
}