目录
命名管道
管道测试小实验
模拟命名管道通信
server.cxx
client.cxx
comm.hpp
Log.hpp
makefile
创建子进程
system V共享内存
模拟实现共享内存
删除共享内存
将共享内存挂接到自己的地址空间
去除挂接
通过共享内存进行通信
comm.hpp
Log.hpp
shmClient.cc(发送端)
shmServer.cc (读取端)
习题
A进程打开fifo.ipc文件后,若B进程也想打开,此时只需要把该文件的地址填到B进程的文件描述符表里面 。此时A,B俩进程可以看到同一份文件。
这里我们探究的目的不是A进程写数据后,把数据刷新到磁盘上,我们是为了A和B俩个进程进行互相通信。但是,我们打开的普通文件只要是在磁盘上,如果对文件进行写操作,必定要把文件刷新到磁盘上,但是刷盘的效率很低,因此操作系统设计了一种新的设计方案:在磁盘上创建一种文件,这种文件叫管道文件,该文件可以被打开,但是不会将内存数据刷新到磁盘,既然是在磁盘上的文件,就一定有所在路径和自己的名字,即该文件一定在系统路径中(路径具有唯一性)。
由于路径具有唯一性,双方进程就可以通过管道文件的路径,看到同一份资源。
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道用于毫不相关的俩个进程。
命名管道是一种特殊类型的文件
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);mkfifo在指定路径下创建一个named pipes(命名的管道)
输入man 3 fifo,我们发现mkfifo还可以添加权限,创建成功返回0,失败了返回错误
由于俩个任意的指令一旦运行起来,就是俩个进程。
先创建一个管道文件,管道名为name_pipe,权限里的第一个字母为p代表管道文件
我们给管道里写入hello world,由于我们写了,但对方没有打开,此时处于阻塞状态
此时对方从管道文件中读数据,状态不再阻塞
我们输入下面这条命令,循环式的往管道里写
while :; do echo "hello world"; sleep 1; done > name_pipe
此时右边进行读取,输入cat > name_pipe右边会一直读取
删除管道文件
unlink name_pipe
这里让client.cxx和server.cxx访问同一个文件,让server.cxx创建管道文件,让client发送内容,server读取内容
运行server之后,默认会出现一个管道文件
#include"comm.hpp"
int main()
{
//1.创建管道文件
if(mkfifo(ipcPath.c_str(),MODE)<0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << " step 1" << endl;
//2.正常进行文件操作
int fd=open(ipcPath.c_str(),O_RDONLY);//以只读的方式打开文件
if(fd<0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功",Debug)<<"step 2"<0)//读取成功
{
std::cout<<"client say> "<
#include"comm.hpp"
int main()
{
//1.获取管道文件
int fd=open(ipcPath.c_str(),O_WRONLY);
if(fd<0)
{
perror("open");
exit(1);
}
//2.进行通信
string buffer;//将数据读取到这里
while(true)
{
cout<<"please Ente Meassage Line:>";
std::getline(cin,buffer);//将用户输入数据保存到BUFFER中
write(fd,buffer.c_str(),buffer.size());//把buffer的内容转为C语言字符串格式,然后写到fd中,大小为buffer
}
//3.关闭
close(fd);
//4.通信完毕,删除通信文件
unlink(ipcPath.c_str());
return 0;
}
#ifndef _COMM_H_
#define _COMM_H_
#include"Log.hpp"
#include
#include//perror头文件
#include
#include
#include
#include//mkfifo的头文件 open的头文件
#include//mkfifo头文件 open的头 文件
#include//open的头文件
using namespace std;
#define SIZE 128
#define MODE 0666//管道文件的权限
string ipcPath="./fifo.ipc";//管道文件路径
#endif
#ifndef _LOG_H_
#define _LOG_H_
#include
#include
//日志等级
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
.PHONY:all
all:client server
client:client.cxx
g++ -o $@ $^ -std=c++11
server:server.cxx
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf client server
运行程序
当左边输入./client时 ,右边才会显示打开文件成功
此时在左边输入消息,右边就能收到
当左边退出时,右边就会读到0,右边就会退出
server.cxx,我们将生成的文件命名为mutiServer
#include"comm.hpp"
#include
static void getMessage(int fd)
{
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
cout <<"[" << getpid() << "] "<< "client say> " << buffer << endl;
}
else if (s == 0)
{
// end of file
cerr <<"[" << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
break;
}
else
{
// read error
perror("read");
break;
}
}
}
int main()
{
//1.创建管道文件
if(mkfifo(ipcPath.c_str(),MODE)<0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", Debug) << " step 1" << endl;
//2.正常进行文件操作
int fd=open(ipcPath.c_str(),O_RDONLY);//以只读的方式打开文件
if(fd<0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功",Debug)<<"step 2"<
只进行./mutiServer操作,当前进程也只有一个
执行./client
此时有3个进程
我们发送多组数据,发现这里的pid在变化,这是因为我们派发了任务之后,这些任务由这几个进程去抢,谁抢到了就是谁的
原理:俩个不同的进程申请同一块空间,之后在各自的页表建立映射关系。
有图可知,共享内存会被加载到堆栈之间的共享区域 ,堆栈之间的共享区属于在用户空间(这个空间可以不经过系统调用用户便可直接访问)双方进程如果要通信,直接进行内存级别的读和写即可。
我们之前的pipe,fifo都要通过read,write进行通信,这是为什么?
因为之前命名管道是文件,文件是内核中的特定数据结构,需要操作系统自己去维护即在3-4G范围内,而共享内存不需要调用这些接口,用户可直接进行调用。
每一个进程0-3G属于用户,3-4G属于内核,页表分为用户级和内核级页表,操作系统在进行调度或使用系统调用接口或库函数,都要将在自己的代码映射到自己的地址空间当中,因为动态库会加载到共享区,无论我们是执行库中的代码或操作系统中的代码,都是在自己的地址空间中完成,无论进程如何切换,每个进程3-4G的空间全部映射的是操作系统的代码和数据
如何建立共享内存?
共享内存的提供者是操作系统。操作系统负责管理共享内存->先描述再组织->重新理解共享内存:共享内存=共享内存快(申请的空间)+对应的共享内存的内核数据结构。
shmget申请一个system V的共享内存段,表示创建并获取共享内存,size表示创建共享内存的大小
第三个参数有这俩种状态:IPC_CREAT, IPC_EXCL
IPC_CREAT:当单独使用时,如果要创建的共享内存已经存在,直接获取并返回,如果不存在就创建并返回。
IPC_EXCL:单独使用时,是没有意义的
IPC_CREAT, IPC_EXCL:合起来使用时,如果底层不存在我们要创建的共享内存,使用这俩个接口会创建共享内存并返回,如果底层存在,则出错返回。
如果成功返回合法标识符,失败返回-1,这个返回值时共享内存的用户层标识符,类似于曾经的fd,但俩者仍有差别。
通过key可以让对方看到共享内存,key的数据数据是几不重要,只要能够再系统唯一即可。
例如上面的server和client,只要保证使用的是同一个key即可,如server使用key去创建共享内存,client通过key获得共享内存并且和自己关联,这俩个进程就能看到同一个共享内存了。共享内存有自己的key(这个key在共享内存的数据结构当中)
只要key值相同,就是看到了同一个共享内存
如何产生key值?
ftok需要传一个路径和项目ID,第一个参数是路径,跟文件路径不一样 ,ID由我们自己指定,ftok不进行任何的系统调用,它内部是一套算法,就是把这俩个参数合起来形成一个唯一值。第一个参数我们一般指明一个文件之后,ftok会读取该文件的inode编号,利用inode值和id值进行数学运算,得到一个随机值。ftok形成key之后,这个key可能会和操作系统里面有的key重复,因此ftok不一定会成功。对于俩个进程使用同样的pathname和id,使用同样的ftok,server和client就能形成唯一的key,server再根据shmget用这个key创建共享内存,并把这个key写入到操作系统的共享内存里面,client将来在获取的时候,只要找到这个key,就能获得这块共享内存。
如果key值创建成功,则返回key值,反之返回-1
让shmServer.cc和shmClient.cc采用相同的算法和数据源,产生相同的key
获取共享内存
此时获取共享内存失败,退出码为1,报错信息为没有这个文件或目录,这是因为我把shmid第三个参数穿错了,这里只能传IPC_CREAT, IPC_EXCL
我们更换参数
此时无报错
再次运行,报错该文件已存在,虽然第一次执行完以后共享内存已经跑完了,但是共享内存还存在
ipcs -m//查看共享内存
ipcs查看ipc资源,-m查看共享内存
查看共享内存之后,我们发现共享内存此时存在,我们需删掉该共享内存
删除的时候输入ipcrm -m shmid值
当进程推出后共享内存还存在:这是因为system V IPC资源,生命周期随内核
我们对共享内存进行手动删除或代码删除,上面的删除方式为手动删除。
shmctl
第二个参数cmd
第三个参数是 共享内存的属性和数据,可设为空
如果删除失败就返回-1,成功返回0
我们在最后一步删除之后,就不会出现上面的问题
while :; do ipcs -m; sleep 1; done
我们写一个让key值变为16进制的函数,之后运行程序
我们可以看到key是0x66011952,shimd是6,当前拥有者是LWX,所占字节大小是4096,这里共享内存的大小我们可以随意修改,但共享内存的大小,最好是页的整数倍,Linux下页(PAGE)大小最小是4096
这里我们看到的perm是权限,0代表无权力读写共享内存
我们可在创建共享内存的时候加一个权限选项,这里是0666,之后我们可以看到perms是0666
attach是把共享内存映射到进程对应的页表当中前面的n代表关联的个数,去掉这种映射detach
shmat,第一个参数是shmid,第二个参数是我们指定的虚拟地址,一般情况下我们对虚拟地址的使用情况不太清楚,这个参数一般设为nullptr,即让共享内存自由去挂接,第三个参数是挂接方式如只读或其它方式,也可设置为0(默认以读写方式挂接)。
返回值:挂接成功的时候返回共享内存的地址(虚拟地址),失败返回-1,并且设置错误码
这个接口很像malloc申请成功返回地址空间,失败返回null
当返回地址后,我们设置了共享内存的大小,这个大小也可看做是偏移量,用返回的地址+这个偏移量就是共享内存所在的整个地址。
我们可以看到关联数量由0变为1
shmdt,只需传入shmat的返回值即可,去关联成功返回0,失败返回-1.
取消挂接之后,关联数量由1变为0
当shmServer和shmClient同时跑起来时,挂接数变为2
上面的所有工作是为了让不同的进程看到同一份资源(内存)
我们这里将共享内存当成一个大字符串。
shm.Server.cc负责读取内容
shmClient.cc每一次for循环都向shmaddr(共享内存的起始地址)写入
只运行shmServer,我们发现在打东西但什么都没打出来,这是因为共享内存在被创建后默认是全0,所以这里打出来的字符串是空串
我们可以看出写端没有启动,但读端可以一直读
我们此时让写端运行起来,当读端读到z的时候不会退出,会一直读,一直打印z,也就是无法执行后面的删除程序,我们需用ipcrm -m shimd手动去删除
若不手动删除,我们加一个标识符,如读到quit就退出读取
client.cc
我们让Client每五秒写一次
此时识别到quit,直接退出读过程,执行后面的代码
我们也可以像数组一样在使用共享内存
结论:
1.只要是通信双方使用共享内存,一方向共享内存中写入数据,另一方就可以立刻看到该数据,共享内存是所有进程间(IPC)通信速度最快的,不需要过多的拷贝(不需要将数据给操作系统)。
共享内存,共享内存可由键盘直接输入给内存,不需要缓冲区,减少了拷贝次数,只在键盘输入和往显示器上打印的时候进行了拷贝
此时我们输入什么,它就读取什么
稍作修改,输入quit 即可退出
2. 共享内存没有访问控制。共享内存无论有无数据,会一直读,读取端不会受写入端的影响。而管道当读取完毕后,若写端未退出会阻塞,若写端写满了,写端也会阻塞。共享内存由于缺乏访问控制,会引来并发问题。
若想进行访问控制,我们可以用管道实现共享内存进行同步。
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
#include
#include
using namespace std; //不推荐
#define PATH_NAME "/home/LWX"//要保证pathname的路径有访问权限
#define PROJ_ID 0x66
#define SHM_SIZE 4096//shmget的第二个参数,共享内存的大小
#define FIFO_NAME "./fifo"//管道文件
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
(void)n;
Log("create fifo success",Notice) << "\n";
}
~Init()
{
unlink(FIFO_NAME);//删除管道
Log("remove fifo success",Notice)<<"\n";
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname,int flags)//打开文件
{
int fd=open(pathname.c_str(),flags);
assert(fd>0);
return fd;
}
void Wait(int fd)//让一个进程等待
{
Log("等待中...",Notice)<<"\n";
uint32_t temp=0;
ssize_t s=read(fd,&temp,sizeof(uint32_t));//把fd内容读取到temp中,大小为4字节
assert(s==sizeof(uint32_t));
}
void Signal(int fd)//唤醒另一个进程,让另一个进程进行读或数据操作
{
uint32_t temp=1;
ssize_t s=write(fd,&temp,sizeof(uint32_t));//向fd中写temp值,大小为4字节
assert(s==sizeof(uint32_t));
(void)s;
Log("唤醒中...",Notice)<<"\n";
}
void CloseFifo(int fd)//关闭管道文件
{
close(fd);
}
#ifndef _LOG_H_
#define _LOG_H_
#include
#include
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
#include "comm.hpp"
int main()
{
key_t k = ftok(PATH_NAME, PROJ_ID);//产生key值
if(k<0)
{
Log("create key failed", Error) << " client key : " << k << endl;
exit(1);
}
Log("create key done", Debug) << " client key : " << k << endl;
int shmid=shmget(k,SHM_SIZE,IPC_CREAT);//获取共享内存
if(shmid<0)
{
Log("create key done", Error) << " client key : " << k << endl;
exit(2);
}
Log("create shm success", Error) << " client key : " << k << endl;
// sleep(10);
char *shmaddr=(char*)shmat(shmid,nullptr,0);//把自己当前进程和共享内存挂接
if(shmaddr==nullptr)
{
Log("attach shm failed", Debug) << " client key : " << k << endl;
exit(3);
}
Log("attach shm sucess", Debug) << " client key : " << k << endl;
//sleep(10);
//使用,shmClient.cc往共享内存写东西
//client将共享内存看作一个char类型的buffer
//每次都向shmaddr(共享内存的起始地址)写入
int fd=OpenFIFO(FIFO_NAME,WRITE);
while(true)
{
ssize_t s=read(0,shmaddr,SHM_SIZE-1);
if(s>0)//读取成功
{
shmaddr[s-1]=0;//我们在输入完数据后会按回车,此时给总大小-1就不接受回车了
Signal(fd);//发送端写入成功,此时唤醒另一个进程
if(strcmp(shmaddr,"quit")==0) break;
}
}
CloseFifo(fd);
//char a='a';
//for(;a<='c';a++)
/*{
shmaddr[a-'a']=a;
//snprintf(shmaddr,SHM_SIZE,"hello serve,我是其它进程,我的pid:%d,inc:%c\n",getpid(),a);//写到共享内存里
sleep(5);
}*/
strcpy(shmaddr,"quit");//我们给写入quit
//去关联
int n=shmdt(shmaddr);
assert(n!=-1);
Log("detach shm sucess", Debug) << " client key : " << k << endl;
// sleep(10);
//cilent不需要删除共享内存,client只管使用
return 0;
}
#include "comm.hpp"
Init init;
string TransToHex(key_t k)//转为16进制
{
char buffer[32];
snprintf(buffer,sizeof(buffer),"0x%x",k);//把key转为16进制写到buffer里
return buffer;
}
int main()
{
Log("child pid is:",Debug)<
以下描述正确的有 C
A.使用ipcrm -m命令删除指定共享内存后,则会直接释放共享内存
B.使用ipcs -m命令删除指定共享内存后,则会直接释放共享内存
C.使用ipcrm -a选项可以删除所有进程间通信资源
D.使用ipcrm命令不指定选项可以删除所有进程间通信资源
A/B 共享内存只有在当前映射连接数为0时才会被删除释放
以下关于ipcrm命令描述正确的有 B
A.ipcrm命令不指定选项可以删除所有进程间通信
B.ipcrm -m命令可以删除共享内存
C.ipcrm -s命令可以删除共享内存
D.ipcrm -q命令可以删除管道
以下关于ipc命令描述正确的有: B
A.ipcs -m用于查看消息队列的信息
B.ipcs -q可以查看消息队列的信息
C.ipcrm -s可以查看共享内存的信息
D.ipcrm -q可以查看共享内存的信息
ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源
-m 针对共享内存的操作
-q 针对消息队列的操作
-s 针对信号量的操作
-a 针对所有资源的操作