进程间通信——管道、信号量、共享内存、消息队列、套接字

进程间通信

  • 管道
    • 有名管道的创建使用
    • 为什么无名管道只能在父子进程通信
  • 信号量
    • 两个程序访问同一个资源
    • 信号量相关API接口
  • 共享内存
    • 共享内存相关API接口
  • 消息队列
    • 消息队列相关API接口
  • 套接字

管道

  • 有名管道
  • 无名管道

区别:
有名管道在任意两个进程间通信
无名管道在父子进程间通信

有名管道的创建使用

  • 创建有名管道使用:mkfifo
  • 打开管道:open()
  • 关闭管道:close()
  • 读数据:read()
  • 写数据:write()

我们创建一个管道
在这里插入图片描述
在这里插入图片描述
我们写一个程序,给管道写入数据,再从另一个程序读取数据

这样看好像用文件操作也可以实现:通过一个程序写入数据,另一个文件读取数据。但是这样做有一些问题:比如我们并不知道什么时候给文件写入了数据,不知道什么时候去从文件读取数据,再一个这样使用的磁盘储存的速度非常的慢,与管道相比慢很多倍

写入管道代码

#include
#include
#include
#include
#include
#include


int main()
{
	int fd = open("fifo",O_WRONLY);//打开管道文件 (默认打开当前位置的fifo)
	assert(fd!=-1);
	
	printf("fd=%d\n",fd);
	while(1)
	{
		printf("input:\n");
		char buff[128] = {0};
		fgets(buff,128,stdin);
		if(strncmp(buff,"end",3)==0)
		{
			break;
		}
		write(fd,buff,strlen(buff));
	}
	close(fd);
}

读取管道代码

#include
#include
#include
#include
#include
#include


int main()
{
	int fd = open("fifo",O_RDONLY);//打开管道文件
	assert(fd!=-1);

	printf("fd=%d\n",fd);
	while(1)
	{
		char buff[128] = {0};
		
		if(read(fd,buff,127)==0)
		{
			break;
		}
		printf("read:%s\n",buff);
	}
	close(fd);
}

编译运行

当我们只运行a,但是并没有输出我们想要的内容(fd),只有一个程序打开管道是阻塞住的,因为需要两个程序同时打开管道才有用(就好像整个世界只有你一个有手机,等于你也没有,你并没有可以打电话的对象)
在这里插入图片描述
我们再执行b
进程间通信——管道、信号量、共享内存、消息队列、套接字_第1张图片
这下才会打印出我们想要的结果
进程间通信——管道、信号量、共享内存、消息队列、套接字_第2张图片
进程间通信——管道、信号量、共享内存、消息队列、套接字_第3张图片

为什么无名管道只能在父子进程通信

无名管道

  • 使用pipe创建无名管道

使用这个方法可以直接创建得到在管道读数据与写数据的描述符,相对简单方便,父子进程通信有限考虑无名管道

我们写一段代码来看一下
进程间通信——管道、信号量、共享内存、消息队列、套接字_第4张图片
通过帮助手册查看,可以看到需要传入两个元素的整型数组,数组一个代表读文件描述符,另一个代表写文件描述符

#include
#include
#include
#include
#include
#include
#include
#include


int main()
{
	int fd[2];
	assert( pipe(fd)!=-1 ); //fd[0] read 读文件描述符, fd[1] write 写文件描述符
	
	pid_t pid = fork();
	assert(pid!= -1);
	
	if(pid == 0)
	{
		close(fd[1]);//进行读操作,我们直接关闭写文件描述符
		char buff[128] = {0};
		read(fd[0],buff,127);
		printf("child read:%s\n",buff);
		close(fd[0]);
	}
	else
	{
		close(fd[0]);//read 关闭读,与上面同理
		write(fd[1],"hello",5);
		close(fd[1]);
	}
	wait(0);
}

编译运行查看

信号量

  • 概念

就像我们的红绿灯,如果不进行管理,两个方向的成车辆会发生拥挤堵车,所以我们通过红绿灯控制,当一方通行,另一方进行等待来做到流畅运行,再举一个例子,比如我们有一个饮水机,有一个人再用另外其他的人就需要进行等待,我们程序也需要进行资源的控制,通过信号量来控制资源的访问

  • 信号量的P V操作

PV操作是原子操作,进行加一减一,P操作进行减一代表获取资源,V操作进行加一代表释放资源。例如我们商场的试衣间有三个位置,当有一个人去使用,我们进行P操作将信号量减一,再有人去使用的时候就要去观察信号量是不是0,如果是0,就代表三个试衣间都被占用,这样这个人就要等待信号量加一,便是阻塞住

  • 作用

控制程序对资源的使用的调控,大部分时候我们的信号量不是0就是1

  • 临界资源

代表计算机的软硬件资源(打印机),他们有一个特性就是某一时刻只允许一个进程或者线程进行使用,我们将这类资源成为临界组员

  • 临界区

临界区就是控制访问临界资源的代码段

两个程序访问同一个资源

进程间通信——管道、信号量、共享内存、消息队列、套接字_第5张图片
如果我们不进行控制,两个程序会通知使用打印机造成打印混乱

我们写两端程序来模拟打印机的使用

#include
#include
#include
#include
#include

int main()
{
	int i = 0;
	sem_init();
	for(;i<5;i++)
	{
		printf("A");
		fflush(stdout);
		int n = rand()%3;//随机睡眠
		sleep(n);
		
		printf("A");
		fflush(stdout);
		n = rand()%3;//随机睡眠
		sleep(n);
	}

	sleep(10);

}
#include
#include
#include
#include
#include

int main()
{
	int i = 0;
	sem_init();
	for(;i<5;i++)
	{
		printf("B");
		fflush(stdout);
		int n = rand()%3;//随机睡眠
		sleep(n);
		
		printf("B");
		fflush(stdout);
		n = rand()%3;//随机睡眠
		sleep(n);
	}

	sleep(10);
}

我们运行这两个程序
在这里插入图片描述
得到的就是乱掉的打印结果:ABABBBABAB

进程间通信——管道、信号量、共享内存、消息队列、套接字_第6张图片
我们现在添加信号量的控制操作

信号量相关API接口

通过帮助手册看一下信号量的API接口
semget:创建一个信号量,或者获取一个已经创建好的信号量,参数:整型key值;创建信号量的数量;标志位
int semget(key_t key, int nsems, int semflg );
进程间通信——管道、信号量、共享内存、消息队列、套接字_第7张图片
进程间通信——管道、信号量、共享内存、消息队列、套接字_第8张图片

semop:参数:semget的返回值semid;结构体指针,成员有编号,进行PV哪个操作,标志位
int semop(int semid, struct sembuf *sops, size_t nsops);
进程间通信——管道、信号量、共享内存、消息队列、套接字_第9张图片
semctl:对信号量进行控制(初始化、移除等);参数:semid,对哪个信号量进行操作,命令
int semctl(int semid, int semnum, int cmd, ...);
进程间通信——管道、信号量、共享内存、消息队列、套接字_第10张图片

我们写一段代码来看一看

//写一个.h文件定义结构体,和方法声明
#include
#include
#include
#include


//初始化需要的联合体,省略了一部分内容
union semun{
	int val;//信号量大小
};

void sem_init();//初始化信号量
void sem_p();//P操作
void sem_v();//v操作
void sem_destroy();//销毁信号量

#include"sem.h"

static int semid = -1;//定义一个静态全局变量

void sem_init()
{
	//首先需要进行判断是不是全新创建,是就创建不是就获得已有的信号量
	//第一个参数key,任意数字就可以
	//第二个参数,信号量大小
	//第三个参数,标志位,代表全新创建,以及加上权限
	semid = semget( (key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//标志位(全新创建) 
	if(semid == -1)
	{
		//不创建不需要写标志位,写上权限
		semid = semget( (key_t)1234,1,0600);//不全新创建
		if(semid == -1)
		{
			perror("semget error");//创建失败
		}
	}

	else
	{
		//初始化操作
		union semun a;
		a.val = 1;
		//第一个参数,id
		//第二个参数,下标,只有一个信号量下标为0
		//第三个参数,命令,设置值
		//第四个参数,a
		if( semctl(semid,0,SETVAL,a) == -1 )
		{
			perror("semctl init error");
		}

	}
}
void sem_p()
{
	//结构体,sem_num下标;sem_op=-1 减一进行P操作;sem_flg标志位 代表若没有将信号量进行V操作,则异常终止则操作系统进行V操作
	struct sembuf buf;
	buf.sem_num = 0;//下标 第几个
	buf.sem_op = -1;//p操作
	buf.sem_flg = SEM_UNDO;
	
	//参数:id;结构体指针;操作个数
	if(semop(semid,&buf,1)==-1)
	{
		perror("p error");
	}
}
void sem_v()
{
	//与上同理
	struct sembuf buf;
	buf.sem_num = 0;
	buf.sem_op = 1;
	buf.sem_flg = SEM_UNDO;
	
	if(semop(semid,&buf,1)==-1)
	{
		perror("v error");
	}
}
void sem_destroy()
{
	//参数:id;第二个参数无意义,会将整个信号量移除;命令 删除;最后一个参数可选,不给
	if(semctl(semid,0,IPC_RMID) == -1)
	{
		perror("destory sem error");
	}
}

我们对之前的两个模拟打印操作的代码进行修改

#include
#include
#include
#include
#include
#include"sem.h"

int main()
{
	int i = 0;
	//初始化
	sem_init();
	for(;i<5;i++)
	{
		sem_p();//若信号量为0 就会阻塞

		printf("A");
		fflush(stdout);
		int n = rand()%3;//随机睡眠
		sleep(n);
		printf("A");
		fflush(stdout);

		sem_v();

		n = rand()%3;
		sleep(n);

	}
	
	sleep(10);//人为让a最后结束,b就不进行销毁
	sem_destroy();
}
#include
#include
#include
#include
#include
#include"sem.h"



int main()
{
	int i = 0;
	sem_init();
	for(;i<5;i++)
	{
		sem_p();

		printf("B");
		fflush(stdout);
		int n = rand()%3;//随机睡眠
		sleep(n);
		printf("B");
		fflush(stdout);

		sem_v();

		n = rand()%3;
		sleep(n);
	}
}

编译运行
在这里插入图片描述
需要给出.c文件

运行看到打印是有序的正确的
在这里插入图片描述
通过ipcs可以查看信号量
在这里插入图片描述
前面的键转换为十进制就是我们之前设置的1234,程序结束在输入ipcs查看
在这里插入图片描述
消息队列销毁

共享内存

将一块物理内存映射到两个不同的进程空间中,当启用一个进程读取或者写入这块内存,对于另一个进程下这块空间也会发生改变
进程间通信——管道、信号量、共享内存、消息队列、套接字_第11张图片
原本操作系统会对进程进行保护,不允许两个进程使用同样一块物理空间,共享内存通过接口打破了这种约定,我们来查看一下这些API接口

共享内存相关API接口

  • int shmget(key_t key,size_t size,int shmflg);
    创建共享内存:参数;key值,共享内存大小,标志位
  • void* shmat(int shmid, const void *shmaddr, int shmflg);
    映射共享内存:参数;创建获得的id;共享内存出现的位置,一般给NULL,系统会自动选择;标志位设置可读可写,一半给0为可读可写
  • int shmdt(const void *shmaddr);
    断开映射:当我们一个进程不再使用共享内存时,会断开映射而不是删除共享内存,因为可能还有其他进程在使用共享内存
  • int shmctl(int shmid, int cmd, struct shmid_da *buf);
    对共享内存进行设置:可以通过不同的命令设置删除共享内存,删除共享内存时候如果还有人在使用共享内存会产生延迟,直到最后一个人断开映射才会删除,参数;id;命令;结构体

我们写一段代码来看一下

#include
#include
#include
#include
#include
#include

int main()
{
	//创建共享内存
	//参数:
	//给定的key值
	//内存大小
	//标志位,IPC_CREATE创建 以及权限
	int shmid = shmget((key_t)1234,256,IPC_CREAT|0600);assert(shmid != -1);
	//创建成功 可以通过ipcs命令查看

	//映射内存
	//参数:
	//shmid 共享内存的id
	//物理内存位置 一般给NULL
	//权限标志 一般给0
	char *s = (char *)shmat(shmid,NULL,0); 
	assert(s != (char*)-1);

	//在共享内存写入数据
	strcpy(s,"hello");
	shmdt(s);//断开链接
}

编译运行后使用ipcs查看
在这里插入图片描述
发现有一个编号0x4d2(1234),大小256的共享内存

我们再写一个读共享内存的代码

#include
#include
#include
#include
#include
#include

int main()
{
	int shmid = shmget((key_t)1234,256,IPC_CREAT|0600);
	assert(shmid != -1);

	char *s = (char *)shmat(shmid,NULL,0); 
	assert(s != (char*)-1);

	printf("%s",s);
	shmdt(s);
}

运行查看
在这里插入图片描述
在这里插入图片描述
这个时候共享内存依旧存在

消息队列

进程间通信——管道、信号量、共享内存、消息队列、套接字_第12张图片

消息队列的原理是这样的,我们往这个队列中添加消息并且消息有一个类型值,如上图我们有两个类型的消息一个是1,一个是2;进程会根据需要获取相应类型的消息,若没有相应类型消息则获取不到消息,当消息队列已满(达到最大值)则在添加消息会被阻塞

消息队列相关API接口

  • int msgget(key_t ket, int msqflg);
    创建或者获取一个消息队列;参数:key值,标志位
  • int msgsed(int msqid, const void *msqp, size_t msqsz,int msqflg);
    向消息队列发送消息;参数:消息队列id(向哪一个消息队列发送消息),结构体指针(结构体第一个变量,代表消息类型,第二个变量就是消息的数据),消息大小(数据部分大小),标志位一般给0
    进程间通信——管道、信号量、共享内存、消息队列、套接字_第13张图片
  • ssize_t msgrcv(int msqid, void *msgp, size_t msqsz, long msqtyp, int msqflg);
    接收消息队列的消息;参数:消息队列id,结构体指针存储接收 的消息,接收数据的大小,指定接受的消息类型,标志位
  • int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    控制消息队列,当消息队列产生如果不去进行删除它会一直存在,除非重启系统,可以通过控制删除消息队列;参数:消息队列id,cmd命令,获取信息的结构体(若删除则不需要直接给NULL)

我们写一段代码来看一下

#include
#include
#include
#include
#include
#include

//消息队列需要的结构体
struct mess
{
	long type;
	char buff[128];
};
int main()
{
	//创建消息队列
	int msgid = msgget((key_t)1234,IPC_CREAT|0600);
	assert(msgid != -1);
	
	struct mess dt;
	dt.type = 1;//发生消息不能为0 接收可以是0代表接收任何类型消息
	strcpy(dt.buff,"hello 1");//写入数据
	
	//发送消息到消息队列
	msgsnd(msgid,&dt,128,0);
}

我们编译运行这段代码,使用ipcs查看
在这里插入图片描述
消息队列产生,当我们再次执行程序,消息会变成2

再写一段接收消息的消息队列代码

#include
#include
#include
#include
#include
#include

struct mess
{
	long type;
	char buff[128];
};
int main()
{
	//IPC_CREAT,冷知识为什么是CREAT而不是CREATE,这是因为作者在编写的时候疏忽导致少写了一个E字母
	int msgid = msgget((key_t)1234,IPC_CREAT|0600);
	assert(msgid != -1);
	
	struct mess dt; 
	
	//id,结构体指针(接收消息存放进去),buff数据大小,type类型(0类型消息 意味着全部接收),标志位0
	msgrcv(msgid,&dt,128,1,0);
	printf("read message:%s\n",dt.buff);
}

编译执行,并再次使用ipcs查看,消息队列消息变成 0
进程间通信——管道、信号量、共享内存、消息队列、套接字_第14张图片
删除消息队列

#include
#include
#include
#include
#include
#include

int main()
{
	int msgid = msgget((key_t)1234,IPC_CREAT|0600);
	assert(msgid != -1);
	//创建消息队列 程序结束不会消失
	msgctl(msgid,IPC_RMID,NULL);//id,命令,获取信息,删除为null
	//控制消息队列
}

进程间通信——管道、信号量、共享内存、消息队列、套接字_第15张图片

套接字

套接字涉及内容较多,我们在这里先不进行讲解,将会在后面单独拿出来进行详细完整的介绍。

你可能感兴趣的:(Linux,c语言,linux,多进程,消息队列)