下面给出了本次实验的具体要求,简单来说就是使用Linux当中fork( )系统调用加上pipe管道通信方式实现经典的生产者—消费者问题;
在看代码之前,我们先要掌握几个最基本的知识,什么是生产者-消费者问题,什么是fork( )系统调用,什么是pipe机制,什么是进程,以及父子进程的相关概念;
(1)什么是生产者-消费者问题?
生产者消费者问题,也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。其中生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
我们可以用一个生活当中的例子说明:
一个箱子里面有四个空格,每个空格的大小刚好可以存放一个物品,箱子旁边有两个人,一个不断地往箱子的空格当中存放物品,而另外一个人则不断的从箱子的空格当中拿走物品,我们将箱子看作缓冲区,放物品的人看作为生产者,拿取物品的人看作为消费者,这就是一个典型的生产者-消费者问题;
我们要想处理好这个问题,关键在于解决以下几个问题:
箱子满的情况下生产者就不能继续往里面放置了,箱子空的情况下消费者就不能再往外拿取了;
(2)关于fork( )系统调用:
大家只需了解几个关键点即可,如果想要完整了解这个函数,请查阅详细资料:
1.fork( )函数用于创建一个新的进程,调用fork( )函数的进程我们称之为父进程,新创建的进程我们称之为子进程;
2.fork创建的子进程几乎与父进程完全相同,你可以将其理解为父进程的一个复制品,但是两者的PID肯定是不一样的,并且新创建的子进程将从fork函数调用的下一行开始执行;我们看下面这个例子:
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("父进程正在执行!!!\n");
fork( );
printf("第一次fork!!!\n");
fork( );
printf("第二次fork!!!\n");
fork( );
printf("第三次fork!!!\n");
}
这一段代码的”可能“执行结果是这样的(这里为什么说可能后面会解释):
这个执行结果看起来非常的混乱,这是因为父进程在创建了子进程之后还会继续执行自己接下来的代码,而子进程在被创建之后又会继续从fork( )下面一行的代码执行,我们在调用了三个fork( )的情况下,子进程的创建与执行相互嵌套,显得十分混乱,我给出下面的图示进行进一步解释:
我们将程序当中的四次printf函数标号为printf1,printf2,printf3,printf4
将程序当中的三次fork函数标号为fork1,fork2,fork3
下面这张图就详细介绍了整个程序运行过程当中每个进程fork函数与printf函数的使用情况:
我们可以看到包括父进程在内,该程序一共包含有八个进程,一共创建了七个子进程,当然这七个子进程并不是都由父进程创建的,而是嵌套创建的,例如father通过fork1创建了child1,而child1通过fork2创建了child1-1,child1-1又通过fork3创建了child1-1-1,并且每个进程不是都从程序的开头开始执行,而是从创建自己的fork函数的下一行开始执行,这才有了执行结果当中的打印情况;
3.fork的返回值:对于调用fork( )函数的父进程,如果接收返回值为负数,则表示创建子进程失败了,如果接受返回值为正数,那么该正数正是子进程的PID,对于新创建的子进程,它所接收到的返回值为0;
下面这段代码的输出结果可能是怎样的?
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if(pid < 0) {
printf("failed to create new process!!!\n");
}
else if(pid == 0) {
printf("hello from child process!!!\n");
}
else {
printf("hello from father process!!! my child process ID is %d\n" , pid);
}
}
下面我给出了其中一种运行结果:
为什么这么说呢?这就涉及到进程的一些知识了,我们往下看:
(3)关于进程:
关于进程的基础知识课本上已经有非常详细的讲解了,我在这里强调一个内容,那就是进程并行满足os的异步性特点,我们永远无法预测进程的前进情况,哪怕一个进程很早之前创建,他的完成也有可能远远慢于新创建的进程,因此对于刚才的代码,在子进程被创建之后,他和父进程谁提前打印出提示语句其实是不确定的,运行结果也有可能是这样的:
hello from child process!!!
hello from father process!!! my child process ID is 2501
(4)关于pipe管道通信:
pipe管道通信方式是Linux当中一种最简单的通信方式,用于实现进程之间的通信,并且由于pipe管道通信是利用内核进行通信,因而往往用于两个具有亲缘关系的进程之间,关于pipe,大家只需要了解几个基本要点即可,详细内容请自行查阅资料;
1.管道是半双工的,因此我们往往会创建两个管道分别用于读写通信,pipe_fd[0]用于读,pipe_fd[1]用于写;
2.管道的创建函数:
#include <unistd.h>
int pipe(int pipefd[2]);
关于pipe函数的返回值:
返回值为负数表示管道创建失败,返回值为零表示管道创建成功;
为了保证不发生内存泄漏,我们在真正使用管道时,往往会暂时关闭那些不用的管道,只保证需要使用的管道畅通;
下面给出一段管道的示例代码,实现父进程通过管道向子进程传送数据,代码比较简单,请大家自己理解:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
fprintf(stderr, "pipe creation failed\n");
return 1;
}
int pid = fork();
if (pid < 0) {
fprintf(stderr, "fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写端
char buffer[256];
read(pipefd[0], buffer, sizeof(buffer));
printf("Child process received: %s\n", buffer);
close(pipefd[0]); // 关闭读端
} else {
// 父进程
close(pipefd[0]); // 关闭读端
char message[] = "Hello from parent process!";
write(pipefd[1], message, sizeof(message));
close(pipefd[1]); // 关闭写端
}
return 0;
}
有了以上知识作为基础,我们正是给出生产者与消费者问题的代码:
#include "sys/types.h"
#include "sys/file.h"
#include "unistd.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char r_buf[4]; //创建一个读缓冲区;
char w_buf[4]; //创建一个写缓冲区;
int pipe_fd[2]; //创建管道,pipe[0]表示读管道,pipe表示写管道;
pid_t pid1, pid2, pid3, pid4;
int producer(int id); //生产者函数;
int consumer(int id); //消费者函数;
int main(int argc, char** argv) {
if (pipe(pipe_fd) < 0) {
//pipe函数用于创建管道当创建成功时返回0,否则返回-1,因此当返回值小于0时,打印管道创建失败的提示;
printf("pipe creat error \n");
exit(-1);
//exit函数返回值为0时表示程序正常退出,返回值为其他值时表示程序发生错误;
//调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,
//关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。
}
else {
printf("pipe is created successfully! \n");
//fork函数用于复制进程,在父进程当中fork函数会返回子进程的pid数值,
//在子进程当中fork函数会返回0;
if ((pid1 = fork()) == 0)
producer(1);
if ((pid2 = fork()) == 0)
producer(2);
if ((pid3 = fork()) == 0)
consumer(1);
if ((pid4 = fork()) == 0)
consumer(2);
}
//关闭读写管道,防止出现生产者函数或消费者函数一直在等待的情况:
close(pipe_fd[0]);
close(pipe_fd[1]);
int i, pid, status;
//父进程为子进程回收残留的数据结构和信息,防止子进程变成僵尸进程或孤儿进程:
for (i = 0; i < 4; i++)
pid = wait(&status);
exit(0);
}
int producer(int id) {
printf("producer %d is running! \n", id);
close(pipe_fd[0]); //关闭读管道;
int i = 0;
for (i = 0; i < 10; i++) {
sleep(3);
if (id == 1)
strcpy(w_buf, "aaa\0");
else
strcpy(w_buf, "bbb\0");
if (write(pipe_fd[1], w_buf, 4) == -1)
//write表示读入函数,将读缓冲当中的内容读入到管道当中,
//如果读入顺利的话返回值为读入的字节数,否则为-1;
printf("write to pipe error\n");
}
close(pipe_fd[1]);
//关闭写管道,并结束producer函数;
printf("producer %d is over! \n", id);
exit(id);
}
int consumer(int id) {
printf("consumer %d is running! \n", id);
//关闭写管道:
close(pipe_fd[1]);
if (id == 1)
strcpy(w_buf, "ccc\0");
else
strcpy(w_buf, "ddd\0");
while (1) {
sleep(1);
strcpy(r_buf, "eee\0");
if (read(pipe_fd[0], r_buf, 4) == 0)
//read()会把参数fd所指的文件传送count 个字节到 buf指针所指的内存中。
//返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。
//若参数count 为0, 则read()不会有作用并返回0。
break;
printf("consumer %d get %s,while the w_buf is %s\n", id, r_buf, w_buf);
}
close(pipe_fd[0]);
//关闭读管道并结束consumer函数:
printf("consumer %d is over! \n", id);
exit(id);
}
代码的运行结果如下: