进程间通信(IPC, Inter-Process Communication)是指在操作系统中,不同进程之间交换数据、信息和命令的过程。在一个多任务的操作系统中,多个进程可以同时运行,但是这些进程是相互独立的,它们有自己的地址空间和上下文,无法直接访问对方的内存空间。如果多个进程需要协作来完成某项任务,或者需要共享某些数据,就需要使用进程间通信机制来进行通信和协作。
进程间通信是实现多个进程之间协同工作的必要手段,实现进程通信的作用:
- 实现进程间数据共享:进程间通信可以使不同进程之间共享数据,避免了数据复制
的开销,提高了系统的性能。- 提高系统的可靠性:进程间通信可以将多个进程组织成一个整体,使得它们可以协
同工作,提高了系统的可靠性- 实现进程间协作:进程间通信可以使进程之间相互协作,共同完成任务。例如,一
个进程可以向另一个进程发送请求,请求另一个进程提供某些服务,如打印文件等。管道、消
息队列、信号量、共享内存等机制都可以用来实现进程间协作。- 提高系统的安全性:进程间通信可以实现不同进程之间的数据隔离和保护,从而提
高了系统的安全性。
根据 IPC 机制所依赖的资源类型可以划分为基于系统资源的 IPC 机制和基于文件系统的 IPC机制。对于各种具体的实现方式总结如下:
无名管道是一种单向的、字节流的通信管道,可以在进程之间传递数据。事实上无名管道是一种特殊的文件,存在于内存中,无名管道使用系统调用pipe()
会创建两个文件描述符,一个用于读取数据,另一个用于写入数据,其特点为:
函数编写时所需要的头文件有
#include
#include
无名管道的创建需要使用系统调用pipe()
,函数原型为:
int pipe(int pipefd[2]);
该函数有一个 int 类型的参数 pipefd,是一个有两个元素的数组,分别代表管道的读端和写端。其中,pipefd[0]代表管道的读端,pipefd[1]代表管道的写端。调用 pipe()函数后,系统会自动创建一个无名管道,并将读端和写端返回给调用进程。如果成功创建管道,返回 0;否则返回-1,表示创建失败。
示例程序中实现:
1.使用
pipe()
函数创建无名管道;
2.使用fork()
函数创建父子进程,其中父进程完成从无名管道中读取数据,而子进程实现每隔一秒向无名管道中写入数据。
#include
#include
int main(void)
{
int fd[2];
/* 创建无名管道 */
int ret = pipe(fd);
if(ret == 0) printf("creating pipe is successful!\n");
printf("%d %d\n",fd[0],fd[1]);
char buff[32];
/* 创建父子进程 */
pid_t pid = fork();
/* 区分父子进程 */
if(pid < 0)
printf("creating subprocess is failed!\n");
else if(pid == 0) // 子进程
{
int i = 0;
while(1){
sprintf(buff,"child data is %d",i++);
// 向无名管道中写入数据
write(fd[1],buff,strlen(buff));
sleep(1);
}
}
else{ // 父进程
while(1)
{
// 读取无名管道中数据
read(fd[0],buff,sizeof(buff));
if(strlen(buff) != 0)
printf("buff: %s\n",buff);
buff[0] = '0';
}
}
return 0;
}
先使用 gcc progress.c -o target
(test.c为程序所在.c文件名),后使用命令./target
执行,其结果为:
示例程序中父子进程的写入读取逻辑如图:
若想要实现父子进程相互写入读取数据,可创建两个无名管道,一个管道实现父进程读取数据,子进程写入数据;另一个进程反之,即可。
有名管道是一种特殊类型的 Unix/Linux 文件,也被称为 FIFO(First-In-First-Out)管道,用来在进程之间传输数据的,与匿名管道不同,有名管道是通过文件系统路径命名的管道,可以在进程之间进行通信。有名管道的操作方式类似于打开文件,即进程可以打开有名管道来读取或写入其中的数据。
有名管道使用函数mkfifo()
创建,创建时需要包含头文件 #include
与#incldue
,其函数原型为:
int mkfifo(const char *pathname, mode_t mode);
函数参数1表示所在路径以及名字,参数2表示创建时的权限;其函数函数返回值表示是否创建成功,若返回-1,则表示创建失败;否则,表示创建成功。
示例程序:
1.在程序1中使用函数
mkfifo()
创建有名管道,再通过write()
向管道中写入数据;
2.在程序使用函数access()
判断管道文件是否创建,若不存在则给出提示信息;若存在则读取管道管道中的数据;
(注:函数access()
有俩参数,参数1表示文件路径,参数2可选择检查文件是否存在、可读或可写,使用时需要包含头文件#include
)
#include
#include
#include
#include
#include
int main(void)
{
char path[] = "/home/zxj/workplace/my_fifo";
if(access(path,F_OK) == -1)
printf("path is error\n");
/* 打开管道文件 */
int fd = open(path,O_RDWR);
/* 向管道中read数据 */
while(1)
{
char buff[32];
read(fd,buff,sizeof(buff));
printf("read buff:%s\n",buff);
memset(buff,0,sizeof(buff));
}
return 0;
}
#include
#include
#include
#include
#include
int main(void)
{
char path[] = "/home/zxj/workplace/my_fifo";
/* 创建有名管道 */
int ret = mkfifo (path, 0644);
if(ret == -1)
{
printf("creating is failed!\n");
exit(-1);
}
/* 打开管道文件 */
int fd = open(path,O_WRONLY);
if(fd == -1) printf("opening is failed!\n");
/* 向管道中写入数据 */
int i = 0;;
while(++i)
{
char buff[32];
sprintf(buff,"input data:%d",i);
int wret = write(fd,buff,strlen(buff));
printf("buff:%s size:%d\n",buff,wret);
sleep(1);
}
return 0;
}
在输入命令ls -al | grep my_fifo
后可见上述程序创建的有名管道文件类型为p
,学过linux文件类型的小伙伴不难看出字母p就表示管道文件。
IPC key 是一个唯一的长整型数值,可以由任意一个进程指定,是为了方便不同进程之间对 IPC 对象的访问和操作而产生的。IPC key 和 IPC 对象与文件描述符和文件之间的关系相似,每个 IPC 对象都会有一个唯一的 IPC key,当一个进程创建或获取一个 IPC key 时,就可以使用该 IPC key 来创建或连接相应的 IPC 对象,并对其进行访问。
为了避免 IPC key 重复,Linux 提供了一个名为 ftok()
的函数来将一个普通的文件路径和一个整数值转换为一个唯一的 IPC key,使用该函数时需要包含头文件#icnlude
,具体函数原型为:
key_t ftok(const char *pathname, int proj_id);
该函数接受两个参数:文件路径和一个整数值。它会根据文件的 inode 节点号和整数值来生成一个唯一的 IPC key。由于不同的文件具有不同的 inode 节点号,因此不同的文件路径和整数值组合会生成不同的 IPC key值。
消息队列是一种先进先出的消息缓冲区,用于在多个进程之间传递消息。在 Linux 中,每个消息队列都由一个唯一的消息队列标识符(Message QueueIdentifier)进行标识。进程可以通过该标识符来连接和访问相应的消息队列,从而进行进程间通信。
在linux中消息队列的使用步骤如下:
1.创建或者获取IPC key值
IPC key 可以通过调用ftok()
函数来创建,或直接传入一个已知的IPC key值。(创建IPC key值见上文)
2.打开或创建消息队列
要打开或创建消息队列,需要使用函数msgget()
,函数原型为:
int msgget(key_t key, int msgflg);
函数调用成功,会返回一个非负整数表示消息队列的标识符(即消息队列的唯一标识),调用失败返回-1,并设置 errno。
函数参数含义为:
3.发送消息
在消息队列中要发送消息,需要使用msgsnd
函数,函数原型如下所示:
int msgsnd(int msqid, const void *msgp, size_t msgsz,int msgflg);
函数参数的解释为:
函数调用成功返回 0。函数调用失败,则返回 -1 并设置 errno 来指示错误类型。
4.接收消息
要从消息队列中读取消息,需要使用 msgrcv 函数,函数原型如下所:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
参数释义:
函数调用成功,会返回一个非负整数表示消息队列的标识符(即消息队列的唯一标识),调用失败返回-1,并设置 errno。
5.操作消息队列(包括删除,获取,设置)
使用 msgctl 函数可以删除消息队列、获取和设置消息队列的属性信息,其函数原型为:
int msgctl(int msqid, int cmd, struct msqid_ds*buff);
参数释义:
函数调用成功之后返回值为 0,否则表示执行失败。
函数实例:
创建父子进程,实现子进程每间隔1s使用消息队列向父进程发送数据,而父进程负责读取消息队列中的数据并且打印到cmd中。
#include
#include
#include
#include
#include
int main(void)
{
key_t key = ftok("/home/zxj/workplace/t.c",'t');
int msgid = msgget(key,0666|IPC_CREAT);
if(msgid < 0)
printf("creating msg is error!\n");
printf("[%d]msgid is %d\n",msgid,getpid());
pid_t pid = fork();
if(pid < 0)
printf("error!\n");
// 子进程
else if(pid ==0)
{
int count = 0;
while((++count) < 10)
{
struct msgbuf msg;
msg.mtype = count;
sprintf(msg.mtext,"data is %d",count*count);
int ret = msgsnd(msgid,&msg,strlen(msg.mtext),0);
printf("[child(%d)]->: ret is %d .mtype is %d ",getpid(),ret,msg.mtype);
printf("mtext is %s.\n",msg.mtext);
sleep(1);
}
}
// 父进程
else
{
while(1)
{
struct msgbuf msg;
int ret = msgrcv(msgid,(void*)&msg,128,0,0);
if(ret == -1) break;
printf("[father(%d)]->: ret is %d .mtype is %d ",getpid(),ret,msg.mtype);
printf("mtext is %s.\n",msg.mtext);
}
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
使用命令gcc progress.c -o target
与命令./target
后,执行结果为:
关于消息队列的使用步骤及其相应的函数就讲解完成了,但在使用过程中仍然有一些需要注意的地方,如下所示:
struct msgbuf {
int mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
共享内存是通过操作系统内核在不同进程之间共享内存区域的一种机制。在创建共享内存时,操作系统会分配一块内存区域,并将其映射到各个进程的地址空间中。进程可以直接读写这个内存区域,而不需要进行任何数据传输的操作。共享内存的优点是速度快,因为不需要数据的复制操作,而且不需要操作系统进行上下文切换,所以它通常比其他 IPC 机制(如管道和消息队列)更快。
使用时需要包含的头文件
#include
#include
#include
1.创建或者获取IPC key值
IPC key 可以通过调用 ftok
函数来创建,或者直接传入一个已知的IPC key值。(创建IPC key值见上文
2.创建共享内存
函数原型
int shmget(key_t key, size_t size, int shmflg);
参数解释:
函数调用成功,返回值为共享内存标识符(shmid),用于标识创建或获取的共享内存对象。如果出错,则返回 -1 并设置 errno 错误
3.映射共享内存
函数原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数解释
函数返回值为共享内存段的首地址,类型为 void *。如果出错,则返回 -1 并设置 errno 错误码。
4.使用完共享内存后解除
函数原型
int shmdt(const void *shmaddr);
参数释义:
5.操作共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数释义:
shmid:共享内存标识符,通过 shmget() 函数创建或
控制命令,可选值为以下几个:
IPC_STAT:获取共享内存的状态信息,将共享内存的信息保存到 buf 结构体中。
IPC_SET:设置共享内存的状态信息,将 buf 结构体中的信息设置到共享内存中。
IPC_RMID:销毁共享内存。
SHM_LOCK:锁定共享内存,防止被换出到磁盘上。
SHM_UNLOCK:解锁共享内存。
buf:指向 shmid_ds 结构体的指针,用于存储共享内存的状态信息。如果cmd 为 IPC_STAT 或 IPC_SET,则需要传递该参数,否则可以设置为 NU
删除共享内存时,可如此使用函数 shmctl(shmid,IPC_RMID,NULL);
使用实例:
#include
#include
#include
#include
#include
int main(void)
{
/* 生成一个key */
key_t key = ftok("/home/zxj/workplace/",1);
if(key == -1)
{
printf("creating key is failed!\n");
_exit(-1);
}
printf("key:%d\n",key);
/* 创建一个共享内存段 */
int shmid = shmget(key,1024,IPC_CREAT|0666);
if(shmid == -1)
{
printf("creating shmget si failed!\n");
_exit(-2);
}
printf("shmid:%d\n",shmid);
// 创建父子进程
pid_t pid = fork();
if(pid < 0)
printf("fork is failed!\n");
// 子进程
else if(pid == 0)
{
/* 链接共享内存段 */
char *shmaddr = shmat(shmid,NULL,0);
for(int i=0;i<2;++i)
strncpy(shmaddr,"hello",5);
printf("child sned:%s\n",shmaddr);
/* 分离共享内存 */
shmdt(shmaddr);
}
// 父进程
else
{
/* 链接共享内存段 */
char *shmaddr = shmat(shmid,NULL,0);
sleep(2);
printf("father recive:%s\n",shmaddr);
/* 分离共享内存 */
shmdt(shmaddr);
/* 删除共享内存段 */
shmctl(shmid,IPC_RMID,NULL);
}
return 0;
}
其运行结果为:
此时可以使用命 ipcs -m查看程序运行时与运行后各个共享内存变化情况:
(运行时)
关于共享内容的使用步骤及其相应的函数就讲解完成了,但在使用过程中仍然有一些需要注意的地方,如下所示:
信号量是一种计数器,用于对多个进程共享的资源进行计数和控制。它是一种 IPC 对象,通常用于进程间互斥和同步,确保多个进程对共享资源的访问顺序和正确性。信号量通常用于解决并发访问共享资源的同步问题。
所涉及的头文件有:
#include
#include
#include
#include
#include
#include
1.创建或者获取IPC key值
IPC key 可以通过调用 ftok()
函数来创建,或者直接传入一个已知的IPC key值。(创建IPC key值见上文)
2.创建或获取信号量
函数原型
int semget(key_t key, int nsems, int semflg);
参数释义:
函数调用成功,返回值为获取或创建的信号量集的标识符。如果出错,则返回 -1 并设置errno 错误码
3.初始化信号量
所涉及的头文件有:
#include
函数原型:
int semctl(int semid, int semnum, int cmd, union semun arg);
参数释义:
4.信号量的PV操作
int semop(int semid, struct sembuf *sops, size_t nsops);
参数释义:
函数返回值为 0 表示成功,如果出错,则返回-1 并设置 errno 错误码。
函数实例:
#include
#include
#include
#include
#include
int main(void)
{
key_t key = ftok("/home/zxj/workplace/t.c",'t');
/* 创建信号量 */
int sem_id= semget(key,1,0666|IPC_CREAT);
printf("sem_id of semget is %d.\n",sem_id);
/* 初始化信号量 */
semctl(sem_id, 0, SETVAL, 0);
/* 创建父子进程 */
pid_t pid = fork();
if(pid < 0)
printf("error\n");
// 子进程
else if(pid == 0)
{
sleep(2);
struct sembuf sem_buf;
sem_buf.sem_num = 0; //信号量编号
sem_buf.sem_op = 1; //P操作
sem_buf.sem_flg = 0;//设置成阻塞模式
semop(sem_id,&sem_buf,1);
printf("I am son.\n");
}
// 父进程
else
{
struct sembuf sem_buf;
sem_buf.sem_num = 0; //信号量编号
sem_buf.sem_op = -1; //V操作
sem_buf.sem_flg = 0;//设置成阻塞模式
semop(sem_id,&sem_buf,1);
printf("I am father.\n");
sem_buf.sem_num = 0; //信号量编号
sem_buf.sem_op = 1; //P操作
sem_buf.sem_flg = 0;//设置成阻塞模式
semop(sem_id,&sem_buf,1);
}
/* 删除信号量 */
semctl(sem_id, 0, IPC_RMID, 0);
return 0;
}
消息队列、共享内存、信号量应用场景的比较:
机制 | 应用场景 |
---|---|
消息队列 | 一对多或多对一的进程通信,日志记录,任务分发,进程间通信 |
共享内存 | 多进程协作,高速缓存,大型数据处理,共享数据缓存 |
信号量 | 进程同步,进程互斥,进程间通信,控制共享资源的访问,保证资源的独占性 |
在ubuntu中可输入命令查看消息队列、共享内存、信号量的相关情况:
ipcs -m
查看共享内存的相关情况:ipcs -q
可查看上述三者的相关情况: