在操作系统中进程具有独立性,那么进程之间进行通信必然成本不低。那么进程间通信方式有哪些呢?
数据传输:一个进程需要将自己的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某些事件(如子进程终止了需要通知父进程)
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变。
一个进程可以完成大多数事情,但有些事情是需要多进程进行协同去完成的,那么就衍生出了进程通信的概念。比如 cat file.cc |grep ‘123456’:cat指令是把文件里的内容打印到屏幕上,|是内存里的一块空间,cat |是把文件的内容打印到内存里的一块空间里,grep '123456’是将有123456内容的这行代码打印到屏幕上。cat和grep也都是进程,那么这行指令就是cat进程把内容通过内存里的一块空间,发送给grep进程,grep筛选出与123456有关的代码打印到屏幕上。 即是数据传输的行为
对于标准:行业上有有两套标准
- POSIX:可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945。
那么posix解决了什么问题呢?
不同操作系统内核为同一功能提供的系统调用(函数)是不同的,例如创建进程,linux下是fork函数,windows下是createprocess函数,如果在Linux下写了一个程序用到了fork函数,要往windows上移植就得把源代码里面的fork通通改成createprocess,然后重新编译。
解决方法: 定义POSIX标准, linux和windows实现基于POSIX标准,提供同样的接口,例如定义创建进程的接口为posix_fork(示例名/非真实名字), 且linux和windows都把各自创建进程的调用封装成posix_fork,都声明在unistd.h里。 这样程序员编写应用时,只需包含unistd.h, 调用这个POSIX标准中定义的API接口: posix_fork函数,即可实现源代码级别的可移植。
即实现了跨主机通信:为了运行在不同操作系统的应用程序提供统一的接口,实现者是不同的操作系统内核。
- System V
SystemV标准的进程间通信方式是在操作系统层面专门为进程间通信设计的一个方案。进程间通信的本质就是让不同的进程能够看到同一份资源。常见的system V结构的通信方式有如下几种:共享内存、消息队列、信号量。
进程间通信层面,对于文件系统有基于文件系统的管道,那么管道是什么呢?
我们回顾进程地址空间,父进程会配有一个文件描述符表,表中有内存中的文件的虚拟地址进而可以找到内存中的文件,内存中的文件有磁盘上的物理地址也进而能找到进行IO流。当父进程创建子进程时,父进程会拷贝一份文件描述符表给子进程,那么子进程也能通过该表找到相同的虚拟地址进而找到相同的内存中文件,也能同磁盘上的文件进行IO。
两个进程通信必须要看到同一块资源,这块资源在文件系统中叫管道文件
进程间通信不需要进行IO流
进程间通信具有不同的种类,种类类型由操作系统提供的模块决定。文件系统模块提供的资源,称为管道文件。内存中提供的资源成为共享内存等等。
根据上面的结论,不难知道,可以通过父进程创建子进程,让父进程和子进程通过相同的文件描述符表上的虚拟地址找到相同的管道文件进而完成通信。而该管道文件专门用来父子进程进行通信的,是没有名字的所以被称为匿名管道
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回指向管道读端和写端的两个文件描述符:
数组元素 | 含义 |
---|---|
pipefd[0] | 管道读端的文件描述符 |
pipefd[1] | 管道写端的文件描述符 |
pipe函数调用时,若成功返回0,失败则返回-1
文件描述符012分别被标准输入输出流stdin、stdout、stderr占用,那么管道读端和写端是匹配哪个文件描述符呢?
#include
#include
#include
using namespace std;
int main()
{
int fds[2];
int n=pipe(fds);
assert(n==0);
cout<<"pipe[0]: "<<fds[0]<<endl;
cout<<"pipe[1]: "<<fds[1]<<endl;
return 0;
}
若父进程负责读取数据,子进程负责写入数据,则创建管道通信过程如下:
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//第一步,父进程创建管道
int fds[2];
int n=pipe(fds);
assert(n==0);//返回0保证匿名管道创建成功
//第二步,父进程创建子进程
pid_t id=fork();
assert(id>=0);//若fork成功,那么子进程返回0, 返回给父进程 子进程的id
if(id==0)//这里对子进程进行操作
{
close(fds[0]);//子进程关闭读端
const char* s="i am child , i am sending message to father";
int cnt=0;
while(true)
{
cnt++;
char buffer[1024];//创建缓冲区
snprintf(buffer,sizeof buffer,"child say to parent:%s,childpid[%d],[%d]",s,getpid(),cnt);
write(fds[1],buffer,strlen(buffer));//子进程往写端进行写入
sleep(1);//子进程间断时间写
}
close(fds[1]);
exit(0);
}
//父进程操作
close(fds[1]);//父进程关闭写端
while(true)
{
char buffer[1024];//创建缓冲区
cout<<"正在读取:......"<<endl;
ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);//流一个位置给/0
cout<<"读取成功!"<<endl;
if(s>0)//写入成功
{
buffer[s]=0;//给字符串末尾添加上0
cout<<"father get message: "<<buffer<<"|fatherpid: "<<getpid()<<endl;
}else if(s==0)//没写入
{
cout<<"read: nothing "<<endl;
}
}
cout<<"父进程关闭读端"<<endl;
n=waitpid(id,nullptr,0);
assert(n==id);//父进程等待子进程(回收子进程)
return 0;
}
可以看到子进程写一段父进程读一段,明显感觉到父进程在等待子进程写入。对代码稍加修改,让子进程睡眠50秒
管道内没有了数据,读端会阻塞等待写端
而当父进程睡眠两秒时,即子进程一直往管道文件里写,父进程间隔性读取数据,间隔时间为2秒
这里我让写端一直写,读端读了一次然后直接break
在父进程休眠2秒期间,子进程往管道文件里写数据,然后读端读完管道文件里的数据后,退出循环
综上可以得出
- 管道的生命周期依托于进程。
当父进程创建好管道时,管道文件被操作系统提供,当父进程退出时,管道文件也就被操作系统释放。
- 管道可以用来提供给具有血缘关系的进程之间进行通信
通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道是面向字节流的
与字节流服务相对应的是数据报服务
- 管道是半双工通信
在这种工作方式下,发送端可以转变为接收端;相应地,接收端也可以转变为发送端。但是在同一个时刻,信息只能在一个方向上传输。因此,也可以将半双工通信理解为一种切换方向的单工通信。
通信的方式另外还有单工通信和全双工通信
单工通信 | 全双工通信 |
---|---|
单工通信(Simplex Communication)是指消息只能单方向传输的工作方式:在单工通信中,通信的信道是单向的,发送端与接收端也是固定的。基于以上,数据信号从一端传送到另外一端,信号流是单方向的。 | 全双工通信(Full duplex Communication)是指在通信的任意时刻,线路上存在A到B和B到A的双向信号传输。即允许数据同时在两个方向上传输,又称为双向同时通信,即通信的双方可以同时发送和接收数据。 |
- 同步与互斥机制
互斥:当一个进程正在临界区中访问临界资源时,其他进程不能进入临界区。
同步:合作的并发进程需要按先后次序执行,例如:一个进程的执行依赖于合作进程的消息或者信号,当一个进程没有得到来自于合作进程的消息或者信号时需要阻塞等待,直到消息或者信号到达后才被唤醒。
临界区:临界区则指的是一段代码,在这段代码中对临界资源的访问需要进行同步操作。进程访问临界资源的那段程序代码即一次仅允许一个进程在临界区中执行。
临界资源:临界资源指的是一些需要被多个进程或线程共享的资源。例如共享内存区、共享文件等。并且临界资源要通过互斥和同步的方式等来进行保护。
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。但想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它被成为命名管道。
可以通过命令行指令创建命名管道,且默认文件位于当前目录
mkfifo name_pipe
- 命名管道文件文件类型为p,即文件前面属性的第一个字符为p
- 命名管道文件大小为0
现在我在另一个端口将通过cat数据读出来,那么数据从一个端口写入,从另一个端口读出,命令行是一个进程,cat将数据读出也是一个进程,数据在一个进程流通到另一个进程,命名管道也完成了进程间通信!并且在通信的过程中,命名管道文件大小依旧为0。
mkfifo函数用于创建命名管道文件,原型如下:
int mkfifo(const char *filename,mode_t mode);
第一个参数filename 是指管道文件路径+文件名
第二个参数是管道文件的权限
一般权限设为0666(读写权限),但文件创建出来后权限会受umask影响
实际权限=(mode&~umask)
当然可以通过 umask=0把umask影响除掉
若文件创建成功返回0,创建失败返回-1,并且可以通过errno查到错误信息
pipehead.hpp
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NAME_PIPE "/home/ljp/name_pipe/mypipe"
bool CreateFifo( const string & path){
umask(0);
int n=mkfifo(path.c_str(),0600);
if(n==0) return true;
else{
cout<<"errno:"<<errno<<"err string: "<<strerror(errno)<<endl;
return false;
}
}
void removeFifo(const string & path){
int n=unlink(path.c_str());
assert(n==0);
(void)n;
}
server.cc
#include"pipehead.hpp"
using namespace std;
int main()
{
bool r= CreateFifo(NAME_PIPE);//创建管道文件
assert(r);
(void)r;
sleep(10);
removeFifo(NAME_PIPE);//删除管道文件
return 0;
}
现在能够通过server.cc创建管道文件,也能让server.cc往管道文件里写数据,然后让client.cc读取管道文件中的数据,完成进程间通信。
server.cc
#include"pipehead.hpp"
using namespace std;
int main()
{
bool r= CreateFifo(NAME_PIPE);//创建管道文件
assert(r);
(void)r;
cout<<"server begin"<<endl;
int rfd=open(NAME_PIPE,O_RDONLY);//当读端打开了文件,然而写端没有打开,那么读端就会阻塞在这里等待写端打开,写端打开了才往后走
cout<<"server end"<<endl;
if(rfd<0) exit(1);
//sleep(10);
//读
char buffer[1024];
while(true)
{
ssize_t n=read(rfd,buffer,sizeof(buffer)-1);//读
if(n>0)
{
buffer[n]=0;
cout<<"server read: "<<buffer<<endl;//打印读到的内容
}
else if(n==0)
{
cout<<"client quit!,i quit either"<<endl;//写端退出,读端也退出
break;
}else
{
cout<<"err string: "<<strerror(errno)<<endl;//打印错误信息
break;
}
(void)n;
}
removeFifo(NAME_PIPE);//删除管道文件
return 0;
}
client.cc
#include"pipehead.hpp"
using namespace std;
int main()
{
cout<<"client begin"<<endl;
int wfd=open(NAME_PIPE,O_WRONLY);
cout<<"client end"<<endl;
if(wfd<0) exit(1);
//写
char buffer[1024];
while(true)
{
cout<<"client says: ";
fgets(buffer,sizeof(buffer),stdin);//把输入流的内容写进buffer,fgets会把\n也输入,所以要把\n去掉
if(strlen(buffer)>0) buffer[strlen(buffer)-1]=0;
ssize_t n= write(wfd,buffer,strlen(buffer));//把buffer的内容写进文件描述符里
assert(n==strlen(buffer));
(void)n;
}
return 0;
}
再谈进程的独立性
- 共享内存就是使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。
- 进程与共享内存的映射叫做进程与共享内存进行挂接
- 进程与共享内存取消映射关系叫做进程与共享内存进行去关联
malloc不能完成进程间通信
malloc用于申请一块连续的指定大小的内存空间,实际上malloc是进程调用来向操作系统申请内存空间,这块空间也只能让该进程看到,因此malloc申请的内存空间也具有独立性,并不能用来进程间通信。
而共享内存是专门用来IPC(inter process communication)的方式,意味着会有许许多多的进程都用它来进行通信,操作系统中自然就会同时存在很多共享内存,那么共享内存也必须能有一定的条件加以区分彼此!即具有一定的标识性!让想通信的进程与特定的共享内存进行挂接!
用于创建共享内存
int shmget(key_t key, size_t size, int shmflg)
- size:共享内存的大小,一般是以4kb(4096字节)为单位。若申请的内存为4097字节,那么操作系统会分配2*4kb大小的内存,但是具有使用权限的只有4097字节。
- shmflg:共享内存的标志位
一般有以下两个选择:
- key:共享内存的关键码,由函数ftok提供
用于创建key值
函数原型
key_t ftok(const char *pathname, int proj_id);
ftok将pathname和proj_id两个参数用一定的算法整合成一个值,保证该值的唯一性。如果创建key成功,就将该值返回;创建失败返回-1,并用errno记录错误信息
shme.hpp
#ifndef _SHME_HPP_
#define _SHME_HPP_
#include
#include
#include
#include
#include
#include
#include
#define PATH_NAME "."//pathname
#define PROJ_ID 0x66//ID
#define MAX_SIZEE 4096//size
using namespace std;
int getshmhelper(key_t k,int flags)
{
int shmid=shmget(k,MAX_SIZEE,flags);
if(shmid<0)//创建失败
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
int getshm(key_t k)//获取共享内存
{
return getshmhelper(k,IPC_CREAT);
}
int creatshm(key_t k)//创建新的共享内存
{
return getshmhelper(k,IPC_CREAT|IPC_EXCL| 0600);
}
key_t getkey()//获取key
{
key_t k= ftok(PATH_NAME,PROJ_ID);
if(k<0)
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(-1);
}
return k;
}
#endif
shm_server.cc
#include"shme.hpp"
int main()
{
key_t k=getkey();
printf("0x%x\n",k);
int shmid=creatshm(k);
cout<<"shmid: "<<shmid<<endl;
//cout<<"hello im shm_server.cc"<
return 0;
}
shm_client.cc
#include"shme.hpp"
int main()
{
key_t k=getkey();
printf("0x%x\n",k);
int shmid=getshm(k);
cout<<"shmid: "<<shmid<<endl;
// cout<<"hello im shm_client.cc"<
return 0;
}
运行打印可以看到key值相同,shmid都是4
- 共享内存生命周期随内核
终止掉程序再次运行时,可以看到报错说明文件以存在。由于shmget函数标记位为IPC_CREAT|IPC_EXCL时,若共享内存已存在则报错返回,则不能再次创建新的共享内存了;说明共享内存的生命周期并不随进程,而是随内核。那么共享内存不使用时必须释放!
ipcs -m :查看共享内存属性
ipcrm -m shimd
ipcs -q :查看消息队列属性
ipcs s:查看system V中的其他通信方式的各种属性
用于操作共享内存
函数原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第一个参数shmid是共享内存标识符
第二个参数cmd是控制的动作
第三个参数buf是指向一个保存着共享内存的模式状态和访问权限的数据结构,通常设置成nullptr
返回值,函数调用成功返回0,调用失败返回-1
控制动作常用的有三个
IPC_STAT | 获取共享内存的当前关联值,此时参数buf作为输出型参数 |
---|---|
IPC_SET | 在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 删除共享内存段 |
使进程与共享内存挂接
函数原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
第一个参数是shmid共享内存标识符;
第二个参数shmaddr为指定连接的地址,通常设置为nullptr,让核心自动选择一个地址
第三个参数是shmflg,它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
使进程与共享内存去关联
函数原型
int shmdt(const void *shmaddr);
shme.hpp
#ifndef _SHME_HPP_
#define _SHME_HPP_
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH_NAME "."//pathname
#define PROJ_ID 0x66//ID
#define MAX_SIZEE 4096//size
using namespace std;
int getshmhelper(key_t k,int flags)
{
int shmid=shmget(k,MAX_SIZEE,flags);
if(shmid<0)//创建失败
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
int getshm(key_t k)//获取共享内存
{
return getshmhelper(k,IPC_CREAT);
}
int creatshm(key_t k)//创建新的共享内存
{
return getshmhelper(k,IPC_CREAT|IPC_EXCL| 0600);
}
key_t getkey()//获取key
{
key_t k= ftok(PATH_NAME,PROJ_ID);
if(k<0)
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(-1);
}
return k;
}
void* attachshm(int shmid)//进程与共享内存挂接
{
void*ret=shmat(shmid,nullptr,0);
if((long long)ret==-1L)
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(-1);
}
return ret;
}
void detech(void*start)//去关联
{
if(shmdt(start))
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(-1);
}
}
void detachShm(int shmid)//释放共享内存
{
int rm=shmctl(shmid,IPC_RMID,nullptr);
if(rm<0)
{
cerr<<errno<<"errno: "<<strerror(errno)<<endl;
exit(-1);
}
}
#endif
shm_server.cc
#include"shme.hpp"
int main()
{
key_t k=getkey();//获取key
printf("0x%x\n",k);
int shmid=creatshm(k);//创建共享内存
cout<<"shmid: "<<shmid<<endl;
sleep(2);
//挂接
char* atshm=(char*)attachshm(shmid);
printf("server attach address: %p\n",atshm);
sleep(1);
//进程与共享内存去关联
detech(atshm);
sleep(1);
//释放共享内存
detachShm(shmid);
return 0;
}
shm_client.cc
#include"shme.hpp"
int main()
{
key_t k=getkey();
printf("0x%x\n",k);
int shmid=getshm(k);//获取共享内存
cout<<"shmid: "<<shmid<<endl;
sleep(1);
//挂接
char* atshm=(char*)attachshm(shmid);
printf("client attach address: %p\n",atshm);
sleep(2);
//进程与共享内存去关联
detech(atshm);
return 0;
}
shm_client.cc
#include"shme.hpp"
int main()
{
key_t k=getkey();
printf("0x%x\n",k);
int shmid=getshm(k);//获取共享内存
cout<<"shmid: "<<shmid<<endl;
//挂接
char* atshm=(char*)attachshm(shmid);
printf("client attach address: %p\n",atshm);
//通信:client作为写端
int cnt=0;
char bufffer[1024];
const char* message="hello server:im client,im talking to you";
while(true)
{
snprintf(atshm,MAX_SIZEE,"%s[pid: %d][消息编号: %d]",message,getpid(),cnt++);
sleep(5);
}
//进程与共享内存去关联
detech(atshm);
return 0;
}
shm_server.cc
#include"shme.hpp"
int main()
{
key_t k=getkey();//获取key
printf("0x%x\n",k);
int shmid=creatshm(k);//创建共享内存
cout<<"shmid: "<<shmid<<endl;
//挂接
char* atshm=(char*)attachshm(shmid);
printf("server attach address: %p\n",atshm);
while(true)
{
printf("client says: %s\n",atshm);
sleep(1);
}
//进程与共享内存去关联
detech(atshm);
sleep(1);
//释放共享内存
detachShm(shmid);
return 0;
}
通过现象可以看出
- 共享内存没有同步与互斥机制,即没有对数据进行保护(共享内存的缺点)
另外还有另一条特性
- 基于相对其他进程间通信的方式,共享内存拷贝次数最少,因此是所有进程间通信方式中速度最快的
由于共享内存是两个进程所共有的,进程一只需把数据写进内存中,进程二就能看到
stdin写入数据到缓冲区,缓冲区拷贝一次数据到进程一,进程一再拷贝一次数据到共享内存中;进程二从共享内存中拷贝一次数据,然后stdout再从进程二中拷贝一次数据加以打印,一共四次拷贝数据
相对于管道通信,进程一需将数据额外拷贝一次给缓冲区,再让缓冲区拷贝一次数据到管道中;相应的,管道中的数据需要拷贝一次到缓冲区,进程二才能从缓冲区中拷贝一次数据拿到。在共享内存中通信一次比管道中通信少了2次拷贝。
实际上操作系统中存在许多共享内存,那么操作系统需要去维护共享内存的内核数据结构,可以通过shmctl接口查看,该内核数据结构体内含一些共享内存内核的信息供用户去调用查看
函数原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第二个参数cmd是控制的动作,当传参IPC_STAT时,可以获取共享内存内核结构里的信息
第三个参数buf是指向一个保存着共享内存的模式状态和访问权限的数据结构,可以设置为shmid_ds结构体。当第二个参数传参IPC_STAT时,操作系统会把共享内存内核信息设置进指向的结构体里。
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
实际上操作系统中会有存在很多的消息队列,系统也必须为消息队列维护内核数据结构
消息队列的数据结构如下:(注:结构体msqid_ds位于构 /usr/include/linux/msg.h中)
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
struct ipc_perm {
key_t __key; /* Key supplied to xxxget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
函数原型如下:
int msgget(key_t key, int msgflg);
函数原型如下:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
控制的动作通常有三个:(其作用和共享内存的一样)
IPC_STAT | 获取消息队列的当前关联值,此时参数buf作为输出型参数 |
---|---|
IPC_SET | 在进程有足够权限的前提下,将消息队列的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 删除消息队列 |
函数原型如下:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
导致msgsnd函数阻塞的原因:
sg_cbytes:消息队列中已使用字节数;
msg_qbytes:消息队列中可以容纳的最大字节数;
函数原型如下:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
第一个参数msqid是由msgget函数返回的消息队列标识符
第二个参数msgp是⼀个指针,该指针指向接收消息的内存缓冲区
第三个参数msgsz是msgp指向的消息长度,这个⻓度不含保存消息类型的那个long int⻓整型
第四个参数msgtyp是指定读取消息的类型
第五个参数msgflg是指定了消息的接收方式,一般有两种选项
IPC_NOWAIT:非阻塞方式读取信息
MSG_NOERROR:截断读取消息
msgrcv函数调用成功返回获取mtext数组的字节数,失败返回-1
接下来通过消息队列,完成server端先接收client发送过来的消息,然后再发消息给client端,这样的来回发送消息完成进程间通信
定义消息结构
struct msggbuf{
long mtype;
char mtext[1024];
};
msg.hpp
#ifndef _MSG_HPP_
#define _MSG_HPP_
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH_NAME "."//消息队列的路径
#define PROJ_ID 0x666//消息队列的自选id
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
using namespace std;
struct msggbuf{
long mtype;
char mtext[1024];
};
int sendmsg(int msgid,int sendtype,char* msg)
{
struct msggbuf buf;
buf.mtype=sendtype;//消息队列的类型
strcpy(buf.mtext,msg);
if(msgsnd(msgid,&buf,sizeof(buf.mtext),0)<0)
{
perror("msgsnd error");
exit(-1);
}
return 0;
}
int receivemsg(int msgid,int receivetype,char out[])
{
struct msggbuf buf;
if(msgrcv(msgid,&buf,sizeof(buf.mtext),receivetype,0)<0)
{
perror("msgrcv error");
exit(-1);
}
strcpy(out,buf.mtext);
return 0;
}
int getmsghelper(int key,int flags)//总的获取消息队列函数
{
int msgid=msgget(key,flags);
if(msgid<0)//创建失败
{
perror("getmsghelper error");
}
return msgid;
}
int creatmsg(int key)//创建消息队列--发送端调用
{
getmsghelper(key,IPC_CREAT|IPC_EXCL| 0600);
}
int Getmsg(int key)//获取消息队列--接收端调用
{
getmsghelper(key,IPC_CREAT);
}
key_t getkey()//获取key值
{
key_t keynum=ftok(PATH_NAME,PROJ_ID);
if(keynum<0)
{
perror("ftok error");
exit(-1);
}
return keynum;
}
void deletemsg(int msgid)//删除消息队列
{
if(msgctl(msgid,IPC_RMID,nullptr)<0)
{
perror("msgctl error");
exit(-1);
}
}
#endif
msg_server.cc
#include"msg.hpp"
int main()
{
cout<<"hello im msg_server.cc"<<endl;
int key=getkey();
int msgid= creatmsg(key);
sleep(1);
cout<<"server create msgqueue success\n"<<endl;
//发送消息
char buf[1024];
while(true)
{
buf[0]=0;
receivemsg(msgid,CLIENT_TYPE,buf);
printf("client# %s\n",buf);
printf("please enter# ");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf));
if(s>0)
{
buf[s-1]=0;
sendmsg(msgid,SERVER_TYPE,buf);
printf("send done,wait recieve:...\n");
}
}
sleep(1);
deletemsg(msgid);
cout<<"server delete msgqueue success\n"<<endl;
return 0;
}
msg_client.cc
#include"msg.hpp"
int main()
{
cout<<"hello im msg_client.cc"<<endl;
cout<<"hello im msg_server.cc"<<endl;
int key=getkey();
int msgid= Getmsg(key);
sleep(1);
cout<<"server create msgqueue success\n"<<endl;
//接收并打印消息
char buf[1024];
while(true)
{
buf[0]=0;
printf("please enter# ");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf));
if(s>0)
{
buf[s-1]=0;
sendmsg(msgid,CLIENT_TYPE,buf);
printf("send done,wait recieve ...\n");
}
receivemsg(msgid,SERVER_TYPE,buf);
printf("server says#: %s\n",buf);
}
sleep(1);
deletemsg(msgid);
cout<<"server delete msgqueue success\n"<<endl;
return 0;
}
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
进程具有独立性,那么在进程间通信时就需要一份共享资源,但如果没有对该共享资源做相应保护的话,会造成各个进程从该共享资源获取的数据不一致问题。
信号量结构体
struct semaphore
{
int value;
pointer_PCB queue;
}
计数器含义
P操作:申请资源
P(s)
{
s.value = s.value--;
if (s.value < 0)
{
// 该进程状态置为等待状状态
//将该进程的PCB插⼊相应的等待队列s.queue末尾
}
}
V操作:释放资源
V(s)
{
s.value = s.value++;
if (s.value < =0)
{
// 唤醒相应等待队列s.queue中等待的⼀个进程
// 改变其状态为就绪态
// 并将其插⼊就绪队列
}
}
实际上操作系统中有许多信号量,那么就需要操作系统去维护信号量的内核结构
结构如下:
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set */
};
函数原型如下:
int semget(key_t key, int nsems, int semflg);
函数原型如下:
int semctl(int semid, int semnum, int cmd, ...);
函数原型如下:
int semop(int semid, struct sembuf *sops, unsigned nsops);
对信号量的介绍就到这,后续我会分享更为深入的信号量知识
通过对system V系列进程间通信的学习,可以发现共享内存、消息队列以及信号量,虽然它们内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量。
这样设计的好处就是,在操作系统内可以定义一个struct ipc_perm*类型的数组,数组里的元素是指针,指向struct ipc_perm类型结构体。此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。