当前位置:   article > 正文

Linux:进程间通信——管道、信号量_管道 互斥锁 信号量

管道 互斥锁 信号量

IPC


进程间通信:IPC(InterProcessCommunication),并发的进程同时处理多个业务模块。多个业务模块的执行可能需要相互传递数据,也可能需要同步控制,因而进程间需要进行通信。

操作系统为每一个进程维护独立的虚拟地址空间,即进程的4G虚拟地址空间,而两个实体需要交互通信,必须能够有可以共享的资源,对于不同的进程来说,磁盘上的文件是共享的,系统的内核空间是共享的,基于这两个共享的资源,就有如下几种进程间通信的方式:
在这里插入图片描述

在这里插入图片描述

对于点对点之间的通信,按照消息传送的方向与时间关系,通信方式可分为单工通信、半双工通信及全双工通信三种

  1. 单工通信:消息只能单方向传输,例:广播
  2. 半双工通信:可以实现双向的通信,但不能在两个方向上同时进行,必须轮流交替地进行,例:对讲机
  3. 全双工通信:允许数据同时在两个方向上传输,例:电话

管道


管道可以用来在两个进程之间传递数据,如: ps -ef | grep "bash", 其中‘|’就是管道,其作用就是将 ps 命令的结果写入管道文件,然后 grep 再从管道文件中读出该数据进行过滤,管道通信属于半双工通信

命名管道

命名管道即有名管道,同一台主机上,同一个系统有权限操作管道文件的任意进程都可以通过其完成进程间通讯。

注意:命名管道文件存储在磁盘上,但管道传递的数据是在内存中存储的,通过文件描述符指向的是一块内存空间,如下图,磁盘上并没有数据,所以管道文件属性信息里大小始终为0
在这里插入图片描述

创建管道文件

1.命令 :

mkfifo name
  • 1

2.系统调用:

int mkfifo(const char *filename, int mode);
  • 1

管道文件的操作

管道文件也是文件,它同Linux其他文件的操作方式一样:Linux:文件操作的系统调用

1.打开文件

int open(const char *pathname, int flag, /*int mode*/);
  • 1

2.读取文件内容

int read(int fd, void *buf, int size);
  • 1

3.向文件中写入内容

int write(int fd, void *buf, int length);
  • 1

4.关闭文件

int close(int fd);
  • 1

无名管道

无名管道只能应用在有关系的两个进程之间,例如fork的父子进程之间,它通过共享fork之前的文件描述符来实现进程之间的通信

无名管道的创建及打开
注意:无名管道创建的时候就要打开,因为它没名字,创建时不打开后期就找不到了

#include<unistd.h>

int fds[2];//fds[0]读端,fds[1]写端
int pipe(int fds[2]);//形参是有两个整型值的数组,2只是给用户一个提示:只需要两个整型值,2并没有实际意义,因为C编译时直接按指针来处理
  • 1
  • 2
  • 3
  • 4

pipe(fds[2])成功返回 0,失败返回-1,

fds[0]是管道读端的描述符

fds[1]是管道写端的描述符

无名管道例题

题目:A进程接收用户输入的数据,B进程将用户输入的字符全部转换为大写并输出

分析:fork之前创建无名管道,父进程接收用户输入的数据,子进程处理字符并输出

注意:因为管道属于半双工通信,信息不能同时双向传递,所以使用无名管道时要先关闭一对读写,只留一对读端和写端,且必须有读有写才有意义

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>

//A进程接收用户输入的数据,B进程将用户输入的字符串全部转换为大写并输出

void Toupper(char buff[])//小写转大写
{
	int i=0;
	while(i<strlen(buff))
	{
		if(isalpha(buff[i]))	
		{
			buff[i] = toupper(buff[i]);
		}
		i++;
	}
}

int main()
{
	int fds[2];//0读端,1写端
	pipe(fds);//创建无名管道

	int pid = fork();
	assert(pid != -1);

	if(pid == 0)//子进程将用户输入的字符串全部转换为大写并输出
	{
		close(fds[1]);//关闭写端

		while(1)
		{
			char buff[128] = {0};
			int n = read(fds[0],buff,127);
			if(n==0)
			{
				break;
			}
			Toupper(buff);
			printf("%s \n",buff);
		}
		close(fds[0]);
		exit(0);
		
	}
	else//父进程接收用户输入的数据
	{
		close(fds[0]);//关闭读端
		printf("please enter the string:\n");
		while(1)
		{
			char buff[128] = {0};
			fgets(buff,127,stdin);

			if(strncmp(buff,"end",3)==0)
			{
				break;
			}
			write(fds[1],buff,strlen(buff)-1);
		}
		close(fds[1]);
		exit(0);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

信号量


基本概念

信号量

是一个特殊的计数器,来完成多进程环境下的进程执行的同步控制,信号量是在内核空间的,操作系统为每个进程提供4G的虚拟地址空间,其中3G是相互独立的用户空间,1G是内核空间。代码中的任何变量都是在3G的用户空间中,而进程的1G内核空间是所有进程共用的,如下图所示
在这里插入图片描述
信号量是一个值,当信号量的值>0时,记录的是临界资源的个数,此时对信号量执行P操作不会阻塞,当信号量的值为0时,执行P操作,会阻塞,直到另一个进程对该信号量执行V操作

临界资源

同一时刻只能被一个进程/线程访问使用的资源,如打印机

临界区

程序中访问临界资源的代码块

原子操作

不能被中断的操作就是原子操作

P、V操作

PV操作都是原语,是实现进程同步和互斥的常用方法

p操作定义:申请一个资源,s:=s-1,若s<0,则设置该进程为阻塞状态,并将其插入阻塞队列;若s>=0,则执行p操作的进程继续执行

v操作定义:释放一个资源,s:=s-1,若s<=0,则从阻塞状态唤醒另一个进程,并将其插入就绪队列,执行v操作的进程继续执行;若s>0,则执行v操作的进程继续执行
在这里插入图片描述

进程需要同步执行的场景

1、间接制约关系:多个进程竞争使用同一个资源

2、直接制约关系:一个进程为另一个进程提供服务

信号量的操作接口

头文件

#include <sys/sem.h>
  • 1

创建或者获取信号量

int semget(key_t key, int nsems, int flag);//创建或者获取的是一个信号量集合(数组)
  • 1

如果是第一次访问(无论哪个进程),则需要创建;如果不是第一次访问,则直接获取

key:是一个用户标识,如果多个进程使用同一个信号量集,要求在调用semget方法时的key值相同

nsems:在创建时使用,用来执行创建的信号量集中的信号量个数

flag:可选,指定操作权限,可以设置IPC_CREAT,来指明本次需要创建信号量集

返回值:成功返回内核信号量的ID值,用于后续其他方法;失败返回-1,信号量ID事实上是信号量集合的ID,一个ID对应的是一组信号量

信号量的PV操作

int sempop(int semid, struct sembuf *buf, int buflength);
  • 1

semid:semget返回的信号量ID

buf:sembuf类型的数组的首地址

buflength:buf数组的元素个数

返回值:成功返回0,失败返回-1

其中sembuf结构体如下:

struct sembuf
{
	unsigned short sem_num; //指定信号量集中的信号量下标
	short          sem_op;  //其值为-1代表P操作,1代表V操作
	short          sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

设置信号量信息

int semctl(int semid, int semnum, int cmd,.../* union semun arg */);
  • 1

semid:semget返回的信号量ID

semnum:指定信号量集中的信号量下标

cmd:SETVAL用来把信号量初始化为一个已知的值,设置的是成员semnum的semval值,该值由第四个参数联合体的arg.val指定;IPCRMID用于删除一个已经无需继续使用的信号量标识符,这种删除是立即发生的,仍在使用此信号量集合的其他进程在它们下次试图对此信号量集合进行操作时,将出错返回

第四个参数union semun结构如下:

union semun
{
	int              val;
	struct semid_ds *buf;
	unsigned short  *array;
	struct seminfo  *_buf;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

信号量接口的封装

0、头文件

#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
  • 1
  • 2
  • 3
  • 4
  • 5

1、获取一个信号量集合

  • 创建,信号量的值进行初始化 (指定创建信号量的个数及信号量的初始值)
  • 获取,直接返回
typedef union SemUn  //semctl的第四个参数
{
	int val;
}SemUn;

//创建nsems个信号量存在initVal数组中,用户标识为key
int SemGet(int key, int initVal[], int nsems)
{
	//先获取,获取失败就创建
	int semid = semget((key_t)key, 0, 0664);
	
	if(semid == -1)//获取失败说明没有,就创建
	{
		//创建信号量集合
		semid = semget((key_t)key, nsems, 0664 | IPC_CREAT);
		if(semid == -1)    return -1;

		//对信号量集合中的所有信号量进行初始化
		int i = 0;
		for(; i< nsems; ++i)
		{
			SemUn arg;
			arg.val = initVal[i];
			semctl(semid, i, SETVAL, arg);
		}
	}
    return semid;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

2、对信号量集合中的信号量执行P操作

//对semid信号量集合中下标为index的信号量执行P操作
int SemP(int semid, int index)
{
	struct sembuf buf;
	buf.sem_num = index;
	buf.sem_op = -1;  //值为-1代表P操作
	buf.sem_flg = SEM_UNDO;
	
	return semop(semid, &buf, 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3、对信号量集合中的信号量执行V操作

//对semid信号量集合中下标为index的信号量执行V操作
int SemV(int semid, int index)
{
	struct sembuf buf;
	buf.sem_num = index;
	buf.sem_op = 1; //值为1代表V操作
	buf.sem_flg = SEM_UNDO;

	return semop(semid, &buf, 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4、删除信号量集合

//删除semid信号量集合
int SemDel(int semid)
{
	return semctl(semid, 0, IPC_RMID);
}
  • 1
  • 2
  • 3
  • 4
  • 5

以上把系统提供的接口封装成使用起来更方便的方法,接下来的例题便是基于这些自己封装的方法的

例:模拟访问打印机

题目:进程 a 和进程 b 模拟访问打印机,进程 a 输出第一个字符‘a’表示开始使用打印机,输出第二个字符‘a’表示结束使用, b 进程操作与 a 进程相同。(由于打印机同一时刻只能被一个进程使用,所以输出结果不应该出现 abab),如图所示:
在这里插入图片描述
代码如下:

进程a:

#include <stdio.h>
#include "sem.h"     //上面封装好的方法
#include <time.h>
#include <stdlib.h>

int main()
{
	srand((unsigned int)time(NULL));
	
	int initVal = 1;
	int semid = SemGet(1234, &initVal, 1);//只需要1个信号量
	assert(semid != -1);
	
	int count = 5;
	while(count--)
	{
		SemP(semid,0);
		
		printf("a");
		fflush(stdout);//刷新输出缓冲区
		int n = rand() % 3;//模拟打印机正在使用
		sleep(n);
		printf("a");
		fflush(stdout);//刷新输出缓冲区
		
		SemV(semid,0);
		
		n = rand() % 3;//模拟使用完打印机去干其他事
		sleep(n);	
	}
	exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

进程b:

#include <stdio.h>
#include "sem.h"  
#include <time.h>
#include <stdlib.h>

int main()
{
	srand((unsigned int)time(NULL));
	
	int initVal = 1;
	int semid = SemGet(1234, &initVal, 1);//两个进程要使用同一个信号量key值1234要一样
	assert(semid != -1);
	
	int count = 5;
	while(count--)
	{
		SemP(semid,0);
		
		printf("b");
		fflush(stdout);//刷新输出缓冲区
		int n = rand() % 3;//模拟打印机正在使用
		sleep(n);
		printf("b");
		fflush(stdout);//刷新输出缓冲区
		
		SemV(semid,0);

		n = rand() % 3;//模拟使用完打印机去干其他事
		sleep(n);	
	}
	exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

运行结果如下:

在这里插入图片描述

例:模拟进程间直接制约

题目:三个进程a, b, c分别输出’A’, ‘B’, ‘C’,要求输出结果必须是"ABCABCABCABC…"

上个例子打印机模拟间接制约关系,该例模拟进程间直接制约关系,A到B需要一个信号量,B到C需要一个,C到A还需要一个,abc进程代码如下图:

在这里插入图片描述
运行结果如下:
在这里插入图片描述
注意:进程结束之后,创建的信号量仍然在内核之中,需要使用ipcsipcrm命令来进行查看与手动删除

ipcs/ipcrm

ipcs/ipcrm命令 是linux/uinx上提供关于一些进程间通信方式的信息,包括共享内存,消息队列,信号

ipcs 可以查看消息队列、共享内存、信号量的使用情况,使用 ipcrm可以进行删除操作

ipcs选项如下:

ipcs -a  默认输出信息:打印出当前系统中所有的进程间通信方式的信息
ipcs -m  查看使用共享内存进行进程间通信的信息
ipcs -q  查看使用消息队列进行进程间通信的信息
ipcs -s  查看使用信号进行进程间通信的信息
  • 1
  • 2
  • 3
  • 4

ipcrm选项如下:

ipcrm -M shmkey   删除用shmkey创建的共享内存段
ipcrm -m shmid    删除用shmid标识的共享内存段
ipcrm -Q msgkey   删除用msqkey创建的消息队列
ipcrm -q msqid    删除用msqid标识的消息队列
ipcrm -S semkey   删除用semkey创建的信号
ipcrm -s semid    删除用semid标识的信号
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

例:
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/675093
推荐阅读
相关标签
  

闽ICP备14008679号