目录
匿名管道
创建管道---pipe()
命名管道
创建FIFO
FIFO 操作
用命名管道实现server&client通信
共享内存
1.创建共享内存函数shmget()
2.获得共享内存地址函数shmat()
3.删除共享内存函数shmdt()
4.共享内存控制函数shmctl()
在Linux下的多个进程间的通信机制叫做IPC,它是多个进程之间相互沟通的一种方法。在Linux下有多种进程间通信的方法:半双工管道、FIFO (命名管道)、消息队列、信号量、共享内存等。使用这些通信机制可以为Linux下的网络服务器开发提供灵活而又坚固的框架。
匿名管道
管道是种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的办法,自UNIX操作系统诞生,管道就存在了。
1.基本概念
由于管道仅仅是将某个进程的输出和另一个进程的输入相连接的单向通信的办法,因此称其为“半双工”。在shell中管道用“ | ”表示。
ls -l | grep *.c
把ls -l的输出当做“grep *.c”的输入,管道在前一个进程中建立输入通道,在后一个进程建立输出通道,将数据从管道的左边传输到管道的右边将ls -l的输出通过管道传给“grep *.c”。
进程创建管道,每次创建两个文件描述符来操作管道。其中一个对管道进行写操作,另一个描述符对管道进行读操作。管道将两个进程通过内核连接起来,两个文件描述符连接在一起。如果进程通过管道fda[0]发送数据,它可以从fdb[0]获得信息。
由于进程A和进程B都能够访问管道的两个描述符,因此管道创建完毕后要设置在各个进程中的方向,希望数据向那个方向传输。这需要做好规划,两个进程都要做统一的设置,在进程A中设置为读的管道描述符,在进程B中要设置为写;反之亦然,并且要把不关心的管道端关掉。对管道的读写与一般的I0系统函数一致, 使用write()函数写入数据,read()函数读出数据,某些特定的IO操作管道是不支持的,例如偏移函数lseek()。
创建管道---pipe()
#include <unistd.h>
int pipe(int fd[2]);
fd是一个文件描述符的数组,用于保存管道返回的两个文件描述符。数组中的第1个元素(下标为0)是为了读操作而打开的,而第2个元素(下标为1),是为了写操作而创建和打开的。(0看作一张嘴,用来读,1看作一支笔,用来写)。当函数执行成功时返回0,失败返回错误代码。
只建立管道看起来没有什么用处,要使管道有切实的用处,需要与进程的创建结合起来,利用两个管道在父进程和子进程之间进行通信。在父进程和子进程之间建立一个管道, 子进程向管道中写入数据,父进程从管道中读取数据。要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。
#include <iostream>
#include <unistd.h>
#include <string>
#include <string.h>
#include <cerrno>
#include <cassert>
#include <sys/types.h>
using namespace std;
int main()
{
// 任何一种任何一种进程间通信中,一定要先保证不同的进程之间看到同一份资源
int pipefd[2] = {0};
//1. 创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
return 1;
}
//2.创建子进程
pid_t id=fork();
assert(id != -1);
if(id==0)
{
//子进程
//3.关闭不需要的fd,让父进程进行读取,子进程进行写入
close(pipefd[0]);
//4.开始通信
string strmessage="hello,我是子进程";
char buffer[1024];
int count=1;
while(true)
{
snprintf(buffer,sizeof buffer,"%s,计数器:%d,我的PId:%d",strmessage.c_str(),count++,getpid());
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3.关闭不需要的fd,让父进程进行读取,子进程进行写入
close(pipefd[1]);
//4.开始通信
char buffer[1024];
while(true)
{
int n=read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
cout<<"我是父进程,child give me message:"<<buffer<<endl;
}
}
close(pipefd[0]);
return 0;
}
特点
- 单向通信
- 管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的。
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。 (pipe打开管道,并不清楚管道的名字---匿名管道)。
- 在管道通信中,写入的次数,和读取的次数,不是严格匹配的读写次数的多少没有强相关。
- 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信---自带同步机制。
#include <iostream>
#include <string>
#include <cerrno>
#include <cassert>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int pipefd[2] = {0};
//1. 创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 读端, 0->嘴巴->读书
std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 写端, 1->笔->写东西的
//2. 创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)// 子进程
{
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[0]);
//4. 开始通信 -- 结合某种场景
// const std::string namestr = "hello, 我是子进程";
// int cnt = 1;
// char buffer[1024];
int cnt = 0;
while(true)
{
char x = 'X';
write(pipefd[1], &x, 1);
std::cout << "Cnt: " << cnt++<<std::endl;
sleep(1);
// break;
// snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());
// write(pipefd[1], buffer, strlen(buffer));
}
close(pipefd[1]);
exit(0);
}
//父进程
//3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
//4. 开始通信 -- 结合某种场景
char buffer[1024];
int cnt = 0;
while(true)
{
// sleep(10);
// sleep(1);
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "我是父进程, 读到了文件结尾" << std::endl;
break;
}
else
{
std::cout << "我是父进程, 读异常了" << std::endl;
break;
}
sleep(1);
if(cnt++ > 5) break;
}
close(pipefd[0]);
int status = 0;
waitpid(id, &status, 0);
std::cout << "sig: " << (status & 0x7F) << std::endl;
sleep(100);
return 0;
}
- 如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待。read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- 如果我们writer端将管道写满了,我们就不能写了。write调用阻塞,直到有进程读走数据。
- 如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾。
- 写端一直写,读端关闭,这是没有意义的。OS不会维护无意义、低效率或者浪费资源的事情。OS会杀死一直在写入的进程。OS会通过信号来终止进程(SIGPIPE--信号13)。
命名管道
命名管道的工作方式与普通的管道非常相似,但也有一些明显的区别。
- 在文件系统中命名管道是以设备特殊文件的形式存在的。
- 不同的进程可以通过命名管道共享数据。
创建FIFO
有许多种方法可以创建命名管道。其中,可以直接用shell来完成。例如,在当前目录下建立一一个名字为namedfifo的命名管道:
mkfifo namedfifo
可以看出namedfifo的属性中有一个p,表示这是个管道。
命名管道也可以从程序里创建,相关函数有:
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *filename,mode_t mode);
创建命名管道:
int main(int argc, char *argv[]) {
mkfifo("p2", 0644);
return 0;
}
FIFO 操作
对命名管道FIFO来说,IO操作与普通的管道IO操作基本上是一样的,二者之间存在着一个主要的区别。在FIFO中,必须使用一个open()函数来显式地建立连接到管道的通道。一般来说FIFO总是处于阻塞状态。也就是说,如果命名管道FIFO打开时设置了读权限,则读进程将一直“阻塞”,一直到其他进程打开该FIFO并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开一个管道写入数据,当没有进程冲管道中读取数据的时候,写管道的操作也是阻塞的,直到已经写入的数据被读出后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在open()调用中使用O_NONBLOCK标志,以关闭默认的阻塞动作。
用命名管道实现server&client通信
comm.hpp(client.cc和server.cc的命名管道)
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname="./fifo";
uint32_t mode = 0666;
server.cc
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
// 1. 创建管道文件,只需要一次创建
umask(0); //这个设置并不影响系统的默认配置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 1;
}
std::cout << "create fifo file success" << std::endl;
// 2. 让服务端直接开启管道文件
int rfd = open(fifoname.c_str(), O_RDONLY);//只读方式打开
if(rfd < 0 )
{
std::cout << errno << " : " << strerror(errno) << std::endl;
return 2;
}
std::cout << "open fifo success, begin ipc" << std::endl;
// 3. 正常通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
std::cout << "client# " << buffer << std::endl;
//printf("%c", buffer[0]);
//fflush(stdout);
}
else if(n == 0)
{
std::cout << "client quit, me too" << std::endl;
break;
}
else
{
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(rfd);
unlink(fifoname.c_str());
return 0;
}
client.cc
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// #include <ncurses.h>
#include "comm.hpp"
int main()
{
//1. 不需创建管道文件,我只需要打开对应的文件即可!
int wfd = open(fifoname.c_str(), O_WRONLY);//只写方式打开
if(wfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
// 可以进行常规通信了
char buffer[NUM];
while(true)
{
std::cout<<"请输入你的消息#:";
char *msg = fgets(buffer,sizeof(buffer),stdin);
assert(msg);
(void)msg;
buffer[strlen(buffer)-1]=0;
if(strcasecmp(buffer,"quit") == 0) break;
ssize_t n = write(wfd,buffer,strlen(buffer));
assert(n > 0);
(void)n;
}
close(wfd);
return 0;
}
共享内存
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程之间对内存段进行映射的方式实现内存共享的。这是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换;与此相反,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅是地址不同而已,因此不需要进行复制,可以直接使用此段空间。
1.创建共享内存函数shmget()
函数shmget()用于创建一个新的共享内存段, 或者访问一个现有的共享内存段。函数shmget()的原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_ t size, int shmflg);key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmget()的第一个参数是关键字的值。然后,这个值将与内核中现有的其他共享内存段的关键字值相比较。在比较之后,打开和访问操作都将依赖于shmflg参数的内容。
- IPC_CREAT:如果在内核中不存在该内存段,则创建它。
- IPC_EXCL:当与IPC CREAT 一起使用时,如果该内存段早已存在,则此次调用将失败。
如果只使用IPC_CREAT, shmget()或者 将返回新创建的内存段的段标识符,或者返回早已存在于内核中的具有相同关键字值的内存段的标识符。如果同时使用IPC_CREAT和IPC_EXCL,则可能会有两种结果:如果该内存段不存在,则将创建一个新的内存段;如果内存段早已存在,则此次调用失败,并将返回-1。IPC_EXCL本身是没有什么用处的,但在与IPC_CREAT组合使用时,它可用于防止一个现有的内存段为了访问而打开着。一旦进程获得了给定内存段的合法IPC标识符,它的下一步操作就是连接该内存段,或者把该内存段映射到自己的寻址空间中。
单独使用 IPC_CREAT :创建一个共享内存,如果共享内存不存在 ,就创建它;如果存在,就获取已经存在的共享内存并且返回
IPC_EXCT不能单独使用,一般都要配合IPC_CREAT
IPC_CREAT | IPC_EXCT:创建一个共享内存,如果共享内存不存在,就创建它,如果存在,出错并返回(如果共享内存创建成功,则这个共享内存一定是最新的)
2.获得共享内存地址函数shmat()
函数shmat()用来获取共享内存的地址,获取共享内存成功后,可以像使用通用内存一样对其进行读写操作。函数的原型如下:
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1
如果shmaddr参数值等于0,则内核将试着查找一个未映射的区域。用户可以指定个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。SHM_RND标志可以与标志参数进行OR操作,结果再置为标志参数,这样可以让传送的地址页对齐。
此外,如果把SHM_RDONLY标志与标志参数进行OR操作,结果再置为标志参数,这样映射的共享内存段只能标记为只读方式。
当申请成功时,对共享内存的操作与一般内存一样,可以直接进行写入和读出,以及偏移的操作。
3.删除共享内存函数shmdt()
函数shmdt()用于删除一段共享内存。 函数的原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt (const void *shmaddr) ;shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
当某进程不再需要一个共享内存段时,它必须调用这个函数来断开与该内存段的连接。这与从内核删除内存段是两回事。在成功完成了断开连接操作以后,相关的shmid ds结构的shm nattch成员的值将减去1。如果这个值减到0,则内核将真正删除该内存段。
4.共享内存控制函数shmctl()
共享内存的控制函数shmctl的使用类似iocl()的方式对共享内存进行操作:向共享内存的句柄发送命令来完成某种功能。函数shmcl()的原型如下,其中shmid是其享内存的句柄,emd是向共享内存发送的命令,最后一个参数 buf则是向共享内存发送命令的参数。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf) ;shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
- IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值。
- IPC_SET:获取内存段的shmid_ds结构,并把它存储在buf参数所指定的地址中。IPC_ SET设置内存段shmid_ds 结构的ipc_perm 成员的值,此命令是从buf参数中获得该值的。
- IPC_ RMID:标记某内存段,以备删除。该命令并不真正地把内存段从内存中删除。相反,它只是标记上该内存段,以备将来删除。只有当前连接到该内存段的最后一个进程正确地断开了与它的连接,实际的删除操作才会发生。当然,如果当前没有进程与该内存段相连接,则删除将立刻发生。为了正确地断开与其共享内存段的连接,进程需要调用shmdt()函数。
小实验:
comm.hpp(对方法的封装)
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
// IPC_CREAT and IPC_EXCT
// 单独使用 IPC_CREAT :创建一个共享内存,如果共享内存不存在 ,就创建它;如果存在,就获取已经存在的共享内存并且返回
// IPC_EXCT不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT |IPC_EXCT:创建一个共享内存,如果共享内存不存在,就创建它,如果存在,出错并返回(如果共享内存创建成功,则这个共享内存一定是最新的)
#define PATHNAME "."
#define PROJID 0x6666
const int gsize = 4096;
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJID);
if (k == -1)
{
cerr << "error:" << errno << strerror(errno) << endl;
exit(1);
}
return k;
}
// 十六进制转换
string toHex(int x)
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
static int createShmHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, size, flag);
if (shmid == -1)
{
cerr << "error:" << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t k, int size)
{
umask(0);
return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(key_t k, int size)
{
umask(0);
return createShmHelper(k, size, IPC_CREAT);
}
char *attachShm(int shmid)
{
char *start = (char *)shmat(shmid, nullptr, 0);
return start;
}
void detachShm(char *start)
{
int n = shmdt(start);
assert(n != -1);
(void)n;
}
void delShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
}
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t)
:type(t)
{
key_t k = getKey();
if(type == SERVER)
shmid = createShm(k,gsize);
else
shmid = getShm(k,gsize);
start = attachShm(shmid);
}
char* getStart(){return start;}
~Init()
{
detachShm(start);
if(type == SERVER)
delShm(shmid);
}
private:
char*start;
int type;//server or client
int shmid;
};
#endif
server.cc
#include "comm.hpp"
#include <unistd.h>
int main()
{
Init init(SERVER);
char* start = init.getStart();
int n = 0;
while(n <= 26)
{
cout<<"client -> server#"<<start<<endl;
sleep(1);
n++;
}
// //1.创建key
// key_t k = getKey();
// cout<<"server key:"<<toHex(k)<<endl;
// //2.创建共享内存
// int shmid = createShm(k,gsize);
// cout<<"server shmid:"<<shmid<<endl;
// //3.将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(5);
// //4.将自己和共享内存去关联
// detachShm(start);
// //删除共享内存
// delShm(shmid);
return 0;
}
client.cc
#include "comm.hpp"
#include <unistd.h>
#include <string>
#include <vector>
int main()
{
Init init(CLIENT);
char* start = init.getStart();
char c = 'A';
while(c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = '\0';
sleep(1);
}
// //1.创建key
// key_t k = getKey();
// cout<<"client key:"<<toHex(k)<<endl;
// //2.创建共享内存
// int shmid = createShm(k,gsize);
// cout<<"client shmid:"<<shmid<<endl;
// //3.将自己和共享内存关联起来
// char* start = attachShm(shmid);
// sleep(10);
// //4.将自己和共享内存去关联
// detachShm(start);
return 0;
}