1.进程间通信介绍
1.1 进程间通信目的
进程间通信的主要目的是实现不同进程之间的数据交换和协作,以实现更复杂的任务和功能。以下是进程间通信的一些主要目的:
数据共享:进程间通信可以用于共享数据,使得多个进程可以访问和修改相同的数据。这对于需要共享大量数据的任务非常有用,可以避免数据的复制和传输开销。
资源共享:进程间通信可以用于共享系统资源,如文件、设备、数据库等。多个进程可以同时访问和操作这些资源,提高系统的并发性和效率。
任务协作:进程间通信可以用于不同进程之间的协作和任务分配。通过消息传递、同步和互斥机制,进程可以协调完成复杂的任务,实现并行处理和分布式计算。
进程管理:进程间通信可以用于进程的管理和控制。父进程可以通过与子进程的通信来监控和控制子进程的运行状态,如启动、暂停、终止等。
消息传递:进程间通信可以用于在不同进程之间传递消息和信息。进程可以通过消息队列、管道、套接字等方式进行通信,实现信息的传递和交换。
并发编程:进程间通信可以用于实现并发编程,多个进程可以并行执行不同的任务,并通过通信机制进行协调和同步,实现更高效的并发处理
1.2 进程间通信分类
①管道:
a.匿名管道 pipe
b.命名管道
②System V IPC
a.System V 消息队列
b.System V 共享内存
v.System V 信号量
③POSIX IPC
a.消息队列
b.共享内存
c.信号量
d.互斥量
e.条件变量
f.读写锁
2.管道
2.1 什么是管道
管道是一种用于进程间通信的机制,可以将一个进程的输出连接到另一个进程的输入,实现它们之间的数据传递。在Linux和Unix系统中,管道通常使用竖线符号( | )来表示,用于连接两个命令,将第一个命令的输出作为第二个命令的输入。
管道有两种类型:匿名管道和命名管道
管道的特点:
a.管道是一种半双工通信机制,数据只能在一个方向上流动。
b.管道具有固定的缓冲区大小,一旦缓冲区满了,写入进程将被阻塞,知道有空间可用。
c.管道是基于字节流的通信,没有消息边界的概念。
2.2 匿名管道
2.2.1 简单介绍
匿名管道是最常见的一种管道类型,他是一种单向的通信机制,只能用于具有父子关系的进程之间或者具有共同祖先的进程之间。他是一种单向的通道,允许一个进程将数据写入管道的一端,另一个进程从另一端读取这些数据。
2.2.2 pipe函数
匿名管道的创建通过调用pipe()函数来完成。
#include <unistd.h>
int pipe(int pipefd[2]);
参数pipefd是一个整型数组,用于存储管道的文件描述符。数组的第一个元素pipefd[0] 是管道的读端文件描述符,用于从管道读取数据,而第二个元素pipe[1]是管道的写端文件描述符,用于向管道写入数据。
调用pipe()函数会创建一个管道,并将读写端的文件描述符存储在pipefd数组中。成功创建管道时,pipe()函数返回0;失败时,返回-1,并设置相应的错误码。
使用pipe()函数创建的管道是一个半双工的通信通道,只能实现单向数据传输。通常,pipe()函数会在父进程中调用,然后再创建子进程来实现父子进程间的通信。父进程通过写入pipefd[1]将数据传递给子进程,子进程通过读取pipefd[0]来接收数据。
2.2.3 匿名管道的特点
单向通信:匿名管道只支持单向数据传输,即一个进程向管道写入数据,另一个进程从管道读取数据。
具有缓冲区:匿名管道具有固定大小的缓冲区,当写入数据超过缓冲区大小时,写入进程会被阻塞,直到有足够的空间可用。同样,当读取数据时,如果缓冲区为空,读取进程也会被阻塞,直到有数据可读。
一次性使用:匿名管道通常用于具有父子关系的进程之间,一般在创建子进程之前创建管道,并在子进程中进行数据的读取和写入。一旦管道被关闭,就无法再次使用。
2.2.4 简单的示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int fds[2];
char buf[100];
int len;
if (pipe(fds) == -1)
perror("make pipe"), exit(1);
// read from stdin
while (fgets(buf, 100, stdin))
{
len = strlen(buf);
// write into pipe
if (write(fds[1], buf, len) != len)
{
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// read from pipe
if ((len = read(fds[0], buf, 100)) == -1)
{
perror("read from pipe");
break;
}
// write to stdout
if (write(1, buf, len) != len)
{
perror("write to stdout");
break;
}
}
}
首先,创建了一个管道,并检查是否创建成功。
接下来进入一个循环,每次循环中从标准输入(stdin)读取数据到缓冲区buf中。
获取读取到的数据的长度len,然后将数据写入到管道的写入端fds[1]中,即将数据发送到管道中。
清空缓冲区buf。
从管道的读取端fds[0]中读取数据,读取的数据长度存储在len变量中。
将读取到的数据写入到标准输出(stdout)中,即将数据打印到控制台。
回到循环开始的地方,继续读取输入并进行管道通信,直到读取到的数据为空(即输入结束)。
2.2.5 从文件描述符的角度看待管道
当父进程创建了管道后,就是在其的PCB中的文件描述符数组增加了两个文件描述符,即管道的写端和读端。当父进程创建一个子进程后,子进程继承了父进程的文件描述符数组。这样就意味着在子进程也打开了相同的管道。这样就可以天然的支撑,父子进程进行通信了。
2.2.6 从内核的角度看待管道
如果从内核的角度看待管道的话,可以发现管道就是两个文件描述符。即内核将管道看作两个文件,一个文件只能写,一个文件只能读。这和Linux系统中一切皆文件的思想不谋而合。
2.2.7 管道为什么不会出现写时拷贝的问题
匿名管道不会发生写时拷贝的问题,因为它是基于内核缓冲区的进程间通信机制。
在匿名管道中,数据传递是通过内核缓冲区进行的。当一个进程向管道写入数据时,数据首先被复制到内核缓冲区,然后从内核缓冲区传递给读取数据的进程。读取进程从内核缓冲区读取数据,而不是从写入进程的用户空间缓冲区读取数据。
因此,在匿名管道中,数据传递是通过内核进行的,而不涉及用户空间的数据拷贝。这就避免了写时拷贝的问题,因为写入进程和读取进程直接共享了同一个内核缓冲区,而不是创建副本。
这种设计使得匿名管道在进程间通信时效率高,并且不会产生额外的内存开销。但需要注意的是,匿名管道只能用于具有父子关系的进程或者具有共同祖先的进程之间通信,因为它们共享同一个文件描述符表。
2.2.8管道读写规则
当没有数据可读时:
如果读端的文件描述符设置为阻塞模式(O_NONBLOCK未设置),read调用会阻塞,直到有数据到来。
如果读端的文件描述符设置为非阻塞模式(O_NONBLOCK已设置),read调用会立即返回-1,且errno被设置为EAGAIN。
当管道已满时:
如果写端的文件描述符设置为阻塞模式,write调用会阻塞,直到管道有足够空间可写入数据。
如果写端的文件描述符设置为非阻塞模式,write调用会立即返回-1,且errno被设置为EAGAIN。
如果所有管道写端对应的文件描述符被关闭,read调用会返回0,表示读到达了文件结束。
如果所有管道读端对应的文件描述符被关闭,write操作会产生SIGPIPE信号,默认行为是终止进程。
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
2.3 命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
2.3.1 创建一个命名管道
a.命名管道可以从命令行上创建,命令行方法如下:
$ mkfifo filename
b.也可以从程序中创建,相关函数为:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
mkfifo 函数用于创建一个命名管道(FIFO)。它接受两个参数:pathname 是指定管道路径的字符串,mode 是管道的权限模式。
2.3.2 匿名管道和命名管道的区别
创建方式:匿名管道通过 pipe 系统调用创建,而命名管道通过 mkfifo 函数创建。
命名:匿名管道没有名字,只是在内核中创建一个管道对象,无法被其他进程打开;而命名管道具有唯一的路径名,可以被多个进程通过路径名打开和使用。
持久性:匿名管道只存在于创建它的进程生命周期内,一旦创建进程终止,匿名管道也会被自动销毁;而命名管道是持久的,可以一直存在于文件系统中,可以在不同的时间点由不同的进程打开和关闭。
通信方式:匿名管道通常用于具有父子关系的进程之间的通信,因为它们可以直接在创建子进程时传递给子进程;而命名管道适用于无关进程之间的通信,它们可以通过文件路径名进行通信。
缓冲区大小:匿名管道的缓冲区大小是有限的,一般较小,取决于操作系统的实现;而命名管道的缓冲区大小可以通过系统配置进行调整。
2.3.3 命名管道的打开规则
打开命名管道使用 open 函数,需要指定管道的路径名和打开模式。
如果管道不存在,且以读模式打开管道,则打开操作会阻塞,直到有其他进程以写模式打开同一路径名的命名管道。
如果管道不存在,且以写模式打开管道,则打开操作会阻塞,直到有其他进程以读模式打开同一路径名的命名管道。
如果以读模式打开命名管道,并且已经有其他进程以写模式打开同一路径名的命名管道,则打开操作会立即成功。
如果以写模式打开命名管道,并且已经有其他进程以读模式打开同一路径名的命名管道,则打开操作会立即成功。
2.3.4 使用命名管道实现server & client 通信
commn.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define MODE 0644
#define BUFSIZE 1024
string ipcpath = "./fifo.ipc";
#endif
client.cc
#include "commn.hpp"
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:" << endl;
std::getline(std::cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
// 3.关闭
close(fd);
return 0;
}
server.cc
#include "commn.hpp"
int main()
{
// 1.创建管道文件
if (mkfifo(ipcpath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
// 2.正常的文件操作
int fd = open(ipcpath.c_str(), O_RDONLY);
if (fd < 0)
{
perror("open");
exit(2);
}
// 3.编写正常的通信代码
char buffer[BUFSIZE];
while (true)
{
memset(buffer, '\0', BUFSIZE);
ssize_t s = read(fd, buffer, BUFSIZE - 1);
if (s > 0)
{
cout << "client say:" << buffer << endl;
}
else if (s == 0)
{
// end of file
cerr << "read end of file, client quit, server quit too!" << endl;
break;
}
else
{
// read error
perror("read");
break;
}
}
// 4.关闭文件
close(fd);
unlink(ipcpath.c_str()); // 通信完毕,就删除管道。
return 0;
}
Makefile
.PHONY: all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
3.共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到
内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
在使用共享内存时,首先需要通过系统调用(如:shmget)创建一个共享内存段。创建共享内存段时需要指定大小(以字节为单位)和权限等参数。成功创建后,会返回一个唯一的标识符(称为共享内存标识符)用于后续的操作。
然后,进程可以通过系统调用(如shmget)将共享内存段连接到自己的地址空间中,使得进程可以之直接访问这块共享内存区域。连接共享内存时需要提供共享内存标识符和一些选项,如访问权限和连接位置等。
一旦共享内存段连接到进程的地址空间,进程就可以像访问普通内存一样,直接读写共享内存区域的数据。多个进程可以同时连接同一块共享内存,它们可以通过在共享内存中写入和读取数据来进行进程间的通信和数据共享。
使用共享内存的优势在于它可以提供高性能和低延迟的数据交换,因为进程可以直接访问共享内存,而无需进行复制或数据传输。但同时也需要谨慎处理同步和互斥问题,以避免数据不一致或竞争条件的发生。
最后当不再需要共享内存的时候,进程可以通过系统调用如(shmdt)将共享内存段从自己的地址空间中分离,使其不在可访问。如果所有进程都分离共享内存段,可以使用系统调用(如shmctl)删除共享内存段,释放资源。
3.1 共享内存的函数
3.1.1 shmget函数
shmget函数是Linux系统提供的一个系统调用,用于创建或获取一个共享内存段。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:共享内存标识符,通常使用ftok函数生成,用于标识共享内存段。不同的key对应不同的共享内存段。
size:共享内存段的大小,以字节为单位。
shmflg:共享内存的访问权限和创建标志位,可以通过位运算符 | 组合多个标志。
常见的标志包括:
IPC_CREAT:如果共享内存不存在,则创建一个新的共享内存段。
IPC_EXCL:与IPC_CREAT一同使用,用于确保只创建一个新的共享内存段。
IPC_PRIVATE:创建一个私有的共享内存段,只允许创建该进程自己访问。
其返回值为:
成功时,返回共享内存标识符(非负整数)。
失败时,返回-1,并设置errno错误码。
ftok函数用于生成一个唯一的key值,通常用于创建和访问system V共享内存、消息队列和信号量等ipc机制。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:一个存在的文件路径名,通常选择一个已存在的文件。可以是绝对路径或相对路径。
proj_id:一个非负整数,作为标识符,用于区分不同的IPC对象。不同的proj_id对应不同的key值。
其返回值:
成功时,返回一个唯一的key值(非负整数)。
失败时,返回-1,并设置errno错误码。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/path/to/file", 'A'); // 生成key值
int shm_id = shmget(key, 1024, IPC_CREAT | 0666); // 创建共享内存段
if (shm_id == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
printf("Shared memory segment created with ID: %d\n", shm_id);
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/home/mh", 'A'); // 生成key值
int shm_id = shmget(key, 1024, IPC_CREAT | 0666); // 创建共享内存段
if (shm_id == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
printf("Shared memory segment created with ID: %d\n", shm_id);
return 0;
}
3.1.2shmat函数:
shmat函数用于将共享内存连接到进程的地址空间,使得进程可以访问共享内存中的数据。
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存的标识符,即由shmget函数返回的共享内存ID。
shmaddr:指定共享内存连接到进程地址空间的地址。通常将其设置为NULL,表示由系统自动选择合适的地址。
shmflg:指定连接共享内存的标志,常用的标志有IPC_CREAT(若共享内存不存在则创建)和SHM_RDONLY(以只读方式连接共享内存)等。
返回值:
连接成功时,返回指向共享内存段的指针,即共享内存连接到进程的地址。
连接失败时,返回-1,并设置errno错误码。
3.1.3 shmdt函数
shmdt函数用于断开进程与共享内存之间的连接。
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
shmaddr:指向共享内存段的指针,即共享内存连接到进程的地址。
返回值:
成功断开连接时,返回0。
失败时,返回-1,并设置errno错误码。
3.1.4 shmctl函数
shmctl函数用于删除共享内存。只有当连接到共享内存的进程数为0时,才应该使用这个函数。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存标识符,通过shmget函数获取。
cmd:控制操作命令,可以是以下值之一:
IPC_STAT:获取共享内存的状态信息,将结果存储在buf中。
IPC_SET:设置共享内存的状态信息,根据buf的值进行设置。
IPC_RMID:删除共享内存段,释放资源。
buf:指向shmid_ds结构体的指针,用于存储共享内存的状态信息。可以为NULL。
返回值:
执行成功时,返回0。
执行失败时,返回-1,并设置errno错误码。
3.2 使用共享内存进行简单的进程间通信
Makefile
.PHONY:all
all:server client
client:client.cc commn.cc
g++ -o $@ $^ -std=c++11
server:server.cc commn.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
commn.hpp
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#define PATHNAME "/home/mh/test/shm"
#define PROJ_ID 0x6666
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#endif
commn.cc
在这里插入代码片#include "commn.hpp"
static int commShm(int size, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0)
{
perror("ftok");
exit(1);
}
int shmid = 0;
if ((shmid = shmget(key, size, flags)) < 0)
{
perror("shmget");
exit(2);
}
return shmid;
}
int destroyShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, NULL) < 0)
{
perror("shmctl");
exit(3);
}
return 0;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(int size)
{
return commShm(size, IPC_CREAT);
}
client.cc
#include "commn.hpp"
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = (char *)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while (i < 26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
server.cc
#include "commn.hpp"
int main()
{
int shmid = createShm(4096);
char *addr = (char *)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while (i++ < 26)
{
printf("client: %s\n", addr);
sleep(1);
}
shmdt(addr);
sleep(2);
destroyShm(shmid);
return 0;
}
上述代码编译之后,应当首先运行server以创建共享内存,否则可能出现问题。上述代码中生成key的路径可能与你本地并不相同,可能需要更改为你本地的路径。
以上是运行成功后的示例。