进程间通信(ipc), 指不同进程之间的信息传输或交换。
我们知道进程间具有独立性,因为写时拷贝存在,父子进程之间向持续性的交换数据是不可能的。如果想让两个进程通信,必须先让不同的进程看到同一份资源作为数据交换的场所。
管道:
System V IPC
POSIX IPC
注意:针对以上分类,我们主要对管道和共享内存详细学习。
什么是管道
匿名管道通信原理:进程之间通过管道进行通信。
主要步骤如下:
假设,我们让父进程对目标文件写入数据,子进程对目标文件读取数据。
注意:
在创建父子进程的时候 进程相关数据结构需要重新拷贝,被打开文件相关内核结构不会被拷贝。因为fork函数只是为了创建子进程,不会对文件相关数据结构作拷贝。
此时父子进程看到的同一份文件资源,并对其进行写,读操作,并不会发生写时拷贝,因为被打开文件内核数据结构是由操作系统维护的,不受进程维护。
文件描述符意义:0:标准输入,1:标准输出,2:标准错误,3:读文件描述符,4:写文件描述符。
进程间通信是内存级通信,不需要将数据写入到磁盘文件中,因为反复的IO会降低效率,也没有必要。
pipe函数一般用于创建匿名管道,pipe函数原型如下:
int pipe( int fd[2] )
参数:
fd: 文件描述符数组,其中fd[0]代表读端,fd[1]代表写端。
返回值:
成功返回0,失败返回-1.
实践代码:
//父进程写入文件,子进程读取文件
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//创建管道
int pipefd[2] = { 0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.
int n = pipe( pipefd );
assert( n !=- 1 );
(void)n;
//创建子进程。
pid_t id = fork();
assert( id != -1 );
if( id == 0 ) //子进程,子进程读取。
{
close(pipefd[1]);
char buffer[1024];
while( true ) //先将数据读取到缓冲区中打印出来。
{
ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);
if( s > 0 )
{
buffer[s] = '\0';
cout << " child get a message[" << getpid() <<"] father#" << buffer << endl;
}
}
exit(0);
}
close(pipefd[0]); //父进程,父进程写入。
string message = "我是父进程,我正在给你发信息";
int count = 0;
char send_buffer[1024];
while( true )
{
snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
}
pid_t ret = waitpid(id,nullptr,0);
(void ) ret;
assert(ret > 0 );
return 0;
}
运行结果如下:
1 . 管道一般用来具有血缘关系的进程用来进程间通信---->常用于父子进程。
2 .管道具有通过让进程间协同,进而提供访问控制。
3.管道提供的是面向流式的通信服务(面向字节流,写一条都一条,如果写得快,读的慢,那么就会一次性读取数据)。
4.管道是基于文件的,文件的生命周期是基于进程的,从而管道的生命周期跟随于进程。如果在通信过程中,父子进程都退出,那么文描述符便会被关闭,管道也会自动退出。
例如,当父进程写入文件10次后便关闭文件描述符退出,而子进程此时read的返回值便会为零,那么子进程也会退出。
int main()
{
//创建管道
int pipefd[2] = { 0 }; //pipefd[0]: 读端 //pipefd[1]: 写端.
int n = pipe( pipefd );
assert( n !=- 1 );
(void)n;
//创建子进程。
pid_t id = fork();
assert( id != -1 );
if( id == 0 ) //子进程,子进程读取。
{
close(pipefd[1]);
char buffer[1024];
while( true ) //先将数据读取到缓冲区中打印出来。
{
ssize_t s = read( pipefd[0],buffer,sizeof(buffer)-1);
if( s > 0 )
{
buffer[s] = '\0';
cout << " child get a message[" << getpid() <<"] father#" << buffer << endl;
}
else if( s == 0 )
{
cout << "writer quit, i quit" << endl;
break;
}
}
close(pipefd[0]);
exit(0);
}
close(pipefd[0]); //父进程,父进程写入。
string message = "我是父进程,我正在给你发信息";
int count = 0;
char send_buffer[1024];
while( true )
{
snprintf( send_buffer,sizeof(send_buffer), "%s: %d",message.c_str(),count++ );
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
cout << count << endl;
if( count == 10 )
{
cout << "writer quit(father)" << endl;
break;
}
}
close( pipefd[1]); //父进程准备退出,关闭文件描述符。
pid_t ret = waitpid(id,nullptr,0);
(void ) ret;
assert(ret > 0 );
return 0;
}
运行结果如下:
5.管道是单项通信,本质上就是半双工通信的一种特殊情况,通信双方中,一方固定为读端,一方固定为写端。
6.两种特殊情况。
注意:
我们可以通过mkfifo命令创建一个命名管道。
[yzh@yzh test1]$ mkfifo fifo
并且,我们可以看到创建出来的文件类型为p,即代表的是管道文件。
此时,我们便可以通过shell脚本命令行每秒将一个字符串写入到命名管道文件中,再从另一端读取该命名管道中的数据打印到显示器上,这也进而体现了两个毫不相关的进程可以通过命名管道通信。
为了让服务端(server)与客户端(client)进程间通信,服务端(client)步骤如下:
#include "common.hpp"
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. 创建管道文件
umask(0);
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" << endl;
int nums = 3;
for (int i = 0; i < nums; i++)
{
pid_t id = fork();
if (id == 0) //随机让每一个子进程读取数据,当client端退出的时候
{
// 3. 编写正常的通信代码了
getMessage(fd);
exit(1);
}
}
for(int i = 0; i < nums; i++) //父进程随机等待子进程退出结果,当子进程全部退出完毕,父进程退出程序。
{
waitpid(-1, nullptr, 0);
}
// 4. 关闭文件
close(fd);
Log("关闭管道文件成功", Debug) << " step 3" << endl;
unlink(ipcPath.c_str()); // 通信完毕,就删除文件
Log("删除管道文件成功", Debug) << " step 4" << endl;
return 0;
}
客户端(server)步骤如下:
#include "common.hpp"
#include
int main()
{
// 1. 获取管道文件
int fd = open(ipcPath.c_str(), O_WRONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
// 2. ipc过程
string buffer;
while(true)
{
cout << "Please Enter Message Line :> ";
std::getline(std::cin, buffer); //从cin中读取数据到buffer中。
write(fd, buffer.c_str(), buffer.size());
}
// 3. 关闭
close(fd);
return 0;
}
log.hpp文件,主要包含打印命名管道文件提示符函数。
#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 cout;
}
common.hpp文件,主要包含一些系统函数头文件。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
#define MODE 0666
#define SIZE 128
string ipcPath = "./fifo.ipc"; //命名管道文件的路径。
运行结果如下:
如果客户端(server)退出后,服务端read读取的返回值变为0,此时表示服务端已经读取到数据结尾,所以子进程也将逐步退出。
如果服务端(server)退出后,客户端(client)写入管道的数据将不会被服务端读取,当客户端(client)下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端也被强制退出。
共享内存是由操作系统维护的,由于在操作系统中可能存在大量使用共享内存进行通信的进程,为了管理这些共享内存,操作系统除了要在物理内存中开辟共享内存之外,还必须通过有关共享内存的数据结构(描述共享内存的所有属性)进行管理。所以,共享内存 = 共享内存块 + 有关共享内存的数据结构。
shmget函数主要用来创建共享内存。
int shmget(key_t key, size_t size, int shmflg);
参数:
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。
使用ftok函数获取key
key_t ftok(const char *pathname, int proj_id);
参数:
注意:
shmget的常见组合方式
IPC_CREAT: 如果操作系统内核中不存在与key值相等的共享内存,则新建一个共享内存并返回共享内存的用户层ID。如果存在这样的共享内存,那么直接返回该共享内存的标句柄。————表明不管操作系统中是否含有与key相等的共享内存,都会返回一个共享内存,但是无法确定是否为新建的共享内存。
IPC_CREAT | IPC_EXCL: 如果操作系统内核中不存在与key值相等的共享内存,则新建一个共享内存,如果存在,则出错返回。————表明如果成功返回,该共享内存一定是那个新建的共享内存。
实践代码如下:
我们创建一个共享内存,并打印该用户层ID和获取到的key值。
int main()
{
//创建公共的key值
key_t k = ftok(PATH_NAME,PROJ_ID);
if (k == -1 )
{
perror("ftok");
}
Log("create key done",Debug) << "server key " << k << std::endl;
//创建共享内存--必须要创建一个全新的共享内存。
int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1 )
{
perror("shmget");
}
cout << "该共享内存的句柄 ->" << shmid << endl;
}
运行结果如下:
当然,我们也可以使用ipcs -m 命令来查看共享内存相关信息。(包含共享内存的key和shmid)
我们知道,管道的生命周期基于文件,文件的生命周期基于进程,所以管道的生命周期基于进程,当进程退出后,管道也将被释放大。
但是,共享内存的生命周期是基于操作系统内核,并不与进程相关联。所以,当相关进程退出时,共享内存并不会释放,直到关机重启。
如果我们需要及时将共享内存释放,有两种方法:
使用命令行释放共享内存
我们可以通过ipcrm -m shmid 命令将共享内存释放。
使用shmctl函数释放共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
返回值:
成功返回0,失败返回-1.
以下为shmctl函数中cmd参数常见命令
命令 | 说明 |
---|---|
IPC_STAT | 获取共享内存的当前关联值,此时参数buf作为输出型参数 |
IPC_SET | 在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 删除共享内存段 |
int main()
{
//创建公共的key值
key_t k = ftok(PATH_NAME,PROJ_ID);
if (k == -1 )
{
perror("ftok");
}
Log("create key done",Debug) << " server key " << k << std::endl;
//创建共享内存--必须要创建一个全新的共享内存。
int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1 )
{
perror("shmget");
}
sleep(10);
//删除共享内存。
int n = shmctl(shmid,IPC_RMID,NULL);
assert( n != -1 );
(void)n;
Log("delete shm done",Debug) << " shmid " << shmid << endl;
}
运行结果如下:
我们使用相关脚本命令监视共享内存,当共享内存创建完毕,shmid值变为1。当进程结束,共享内存释放完毕,shmid值变为0。
我们可以通过shmat函数将共享内存段连接到对应的进程地址空间中。
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
返回值:
成功:返回指向共享内存的指针;失败:返回-1
以下我们便使用shmat函数对共享内存关联。
int main()
{
//创建公共的key值
key_t k = ftok(PATH_NAME,PROJ_ID);
if (k == -1 )
{
perror("ftok");
}
Log("create key done",Debug) << " server key " << k << std::endl;
//创建共享内存--必须要创建一个全新的共享内存。
int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1 )
{
perror("shmget");
}
sleep(10);
//关联共享内存
char* shmaddr = (char*)shmat(shmid,nullptr,0);
Log("attach shm",Debug) << "shmid " << shmid << std::endl;
sleep(10);
//删除共享内存。
int n = shmctl(shmid,IPC_RMID,NULL);
assert( n != -1 );
(void)n;
Log("delete shm done",Debug) << " shmid " << shmid << endl;
}
运行结果如下:
此时,我们发现关联该共享内存进程数由0变为1,随着该进程退出,又变为了0。
我们可以使用shmdt函数将共享内存段与当前进程脱离。
int shmdt(const void *shmaddr);
参数:
shmaddr: 由shmat所返回的指向共享内存的指针。
返回值:
成功返回0;失败返回-1.
注意:
将共享内存段与当前进程脱离不等于删除共享内存段。
我们接下来增加shmdt函数对该共享内存去关联。
int main()
{
//创建公共的key值
key_t k = ftok(PATH_NAME,PROJ_ID);
if (k == -1 )
{
perror("ftok");
}
Log("create key done",Debug) << " server key " << k << std::endl;
//创建共享内存--必须要创建一个全新的共享内存。
int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1 )
{
perror("shmget");
}
sleep(10);
//关联共享内存。
char* shmaddr = (char*)shmat(shmid,nullptr,0);
Log("attach shm",Debug) << "shmid " << shmid << std::endl;
sleep(10);
//共享内存去关联
//将指定的内存空间,去关联
int n1 = shmdt(shmaddr);
assert( n1 != -1 );
Log("detach shm",Debug) << " shmid " << shmid << std::endl;
sleep(10);
//删除共享内存。
int n2 = shmctl(shmid,IPC_RMID,NULL);
assert( n2 != -1 );
(void)n2;
Log("delete shm done",Debug) << " shmid " << shmid << endl;
}
``
通过结果发现,随着shmdt函数去关联,nattch(关联数)也由1变为了0,即表示去关联成功。
结论一:只要通信双方使用共享内存,一方直接向共享内存写入数据,另一方,就可以立马看到。因为共享内存是所有进程间通信(IPC)速度最快的,不需要过多的拷贝。
shmServer.cpp
int main()
{
//创建公共的key值
key_t k = ftok(PATH_NAME,PROJ_ID);
if (k == -1 )
{
perror("ftok");
}
Log("create key done",Debug) << " server key " << k << std::endl;
//创建共享内存--必须要创建一个全新的共享内存。
int shmid = shmget(k,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1 )
{
perror("shmget");
}
sleep(10);
//关联共享内存。
char* shmaddr = (char*)shmat(shmid,nullptr,0);
Log("attach shm",Debug) << "shmid " << shmid << std::endl;
sleep(10);
//向shamaddr中读取数据。
for(;;)
{
// 临界区
printf("%s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
sleep(1);
}
//共享内存去关联
//将指定的内存空间,去关联
int n1 = shmdt(shmaddr);
assert( n1 != -1 );
Log("detach shm",Debug) << " shmid " << shmid << std::endl;
sleep(10);
//删除共享内存。
int n2 = shmctl(shmid,IPC_RMID,NULL);
assert( n2 != -1 );
(void)n2;
Log("delete shm done",Debug) << " shmid " << shmid << endl;
}
shmClient.cpp
int main()
{
//1.创建公共的key值。
key_t k = ftok(PATH_NAME,PROJ_ID);
assert( k != -1 );
Log("create key done",Debug) << "client key " << k << std::endl;
//2. 获取共享内存。
int shmid = shmget(k, SHM_SIZE, 0);
assert( shmid >= 0 );
Log("create shm success", Error) << " client key : " << k << endl;
//3.关联共享内存。
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if(shmaddr == nullptr)
{
Log("attach shm failed", Error) << " client key : " << k << endl;
exit(3);
}
Log("attach shm success", Error) << " client key : " << k << endl;
//4.向共享内存写入数据
char a = 'a';
for(; a <= 'z'; a++)
{
shmaddr[a-'a'] = a;
// 我们是每一次都向shmaddr[共享内存的起始地址]写入
snprintf(shmaddr, SHM_SIZE - 1,\
"hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
getpid(), a);
sleep(1);
}
return 0;
}
common.hpp
#include
//#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
#define PATH_NAME "/home/yzh/test1"
#define PROJ_ID 0X66
#define SHM_SIZE 4096
由运行结果可知,当shmServer端运行,创建好共享内存后,随后运行shmClient端写入数据到共享内存时(每一次写入数据都写入到共享内存的初始地址),由shmServer读取内存数据打印在屏幕上。
1.只要通信双方使用shm,一方直接向共享内存中写入数据,另一方就立马可以从共享内存中获取,因为相比于管道而言,共享内存是所有进程通信(IPC)中速度最快的。
管道通信过程:
当我们使用管道通信,需要调用read,write接口进行数据传输。而在这个过程中,需要进行四次拷贝。
我们很明显的可以看出,两个进程间使用共享内存通信之间并不需要在客户端和服务端中建立临时缓冲区传输数据,所以只需要两次拷贝。
2.相比于管道而言,共享内存缺乏访问控制,会带来并发问题
并发问题,有可能会带来当一端还在写入数据时,还没有将数据写完,但是另一端就立马读取的问题。
为此,我们可以在shmServer端和shmClient端通信时增加一个命名管道通信(通信内容不重要),此时相当于也让两端在共享内存中通信时也具备着命名管道中进程控制的性质。
common.hpp
我们让命名管道在程序运行时就已经创建了,在进程退出时自动销毁并且对它一些函数进行包装。
#include
//#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
#include
#include
#include
using namespace std;
#define FIFO_NAME "myfifo"
#define PATH_NAME "/home/yzh/test1"
#define PROJ_ID 0X66
#define SHM_SIZE 4096
#define READ O_RDONLY
#define WRITE O_WRONLY
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));
assert(s == sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
Log("唤醒中....", Notice) << "\n";
}
void CloseFifo(int fd)
{
close(fd);
}
shmServer.cpp
在通信时,我们先能够读取命名管道中的数据,才能读取共享额你存中的数据。
Init init;
int main()
{
Log("child pid is : ", Debug) << getpid() << endl;
key_t k = ftok(PATH_NAME, PROJ_ID);
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 | IPC_EXCL | 0666); //
if(shmid < 0)
{
Log("create shm failed", 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", Error) << " client key : " << k << endl;
exit(3);
}
Log("attach shm success", Error) << " client key : " << k << endl;
int fd = OpenFIFO(FIFO_NAME, READ);
for(;;)
{
Wait(fd);
// 临界区
printf("%s\n", shmaddr);
if(strcmp(shmaddr, "quit") == 0) break;
// sleep(1);
}
// 去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << k << endl;
// sleep(10);
// client 要不要chmctl删除呢?不需要!!
return 0;
}
shmServer.cpp
当shmServer运行,并且输入数据,才能激活命名管道。
int main()
{
Log("child pid is : ", Debug) << getpid() << endl;
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0)
{
Log("create key failed", Error) << " client key : " << k << endl;
exit(1);
}
Log("create key done", Debug) << " client key : " << k << endl;
// 获取共享内存
if(shmid < 0)
{
Log("create shm failed", 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", Error) << " client key : " << k << endl;
exit(3);
}
Log("attach shm success", Error) << " client key : " << k << endl;
// sleep(10);
int fd = OpenFIFO(FIFO_NAME, WRITE);
// 使用
// client将共享内存看做一个char 类型的buffer
while(true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE-1);
if(s > 0)
{
shmaddr[s-1] = 0;
Signal(fd);
if(strcmp(shmaddr,"quit") == 0) break;
}
}
close(fd);
// 去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << k << endl;
// sleep(10);
// client 要不要chmctl删除呢?不需要!!
return 0;
}
在之前进程通信的几种方式中,本质上都是优先解决一个问题,让不同的进程看到同一份资源,比如共享内存,也带来了一些时序问题,造成两端数据不一致问题。
信号量的相关概念。
每一个进程想访问临界资源,必须先申请信号量,而信号量本质上就是一个计数器(类似于 int count )。当申请信号量成功,本质上就是让信号量计数器–,当信号量减为0时,就不能再增加进程访问临资源了。
能不能用一个整数(n)去标识信号量呢?
cpu执行命令流程:
1.将内存中的数据加载到cpu内的寄存器中(读指令)
2:申请信号量( 分析 & 执行指令 )——> n–;
3:将CPU修改完毕的n写回内存。
然而执行流CPU执行命令的时候,每一步都有可能被另一个进程切换,此时寄存器中包含着执行流的上下文数据,被整个执行流所共享。
假设n = 2;
如果当一个进程正在执行到第2步( n = 1 ) ,然而又被另一个进程切换到第一步( 此时 n 又变成了 2 )并且执行完第三步,n 又变成了1,原本n = 2意味着只能让2个进程访问临界资源的某一部分,执行完毕后,n = 1,表明这还能容纳一个进程访问临界资源。
这就是说明整数count导致的时序问题,此时n有中间状态,进而造成数据并不一致问题。
信号量计数器
申请信号量 - > 计数器-- -> P操作(必须是具有原子性)
释放信号量 -> 计数器++ -> V操作(必须是原子性)