内核信号量

信号量

信号量(Semaphore)是一种用于同步和互斥的机制,能够控制对共享资源的访问,广泛应用于多线程和多进程编程中。应用程序中使用的信号量是基于内核信号量实现的。

信号量的基本概念

信号量是一个计数器,用于管理对共享资源的访问。它支持两种基本操作:

  • P操作(**<font style="color:rgb(64, 64, 64);">sem_wait</font>**:尝试获取信号量。如果信号量的值大于 0,则将其减 1;否则,调用者会被阻塞,直到信号量的值变为正数。
  • V操作(**<font style="color:rgb(64, 64, 64);">sem_post</font>**:释放信号量。将信号量的值加 1,并唤醒等待该信号量的线程或进程。

信号量是由荷兰计算机科学家提出的,P/V是荷兰语的缩写。

信号量可以分为:

  • 二进制信号量(Binary Semaphore),又称互斥信号量:值只有 0 和 1,用于互斥。
  • 计数信号量(Counting Semaphore):值可以是任意非负整数,用于资源池管理。

内核信号量

信号量数据结构

内核中的信号量是 计数信号量(Counting Semaphore),它由一个 计数值(count)等待队列 组成:

  • count > 0:资源可用,线程/进程可以继续执行;
  • count == 0:资源已被占用,新的进程/线程必须阻塞;
  • count < 0:表示有多个进程/线程正在等待该资源。

内核中的信号量定义如下:

struct semaphore {
	raw_spinlock_t		lock; // 保护其他成员
	unsigned int		count; // 表示还可以允许多少个进程进入临界区
	struct list_head	wait_list; // 等待进入临界区的进程队列
};

信号量的操作

内核中的信号量提供以下几个主要操作:

  • 初始化信号量
  • 获取信号量(P操作)
  • 释放信号量(V操作)
初始化信号量

宏定义初始化信号量的方法:

  • __SEMAPHORE_INITIALIZER(name, n),指定信号量名称和计数值,允许n个进程同时进入临界区。
  • DEFINE_SEMAPHORE(name),初始化一个互斥信号量。
#define __SEMAPHORE_INITIALIZER(name, n)				\
{									\
	.lock		= __RAW_SPIN_LOCK_UNLOCKED((name).lock),	\
	.count		= n,						\
	.wait_list	= LIST_HEAD_INIT((name).wait_list),		\
}

#define DEFINE_SEMAPHORE(name)	\
	struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

函数初始化信号量的方法:

static inline void sema_init(struct semaphore *sem, int val)
{
	static struct lock_class_key __key;
	*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
	lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

将信号量的计数器设置为指定的初始值val,并初始化等待队列。

获取信号量(P操作)

<font style="color:rgb(64, 64, 64);">down()</font> 操作用于获取信号量。如果信号量的 <font style="color:rgb(64, 64, 64);">count > 0</font>,则直接减少 <font style="color:rgb(64, 64, 64);">count</font>;否则,当前进程会被阻塞并加入等待队列。

伪代码如下:

void down(struct semaphore *sem) {
    unsigned long flags;

    // 获取自旋锁,保护对信号量的操作
    raw_spin_lock_irqsave(&sem->lock, flags);

    if (sem->count > 0) {
        // 如果 count > 0,直接减少 count
        sem->count--;
    } else {
        // 如果 count == 0,将当前进程加入等待队列并阻塞
        struct task_struct *task = current;
        list_add_tail(&task->wait_entry, &sem->wait_list); // 加入等待队列
        set_current_state(TASK_UNINTERRUPTIBLE); // 设置进程状态为不可中断睡眠
        raw_spin_unlock_irqrestore(&sem->lock, flags); // 释放自旋锁
        schedule(); // 让出 CPU,进入睡眠状态

        // 当进程被唤醒后,重新获取自旋锁
        raw_spin_lock_irqsave(&sem->lock, flags);
        list_del(&task->wait_entry); // 从等待队列中移除
    }

    // 释放自旋锁
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}
  • 自旋锁的作用
    • 在修改 <font style="color:rgb(64, 64, 64);">count</font> 或操作 <font style="color:rgb(64, 64, 64);">wait_list</font> 时,必须持有自旋锁。
    • 自旋锁通过 <font style="color:rgb(64, 64, 64);">raw_spin_lock_irqsave</font><font style="color:rgb(64, 64, 64);">raw_spin_unlock_irqrestore</font> 实现,同时禁用本地 CPU 的中断。
  • 等待队列的作用
    • <font style="color:rgb(64, 64, 64);">count == 0</font> 时,当前进程会被加入等待队列,并进入睡眠状态(<font style="color:rgb(64, 64, 64);">TASK_UNINTERRUPTIBLE</font>)。
    • 当信号量被释放时(其他进程调用<font style="color:rgb(64, 64, 64);">up()</font>释放信号量),等待队列<font style="color:rgb(64, 64, 64);">wait_list</font>中队首的进程会被唤醒。
释放信号量(V操作)

<font style="color:rgb(64, 64, 64);">up()</font> 操作用于释放信号量。它会增加 <font style="color:rgb(64, 64, 64);">count</font>,并唤醒等待队列中的一个进程。

伪代码如下:

void up(struct semaphore *sem) {
    unsigned long flags;

    // 获取自旋锁,保护对信号量的操作
    raw_spin_lock_irqsave(&sem->lock, flags);

    if (list_empty(&sem->wait_list)) {
        // 如果等待队列为空,直接增加 count
        sem->count++;
    } else {
        // 如果等待队列不为空,唤醒队列中的第一个进程
        struct task_struct *task = list_first_entry(&sem->wait_list, struct task_struct, wait_entry);
        wake_up_process(task); // 唤醒进程
    }

    // 释放自旋锁
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}
  • 唤醒进程
    • 如果等待队列不为空,<font style="color:rgb(64, 64, 64);">up</font> 操作会唤醒队列中的第一个进程。
    • 被唤醒的进程会从 <font style="color:rgb(64, 64, 64);">down</font> 操作中的 <font style="color:rgb(64, 64, 64);">schedule()</font> 返回,并继续执行。

信号量的特性

基于上述实现,信号量具有以下特性:

  • 原子性
    • 通过自旋锁确保对 <font style="color:rgb(64, 64, 64);">count</font><font style="color:rgb(64, 64, 64);">wait_list</font> 的操作是原子的。
  • 阻塞机制
    • <font style="color:rgb(64, 64, 64);">count == 0</font> 时,请求信号量的进程会被阻塞并加入等待队列。
  • 唤醒机制
    • 当信号量被释放时,等待队列中的进程会被唤醒。

信号量的变体

Linux 内核提供了多种信号量变体,以适应不同的使用场景:

  • **<font style="color:rgb(64, 64, 64);">down_interruptible</font>**
    • 类似于 <font style="color:rgb(64, 64, 64);">down</font>,但可以被信号中断(返回 <font style="color:rgb(64, 64, 64);">-EINTR</font>)。
  • **<font style="color:rgb(64, 64, 64);">down_killable</font>**:
    • 类似于 <font style="color:rgb(64, 64, 64);">down</font>,但可以被kill信号中断
  • **<font style="color:rgb(64, 64, 64);">down_trylock</font>**
    • 非阻塞版本,如果 <font style="color:rgb(64, 64, 64);">count == 0</font>,立即返回失败。
  • **<font style="color:rgb(64, 64, 64);">down_timeout</font>**
    • 带有超时机制的 <font style="color:rgb(64, 64, 64);">down</font> 操作,如果超时仍未获取到信号量,返回失败。

信号量的应用场景

信号量在内核中广泛应用于以下场景:

  • 资源管理
    • 用于管理有限的资源(如内存池、设备访问)。
  • 同步机制
    • 用于多个进程或线程之间的同步。
  • 互斥锁的实现
    • 二进制信号量(<font style="color:rgb(64, 64, 64);">count = 1</font>)可以用作互斥锁。

<font style="color:rgb(64, 64, 64);">mutex</font> 的区别

特性SemaphoreMutex
计数值>=0可多个进程/线程同时获取只能是 01
是否允许多个进程持有
释放机制任何进程 都能 up()释放,即使它没有获取过信号量只能由持有者释放
递归锁不支持,递归执行P操作会死锁支持(posix可以设置mutex属性支持递归,内核mutex不支持递归)
适用场景资源计数(如生产者-消费者)互斥访问(如临界区保护)

如果需要纯粹的互斥锁,应该使用 **mutex**,而不是 **semaphore**

Sample

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/semaphore.h>

static struct semaphore my_sema;

int my_thread(void *arg) {
    down(&my_sema); // 获取信号量
    printk(KERN_INFO "Thread running...\n");
    msleep(5000);
    up(&my_sema); // 释放信号量
    return 0;
}

static int __init my_init(void) {
    sema_init(&my_sema, 1); // 初始化信号量,初始值 1
    kthread_run(my_thread, NULL, "my_thread");
    return 0;
}

static void __exit my_exit(void) {
    printk(KERN_INFO "Module exit\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

用户空间的信号量

应用程序使用的信号量,实现上可以依赖内核信号量,也可以基于内核其它互斥机制实现,甚至可以直接在用户空间实现

Linux 提供了两种主要的信号量实现:

  1. System V 信号量:较老的实现,功能强大但接口复杂。
  2. POSIX 信号量:较新的实现,接口简单且更符合现代编程需求。

POSIX 信号量是更推荐的方式,因为它接口简单且易于使用。POSIX信号量在实现上与内核中的信号量有所不同,但它们的基本原理是相同的,都是用于同步多线程或多进程访问共享资源。POSIX 信号量分为两种:

  1. 进程间信号量(命名信号量):基于文件系统实现的信号量(实际实现依赖于内核的共享内存,而不是普通的文件系统存储),用于不同进程间的同步。
  2. 进程内信号量(未命名信号量):基于内存实现的信号量,用于同一进程中的线程同步。

(1)命名信号量(进程间同步)

命名信号量通过一个名字标识,可以在不同进程之间共享。

在 Linux 中,POSIX 命名信号量 会以 伪文件 形式出现在 <font style="color:rgb(64, 64, 64);">/dev/shm/sem.<name></font>,但它们并不是真正的文件,而是由 内核管理的共享内存对象

ls -l /dev/shm/sem.*

示例输出:

-rw------- 1 user user 32 Feb 13 12:00 /dev/shm/sem.my_sem
  • 这个 “文件” 只是 POSIX 共享内存的标识,但数据本身在内核空间管理。
  • 进程间可以通过 **sem_open()****sem_wait()****sem_post()** 等方法访问这个共享信号量。
Sample
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>

int main() {
    const char *sem_name = "/my_named_sem";
    sem_t *sem;

    // 创建或打开一个命名信号量
    sem = sem_open(sem_name, O_CREAT, 0644, 1);  // 初始值为 1
    if (sem == SEM_FAILED) {
        perror("sem_open failed");
        return 1;
    }
    printf("Semaphore opened successfully\n");

    // 获取信号量(P 操作)
    if (sem_wait(sem) < 0) {
        perror("sem_wait failed");
        return 1;
    }

    printf("Critical section start\n");
    sleep(2);  // 模拟临界区操作
    printf("Critical section end\n");

    // 释放信号量(V 操作)
    if (sem_post(sem) < 0) {
        perror("sem_post failed");
        return 1;
    }

    // 关闭信号量
    sem_close(sem);

    // 删除信号量
    sem_unlink(sem_name);

    return 0;
}
关键函数
  • <font style="color:rgb(64, 64, 64);">sem_open</font>:创建或打开一个命名信号量。
  • <font style="color:rgb(64, 64, 64);">sem_wait</font>:获取信号量(P 操作),如果信号量为 0,则阻塞。
  • <font style="color:rgb(64, 64, 64);">sem_post</font>:释放信号量(V 操作),增加信号量的值。
  • <font style="color:rgb(64, 64, 64);">sem_close</font>关闭 进程当前打开的信号量句柄,但不会删除信号量
  • <font style="color:rgb(64, 64, 64);">sem_unlink</font>:删除命名信号量(从 <font style="color:rgb(64, 64, 64);">/dev/shm/</font> 中移除)。

(2)未命名信号量(线程间同步)

未命名信号量通常用于线程间同步,需要放在共享内存中。未命名信号量其实也可以用于进程间通信,但要将信号量放在共享内存中,使用比较麻烦,一般不这样用。

Sample
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t sem;

void* thread_func(void* arg) {
    printf("Thread waiting for semaphore\n");
    sem_wait(&sem);  // 获取信号量
    printf("Thread entered critical section\n");
    sleep(2);  // 模拟临界区操作
    printf("Thread leaving critical section\n");
    sem_post(&sem);  // 释放信号量
    return NULL;
}

int main() {
    pthread_t tid;

    // 初始化未命名信号量
    if (sem_init(&sem, 0, 1) < 0) {  // 初始值为 1
        perror("sem_init failed");
        return 1;
    }

    // 创建线程
    if (pthread_create(&tid, NULL, thread_func, NULL) < 0) {
        perror("pthread_create failed");
        return 1;
    }

    // 主线程获取信号量
    printf("Main thread waiting for semaphore\n");
    sem_wait(&sem);
    printf("Main thread entered critical section\n");
    sleep(2);  // 模拟临界区操作
    printf("Main thread leaving critical section\n");
    sem_post(&sem);

    // 等待线程结束
    pthread_join(tid, NULL);

    // 销毁信号量
    sem_destroy(&sem);

    return 0;
}
关键函数
  • <font style="color:rgb(64, 64, 64);">sem_init</font>:初始化未命名信号量。
  • <font style="color:rgb(64, 64, 64);">sem_wait</font>:获取信号量(P 操作)。
  • <font style="color:rgb(64, 64, 64);">sem_post</font>:释放信号量(V 操作)。
  • <font style="color:rgb(64, 64, 64);">sem_destroy</font>:销毁未命名信号量。

几个需要注意的问题:

  1. sem_wait()是否会被信号(SIGINT)打断?

不确定,取决于操作系统实现和具体的使用场景。

  1. 任何进程都可以通过sem_post()释放信号量,即使该进程没有获取到信号量。

虽然没有限制,但不建议这样使用。

参考资料

  1. Professional Linux Kernel Architecture,Wolfgang Mauerer
  2. Linux内核深度解析,余华兵
  3. Linux设备驱动开发详解,宋宝华
  4. linux kernel 4.12
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值