目录
信号量:
进程间通信介绍
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。主要匿名管道、命名管道、共享内存等。
目的:
1.数据传输:一个进程需要将它的数据发送给另一个进程。
2.资源共享:多个进程之间共享同样的资源。
3.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
4.进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
上述功能往往需要多个进程协同工作,而进程之间又存在独立性,不同进程之间不能直接进行数据资源的传递,这时就需要借助进程间通信来完成不同进程间的数据资源的传递。
本质:
由于进程之间存在独立性,因此要让两个进程之间实现通信,首先要让不同进程看到同一份资源。
原则:
1.需要有交换数据的空间(内存)。
2.交换数据的空间不能由通信双方中的任何一方提供,需要由操作系统(OS)提供。
进程A、B之间要进行通信,无论是在进程A中开辟一块空间供A、B之间通信使用,还是在进程B中开辟一块空间供A、B之间通信使用,都会与进程之间的独立性相矛盾,因此只能由OS提供一块空间,供A、B通信使用。
IPC分类;
管道:
匿名管道(pipe)、命名管道。
System V IPC:
System V消息队列、System V共享内存、System V信号量。
POSIX IPC:
消息队列、共享内存、信号量、互斥量、条件变量、读写锁。
本文主要讲解管道和System V共享内存。
管道
什么是管道:
管道是Unix中最古老的进程间通信的形式,它是一个把两个进程连接起来的数据流。
管道通信是基于文件实现的,它的生命周期是随进程的,进程退出,则对应的管道销毁。
管道是单向的,只能一个进程写数据,另一个进程读数据,要想实现双向通信,需要创建两个管道。
匿名管道:
顾名思义就是没有名字的管道。
创建匿名管道需要使用pipe函数:
#include <unistd.h>
int pipe(int fd[2]);
pipe函数的功能就是创建一个匿名管道,创建成功返回 0 ,创建失败返回 错误代码 。
其中fd是一个存储文件描述符的数组,是一个输出型参数,若匿名管道创建成功,fd[0]存储匿名管道的读端文件描述符,fd[1]存储匿名管道的写端文件描述符。
由于pipe函数不会返回匿名管道的标识符之类的信息,因此通过pipe创建的匿名管道,只能实现父子进程间的IPC。因为子进程会继承父进程的相关属性和数据。
实例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void writer(pid_t wfd)
{
char buffer[128]={0};
const char*str="hello father,I am child";
int count=0;
pid_t id=getpid();
while(1)
{
snprintf(buffer,sizeof(buffer),"message:%s,pid:%d,count:%d\n",str,id,count);
write(wfd,buffer,sizeof(buffer));
count++;
sleep(1);
}
}
void reader(pid_t rfd)
{
char buffer[1024]={0};
while(1)
{
ssize_t n=read(rfd,buffer,sizeof(buffer));
printf("father get a message:%s",buffer);
}
}
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);//创建一个管道,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
if(n!=0)
{
printf("管道创建失败\n");
return 0;
}
printf("pipefd[0],read:%d pipefd[1],writ:%d\n",pipefd[0],pipefd[1]);
pid_t id=fork();
if(id==0)//子进程,写
{
close(pipefd[0]);//关闭读端
writer(pipefd[1]);
exit(-1);
}
//父进程,读
close(pipefd[1]);//关闭写端
reader(pipefd[0]);
wait(NULL);
return 0;
}
上述代码主要功能是,创建一个匿名管道,然后通过fork创建子进程,子进程保留管道的写端,关闭读端,父进程保留读端,关闭写端,子进程每隔一秒向管道中写入一条信息,并且有一个计数器记录子进程写了多少次,父进程读取信息。
管道的四种情况:
管道内无数据 且 写端不写入也不关闭,读端会一直阻塞等待。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void writer(pid_t wfd)
{
char buffer[128]={0};
const char*str="hello father,I am child";
int count=0;
pid_t id=getpid();
while(1)
{
printf("child pid:%d\n",getpid());
count++;
sleep(1);
}
}
void reader(pid_t rfd)
{
char buffer[1024]={0};
while(1)
{
printf("begin read....\n");
ssize_t n=read(rfd,buffer,sizeof(buffer));
printf("read end\n");
printf("father get a message:%s",buffer);
}
}
int main()
{
printf("father pid:%d\n",getpid());
int pipefd[2]={0};
int n=pipe(pipefd);//创建一个管道,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
if(n!=0)
{
printf("管道创建失败\n");
return 0;
}
pid_t id=fork();
if(id==0)//子进程,写
{
close(pipefd[0]);//关闭读端
writer(pipefd[1]);
exit(0);
}
//父进程,读
close(pipefd[1]);//关闭写端
reader(pipefd[0]);
wait(NULL);
return 0;
}
上述代码中,子进程虽然一直在运行,但是并没有往管道中写数据。
运行起来后可以发现,父进程阻塞在了read函数位置,等待子进程写入数据。
- 管道内被写满 且 读端不读取也不关闭,则写端在写满管道后,阻塞等待数据被读取。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void writer(pid_t wfd)
{
char buffer[1]={1};
int count=0;
pid_t id=getpid();
while(1)
{
printf("begin write....\n");
write(wfd,buffer,sizeof(buffer));
printf("write end\n");
count++;
printf("child pid:%d count:%d\n",getpid(),count);
}
}
void reader(pid_t rfd)
{
char buffer[1024]={0};
while(1)
{
printf("father pid:%d\n",getpid());
sleep(1);
}
}
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);//创建一个管道,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
if(n!=0)
{
printf("管道创建失败\n");
return 0;
}
pid_t id=fork();
if(id==0)//子进程,写
{
close(pipefd[0]);//关闭读端
writer(pipefd[1]);
exit(0);
}
//父进程,读
close(pipefd[1]);//关闭写端
reader(pipefd[0]);
wait(NULL);
return 0;
}
上述代码中,父进程一直在运行,但是一直不从管道中读取数据,而子进程一直在向管道中写入数据,每次写入 1 字节数据,并计数。
可以发现,子进程在写入65536字节(64KB)数据后就不在写入了,一直阻塞在write函数,等待父进程读取数据。
- 写端关闭后,因为read函数会返回独到的数据数量,所以读端会把管道中的数据读完,最后read函数会返回 0 ,标志读取结束。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void writer(pid_t wfd)
{
char buffer[128]={0};
const char*str="hello father,I am child";
int count=0;
pid_t id=getpid();
while(1)
{
snprintf(buffer,sizeof(buffer),"message:%s,pid:%d,count:%d\n",str,id,count);
write(wfd,buffer,sizeof(buffer));
if(count==5)
{
printf("write end...\n");
break;
}
count++;
sleep(1);
}
close(wfd);
}
void reader(pid_t rfd)
{
char buffer[1024]={0};
while(1)
{
ssize_t n=read(rfd,buffer,sizeof(buffer));
if(n==0)
{
printf("father get a %s n:%ld\n",buffer,n);
printf("read end....\n");
break;
}
else if(n>0)
{
printf("father get a %s n:%ld\n",buffer,n);
}
else
{
break;
}
sleep(1);
}
}
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);//创建一个管道,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
if(n!=0)
{
printf("管道创建失败\n");
return 0;
}
pid_t id=fork();
if(id==0)//子进程,写
{
close(pipefd[0]);//关闭读端
writer(pipefd[1]);
exit(0);
}
//父进程,读
close(pipefd[1]);//关闭写端
reader(pipefd[0]);
wait(NULL);
return 0;
}
上述代码中,子进程写入五次数据后不再写入数据,并且把管道写端关闭,父进程读取到 0 后结束循环。
可以发现,最后read函数最后的返回值为0。
- 读端关闭后,写端再继续写入已没有意义,OS会直接终止写端的写入操作(通过13号信号SIGPIPE,杀掉进程)。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void writer(pid_t wfd)
{
char buffer[128]={0};
const char*str="hello father,I am child";
int count=0;
pid_t id=getpid();
while(1)
{
snprintf(buffer,sizeof(buffer),"message:%s,pid:%d,count:%d\n",str,id,count);
write(wfd,buffer,sizeof(buffer));
count++;
sleep(1);
}
}
void reader(pid_t rfd)
{
char buffer[1024]={0};
int cnt=0;
while(1)
{
ssize_t n=read(rfd,buffer,sizeof(buffer));
if(n==0)
{
printf("father get a %s n:%ld\n",buffer,n);
printf("read end....\n");
break;
}
else if(n>0)
{
printf("father get a %s n:%ld\n",buffer,n);
}
else
{
break;
}
cnt++;
if(cnt==2)
{
printf("read end....\n");
break;
}
sleep(1);
}
close (rfd);
}
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);//创建一个管道,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
if(n!=0)
{
printf("管道创建失败\n");
return 0;
}
//printf("pipefd[0],read:%d pipefd[1],writ:%d\n",pipefd[0],pipefd[1]);
pid_t id=fork();
if(id==0)//子进程,写
{
close(pipefd[0]);//关闭读端
writer(pipefd[1]);
exit(0);
}
//父进程,读
close(pipefd[1]);//关闭写端
reader(pipefd[0]);
int status=0;
int rid=waitpid(id,&status,0);
if(rid==id)
{
printf("exit code:%d ,exit signal :%d\n",WEXITSTATUS(status),status&0x7f);
}
return 0;
}
上述代码中,读端读入两次数据后直接关闭,写端一直循环。 最后通过等待子进程获得子进程的退出信息。
可以发现,当读端关闭后,写端也立即终止了写入操作,OS发送13号信号杀掉子进程。
管道的特点:
1.只能用于具有亲缘关系的进程之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
2.管道是面向字节流的,读取和写入的次数没有必然关系。
3.一般而言,进程退出,管道释放,所以管道的生命周期随进程。
4.一般而言,内核会对管道操作进行同步与互斥。
5.管道是半双工的,数据只能向一个方向流动。需要双方通信时,需要建立起两个管道。
命名管道:
匿名管的一大限制就是只能用于父子进程间通信。
想要实现任意进程间的通信,可以使用FIFO文件来实现,它被称为命名管道。
命名管道是一种特殊类型的文件。
注意:
命名管道是一种特殊类型的文件,可以被打开,但是不会将内存数据刷新到磁盘中,相对于匿名管道来说,命名管道有文件名,并且存在于系统路径中。
- 命名管道可以从命令行上创建:
使用命令:mkfifo + 命名管道的名字(不带路径,默认在当前目录下创建)
mkfifo file
执行上述命令后,会在当前目录下创建一个管道文件file。
p 就表示该文件是一个管道文件,并且管道文件的后边有一个 | 标志。
- 使用程序代码创建命名管道:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname是一个字符串指针,指向的是命名管道的名字(可加路径,也可不加路径),mode是所创建出来的命名管道文件的权限。
既然有创建命名管道的函数,就有删除命名管道的函数:
#include <unistd.h>
int unlink(const char *pathname);
两个函数都是执行成功返回 0 ,失败返回非 0 ,并设定错误码。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
int main()
{
umask(0);
int n=mkfifo("file",0666);
if(n==0)
{
cout<<"makfifo success"<<endl;
}
else
{
cerr<<"mkfifo failed,errno:"<<errno<<endl;
exit(-1);
}
sleep(5);
n=unlink("file");
if(n==0)
{
cout<<"unlink success"<<endl;
}
else
{
cerr<<"unlink failed,errno:"<<errno<<endl;
exit(-1);
}
return 0;
}
上述代码,先在当前目录下创建一个file管道文件,5s后再把file管道文件删除。
通过命名管道实现任意进程间通信:
- FIFO.hpp---创建管道
通过FIFO类封装mkfifo函数,创建管道。
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
#define MODE 0666 //创建管道的默认权限
#define PATH "./fifo"//创建管道的默认路径和名字
//创建一个命名管道
class FIFO
{
public:
FIFO(const string& path=PATH)
:_path(path)
{
umask(0);//设置系统默认权限掩码为0
int n=mkfifo(_path.c_str(),MODE);
if(n!=0)
{
cerr<<"mkfifo failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
}
else
{
cout<<"mkfifo success!"<<endl;
}
}
~FIFO()
{
int n=unlink(_path.c_str());
if(n==0)
{
cout<<"unlink success!"<<endl;
}
else
{
cerr<<"unlink failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
}
}
private:
string _path;//路径+管道名字
};
- PipeClient.cpp---客户端,写数据
以只写方式打开管道文件,并不断从终端读取数据,写入到管道文件中。
#include"FIFO.hpp"
//客户端--写
int main()
{
int wfd=open(PATH,O_WRONLY);
if(wfd<0)
{
cerr<<"open failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
return 1;
}
string buffer;
while(1)
{
getline(cin,buffer);
if(buffer=="quit")
{
break;
}
ssize_t n=write(wfd,buffer.c_str(),buffer.size());//向管道中写数据
if(n<0)
{
cerr<<"write failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
}
sleep(1);
}
close(wfd);
return 0;
}
- PipeServer.cpp----服务端,读数据
以只读方式打开管道文件,并不断从管道文件中读取数据。
#include"FIFO.hpp"
//服务端--读
int main()
{
FIFO fifo(PATH);
int rfd=open(PATH,O_RDONLY);
if(rfd<0)
{
cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
return 1;
}
cout<<"Server success!!"<<endl;
char buffer[1024];
while(1)
{
int n=read(rfd,buffer,sizeof(buffer)-1);
if(n==0)
{
cout<<"Client quit!!!"<<endl;
break;
}
else if(n<0)
{
cerr << "read failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
break;
}
else
{
buffer[n]=0;//结束标志
cout<<"Client say:"<<buffer<<endl;
}
}
close(rfd);
return 0;
}
运行结果:
命名管道与匿名管道:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,由open函数打开。
- 命名管道有文件名,且会存在于系统路径中。
- 匿名管道没有文件名,也不会存在于系统路径中。
- 匿名管道与命名管道的最大的区别在于它们创建与打开的方式不同,一旦创建并打开后,它们具有相同的语义。
System V 共享内存
共享内存 是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,即进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存的生命周期是随内核的,需要手动释放,或者系统退出,内存空间才会释放。
使用共享内存进行通信一定是一个进程创建共享内存,另一个进程直接获取这个共享内存。
共享内存的基本构成:
共享内存是由操作系统维护的,由于在操作系统中可能存在大量使用共享内存进行通信的进程,为了管理这些共享内存,操作系统除了要在物理内存中开辟共享内存之外,还必须对共享内存进行管理---先描述,再组织。所以,共享内存 = 共享内存块 + 描述共享内存的数据结构。
共享内存函数:
-
创建共享内存:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
- key:这个共享内存段在内核中的唯一性标识,在内核角度区分共享内存的唯一性,必须使用户手动传入,否则无法让任意两个进程看到同一块共享内存。
- size:共享内存的大小,一字节为单位。系统实际分配时是以4KB为单位分配的。
- shmflg:权限标志位,用法与创建文件时使用的mode标志类似。
shmflg常用的标记有三个:
- IPC_CREAT:如果key标识的共享内存不存在,就创建;存在,就获取。
- IPC_EXCL:不能单独使用,没有意义。
- IPC_CREAT | IPC_EXCL:若key标识的共享内存不存在,就创建;存在,就出错返回。
可以在shmflg后边追加创建的共享内存的权限。
如:IPC_CREAT | 0666:表示不存在就创建,存在就获取,并且设定共享内存权限为666。
返回值:
- 成功返回一个非负数,即创建的共享内存的标识码;失败返回 -1;
- 该返回值不同于key,key仅创建共享内存时使用,在需要使用该共享内存时,都需要使用该返回值。
创建更新内存时需要手动传入一个key,这个key通常借助 ftok 函数获得:
-
获取key:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:
- pathname:给定一个字符串。
- proj_id:给定一个常数。
返回值:
- 返回一个具有唯一性的数值key。
-
将共享内存链接到进程地址空间:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:共享内存标识码,即shmget函数的返回值。
- shmaddr:指定链接地址,通常设置为NULL,让OS自己选择一个合适的位置。
- shmflg:关联共享内存时设置的某些属性,它的两个可能取值是SHM_RND和SHM_RDONLY,一般设置为 0 ,默认设置为读写权限。
返回值:
- 成功,返回指向共享内存第一个字节的指针;失败,返回-1。
-
将共享内存与进程地址空间接触链接
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
- shmaddr:由shmat所返回的指针。
返回值:
- 成功,返回0;失败返回 -1。
需要注意,shmdt函数只是把共享内存与进程的链接断开,并不是删除共享内存。
-
控制共享内存:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:共享内存标识码,即shmget函数的返回值。
- cmd:标记要对共享内存采取的操作。
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。
cmd有三个:
- IPC_STAT:把当前共享内存中的属性拷贝到buf指向的结构体中。
- IPC_SET:在权限足够的前提下,把共享内存的属性设置为buf指向的结构体中的数据。
- IPC_RMID:删除共享内存。
共享内存的优点:
- 共享内存是最快的IPC形式。
共享内存的缺点:
- 共享内存不提供进程间通信的协同机制。可以通过一个管道实现进程间的协同机制。
- 默认情况下,共享内存的读取方不会管写入方是否写入数据,即使为空也会读取。
共享内存实现进程间通信:
- Com.hpp:
主要是有关共享内存操作的一些函数和创建共享内存时需要用到的数据:
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include "FIFO.hpp"
using namespace std;
const char* pathname="/root/PFG";
const int Proj_id=0x66;
const int Default_Size=4096;//单位字节,共享内存以4KB为单位创建
//将key转成16进制
string ToHex(key_t key)
{
char buffer[1024]={0};
snprintf(buffer,sizeof(buffer),"0x%x",key);
return buffer;
}
//获取开辟共享内存的标识符key
key_t GetShmKey()
{
key_t key=ftok(pathname,Proj_id);
if(key<0)//获取key失败,打印错误信息并退出
{
cerr<<"GetShmKey failed,errno:"<<errno<<",error message:"<<strerror(errno)<<endl;
exit(1);
}
return key;
}
//使用shmget函数创建共享内存
int _CreateShm(key_t key,size_t size,int shmflag)
{
int shmid=shmget(key,size,shmflag);
if(shmid<0)//创建共享内存失败
{
cerr<<"shmget failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
//创建新的共享内存
int CreateShm(key_t key,size_t size)
{
//IPC_CREAT:key标识的共享内存,不存在就创建,存在就获取它的shmid
//IPC_EXCL:单独使用没有意义
//IPC_CERAT|IPC_EXCL:key标识的共享内存,不存在就创建,存在就出错返回
return _CreateShm(key,size,IPC_CREAT|IPC_EXCL|0666);//0666--给共享内存设置权限
}
//获取共享内存
int GetShm(key_t key,size_t size)
{
return _CreateShm(key,size,IPC_CREAT|0666);
}
//删除共享内存
void DelShm(int shmid)
{
int n=shmctl(shmid,IPC_RMID,nullptr);
if(n<0)
{
cerr<<"DelShm failed,errno:"<<errno<<"errno message:"<<strerror(errno)<<endl;
}
else
{
cout<<"DelShm success,shmid:"<<shmid<<endl;
}
}
//输出共享内存的属性
void DebugShm(int shmid)
{
struct shmid_ds buf;
int n=shmctl(shmid,IPC_STAT,&buf);//把共享内存的属性信息拷贝到buf中
if(n<0)
{
cerr<<"DebugShm failed,errno:"<<errno<<"errno message:"<<strerror(errno)<<endl;
}
cout<<"shmid_ds.shm_segsz:"<<buf.shm_segsz<<endl;//共享内存大小
cout<<"shmid_ds.shm_atime:"<<buf.shm_atime<<endl;//共享内存最后一次挂接的时间
cout<<"shmid_ds.shm_dtime:"<<buf.shm_dtime<<endl;//共享内存最后一次脱离的时间
cout<<"shmid_ds.shm_ctime:"<<buf.shm_ctime<<endl;//共享内存最后一次修改的时间
cout<<"shmid_ds.shm_perm.__key:"<<ToHex(buf.shm_perm.__key)<<endl;//共享内存的key值
}
//挂载共享内存
void* AttachShm(int shmid)
{
void* addr=shmat(shmid,nullptr,0);//系统自动选择一个地址,与shmid标识的共享内存进行连接
if((long long)addr==-1)
{
cerr<<"AttachShm failed,errno:"<<errno<<"errno message:"<<strerror(errno)<<endl;
return nullptr;
}
cout<<"AttachShm success"<<endl;
return addr;
}
//解挂共享内存
void DetachShm(void* addr)
{
int n=shmdt(addr);
if(n<0)
{
cerr<<"DetachShm failed,errno:"<<errno<<"errno message:"<<strerror(errno)<<endl;
return;
}
cout<<"DetachShm success"<<endl;
}
- FIFO.hpp:
共享内存不提供进程间的协同机制,需要人为控制。
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
using namespace std;
#define MODE 0666 //创建管道的默认权限
#define PATH "./fifo"//创建管道的默认路径和名字
//创建一个命名管道
class FIFO
{
public:
FIFO(const string& path=PATH)
:_path(path)
{
umask(0);//设置系统默认权限掩码为0
int n=mkfifo(_path.c_str(),MODE);
if(n!=0)
{
cerr<<"mkfifo failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
}
else
{
cout<<"mkfifo success!"<<endl;
}
}
~FIFO()
{
int n=unlink(_path.c_str());
if(n==0)
{
cout<<"unlink success!"<<endl;
}
else
{
cerr<<"unlink failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
}
}
private:
string _path;//路径+管道名字
};
class Sync//通过命名管道控制共享内存读写协同
{
public:
Sync()
:rfd(-1),wfd(-1)
{}
void OpenRead()
{
rfd=open(PATH,O_RDONLY);
if(rfd<0)
{
cerr<<"OpenRead failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
exit(1);
}
}
void OpenWrite()
{
wfd=open(PATH,O_WRONLY);
if(wfd<0)
{
cerr<<"OpenWrite failed,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
exit(1);
}
}
bool Wait()//读端等待写端写入
{
bool ret=true;
int temp=0;
int n=read(rfd,&temp,sizeof(temp));
if(n==sizeof(temp))
{
cout<<"Sever begin read shm......"<<endl;
}
else
{
ret=false;
}
return ret;
}
void Wakeup()//写端唤醒读端读取
{
int temp=0;
int n=write(wfd,&temp,sizeof(temp));
assert(n==sizeof(temp));
cout<<"Server Wake..."<<endl;
}
private:
int rfd;
int wfd;
};
- ShmClient.cpp:
用户端----写入数据。
#include"Com.hpp"
//用户端--写
int main()
{
key_t key = GetShmKey();
cout << "key: " << ToHex(key) << endl;
int shmid = GetShm(key, Default_Size);
cout << "shmid: " << shmid << endl;
DebugShm(shmid);
char* addr=(char*)AttachShm(shmid);
memset(addr,0,Default_Size);
Sync sy;
sy.OpenWrite();
sleep(10);
for(int i=0;i<26;i++)//写入数据
{
addr[i]=i+'a';
sleep(1);
sy.Wakeup();//唤醒服务端读取
}
cout<<"Client end"<<endl;
DetachShm(addr);
sleep(5);
//DelShm(shmid);
return 0;
}
- ShmServer.cpp:
服务端----读取数据。
#include"Com.hpp"
//服务端--读
int main()
{
// 1. 获取key
key_t key = GetShmKey();
cout << "key: " << ToHex(key) << endl;
// sleep(2);
// 2. 创建共享内存
int shmid = CreateShm(key, Default_Size);
cout << "shmid: " << shmid << endl;
DebugShm(shmid);
sleep(2);
//挂载
char* addr=(char*)AttachShm(shmid);
FIFO fi;
Sync sy;
sy.OpenRead();
//int count=40;
while(1)
{
if(!sy.Wait())//等待写端写入
{
break;
}
cout<<"read success:"<<addr<<endl;
sleep(1);
}
cout<<"Server end"<<endl;
DetachShm(addr);
sleep(5);
// 3. 删除
DelShm(shmid);
return 0;
}
运行结果:
查看共享内存:
- ipcs + -m :可以查看已创建的共享内存。
- ipcrm + -m + shmid : 删除指定的共享内存块。(shmid是共享内存的标识符,即shmget函数的返回值)
System V 消息队列 与共享内存有许多相似之处,感兴趣的可以自己摸索研究一下,这里就不在赘述了。
浅谈System V 信号量
信号量主要用于同步和互斥。
System V IPC资源的生命周期随内核的,IPC资源必须手动删除,否则不会自动清除,除非重启系统。
进程互斥:
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
- 在进程中涉及到互斥资源的程序段叫临界区。
注:
- 临界资源需要被保护,保护临界资源的实质就是保护临界区。
原子性:
在操作对象时只有两种状态,要么没开始、要么已经结束。
信号量:
信号量又称信号灯,本质是一个描述临界资源的计数器(类似于一个int count,当有进程申请信号量成功就count--,有进程释放信号量就count++),是对资源的预定机制。被预定的资源不一定被预定者持有,才是预定者的,只要预定者预定了,那么在未来的某一个时间,这份资源就是预定者的。
任何进程要访问临界资源,都需要申请信号量,只有申请成功才能访问资源,申请失败就继续等待,申请信号量又被称为P操作。信号量申请成功后,就可以访问资源。最后释放掉信号量,便于其他进程申请使用,释放信号量又被称为V操作。
注意:
- 任何进程要访问临界资源,都需要申请信号量,则意味着所有进程都能看到同一个信号量,说明信号量本身就是共享资源,那么信号量的申请和释放都必须是原子性的。
- 由于信号量是共享资源,因此单纯的int型数据,并不能充当信号量。 原因有两点:
1.单纯的 int 型数据无法在进程间共享,无法让所有进程都看到同一份资源。
2.int类型的++ 、- 操作不是原子性的。
- 通过申请信号量和释放信号量来保护临界资源,是所有进程都要遵守的规则。