目录
前言
一、初始进程间通信
二、管道
1.匿名管道
2.命名管道
三、共享内存
四、消息队列(了解)
五、信号量(了解)
前言
提示:这里可以添加本文要记录的大概内容:
进程是一个能够独立运行,独立分配资源,独立调度的基本单位,每个进程都有自己独立的虚拟地址空间。当两个进程要想通信,必须使用特定的通信机制来实现进程间的通信。
提示:以下是本篇文章正文内容,下面案例可供参考
一、初始进程间通信
进程间的通信方式:
进程间的通信方式有多种,面对不同的场景延伸出的情况也有多种。下面就对这四种进程间的通信方式进行说明。管道、共享内存、消息队列、信号量。
二、管道
管道:实现进程间通信的一种方式。
特性:是一种半双工通信。 本质:内核中的一块缓冲区。
生命周期:随进程存在而存在,只有当所有使用管道的进程全部结束,管道才结束。
分类:匿名管道、命名管道。
在Linux下常用 | 来标识匿名管道
1.匿名管道
匿名管道只能用于具有亲缘关系的进程之间进行通信。如:父子进程。同时匿名管道是没有名字的,也就是无法通过文件来访问。那么匿名管道是如何让进程进行访问的呢?这时候就要引入一个概念,文件描述符(操作句柄)
文件描述符(操作句柄)
文件描述符主要用于Linux系统,操作句柄主要用于Windows系统。
我们首先要知道Linux下一切皆文件,所有的东西,包括显示都是文件。文件描述符是用来标识文件的一系列整数。是进程和内核直接进行交流的桥梁。通过文件标识符可以访问系统调用接口,来达到进程调用和访问Linux中的文件。标识了当前要操作的文件
如:一个进程运行会默认打开三个文件,标准输入、标准输出、标准错误,分别是0、1、2。在Linux下程序运行会加载到内存中,变成一个结构体PCB,这个结构体中有一个FILE类型的指针其中包含FILE* fd_arr[],指向文件结构体中,这个结构体像一个数组指针,包含指向标准输入、标准输出、标准错误的文件。我们通过下标来进行访问操作。如标准输入的下标为0,通过0作数组下标来访问指向标准输入文件的指针,达到调用标准输入的目的。
匿名管道的实现原理、
1、进程A创建一个匿名管道(获取到管道的操作句柄--文件描述符)
2、进程A创建一个子进程
被创建出来的子进程复制父进程的时候也复制了文件描述符,也就获得父进程能够进入管道的文件描述符,那么子进程和父进程拥有的就是指向同一个内核中的管道的文件描述符,这时候父子进程就可以通过这个管道实现数据通信
接口:
int pipe(int pipefd[2]); // 功能:创建一个管道
参数:
pipefd[2]:一个整形数组,用于存储管道的文件描述符。pipefd[0]用于从管道读数据。
pipefd[1]描述符用以向管道写入数据。
返回值:成功返回0,失败返回-1。
ssize_t read(int fd, void *buf, size_t len); // 功能读取数据
参数:
ssize_t 是返回类型,表示成功读取的字节数,或者在出错时返回
-1;
int fd是文件描述符,指向要读取的文件或管道;
void *buf 是空间首地址,指向缓冲区,会读取这个缓冲区的数据
siez_t len 要读取数据的长度
返回值:失败返回-1
ssize_t write(int fd, const void *buf, size_t len); // 功能写入数据
参数:
ssize_t 是返回类型,表示成功读取的字节数,或者在出错时返回
-1;
int fd是文件描述符,指向要读取的文件或管道;
const void *buf 指向要写入数据的缓冲区
size_t len:写入文件的数据长度
返回值:成功返回写入的字节数,失败返回-1
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
//父进程向管道写数据,子进程读取父进程的数据
int main()
{
//创建两个描述符
int pipefd[2];
int pip = pipe(pipefd);//创建管道
if (pip < 0) { //判断管道是否创建成功
perror("pip error");
return -1;
}
pid_t ppid = fork(); //创建子进程
if (ppid < 0){ //判断进程是否创建成功
perror("ppid error");
return -1;
}else if (ppid == 0) { //子进程
char verser[1024] = {0};
close(pipefd[1]); //关闭写端
int pip = read(pipefd[0], verser, 1023);//子进程从管道中读取数据
if(pip < 0) { //是否通信成功
perror("read error");
return -1;
}
printf("verser[%s]\n", verser);
close(pipefd[0]); //读取完后关闭读端
}else { //父进程
char *verser = "大鹏一日同风起,扶摇直上九万里";
write(pipefd[1], verser, strlen(verser)); //父进程向管道写入数据
close(pipefd[1]); //写入完毕后关闭写端
wait(NULL); //等待子进程退出,避免僵尸进程
}
return 0;
}
2.命名管道
命名管道是具有标识符的管道,可用于同一主机上任意进程间通信。命名管道以文件的形式存在。文件只是一个名字,其本质的内核中的缓冲区。多个进程通过打开同一个管道文件来访问同一块内核中的缓冲区
命名管道创建
mkfifo text.fifo 这里是用mkfifo创建了一个管道文件 text.fifo。它只是用来命名管道的管道文件,只是一个满足并不存储数据。多个进程通过打开同一个管道文件来进行通信。
命名管道的特性
1、一个命名管道如果以只读的方式打开会阻塞,直到这个管道文件被写的方式打开
2、一个命名管道如果以只写的方式打开会阻塞,直到这个管道文件被读的方式打开
3、一个命名管道要以读写的方式打开才行。
接口:
int mkfifo(const char* filename, mode_t mode); //功能:创建管道文件
参数:
const char* filename //指向null结尾的字符串的指针,管道文件的名称,文件的路径名
mode_t mode //是一个整数,指定了管道文件的访问权限,限制了一个进程
//能够对管道进行的操作
返回值:成功返回0;失败返回-1;
//
int open(const char *pathname, int flags); //功能:打开一个文件
int open(const char *pathname, int flags, mode_t mode); //功能:创建一个文件参数:
const char *pathname //要打开或创建的文件的路径
int flags //打开指定文件后能做的权限
mode_t mode //创建新文件时使用
返回值:成功返回一个非负的文件描述符,失败返回-1;
ssize_t write(int fd, const void *buf, size_t count) //功能向文件写数据
参数:
int fd //要写入的文件描述符
const void *buf //指向要写入数据的缓冲区的指针
size_t count //要写入数据的字节数
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
// 读去管道数据的进程
int main()
{
int ret = mkfifo("./text.fifo", 0664); //访问text.fifo的管道文件,并且设置权限为-rw-rw-r--
if(ret < 0){
perror("mkfifo, error");
return -1;
}
while(1){ //读取数据
char buf[1024] = {0};
int ret = read(fd, buf, 1023);
if(ret < 0){
perror("read error");
return -1;
}
else if(ret == 0){
printf("所以写端被关闭");
return -1;
}
printf("%s\n", buf);
}
close(fd); //关闭打开的文件
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
// 向管道中写入数据的进程
int main()
{
int ret = mkfifo("./text.fifo", 0664); //访问text.fifo的管道文件,并且设置权限为-rw-rw-r--
if(ret < 0){
if(errno != EEXIST){ //EEXIST表示文件已经存在,errno是记录系统错误的int型,这里管道文件已经存
在所以当文件不存在的时候在退出
perror("mkfifo, error");
return -1;
}
}
printf("fifo create success!!\n"); //成功访问到管道文件
int fd = open("./text.fifo", O_WRONLY);
if( fd < 0){
perror("open error");
return -1;
}
printf("open fifo success!!\n");
int left_time = 100;
while(1){
char buf[1024] = {0};
sprintf(buf, "还有%d秒后关机", left_time--);
write(fd, buf, strlen(buf)); //向文件写入数据
sleep(1); //休眠眠一秒
}
close(fd);
return 0;
}
在命名管道中,只有同时具备读写的时候才能进行通信,若两个进程一个读,一个写,任意一个进程运行都不能通信,任何一个进程关闭也会退出管道。
三、共享内存
共享内存是最快的进程间通信方式
作用:实现进程间的数据共享
原理:内核开辟一块物理内存成为共享空间,其他想要实现进程间通信的进程,将这块
内存映射到自己的虚拟地址空间中,通过虚拟地址进行访问。
为什么共享内存是最快的:
共享内存通过虚拟地址空间访问数据,不需要像管道那样,还有将数据拷贝到自己的进
程空间中,同其他通信方式减少了拷贝这一步,故是最快的。
特征:
1、生命周期随内核
2、最快的进程间通信方式
注意事项
多个进程对共享内存的访问操作是存在安全隐患的,需要借助其他用于实现,同步互斥的技术来搭配使用
Linux下实现共享的流程和接口
1、创建一块共享内存,并获取操作句柄
int shmget(key_t key, size_t size, int flag)
key_t key:共享内存的标识符,通过者访问相同的共享内存段,通常通过ftok函数生成
size_t size:共享内存的大小,以字节为单位
int flag:控制 shmget 函数的行为,如0666权限和一些特殊的标志
IPC_CREAT:指定的共享内存不存在,则创建一个新的共享内存
IPC_EXCL:与 IPC_CREAT一起使用,如果共享内存段已经存则 shmget 将
失败。这通常用于确保创建的共享内存段是唯一的。
返回值:成功返回一个非负整数-共享内存的操作句柄,失败返回-1;
key_t ftok(const char *pathname, int proj_id);
const char *pathname:这是一个指向字符数组(C字符串)的指针,表示一个已存在的文件路径
int proj_id:这是一个整型值,通常是一个字符
返回值:成功返回一个唯一的键值(key_t类型),失败返回key_t-1
2、将共享内存映射到自己的虚拟地址空间
void *shmat(int shmid, void* addr, int flag);
int shmid:shmget返回的操作句柄
void* addr:映射首地址,通常置NULL
int flag:标识符 其中0-默认读写方式映射,SHM_RDONLY表示以只读方式映射。
返回值:成功返回映射的首地址,失败返回(void*)-1,并设置errno指示错误类型
3、内存操作
如 printf、strcpy 等。凡是涉及到对内存进行操作的。
4、解除映射关系
int shmdt(void* start) //接触映射关系不代表删除共享内存
void* start:指向之前通过 shmdt 返回的共享内存起始地址
返回值:成功返回0,失败返回-1
5、删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
int shmid:shmget返回的操作句柄
int cmd:对共享内存进行的操作
IPC_STAT:获取共享内存的状态存储在buf指向的shmid_ds结构中
IPC_SET:设置共享内存的属性,从buf指向的shmid_ds结构中获取
IPC_RMID:删除共享内存,不会立即释放,而是标记为不再使用。当所有进
程都不再该共享内存中的时候,系统才会回收释放
struct shmid_ds *buf:这是一个指向shmid_ds结构的指针,用于存储或接收共享
内存的状态信息。通常也包含共享内存段的标识符、创建者
和所有者的用户ID和组ID、访问权限、最后附加和分离的时
间等信息。
返回值:成功返回0,失败返回-1;
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_KEY 0x010101 //key值的标识
int main()
{
int shmid = shmget(IPC_KEY, 4096, IPC_CREAT|0664); //创建一个共享内存
if(shmid < 0){
perror("shmget error");
return -1;
}
void* start = shmat(shmid, NULL, 0); //建立映射关系
if(start == (void*)-1){
perror("shmat error");
return -1;
}
while(1){ //访问数据
printf("%s\n", (char*)start);
sleep(1);
}
shmdt(start); //解除映射关系
shmctl(shmid, IPC_RMID, NULL); //删除内存
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
#define IPC_KEY 0x010101 //key值的标识
int main()
{
int shmid = shmget(IPC_KEY, 4096, IPC_CREAT|0664); //创建一个共享内存
if(shmid < 0){
perror("shmget error");
return -1;
}
void* start = shmat(shmid, NULL, 0); //建立映射关系
if(start == (void*)-1){
perror("shmat error");
return -1;
}
while(1){ //访问数据
int num = 1;
sprintf(start, "%s+%d", "正在写入数据", num++);
sleep(1);
}
shmdt(start); //解除映射关系
shmctl(shmid, IPC_RMID, NULL); //删除内存
return 0;
}
通过两个代码,一个写数据,一个读数据来进行演示。
四、消息队列(了解)
作用:实现进程间的数据传输
原理:在内核中创建一个指定标识符的优先级队列,多个进程通过相同的标识符访问内核中
的同一个队列,通过向队列添加节点来实现进程间的数据传输。
接口:几个常用接口
int msgget(key_t key, int msgflg); //创建或打开消息队列
int msgsnd(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio); //将消息发送到消息队列中
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
//从消息队列中接收消息
int msgctl(int msqid, int cmd, struct msqid_ds *buf); //控制消息队列
特性:生命周期随内核,自带同步与互斥
五、信号量(了解)
作用:用于实现进程间的同步与互斥。是一个非负整数
本质:内核中的一个计数器+pcd等待队列。信号量大于0资源可用,等于0资源被占用。
操作:
P操作(等待):对计数器进行判断,如果计数器数组<=0则阻塞进程,否则返回计数-1;
V操作(释放):对计数器进行+1,且唤醒一个阻塞队列中的进程;
同步:
通过某种条件控制,让资源分配的更加合理—通过计数器对资源进行计数。进程获取资
源前进行P操作,其他进程产生一个资源后进行V操作。
互斥:
临界资源:多个进程都可以访问的资源
临界区:代码中专门用来访问临界资源的这段代码叫临界区。
原子性:不可分割特性
原子操作:访问临界资源的过程不可被打断。
通过同一时间的唯一访问,让进程对临界资源的获取更加合理安全。
实现:
将信号量初始值设置为1,表示只有一个资源
访问临界资源前进行P操作
访问临界资源结束后进行V操作