进程间通信基本概念
IPC(Inter Process Communication)
进程间通信
进程通信就是不同进程之间进行信息的交换或传播
为什么进程之间实现通信和困难
因为进程之间具有独立性,数据独立,程序可能独立也可能不独立(父子进程的程序时一样的)
所以要想进行进程间的数据交换,必须借助一个第三方的资源,进程一可以对这个资源进行读写,进程二也可以对这个资源进行读写,如图所示
这个第三方的资源实际上就时OS提供的内存区域
进程通信的本质
进程通信的本质就时让不同进程访问同一份第三方资源,这个第三方资源可以由OS的不同模块提供,因此出现在不同的进程间的通信方式
进程通信的目的
1.数据传输:一个进程传输它的数据给另一个进程
2.资源共享:多个进程之间共享同样的资源
3.通知事件:一个进程需要向另一个或另一组进程发送消息,通知它们发生了某种事件,比如进程终止时需要通知其父进程(信号)
4.进程控制:有些进程希望完全控制另一个进程的执行(Bebug进程),此时控制的进程希望能够阻拦另一个进程所有陷入和异常,并且能够及时知道它的状态改变
管道
一个进程连接到另一个进程的数据流称为管道
管道符(|)
管道符就时利用这种管道的通信方式
进程1 | 进程2
ls / | wc -l
匿名管道
用于有血缘关系的进程间的通信
本质
进程通信的本质就是让不同的进程访问同一份第三方资源,而管道中第三方资源就是同一份打开的文件,父子进程都可以对这份文件进行读写,从而实现进程间的通信。
然后就是这个打开文件的位置,这个文件不可能放在磁盘中,因为磁盘的IO效率太慢了,所以它应该是在内存之中
pipe函数
创建管道的函数
int pipe(int pipefd[2])
pipefd[0] pipefd[1] 管道的读写端,传出参数
返回值:0 或 -1
因为管道是实现父子进程间的通信的,所以一般要配合fork函数使用
实现子进程向父进程中写入hello
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(int argc, char* argv[])
{
int pipefd[2];
pipe(pipefd);//创建匿名管道,pipefd[0]读端,pipefd[1]写端
int pid = fork();
if(pid == 0){//子进程写
close(pipefd[0]);//关闭读端
// close(pipefd[1]);
int write_count = write(pipefd[1],"hello",6);
printf("write_count = %d\n",write_count);
}else{//父进程读
close(pipefd[1]);//关闭写端
char buf[1024]={'\0'};
int read_count = read(pipefd[0],buf,sizeof(buf));
//当写段开着但管道中没有数据的时候,不会直接返回-1,而是阻塞等待管道中数据的出现
//如果写段没开,不会阻塞等待,直接然后0
printf("read_count = %d,buf = %s\n",read_count,buf);
}
while(1);
return 0;
}
管道中状态的描述
初始时管道中没有数据:写端write返回成功读入的字节数,读端read阻塞等待管道中数据的产生
管道中有数据但没满:写端write返回成功读入的字节数,读端read返回成功读入的字节数
管道中数据满了:写端write阻塞等待,读端read返回成功读入的字节数
写端关闭:读端read返回成功读入的字节数,如果没有写入,总会有读完的时候,这个时候不会阻塞等待直接返回0
读端关闭:写端write会异常终止进程(被SIGPIPE信号杀死)
模拟ls / | wc -l
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
int pipefd[2];
pipe(pipefd);
int pid = fork();
if(pid > 0){//父进程
//ls /内容由stdout转到管道的写端
close(pipefd[0]);
dup2(pipefd[1],1);
execlp("ls","ls","/",NULL);
}else{//子进程
//wc -l读的由stdin到读端
close(pipefd[1]);
dup2(pipefd[0],0);
execlp("wc","wc","-l",NULL);
}
while(1);
return 0;
}
命名管道
创建管道文件有两种方式
mkfifo fifo文件
调用函数mkfifo(path,0644)
mkfifo函数
创建命名管道文件
int mkfifo(const char* pathname,mode_t mode)
wififo.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(int argc, char* argv[])
{
// mkfifo("fifo",0644);
int fd=open("./fifo",O_WRONLY);
int write_count = write(fd,"hello",6);
printf("write_count = %d\n",write_count);
return 0;
}
riffo.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
int fd=open("./fifo",O_RDONLY);
char buf[1024]={'\0'};
int read_count = read(fd,buf,sizeof buf);
printf("read_count = %d,buf = %s\n",read_count,buf);
return 0;
}
如果写端没开,读端read会阻塞等待写端的打开(和匿名管道不同)
如果读端没开,写端write会阻塞等待读端的打开(和匿名管道不同)
命名管道的打开规则
在没有设置非阻塞属性的条件下,fifo读端和写端只要有一个没有打开,就会阻塞等待另一个的打开
在设置非阻塞属性的条件下,fifo的读端没有打开,立即返回失败,fifo写端没有打开,立即返回成功
user1和user2聊天实现
main.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(int argc, char* argv[])
{
mkfifo("./u1r_u2w_fifo",0644);
mkfifo("./u1w_u2r_fifo",0644);
return 0;
}
u1.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
//首先发送和结束数据不应该有先后顺序,所以应该一个进程接收,一个进程发送
int pid = fork();
if(pid == 0){//子进程,发送数据
//打开u1的写管道
int fd = open("./u1w_u2r_fifo",O_WRONLY);
while(1){
char buf[1024];
//从终端把输入读到buf里面
int r_count = read(0,buf,sizeof(buf));
//buf中的数据写到写管道里面
write(fd,buf,r_count);
}
}else{//父进程接收数据
//打开u1的读管道
int fd = open("./u1r_u2w_fifo",O_RDONLY);
while(1){
char buf[1024];
//从管道中把数据读到buf中
int r_count = read(fd,buf,sizeof buf);
//把buf中的数据写道终端中
write(1,buf,r_count);
}
}
return 0;
}
u2.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
//首先发送和结束数据不应该有先后顺序,所以应该一个进程接收,一个进程发送
int pid = fork();
if(pid == 0){//子进程,发送数据
//打开u2的写管道
int fd = open("./u1r_u2w_fifo",O_WRONLY);
while(1){
char buf[1024];
//从终端把输入读到buf里面
int r_count = read(0,buf,sizeof buf);
//buf中的数据写到写管道里面
write(fd,buf,r_count);
}
}else{//父进程接收数据
//打开u2的读管道
int fd = open("./u1w_u2r_fifo",O_RDONLY);
while(1){
char buf[1024];
//从管道中把数据读到buf中
int r_count = read(fd,buf,sizeof buf);
//把buf中的数据写道终端中
write(1,buf,r_count);
}
}
return 0;
}
死锁问题
前提:我们都知道命名管道假设没有设置非阻塞属性的话,读写俩端必须都打开才能进行数据的传输,只要有一个没有打开,另外一个就会阻塞等待。比如写端没有打开读端就会阻塞等待写端的打开
如果想实现进程间互相通信,必须有俩个命名管道文件,假设叫fifo1和fifo2,现在假如在进程1中fifo1的写端打开了,进程2中fifo2的读端打开了,这种情况就会造成死锁问题,因为进程1在阻塞等待fifo1读端的打开,fifo2在阻塞等待fifo2写端的打开,双方互相等待,造成死锁问题
解决方法:让fifo1的读端打开,fifo1的写端打开,fifo2的读端打开,fifo2的写端打开,放在四个进程中,单独执行,及时阻塞,但并不影响执行
管道通信总结
进程间的通信就如它表面上一样,即不同进程之间进行信息的交流,而本质上是因为进程与进程之间是独立的,也就是说修改进程1中的数据对进程2不会产生影响,所以为了实现进程之间的交流必须引入一个第三方资源,不同进程之间都可以对第三方资源进程读写操作,这样就完成了进程之间的通信。因为第三方资源可以由操作系统的不同模块提供,也就产生了各种各样的通信方式,管道就是其中之一,管理利用的第三方资源是内存中的文件(也就虚拟内存)。管道又分为匿名管道和命名管道。匿名管道只能用于有血缘关系进程间的通信,因为匿名管道相当于一个没有文件名的虚拟文件,只有在父子进程中,它打开该虚拟文件的fd才是一样的,这样就可以对同一个打开的虚拟文件进行读写操作,从而实现进程间的通信。而命名管道则可以实现任意两个进程间的通信,因为它是有名字的,不同进程中通过open它的路径就是找到该虚拟文件。
以上就是我对管道通信的一些理解。
内存映射
共享内存
俩个进程共享同一块虚拟内存
什么是内存映射?
内存映射指的是将磁盘文件中的数据映射到内存之中,这样修改内存就相当于修改了磁盘
mmap函数
void char* mmap(void *addr, size_t length,int prot, int flags, int fd, off_t offset)
参数描述:
addr:磁盘文件映射到内存中的地址,如果不特殊指定,为NULL即可
length:要映射多少字节,一般为1024
prot:映射区域的保护方式是什么
PROT_READ//可读 PROT_WRITE//可写
flags:映射这段区域的属性是什么
MMAP_SHARED//创建共享映射,内存的写入即磁盘文件的写入 MMAP_PRIVATE//创建私有映射 MMAP_ANONYMOUS//创建匿名映射
fd:映射磁盘文件的文件描述符
offset:文件偏移量
munmap函数
解除映射区域
int munmap(void *addr, size_t length);
匿名映射
匿名映射相当于没有磁盘文件映射到内存之中,这段映射内存会被初始化为0
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>
int main(int argc, char* argv[])
{
char *ptr = (char*)mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
int pid = fork();
if(pid == 0 ){//子进程
strcpy(ptr,"hello world");
printf("child ps ptr = %s\n",ptr);
}else{//父进程
sleep(1);
printf("father ps ptr = %s\n",ptr);
}
return 0;
}
文件映射
磁盘中文件的数据映射到内存中,对内存的修改就相当于对磁盘文件的修改
wmmap.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{
int fd = open("./mmap.txt",O_RDWR | O_CREAT,0644);
ftruncate(fd,1024);
char *ptr=(char*)mmap(NULL,1024,PROT_WRITE | PROT_READ,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED){
perror("mmap error:");
exit(1);
}
int i = 0;
while(1){
sprintf(ptr,"-------------%d-----------\n",i++);
sleep(1);
}
return 0;
}
rmmap.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{
int fd=open("./mmap.txt",O_RDWR);
char *ptr=(char*)mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED){
perror("mmap error:");
exit(1);
}
while(1){
printf("ptr = %s\n",ptr);
sleep(1);
}
return 0;
}
网络问题
sudo nmcli network off
sudo nmcli network on
消息队列
message queue
消息队列的优点
如果要实现一条一条消息发送的这种通信方式,管道和内存映射没有办法满足,因为它俩本质就是文件,没办法区分这是第一条消息,那是第二条,除非你加上某种特定的方式
基本概念
消息队列是内核中维护的消息链表,是面向消息进行通信的,每次读取一条完整的消息。每条消息具有的属性为:
1.消息本身内容
2.消息数据部分长度
3.表示消息优先级的整数
头文件
#include <fcntl.h> #include <sys/stat.h> #include <mqueue.h>
mq_open函数
打开消息队列
mqd_t mq_open(const char* name,int flags)
mqd_t mq_open(const char* name,int flags,mode_t mode,struct mq_ *attr)
1.返回值是消息队列的fd,mqd
2.name消息队列的名字
3.flags,mq的访问模式,O_RDONLY,O_WRONLY,O_RDWR,O_NONBLOCK,O_CREAT,O_EXCL
4.mode,mq的访问权限,0644
5.attr,mq的属性
mq_close函数
关闭消息队列
int mq_close(mqd_t mqd);
mq_getattr函数
获取消息队列属性
int mq_getattr(mode_t mqd,struct mq_attr *attr);
mq_setattr函数
设置消息队列属性
int mq_setattr(mode_t mqd,struct mq_attr *newattr,struct mq_attr *oldattr);
mq_send函数
向消息队列中发送消息
int mq_send(mqd_t mqd,const char *buf,size_t msg_len,unsigned int msg_prio)
mq_receive函数
接收消息队列中传来的消息
int mq_receive(mqd_t mqd,const char *buf,size_t msg_len,unsigned int * msg_prio)
mq_unlink函数
删除消息队列队列名
int mq_unlink(const char* name);
消息队列的四个属性
struct mq_attr{ long mq_flags;//是否阻塞 long mq_maxmsg;//最大消息数 long mq_msgsize;//最大消息的大小,1024*9 long mq_curmsgs;//当前消息的个数 }
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<mqueue.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{
mqd_t mqd=mq_open("/msgqueue",O_RDWR | O_CREAT,0644,NULL);
int w_count = mq_send(mqd,argv[1],strlen(argv[1]),atoi(argv[2]));
if(w_count == -1){
perror("w_count:");
exit(1);
}
return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<mqueue.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{
mqd_t mqd=mq_open("/msgqueue",O_RDWR,0644,NULL);
char buf[8192];
int flags;
int r_count = mq_receive(mqd,buf,sizeof buf,&flags);
if(r_count == -1){
perror("w_count:");
exit(1);
}
printf("buf = %s\n",buf);
return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<mqueue.h>
#include<stdlib.h>
#include<sys/stat.h>
int main(int argc, char* argv[])
{
int mqd = mq_open("/msgqueue",O_RDONLY);
if(mqd == -1){
perror("open mq error:");
exit(1);
}
struct mq_attr attr;
int ret = mq_getattr(mqd,&attr);
struct mq_attr attr_new;
attr.mq_maxmsg=20;
attr_new=attr;
mq_setattr(mqd,&attr_new,&attr);
if(ret == -1){
perror("getattr error:");
exit(1);
}
printf("mq_flags = %lu\n",attr_new.mq_flags);
printf("mq_maxmsg = %lu\n",attr_new.mq_maxmsg);
printf("mq_msgsize = %lu\n",attr_new.mq_msgsize);
printf("mq_curmsgs = %lu\n",attr_new.mq_curmsgs);
return 0;
}