目录
一、为什么需要进程间通信?
二、进程间通信的分类
三、管道
四、共享内存
一、为什么需要进程间通信?
父进程创建子进程,子进程复制父进程的虚拟内存、PCB、代码、页表等数据,当父子进程任意一方改变数据时,操作系统将通过写时拷贝技术保证父子进程数据不受一方的改变而影响,从而保证了进程间的独立性。在计算机中,往往是多个进程之间协同工作,因此就需要进程间进行通信。
1)进程间通信的目的
数据传输:一个进程需要将自己的数据发送给另一个进程,因此就需要进程间通信。
资源共享:多个进程之间需要共享一些资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二、进程间通信的分类
三、管道
1、什么是管道?
在linux centos 7中使用ps aux | grep 23800命令可以查看pid为23800的进程的详细信息。其中,|就是管道指令,它的作用是将ps指令的输出(写端)结果通过管道传送到grep命令的输入(读端)作为grep的输入进行执行。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“
2、匿名管道基本原理
管道实际上是通过文件系统实现的,基本原理就是使用读写的方式打开同一个文件,将返回的两个文件描述符提供给两个进程,一个进程以写的方式向文件中写入数据另一个进程以读的方式从文件中读取数据,从而实现具有亲缘关系的两个进程间的通信。
1)父进程分别以读和写的方式打开同一个文件,返回两个文件描述符。(父进程创建出管道)
2)创建子进程,使具有亲缘关系的进程进行通信。
3)一个进程使用写文件描述对文件进行写入,另一个进程使用读文件描述符从文件中读取数据。
3、pipe创建匿名管道
1)pipe函数
int pipe(int pipefd[2]);
功能:创建一个匿名管道
参数:输出型参数,文件描述符数组,fd[0]表示读端 fd[1]表示写端
返回值:成功返回0,失败返回错误代码。
2)fork共享管道的原理
父进程使用pipe创建一个管道,获得两个文件描述符,分别对应管道的读端和写端。父进程使用fork创建子进程,子进程会复制父进程的数据和代码,子进程也拥有该管道的两个文件描述符。当父子进程拥有相同的额文件描述符后,各自关闭自己不需要的文件描述符(保证父子进程,一个进行读一个进行写,互不干扰),一个进程写入数据另一个进程读数据。
代码描述
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int pipefd[2];
//父进程使用pipe创建管道
int p = pipe(pipefd);
//父进程创建子进程
pid_t pid = fork();
if(pid == 0)
{
//child
//关闭读端文件描述符,向管道中写入数据
close(pipefd[1]);
while(1)
{
char buf1[11] = {'h','e','l','l','o',' ','w','o','r','l','d'};
//write(pipefd[1],buf1,11);
sleep(3);
}
}
else if(pid > 0)
{
//father
//关闭写端文件描述符,从管道中读数据
close(pipefd[1]);
while(1)
{
char buf2[11];
int id = read(pipefd[0],buf2,11);
if(id < 0)
{
printf("%d\n",id);
exit(-1);
}
printf("i am father:%s\n",buf2);
sleep(3);
sleep(3);
}
}
else
{
//error
perror("fork error");
}
return 0;
}
3)从文件描述符的角度理解匿名管道的创建过程
①父进程使用pipe创建管道
②父进程创建子进程
③父子进程关闭各自不需要的文件描述符
4)从内核的角度理解管道的创建
4、匿名管道的读写规则和特点
1)读写规则
①写端关闭读文件描述符不关闭写文件描述符且不进行写入,读端读数据时可能发生阻塞。
代码现象:读端不会执行read之后的printf代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int pipefd[2];
//父进程使用pipe创建管道
int p = pipe(pipefd);
//父进程创建子进程
pid_t pid = fork();
if(pid == 0)
{
//child,关闭读端文件描述符,不关闭写端文件描述符且不向管道中写入
close(pipefd[0]);
while(1)
sleep(1);
}
else if(pid > 0)
{
//father,关闭写端文件描述符,从管道中读数据
close(pipefd[1]);
while(1)
{
char buf2[11];
int id = read(pipefd[0],buf2,11);
printf("i am father:%s\n",buf2);//写端没有进行写入,读端则阻塞,不会打印这一
}
}
else
{
//error
perror("fork error");
}
return 0;
}
②当写端不满足写入条件时(管道被写满),写端可能会被阻塞
代码现象:开始时写端会一直向管道中写入,管道写满时,write后的依据代码不在被执行。此后,一直进行读
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int pipefd[2];
//父进程使用pipe创建管道
int p = pipe(pipefd);
//父进程创建子进程
pid_t pid = fork();
if(pid == 0)
{
//child,关闭读端文件描述符,向管道中写入数据
close(pipefd[0]);
while(1)
{
char buf1[11] = {'h','e','l','l','o',' ','w','o','r','l','d'};
write(pipefd[1],buf1,11);
printf("i am child\n");//当写入一段时间后,管道就被写满了,这句代码就不在执行了
}
}
else if(pid > 0)
{
//father,关闭写端文件描述符,从管道中读数据
close(pipefd[1]);
while(1)
{
char buf2[11];
int id = read(pipefd[0],buf2,11);
printf("i am father:%s\n",buf2);
sleep(1);//每隔1秒进行一次读,如果注释掉这句代码,则当写端写满时读端开始读,读完写端会继续写
}
}
else
{
//error
perror("fork error");
}
return 0;
}
③写端向管道中写入一些数据后关闭写端文件描述符,读端在读完管道中的数据后结汇读到文件的结尾
代码现象:当读端读完管道中的文件,read函数会返回0表示读到了文件结尾,此时结束循环
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int pipefd[2];
//父进程使用pipe创建管道
W> int p = pipe(pipefd);
//父进程创建子进程
pid_t pid = fork();
if(pid == 0)
{
//child,关闭读端文件描述符,向管道中写入数据,在关闭写端文件描述符
close(pipefd[0]);
char buf1[11] = {'h','e','l','l','o',' ','w','o','r','l','d'};
write(pipefd[1],buf1,11);
close(pipefd[1]);
}
else if(pid > 0)
{
//father,关闭写端文件描述符,从管道中读数据
close(pipefd[1]);
while(1)
{
char buf2[11];
int id = read(pipefd[0],buf2,11);
if(id == 0)
{
printf("i am father:read end!\n");
break;
}
printf("i am father:%s\n",buf2);
}
}
else
{
//error
perror("fork error");
}
return 0;
}
④读端将读和写文件描述符都关闭,写端在进程会被操作系统直接杀掉
代码现象:使用ps指令查看子进程(写端),子进程已经不存在了
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int pipefd[2];
//父进程使用pipe创建管道
W> int p = pipe(pipefd);
//父进程创建子进程
pid_t pid = fork();
if(pid == 0)
{
//child,关闭读端文件描述符,向管道中写入数据
close(pipefd[0]);
while(1)
{
printf("i am child:%d\n",getpid());
char buf1[11] = {'h','e','l','l','o',' ','w','o','r','l','d'};
write(pipefd[1],buf1,11);
}
}
else if(pid > 0)
{
//father,关闭写端文件描述符和读端文件描述符
close(pipefd[1]);
close(pipefd[0]);
}
else
{
//error
perror("fork error");
}
return 0;
}
2)特点
- 只能用于具有亲缘关系的进程之间的通信。通常情况下,一个管道由父进程创建,然后父进程在使用fork函数创建子进程,之后父子进程之间进行通信。
- 管道提供的是流式服务,一个进程从管道的写端进行写另一个进程从读端读取数据。
- 管道的生命周期随进程,一般情况进程退出管道释放。
- 一般而言,内核会对管道操作进行同步与互斥。当读端不在读数据时,写端就会阻塞;当写端不在写数据,读端读完数据就会读到文件结束。
- 管道是半双工的,数据只能从写端流向读端。需要双向通信,父子进程之间需要建立两个管道。
5、命名管道
匿名管道只能用于具有亲缘关系的进程之间的进程通信,如果想要不同进程(没有亲缘关系)的进程之间进行数据交换,可以使用FIFO文件进行不同进程间的通信,FIFO称为命名管道。命名管道是一种特殊类型的文件。
1)创建命名管道
命令行创建
mkfifo filename;
程序中创建
int mkfifo(const char *filename,mode_t mode);
参数:filename文件名;mode权限
返回值:成功返回0,失败返回-1
注意:使用mkfifo函数创建管道时,权限会受umask值的影响。即:newMode = mode & ~umask
2)命名管道的打开
命名管道也是文件,因此打开命名管道和打开文件一样,使用open函数打开。
打开规则
- 如果当前进程是为读打开的,则阻塞直到有进程为写打开,立即返回打开成功。
- 如果当前进程是为写打开的,则阻塞直到有进程为读打开,立即返回打开成功。
3)使用命名管道实现server & client通信
server程序
int main()
{
//创建管道
int f = mkfifo("fifo",0666);
if(f == 0)
{
//success
//以读的方式打开命名管道fifo,服务器端获取客户端的数据
int fd = open("fifo",O_RDONLY);
//读取管道内容,输出到显示器
while(1)
{
char buffer[1024];
ssize_t s = read(fd,buffer,sizeof(buffer)-1);
buffer[s] = '\0';
if(s == 0)
{
printf("client quit\n");
exit(0);
}
else if(s > 0)
{
printf("server# %s",buffer);
fflush(stdout);
}
else
{
perror("read error");
}
}
}
else
{
//error
perror("create error");
}
return 0;
}
client程序
int main()
{
//以写的方式打开管道
int fd = open("fifo",O_WRONLY);
//向管道中写入
char buffer[1024];
while(1)
{
buffer[0] = '\0';
//从标准输入获取数据
printf("please enter# ");
fflush(stdout);
fgets(buffer,1024,stdin);
ssize_t s = write(fd,buffer,1023);
buffer[s] = '\0';
}
return 0;
}
运行结果
6、命名管道和匿名管道的区别
- 匿名管道由pipe函数创建并打开
- 命名管道由mkfifo函数创建,由open函数打开
- 匿名管道一般用于具有亲缘关系的进程之间的通信,命名管道可以用于不相关的进程之间的通信。
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
注意:管道的本质是内核中的缓冲区,文件知识管道的标识
四、共享内存
通过管道进行进程间通信时,通信双方向文件中写入、读出等需要多次数据拷贝,使得数据传输的效率降低。而共享内存进行进程间通信,是将内存中的一块区域映射到两个进程的程序地址空间中的共享映射区,写进程直接向该内存位置写入、读进程直接从该内存位置读取数据,这些进程间数据传递不在涉及到内核,也就是说进程不在通过执行进入系统内核的调用来传递彼此的数据。因此,共享内存也是最快的IPC形式。
1、共享内存函数
1)生成唯一标识_key(操作系统通过生成的唯一表示创建共享内存)
key_t _key = ftok(const char *pathname, int proj_id);
作用: uses the identity of the file named by the given pathname and the proj_id to generate a key_t type System V IPC key,-使用pathname和proj_id生成一个用于System V IPC的唯一标识
参数:pathname:路径、文件名的组合,该文件必须存在 proj_id:不为0 的整数
返回值:成功返回key(共享内存段的名字),失败返回-1
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdlib.h>
int main()
{
//生成唯一表示
key_t _key = ftok("./test",0x6666);
if(_key < 0)
{
perror("ftok fail");
exit(-1);
}
printf("%d\n",_key);
return 0;
}
2)使用shmget创建共享内存
int shmget(key_t _key,size_t size, int shmflg);
作用:根据唯一标识_key(共享内存段的名字)创建指定大小的共享内存
参数:_key:通过ftok函数生成的唯一标识 size:需要创建共享内存的大小
由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的(常用两个选项如下)
IPC_CREAT:创建一个新的共享内存段,如果未使用这个标志则检查与_key关联的段并检查该用户是否有查看这个段的权限
IPC_EXCL:如果段已经存在,则与IPC_CREAT一起使用以确保失败。
成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
注意:size在指定时最好以页大小的整数倍进行指定,因为系统在分配的时候按照页面大小的整数倍进行分配。
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdlib.h>
int main()
{
//生成唯一表示
key_t _key = ftok("./test",0x6666);
if(_key < 0)
{
perror("ftok fail");
exit(-1);
}
printf("%d\n",_key);
//创建共享内存
int id = shmget(_key,4096,IPC_CREAT | IPC_EXCL | 0666);
if(id < 0)
perror("create shmget fail");
printf("%d\n",id);
return 0;
}
3)使用shmat函数将共享内存段连接到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
作用:将共享内存段连接到进程地址空间
参数:shmid:共享内存标识(shmget函数的返回值)shmaddr:指定连接的地址
它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功则返回指向该共享内存第一个字节的指针,失败返回-1
注意:当shmaddr为NULL时会自动选择一个合适的地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
4)使用shmdt函数将共享内存段与当前进程脱离
int shmdt(const void *shmaddr);
shmaddr: 由shmat所返回的指针
返回值:成功返回0,失败返回-1
注意:将进程与共享内存段脱离不等于删除共享内存段
5)使用shmctl函数控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
:由shmget返回的共享内存标识码 buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
cmd:将要采取的动作(有三个可取值,第三个最常用)
IPC_STAT 将信息从与shmid相关的内核数据结构复制到buf指向的shmid_ds结构中。 调用方必须对共享内存段具有读取权限。
IPC_SET 将buf指向的shmid_ds结构的某些成员的值写入与此共享内存段关联的内核数据结构中,同时还要更新其shm_ctime成员。 可以更改以下字段:shm_perm.uid,shm_perm.gid和shm_perm.mode(最低9位)。 调用过程的有效UID必须与共享内存段的所有者(shm_perm.uid)或创建者(shm_perm.cuid)相匹配,否则必须为调用者特权。
IPC_RMID 标记要销毁的线段。只有在最后一个进程将其分离后(即,关联结构shmid_ds的shm_nattch成员为零时),该段才真正被销毁。 调用者必须是所有者或创建者,或具有特权。 如果已将某个段标记为要销毁,则将设置IPC_STAT检索的关联数据结构中shm_perm.mode字段的(非标准)SHM_DEST标志。 否则,出错的页面将保留在内存或交换中。
返回值:成功返回0,失败返回-1
其中buf是一个结构体指针,该结构体中保存了该共享内存段的权限、连接时间、脱离时间、改变时间、创建者等信息。
2、示例
comm.h
#pragma once
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<stdlib.h>
#include<unistd.h>
#define SHM_SIZE 4096
int create_shmid();
int destory_shm(int shmid);
int commshm(int size,int flags);
int get_shmid();
comm.c
#include"comm.h"
//创建共享内存
int commshm(int size,int flags)
{
//生成唯一表示
key_t key = ftok("./test",0x6666);
if(key == -1)
{
//error
perror("create key error");
exit(-1);
}
//创建共享内存
int shmid = shmget(key,size,flags);
if(shmid < 0)
{
perror("create shmid error");
exit(-1);
}
return shmid;
}
//销毁
int destory_shm(int shmid)
{
if(shmctl(shmid, IPC_RMID, NULL) < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
//创建
int create_shmid()
{
return commshm(SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
}
//获取shmid
int get_shmid()
{
int shmid = commshm(SHM_SIZE,IPC_CREAT);
return shmid;
}
client.c
#include"comm.h"
int main()
{
//获取shmid
int shmid = get_shmid();
sleep(1);
//将共享内存段连接到进程地址空间
char *addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i<26){
//向共享内存段写入数据
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
//脱离
shmdt(addr);
sleep(2);
return 0;
}
server.c
#include"comm.h"
int main()
{
//创建共享内存段
int shmid = create_shmid();
//连接共享内存段到进程地址空间
char *addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i++<26){
printf("client# %s\n", addr);
sleep(1);
}
//脱离内存段
shmdt(addr);
sleep(2);
//销毁共享内存
destory_shm(shmid);
return 0;
}
注意:IPC资源的生命周期随进程;共享内存底层不提供任何同步与互斥机制。