进程通信
进程有独立性,但是进程间也可以有信息的交流,叫进程间通信,进程间通信可以通过匿名管道,命名管道,共享内存来进行通信。
进程间通信
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变。
匿名管道通信
- 匿名管道只能对有血缘关系的父子进程间通信。因为是匿名,找不到进程的路径,所以要在父子关系的进程使用匿名管道。
- 创建匿名管道使用系统调用pipe(),成功创建返回值为0,创建管道需要一个数组pipefd来接收写管道和读管道这两个文件描述符进行保存。
- 匿名管道中,父进程如果是写管道,就要把读管道close关闭,然后子进程只能是读管道,把子进程的写管道进行关闭。反之一样。
- pipefd[0]为读端,pipefd[1]为写端。
- 读写管道正常,读进程要等待写进程写了才读,读管道要等待写管道。
- 写端非常块,读端非常慢,写端写慢管道,读端就会等待,等待读端读完。
- 如果写管道关闭了,读管道的read()系统调用接口读到返回值为0,用户可通过条件判断来退出或让做为读端的父进程执行其他的任务,或创建新的子进程执行其他的任务。
- 如果读管道关闭,写管道无意义了,进程直接异常退出。信号码为13。
- 匿名管道是内核的一段缓冲区,如果写端一直写,读端不取走数据,内核数据到达65536字节读端则阻塞。但是管道大小为521*8字节,有16个管道。
- 所以一下代码我采用父进程作为读端。如果父进程作为写端,不管父进程有没有等待子进程,父进程退出的话,就会造成子进程成为孤儿进程,且进程是异常退出的。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <stdbool.h>
#include <string.h>
using namespace std;
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);//管道创建,只能有一个读和一个写管道的文件
if(n != 0)
{
cout << "创建管道失败" << endl;
assert(n == 0);
}
else if(n == 0)
{
cout << "创建管道成功" << endl;
}
cout << "管道pipefd[0]: " << pipefd[0] << endl;
cout << "管道pipefd[1]: " << pipefd[1] << endl;
pid_t id = fork();
if(id == 0)//子进程
{
close(pipefd[1]);
char rbuff[1024];
int cnt = 5;
while(true)
{
ssize_t len = read(pipefd[0],rbuff,sizeof(rbuff) - 1);
if(len > 0)
{
rbuff[len] = 0;
cout << "I am child process my pid is: " << getpid() <<": " << rbuff << endl;
}
if(len == 0)
{
close(pipefd[0]);
cout << "写端停止写了,读端读到0就退出" << endl;
break;
}
cnt--;
if(cnt == 0)
{
close(pipefd[0]);
cout << "关闭读端" << endl;
break;
}
sleep(1);
}
};
while(true)
{
close(pipefd[0]);
char wbuff[1024];
while(true)
{
snprintf(wbuff,sizeof(wbuff),"Hello,I am father process,my pid is %d",getpid());
write(pipefd[1],wbuff,strlen(wbuff));
sleep(1);
}
}
cout << "#####################" << endl;
int statu = 0;
int ret = waitpid(id,&statu,0);
if(ret == id)
{
cout << "等待成功........" << endl;
printf("退出码: %d,退出信息:%s\n",(statu >> 8) & 0xFF,strerror((statu >> 8) & 0xFF));
printf("退出信号:%d\n",statu & 0x7F);
}
return 0;
}
//父进程作为写端,读端以但关闭,进程异常退出,linux查看echo $?退出信号为13
命名管道
匿名管道只能是父子进程之间通信,命名管道可以是没有任何血缘关系的进程进行通信交流。命名管道会创建一个特殊的管道文件。由系统调用接口mififo(*pathname,mode)函数接口创建。*pathname为管道文件路径名称,mode表示文件打开权限。
创建管道的文件一般都是服务端的进程创建。客户端文件就不需要创建了。命名管道是基于先进先出的原理的。写端在队头数据一直写到队尾,读端在对头取数据一直到队尾。
当服务的程序执行的时候,会有一个fifo的文件,就是管道文件,如果服务端创建的管道还没有完成,打开客户端进程是无意义的。如果由了管道的话,先打开客户端进程是可以的,所以一般是先执行服务端进程再执行客户端。
进程可以通过共享内存来进行通信
匿名管道和命名管道是通过使用一个共同的文件进行通信,而共享内存使用的是同一个内存,不需要文件。不同的进程的虚拟地址空间指向同一个物理内存的区域。
如何指向同一块内存
- 不同的进程有一个叫shmid_ds的结构体对象,描述着共享内存。有着共享内存的大小,进程挂接的时间和进程断掉挂接的时间等其他共享内存的属性。要创建共享内存,两个不同的进程要看到同一份的共享内存,就需要看到该共享内存的唯一的标识key_t。
- 我们可以使用key_t ftok(const char *pathname, int proj_id)函数来创建一个唯一标识符,字符串pathname可以是任意字符串,但最好使用当前文件夹的路径作为字符串,proj_id也可以是任意数字,不过最好大一点。该函数的返回值其实是一个地址,我们可以使用该函数返回的值来继续创建共享内存。
- 使用shmget来创建共享内存。int shmget(key_t key, size_t size, int shmflg);如果创建成功,返回创建共享内存的标识id,如果创建失败,返回-1,key值即ftok函数生成的地址变量,size即要使用多少字节的共享内存,shmflg是创建共享内存的模式,IPC_CREAT | IPC_EXCL | 0666,即没有就创建,然后以权限0666的方式使用共享内存,另一个进程的shmflg填0即可。这个时候共享内存就创建成功。IPC_CREAT共享内存不存在就创建,IPC_CREAT | IPC_EXCL 即不存在共享内存就创建,如果存在,就出错返回, IPC_EXCL 不单独使用,单独使用无意义。
- 使用shmat把进程挂接到共享内存 有共享内存之后,两个进程要挂接到共享内存,使用系统调用shmat。void *shmat(int shmid, const void *shmaddr, int shmflg); shmid即创建的共享内存唯一识别id,shmaddr是指定地址挂接,但我们可以使用NULL自行找地址挂接,shmflg填0
- 使用shmdt脱离挂接int shmdt(const void *shmaddr);shmaddr填NULL,脱离挂接不等于删除共享内存。
- 使用shmctl删除共享内存共享内存需要用户自行删除,如果不删除,该共享内存的生命周期随内核的。int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmid即共享内存的id,cmd填IPC_RMID,buf填NULL即可删除共享内存。
- Linux下可以使用指令ipcs -m查看共享内存信息,ipcrm -m id指令级别删除共享内存。ipcs -m查看指令。
- 创建共享内存标识->创建共享的物理内存->通过标识将两个进程的虚拟地址映射到同一物理内存->脱离共享内存->删除共享内存。
//共用的文件
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
const char *pathname = "/home/USR/SHM";//是当前文件路径即可
const int proj_id = 0x11223344;
const int size = 4096;
using namespace std;
key_t Great_shm_key()
{
key_t ret = ftok(pathname,proj_id);
if(ret < 0)
{
cout << "创建key失败" << endl;
}
return ret;
}
//创建key值
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "commond.hpp"
using namespace std;
int main()
{
key_t key = Great_shm_key();
cout << "key: " << key << endl;
int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL|0666);//服务端创建共享内存
if(shmid < 0)
{
cout << "创建shmid失败" << endl;
exit(0);
}
char* p = (char*)shmat(shmid,NULL,0);
int cnt = 30;
while(true)
{
cout << p << endl;
sleep(1);
cnt--;
if(cnt == 0)
{
break;
}
}
shmdt(NULL);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
//用户端
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "commond.hpp"
using namespace std;
int main()
{
key_t key = Great_shm_key();
cout << "key: " << key << endl;
int shmid = shmget(key,size,0);
if(shmid < 0)
{
cout << "创建shmid失败" << endl;
exit(0);
}
char* p = (char*)shmat(shmid,NULL,0);
char c = 'a';
for(int i = 0;i < 26;i++)
{
cout << c << endl;
p[i] = c++;
sleep(1);
}
shmdt(NULL);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
注意:上述代码一定先要服务端先启动,因为服务端创建共享内存。共享内存没有说哪个进程先要等待哪个进程输入信息才运行的,没有同步的机制,所以上述代码要有同步的功能的话,可以使用命名管道进行等待。
信号量
共享内存进行通信,如果多个进程对同一个共享内存进行修改,就会造成数据的冲突,进程A会覆盖进程B的数据,为了防止多个进程对同一共享资源的竞争,需要使用信号量这种保护机制,信号量就是一个整形的计数器,主要实现进程或线程之间的同步和互斥,而不是对通信的数据进行缓存的。
信号量表示的是资源的数量,有两种原子的操作,P和V。
- P操作会对计数器的数量进行减一操作,如果减一操作的计数器信号<0,则表明资源已经使用完了,需要进行阻塞等待,如果>=0则表示还有资源可以使用,则进程还可以继续执行获取资源。
- V操作会将该信号量的值加一,在V操作中,如果信号量<=0,说明P操作的进程被阻塞,V操作会进行加一并唤醒P操作的进程。如果信号量>0,则说明P操作的进程未被阻塞。V操作的进程可以不被阻塞。
- P操作时使用共享资源之前的操作,V操作是离开共享资源之后的,需要PV一起使用。
具体过程
- 当初始化一个信号量为1的时候,进程A先执行,进行P操作进行减一操作,信号量>=0表示共享资源可用。
- 此时进程B访问共享资源,进行P操作进行减一,信号量 <0表示资源不足不许访问,进程B阻塞。
- 进程A结束任务进行V操作加一,发现为信号量为0,唤醒阻塞的B进程。
- B进程被唤醒,任务也执行完毕,进行V操作,信号量重新为1了。
使用信号量进行生产消费
- 此时A进程作为生产者,B进程作为消费者,AB进程相互合作与依赖,此时A为生产者,此时初始化信号量为0,B进程要进行P操作进行消费进行减一操作,信号量为-1了,B进程则阻塞。
- 此时A进程才开始执行,进行V操作加一,信号量-1->0,会将阻塞进程B进行唤醒,B进程进行数据的获取消费。
- 信号量初始化为0,可以保证V操作的进程会比P操作的进程先执行完毕。实现了进程的同步。
消息队列进行通信
消息队列是在消息的传输过程中保存消息的容器,可用于生产消费者模型,生产者将消息推送至消息队列,消费者可以在消息队列获取数据,也可以多生产者多消费者。这样的模型包含三个重要的元素:生产者,消息队列,作为进行数据的临时存储和发送的确认,消费者。
- 使用消息队列通信,生产者和消费者可以进行异步的处理,生产者推送的数据到消息队列,但消费者不需要立刻进行处理,可以过了一段时间进行处理。即生产者可以一直生产数据,消费者不消费。或者消息队列有数据,生产者不生产,消费者一直消费,直到消息队列没有消息。
网络socket通信
网络socket套接字进行通信,可以让两个不同主机的不同进程进行通信,如TCP/IP协议的支持下通过底层物理的链接进行进程的通信,比如说两个微信用户使用微信进行文件的传输。