Linux 0.11下信号量的实现和应用

本文详细介绍了在Linux 0.11中信号量的实现原理和应用,包括生产者-消费者问题的背景、信号量的作用、信号量的系统调用实现,以及如何通过信号量解决进程间的同步问题。文中还探讨了信号量的保护机制,如临界区保护,并提供了简化的信号量系统调用示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux 0.11下信号量的实现和应用

1.生产者-消费者问题

从一个实际的问题:生产者与消费者出发,谈一谈为什么要有信号量?信号量用来做什么?

问题描述:现在存在一个文件”.\buffer.txt”作为一个共享缓冲区,缓冲区同时最多只能保存10个数。现有一个生产者进程,依次向缓冲区写入整数0,1,2,……,M,M>=500;有N个消费者进程,消费者进程从缓冲区读数,每次读一个,并将读出的数从缓冲区删除。

  • 为什么要有信号量?
    对于生产者来说,当缓冲区满,也就是空闲缓冲区个数为0时,此时生产者不能继续向缓冲区写数,必须等待,直到有消费者从满缓冲区取走数后,再次有了空闲缓冲区,生产者才能向缓冲区写数。
    对于消费者来说,当缓冲区空时,此时没有数可以被取走,消费者必须等待,直到有生产者向缓冲区写数后,消费者才能取数。并且如果当缓冲区空时,先后有多个消费者均想从缓冲区取数,那么它们均需要等待,此时需要记录下等待的消费者的个数,以便缓冲区有数可取后,能将所有等待的消费者唤醒,确保请求取数的消费者最终都能取到数。
    也就是说,当多个进程需要协同合作时,需要根据某个信息,判断当前进程是否需要停下来等待;同时,其他进程需要根据这个信息判断是否有进程在等待,或者有几个进程在等待,以决定是否需要唤醒等待的进程。而这个信息,就是信号量。
  • 信号量用来做什么?
    设有一整形变量sem,作为一个信号量。此时缓冲区为空,sem=0。
    1. 消费者C1请求从缓冲区取数,不能取到,睡眠等待。sem=-1<0,表示有一个进程因缺资源而等待。
    2. 消费者C2也请求从缓冲区取数,睡眠等待。sem=-2<0,表示有两个进程因缺资源而等待。
    3. 生产者P往缓冲区写入一个数,sem=sem+1=-1<=0,并唤醒等待队列的头进程C1,C1处于就绪态,C2仍处于睡眠等待。
    4. 生产者P继续往缓冲区写入一个数,sem=0<=0,并唤醒C2,C1、C2就处于就绪状态。
      由此可见,通过判断sem的值以及改变sem的值,就保证了多进程合作的合理有序的推进,这就是信号量的作用。

2. 实现信号量

  • 信号量有什么组成?

    1. 需要有一个整形变量value,用作进程同步。
    2. 需要有一个PCB指针,指向睡眠的进程队列。
    3. 需要有一个名字来表示这个结构的信号量。

    同时,由于该value的值是所有进程都可以看到和访问的共享变量,所以必须在内核中定义;同样,这个名字的信号量也是可供所有进程访问的,必须在内核中定义;同时,又要操作内核中的数据结构:进程控制块PCB,所以信号量一定要在内核中定义,而且必须是全局变量。由于信号量要定义在内核中,所以和信号量相关的操作函数也必须做成系统调用,还是那句话:系统调用是应用程序访问内核的唯一方法。

  • 和信号量相关的函数

    Linux在0.11版还没有实现信号量,我们可以先弄一套缩水版的类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是信号量类型,根据实现的需要自己定义。

  • 信号量的保护
    使用信号量还需要注意一个问题,这个问题是由多进程的调度引起的。当一个进程正在修改信号量的值时,由于时间片耗完,引发调度,该修改信号量的进程被切换出去,而得到CPU使用权的新进程也开始修改此信号量,那么该信号量的值就很有可能发生错误,如果信号量的值出错了,那么进程的同步也会出错。所以在执行修改信号量的代码时,必须加以保护,保证在修改过程中其他进程不能修改同一个信号量的值。也就是说,当一个进程在修改信号量时,由于某种原因引发调度,该进程被切换出去,新的进程如果也想修改该信号量,是不能操作的,必须等待,直到原来修改该信号量的进程完成修改,其他进程才能修改此信号量。修改信号量的代码一次只允许一个进程执行,这样的代码称为临界区,所以信号量的保护,又称临界区保护。
    实现临界区的保护有几种不同的方法,在Linux 0.11上比较简单的方法是通过开、关中断来阻止时钟中断,从而避免因时间片耗完引发的调度,来实现信号量的保护。但是开关中断的方法,只适合单CPU的情况,对于多CPU的情况,不适用。Linux 0.11就是单CPU,可以使用这种方法。

3. 信号量的代码实现

  1. sem_open()
    原型:sem_t *sem_open(const char *name, unsigned int value)
    功能:创建一个信号量,或打开一个已经存在的信号量
    参数:

    • name,信号量的名字。不同的进程可以通过同样的name而共享同一个信号量。如果该信号量不存在,就创建新的名为name的信号量;如果存在,就打开已经存在的名为name的信号量。
    • value,信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。
    • 返回值。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID等)。如失败,返回值是NULL。

    由于要做成系统调用,所以会穿插讲解系统调用的相关知识。
    首先,在linux-0.11/kernel目录下,新建实现信号量函数的源代码文件sem.c。同时,在linux-0.11/include/linux目录下新建sem.h,定义信号量的数据结构。
    linux-0.11/include/linux/sem.h

    
    #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
    

    由于sem_open()的第一个参数name,传入的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以要用get_fs_byte()函数获取用户空间的数据。get_fs_byte()函数的功能是获得一个字节的用户空间中的数据。同样,sem_unlink()函数的参数name也要进行相同的处理。

  2. sem_unlink()
    原型:int sem_unlink(const char *name)
    功能:删除名为name的信号量。
    返回值:返回0表示成功,返回-1表示失败
  3. sem_wait()
    原型:int sem_wait(sem_t *sem)
    功能:信号量的P原子操作(检查信号量是不是为负值,如果是,则停下来睡眠等待,如果不是,则向下执行)。
    返回值:返回0表示成功,返回-1表示失败。

  4. sem_post()
    原型:int sem_post(sem_t *sem)
    功能:信号量的V原子操作(检查信号量的值是不是为0,如果是,表示有进程在睡眠等待,则唤醒队首进程,如果不是,向下执行)。

    返回值:返回0表示成功,返回-1表示失败。

关于sem_wait()和sem_post()

我们可以利用linux 0.11提供的函数sleep_on()实现进程的睡眠,用wake_up()实现进程的唤醒。
但是,sleep_on()比较难以理解。我们先看下sleep_on()的源码。

void sleep_on(struct task_struct **p)
{
    struct task_struct *tmp;

    if (!p)
        return;
    if (current == &(init_task.task))
        panic("task[0] trying to sleep");
    tmp = *p;
    *p = current;
    current->state = TASK_UNINTERRUPTIBLE;
    schedule();
    if (tmp)
        tmp->state=0;
}

还拿生产者和消费者的例子来说,依然是有一个生产者和N个消费者,目前缓冲区为空,没有数可取。

  1. 消费者C1请求取数,调用sleep_on(&sem->queue)。此时,tmp指向NULL,p指向C1,调用schedule(),让出CPU的使用权。此时,信号量sem处等待队列的情况如下:

    图片名称

    由于tmp是进程C1调用sleep_on()函数时申请的局部变量,所以会保存在C1运行到sleep_on()函数中时C1的内核栈中,只要进程C1还没有从sleep_on()函数中退出,tmp就会一直保存在C1的内核栈中。而进程C1是在sleep_on()中调用schedule()切出去的,所以在C1睡眠期间,tmp自然会保存在C1的内核栈中。这一点对于理解sleep_on()上如何形成隐式的等待队列很重要。

  2. 消费者C2请求取数,调用sleep_on(&sem->queue)。此时,信号量sem处的等待队列如下:

    图片名称

    从这里就可以看到隐式的等待队列已经形成了。由于进程C2也会由于调用schedule()函数在sleep_on()函数中睡眠,所以进程C2内核栈上的tmp便指向之前的等待队列的队首,也就是C1,通过C2的内核栈便可以找到睡眠的进程C1。这样就可以找到在信号量sem处睡眠的所有进程。

  3. 我们在看下唤醒函数wake_up()

    void wake_up(struct task_struct **p)
    {
        if (p && *p) {
            (**p).state=0;
            *p=NULL;
        }
    }

    从中我们可以看到唤醒函数wake_up()负责唤醒的是等待队列队首的进程。
    当队首进程C2被唤醒时,从schedule()函数退出,执行语句:

    if (tmp)
        tmp->state=0;

    会将内核栈上由tmp指向的进程C1唤醒,如果进程C1的tmp还指向其他睡眠的进程,当C1被调度执行时,会将其tmp指向的进程唤醒,这样只要执行一次wake_up()操作,就可以依次将所有等待在信号量sem处的睡眠进程唤醒。

sem_wait()和sem_post()函数的代码实现

由于我们要调用sleep_on()实现进程的睡眠,调用wake_up()实现进程的唤醒,我们在上面已经讲清楚了sleep_on()wake_up()的工作机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值