[Linux]进程间通信
文章目录
- [Linux]进程间通信
- 进程间通信
- 什么是进程间通信
- 进程间通信的目的
- 进程间通信的本质
- 为什么存在进程间通信
- 进程间通信的分类
- 管道
- 什么是管道
- 匿名管道
- 本质
- pipe
- pipe的使用
- 匿名管道读写情况
- 匿名管道的特征
- 命名管道
- 本质
- 命令行创建命名管道
- 创建和删除命名管道
- 实现服务端与客户端通信
- System V
- system V共享内存
- 共享内存的原理
- IPC资源的查看
- 共享内存的创建和释放
- 共享内存的关联和去关联
- 实现服务端与客户端通信
进程间通信
什么是进程间通信
进程之间具有独立性,如果需要进行通信,就必须打破进程间的独立性。进程通信需要提供一块公共的能够进行信息存储和取出的空间。文件系统提供的我们称为管道,操作系统提供的System V。
进程间通信的目的
- 数据传输:一个进程将数据发送给另一个进程。
- 资源共享:多个进程共享相同的资源。
- 事件通知:一个进程需要向另一个或一组进程发送消息。通知它们发生了某种事件,例如子进程终止时需要通知父进程。
- 进程控制:有的进程希望完全控制另一个进程的执行,例如Debug进程。
进程间通信的本质
进程间通信的本质就是让不同的进程看到同一份资源。
实际上就是构建一个公共区域,供不同进程进行写入或读取数据。
为什么存在进程间通信
实际情况中,有时候我们需要多进程协作完成某种业务。
进程间通信的分类
- 管道
- 匿名管道
- 命名管道
- System V
- System V消息队列
- System V信号量
- System V共享内存
- POSIX
- 共享内存
- 信号量
- 消息队列
- 互斥量
- 条件变量
- 读写锁
管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
其实我们早在之前命令中的学习就已经见过了管道|
,我们也经常使用管道命令。
例如,查看服务器连接人数:
[---@VM-8-4-centos day04]$ who | wc -l
匿名管道
本质
匿名管道主要用于父子间的通信。它本质上就是让父子进程看到同一个被打开的文件,然后让父子进程进行写入或读取数据,从而实现父子间的通信。
这里的文件是由操作系统提供的,所以在父进程或子进程写入数据时,并不会发生写时拷贝。
pipe
int pipe(int pipefd[2]);
- 头文件:
#include<unistd.h>
- pidfd是一个输出型参数,pipfd[0]:管道读端的文件描述符;pipfd[1]:管道写端的文件描述符。
- 返回值:调用成功,返回0;调用失败,返回-1。
pipe的使用
实际上,我们需要对一对父子进程关闭相反的两个端口来使用匿名管道进行通信。
- 需要父进程读,则关闭父进程的写端和关闭子进程的读端。
- 需要父进程写,则关闭父进程的读端和关闭子进程的写端。
例如,父进程关闭写端,子进程关闭读端。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fd[2] = {0};
int n = pipe(fd);
if (n != 0)
perror("pipe fail");
pid_t id = fork();
int cnt = 0;
const char *str = "I am a child process. MYPID->";
if (id > 0)
{
close(fd[1]); // 父进程关闭写端
while (1)
{
char buffer[1024];
ssize_t r = read(fd[0], buffer, sizeof(buffer) - 1);
if (r > 0)
{
buffer[r] = '\0';
cout << "parent process get message-> " << buffer << endl;
}
else if (r == 0)
{
// 读完了
cout << endl;
cout << "数据读取完毕!!!" << endl;
break;
}
else
{
perror("read fail");
return -1;
}
}
int wp = waitpid(-1, nullptr, 0);
close(fd[0]);
return 0;
}
else if (id == 0)
{
close(fd[0]); // 子进程关闭读端
while (1)
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "Message: %s%d, count: %d", str, getpid(), cnt++);
write(fd[1], buffer, strlen(buffer));
sleep(1); // 每隔1秒写入一次
if (cnt == 6)
break;
}
close(fd[1]);
exit(0);
}
else
{
perror("fork fail");
return -1;
}
return 0;
}
匿名管道读写情况
- 如果管道中没有数据,读端进行读取,就会阻塞当前读取的进程
- 如果写端写满了,写端还进行写入,就会阻塞当前写端的进程
- 如果写端关闭了,读端读完数据后就会返回0,正常退出
- 如果读端关闭了,操作系统会向写端发送13号信号
SIGPIPE
,从而让写端关闭
匿名管道的特征
- 匿名管道是半双工通信的。
- 单工通信:数据传输在通信双方是单向的,一方为固定发送端,另一方为固定接收端。
- 半双工通信:数据传输在通信双方是双向的,但不能同时发送数据。
- 全双工通信:数据传输在通信双方是双向的,允许同时发送数据。
- 管道的生命周期随进程,进程退出,则管道释放。
- 管道的本质是通过一个文件进行通信的,当打开这个文件的进程退出后,这个文件也会被释放掉。
- 管道提供的是流式服务。
- 对于写端写入的数据,读端读取的数据是任意的,这就是流式服务。
- 对于写端写入的数据,读端读取时根据数据的分割(按一定的报文段)读取,这就是与流式服务相对的数据报服务。
- 内核对管道操作会进行同步和互斥。
- 同步:在特定的时间点或条件下,不同进程之间的操作按照一定的顺序和速度进行,以保证它们之间的状态和行为达到预期的一致性。
- 互斥:一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
对于管道来说,同步就是指这两个进程不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作;互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作。
ps:管道的最大容量一般是65536字。
我们也可以通过以下命令查看:
[---@VM-8-4-centos day04]$ ulimit -a
命名管道
匿名管道通常只能用于父子进程间的通信,为了让不具有亲缘关系的进程相互通信,由此有了命名管道。
本质
通过创建一个特殊的文件,让两个进程看到同一份资源,从而实现通信。
匿名管道和命名管道都是内存文件,但是命名管道在磁盘上有一个特殊的映像。(大小为0,因为命名管道和匿名管道都不会刷新到磁盘上)
命令行创建命名管道
[---@VM-8-4-centos day04]$ mkfifo named_pipe
我们可以从第一个p看到出,这个文件类型是管道文件,管道文件大小默认是0。
此时我们已经可以进行通信了,我们使用两个不同的命令行进行通信测试:
如果我们不进行读取,写端就会阻塞等待读端读取。
创建和删除命名管道
int mkfifo(const char *pathname, mode_t mode);
-
pathname:命名管道创建路径,若给出文件名,则创建在当前路径下;若给出路径,按路径创建
-
mode:管道文件默认权限
-
返回值:创建成功,返回0;创建失败,返回-1。
int main()
{
umask(0);
int n = mkfifo("named_pipe", 0666);
if(n < 0) perror("mkfifo fail");
//创建成功
cout << "mkfifo success..." << endl;
return 0;
}
int unlink(const char *path)
- path:管道路径
- 返回值,创建成功,返回0;创建失败,返回-1
int main()
{
int n = unlink("./named_pipe");
if(n == -1)
{
perror("unlink fail");
return -1;
}
cout << "unlink success ......." << endl;
return 0;
}
实现服务端与客户端通信
com.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
using namespace std;
bool create_named_pipe()
{
umask(0);
int n = mkfifo("./named_pipe", 0600); // 创建管道文件
if (n != 0)
{
perror("mkfifo fail");
return false;
}
cout << "mkfifo success ......" << endl;
return true;
}
void unlink_named_pipe()
{
int n = unlink("./named_pipe");
if (n != 0)
perror("unlink fail");
cout << "unlink success ......" << endl;
}
serve.cc
#include "com.hpp"
int main()
{
bool flag = create_named_pipe();
if (flag == false)
{
perror("create_named_pipe fail");
exit(-1);
}
int fd = open("./named_pipe", O_RDONLY);
if (fd < 0)
perror("open fail");
char buffer[1024];
while (1)
{
ssize_t r = read(fd, buffer, sizeof(buffer) - 1);
if (r > 0)
{
buffer[r] = '\0';
cout << "serve get message -> " << buffer << endl;
}
else if (r == 0)
{
cout << "read end ......" << endl;
break;
}
else
{
perror("read fail");
return -1;
}
}
close(fd);
unlink_named_pipe();
return 0;
}
client.cc
#include "com.hpp"
int main()
{
int fd = open("./named_pipe", O_WRONLY);
if (fd < 0)
perror("open fail");
char buffer[1024];
while(1)
{
cout << "client Enter # ";
fgets(buffer, sizeof(buffer), stdin);//fgets剩一个空间会被系统填充'\0',不用-1
ssize_t w = write(fd, buffer, strlen(buffer));
if(w != strlen(buffer))
{
perror("write fail");
exit(-1);
}
}
close(fd);
return 0;
}
另外一提,命令行中的管道|
是匿名管道。
System V
之前我们提到过System V通信方式有:System V共享内存、System V消息队列、System V信号量,下面我们就着重说说System V共享内存。
system V共享内存
共享内存的原理
用户使用操作系统提供的接口在物理内存中申请一块资源,通过页表将这段物理空间映射至进程地址空间,进程将这段虚拟地址的起始地址返回给用户。
操作系统中的进程都可以通过共享内存进行通信,一个操作系统可以有多个共享内存。
共享内存不止一个,操作系统必然对这些共享内存也要进行管理,系统也为他维护了一个数据结构:
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 */
};
第一个成员shm_perm
的类型ipc_perm结构是这样的。(key也存在shm_perm中)
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
IPC资源的查看
我们使用以下命名,可以查看系统中的IPC资源:
[---@VM-8-4-centos day05]$ ipcs
ipcs
这个命令还有3个选项,分别来查看共享内存、消息队列和信号量。
-q
:仅显示消息队列的信息-m
:仅显示共享内存的信息-a
:仅显示信号量的信息
共享内存的创建和释放
key_t ftok(const char *pathname, int proj_id);
- 作用:将一个存在且可获取的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,方便共享内存创建唯一标识。
- 返回值:创建成功,返回key值;创建失败,返回-1。
int shmget(key_t key, size_t size, int shmflg);
- key:形成唯一标识,保证进程看到的是同一块共享内存。(使用
ftok
获取) - size:创建共享内存的大小。
- shmflg:共享内存的创建方式。IPC_CREAT:共享内存不存在,则创建,如果存在则获取;IPC_EXCL:无法单独使用,IPC_CREAT|IPC_EXCL:如果不存在就创建,如果存在就出错返回。(记得设置权限0666等,例如
IPC_CREAT | IPC_EXCL | 0666
) - 返回值:创建成功,返回共享内存标识符;创建失败,返回-1。
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
using namespace std;
int main()
{
key_t key = ftok("./makefile", 0x6666);
if(key < 0)
{
perror("ftok fail");
return -1;
}
int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL);
if(shm < 0)
{
perror("shmget fail");
return -2;
}
cout << key << endl << shm << endl;
return 0;
}
我们使用ipcs
命令来看一下,我们是否创建成功:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:表示所控制共享内存的用户级标识符。
- cmd:表示具体的控制动作。IPC_RMID最为常用,表示删除共享内存。
- buf:用于获取或设置所控制共享内存的数据结构。一般设置为nullptr
- 返回值:调用成功,返回0;调用失败。返回-1。
int main()
{
key_t key = ftok("./makefile", 0x6666);
if (key < 0)
{
perror("ftok fail");
return -1;
}
int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL);
if (shm < 0)
{
perror("shmget fail");
return -2;
}
cout << key << endl << shm << endl;
cout << "create success..." << endl;
sleep(3);
int ctl = shmctl(shm, IPC_RMID, nullptr);
if(ctl < 0)
{
perror("shmctl fail");
return -3;
}
cout << "delete success..." <<endl;
return 0;
}
我们创建一块共享内存,让程序休眠3秒。休眠后,删除这块共享内存。(我们可以看见其中有3行打印了这块共享内存,第4行就没有了,这也证明了我们代码的逻辑是正确的)
右边命令行使用以下的监控脚本:
[wsj@VM-8-4-centos day05]$ while :; do ipcs -m;echo "###################################";sleep 1;done
我们也可以使用命令行来删除共享内存:
[wsj@VM-8-4-centos day05]$ ipcrm -m 6(数字代表自己共享内存的shm)
共享内存的关联和去关联
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:待关联的共享内存标识符
- shmaddr:指定共享内存映射到进程地址空间中的某一地址,一般设置
nullptr
让内核自己选择。 - shmflg:SHM_RDONLY,表示关联共享内存后,仅进行读取操作;SHM_RND,表示如果shmaddr不为空,则自动向下调整为SHMLBA的整数倍;0,默认为读写权限。
- 返回值:调用成功,返回映射到进程地址空间的共享内存的地址;调用失败,返回(void*)-1。
int shmdt(const void *shmaddr);
-
shmaddr:待去关联的共享内存,使用shmat得到的地址。
-
返回值,调用成功,返回0;调用失败,返回-1。
接下来,我们使用一段代码加深一下对这些接口调用的理解:
int main()
{
key_t key = ftok("./makefile", 0x6666);
if (key < 0)
{
perror("ftok fail");
return -1;
}
int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);
if (shm < 0)
{
perror("shmget fail");
return -2;
}
cout << "create success..." << endl;
cout << "-----------------------" << endl;
void* mem = shmat(shm, nullptr, 0);
if(mem == (void*)-1)
{
perror("shmat fail");
return -3;
}
cout << "attach success..." << endl;
cout << "-----------------------" << endl;
int dt = shmdt(mem);
if(dt < 0)
{
perror("shmdt fail");
return -4;
}
cout << "detach success ..." << endl;
cout << "-----------------------" << endl;
int ctl = shmctl(shm, IPC_RMID, nullptr);
if(ctl < 0)
{
perror("shmctl fail");
return -5;
}
cout << "delete success..." << endl;
return 0;
}
右边的命令行,任然使用上面的监控脚本。
我们从共享内存,从无到有;从关联数,从0到1,再到0。证明我们代码的逻辑是正确的。(创建共享内存->关联该共享内存->去关联该共享内存->删除共享内存)
ps:创建共享内存时需要设置权限,不然就无法正常关联。
实现服务端与客户端通信
com.hpp
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
#define PATH "./makefile"
#define PROJ_ID 0X888
#define MAX_SIZE 4096
key_t getKey() // 获取key值
{
key_t key = ftok(PATH, PROJ_ID);
if (key == -1)
{
perror("ftok fail");
exit(-1);
}
return key;
}
int getShm(key_t key, int flag) // 创建共享内存,为下面两个函数服务
{
int shm = shmget(key, MAX_SIZE, flag);
if (shm < 0)
{
perror("shmget fail");
exit(-2);
}
return shm;
}
int shmHelper(key_t key) // 获取共享内存(已创建的前提)
{
return getShm(key, IPC_CREAT);
}
int createShm(key_t key) // 创建共享内存
{
return getShm(key, IPC_CREAT | IPC_EXCL | 0666);
}
void *attachShm(int shm) // 关联
{
void *mem = shmat(shm, nullptr, 0);
if (mem == (void *)-1)
{
perror("shmat fail");
exit(-3);
}
return mem;
}
void detachShm(void *mem) // 去关联
{
if (shmdt(mem) < 0)
{
perror("shmdt fail");
exit(-4);
}
}
int deleteShm(int shm) // 删除共享内存
{
if (shmctl(shm, IPC_RMID, nullptr) < 0)
{
perror("shmctl fail");
exit(-5);
}
}
serve.cc
#include "com.hpp"
int main()
{
int key = getKey();
int shm = createShm(key);
cout << shm << endl;
void *mem = attachShm(shm);
cout << mem << endl;
int cnt = 0;
while (cnt++ < 10)
{
printf("client # %s\n", mem);
struct shmid_ds ds;
shmctl(shm,IPC_STAT,&ds);
cout << "PID->" << getpid() << ", creator->" << ds.shm_cpid << ", key->" << ds.shm_perm.__key << endl;
sleep(1);
}
detachShm(mem);
deleteShm(shm);
return 0;
}
client.cc
#include "com.hpp"
int main()
{
int key = getKey();
int shm = shmHelper(key);
cout << shm << endl;
void *mem = attachShm(shm);
cout << mem << endl;
int cnt = 0;
char *message = "Hello, I`m client";
while (1)
{
snprintf((char *)mem, MAX_SIZE, "PID->%d : %s, count : %d\n", getpid(), message, ++cnt);
sleep(1);
}
detachShm(mem);
return 0;
}
共享内存是所有通信中最快的通信方式,因为它没有缓冲区,能大大减少通信数据的拷贝次数。