操作系统--信号量的实现

在早期linux版本中,linus并没有在操作系统上实现信号量机制。但是这是个非常重要有用的机制。在linux3.0版本以后开始引用。(我也不知道对不对,不对请原谅)。信号量机制最早在1965年,由荷兰学者Dijkstra提出,信号量(Semaphores)在进程同步中发挥了重要的作用。

struct semaphore {
	spinlock_t		lock;
	unsigned int		count;
	struct list_head	wait_list;
};

这个结构体定义在semaphore.h中。是描述一个信号量结构的,其中的三个成员含义如下:

  1. spinlock_t lock 这是一个类型为 spinlock_t 的成员变量,用于实现对信号量的自旋锁(spin lock)。自旋锁是一种轻量级的同步机制,用于保护临界区,以确保在多线程环境下的原子操作。

  2. unsigned int count: 这是一个无符号整数型的成员变量,用于记录信号量的计数值。计数值表示当前可用的资源数量。当计数值大于零时,表示有可用资源;当计数值等于零时,表示资源已被占用,需要等待其他线程释放资源(在哈工大操作系统上李志军老师的代码讲解中用sem来表示这个量,稍微有点小不同,但是思想是一样的)。

  3. struct list_head wait_list 这是一个名为 wait_list 的链表头,用于存储等待该信号量的线程的链表。当一个线程无法获取信号量时,它会被添加到该链表中,等待资源的释放(相当于一个等待队列,不过是用链表实现的)。


    接下来我们看semaphore.c文件中的代码。代码开头有一段注释我觉得对于理解有所帮助就翻译过来,原文如下:

 * Some notes on the implementation:
 *
 * The spinlock controls access to the other members of the semaphore.
 * down_trylock() and up() can be called from interrupt context, so we
 * have to disable interrupts when taking the lock.  It turns out various
 * parts of the kernel expect to be able to use down() on a semaphore in
 * interrupt context when they know it will succeed, so we have to use
 * irqsave variants for down(), down_interruptible() and down_killable()
 * too.
 *
 * The ->count variable represents how many more tasks can acquire this
 * semaphore.  If it's zero, there may be tasks waiting on the wait_list.

翻译如下:

自旋锁用于控制对信号量的其他成员的访问。
down_trylock() 和 up() 可能会在中断上下文中调用,因此在获取锁时必须禁用中断。
事实上,内核的各个部分希望能够在中断上下文中使用 down() 来获取信号量,
当它们知道这将成功时,
因此对于 down()、down_interruptible() 和 down_killable() 也必须使用 irqsave 变体。
->count 变量表示还有多少个任务可以获取该信号量。
如果为零,则可能有任务在等待队列上等待。


下面是定义了五个静态函数:

static noinline void __down(struct semaphore *sem);
static noinline int __down_interruptible(struct semaphore *sem);
static noinline int __down_killable(struct semaphore *sem);
static noinline int __down_timeout(struct semaphore *sem, long jiffies);
static noinline void __up(struct semaphore *sem);

noinline是一个函数修饰符,它用于告诉编译器不要将被修饰的函数进行内联优化。下面是这五个函数功能的简介:

static noinline void __down(struct semaphore *sem); 这个函数用于获取(down)信号量,它接受一个指向信号量结构的指针作为参数。函数的作用是尝试获取信号量,如果信号量的值大于零,则将其减一并继续执行。如果信号量的值为零,则函数会阻塞线程,直到信号量的值大于零。


static noinline int __down_interruptible(struct semaphore *sem); 这个函数与前面的函数类似,但是它是可中断的。如果在等待信号量时接收到中断信号,则函数会立即返回一个错误码,以指示被中断。


static noinline int __down_killable(struct semaphore *sem); 这个函数也类似于前面的函数,但它是可被终止的。如果在等待信号量时接收到可终止信号,则函数会立即返回一个错误码,以指示被终止。


static noinline int __down_timeout(struct semaphore *sem, long jiffies); 这个函数允许设置等待超时时间。它会尝试获取信号量,但如果超过指定的超时时间仍未成功获取,则函数会返回一个错误码。


static noinline void __up(struct semaphore *sem); 这个函数用于释放(up)信号量,将其值加一它接受一个指向信号量结构的指针作为参数。


void down(struct semaphore *sem)
{
	unsigned long flags;  //保存中断状态的标志

	spin_lock_irqsave(&sem->lock, flags); //通过自旋锁改变flags标志。保证访问是具有原子性的
	if (likely(sem->count > 0))  //如果有资源,count-1表示资源被占用;
		sem->count--;
	else
		__down(sem);             //表示没有可用资源,那么调用 __down 函数来等待资源的释放
	spin_unlock_irqrestore(&sem->lock, flags); //恢复之前保存的中断状态来完成对临界区的解锁
}

这段代码的作用是尝试获取信号量它其实相当于PV操作中的P操作。如果有可用资源,则将计数值减一,表示资源被占用;如果没有可用资源,则进入等待状态。通过使用自旋锁来保护临界区,确保对信号量的操作是原子的,一些注释我已经标注。


int down_interruptible(struct semaphore *sem)
{
	unsigned long flags;
	int result = 0;

	spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		result = __down_interruptible(sem);
	spin_unlock_irqrestore(&sem->lock, flags);

	return result;
}

这个和上面down函数是一样的原理,它两实现的主要区别是:

  1. 中断处理:down_interruptible函数是一个中断可中断的下降操作,如果进程在等待过程中被中断信号中断,函数会返回适当的错误码。而down函数是一个不可中断的下降操作,如果进程在等待过程中被中断信号中断,它将一直等待直到获取到信号量资源或者被显式唤醒。

  2. 返回值:down_interruptible函数的返回值是一个整数,表示操作结果。如果在等待过程中被中断,则返回一个非零值;如果成功获取到信号量资源,则返回0。而down函数没有返回值,它只是在获取到信号量资源之前阻塞进程。

  3. 使用场景:down_interruptible函数通常用于对可中断的等待情况做处理,即在等待信号量资源时允许被中断,比如在用户空间的应用程序中。而down函数通常用于内核空间,或者在不允许被中断的关键代码段中,确保在获取到信号量资源之前不被中断。


    还有2个down系列的函数我就不一一介绍了列出函数原型仅供参考:

    int down_trylock(struct semaphore *sem);
    int down_timeout(struct semaphore *sem, long jiffies);

    下面我们介绍PV原理中的V,也就是up函数,基本注释如下:

    void up(struct semaphore *sem)
    {
    	unsigned long flags; //保存中断状态
    
    	spin_lock_irqsave(&sem->lock, flags); //自旋锁保护临界区
    	if (likely(list_empty(&sem->wait_list)))  //判断信号量的等待队列(wait_list)是否为空
    		sem->count++;                    //空则释放资源 count+1
    	else
    		__up(sem);              //如果等待队列不为空,表示有进程在等待信号量资源
                                    //需要调用__up函数进行唤醒操作
    	spin_unlock_irqrestore(&sem->lock, flags);  //解锁自旋锁并恢复中断状态
    }

    up函数用于对信号量进行上升操作(V操作)。如果等待队列为空,直接增加计数器的值,表示增加了可用的信号量资源。如果等待队列不为空,唤醒等待队列中的一个或多个进程,使它们可以继续执行。


    以上就是信号量PV操作的代码实现与解析。实现原理很简单,就是把资源抽象化然后用自旋锁来控制访问。最后在介绍一个比较重要的结构体:

    struct semaphore_waiter {
    	struct list_head list;
    	struct task_struct *task;
    	int up;
    };

    list字段是一个list_head类型的变量,它用于将struct semaphore_waiter结构体连接到一个链表中。list_head是Linux内核中双向链表的基本数据结构,它包含前后指针,用于链接多个元素。

    task字段是一个指向task_struct结构体的指针。task_struct是Linux内核中表示进程或线程的结构体,它包含了进程的各种信息,如进程ID、状态、寄存器等。在这个结构体中,task字段用于指向等待信号量资源的具体进程的task_struct结构。


     up字段是一个整数,用于记录进程是否已经被唤醒(或称为上升)。当进程等待信号量资源时,该字段的初始值为0。当进程被唤醒后,该字段的值会被设置为非零值,表示进程已经被唤醒。


    struct semaphore_waiter结构体用于表示等待信号量资源的进程。通过链表的方式,将多个等待进程连接起来。每个结构体中的task字段指向一个具体的进程,而up字段记录了该进程是否已经被唤醒。这样可以方便地管理和操作等待信号量资源的进程列表。


    最主要的还是理解进程同步中信号量的实现机制也就是PV操作。理解down和up函数所实现的原理,其他的并不是很重要。

你可能感兴趣的:(操作系统,链表,数据结构)