李治军操作系统实验4——信号量的实现和应用

操作系统实验4——信号量的实现和应用

代码仓库

GitLab

实验内容

  1. 在Ubuntu下编写程序,用信号量解决生产者——消费者问题。
  2. 在0.11中实现信号量,用信号量解决生产者—消费者问题。

实验步骤

1. 在Ubuntu下编写程序,用信号量解决生产者——消费者问题。

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

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

使用信号量解决生产者消费者问题有全球统一模板,就不再赘述。使用"buffer"文件当作缓冲区,第0-9个int存放产品,第10个int处存放费者要读取和生产者要放在哪个字节的标记pos。整个缓冲区(第0-9个int)可以看作是一个长度为10的循环缓冲区。典型buffer文件,可以看到这里int为4个字节:

/* Open buffer file */
fid = open("buffer", O_RDWR | O_CREAT | O_TRUNC, 0666)
/* Produter *****************************************/
/* Write data */
lseek(*id, (*pos) * sizeof(int), SEEK_SET);
write(*id, data, sizeof(int));
*pos = (*pos + 1) % BUFSIZE;
/* Consumer *****************************************/
/* Get pos */
lseek(*id, 10 * sizeof(int), SEEK_SET);
read(*id, &pos, sizeof(int));
/* Get data */
lseek(*id, pos * sizeof(int), SEEK_SET);
read(*id, &data, sizeof(int));
/* Update pos */
lseek(*id, 10 * sizeof(int), SEEK_SET);
pos = (pos + 1) % BUFSIZE;
write(*id, &pos, sizeof(int));

使用信号量,消费者读取必然是有序的:


consumer | pid | product
...
1       60376   36
1       60376   37
0       60375   38
2       60377   39
2       60377   40
2       60377   41
...

2. 实现信号量

可以在刚才写的程序“pc.c”中看到我们使用的是POSIX信号量,主要有 sem_open() , sem_wait(), sem_post(), sem_unlink(),他们分别是:打开信号量,信号量“减1”操作,信号量“增1”操作,销毁信号量。我们这里仅实现这4个接口,实现一个最小的可用的信号量。

>>>基本思路<<<

我们先来看一个信号量的结构:

// linux-0.11/include/linux/sem.h:4
typedef struct semaphore_t
{
    int value;
    char name[20];
    struct semaphore_t *next;
    struct task_struct *wait_queue[NR_TASKS];
    unsigned int wait_queue_front;
    unsigned int wait_queue_rear;
} sem_t;
成员 描述
value 最重要的,也就是信号量的值
name 信号量名称
next 链表next指针,这是为了把所有的信号量,用sem.c中的头节点以链表方式组织起来。
wait_queue[NR_TASKS] 每个信号量都有一个存放所有因对这个信号量P操作而导致阻塞的进程地址(task_struct *)的队列,以便于有进程对这个信号量V操作后唤醒被阻塞进程。
wait_queue_front 上述队列数组的头部秩,以便于队列取和删除头部节点操作。
wait_queue_rear 上述队列数组的尾部秩,以便于队列插入操作。

看了信号量的结构,对信号量的实现也有了一点思路。

  • 打开信号量,首先应该在链表中取找名字匹配的节点,若找不到我们则考虑新建一个信号量,并把它插到链表中。

  • 删除信号量,一样是先在链表中找,找到后使用free删除掉信号量,并从链表中删去即可。

  • P操作,首先对信号量值减1操作,随后判断是否小于0(从0减到-1)。若小于0,则阻塞自己,把自己进程地址(task_struct *)插入到该信号量的wait_queue队列的尾部,并调用进程调度程序。需要注意的是对信号量的值减1操作一定要保证原子性,我们这里用了非常简单粗暴的方法:使用sti()和cli()来开关中断保证原子性(仅适用于单处理器)。

  • V操作,相对应的需要对信号量值加1操作。判断是否有因对这个信号量P操作而导致阻塞的进程(也就是wait_queue里有没有进程地址),若有则唤醒 1个 ,并把这个进程从信号量里的wait_queue中删去。一样的,这个操作也需要保证原子性。

>>>sem_open()<<<

以名字打开一个信号量或者创建一个新的信号量,同时需要对信号量值完成初始化,最后返回信号量。

// sem_open()原型
sem_t *sem_open(const char *name, unsigned int value);

利用get_sem_by_name()函数,可以通过名字查找信号量。

// sem_open()实现 
// linux-0.11//kernel/sem.c:33
sem_t *sys_sem_open(const char *name, unsigned int value)
{
    sem_t *sem_tmp;
    sem_t *sem_new;
    sem_tmp = get_sem_by_name(name);

之前说过,所有的信号量是通过sem.c中的头节点按链表组织起来的。而get_sem_by_name()函数就是通过头节点去遍历链表找到名字匹配的信号量的 前一个节点 (方便删除操作)。若在链表中没有成功找到,函数将会返回最后一个节点(方便创建插入操作)。

// linux-0.11/kernel/sem.c:7
sem_t sem_chain_head = {0, "", {NULL}, NULL, 0, 0};

// get_sem_by_name()实现
// linux-0.11/kernel/sem.c:12
sem_t *get_sem_by_name(const char *name)
{
    ...

根据get_sem_by_name()函数返回特性,我们在sem_open()中需要对返回回来的信号量在做一次名字匹配。若匹配成功则直接返回该信号量,否则我们就需要创建一个信号量,并将其插入链表当中,最后返回信号量。

// sem_open()实现 
// linux-0.11//kernel/sem.c:37
    sem_tmp = get_sem_by_name(name);
    if (strcmp(sem_tmp->next->name, name) == 0)
        return sem_tmp->next;
    /* Not found \|/ */
    sem_new = (sem_t *)malloc(sizeof(sem_t));
    strcpy(sem_new->name, name);
    sem_new->value = value;
    sem_new->next = NULL;
    sem_new->wait_queue_front = 0;
    sem_new->wait_queue_rear = 0;
    sem_tmp->next = sem_new;
    return sem_new;
}
// sem_open()实现结束

>>>sem_unlink()<<<

以名字删除一个信号量。

同样用到了get_sem_by_name()函数,同样根据它的返回特性,我们需要对返回回来的信号量做一次名字匹配。匹配成功则将该信号量从链表中删去,并用free()将其从内存中删去,返回成功标记0。若名字匹配失败,就意味着没有找到信号量,返回失败标记-1。

// sem_unlink()实现
// linux-0.11//kernel/sem.c:51
int sys_sem_unlink(const char *name)
{
    sem_t *sem_del;
    sem_t *sem_tmp = get_sem_by_name(name);
    if (strcmp(sem_tmp->next->name, name) == 0)
    {
        sem_del = sem_tmp->next;
        sem_tmp->next = sem_tmp->next->next;
        free(sem_del);
        return 0;
    }
    return -1;
}

>>>sem_wait()<<<

P操作。

该操作需要保证原子性,这里简单粗暴的使用开关中断(cli()和sti())来保证原子性。首先最重要的就是把信号量值减1。然后判断信号量值是否严格小于0,若是,根据信号量定义,就需要阻塞自己。由于执行V操作时,就需要唤醒一个因为对同一个信号量P操作而导致阻塞的进程,所以我们就必须把每个因P操作而阻塞的进程 保存 入信号量。前面的信号量结构体说过,结构体里的等待队列wait_queue[] 就是用来放被阻塞的进程的。所以这里除了改变 current->state和调用调度函数以完成阻塞进程外,还需要将进程地址current(task_struct *)存入信号量里。

// sem_wait()实现
// linux-0.11//kernel/sem.c:65
int sys_sem_wait(sem_t *sem)
{
    cli();
    sem->value--;
    if (sem->value < 0)
    {
        sem->wait_queue[sem->wait_queue_rear++] = current;
        sem->wait_queue_rear %= NR_TASKS;
        current->state = TASK_UNINTERRUPTIBLE;
        schedule();
    }
    sti();
    return 0;
}

>>>sem_post()<<<

V操作。

与P操作相同,该操作依然使用cli()和sti()来保证原子性。最重要的也是对信号量值加1操作。之后就是我们在P操作里提到的,需要唤醒一个因对同一个信号里P操作而导致阻塞的进程,他们被存放在信号量里的等待队列 wait_queue[] ,我们只需利用队列的尾部头部秩 wait_queue_front 就可方便将它取出进行唤醒。当然如果队列里一个阻塞进程都没有,我们自然也不需要进行这一系列操作。判断等待 wait_queue[] 队列内有没有进程,可以利用队列的头部秩和尾部秩,也可以利用信号量值进行判断。

// sem_post()实现
// linux-0.11//kernel/sem.c:80
int sys_sem_post(sem_t *sem)
{
    cli();
    struct task_struct *p;
    sem->value++;
    if (sem->value <= 0) /* Some process in wait queue */
    {
        p = sem->wait_queue[sem->wait_queue_front++];
        sem->wait_queue_front %= NR_TASKS;
        p->state = TASK_RUNNING;
    }
    sti();
    return 0;
}

3. 修改pc.c,使用我们实现的信号量

修改pc.c

在系统调用实验那里,学会了如何调用我们自己实现的系统调用:

// pc-linux-0.11.c:1
#define __LIBRARY__
...
_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);

同时别忘了我们信号量结构体定义在linux/sem.h:

// pc-linux-0.11.c:7
#include <linux/sem.h>

做这些修改后(记得gcc3.4,注释只能用/* 😭 */),就能愉快的在linux0.11使用我们实现的信号量运行了。

添加系统调用声明

// linux-0.11/include/linux/sys.h:73
extern int sys_setregid();
extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();

// linux-0.11/include/linux/sys.h:90
...
sys_setreuid, sys_setregid, sys_sem_open, sys_sem_wait, sys_sem_post,
sys_sem_unlink };
// unistd.h:132
#define __NR_sem_open 	72
#define __NR_sem_wait	73
#define __NR_sem_post	74
#define __NR_sem_unlink 75

修改系统调用数

# /kernel/system_call.s:61
nr_system_calls = 76

编译linux0.11

把我们写好的sem.h放入linux0.11/include/linux下,把sem.c放入linux0.11/kernel下。修改linux-0.11/kernel/Makefile,编译。

# linux-0.11/kernel/Makefile:27
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


# linux-0.11/kernel/Makefile:84
sem.s sem.o: sem.c ../include/unistd.h ../include/linux/sched.h \
  ../include/linux/kernel.h ../include/asm/segment.h ../include/asm/system.h \
  ../include/linux/sem.h

编译后还得记得手动把修改后的unistd.h放入linux0.11系统的/usr/include下。

运行pc-linux0.11.c

这样,就可以在linux0.11编译运行pc-linux0.11.c了。

我们把pc-linux0.11.c的运行结果重定向到一个文件里,sync后关机到我们的电脑上再对其进行查看,可以看到消费者进程交替取出产品,产品有序被取出:

2       18      291
2       18      292
1       17      293
1       17      294
1       17      295
1       17      296
1       17      297
1       17      298
1       17      299
1       17      300
0       16      301
0       16      302
0       16      303
0       16      304
0       16      305
0       16      306
0       16      307
0       16      308
2       18      309
2       18      310
2       18      311

至此实验完成。

posted @ 2021-02-27 18:01  ithepug  阅读(1195)  评论(2编辑  收藏  举报