成功完成上一次平时作业后,成就感还是蛮大滴,兴趣也更浓厚了。收到了新一次的作业,便迫不及待地尝试、研究了起来。
本次作业,针对Linux进程间通信。操作系统提供的进程间的通信主要有两种方式:一种是较低级的、间接的通信方式,它是利用同步机构(如锁和信号量),通过共享的存储器来实现的,因装置常常限制为一个字或几个字的信息存储,因而进程间传递的只能是单一的信号;另一种是进程通信(IPC)的机制,它能传递大量的信息,是实现进程间更有效同步的直接的进程通信方式。
下面通过具体的题目来深入探讨吧。
1、利用管道实现多进程间通信
利用管道实现进程间的通信,顾名思义是在进程之间搭建一个通道。然后从通道的一段放入数据,另一端取出数据,从而实现一方发出信息、另一方接收信息的功能,即进程间的通信。
管道通信具有单向,无结构,先进先出的字节流特点。根据管道提供的应用接口的不同,可将管道分为无名管道和命名管道。
(1)无名管道
无名管道是一种特殊类型的文件,完全由操作系统管理和维护。因为其存储位置只有亲缘关系的进程知道,所以只能用于亲缘关系的进程之间的通信。
无名函数的创建函数为int pipe(int fd[2]),该函数定义于头文件unistd.h中。创建一个无名管道fd包含2个文件描述符的数组,fd[0]用于读,fd[1]用于写若成功返回0,否则返回-1。
一般某个进程用于读,另一个进程用于写,用于读的进程需要close(fd[1]),用于写的进程需要close(fd[0])。
若只想读取某个进程的结果,或写入某个进程的输入可以使用popen函数。FILE *popen(const char *command,const char *type),定义于头文件stdio.h中,command执行的shell命令,type命令的输入输出类型(r:打开命令执行的标准输出w:打开命令执行的标准输入),成功返回文件I/O流否则返回NULL。
这里,正序输出了当前目录下所有文件和子目录。
(2)命名管道
命名管道作为一个特殊的文件保存在文件系统中,任意一个进程都可以对命名管道进行读写,只要进程具有相应的读写权限。
命名管道通过int mkfifo(const char *pathname,mode_t mode)函数创建,pathname为文件路径名,mode为存取权限,若成功返回0,否则返回-1。;
以上就是利用管道实现多进程间通信的基本内容及实例程序啦,感觉这一部分要多接触,尝试不同的情况。比如,command执行的shell命令、存取权限mode,每一种对应有什么效果,都是要亲自尝试,才能理解、掌握的呀。
2、利用信号量机制实现生产者消费者问题(用进程通信进行模拟)
为了实现进程间正确的协作,操作系统提供了实现进程协作的措施和方法,称为同步机构。同步机构有两种,一种有锁和上锁、开锁操作,另一种是称为信号灯(信号量)的同步机构,包括信号灯和P、V操作。使用信号灯能方便地解决临界区问题。
信号灯是一个确定的二元组(s,q),s是一个具有非负初值的整型变 量,q是一个初始状态为空的队列。操作系统利用信号灯的状态对并发进程和共享资源进行控制和管理。
信号灯是整型变量:
变量值>=0 时,表示绿灯,进程执行;
变量值< 0 时,表示红灯,进程停止执行。
P操作:
对信号灯s的 p操作记为 p(s),p(s)是一个不可分割的原语操作,即取信号灯值减1,若相减结果为负,则调用p(s)的进程被阻,并插入到该信号灯的等待队列中,否则可以继续执行。
V操作:
对信号灯s的 v操作记为 v(s),v(s)是一个不可分割的原语操作,即取信号灯值加1,若相加结果大于零,进程继续执行,否则,要帮助唤醒在信号灯等待队列上的一个进程。
生产者-消费者问题是一种典型的同步问题。计算机系统中的每个进程都可以消费(使用)或生产(释放)某类资源,这些资源可以是硬资源(如主存缓冲区、外设或处理机),也可以是软资源(如临界区、消息等)。当系统中进程使用某一资源时.可以看做是消耗,且将该进程称为消费者。而当某个进程释放资源时,则它就相当于一个生产者。当两个进程之间互通信件时,也可抽象为一一个发信件进程产生消息,然后把它放置到缓冲存储器中去,与此平行地,一个读消息进程从缓冲存储器中移走信息并处理(消费)它。
同步关系:
生产者:当有界缓冲区中无空位置时,要等待;
向有界缓冲区放入物品后,要发消息。
消费者:当有界缓冲区中无物品时,要等待;
从有界缓冲区取出物品后,要发消息。
问题图示:
信号灯设置:
两个同步信号灯:
sb :表示空缓冲区的数目,初值 = n
sa : 表示满缓冲区(即信息)的数目,初值 = 0
一个互斥信号灯:
mutex:表示有界缓冲区是否被占用,初值 = 1
同步描述:
伪代码描述:
具体实现代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/types.h>
#define SHM_MODE 0600
#define SEM_MODE 0600
#define SEM_FULL 0
#define SEM_EMPTY 1
#define MUTEX 2
#define MAX_BUFFER_SIZE 10
const int N_CONSUMER = 2; //消费者数量
const int N_PRODUCER = 2; //生产者数量
const int N_BUFFER = 10; //缓冲区容量
const int N_WORKTIME = 6; //工作次数
int shm_id = -1;
int sem_id = -1;
pid_t child;
pid_t parent;
struct my_buffer
{
int head;
int tail;
char str[MAX_BUFFER_SIZE];
int is_empty;
}*buf;
char getRandChar() //得到A~Z的一个随机字母
{
char letter;
srand((unsigned)(getpid()));
letter = (char)((rand() % 26) + 'A');
return letter;
}
//semSetId 表示信号量集合的 id
//semNum 表示要处理的信号量在信号量集合中的索引
void waitSem(int semSetId,int semNum) //P操作
{
struct sembuf sb;
sb.sem_num = semNum;
sb.sem_op = -1;//表示要把信号量减一
sb.sem_flg = SEM_UNDO;//
//第二个参数是 sembuf [] 类型的,表示数组
//第三个参数表示 第二个参数代表的数组的大小
if(semop(semSetId,&sb,1) < 0){
perror("waitSem failed");
exit(1);
}
}
void sigSem(int semSetId,int semNum) //V操作
{
struct sembuf sb;
sb.sem_num = semNum;
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
//第二个参数是 sembuf [] 类型的,表示数组
//第三个参数表示 第二个参数代表的数组的大小
if(semop(semSetId,&sb,1) < 0){
perror("waitSem failed");
exit(1);
}
}
void init()
{
if((shm_id = shmget(IPC_PRIVATE,MAX_BUFFER_SIZE,SHM_MODE)) < 0) //缓冲区分配以及初始化
{
perror("create shared memory failed");
exit(1);
}
buf = shmat(shm_id, 0, 0); //将申请的共享内存附加到申请通信的进程空间
if (buf == (void*)-1)
{
perror("add buffer to using process space failed!\n");
exit(1);
}
if((sem_id = semget(IPC_PRIVATE,3,SEM_MODE)) < 0) //创建信号量
{
perror("create semaphore failed");
exit(1);
}
if(semctl(sem_id,SEM_FULL,SETVAL,0) == -1) //将索引为0的信号量设置为0
{
perror("sem set value error! \n");
exit(1);
}
if(semctl(sem_id,SEM_EMPTY,SETVAL,10) == -1) //将索引为1的信号量设置为10
{
perror("sem set value error! \n");
exit(1);
}
if(semctl(sem_id,MUTEX,SETVAL,1) == -1) //将索引为2的信号量设置为1
{
perror("sem set value error! \n");
exit(1);
}
buf -> head = 0;
buf -> tail = 0;
buf -> is_empty = 1;
}
void main()
{
init();
for(int i = 0; i < N_PRODUCER; i++)
{
parent = fork();
if(parent < 0)
{
perror("the fork failed");
exit(1);
}
else if(parent == 0)
{
buf = shmat(shm_id, 0, 0); //将申请的共享内存附加到申请通信的进程空间
if (buf == (void*)-1)
{
perror("add buffer to using process space failed!\n");
exit(1);
}
int count = 0;
for(int j = 0; j < N_WORKTIME; j++)
{
waitSem(sem_id, SEM_EMPTY);
waitSem(sem_id, MUTEX);
sleep(1);
printf("生产者进程%d,PID = %d\n", i + 1, getpid());
char c = getRandChar();
buf -> str[buf->tail] = c;
buf -> tail = (buf->tail + 1) % MAX_BUFFER_SIZE;
buf -> is_empty = 0;
int p;
printf("缓冲区数据:");
p = (buf->tail-1 >= buf->head) ? (buf->tail-1) : (buf->tail-1 + MAX_BUFFER_SIZE);
for (p; !(buf -> is_empty) && p >= buf -> head; p--)
{
printf("%c", buf -> str[p % MAX_BUFFER_SIZE]);
}
printf("\n生产者 %d 放入 '%c'. \n\n", i + 1, c);
fflush(stdout);
sigSem(sem_id, MUTEX);
sigSem(sem_id, SEM_FULL);
}
shmdt(buf);
exit(0);
}
}
for(int i = 0; i < N_CONSUMER;i++)
{
child = fork();
if(child < 0)
{
perror("the fork failed");
exit(1);
}
else if(child == 0)
{
int count = 0;
buf = shmat(shm_id, 0, 0);
if (buf == (void*)-1)
{
perror("add buffer to using process space failed!\n");
exit(1);
}
for(int j = 0; j < N_WORKTIME; j++)
{
waitSem(sem_id, SEM_FULL);
waitSem(sem_id, MUTEX);
sleep(1);
printf("消费者进程%d,PID = %d\n", i + 1, getpid());
char lt = buf -> str[buf -> head];
buf -> head = (buf -> head + 1) % MAX_BUFFER_SIZE;
buf -> is_empty = (buf->head == buf->tail);
int p;
printf("缓冲区数据:");
p = (buf -> tail - 1 >= buf -> head) ? (buf -> tail-1) : (buf -> tail - 1 + MAX_BUFFER_SIZE);
for (p; !(buf -> is_empty) && p >= buf -> head; p--)
{
printf("%c", buf -> str[p % MAX_BUFFER_SIZE]);
}
printf("\n消费者 %d 取出 '%c'. \n\n", i + 1, lt);
fflush(stdout);
sigSem(sem_id,MUTEX);
sigSem(sem_id,SEM_EMPTY);
}
shmdt(buf); //将共享段与进程之间解除连接
exit(0);
}
}
//主进程最后退出
while (wait(0) != -1); //将共享段与进程之间解除连接
shmdt(buf); //对共享内存区执行控制操作
shmctl(shm_id,IPC_RMID,0); //当cmd为IPC_RMID时,删除该共享段
shmctl(sem_id,IPC_RMID,0);
printf("主进程运行结束!\n");
fflush(stdout);
exit(0);
}
程序运行截图:
·遇到的一个小问题
查阅资料得知,C语言中使用const不能直接当成常量,const修饰的变量一般只当成“只读的”,而不是将其作为一个常量。所以,在全局声明一个数组,不能使用const变量做下标。将const int MAX_BUFFER_SIZE=10;改为#define MAX_BUFFER_SIZE 10 问题解决。
终于成功地利用信号量机制实现了生产者消费者问题,超开心!不得不承认,这个过程还是很考验人的。遇到了一些大大小小的问题,一直弄不好感到有些烦躁,甚至萌生了放弃的念头。不过,好在自己还是坚持了下来,最终取得胜利。分析一下,感到实现过程有些困难,主要是因为这个问题本身稍显复杂,再加上在理论知识学习过程中,写的是伪代码,而非实际的代码。很多函数需要通过查阅资料,一点点理解、应用,才能将伪代码转化为实际的代码。这也说明,平时自己动手操作是十分重要的,只有这样才能真正做到把所学知识融会贯通。未来也要继续努力,攻克一个个难题,不断进步呀。
参考资料
【1】https://www.cnblogs.com/CodingUniversal/p/7400219.html
【2】https://blog.csdn.net/qq_31490151/article/details/78984593
【3】https://blog.csdn.net/zhaohaibo_/article/details/89322902