进程间的通信

每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但是内核空间是每个进程共享的,所以进程之间要通信必须通过内核。进程间的通信方式主要为管道、消息队列、共享内存、信号量、信号、socket。主要理解各种方式的优缺点和适用场景。

1、管道

优点:实现简单
缺点:不适合进程间频繁的通信
底层原理:
例子
ps auxf | grep mysql
上述命令行中的|则是管道,只能单向通信,用完就销毁了,也称匿名管道。
匿名管道的创建需要通过下面的系统调用:
int pipe(int fd[2]);//linux下C语言
返回两个文件描述符,fd[0]读入端描述符,fd[1]写入端描述符。匿名管道是特殊的文件,只存在于内存中,不存在于文件系统中。
匿名管道使用场景:
使用fork创建子进程,创建的子进程会复制父进程的文件描述符,这样两个进程就各有fd[0],fd[1]文件描述符。一般情况下,每个进程只能读或者写,如果想实现双向通信则需要创建两个匿名管道。
C++代码:

/* * * 父进程 * * */
cout << "Parent Process" << endl;
HANDLE hPipeW = NULL, hPipeR = NULL;//读管道句柄和写管道句柄
//安全性结构
SECURITY_ATTRIBUTES sa{ 0 };
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;//填充安全性结构使句柄被继承 
// 创建匿名管道
if (!CreatePipe(&hPipeR, &hPipeW, &sa, 0))
{
	cout << "Create pipe failed! Processs return!" << endl;
	return 1;
}
// 进程信息结构体
PROCESS_INFORMATION pi{ 0 };
// 进程启动窗体信息结构体
STARTUPINFOA si{ 0 };
si.cb = sizeof(si);
si.hStdInput = hPipeR;
si.dwFlags = STARTF_USESTDHANDLES;
// 为子进程命令行添加参数
char param[1024];
sprintf_s(param, "%s %s", argv[0], "test"); 
// 创建子进程
if (!CreateProcessA(nullptr, param, nullptr, nullptr, TRUE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi))
{
CloseHandle(hPipeR);
CloseHandle(hPipeW);
cout << "Create child process failed! Process return!" << endl;
return 2;
}
// 写管道数据
WriteFile(hPipeW, "ezhchai", 7, nullptr, nullptr);

/* * * 子进程 * * */
cout << "Child Process" << endl;
/* * 接收管道数据,并显示 * */
CHAR szBuffer[16]{ 0 };
ReadFile(GetStdHandle(STD_INPUT_HANDLE), szBuffer, sizeof(szBuffer), nullptr, nullptr);
cout << szBuffer << endl;

在这里插入图片描述
可以发现匿名管道适用于父子进程间的通信,而不能使用在无关进程中。
命名管道则可以克服这个缺点,命名管道在操作系统中是设备文件符,可以被不同的进程使用。
命名管道C++代码:

HANDLE h_pipe;
char buf_msg[BUF_SIZE];
DWORD num_rcv; //实际接收到的字节数
//创建命名管道,命名为MyPipe,消息只能从客户端流向服务器,读写数据采用阻塞模式,字节流形式,超时值置为0表示采用默认的50毫秒
h_pipe = ::CreateNamedPipe("\\\\.\\pipe\\MyPipe", PIPE_ACCESS_INBOUND, PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, BUF_SIZE, BUF_SIZE, 0, nullptr);
if (h_pipe == INVALID_HANDLE_VALUE)
{
	cerr << "Failed to create named pipe!Error code: " << ::GetLastError() << "\n";
	system("pause");
	return 1;
}
else
{
	cout << "Named pipe created successfully...\n";
}
//等待命名管道客户端连接
if (::ConnectNamedPipe(h_pipe, nullptr))
{
	cout << "A client connected...\n";
	memset(buf_msg, 0, BUF_SIZE);
//读取数据
if (::ReadFile(h_pipe, buf_msg, BUF_SIZE, &num_rcv, nullptr))
{
	cout << "Message received: " << buf_msg << "\n";
}
else
{
	cerr << "Failed to receive message!Error code: " << ::GetLastError() << "\n";
	::CloseHandle(h_pipe);
	::system("pause");
	return 1;
}
}
::CloseHandle(h_pipe);
::system("pause");
return 0;
HANDLE h_pipe;
char buf_msg[] = "Test for named pipe...";
DWORD num_rcv; //实际接收到的字节数
cout << "Try to connect named pipe...\n";
//连接命名管道
if (::WaitNamedPipe("\\\\.\\pipe\\MyPipe", NMPWAIT_WAIT_FOREVER))
{
//打开指定命名管道
h_pipe = ::CreateFile("\\\\.\\pipe\\MyPipe", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h_pipe == INVALID_HANDLE_VALUE)
{cerr << "Failed to open the appointed named pipe!Error code: " << ::GetLastError() << "\n";
::system("pause");
return 1;
}
else
{
if (::WriteFile(h_pipe, buf_msg, BUF_SIZE, &num_rcv, nullptr))
{
cout << "Message sent successfully...\n";
}
else
{
cerr << "Failed to send message!Error code: " << ::GetLastError() << "\n";
::CloseHandle(h_pipe);
::system("pause");
return 1;
}
}
::CloseHandle(h_pipe);
}
::system("pause");
return 0;

2、消息队列

特点:消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,编程时可以按消息的类型读取。
与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除。
每个消息队列都有消息队列标识符,消息队列的标志符在整个系统中是唯一的。
消息队列是消息的链表,存放在内存中,由内核维护。只有内核重启或者人工删除程序队列时,该消息队列才会被删除。若不人工删除消息队列,消息队列会一直存在于系统中。

#include <sys/msg.h>
#include <sys/types.h>
#include <sys/ipc.h>
 
key_t ftok(const char *pathname, int proj_id);//返回目标地址的(”.”当前文件夹)key值,
参数:pathname消息队列文件地址,proj_id项目编号
int msgget(key_t key, int msgflg);//创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的 key值就能得到同一个消息队列的标识符。
参数:key值,msgflg: 标识函数的行为及消息队列的权限,其取值如下:IPC_CREAT:创建消息队列。IPC_EXCL: 检测消息队列是否存在。返回消息队列标志符,失败则是-1int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
//功能:从消息队列中接受一个消息,一旦接受消息成功,则消息在消息队列中被删除。
参数:Msqid消息队列标识符,msg_ptr存放消息机构提的地址,msg_zs消息正文的字节数,msgtype消息的类型msgtyp = 0:返回队列中的第一个消息。msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)。msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。注意:在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进先出原则。
msgflg:函数的控制属性。其取值如下:0msgrcv() 调用阻塞直到接收消息成功为止。MSG_NOERROR: 若返回的消息字节数比 nbytes 字节数多,则消息就会截短到 nbytes 字节,且不通知消息发送进程。IPC_NOWAIT: 调用进程会立即返回。若没有收到消息则立即返回 -1。调用成功则是消息的长度,失败则是-1.

int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);//将新消息加入到消息队列中。
参数:msqid是消息队列的标志符,msgp待发送消息结构体的地址,msgsz消息正文的字节数,msgflg函数的控制属性,0msgsnd() 调用阻塞直到条件满足为止。IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回。

int msgctl(int msqid, int cmd, struct msqid_ds *buf);//修改消息队列属性或者删除消息队列的消息。
参数:msqid消息队列的标志符,cmd:函数功能的控制。其取值如下:IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关数据结构。IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指向的结构中。相对于,把消息队列的属性备份到 buf 里。IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中的对应值。相当于,消息队列原来的属性值清空,再由 buf 来替换。buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。
返回值:成功:0,失败:-1

消息结构体

typedef struct _msg
{
	long mtype;		 // 消息类型
	char mtext[100]; // 消息正文
	//…… ……          // 消息的正文可以有多个成员
}MSG;

3、共享内存

消息队列中消息的读取和写入的过程,都会发生用户态和内核态之间的消息拷贝过程,会造成开销大的缺点。而共享内存则是多个进程共同映射到一块物理内存上读写。
优点:读取数据效率高
缺点:没有相应的同步机制,需要通过外部的信号量来控制同步。
使用共享内存三步走:
step1:创建物理内存,shmget()
step2:将进程的虚拟地址链接上物理内存,shmat
step3:使用完释放内存,shmdt()
然后使用struct作为一个变量写入,注意只有在共享内存可写的时候写入。
代码:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//创建共享内存—>shmget()函数
int shmget(key_t key, size_t size, int shmflg);//成功则返回共享内存ID,出错返回-1
参数:key通过ftok()函数得到;size为共享内存的大小,为页数的整数倍
Shmflg是一组标志,创建一个新的共享内存,IPC_CREAT标志表示共享内存存在则打开,IPC_CREAT|IPC_EXCL存在则打开,不存在则创建一个新的共享内存。

操作共享内存—>shmctl()函数
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);//成功返回,出错返回-1
参数:shm_id是shmget函数返回的共享内存标志符。
cmd表示要采取的操作,他可以取下面三个值:(IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds给出的值。IPC_RMID:删除共享内存段。)
*buf 是一个结构体指针,它指向共享内存模式和访问权限的结构。shmid_ds结构至少包括以下成员
struct shmid_ds
{
uid_t shm_perm.uid; 
uid_t shm_perm.gid; 
mode_t shm_perm.mode; 
};

挂接操作---> shmat()函数
创建共享存储段之后,将进程连接带它的地址空间
void *shmat(int shm_id, const void *shm_addr, int shmflg);//成功返回指向共享内存段的指针,出错返回-1
参数:shm_id是由shmget函数返回的共享内存标志。
shm_addr指定共享内存连接到当前进程中的地址位置,通常为0,表示让系统来选择共享内存的地址。
shm_flg是一组标志位,通常为0;

分离操作--->该操作不从系统中删除标志符合其数据结构,要显示调用shmctl(带命令IPC_RMID)才能删除它。
int shmdt(const void *shmaddr);//成功返回,出错返回-1
参数:shmaddr参数是之前调用shmat时返回的值。

4、信号量

信号量是在posix中创建的,具有协助共享内存同步和异步的功能。其中资源数量设置为1,是互斥信号量用来实现同步的功能。

#include <semaphore.h>
//初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);//成功返回0,错误-1
参数:sem是定位在sem的匿名信号量,sem_t binSem;
Pshared参数指明信号量是由进程内线程共享,还是由进程之间共享,如果pshared的值为0,那么信号量将被进程内的线程共享,并且应该放置在这个进程的所有线程都可见的地址上(如全局变量,或者堆上动态分配的变量)。如果pshared是非0值,那么消耗量将在进程间共享,并且应该定位共享区域(shm_open(2)mmap(2)shmget(2)))。

int sem_destroy(sem_t *sem);//销毁
int sem_wait(sem_t *sem);//资源减1
int sem_post(sem_t *sem);//资源加1
int sem_trywait(sem_t *sem);
int sem_getvalue(sem_t *sem);

5、信号

信号与信号量是完全不同的两个概念:可以使用 kill -l来查看所有的信号。信号是进程间通信的唯一异步机制,可以在任何时候给进程发送信号,传达命令。可以用硬件比如CTRL+C或者软件kill -9 PID强制停止进程。
功能:在异常情况下通知进程的相关信息。
进程的处理方式:
执行默认操作,比如停止进程。
捕捉信号,可以为信号定义一个信号处理函数,当信号发生时,就执行相应的信号处理函数。
忽略信号,当我们不希望处理某些信号的时候,就可以忽略该信号,不做处理。只有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SIGSTOP,它们用于任何时候中断或结束某一进程。

6、SOCKET

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
Socket也可以在同一台主机上的不同进程间进行交换,下面是socket的系统调用

int socket(int domain, int type, int protocal);

参数:domain参数用来指定协议簇,比如AF_INET用于IPV4、AF_INET6用于ipv6、AF_LOCAL/AF_UNIX用于本机;
Type参数用来指定通信特性,比如SOCK_STREAM表示的是字节流,对应TCP、SOCK_DGRAM表述的是数据报,对应UDP、SOCK_RAM表示的是原始套接字;
protocal参数原本是用来指定通信协议的,但现在基本废弃,一般写作0即可。
三种socket类型:
实现TCP字节流通信:socket类型是AF_INET和SOCK_STREAM;

在这里插入图片描述
服务端和客户端初始化socket,得到文件描述符,int sockfd = socket (AF_INET, SOCK_STREAM, 0);
Ps:写入ip地址和端口,
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
服务端调用bind,将绑定在ip地址和端口;
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
服务端调用listen,进行监听;
listen(listenfd, 10);
服务端调用accept,等待客户端连接;
accept(listenfd, (struct sockaddr*)NULL, NULL));
服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
客户端调用connect,向服务器端的地址和端口发起连接请求;
PS:
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
把ip地址转化为用于网络传输的二进制数值
//int inet_aton(const char *cp, struct in_addr *inp);
将网络传输的二进制数值转化为成点分十进制的ip地址
//char *inet_ntoa(struct in_addr in);
int inet_pton(int family, const char *strptr, void *addrptr);//将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
//const char * inet_ntop(int family, const void *addrptr, char strptr, size_t len);//将数值格式转化为点分十进制的ip地址格式。返回值:若成功则为指向结构的指针,若出错则为NULL
connect(sockfd, (struct sockaddr
)&servaddr, sizeof(servaddr));

服务端accept返回用于传输socket的文件描述符;
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
客户端调用send写入数据;
send(sockfd, sendline, strlen(sendline), 0)
服务端调用recv读取数据;
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = ‘\0’;
printf(“recv msg from client: %s\n”, buff);
客户端断开连接时,会调用close,那么服务端read读取数据的时候,就会读取到了EOF,待处理完数据后,服务端调用close,表示连接关闭。
close(connfd);
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

实现UDP数据报通信:socket类型是AF_INET和SOCK_DGRAM;
在这里插入图片描述
UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。
对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。
另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。

实现本地进程间的通信:本地字节流socket类型是AF_LOCAL和SOCK_STREAM,本地数据报socket类型是AF_LOCAL和SOCK_DGRAM。另外AF_UNIX和AF_LOCAL是等价的。
本地socket被用于在同一台主机上进程间通信的场景:
本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
本地 socket 的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现;
对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和SOCK_STREAM。
对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。
本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐