一、实验目的
1)理解线程/进程的通信机制和编程;
2)理解线程/进程的死锁概念和如何解决死锁
二、实验内容
1)在Ubuntu 或Fedora 环境使用创建一对父子进程,使用共享内存的方式实现进程间的通信。父进程提供数据(1-100,递增),子进程读出来并显示。
2)(考虑信号通信机制)在Ubuntu 或Fedora 环境创建父子2 个进程A,B。进程A 不断获取用户从键盘输入的字符串或整数,通过信号机制传给进程B。如果输入的是字符串,进程B 将其打印出来;如果输入的是整数,进程B 将其累加起来,并输出该数和累加的和。当累加和大于100 时结束子进程,子进程输出“My work done!”后结束,然后父进程也结束。
3)在windows 环境使用创建一对父子进程,使用管道(pipe)的方式实现进程间的通信。父进程提供数据(1-100,递增),子进程读出来并显示。
4)(考虑匿名管道通信)在windows 环境下创建将CMD 控制台程序封装为标准的windows 窗口程序。
5)在windows 环境下,利用高级语言编程环境(限定为VS 环境或VC 环境)调用CreateThread 函数哲学家就餐问题的演示。要求:(1)提供死锁的解法和非死锁的解法;(2)有图形界面直观显示哲学家取筷子,吃饭,放筷子,思考等状态。(3)为增强结果的随机性,各个状态之间的维持时间采用随机时间,例如100ms-500ms 之间。
三、实验过程
1)共享内存实现进程通信
①任务环境
Ubuntu版本:16.04 64位
Linux内核版本:4.5.0-29-generic
②何为共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址
③函数
声明在头文件 sys/shm.h 中
④shmget函数:该函数用来创建共享内存
1. int shmget(key_t key, size_t size, int shmflg);
key_t key: 它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数)。调用失败返回-1.
size_t size: size以字节为单位指定需要共享的内存容量
int shmflg: 共享内存的权限标志
⑤shmat函数: shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间
1. void *shmat(int shm_id, const void *shm_addr, int shmflg);
shm_id: shm_id是由shmget函数返回的共享内存标识。
shm_addr: shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
shmflg: shm_flg是一组标志位,通常为0。
⑥shmdt函数: 该函数用于将共享内存从当前进程中分离,使该共享内存对当前进程不再可用。
1. int shmdt(const void *shmaddr);
参数shmaddr: 是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.
⑦shmctl函数: 与信号量的semctl函数一样,用来控制共享内存
1. int shmctl(int shm_id, int command, struct shmid_ds *buf);
shm_id: 是shmget函数返回的共享内存标识符。
command: 是要采取的操作,它可以取下面的三个值 :
IPC_STAT: 把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID: 删除共享内存段
buf: 是一个结构指针,它指向共享内存模式和访问权限的结构。
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
2)Linux父子进程管道通信
①管道的特点:
1. 管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
2. 管道只允许单向通信。
3. 管道内部保证同步机制,从而保证访问数据的一致性。
4. 面向字节流
5. 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。
②注意事项:
1. 如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
2. 如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
3. 而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到⽂件末尾⼀样。
3)哲学家问题
任务环境:Windows 10 x64 + Visual Studio 2017 Enterprise
饥饿解法:
1. #define N 5
2.
3. void philosopher(int i)
4. {
5. while (true) {
6. think() ;
7. take_fork(i) ;
8. take_fork((i+1)%N) ;
9. eat() ;
10. put_fork(i) ;
11. put_fork((i+1)%N) ;
12. }
13. }
非死锁解法:
#define N 5 // 哲学家的数目
#define LEFT (i+N-1)%N // i的左邻居编号
#define RIGHT (i+1)%N // i的右邻居编号
#define THINKING 0 // 哲学家在思考
#define HUNGRY 1 // 哲学家试图拿起叉子
#define EATING 2 // 哲学家就餐
typedef int semaphore ; // 信号量是一种特殊的整型数据
int state[N] ; // 数组用来跟踪记录每位哲学家的状态,全局变量初始化为0,即是说哲学家的初始化状态是THINKING ;
semaphore mutex = 1 ; // 临界区的互斥
semaphore s[N] ; // 每个哲学家一个信号量
void philosopher(int i) { // i:哲学家编号,从0到i-1
while (true) {
think() ;
take_forks(i) ;
eat() ;
put_forks(i) ;
}
}
void take_forks(int i) {
wait(mutex) ; // 进入临界区
state[i] = HUNGRY ; // 记录哲学家i处于饥饿的状态
test(i) ; // 尝试获得2把尺子
signal(mutex) ; // 离开临界区
wait(s[i]) ; // 如果得不到需要的尺子则阻塞
}
void put_forks(int i) {
wait(mutex) ; // 进入临界区
state[i] = THINKING ; // 记录哲学家i已经就餐完毕
test(LEFT) ; // 检查左边的邻居现在可以吃吗
test(RIGHT) ; // 检查右边的邻居现在可以吃吗
signal(mutex) ; // 离开临界区
}
void test(int i) {
if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) {
state[i] = EATING ;
signal(s[i]) ;
}
}
四、实验结果
1)共享内存实现进程通信
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <errno.h>
#define TEXT_SZ 101
struct shared_use_st
{
int written; //作为一个标志,非0:表示可读,0表示可写
int text[TEXT_SZ]; //记录写入和读取的文本
};
int main()
{
int pRunning = 1; //程序是否继续运行的标志(父)
int cRunning = 1; //程序是否继续运行的标志(子)
void *shm = NULL; //分配的共享内存的原始首地址
struct shared_use_st *shared; //指向shm
char buffer[BUFSIZ + 1]; //用于保存输入的文本
int shmid; //共享内存标识符
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if (shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if (shm == (void *)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
//设置共享内存
shared = (struct shared_use_st *)shm;
shared->written = 0;
pid_t pid = fork();
if (pid < 0)
{
//printf('子进程创建失败');
fprintf(stderr, "can't fork ,error %d\n", errno);
exit(1);
}
else if (pid == 0) //子进程
{
while (cRunning) //读取共享内存中的数据
{
//没有进程向共享内存定数据有数据可读取
if (shared->written != 0)
{
int i = 0;
while (i < 100)
{
printf("child read: %d\n", shared->text[i]);
i++;
}
//读取完数据,设置written使共享内存段可写
shared->written = 0;
//退出循环(程序)
cRunning = 0;
}
else //有其他进程在写数据,不能读取数据
sleep(1);
}
}
else //父进程
{
int i = 1;
while (pRunning) //向共享内存中写数据
{
//数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
while (shared->written == 1)
{
sleep(1);
}
//向共享内存中写入数据
shared->text[i - 1] = i;
printf("parent write: %d\n", shared->text[i - 1]);
i++;
if (i > 100)
{
//写完数据,设置written使共享内存段可读,退出循环(程序)
shared->written = 1;
pRunning = 0;
}
}
}
//把共享内存从当前进程中分离
if (shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
//删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
2)Linux父子进程管道通信
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe error \n");
return 1;
}
pid_t id = fork();
if (id > 0) //parent
{
int i = 1;
close(fd[0]);
while (i < 101)
{
write(fd[1], &i, 1);
//每隔 1 秒打印一次
sleep(1);
i++;
}
}
else if (id == 0) //child
{
close(fd[1]);
int msg;
int j = 0;
while (j < 100)
{
ssize_t s = read(fd[0], &msg, sizeof(msg));
printf("%d\n", msg);
j++;
}
}
else
{ //error
perror("fork error \n");
return 2;
}
return 0;
}
3)哲学家问题
饥饿解法:
#define N 5
void philosopher(int i)
{
while (true) {
think() ;
take_fork(i) ;
take_fork((i+1)%N) ;
eat() ;
put_fork(i) ;
put_fork((i+1)%N) ;
}
}
非死锁解法:
#define N 5 // 哲学家的数目
#define LEFT (i+N-1)%N // i的左邻居编号
#define RIGHT (i+1)%N // i的右邻居编号
#define THINKING 0 // 哲学家在思考
#define HUNGRY 1 // 哲学家试图拿起叉子
#define EATING 2 // 哲学家就餐
typedef int semaphore ; // 信号量是一种特殊的整型数据
int state[N] ; // 数组用来跟踪记录每位哲学家的状态,全局变量初始化为0,即是说哲学家的初始化状态是THINKING ;
semaphore mutex = 1 ; // 临界区的互斥
semaphore s[N] ; // 每个哲学家一个信号量
void philosopher(int i) { // i:哲学家编号,从0到i-1
while (true) {
think() ;
take_forks(i) ;
eat() ;
put_forks(i) ;
}
}
void take_forks(int i) {
wait(mutex) ; // 进入临界区
state[i] = HUNGRY ; // 记录哲学家i处于饥饿的状态
test(i) ; // 尝试获得2把尺子
signal(mutex) ; // 离开临界区
wait(s[i]) ; // 如果得不到需要的尺子则阻塞
}
void put_forks(int i) {
wait(mutex) ; // 进入临界区
state[i] = THINKING ; // 记录哲学家i已经就餐完毕
test(LEFT) ; // 检查左边的邻居现在可以吃吗
test(RIGHT) ; // 检查右边的邻居现在可以吃吗
signal(mutex) ; // 离开临界区
}
void test(int i) {
if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) {
state[i] = EATING ;
signal(s[i]) ;
}
}
五、体会
通过本次实验,我对操作系统线程/进程的通信机制和编程、操作系统线程/进程的死锁概念有了更深刻的理解,掌握了如何解决死锁,对操作系统的功能与原理有了进一步的了解。