操作系统实验六 信号量的实现和应用(哈工大李治军)

实验六 信号量的实现和应用

实验目的

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

实验内容

本次实验的基本内容是:

  • 在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;
  • 在 0.11 中实现信号量,用生产者—消费者程序检验之。

用信号量解决生产者—消费者问题

在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:

  • 建立一个生产者进程,N 个消费者进程(N>1);
  • 用文件建立一个共享缓冲区;
  • 生产者进程依次向缓冲区写入整数 0,1,2,…,M,M>=500;
  • 消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;
  • 缓冲区同时最多只能保存 10 个数。

一种可能的输出效果是:

10: 0
10: 1
10: 2
10: 3
10: 4
11: 5
11: 6
12: 7
10: 8
12: 9
12: 10
12: 11
12: 12
……
11: 498
11: 499

其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。

pc.c 中将会用到 sem_open()sem_close()sem_wait()sem_post() 等信号量相关的系统调用,请查阅相关文档。

实现信号量

Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:

sem_t *sem_open(const char *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);
  • sem_open()​ 的功能是创建一个信号量,或打开一个已经存在的信号量。

    • sem_t 是信号量类型,根据实现的需要自定义。
    • name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。
    • value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。
  • sem_wait() 就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。

  • sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。

  • sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。

kernel 目录下新建 sem.c 文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。

什么是信号量?

信号量,英文为 semaphore,最早由荷兰科学家、图灵奖获得者 E. W. Dijkstra 设计,任何操作系统教科书的“进程同步”部分都会有详细叙述。

Linux 的信号量秉承 POSIX 规范,用man sem_overview可以查看相关信息。

本次实验涉及到的信号量系统调用包括:sem_open()sem_wait()sem_post()sem_unlink()

生产者—消费者问题

生产者—消费者问题的解法几乎在所有操作系统教科书上都有,其基本结构为:

Producer()
{
    
    
    // 生产一个产品 item;

    // 空闲缓存资源
    P(Empty);

    // 互斥信号量
    P(Mutex);

    // 将item放到空闲缓存中;
    V(Mutex);

    // 产品资源
    V(Full);
}

Consumer()
{
    
    
    P(Full);
    P(Mutex);

    //从缓存区取出一个赋值给item;
    V(Mutex);

    // 消费产品item;
    V(Empty);
}

显然在演示这一过程时需要创建两类进程,一类执行函数 Producer(),另一类执行函数 Consumer()

思路

大家可以参考这个演示视频,先总体梳理一遍。如果实验三系统调用学的不错的话,这里的系统调用编写和编译应该是很清晰的。

通俗的讲,在用户程序中想要使用内核态中的系统调用命令,需要在用户态执行对系统调用命令的调用

image-20211020150559446

通过宏展开

image-20211020150719338

image-20211020150742304

由此通过著名的 int 0x80 中断进入内核态

用户程序 pc.c

知识点

文件操作

image-20211021215904645

image-20211021215715628

信号量作用

  • mutex 是保证互斥访问缓存池
  • empty 是缓冲池里空位的剩余个数,即空缓冲区数,初始值为n
  • full 是用来记录当前缓冲池中已经占用的缓冲区个数,初始值为0

代码展示


#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>

_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)

const char *FILENAME = "/usr/root/buffer_file";    /* 消费生产的产品存放的缓冲文件的路径 */
const int NR_CONSUMERS = 5;                        /* 消费者的数量 */
const int NR_ITEMS = 50;                        /* 产品的最大量 */
const int BUFFER_SIZE = 10;                        /* 缓冲区大小,表示可同时存在的产品数量 */
sem_t *metux, *full, *empty;                    /* 3个信号量 */
unsigned int item_pro, item_used;                /* 刚生产的产品号;刚消费的产品号 */
int fi, fo;                                        /* 供生产者写入或消费者读取的缓冲文件的句柄 */


int main(int argc, char *argv[])
{
    
    
    char *filename;
    int pid;
    int i;

    filename = argc > 1 ? argv[1] : FILENAME;
    /* O_TRUNC 表示:当文件以只读或只写打开时,若文件存在,则将其长度截为0(即清空文件)
     * 0222 和 0444 分别表示文件只写和只读(前面的0是八进制标识)
     */
    fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222);    /* 以只写方式打开文件给生产者写入产品编号 */
    fo = open(filename, O_TRUNC| O_RDONLY, 0444);            /* 以只读方式打开文件给消费者读出产品编号 */

    metux = sem_open("METUX", 1);    /* 互斥信号量,防止生产消费同时进行 */
    full = sem_open("FULL", 0);        /* 产品剩余信号量,大于0则可消费 */
    empty = sem_open("EMPTY", BUFFER_SIZE);    /* 空信号量,它与产品剩余信号量此消彼长,大于0时生产者才能继续生产 */

    item_pro = 0;

    if ((pid = fork()))    /* 父进程用来执行消费者动作 */
    {
    
    
        printf("pid %d:\tproducer created....\n", pid);
        /* printf()输出的信息会先保存到输出缓冲区,并没有马上输出到标准输出(通常为终端控制台)。
         * 为避免偶然因素的影响,我们每次printf()都调用一下stdio.h中的fflush(stdout)
         * 来确保将输出立刻输出到标准输出。
         */
        fflush(stdout);

        while (item_pro <= NR_ITEMS)    /* 生产完所需产品 */
        {
    
    
            sem_wait(empty);
            sem_wait(metux);

            /* 生产完一轮产品(文件缓冲区只能容纳BUFFER_SIZE个产品编号)后
             * 将缓冲文件的位置指针重新定位到文件首部。
             */
            if(!(item_pro % BUFFER_SIZE))
                lseek(fi, 0, 0);

            write(fi, (char *) &item_pro, sizeof(item_pro));        /* 写入产品编号 */
            printf("pid %d:\tproduces item %d\n", pid, item_pro);
            fflush(stdout);
            item_pro++;

            sem_post(full);        /* 唤醒消费者进程 */
            sem_post(metux);
        }
    }
    else    /* 子进程来创建消费者 */
    {
    
    
        i = NR_CONSUMERS;
        while(i--)
        {
    
    
            if(!(pid=fork()))    /* 创建i个消费者进程 */
            {
    
    
                pid = getpid();
                printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i);
                fflush(stdout);

                while(1)
                {
    
    
                    sem_wait(full);
                    sem_wait(metux);

                    /* read()读到文件末尾时返回0,将文件的位置指针重新定位到文件首部 */
                    if(!read(fo, (char *)&item_used, sizeof(item_used)))
                    {
    
    
                        lseek(fo, 0, 0);
                        read(fo, (char *)&item_used, sizeof(item_used));
                    }

                    printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used);
                    fflush(stdout);

                    sem_post(empty);    /* 唤醒生产者进程 */
                    sem_post(metux);

                    if(item_used == NR_ITEMS)    /* 如果已经消费完最后一个商品,则结束 */
                        goto OK;
                }
            }
        }
    }
OK:
    close(fi);
    close(fo);
    return 0;
}

修改内核

编写 sem.h

文件位置:oslab/linux-0.11/include/linux

#ifndef _SEM_H
#define _SEM_H

#include <linux/sched.h>

#define SEMTABLE_LEN    20
#define SEM_NAME_LEN    20


typedef struct semaphore{
    
    
    char name[SEM_NAME_LEN];
    int value;
    struct task_struct *queue;
} sem_t;
extern sem_t semtable[SEMTABLE_LEN];

#endif

image-20211022150430997

image-20211101211843916

编写 sem.c

#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>

sem_t semtable[SEMTABLE_LEN];
int cnt = 0;

sem_t *sys_sem_open(const char *name,unsigned int value)
{
    
    
    char kernelname[100];   
    int isExist = 0;
    int i=0;
    int name_cnt=0;
    while( get_fs_byte(name+name_cnt) != '\0')
    name_cnt++;
    if(name_cnt>SEM_NAME_LEN)
    return NULL;
    for(i=0;i<name_cnt;i++)
    kernelname[i]=get_fs_byte(name+i);
    int name_len = strlen(kernelname);
    int sem_name_len =0;
    sem_t *p=NULL;
    for(i=0;i<cnt;i++)
    {
    
    
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
    
    
                if( !strcmp(kernelname,semtable[i].name) )
                {
    
    
                    isExist = 1;
                    break;
                }
        }
    }
    if(isExist == 1)
    {
    
    
        p=(sem_t*)(&semtable[i]);
        //printk("find previous name!\n");
    }
    else
    {
    
    
        i=0;
        for(i=0;i<name_len;i++)
        {
    
    
            semtable[cnt].name[i]=kernelname[i];
        }
        semtable[cnt].value = value;
        p=(sem_t*)(&semtable[cnt]);
         //printk("creat name!\n");
        cnt++;
     }
    return p;
}


int sys_sem_wait(sem_t *sem)
{
    
    
    cli();
    while( sem->value <= 0 )       
        sleep_on(&(sem->queue));   
    sem->value--;               
    sti();
    return 0;
}
int sys_sem_post(sem_t *sem)
{
    
    
    cli();
    sem->value++;
    if( (sem->value) <= 1)
        wake_up(&(sem->queue));
    sti();
    return 0;
}

int sys_sem_unlink(const char *name)
{
    
    
    char kernelname[100];  
    int isExist = 0;
    int i=0;
    int name_cnt=0;
    while( get_fs_byte(name+name_cnt) != '\0')
            name_cnt++;
    if(name_cnt>SEM_NAME_LEN)
            return NULL;
    for(i=0;i<name_cnt;i++)
            kernelname[i]=get_fs_byte(name+i);
    int name_len = strlen(name);
    int sem_name_len =0;
    for(i=0;i<cnt;i++)
    {
    
    
        sem_name_len = strlen(semtable[i].name);
        if(sem_name_len == name_len)
        {
    
    
                if( !strcmp(kernelname,semtable[i].name))
                {
    
    
                        isExist = 1;
                        break;
                }
        }
    }
    if(isExist == 1)
    {
    
    
        int tmp=0;
        for(tmp=i;tmp<=cnt;tmp++)
        {
    
    
            semtable[tmp]=semtable[tmp+1];
        }
        cnt = cnt-1;
        return 0;
    }
    else
        return -1;
}

文件位置:oslab/linux-0.11/kernel

image-20211022205328870

添加系统调用号

/* 添加的系统调用号 */
#define __NR_sem_open 72
#define __NR_sem_wait 73
#define __NR_sem_post 74
#define __NR_sem_unlink 75

文件位置:oslab/linux-0.11/include/unistd.h

image-20211022210605898

image-20211022211245623

改写系统调用数

nr_system_calls = 76

文件位置:oslab/linux-0.11/kernel/system_call.s

image-20211022211536792

image-20211022212943278

添加系统调用的定义

/* ... */
extern int sys_setregid();
/* 添加的系统调用定义 */
#include <linux/sem.h>
extern i sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();

/* 在sys_call_table数组中添加系统调用的引用: */
fn_ptr sys_call_table[] = 
{
    
     sys_setup, sys_exit, sys_fork, sys_read,……, sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink}

文件位置:oslab/linux-0.11/include/linux/sys.h

image-20211022213505003

image-20211022215552936

image-20211022214317003

修改工程文件的编译规则

/* 第一处 */
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
       
/* 第二处 */
### Dependencies:
sem.s sem.o: sem.c ../include/linux/kernel.h ../include/unistd.h \
  ../include/linux/sem.h ../include/linux/sched.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

文件位置:oslab/linux-0.11/kernel/Makefile

image-20211022214634260

image-20211022214742145

image-20211022215123328

Debug

image-20211023155037517

反复查看 sem.h 中的代码,找不到错误。后来看了一下 sem.h 文件,发现拙劣的拼写错误

image-20211023154327001

修改后,编译成功!

挂载文件

将已经修改的 unistd.hsem.h 文件以及用户文件 pc.c 拷贝到linux-0.11系统中,用于测试实现的信号量

sudo ./mount-hdc 
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
cp ./pc.c ./hdc/usr/root/
sudo umount hdc/

测试

启动新编译的linux-0.11内核,用pc.c测试实现的信号量

gcc -o pc pc.c
./pc > wcf.txt
sync

结果展示:

image-20211101214610618

天道酬勤

Debug好几天,也看不出来哪里有问题,索性完全复制了一份别人的,这是做的最失败的一次实验,只能保证运行结果成功。

复制的博主源码

猜你喜欢

转载自blog.csdn.net/leoabcd12/article/details/121089715