【管道,父子间通讯,共享内存,信号量】

进程间通讯缩写 :IPC

进程间通讯包括:管道 信号量 共享内存 消息队列 套接字socket

管道

受协同程序[ RrTC84]概念的启发,管道是一个环形缓冲区,允许两个进程以生产者/消费者的模型进行通信。因此,这是一个先进先出( FIFO)队列,由一个进程写,而由另一个进程读。
创建一个管道后,它的大小是固定的字节数。当一-个进程试图往管道中写时,如果有足够的空间,则写请求被立即执行;否则该进程被阻塞。类似地,如果一个读进程试图读取多于当前管道中的字节数时,它也被阻塞;否则读请求被立即执行。操作系统强制实施互斥,即一次只能有一个进程可以访问管道。
有两类管道:有名管道和无名管道。只有有“血缘"关系4的进程才可以共享无名管道,而不相关的进程只能共享有名管道。

记住,操作系统强制实施互斥,即一次只能有一个进程可以访问管道。

创建管道

mkfifo

什么是生产者消费者模型?

生产者/消费者问题,也被称作有限缓冲问题。可以描述为:两个或者更多的线程共享同一个缓冲区,其中一个或多个线程作为“生产者"会不断地向缓冲区中添加数据,另一个或者多个线程作为“消费者"从缓冲区中取走数据。生产者/消费者模型关注的是以下几点:
生产者和消费者必须互斥的使用缓冲区 缓冲区空时,消费者不能读取数据缓冲区满时,生产者不能添加数据

1.解耦:因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这样生产者和消费者的代码发生变化,都不会对对方产生影响。这样其实就是把生产者和消费者之间的强耦合解开,变成了生产者和缓冲区,消费者和缓冲区之间的弱耦合。
2.支持并发:如果消费者直接从生产者拿数据,则消费者需要等待生产者生产数据,同样生产者需要等待消费者消费数据。而有了生产者/消费者模型,生产者和消费者可以是两个独立的并发主体。生产者把制造出来的数据添加到缓冲区,就可以再去生产下一个数据了。而消费者也是一-样的, 从缓冲区中读取数据,不需要等待生产者。这样,生产者和消费者就可以并发的执行。
3.支持忙闲不均:如果消费者直接从生产者这里拿数据,而生产者生产数据很慢,消费者消费数据很快,或者生产者生产数据很多,消费者消费数据很慢。都会造成占用CPU的时间片白白浪费。生产者/消费者模型中,生产者只需要将生产的数据添加到缓冲区,缓冲区满了就不生产了。消费者从缓冲区中读取数据,缓冲区空了就不消费了,使得生产者/消费者的处理能力达到一个动态的平衡。

为什么用管道文件接收数据?

管道文件是从内存中存储的,相较来说磁盘中的文件接收开销比较大。
一、有名管道

1.管道必须读,写进程,需要同时open,否则阻塞;
2.管道中没有数据会阻塞住;

如何循环读写?(终止如何设置?)

3.管道的写端关闭,读read返回值为0
管道的读端关闭,写会产生异常,(发送信号SIGPIPE)

在这里插入图片描述
在这里插入图片描述

以下的注释说明,注意数组是否初始化对程序的影响。char buff[128];

在这里插入图片描述

在这里插入图片描述

二、无名管道pipe

无名管道多用于父子进程通讯
在这里插入图片描述
创建完管道读写端都是默认阻塞。管道满,write ()阻塞;管道空,read()阻塞。
管道传输的数据流和TCP数据流的略微差别:
管道内部传输的数据是字节流,这和TCP字节流的概念相同。但二者又有细微的区别。应用层程序能往一个TCP连接中写人多少字节的数据,取决于对方的接收通告窗口的大小和本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据。自Linux 2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量、。

int pipe (int pipefd[2]);
fds[0]是管道读端的描述符
. fds[1]是管道写端的描述符


管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。比如,我们要使用管道实现从父进程向子进程写数据,就应该按照下图所示来操作。
在这里插入图片描述

成功返回0.错误返回-1。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
int main()
{
	int fd[2];
	int res = pipe(fd);
	assert(res != -1)
	pid_t pid = fork();
	assert(pid!=-1)
		if (pid == 0)
		{
			write(fd[1], "hello", 5);
		}
		else {
			char buff[128] = { 0 };
			read(fd[0], buff, 127);
			printf("father buff read :%s\n", buff);

		}
	close(fd[0]);
	close(fd[1]);
	exit(0);

}

在这里插入图片描述
我们把父子进程的代码狡交换发现结果也可以正常打印。
在这里插入图片描述

如果把写端的打印buff的代码去掉\n 会怎么样?看一下
在这里插入代码片

在这里插入图片描述


管道的通讯是:半双工

半双工(Half Duplex)数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。例如,在一个局域网上使用具有半双工传输的技术,一个工作站可以在线上发送数据,然后立即在线上接收数据,这些数据来自数据刚刚传输的方向。像全双工传输一样,半双工包含一个双向线路(线路可以在两个方向上传递数据)。

管道是一种半双工通信方式(通信方式有单工、半双工、全双工)
写入管道的数据在内存中
有名:可以在任意两个进程中通信

在这里插入图片描述

把管道用作标准输入和输出

函数原型:
在这里插入图片描述
代码:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
参考资料:
https://blog.csdn.net/qq_35456045/article/details/104806703

https://blog.csdn.net/silent123go/article/details/71108501


信号量

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

信号量:特殊的变量 值>=0 减一代表获取资源 (p操作)加一代表释放资源(v操作)
同步进程

临界资源 只允许一个进程访问的资源
临界区 访问临界资源的代码段
两个进程使用打印机的情况

1.信号量的值如果只取 0,1,将其称为二值信号量
2.如果信号量的值大于 1,则称之为计数信号量

访问打印机的模拟
在这里插入图片描述


命令行: man semget
semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。其定义如下:
在这里插入图片描述

#include <sys/sem.h> int semget ( key_t key, int num_sems,int sem_flags ) ;
key参数是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯一地标识一个文件一样。要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。
num_sems参数指定要创建/获取的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,则可以把它设置为0。

IPC|CREATE 和IPC_EXCL标志来确保创建一组i新的,唯一的信号量集。
成功返回一个正整数值,他是信号量集的标识符
失败返回-1。


命令行: man semctl 控制信号量
在这里插入图片描述

sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的信号量集.sem_num参数指定被操作的信号量在信号量集中的编号。command参数指定要执行的命令。有的命令需要调用者传递第4个参数。第4个参数的类型由用户自己定义,但sys/sem.h头文件给出了它的推荐格式,具体如下:
在这里插入图片描述

成功返回0,失败返回-1;

在这里插入图片描述
cmd选项: SETVAL : 成功返回0;在这里插入图片描述IPC_RMID在这里插入图片描述

IPC_RMID: destroy 时用到:
if (semctl(semid, 0, IPC_RMID) == -1)
			{
				printf("destroy failed\n");
			}

命令行:man semop (v操作和p操作)

在这里插入图片描述

sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的目标信号量集。sem_ops参数指向一个sembuf结构体类型的数组,sembuf结构体的定义如下:

在这里插入图片描述
sem_flag 可选值是IPC_NOWAIT 和SEM_UNDO.
sem_fg成员的影响。sem_flg的可选值是IPC_NOWAIT和SEM_UNDO。IPC_NOWAIT的含义是,无论信号量操作是否成功,semop调用都将立即返回,这类似于非阻塞IO操作。SEM_UNDO的含义是,当进程退出时取消正在进行的semop操作。

void sem_p()
		{
			struct sembuf a; 
			a.sem_num = 0;信号集信号量的编号为0;
			a.sem_op = -1;设置成-1,说明对信号量值进行减操作,期望获得信号量。
			a.sem_flag = SEM_UNDO;当进程退出时取消正在进行的semop操作。
			if (semop(semid, &a, 1) == -1)
			{
				printf("semop p failed\n");
			}

		}

v操作:


void sem_v()
		{
			struct sembuf a;
			a.sem_num = 0;
			a.sem_op = 1; 设置为1代表信号量的值sem_val增加sem_op.
			a.sem_flag = SEM_UNDO;
			if (semop(semid, &a, 1) == -1)
			{
				printf("semop v failed\n");
			}
		}

例题:设置A和B两个进程,同时打印A和B,打印保持AABBAA…的同步。
原码如下:
sem…c

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/sem.h>
#include<assert.h>
static int semid = -1;//semget()失败返回-1;
void sem_init()
{
	semid = semget((key_t)1234, 1, IPC_CREATE | IPC_EXCL | 0600);//nsems:信号量集的个数为1 ;
	if (semid == -1)
	{
		semid = semget((key_t)1234, 1, 0600);
		if (semid == -1)
		{
			printf("create sem failed\n");
		}
		else {
			union semun a;
			a.val = 1;
			if (semctl(semid, 0, SETVAL, a) == -1)
			{

				printf("semctl setval failed\n");
			}
		}
		void sem_p()
		{
			struct sembuf a;
			a.sem_num = 0;
			a.sem_op = -1;
			a.sem_flag = SEM_UNDO;
			if (semop(semid, &a, 1) == -1)
			{
				printf("semop p failed\n");
			}

		}
		void sem_v()
		{
			struct sembuf a;
			a.sem_num = 0;
			a.sem_op = 1;
			a.sem_flag = SEM_UNDO;
			if (semop(semid, &a, 1) == -1)
			{
				printf("semop v failed\n");
			}
		}
		void sem_destroy()
		{
			if (semctl(semid, 0, IPC_RMID) == -1)
			{
				printf("destroy failed\n");
			}
		}
			
	}

}

a.c:

int main()
{

	sem_init();
	for (int i = 0; i < 5; i++)
	{
		sem_p();
		printf("A");
		fflush(stdout);
		int n = rand() % 3;
		sleep(n);
		printf("A");
		fflush(stdout);
		sem_v();
		sleep(n);
		
	}
	sleep(10);
	sem_destroy();
	exit(0);
}

b.c


sem.h

union semun
{
int val;
};
void sem_init();
void sem_p();
void sem_v();
void sem_destroy();

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

共享内存

共享内存为多个进程之间共享和传递数据提供了一种有效的方式。共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由 malloc 分配的一样。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。

ipcs -m 查看共享内存状态

在这里插入图片描述

int shmget(key_t key, size_t size, int shmflg);

/*
6. shmget()用于创建或者获取共享内存
7. shmget()成功返回共享内存的 ID, 失败返回-1
8. key: 不同的进程使用相同的 key 值可以获取到同一个共享内存
9. size: 创建共享内存时,指定要申请的共享内存空间大小
10. shmflg: IPC_CREAT IPC_EXCL
11. */

在这里插入代码片

void* shmat( int shmid, const void *shmaddr, int shmflg);

. /*
15. shmat()将申请的共享内存的物理内存映射到当前进程的虚拟地址空间上
16. shmat()成功返回返回共享内存的首地址,失败返回 NULL
17. shmaddr:一般给 NULL,由系统自动选择映射的虚拟地址空间
18. shmflg: 一般给 0, 可以给 SHM_RDONLY 为只读模式,其他的为读写
19. */

int shmdt( const void *shmaddr);

/*
23. shmdt()断开当前进程的 shmaddr 指向的共享内存映射
24. shmdt()成功返回 0, 失败返回-1
25. */

int shmctl( int shmid, int cmd, struct shmid_ds *buf);

/*
29. shmctl()控制共享内存
30. shmctl()成功返回 0,失败返回-1
31. cmd: IPC_RMID
32. */


共享内存创建后的显示

在这里插入图片描述

设置共享内存******************
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
int main()
{
	int shmid = shmget((key_t)1234, 128, IPC_CREATE | 0600);
	if (shmid == -1)
	{
		exit(1);

	}
	char* s = (char*)shmat(shmid, NULL, 0);
	if (s == (char*)-1)
	{
		exit(1);
	}
	strcpy(s, "hello");
	shmdt(s);
	exit(0);


}
**********打印共享内存********
int main()
{
	int shmid = shmget((key_t)1234, 128, IPC_CREATE | 0600);
	if (shmid == -1)
	{
		exit(1);

	}
	char* s = (char*)shmat(shmid, NULL, 0);
	if (s == (char*)-1)
	{
		exit(1);
	}
	printf("read :%s\n", s);
	shmdt(s);
	exit(0);

}

信号量控制读和写同步:
在这里插入图片描述


if语句中的0替换为i

小练习:有三个进程同时进行打印,A进程打印a,B进程打印b,C进程打印c,请模拟输出以下结果:

abcabcabcabc

在这里插入图片描述

原理是什么?

因为是三个进程同时打印,想要ABCABC的结果,必须需信号量去控制

所以我们要准备三个信号来控制三个进程,信号值用数组(1,0,0)去表示。

我们给出的解决方案如下表:
在这里插入图片描述

我们默认ps1为1,所以是A进程可以使用资源(输出缓冲区)。
当A打印结束时VS2+1变为1,所以现在是(0,1,0)状态,也就是说,ps2可以减一动作(B进程可以打印b了),以此类推。

a.c代码:
在这里插入图片描述b.c代码:
在这里插入图片描述
c.c代码:
在这里插入图片描述
sem.c代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
sem.h代码:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这是一个比较典型的进程通信的例子。我会尽力解答你的问题。 首先,我们需要使用共享内存来实现进程通信。在 Linux 系统中,可以使用 System V IPC(Inter-Process Communication)机制来实现共享内存操作。具体来说,我们需要使用以下几个函数: - `shmget`:创建共享内存区域,或获取已有共享内存区域的标识符。 - `shmat`:将共享内存区域映射到进程的地址空中。 - `shmdt`:解除共享内存区域和进程地址空的映射关系。 - `shmctl`:控制共享内存区域的状态,如删除共享内存区域。 接下来,我们可以使用 fork() 函数创建父进程和三个子进程。父进程可以使用 shmget() 函数创建一个长度为 10 的整型数组的共享内存区域,并使用 shmat() 函数将其映射到自己的地址空中。然后,父进程可以使用 fork() 函数创建三个子进程,每个子进程都可以使用 shmat() 函数将共享内存区域映射到自己的地址空中。这样,父进程和三个子进程就可以通过共享内存来进行通信了。 具体实现如下: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #define SHM_SIZE 10 int main() { int shmid; int *shm; int i; // 创建共享内存区域 shmid = shmget(IPC_PRIVATE, SHM_SIZE * sizeof(int), IPC_CREAT | 0666); if (shmid == -1) { perror("shmget"); exit(1); } // 映射共享内存区域到父进程地址空中 shm = (int *)shmat(shmid, NULL, 0); if (shm == (int *)-1) { perror("shmat"); exit(1); } // 创建三个子进程 for (i = 0; i < 3; i++) { if (fork() == 0) { // 子进程 int j; int start, end; switch (i) { case 0: // 子进程1往共享内存中存放数据1~10 for (j = 0; j < SHM_SIZE; j++) { shm[j] = j + 1; } printf("子进程1往共享内存中存放数据1~10\n"); break; case 1: // 子进程2从共享内存中读取数据1~5 start = 0; end = SHM_SIZE / 2 - 1; printf("子进程2从共享内存中读取数据1~5:"); for (j = start; j <= end; j++) { printf("%d ", shm[j]); } printf("\n"); break; case 2: // 子进程3从共享内存中读取数据6~10 start = SHM_SIZE / 2; end = SHM_SIZE - 1; printf("子进程3从共享内存中读取数据6~10:"); for (j = start; j <= end; j++) { printf("%d ", shm[j]); } printf("\n"); break; default: break; } // 解除共享内存区域和进程地址空的映射关系 if (shmdt(shm) == -1) { perror("shmdt"); exit(1); } // 子进程结束 exit(0); } } // 等待子进程结束 for (i = 0; i < 3; i++) { wait(NULL); } // 删除共享内存区域 if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl"); exit(1); } // 解除共享内存区域和进程地址空的映射关系 if (shmdt(shm) == -1) { perror("shmdt"); exit(1); } // 父进程结束 return 0; } ``` 在上述代码中,我们使用了 switch 语句来区分不同的子进程。子进程1往共享内存中存放数据1~10,子进程2从共享内存中读取数据1~5,子进程3从共享内存中读取数据6~10。父进程等待三个子进程结束后,删除共享内存区域,并解除共享内存区域和进程地址空的映射关系。 需要注意的是,父子进程之的同步关系需要使用进程通信的机制来实现。常见的进程通信的机制包括信号、消息队列、管道共享内存等。在本例中,我们使用了共享内存来实现进程通信,因此不需要额外使用其他的进程通信的机制来实现同步关系。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值