进程间通信基本概念
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*9long 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;
}