HIT Linux-0.11 实验六 信号量的实现与运用 实验报告

实验要求与实验指导见 实验楼。
实验环境为 配置本地实验环境。

一、实验目标

  1. 加深对进程同步与互斥概念的认识;
  2. 掌握信号量的使用,并应用它解决生产者——消费者问题;
  3. 掌握信号量的实现原理。

二、实验内容和结果

1. 生产者-消费者问题

  根据实验要求,pc.c程序需打开一个文件作为共享缓冲区、创建一个生产者进程和多个消费者进程,其中生产者进程向缓冲区写入连续的整数,消费者进程从缓冲区依次读取数字并输出。

  对于涉及到的文件操作,这里使用 open()write()read()系统调用来实现,并使用 lseek()修改文件读写指针来控制读写的位置。为了达到删除读出的数字的要求,本程序进行循环读写缓冲区的前 10 个数字的空间,生产者在第 10 个位置写入数字后,又回到第 1 个位置继续写入;消费者读取了第 10 个位置的数字后,也回到第 1 个位置。于是各个消费者进程间需要记录各自读取的位置,这里使用另一个共享的文件 share.txt来实现:每次消费者先从该文件处获取上一个文件记录的位置,然后从缓冲区读取下一个字符,最后将新的位置记录回去。

  限制缓冲区的最大保存数量需要两个信号量来共同控制:sem_empty 视为缓冲区剩余的空间资源、sem_full 视为缓冲区已被使用的资源;另外对文件的读写和消费者的整个“消费”过程都是一个原子操作,不可以打断,于是使用两个信号量进行限制。

  pc.c程序的完整代码如下:

#include 
#include 
#include 
#include 
#include 

#define SIZE 10 // 缓冲区大小
#define N 5     // 消费者数量
#define M 510   // 写入的最大整数

int main()
{
    int fd, pt;
    int val, current_point;
    pid_t pid;
    sem_t *sem_empty, *sem_full, *sem_fd, *sem_pt;
    fd = open("buffer.txt", O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR); // 用作缓冲区
    pt = open("share.txt", O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);  // 记录消费者读取位置
    val = 0;
    lseek(pt, 0, SEEK_SET);
    write(pt, &val, sizeof(val));  // 初始化位置为 0
    sem_empty = sem_open("empty", O_CREAT|O_EXCL, 0644, SIZE);
    sem_full = sem_open("full", O_CREAT|O_EXCL, 0644, 0);
    sem_fd = sem_open("fd", O_CREAT|O_EXCL, 0644, 1);
    sem_pt = sem_open("pt", O_CREAT|O_EXCL, 0644, 1);

    pid = fork();
    if(pid == -1) {
        printf("fork error!\n");
        exit(-1);
    }
    else if(pid == 0) {
        // 生产者进程
        printf("I'm producer. pid = %d\n", getpid());
        int count = 0;
        while(count <= M) {
            sem_wait(sem_empty);
            sem_wait(sem_fd);
            current_point = count % SIZE;
            lseek(fd, current_point * sizeof(current_point), SEEK_SET);
            write(fd, &count, sizeof(count));
            sem_post(sem_fd);
            sem_post(sem_full);
            count++;
        }
        printf("producer end.\n");
        fflush(stdout);
        return 0;
    }
    int i;
    for (i = 0; i < N; i++) {
        if((pid=fork()) == 0) // 创建 N 个消费者进程
            break;
    }
    if(pid == -1) {
        printf("fork error.\n");
        exit(-1);
    } else if(pid == 0) {
        // 消费者进程
        while(1) {
            // 从 share.txt 获取读取位置
            sem_wait(sem_pt);
            lseek(pt, 0, SEEK_SET);
            read(pt, &val, sizeof(val));
            if (val >= M) { // 读取完毕,退出
                sem_post(sem_pt);
                break;
            }
            sem_wait(sem_full);
            sem_wait(sem_fd);
            current_point = (val + 1) % SIZE;
            lseek(fd, current_point*sizeof(current_point), SEEK_SET);
            read(fd, &val, sizeof(val)); // 从缓冲区读取数字
            printf("%d:%d\n", getpid(), val);
            fflush(stdout);
            sem_post(sem_empty);
            sem_post(sem_fd);
            // 将新的位置记录在 share.txt
            lseek(pt, 0, SEEK_SET);
            write(pt, &val, sizeof(val));
            sem_post(sem_pt);
        }
        printf("child-%d: pid = %d end.\n", i, getpid());
        return 0;
    }
    int child = N + 1;
    while(child--)
        wait(NULL);
    close(fd);
    close(pt);
    sem_unlink("full");
    sem_unlink("empty");
    sem_unlink("fd");
    sem_unlink("pt");
    return 0;
}

  在 Ubuntu 中编译运行此程序,结果如下:

HIT Linux-0.11 实验六 信号量的实现与运用 实验报告_第1张图片

2. 实现信号量

  信号量的信息由一个名为 sem_t的结构体保存,至少需要保存名字和值两个属性,根据信号量的定义,还需要一个等待进程的队列。该结构体在 include/sem.h中定义:

typedef struct {
    char name[_SEM_NAME_MAX];
    int value;
    // 待添加队列属性
} sem_t;

  sem_open()函数的功能是新建一个信号量或打开一个已存在的信号量,首先需要将参数字符串从用户空间复制到内核空间,然后对比已经存在的信号量,如果名字相同则直接返回该信号量,否则创建新的信号量。sem_unlink()函数的功能是删除名为 name 的信号量,为了保证系统的多个信号量能在有限长度的数组内都正常工作,当删去信号量时数组后面的元素往前移动,腾出空间。代码如下:

#include 
#include 
// #include  // 有问题,见实验楼指导
#include 

sem_t sems[_SEM_MAX]; // 信号量数组
int count = 0;        // 信号量数量

sem_t *sys_sem_open(const char *name, unsigned int value)
{
    char sem_name[_SEM_NAME_MAX];
    int len = 0, i = 0;
    // 从用户态复制到内核态
    do {
        sem_name[i] = get_fs_byte(name+i);
    } while(sem_name[i++]!='\0');
    if (i == _SEM_NAME_MAX) // 判断长短
        return NULL;
    len = i - 1;
    // 比较是否同名
    for (i = 0; i < count; i++)
        if(!strcmp(sem_name, sems[i].name))
            return &sems[i];
    // 创建新信号量
    for (i = 0; i < len; i++)
        sems[count].name[i] = sem_name[i];
    sems[count].value = value;
    count++;
    return &sems[count-1;
}

int sys_sem_unlink(const char *name)
{
    char sem_name[_SEM_NAME_MAX];
    int len = 0, i;
    // 从用户态复制到内核态
    for (i = 0;; i++) {
        sem_name[i] = get_fs_byte(&name[i]);
        if(sem_name[i] == '\0' || i == _SEM_NAME_MAX) {
            break;
        }
    }
    int flag = 0, j;
    for (i = 0; i < count; i++) {
        if(!strcmp(sem_name, sems[i].name)) {
            flag = 1;
            break;
        }
    }
    if(flag) {
        for (j = i; j < _SEM_MAX; j++)
            sems[j] = sems[j + 1];
        count--;
        return 0;
    } else
        return -1;
}

  sem_wait()函数是 P 原子操作,功能是使信号量的值减一,如果信号量值为 0 就阻塞当前进程;sem_post函数是 V 原子操作,功能是使信号量的值加一,如果有等待该信号量的进程,则唤醒其中一个。阻塞和唤醒进程由 kernel/sched.c中的 sleep_on()wake_up函数实现。

  sleep_on()函数参数获取的指针 *p 是等待队列的头指针,每次执行该函数时,指针 *tmp 指向原本的等待队列,*p 则指向当前进程,即将当前进程插入等待队列头部,然后将当前进程设为睡眠状态,执行 schedule()进行调度。wake_up()函数将唤醒 *p 指向的进程。某个进程被唤醒后,回到 sleep_on()继续执行,它将 *tmp 指向的进程继续唤醒,即唤醒等待队列的上一个进程。依次执行下去,等待队列的所有进程都会被唤醒。

  因此,每个信号量需要维护自己的进程等待队列,当阻塞时将进程放进等待队列,唤醒时将其移出。根据 sleep_on()的特点,信号量结构体中只需设置一个 PCB 指针即可在该函数中形成隐式等待队列。

typedef struct {
    char name[_SEM_NAME_MAX];
    int value;
    struct task_struct *queue;
} sem_t;

  需要注意的是,当等待队列中有多个进程时,信号量值加一只能唤醒其中一个进程,而 wake_up()函数会唤醒队列中的所有进程,所以需要在 sem_wait()中使用 while 循环,当信号量的值小于等于 0 时保证其他进程一直阻塞,由于有这层限制,sem_post()中也就不需要对 wake_up()函数的使用进行条件限制了。代码如下:

int sys_sem_wait(sem_t *sem)
{
    if (sem == NULL || sem < sems || sem > sems + _SEM_MAX)
        return -1;
    cli(); // 关中断
    while(sem->value <= 0)
        sleep_on(&(sem->queue));
    sem->value--;
    sti(); // 开中断
    return 0;
}

int sys_sem_post(sem_t *sem)
{
    if(sem==NULL || sem < sems || sem > sems+_SEM_MAX)
        return -1;
    cli();
    sem->value++;
    wake_up(&(sem->queue));
    sti();
    return 0;
}

  完整的 include/sem.h的代码如下。该头文件也需要放进 Linux-0.11 的 /usr/include/路径下。

#include 

#define _SEM_MAX 24
#define _SEM_NAME_MAX 100

#define sti() __asm__ ("sti"::)
#define cli() __asm__ ("cli"::)

typedef struct {
    char name[_SEM_NAME_MAX];
    int value;
    struct task_struct *queue;
} sem_t;

extern sem_t sems[_SEM_MAX];

  修改 kernel/Makefile,将新增的程序一块编译进 Image:

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
    panic.o printk.o vsprintf.o sys.o exit.o \
    signal.o mktime.o sem.o # 末尾添加
# 最后一行添加:
sem.s sem.o: sem.c ../include/linux/kernel.h ../include/asm/segment.h \
 ../include/string.h ../include/sem.h

  然后对系统调用的实现做后续补充。在 include/linux/sys.h中添加:

extern int sys_sem_open(); // 不能是 sem_t,会无法编译
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();

fn_ptr sys_call_table[] = { ..., sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink };

  在 include/unistd.h及 Linux-0.11 中的 usr/include/unistd.h添加:

#define __NR_sem_open   72
#define __NR_sem_wait   73
#define __NR_sem_post   74
#define __NR_sem_unlink 75

  最后修改 kernel/system_call.s中系统调用的数量:

nr_system_calls = 76

这样信号量就添加完成了。

  接下来修改 pc.c,从而能够在 Linux-0.11 运行此程序:

#define __LIBRARY__  /* add */

#include 
#include 
#include    /* change */
#include 
#include 

#define SIZE 10
#define N 5
#define M 50
/* add */
_syscall2(int,sem_open,const char*,name,unsigned int,vaule)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)

int main()
{
    ...
    /* change */
    sem_empty = sem_open("empty", SIZE);
    sem_full = sem_open("full", 0);
    sem_fd = sem_open("fd", 1);
    sem_pt = sem_open("pt", 1);
    ...
}

  在 Linux-0.11 下编译运行,结果如下:

HIT Linux-0.11 实验六 信号量的实现与运用 实验报告_第2张图片
HIT Linux-0.11 实验六 信号量的实现与运用 实验报告_第3张图片

三、、问题

在 pc.c 中去掉所有与信号量有关的代码,再运行程序,执行效果有变化吗?为什么会这样?

有变化,输出的数字顺序完全混乱。

没有了信号量,进程之间无法同步工作,而且无法保证对文件的读写是原子操作,无法保证顺序,工作异常。

实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:

Producer()
{
    // 生产一个产品 item;
    // 空闲缓存资源
    P(Empty);  
    // 互斥信号量
    P(Mutex);  
    // 将item放到空闲缓存中;
    V(Mutex);
    // 产品资源
    V(Full);  
}
Consumer()
{
    P(Full);  
    P(Mutex);  
    //从缓存区取出一个赋值给item;
    V(Mutex);
    // 消费产品item;
    V(Empty);
}

这样可行吗?如果可行,那么它和标准解法在执行效果上会有什么不同?如果不可行,那么它有什么问题使它不可行?

从信号量的设计来看,Mutex 信号量保证了对缓冲区的互斥读写,Empty 和 Full 两个信号量保证了缓冲区的数量限制,并且不会出现死锁。与上述 pc.c的逻辑基本一致。(可能实验楼题目把两个信号量顺序弄反了。。。

你可能感兴趣的:(OS,and,Linux)