进程间通信的本质
进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的简单理解(举例)
看代码:
#include
#include
using namespace std;
int main()
{
pid_t id=fork();
if(id==0)
{
cout<<"hello i am father!"<
为什么父子进程会向同一个显示器文件打印?
进程间通信的分类
管道
匿名管道pipe
命名管道
System V IPC
消息队列
共享内存
信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
何为管道?
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“
who进程的运行结果通过标准输出将数据流入管道,wc -l 通过标准输入从管道内读取数据,处理后得到的结果再打到标准输出上让用户看到。
who命令查看当前服务器登录用户,wc -l 统计行数
仅限于父子进程间通信的管道文件,本质是双方进程一方打开写端关闭读端,另一端打开读端关闭写端,刻意营造单向流动的局面的一种管道
简明阐述:
创建管道文件函数:pipe()
使用pipe()完成进程间通信:提供框架,具体自行测试
// 半双工,要么在读要么在写
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n;
pid_t id = fork();
if (id == 0)
{
// child:read
close(pipefd[1]);
while (true)
{
//读操作
}
close(pipefd[0]);
exit(1);
}
else
{
// father:write
close(pipefd[0]);
while (true)
{
//写操作
}
close(pipefd[1]);
pid_t ret = waitpid(id, nullptr, 0);
assert(ret > 0);
(void)ret;
}
return 0;
}
写操作示例:
// father:write
close(pipefd[0]);
char send_buffer[1024 * 8]; // 缓冲区
while (true)
{
fgets(send_buffer, sizeof send_buffer - 1, stdin);
ssize_t s = write(pipefd[1], send_buffer, strlen(send_buffer));
}
读操作示例:
// child:read
close(pipefd[1]);
char buffer[1024 * 8];
while (true)
{
// sleep(5);
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
if (strcmp(buffer, "quit") == 0)
{
cout << "ready to close child" << endl;
break;
}
cout << "copy that:[" << getpid() << "] " << buffer;
}
else
{
// 读不到东西了,写端关闭会走到这里
cout << "writing quit, reading quit!" << endl;
break;
}
运行结果:
注意
管道读写规则
何为原子性?
管道的特点
何为互斥与同步?
首先我们需要知道什么是临界资源?临界资源是一次仅允许一个进程独占使用的不可剥夺的资源,相应的,临界区就是进程访问临界资源的那段程序代码。一次仅允许一个进程在临界区中执行
互斥:当一个进程正在临界区中访问临界资源时,其他进程不能进入临界区
同步:合作的并发进程需要按先后次序执行,例如:一个进程的执行依赖于合作进程的消息或者信号,当一个进程没有得到来自于合作进程的消息或者信号时需要阻塞等待,直到消息或者信号到达后才被唤醒
以前面所提到的进程池为例,多个管道,但写端都是父进程,而读端是由父进程所创建的多个子进程,那么父进程向管道写进资源时,此时多个读端都会处于堵塞状态,等待父进程写完毕,这就体现了同步过程,一旦写完毕,多个子进程便会争相去读取这份临界资源,但每次最多只能有一个进程读取此时的管道数据,这就体现了互斥,当然这只是冰山一角,更深层次的还有待探讨。
再次理解管道读写规则的四种特殊情况
进程池代码举例
// 进程池:父进程派发任务让多个子进程执行
#include
#include
#include
#include
#include
#include
#include
#include
#include "Task.hpp"
#define PROCESS_NUM 5
int waitCommand(int waitFd, bool &quit)
{
// waiting for father's writing, now is blocking
int command = 0;
ssize_t s = read(waitFd, &command, sizeof(command));
if (s == 0) // writing's closing
{
quit = true;
return -1;
}
// promise of correct command
assert(s == sizeof(uint32_t));
return command;
}
void distriAndWakeUp(pid_t id, int fd, uint32_t command)
{
write(fd, &command, sizeof(command));
std::cout << "main process: call procesee:[" << id << "] execute-> " << desc[command] << " through " << fd << std::endl;
}
int main()
{
load();
std::vector> slots;
for (int i = 0; i < PROCESS_NUM; ++i)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n;
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// exit in the process, ineffect of father
// child:read
// turn down write
close(pipefd[1]);
while (true)
{
// wait command
bool quit = false;
int command = waitCommand(pipefd[0], quit);
if (quit)
break;
// coduct command
if (command >= 0 && command < handlerSize())
{
callbacks[command]();
}
else
{
std::cout <<"error command"<< command << std::endl;
}
}
exit(1);
}
// father:write
close(pipefd[0]);
slots.push_back(std::pair(id, pipefd[1]));
}
// dispatch order
//more random
srand((unsigned long)time(nullptr) ^ getpid());
//srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 让数据源更随机
while (true)
{
// choose a task
int command = rand() % handlerSize();
// choose a process
int choice = rand() % slots.size();
// distribute to a pointed process
distriAndWakeUp(slots[choice].first, slots[choice].second, command);
sleep(1);
}
// close fd
for (const auto &slot : slots)
{
close(slot.second);
}
// recycle information
for (const auto &slot : slots)
{
waitpid(slot.first, nullptr, 0);
}
return 0;
}
Task.hpp文件:
//.hpp include func implementation
#pragma once
#include
#include
#include
#include
#include
#include
typedef std::function func;
std::unordered_map desc;
std::vector callbacks;
void readMySQL()
{
std::cout << "sub process[ " << getpid() << " ]Database Access task!\n"
<< std::endl;
}
void AnalyseURL()
{
std::cout << "sub process[ " << getpid() << " ]URL Analysis task!\n"
<< std::endl;
}
void cal()
{
std::cout << "sub process[ " << getpid() << " ]Encryption task!\n"
<< std::endl;
}
void save()
{
std::cout << "sub process[ " << getpid() << " ]Data Persistence task!\n"
<< std::endl;
}
void load()
{
// load task
desc.insert(std::make_pair(callbacks.size(), "readMySQL:Database Access task\n"));
callbacks.push_back(readMySQL);
desc.insert(std::make_pair(callbacks.size(), "URL Analysis task!\n"));
callbacks.push_back(AnalyseURL);
desc.insert(std::make_pair(callbacks.size(), "URL Analysis task!\n"));
callbacks.push_back(cal);
desc.insert(std::make_pair(callbacks.size(), "Data Persistence task!\n"));
callbacks.push_back(save);
}
// Preview task
void showHandler()
{
for (const auto &dc : desc)
{
std::cout << dc.first << "\t" << dc.second << std::endl;
}
}
// task number
int handlerSize()
{
return callbacks.size();
}
匿名管道是仅限与父子进程通信的渠道,而让没有关系的两个之间通信,可以使用命名管道。
命名管道是一种特殊类型的文件,又叫FIFO文件
这种文件不具有文件内容,但具有文件属性,也就是是实实在在存在于磁盘上的文件,但又和匿名管道一样,是内存级的文件,并且不会将数据刷到磁盘上
创建命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
可以从程序创建,相关函数:
int mkfifo(const char* filename,mode_t mode);
mode为文件的默认权限,会受到umask掩码的影响,因此在一个进程中可以将默认掩码设置为0
命名管道的打开规则
匿名与命名管道的区别
用命名管道实现server/client间通信:
commu.hpp 文件代码
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
#define SIZE 128
#define FIFO_MODE 0666
std::string ipcPath="./fifo.ipc";
Log.hpp文件代码
#pragma once
#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(const std::string message, int leval)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[leval] << " | " << message << std::endl;
}
client.cc文件代码
//open fifo -> write message to server
#include "commu.hpp"
int main()
{
//open fifo
int fd=open(ipcPath.c_str(),O_WRONLY);
assert(fd!=-1);
//ipc
std::string buffer;
while(true)
{
std::cout<<"Please input the message :> ";
std::getline(std::cin,buffer);
write(fd,buffer.c_str(),buffer.size());
}
//close fifo
close(fd);
return 0;
}
server.cc文件代码
// make fifo -> open fifo -> read client
#include "commu.hpp"
void getMessage(int fd)
{
char buffer[SIZE];
while (true)
{
memset(buffer, 0, sizeof(buffer));
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
if(strcmp(buffer,"quit")==0)
break;
std::cout << "[" << getpid() << "]"
<< "client say: " << buffer << std::endl;
}
else if (n == 0)
{
std::cout << "[" << getpid() << "]"
<< "End of the File, client quit, server quit,too! " << std::endl;
break;
}
else
{
perror("error");
break;
}
}
}
int main()
{
// make fifo
int n = mkfifo(ipcPath.c_str(), FIFO_MODE);
assert(n != -1);
Log("Creat fifo successfully!", DEBUG);
// open fifo
int fd = open(ipcPath.c_str(), O_RDONLY);
assert(fd != -1);
Log("Open fifo successfully!", DEBUG);
// ipc
int nums = 3;
for (int i = 0; i < nums; ++i)
{
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// child:
getMessage(fd);
exit(1);
}
}
for (int i = 0; i < nums; ++i)
{
waitpid(-1, nullptr, 0);
}
// close fifo
close(fd);
Log("close fifo successfully!", DEBUG);
// delete fifo
unlink(ipcPath.c_str());
Log("delete fifo successfully!", DEBUG);
return 0;
}
由于我设置了三个子进程同时接收,因此收到quit命令时,由于管道是临界资源,只有其中一个进程收到退出命令,其他进程依旧存在,所以需要quit三次才能将服务端退出。也作为一个验证的调试程序,可以自行根据要求修改代码。
用命名管道实现文件拷贝
整体代码只需要对ipc过程进行修改,因此只展示ipc部分代码:
server.cpp:
// ipc
int fd_copy=open("test_copy.txt",O_WRONLY | O_CREAT,0666);
assert(fd_copy);
char msg[SIZE];
ssize_t s=read(fd,msg,sizeof(msg)-1);
if(s>0)
{
write(fd_copy,msg,s);
}
client.cpp:
//ipc
char buffer[SIZE];
int fd_sorce=open("test.txt",O_RDONLY);
assert(fd_sorce);
while(true)
{
ssize_t s =read(fd_sorce,buffer,sizeof(buffer)-1);
if(s>0)
{
write(fd,buffer,s);
}
else
{
//DEBUG
break;
}
}
客户端运行后,服务端执行完后就立马退出了,而此时对应文件就已经拷贝完成
除了使用管道文件让不同进程间看到同一份资源外,操作系统还专门设计有一种通信方式:System V IPC,其中System V共享内存就是我们要学习的一种临界资源。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。通俗点理解,使用管道文件时,我们还需要用的系统调用接口来建立管道与使用管道,但共享内存是操作系统已经设计好的一种具有内存块和数据结构的资源,不再需要使用系统调用接口。
共享内存数据结构:
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 */
};
可以理解成临界资源从文件转到了内存里。
想要使用共享内存,我们需要经过以下步骤:
shmget函数
int shmget(key_t key,size_t size, int shmflg);
参数:key
key:不同进程找到相同共享内存段的键值,也就是标识共享内存段的特殊值
相当于有一扇门,叫做共享内存,而不同进程想要实现通信,就得打开这扇门,而打开这扇门的唯一密码就是key值,其中一个进程设定好key值后,并申请好共享内存空间,另一个进程想要通信,就得拥有相同的键值。键值一般通过算法来转化,我们使用ftok函数来转化获取key
参数:size
size:共享内存大小,且大小最好为页的整数倍!页的大小:4096字节
参数:shmflg
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码,失败返回-1。类比文件成功打开时的文件描述符fd!
shmat函数
void *shmat(int shmid,const void* shmaddr,int shmflg);
参数:shmid
shmid:共享内存标识,即shmget函数的返回值,旨在告诉编译器想要链接哪一块被申请的共享内存
参数:shmaddr
shmaddr:指定连接的地址
说明:一般都为NULL,让系统自由挂接合适的位置
参数:shmflg
shmflg:它的两个可能取值是SHM_RND和SHM_RONLY
返回值:成功返回一个指针,指向共享内存的第一个节,失败返回-1
shmdt函数
功能:将共享内存段与当前进程脱离,又叫去关联
int shmdt(const void* shmaddr);
参数:shmaddr
shmaddr:由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
int shmctl(int shmid,int cmd.struct shmid_ds *buf);
参数:shmid
shmid:由shmget返回的共享内存标识码
参数:cmd
cmd:将要采取的动作->三个可取值
参数:buf
buf:指向一个保存着共享内存模式状态和访问权限的数据结构,若cmd设置为IPC_RMID即删除共享内存段时,buf设为nullptr
返回值:成功返回0;失败返回-1
使用共享内存进行server/client 进行ipc的大致框架
// creat shared memory -> link to shared memory -> ipc -> unlink -> delete
int main()
{
// creat key for shm
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key == -1)
{
exit(1);
}
// creat shared memory
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | MODE);
if (shmid == -1)
{
exit(1);
}
// link to shared memory
char *shmadd = (char *)shmat(shmid, nullptr, 0);
if (shmadd == (void *)-1)
{
exit(1);
}
// ipc
while (true)
{
//举例
//Wait(fd);
// printf("%s\n", shmadd);
//sleep(1);
//if (strcmp(shmadd, "quit") == 0)
// break;
}
// unlink
int n = shmdt(shmadd);
if (n == -1)
{
exit(1);
}
// remove
n = shmctl(shmid, IPC_RMID, nullptr);
if (n == -1)
{
exit(1);
}
Closefifo(fd);
return 0;
}
// get shared memory -> link to -> unlink
int main()
{
// get shared memory
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key == -1)
{
exit(1);
}
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid == -1)
{
exit(1);
}
// link to
char *shmadd = (char *)shmat(shmid, nullptr, 0);
if (shmadd == (void *)-1)
{
exit(1);
}
// ipc
while (true)
{
//举例
// ssize_t s = read(0, shmadd, SHM_SIZE - 1);
// if (s > 0)
// {
// shmadd[s - 1] = 0;
// Signal(fd);
// if (strcmp(shmadd, "quit") == 0)
// break;
// }
}
Closefifo(fd);
// unlink
int n = shmdt(shmadd);
if (n == -1)
{
exit(1);
}
return 0;
}
整体框架就是如此,具体ipc过程可根据需求测试。
命令ipcs -m 可以用来查看此时系统内被申请的共享内存的属性状态
命令ipcrm +shmid也可以用来删除共享内存,但此操作并不会去关联
结论1:
只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到,因此共享内存是所有进程间通信速度最快的!
原因:
结论2:
while (true)
{
//举例
// ssize_t s = read(0, shmadd, SHM_SIZE - 1);
// if (s > 0)
// {
// shmadd[s - 1] = 0;
// Signal(fd);
// if (strcmp(shmadd, "quit") == 0)
// break;
// }
}
Closefifo(fd);
// unlink
int n = shmdt(shmadd);
if (n == -1)
{
exit(1);
}
return 0;
}
整体框架就是如此,具体ipc过程可根据需求测试。
命令ipcrm +shmid也可以用来删除共享内存,但此操作并不会去关联
结论1:
只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到,因此共享内存是所有进程间通信速度最快的!
原因:
在此简单io中,相较于管道,若使用共享内存能减少2次拷贝
结论2:
共享内存缺乏访问控制,会带来并发问题
相比于管道文件通信方式,管道文件自带同步与互斥机制,因此能够有条不紊的进行,但由于共享内存专注于速度,少了访问控制,因此当多个进程一起看到同一份临界资源时,一旦有数据在临界资源里,这份数据将遭到哄抢,有可能会造成数据丢失或数据不一。