由于进程地址空间的存在,所以进程间有具有独立性,一个进程看不到另一个进程的数据。那么如果我们想让进程间通信,就必须先让它们先看到同一份资源。常见的进程间通信的方法有管道,System V IPC, POSIX IPC。管道是最古老的一种通信手段,System V ,POSIX 是用于进程间通信的标准。
管道的本质是文件,让两个进程看到同一份文件,即指向同一个struct file 结构体
,就能让两个进程看到同一份资源,之后就可以开始正常通信了。
匿名管道就是利用系统调用在程序内创建一个内存级文件(只有struct file
结构体),通过父子进程间的继承关系来看到同一份文件。
接口:
当父进程创建子进程时,子进程会复制大部分父进程的task_struct
。故此父进程打开的文件,子进程也能看到。如图:
由于管道是单向通信,所以我们应该关闭不需要的fd。如果我们想让父进程读,子进程写,则需关闭父进程写端pipefd[1]
,子进程读端pipefd[0]
。之后父进程就可以正常通信了。代码示例如下:
int main()
{
int pipefd[2];
int n = pipe(pipefd);
assert(n != -1);
pid_t id = fork();
if (id == 0)
{
// 子进程
close(pipefd[0]);
// 通信代码。。。
//通信完毕,关闭文件描述符
close(pipefd[1]);
exit(0);
}
// 父进程
close(pipefd[1]);
// 通信代码。。。。
waitpid(id, nullptr, 0);
close(pipefd[0]);
return 0;
}
特性:
SIGPIPE
信号匿名管道主要用于具有血缘关系的进程,如果想让两个没有关系的进程通信,就需要用到命名管道,命名管道具有Inode节点,但是没有Data Blocks,不会将数据刷新到磁盘,它只用于进程间通信。创建命名管道有两种方式:1.命令行:mkfifo 指令 2.函数接口mkfifo
接口:
需要通信的多个进程中,只要有一个进程创建了命名管道,然后其他进程只需要知道路径,就能满足多个进程看到同一份资源。
代码:
下面构造一个客户端与服务端的代码示例。由服务器端创建命名管道,服务区读,客户端写
#define PATHNAME "./fifo"
// server.c
int main()
{
mkfifo(PATHNAME, 0666);
int fd = open(PATHNAME, O_RDONLY);
// 调用read函数进行读取数据通信
close(fd);
// 删除管道文件
unlink(PATHNAME);
return 0;
}
//
// client.c
int main()
{
int fd = open(PATHNAME, O_WRONLY);
// 调用write函数向管道写入数据
close(fd);
return 0;
}
进程间通信除了管道,还可以调用系统接口在内存中创建一块共享空间,并且将这块共享空间映射到程序地址空间中(关联),此时进程就可以读写这块空间。共享内存在操作系统内可能有很多,为了便于管理,需要有一个结构体。类似于进程等于内核数据结构+代码,共享内存=内核数据结构+空间。
ipcs -m 可以查看系统内部的共享内存
ipcrm -m shmid 可以根据shmid删除共享内存
三个文件,一个头文件,放函数定义,一个客户端文件,一个服务端文件,服务端创建共享内存和删除共享内存。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
//int shmget(key_t key, size_t size, int shmflg);
//key_t ftok(const char *pathname, int proj_id);
const string pathname = ".";
const int proj_id = 0432;
const size_t shmSize = 4096;
key_t getKey(const string& pathname, int proj_id)
{
return ftok(pathname.c_str(), proj_id);
}
// 当shmflg = IPC_CREAT | IPC_EXCL ,表明创建一个共享内存,
// 如果不存在,则创建,存在则报错
int shmHelper(int shmflg)
{
key_t key = getKey(pathname, proj_id);
int shmid = shmget(key, shmSize, shmflg);
cout << shmid << endl;
if (shmid == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(1);
}
return shmid;
}
int createShm()
{
umask(0);
return shmHelper(IPC_CREAT | IPC_EXCL|0666);
// 不存在则创建,存在则返回shmid
}
int getShm()
{
return shmHelper(IPC_CREAT);
// 不存在则创建,存在则失败返回
}
char* attchShm(int shmid)
{
// 用法和malloc差不多
char* begin = (char*)shmat(shmid, nullptr, 0);
if (begin == (void*)-1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(1);
}
return begin;
}
void detachShm(char* begin)
{
int n = shmdt(begin);
if (n == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(1);
}
}
void destroyShm(int shmid)
{
int n = shmctl(shmid, IPC_RMID, nullptr);
if (n == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(1);
}
}
#include"common.hpp"
int main()
{
// 1. 创建共享内存
int shmid = createShm();
// 2.与进程关联
char* begin = attchShm(shmid);
//中间就是通信部分
int i = 0;
while (true)
{
sleep(3);
cout << begin[i++] << endl;
i %= 25;
sleep(1);
}
// 3.去关联
detachShm(begin);
// 4. 销毁共享内存
destroyShm(shmid);
return 0;
#include"common.hpp"
int main()
{
// 1. 获取共享内存
int shmid = getShm();
// 2.与进程关联
char* begin = attchShm(shmid);
//通信部分
// 共享内存没有保护机制,所以
int i = 0;
while (true)
{
begin[i++] = 'a'+i;
i %= 25;
sleep(1);
}
// 3.去关联
detachShm(begin);
return 0;
System V IPC 有三种通信方式:共享内存,消息队列,信号量。由于System V 定义的是一套标准,所以它们的接口都很相似。
命令行中可以用ipcs
查看这三种通信方式的属性。也可以用 ipcs -q
查看消息队列,ipcs -s
查看信号量,ipcs -m
查看共享内存。删除命令:ipcrm -m/-s/-q
下面初步了解一下接口,知道它们的相似性。
信号量(信号灯),可以将它看作一个计数器,表明可用资源的数量。信号量可与共享内存协同工作,每一次申请空间资源时,信号量-1,当信号量为0时,申请资源的进程挂起。当释放空间资源时,信号量+1。
当sem_op 为-1时,表明信号量做-1操作;当sem_op为1时,表明信号量做+1操作