进程间通信介绍
进程间通信的目的
- 数据传输:一个进程需要将它的数据传输到另一个进程
- 资源共享:多个进程之间需要共享同样的资源
- 事件通知:一个进程需要向另一个进程或者一组进程发送消息,通知它们发生了某种时间
- 进程控制:一个进程要完全控制另一个进程的进行(Debug进程),控制进程希望完全控制另一个进程的陷入或者异常
进程间通信的发展
-
管道
-
System V进程间通信
-
POSIX 进程间通信
进程间通信的分类
- 管道
- 匿名管道
- 命名管道
- System V进程间通信
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥锁
- 条件变量
- 读写锁
管道
概念:
- UNIX古早的一种进程间通信方式
- 是一个进程连接到另一个进程的数据流
管道在Linux中各种地方都使用过,比如我们使用ps命令查找一些东西时可能就会用到管道。
比如这条命令 "ps axj | grep -v grep",这条命令中的 " | "这个细线就表示管道,它同时链接了两个进程 "ps" 和 "grep",并打印在屏幕上。
匿名管道
匿名管道只可用于本地进程通信,一般用于父子进程之间通信
进程间通信就是让两个进程看到同一份资源,而匿名管道则是开辟一份资源,然后父进程fork出子进程,子进程继承了父进程fork之前的所有资源,因而父子进程看到了同一份资源,从而可以进行通信。
- 虽然管道用的是文件的方式,但是管道文件并没有在磁盘中存在,父子进程对管道文件进行读写操作也不会有IO参与,这样就提高了效率
pipe函数
pipe函数就是操作系统提供给用户用来操作匿名管道的函数。
它需要一个长度为2的int类型的数组作为参数,通过该函数可以打开一个管道文件。
而其数组的两个成员分别储存了不同的文件描述符,0下标是以读方式打开的文件描述符,1下标是以写方式打开的文件描述符。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string>
#include<sys/types.h>
using namespace std;
int main()
{
int fds[2];
int ret = pipe(fds);
signal(SIGCHLD,SIG_IGN);
if(ret == -1)
{
cerr<<"pipe failed!"<<endl;
exit(-1);
}
pid_t id = fork();
if(id == 0)
{
close(fds[0]);
string tmp = "I am child process!";
ret = write(fds[1],tmp.c_str(),tmp.size());
if(ret == -1)
{
cerr<<"write failed!"<<endl;
exit(-1);
}
while(1) sleep(1);
}
close(fds[1]);
char buffer[1024] = {0};
ret = read(fds[0],buffer,sizeof(buffer) - 1);
cout<<buffer<<endl;
return 0;
}
我们能够看到子进程确实成功向父进程发送了消息。
在文件描述符角度描述管道
首先,父进程通过 pipe 函数创建了一个管道文件,并且让 fd[0] 和 fd[1] 分别保存对管道文件的读文件描述符和写文件描述符。
然后父进程 fork 出子进程,子进程获取到了同一份管道文件,也有相同的描述符。
然后让父进程关闭写端,子进程关闭读端。
- 需要注意的是,管道一般只能单向通信,因此需要我们选择一方读和一方写
- 向管道文件写的数据会保存在管道文件的文件缓冲区中,而不会写进磁盘,直到被读端读取。
管道读写规则
- 管道没有数据可读
- O_NONBLOCK disable :read阻塞,直到有数据到来
- O_NONBLOCK enable :read返回-1,errno值为 EAGAIN
- 管道数据已满
- O_NONBLOCK disable : write阻塞,直到有数据被读走
- O_NONBLOCK enable : write返回-1,errno 值为 EAGAIN
- 若所有管道对应写端的文件描述符关闭,read返回0
- 若所有管道对于读端的文件描述符关闭,write操作会产生信号SIGPIPE,进而导致write进程退出。
- 当写入数据量不大于PIPE_BUF时,linux将保证写入的原子性
- 当写入数据量大于PIPE_BUF时,linux不保证写入的原子性
管道的特点
1:管道自带互斥和同步机制
一般情况下,管道只允许一个进程对其进行读取或写入操作,像这种只允许一个进程访问的资源就是临界资源,而这种临界资源需要保护,否则可能导致同时读写,交叉读写等错误。
为了避免这种问题,内核会对管道操作进行同步和互斥。
- 同步:多个进程同时访问临界资源的时候,会按顺序运行
- 互斥:一个临界资源同时只允许一个进程访问或使用。
而管道在同一时间只运行一个进程来读或写,并且需要先写再读,这就是一种互斥与同步。
2:管道的生命周期跟随进程退出而退出
管道实际上是一种文件,打开该文件的进程退出后,管道也会被销毁。
3:管道提供的是流式服务
当一个进程读取管道中的数据时,它读取的数据多少是任意的,相对的还有数据包服务。
- 流式服务:数据没有明显的分割,没有明显的边界
- 数据包服务: 数据分段拿取,按数据报文端读取数据
4:管道是半双工通信的
- 单工通信:数据的传输是单向的,不可改变方向。
- 半双工通信:允许数据双向流通,但是同一时刻只能一个方向传输。
- 全双工通信:允许数据的双向同时流通,可以进行同时进行数据的双向传输。
管道就是典型的半双工通信,而想实现全双工通信就需要打开两个管道。
管道的四种特殊情况
- 读进程一直读,写进程不写:读端会被挂起,直到管道中有数据才会被唤醒。
- 写进程一直写,读进程不读:管道写满后,写端挂起,直到读端读取数据后被唤醒。
- 写进程写完关闭写段,那么读端读完后,会执行后续代码,而不会挂起。
- 读进程将读端关闭,而写进程还在向写段写入数据,写进程会被杀死。
比如我们这里让子进程发送数据,但是父进程关闭了读端,会受到13号信号。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int ret = pipe(fds);
if(ret == -1)
{
cerr<<"pipe failed!"<<endl;
exit(-1);
}
pid_t id = fork();
if(id == 0)
{
close(fds[0]);
string tmp = "I am child process!";
int cnt = 0;
while(1)
{
ret = write(fds[1],tmp.c_str(),tmp.size());
}
if(ret == -1)
{
cerr<<"write failed!"<<endl;
exit(-1);
}
}
close(fds[1]);
int n = 0;
while(n != 5)
{
n++;
cout<<n<<"秒过去了!"<<endl;
sleep(1);
}
close(fds[0]);
cout<<"关闭读端了!"<<endl;
int statu = 0;
waitpid(id,&statu,0);
cout<<"收到信号: "<<(statu & 0x7f)<<endl;
return 0;
}
我们发现5秒后关闭读端,子进程就被关闭了。
管道的大小
管道的大小也是固定的,当写进程写满管道后,就会被挂起,我们也能自行查看管道的大小。
1:Linux 2.6.11后的版本最大容量是65536字节
2:使用ulimit命令,可以看到pipe size
3:自己通过代码来测试大小
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int ret = pipe(fds);
if(ret == -1)
{
cerr<<"pipe failed!"<<endl;
exit(-1);
}
pid_t id = fork();
if(id == 0)
{
close(fds[0]);
string tmp = "";
while(1)
{
ret = write(fds[1],tmp.c_str(),tmp.size());
tmp += 'a';
cout<<tmp.size()<<endl;
}
if(ret == -1)
{
cerr<<"write failed!"<<endl;
exit(-1);
}
}
close(fds[1]);
while(1)
sleep(1);
return 0;
}
能够看到我这里是357字节大小。
命名管道
匿名管道只能在具有亲缘关系的进程之间进行通信,但我们也有需求在非亲缘关系的进程之间通信,这就用到了命名管道。
命名管道也是一种特殊的文件,它虽然能在磁盘上看到,但是它的大小永远为0,因为它不会将通信数据刷新在磁盘上。
创建命名管道
- 在命令行上创建
- 输入 mkfifo filename
- 在程序中创建
- int mkfifo (const char* filename, mode_t mode);
这样我们就创建了一个命名管道,可以在程序内通过open打开它。
也可以在命令行中直接向命名管道发送消息,让另一个终端获取消息。
命名管道和匿名管道的区别
- 命名管道需要通过mkfifo来创建,并用open函数打开,匿名管道通过pipe函数创建并打开。
- 之外并没有什么区别,二者在打开之后具有相同的语义。
命名管道的读写规则
-
以读方式打开
-
O_NONBLOCK disable :当有进程以写方式打开该管道才会继续运行,否则就会阻塞
-
O_NONBLOCK enable : 立即返回成功
-
-
以写方式打开
-
O_NONBLOCK disable : 当有进程以读方式打开该管道才会继续运行,否则就会阻塞
-
O_NONBLOCK enable : 立即返回失败,错误码为 ENXIO
-
通过命名管道使sever&client通信
comm.hpp中有两个函数。
bool Create_Named_Pipe(mode_t mode);
通过mode值来设置管道的权限。
不过需要注意的是,文件权限在设置时会和权限掩码进行 mode = mode & ~umask 的操作,因此这里先把umask值设为0,直接设置文件权限。
int Get_Pipe_Fd(int flags);
通过flags值来设置是以什么方式打开文件。
//comm.hpp
#pragma once
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<iostream>
using namespace std;
#define NAMED_PIPE "./name_pipe"
bool Create_Named_Pipe(mode_t mode)
{
umask(0);
int ret = mkfifo(NAMED_PIPE,mode);
if(ret == 0)
{
return true;
}
else
{
return false;
}
}
int Get_Pipe_Fd(int flags)
{
int ret = open(NAMED_PIPE,flags);
if(ret == -1)
{
cout<<"open failed!"<<endl;
exit(-1);
}
return ret;
}
//sever.cc
#include"comm.hpp"
#include<string>
#include<iostream>
using namespace std;
int main()
{
//先设置umask为0
bool ret = Create_Named_Pipe(0666);
int fd = Get_Pipe_Fd(O_WRONLY);
string send_str;
while(1)
{
cin>>send_str;
write(fd,send_str.c_str(),send_str.size());
cout<<"Sever Send!"<<endl;
}
return 0;
}
//sever.cc
#include"comm.hpp"
#include<iostream>
using namespace std;
int main()
{
int fd = Get_Pipe_Fd(O_RDONLY);
char buffer[1024];
while(1)
{
ssize_t s = read(fd,buffer,sizeof(buffer));
if(s == -1)
{
cout <<" read failed "<<endl;
return -1;
}
else if(s == 0)
{
cout<<"sever quit!"<<endl;
break;
}
buffer[s] = 0;
cout<<"Sever Say # "<<buffer<<endl;
}
return 0;
}
可以看到确实成功通信了。
共享内存
共享内存的原理
共享内存其实也是通过让两个进程看到同一份资源从而使得两个进程能够通信。
它是在物理内存开辟一块空间,然后让在虚拟地址开辟一块空间后,再填充到页表中,让物理内存和虚拟地址建立映射关系,这样进程就看到了共享内存。
共享内存数据结构
共享内存也是一种资源,那么操作系统当然也需要管理它,于是操作系统为共享内存定义了属于它的数据结构。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
两个进程需要看到同一份共享内存才能通信,为了保证看到的共享内存是同一份资源,操作系统为共享内存设定了key值,这个key值具有唯一性,因此能够保证是同一份资源。
而shmid_ds的第一个成员 ipc_perm 内部就保存了这样一个key值。
struct ipc_perm
{
__key_t __key; /* Key. */
__uid_t uid; /* Owner's user ID. */
__gid_t gid; /* Owner's group ID. */
__uid_t cuid; /* Creator's user ID. */
__gid_t cgid; /* Creator's group ID. */
__mode_t mode; /* Read/write permission. */
unsigned short int __seq; /* Sequence number. */
unsigned short int __pad2;
__syscall_ulong_t __glibc_reserved1;
__syscall_ulong_t __glibc_reserved2;
};
共享内存函数
shmget函数
该函数用来创建共享内存。
key:用来标识共享内存的唯一标识
size:创建的共享内存的大小
shmflg:权限标志,标识创建共享内存的方式
返回值:
- 成功返回一个共享内存的标识码。
- 失败返回-1.
注意:用来标识某个资源的字段称为句柄,而这个函数的返回值就是共享内存的句柄,之后都是通过这个句柄来访问共享内存。
key_t 是系统用来标识共享内存的类型,该类型的值需要通过 ftok 函数生成。
参数
- pathname:一个文件名,这个文件必须是存在并且允许访问的文件。
- proj_id:一个整型的参数,必须不为0.
返回值
返回一个 key_t 类型的key值,可以用来创建共享内存。
shmflg表示创建共享内存的方式,其中最常用的是 IPC_CREAT 和 IPC_EXCL。
IPC_CREAT | 若对应key值的共享内存不存在,就创建,否则就获取 |
IPC_EXCL | 单独使用没有效果,需要配合 IPC_CREAT 使用 |
IPC_CREAT | IPC_EXCL | 若对应key值共享内存不存在,就创建,存在则出错 |
一般 IPC_CREAT | IPC_EXCL 是用来保证创建的 shm 一定是新的。
通过这个文件我们就能够创建一个共享内存。
#include<sys/ipc.h>
#include<sys/shm.h>
#include<iostream>
#include<unistd.h>
using namespace std;
#define FILENAME "."
#define PROJ_ID 0x6666
int main()
{
key_t key = ftok(FILENAME,PROJ_ID);
int shmid = shmget(key,1024,IPC_CREAT);
while(1) sleep(1);
return 0;
}
并且能够通过 ipcs 命令 查看创建的共享内存。
ipcs命令会查看 消息队列,共享内存和信号量的所有信息。
但我们能够携带选项来选择查看某一选项。
- -q :查看消息队列的信息
- -m: 查看共享内存的信息
- -s : 查看信号量的信息
标题 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id(句柄) |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 关联共享内存的进程数 |
status | 共享内存的状态 |
shmctl函数
创建的共享内存有两种方式释放。
- 通过 ipcrm -m shmid 命令释放
- 通过shmctl 函数释放
首先是通过 ipcrm -m shmid 释放,可以看到确实释放了。
- shmid: 对应共享内存的id
- cmd : 对共享内存进行的操作
- buf* : 指向一个保存着共享内存的模式状态和访问权限的数据结构
其中 cmd 共有三种操作方式。
命令 | 说明 |
IPC_STAT | 将对应共享内存的数据读取到 buf 中 |
IPC_SET | 在进程有足够权限的情况下,把共享内存的当前关联值设置为 shmid_ds 数据结构中给出的值 |
IPC_RMID | 删除对应共享内存 |
我们可以试验一下。
#include<sys/ipc.h>
#include<sys/shm.h>
#include<iostream>
#include<unistd.h>
using namespace std;
#define FILENAME "."
#define PROJ_ID 0x6666
int main()
{
key_t key = ftok(FILENAME,PROJ_ID);
int shmid = shmget(key,1024,IPC_CREAT);
int n = 5;
while(n--) sleep(1);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
进程会在5秒后删除共享内存,然后我们通过使用命令行来循环查看ipcs。
while :; do echo "#####################" ; ipcs -m ; sleep 1 ; done
可以发现在5秒后共享内存确实被释放了。
shmat函数
在使用 shmget 函数创建共享内存后,依旧无法通信,因为这只是在物理内存中创建了共享内存,但是当前进程并不能通过页表来访问该共享内存。
因此我们需要通过关联函数来把共享内存和进程之间建立关联。
- shmid : 对应共享内存的 id
- shmaddr : 指定连接的地址,一般设置为NULL,表示自动设置地址
- shmflg : 表示关联的共享内存设置的属性
shmflg 一般有两种设置方式。
设置 | 说明 |
SHM_RDONLY | 表示该进程只能读取共享内存中的数据 |
0 | 表示该进程可读可写 |
- 成功则返回共享内存在进程的起始地址
- 失败返回 (void *) -1
#include<sys/ipc.h>
#include<sys/shm.h>
#include<iostream>
#include<unistd.h>
#include<string.h>
using namespace std;
#define FILENAME "/home/lbx/进程间通信博客代码/shmtest.cc"
#define PROJ_ID 0x666
int main()
{
key_t key = ftok(FILENAME,PROJ_ID);
int shmid = shmget(key,4096,IPC_CREAT | 0777);
cout<<"key : " << key<<endl;
sleep(5);
void * start = shmat(shmid,NULL,0);
if(start == (void*)-1)
{
perror("shmat");
return -1;
}
cout<<"start : "<<start<<endl;
sleep(2);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
可以看到,确实成功关联到进程了。
shmdt函数
既然有了关联函数,就有去关联函数。
- shmdt 函数只有一个参数,就是 shmat 的返回值
成功返回0,失败返回-1。
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/stat.h>
#include<iostream>
#include<unistd.h>
#include<string.h>
using namespace std;
#define FILENAME "/home/lbx/进程间通信博客代码/shmtest.cc"
#define PROJ_ID 0x666
int main()
{
umask(0);
key_t key = ftok(FILENAME,PROJ_ID);
int shmid = shmget(key,4096,IPC_CREAT | 0666);
cout<<"key : " << key<<endl;
sleep(5);
void * start = shmat(shmid,NULL,0);
if(start == (void*)-1)
{
perror("shmat");
return -1;
}
cout<<"start : "<<start<<endl;
sleep(2);
int ret = shmdt(start);
if(ret == -1)
{
cout<<"error : "<<errno<< " : "<<strerror(errno)<<endl;
return -1;
}
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
通过共享内存实现sever & client 通信
//shmcom.hp
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
#define FILENAME "/home/lbx/进程间通信博客代码/shmtest.cc"
#define PROJ_ID 0x666
key_t GetKey()
{
key_t key = ftok(FILENAME, PROJ_ID);
if (key == -1)
{
cout << "errno : " << errno << ": " << strerror(errno) << endl;
return -1;
}
return key;
}
int CreatShm(key_t key, int size)
{
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);//此处 0666 表示共享内存可读可写
if (shmid == -1)
{
cout << "errno : " << errno << ": " << strerror(errno) << endl;
return -1;
}
return shmid;
}
int GetShm(key_t key, int size)
{
int shmid = shmget(key, size, IPC_CREAT | 0666);
if (shmid == -1)
{
cout << "errno : " << errno << ": " << strerror(errno) << endl;
return -1;
}
return shmid;
}
void *atachShm(int shmid)
{
void *mem = shmat(shmid, NULL, 0);
if (mem == (void *)-1)
{
cout << "errno : " << errno << ": " << strerror(errno) << endl;
exit(-1);
}
return mem;
}
int dtachShm(void *mem)
{
int ret = shmdt(mem);
if (ret == -1)
{
cout << "errno : " << errno << ": " << strerror(errno) << endl;
return -1;
}
return ret;
}
int ShmDel(int shmid)
{
int ret = shmctl(shmid, IPC_RMID, NULL);
if (ret == -1)
{
cout << "errno : " << errno << ": " << strerror(errno) << endl;
return -1;
}
return ret;
}
//client代码
#include"shmcomm.hpp"
#include<iostream>
using namespace std;
int main()
{
key_t key = GetKey();
int shmid = GetShm(key,4096);
char* start = (char * )atachShm(shmid);
while(1)
{
cout<<"Sever# "<<start<<endl;
sleep(1);
}
int ret = dtachShm((void*)start);
ShmDel(shmid);
return 0;
}
//sever代码
#include"shmcomm.hpp"
#include<iostream>
using namespace std;
int main()
{
key_t key = GetKey();
int shmid = CreatShm(key,4096);
char* start = (char * )atachShm(shmid);
while(1)
{
cin >> start;
cout<<"Send!";
}
int ret = dtachShm((void*)start);
ShmDel(shmid);
return 0;
}
我们可以看到,即使sever没有输入,client依旧一直在读取,这是共享内存和管道不同的地方。
管道和共享内存的区别
通过sever 和 client 的通信,我们发现共享内存并不像管道,具有互斥和同步的特点,需要使用者自行控制保存。
通过观察管道的通信方式我们发现,管道通信需要经过四次拷贝。
- 从标准输入中读取数据到缓冲区中
- 将缓冲区中的数据复制到管道中
- 把管道中的数据复制到sever的缓冲区中
- 将缓冲区中的数据拷贝到标准输出中
而共享内存则不一样。
由于进程都和共享内存相互关联了,因此都是直接从共享内存中读取数据出来。
这也是共享内存比管道快的原因。
总结
本篇博客总结了匿名管道,命名管道以及共享内存的原理和使用方法,相信对大家的学习有一定的帮助。