进程间通信(Interprocess communication)
一、概述:
进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。进程间通信是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。
二、目的:
(1)数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
(2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
(3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
(4)资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
(5)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
- 进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。
三、种类:
进程间通信主要包括:管道, 系统IPC(包括消息队列,信号,共享内存),套接字(SOCKET)
System V 标准
管道—用于进程间的数据传输
共享内存—用于进程间的数据共享
消息队列—用于进程间的数据传输
信号量—用于实现进程间的控制
四、管道:
1、本质:
内核中的一块缓冲区—通过半双工通信实现数据传输,通过让多个进程都能访问到同一块缓冲区,进而实现进程间通信。
2、管道的分类:
(1)匿名管道PIPE:这块缓冲区在内核中没有标识,通常有两种限制:
- 一是单工,只能单向传输;
- 二是只能在父子或者兄弟进程间使用.
( 2)流管道s_pipe: 和匿名管道一样,只是去除了匿名管道中的第一种限制,为半双工,可以双向传输。
在创建管道时,操作系统会提供两个操作句柄(文件描述符),其中一个从管道读取数据,一个向管道写入数据,子进程通过复制父进程的方式,获取到管道的操作句柄,和父进程,兄弟进程访问同一个管道,进而实现进程间通信。
( 3)命名管道:name_pipe, 在内核中有标识的缓冲区(标识符是一个可见于文件系统的管道文件), 其他进程你可以通过这个标识符,找到这块缓冲区(通过打开统一个管道文件,进而访问到同一块缓冲区),可以在许多并不相关的进程之间进行通讯.
3、接口
1.匿名管道
int pipe(int pipefd[2]);//创建一个匿名管道,向用户通过参数pipefd返回管道的操作句柄
//pipefd[0]---从管道读取数据
//pipefd[1]---向管道写入数据
//返回值--- 0,成功; -1,失败
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
char buf[1024] = {0};
int pipefd[2] = {0};
int ret = pipe(pipefd);//在创建子进程之前创建管道,子进程才会复制到创建的管道
if(ret < 0)
{
perror("pipe error:");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error:");
return -1;
}
else if(pid == 0)//子进程
{
int fd = read(pipefd[0],buf,1023);//若管道中没有数据,则read就阻塞
if(fd < 0)
{
perror("read error:");
return -1;
}
printf("子进程读取成功~~\n");
printf("buf[%s]-[%d]\n",buf,fd);
}
else//父进程
{
char* ptr = "这是一个测试匿名管道的demo~~";
if(write(pipefd[1],ptr,strlen(ptr)) < 0)//若管道写满,则write就阻塞
{
perror("write error:");
return -1;
}
printf("父进程写入成功~~~\n");
}
while(1)
{
printf("---------------%d\n",getpid());
sleep(1);
}
return 0;
}
特性:
(1)若管道中没有数据,则read会阻塞;若管道写满了,则write会阻塞;----管道自带同步与互斥。
- 同步:对临界资源访问的合理性,保证临界资源访问的安全性
- 互斥:通过保证同一时间只有一个进程能够访问临界资源(一次仅允许一个进程使用的资源),保证临界资源访问的安全性,对管道进行数据操作的大小不超过PIPE_BUF(4096)的时候,则保证操作的原子性。
(2)若管道所有的写端被关闭(当前没有进程继续在写入数据了),read读完管道中的数据之后,就不会阻塞,返回0。
(3)若管道所有读端被关闭(当前没有进程读数据了),继续write就会触发异常,程序退出。
这个命令就利用了一个管道:
ps -ef | grep ssh
,ps -ef
是的将打印到标准输出中,grep sshd
是从标准输入读取数据过滤sshd打印到标准输出中,而|
将两者链接,ps -ef的结果经过grep sshd的过滤打印到标准输出上,这就用到了管道。
简单模拟实现一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int pipefd[2];
int ret = pipe(pipefd);//创建子进程之前创建管道
if (ret < 0)
{
perror("pipe error");
return -1;
}
int pid1 = fork();//ps进程
if(pid1 < 0)
{
perror("fork1 error:");
return -1;
}
else if (pid1 == 0)
{
dup2(pipefd[1], 1);//将ps -ef命令打印在标准输出的内容重定向到管道的写入端
execl("/usr/bin/ps", "ps", "-ef", NULL);//替换程序ps -ef
exit(0);
}
int pid2 = fork();//grep进程
if (pid2 == 0)
{
close(pipefd[1]);//进入grep进程时,grep要从管道读取端读取数据,先要关闭管道的写入端,read才不会阻塞
dup2(pipefd[0], 0);//grep要从标准输入读取数据,这是就得将标准输入重定向到管道的读取端
execl("/usr/bin/grep", "grep", "sshd", NULL);//替换程序grep sssh
exit(0);
}
//父进程
close(pipefd[0]);//父进程既不读也不写,将写端和读端关闭。
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}
2.命名管道:
mkfifo 文件名.fifo //命令操作
int mkfifo(const char* pathname,mode_t mode);//系统调用函数操作,创建命名管道文件
//pathname---管道文件名称
//mode---文件权限
//返回值--- 0,成功; -1,失败
特性:若管道文件以只读方式代开,则会阻塞,直到这个管道文件文件被以写的方式打开;
若管道文件以只只写方式代开,则会阻塞,直到这个管道文件文件被以读的方式打开;
若管道文件以只读写方式代开,则不会阻塞。
//fifo_write ---命名管道写入端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
umask(0);
char* file = "./test.fifo";
int ret = mkfifo(file,0664);
if(ret < 0 && errno != EEXIST)
{
perror("mkfifo error:");
return -1;
}
printf("creat fifo success\n");
int fd = open(file,O_WRONLY);
if(fd < 0)
{
perror("open error:");
return -1;
}
printf("open success\n");
while(1)
{
char buf[1024] = {0};
scanf("%s",buf);
ret = write(fd,buf,strlen(buf));
if(ret < 0)
{
perror("write error:");
return -1;
}
printf("写入数据buf[%s]\n",buf);
}
close(fd);
return 0;
}
//fifo_write ---命名管道读取端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
umask(0);
char* file = "./test.fifo";
int ret = mkfifo(file,0664);
if(ret < 0 && errno != EEXIST)
{
perror("mkfifo error:");
return -1;
}
printf("creat fifo success\n");
int fd = open(file,O_RDONLY);
if(fd < 0)
{
perror("open error:");
return -1;
}
printf("open success\n");
while(1)
{
char buf[1024] = {0};
ret = read(fd,buf,1023);
if(ret < 0)
{
perror("read error:");
return -1;
}
else if(ret == 0)//read函数没有读到数据,返回0,说明所有写端被关闭
{
printf("all write close\n");
return -1;
}
printf("读取数据buf[%s]\n",buf);
}
close(fd);
return 0;
}
所有管道的特性:
- 管道生命周期随进程:管道文件被创建后内核暂时不给他分配缓冲区,只有以读写方式打开之后才会分配,进程退出后,管道缓冲区也随之释放。
- 半双工通信:数据只能在一个方向上移动
- 提供字节流服务:有序,可靠,连接的字节传输,传输比较灵活。(比如传输较大的数据,会慢慢一点一点的传输)
五、共享内存(share memory)
1、简介
步骤:
- 1、创建共享内存(开辟物理内存空间–具有标识符)
- 2、将共享内存映射到各个进程的虚拟地址空间
- 3、各个进程直接通过虚拟地址操作共享内存
- 4、操作完毕,解除映射关系
- 5、释放共享内存
最快的进程间通信方式:是因为共享内存直接通过虚拟地址映射直接访问内存,而其他方式都是内核中的缓冲区,通信都会涉及到用户态和内核态之间的两次数据拷贝,但是共享内存不需要,因此共享内存是最快的通信方式。
2、接口
1.系统调用接口
int shmget(key_t key,int size_t size,int flag);
//创建共享内存
//key---共享内存标识符,多个进程通过相同的标识符可以操作同一块共享内存
//size--共享内存大小
//flag---IPC_CREAT(不存在则创建,存在则打开)|IPC_EXCL(存在则报错,不存在则创建)|权限
//返回值---成功返回操作句柄,失败返回-1
void* shmat(int shmid,void* shmaddr,int shmflag);
//建立映射
//shmid---共享内存的操作句柄
//addr---映射到虚拟地址空间的首地址,通常置空
//shflag---通常置0,可读可写; SJM_RDONLY,只读
//返回值---成功返回映射的虚拟地址空间首地址,通过这个地址堆内存进行操作;失败返回-1。
int shmdt(void* shmstart);
//解除映射
//shmstart---映射到虚拟地址空间的首地址
//返回值---成功返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//操作共享内存
//shmid---操作句柄
//cmd---具体对共享内存要进行的操作---IPC_RMID-删除共享内存
2.命令
ipcs---查看共享内存信息
ipcm [shmid]---删除进程间通信资源
-m---查看共享内存
-q---查看消息队列
-s---查看信号量
当删除共享内存的时候,共享内存并不会被立即删除(因为有可能会造成正在访问的进程奔溃),而是将Key值修改为0,
状态改为(Dest)Destory表示这块内存将不再继续接受连接映射,当这块共享内存的映射连接数为0时,则自动会被释放。
nattch为当前连接这块共享内存进程的个数
//write_shm.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
umask(0);
char* file = "./test.fifo";
int ret = mkfifo(file,0664);
if(ret < 0 && errno != EEXIST)
{
perror("mkfifo error:");
return -1;
}
printf("creat fifo success\n");
int fd = open(file,O_WRONLY);
if(fd < 0)
{
perror("open error:");
return -1;
}
printf("open success\n");
while(1)
{
char buf[1024] = {0};
scanf("%s",buf);
ret = write(fd,buf,strlen(buf));
if(ret < 0)
{
perror("write error:");
return -1;
}
printf("写入数据buf[%s]\n",buf);
}
close(fd);
return 0;
}
//read_shm.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main()
{
umask(0);
char* file = "./test.fifo";
int ret = mkfifo(file,0664);
if(ret < 0 && errno != EEXIST)
{
perror("mkfifo error:");
return -1;
}
printf("creat fifo success\n");
int fd = open(file,O_RDONLY);
if(fd < 0)
{
perror("open error:");
return -1;
}
printf("open success\n");
while(1)
{
char buf[1024] = {0};
ret = read(fd,buf,1023);
if(ret < 0)
{
perror("read error:");
return -1;
}
printf("读取数据buf[%s]\n",buf);
}
close(fd);
return 0;
}
3、特性:
1、最快的进程间通信方式;
2、生命周期随内核(消息队列和信号量数组也是)
注意事项:共享内存的操作是不安全的,不会具备同步与互斥的关系,需要操作用户进行控制。
六、消息队列
1、本质:
内核中的一个队列,多个进程通过向同一个队列添加节点和获取节点,传输一个有类型(优先级)的数据块。
struct msgbuf
{
long mtype; /* message type, must be > 0 ,优先级*/
char mtext[1]; /* message data ,数据块大小*/
};
2、接口:
//创建消息队列
msgget();
//添加节点
msgsnd();
//获取节点
msgrcv();
//操作-删除消息队列(IPC_RMID)
msgctl();
3、特性:
- 自带同步与互斥
- 生命周期随内核
- 数据传输自带优先级
来源:CSDN
作者:托马斯.杨
链接:https://blog.csdn.net/weixin_43886592/article/details/103999048