信号量的实现与应用

本实验需要完成两个任务:
(1)在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;
(2)在 linux-0.11 中实现信号量,用生产者—消费者程序检验之。

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

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

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

在0.11中实现信号量,用生产者-消费者程序检验之
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_t是信号量类型,根据实现的需要自定义
  • sem_open

功能是创建一个信号量,或打开一个已经存在的信号量。 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下,测试自己实现的信号量。

生产者—消费者问题

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

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

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

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

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

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

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

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

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

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

多进程共享文件
在 Linux 下使用 C 语言,可以通过三种方法进行文件的读写:

使用标准 C 的 fopen()、fread()、fwrite()、fseek() 和 fclose() 等;
使用系统调用 open()、read()、write()、lseek() 和 close() 等;
通过内存镜像文件,使用 mmap() 系统调用。
在 Linux 0.11 上只能使用前两种方法。
fork() 调用成功后,子进程会继承父进程拥有的大多数资源,包括父进程打开的文件。所以子进程可以直接使用父进程创建的文件指针/描述符/句柄,访问的是与父进程相同的文件。

使用标准 C 的文件操作函数要注意,它们使用的是进程空间内的文件缓冲区,父进程和子进程之间不共享这个缓冲区。因此,任何一个进程做完写操作后,必须 fflush() 一下,将数据强制更新到磁盘,其它进程才能读到所需数据。

建议直接使用系统调用进行文件操作。

原子操作、睡眠和唤醒
Linux 0.11 是一个支持并发的现代操作系统,虽然它还没有面向应用实现任何锁或者信号量,但它内部一定使用了锁机制,即在多个进程访问共享的内核数据时一定需要通过锁来实现互斥和同步。

锁必然是一种原子操作。通过模仿 0.11 的锁,就可以实现信号量。

多个进程对磁盘的并发访问是一个需要锁的地方。Linux 0.11 访问磁盘的基本处理办法是在内存中划出一段磁盘缓存,用来加快对磁盘的访问。进程提出的磁盘访问请求首先要到磁盘缓存中去找,如果找到直接返回;如果没有找到则申请一段空闲的磁盘缓存,以这段磁盘缓存为参数发起磁盘读写请求。请求发出后,进程要睡眠等待(因为磁盘读写很慢,应该让出 CPU 让其他进程执行)。这种方法是许多操作系统(包括现代 Linux、UNIX 等)采用的较通用的方法。这里涉及到多个进程共同操作磁盘缓存,而进程在操作过程可能会被调度而失去 CPU。因此操作磁盘缓存时需要考虑互斥问题,所以其中必定用到了锁。而且也一定用到了让进程睡眠和唤醒。

下面是从 kernel/blk_drv/ll_rw_blk.c 文件中取出的两个函数

static inline void lock_buffer(struct buffer_head * bh)
{
    // 关中断
    cli();    

    // 将当前进程睡眠在 bh->b_wait    
    while (bh->b_lock)
        sleep_on(&bh->b_wait);    
    bh->b_lock=1;
    // 开中断
    sti();        
}

static inline void unlock_buffer(struct buffer_head * bh)
{
    if (!bh->b_lock)
        printk("ll_rw_block.c: buffer not locked\n\r");
    bh->b_lock = 0;

    // 唤醒睡眠在 bh->b_wait 上的进程
    wake_up(&bh->b_wait);    
}

分析 lock_buffer() 可以看出,访问锁变量时用开、关中断来实现原子操作,阻止进程切换的发生。当然这种方法有缺点,且不适合用于多处理器环境中,但对于 Linux 0.11,它是一种简单、直接而有效的机制。

另外,上面的函数表明 Linux 0.11 提供了这样的接口:用 sleep_on() 实现进程的睡眠,用 wake_up() 实现进程的唤醒。它们的参数都是一个结构体指针—— struct task_struct *,即进程都睡眠或唤醒在该参数指向的一个进程 PCB 结构链表上。

因此,我们可以用开关中断的方式实现原子操作,而调用 sleep_on() 和 wake_up() 进行进程的睡眠和唤醒。

sleep_on() 的功能是将当前进程睡眠在参数指定的链表上(注意,这个链表是一个隐式链表,详见《注释》一书)。wake_up() 的功能是唤醒链表上睡眠的所有进程。这些进程都会被调度运行,所以它们被唤醒后,还要重新判断一下是否可以继续运行。可参考 lock_buffer() 中的那个 while 循环。

信号量的实现

信号量的组成

1、需要有一个整形变量value,作为共享数据的计数器。
2、需要一个等待队列,在缺少共享资源时存放等待该资源的进程。
3、 需要有一个名字在多个进程之间引用该信号量。
同时,系统需要支持多个信号量,可以考虑将这些信号量存放在一个数组中。这样,在新建信号量时要在数组中寻找空闲空间存放该信号量。在使用信号量的名字打开该信号量时,需要在数组中查找该信号量的名字。在删除该信号量时,需要删除该信号量的名字并将该信号量所占的数组位置标记为可用。

等待队列的构造

当前进程调用sem_wait()函数进行信号量的P操作时,如果缺乏资源就需要将当前进程阻塞在等待队列上;当调用sem_post()函数进行信号量的V操作时,如果有进程阻塞在等待队列,就唤醒队首进程。所以等待队列的构造有两种思路:一是自己创建等待队列,然后手动将需要阻塞的进程置为不可中断睡眠状态,将其放入等待队列。或将需要唤醒的队首进程出队,并将其由不可中断睡眠置为就绪状态;另外一种是利用Linux 0.11提供的sleep_on()函数实现进程的睡眠,用wake_up()函数唤醒进程,由于sleep_on()函数会利用睡眠进程内核栈中的tmp变量形成一个隐式的等待队列,所以无需我们在自己构造等待队列。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值