信号量

信号量是 Linux 内核中一种允许进程进入睡眠状态等待资源的同步机制。它与自旋锁的“忙等待”形成鲜明对比,适用于**临界区执行时间可能较长、或者执行过程中可能发生阻塞(睡眠)**的场景。Linux 内核也提供了信号量机制,信号量常常用于控制对共享资源的访问

其核心原理基于一个计数器和一个等待队列

  1. 计数器 (count):
    • 表示可用资源的数量
    • count > 0 时,表示有资源可用,进程可以立即获取资源(减少计数器)并继续执行。
    • count = 0 时,表示资源已被占用完,后续试图获取资源的进程需要睡眠等待
    • 计数器初始值决定了信号量的类型:
      • 初始值 = 1: 称为互斥信号量 (Mutex Semaphore)二进制信号量 (Binary Semaphore)。这是最常用的类型,用于实现互斥访问,保证同一时刻只有一个进程可以进入临界区。它本质上可以当作一个允许睡眠的锁来用。
      • 初始值 = N (N > 1): 称为计数信号量 (Counting Semaphore)。用于控制对一类有多个实例的资源(如 N 个空闲缓冲区、N 个设备槽位)的并发访问。最多允许 N 个进程同时访问该资源。
  2. 等待队列 (wait_list):
    • 当进程尝试获取信号量 (down 操作) 但计数器为 0 时,该进程会被放入信号量的等待队列中。
    • 进程状态被置为 TASK_UNINTERRUPTIBLETASK_INTERRUPTIBLE(取决于调用的 API),主动放弃 CPU,进入睡眠状态。
    • 内核调度器会选择其他就绪状态的进程运行。
  3. down 操作 (P 操作, 获取信号量):
    • 进程尝试获取信号量。
    • 原子地检查计数器 count
      • 如果 count > 0,则将 count 减 1,进程成功获取信号量,继续执行临界区代码。
      • 如果 count = 0,进程将自己加入等待队列,设置状态为睡眠,然后调用调度器 schedule() 让出 CPU。
  4. up 操作 (V 操作, 释放信号量):
    • 进程离开临界区后,调用 up 释放信号量。
    • 原子地将计数器 count 加 1。
    • 检查等待队列:
      • 如果等待队列非空(有进程在睡眠等待),则唤醒队列中等待时间最长(通常是队列头部)的一个进程(或根据优先级策略唤醒)。
      • 被唤醒的进程会从它之前睡眠的地方(down 操作内部)继续执行,再次尝试减少计数器(此时因为 up 增加了计数,所以通常能成功),然后进入临界区。

核心特性

  • 睡眠等待: 这是与自旋锁最本质的区别。当资源不可用时,进程睡眠,不占用 CPU。
  • 适用于长临界区/可阻塞操作: 因为进程在等待时可以睡眠,所以临界区代码执行时间长、或者包含可能阻塞的操作(如 I/O 等待、磁盘读写、获取其他信号量/互斥锁、kmalloc(GFP_KERNEL) 内存分配)是允许的。
  • 互斥与计数: 通过初始计数实现互斥或资源池管理。
  • 可能导致睡眠: down 操作可能导致调用者进程睡眠。
  • 进程上下文: 信号量只能进程上下文中使用(因为睡眠操作需要进程上下文)。不能在中断上下文、软中断、Tasklet 等不可睡眠的上下文中使用信号量的 down 操作! (但 up 操作可以在中断上下文调用),因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  • 开销: 睡眠和唤醒操作涉及进程状态切换、调度器决策、等待队列管理等,开销比自旋锁大。因此,在临界区非常短且不会阻塞的场景下,自旋锁通常是更好的选择

核心API函数

1. 初始化信号量

1
2
3
4
5
6
7
8
9
10
11
12
#include <linux/semaphore.h>

// 静态初始化一个互斥信号量 (count = 1)
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

// 静态初始化一个计数信号量 (count = n)
#define DEFINE_SEMAPHORE_COUNT(name, n) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, n) // 注意:早期内核可能没有这个宏,常用下面动态初始化

// 动态初始化
void sema_init(struct semaphore *sem, int val); // val 是初始计数值
  • 示例:

    1
    2
    3
    DEFINE_SEMAPHORE(my_mutex); // 初始化一个互斥信号量 my_mutex
    struct semaphore my_count_sem;
    sema_init(&my_count_sem, 5); // 初始化一个计数信号量,初始有5个资源

2. 获取信号量 (down 操作 - P 操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 基本不可中断的 down 操作 (推荐)
void down(struct semaphore *sem);

// 可中断的 down 操作
int down_interruptible(struct semaphore *sem);
// 返回值:
// 0 - 成功获取信号量。
// -EINTR - 在等待过程中被信号中断。调用者应检查返回值并通常返回 -ERESTARTSYS。

// 非阻塞尝试获取
int down_trylock(struct semaphore *sem);
// 返回值:
// 0 - 成功获取信号量。
// 1 - 未能立即获取信号量 (count 为 0)。

// 带超时的 down 操作 (可中断)
int down_timeout(struct semaphore *sem, long jiffies);
// 返回值:
// 0 - 成功获取信号量。
// -ETIME - 在指定的 jiffies 超时时间内未获取到信号量。
// -EINTR - 在等待过程中被信号中断。
  • 选择指南:
    • down(): 最简单,但进程在等待时不能被信号唤醒。适用于必须完成等待的场景。
    • down_interruptible(): 最常用。允许进程在等待时被信号中断(如用户按了 Ctrl+C)。驱动程序等需要处理用户交互的场景必须使用此版本,并检查返回值。如果返回 -EINTR,通常需要向上层返回 -ERESTARTSYS
    • down_trylock(): 不想等待时使用。通常用于实现非阻塞操作或避免死锁检查。
    • down_timeout(): 需要限制等待时间的场景。

3. 释放信号量 (up 操作 - V 操作)

1
void up(struct semaphore *sem);
  • 这个函数相对简单:增加计数器 count,并唤醒等待队列中的一个进程(如果存在)。
  • 可以在进程上下文和中断上下文中安全调用。

读写信号量 (struct rw_semaphore)

类似于读写自旋锁,内核也提供了读写信号量,允许多个读者同时持有“读锁”,但写者必须独占持有“写锁”。

  • 变量类型: struct rw_semaphore

  • 初始化:

    1
    void init_rwsem(struct rw_semaphore *sem);
  • 读者操作:

    1
    2
    3
    void down_read(struct rw_semaphore *sem);       // 获取读锁 (可睡眠)
    int down_read_trylock(struct rw_semaphore *sem); // 尝试获取读锁 (非阻塞)
    void up_read(struct rw_semaphore *sem); // 释放读锁
  • 写者操作:

    1
    2
    3
    4
    void down_write(struct rw_semaphore *sem);       // 获取写锁 (可睡眠)
    int down_write_trylock(struct rw_semaphore *sem); // 尝试获取写锁 (非阻塞)
    void up_write(struct rw_semaphore *sem); // 释放写锁
    void downgrade_write(struct rw_semaphore *sem); // 将写锁降级为读锁 (释放写锁同时持有读锁)
  • 特性: 适用于“读多写少”的场景,且临界区较长或可能阻塞。同样遵循信号量的睡眠等待特性。

实验:

我们实验对LED的设备文件进行管理,在有单元对其进行访问的时候,其它所有单元都不能对其进行访问,主要的原理如下:

  1. 本次实验我们采用的是互斥信号量的原理进行编程的,我们将信号量初始化为1

    1
    sema_init(&leddev->sem,1);/* 初始化信号量 */
  2. 获取信号量,获取信号量,进入休眠状态的进程可以被信号打断 。在open函数中申请信号量,可以使用down函数,也可以使用down_interruptible函数。如果信号量值大于等于 1 就表示可用,那么应用程序就会开始使用 LED 灯。如果信号量值为 0 就表示应用程序不能使用 LED 灯,此时应用程序就会进入到休眠状态。等到信号量值大于 1 的时候应用程序就会唤醒,申请信号量,获取 LED 灯使用权。

    1
    2
    3
    if(down_interruptible(&leddev->sem)){
    return -ERESTARTSYS;
    }
  3. 释放信号量

    1
    up(&leddev->sem);

代码:

驱动代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/semaphore.h>


#define LEDDRV_CNT 1
#define LEDDRV_NAME "gpioled"
#define LED_OPEN 1
#define LED_CLOSE 0

struct leddev_struct{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
struct gpio_desc *leddes;
struct semaphore sem; /* 信号量 */
};
struct leddev_struct *leddev;

static int ledopen(struct inode *node, struct file *file)
{

file->private_data = leddev;
//printk("led device open\n");
if(down_interruptible(&leddev->sem)){
return -ERESTARTSYS;
}

return 0;
}

static int ledrelease (struct inode *node, struct file *file)
{
//printk("led device closed\n");
struct leddev_struct *leddev=(struct leddev_struct *)file->private_data;
up(&leddev->sem);

return 0;
}
static ssize_t ledwrite (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
struct leddev_struct *leddev=(struct leddev_struct *)file->private_data;
int ret=0;
char receive;
if(size!=1)
return -EINVAL;
ret=copy_from_user(&receive,buf,size);
if(ret<0){
return -EFAULT;
}

if(receive==LED_CLOSE){
//printk("led close\n");
gpiod_set_value(leddev->leddes,0);
}
else if(receive==LED_OPEN){
//printk("led open\n");
gpiod_set_value(leddev->leddes,1);
}
return size;
}

static struct file_operations led_fops={
.owner=THIS_MODULE,
.open=ledopen,
.write=ledwrite,
.release=ledrelease,
};

int leddrv_probe(struct platform_device *pdev){
int ret=0;
struct device *dev=&pdev->dev;
printk("matched success!\n");
leddev=devm_kzalloc(dev,sizeof(struct leddev_struct),GFP_KERNEL);
if(!leddev){
printk("kzalloc failed!\n");
return -ENOMEM;
}
if(leddev->major)
{
leddev->devid=MKDEV(leddev->major,0);
ret=register_chrdev_region(leddev->devid,LEDDRV_CNT,LEDDRV_NAME);
}
else{
ret=alloc_chrdev_region(&leddev->devid,0,LEDDRV_CNT,LEDDRV_NAME);
leddev->major=MAJOR(leddev->devid);
}
if(ret<0){
printk("devid register failed!\n");
goto chrdev_fail;
}
printk("major is %d\n",leddev->major);

cdev_init(&leddev->cdev,&led_fops);
ret=cdev_add(&leddev->cdev,leddev->devid,LEDDRV_CNT);
if(ret<0)
{
printk("cdev create error!\n");
goto cdev_fail;
}

leddev->class=class_create(THIS_MODULE,LEDDRV_NAME);
if(IS_ERR(leddev->class))
{
printk("class create error!\n");
ret=PTR_ERR(leddev->class);
goto class_fail;
}

leddev->device=device_create(leddev->class,NULL,leddev->devid,NULL,LEDDRV_NAME);
if(IS_ERR(leddev->device))
{
printk("device create error!\n");
ret=PTR_ERR(leddev->device);
goto device_fail;
}

leddev->leddes=devm_gpiod_get(dev,"led",GPIOD_OUT_LOW);
if(IS_ERR(leddev->leddes))
{
printk("leddes get error!\n");
ret=PTR_ERR(leddev->leddes);
goto leddes_fail;
}
platform_set_drvdata(pdev, leddev);

sema_init(&leddev->sem,1);/* 初始化信号量 */

return 0;
leddes_fail:
device_destroy(leddev->class,leddev->devid);
device_fail:
class_destroy(leddev->class);
class_fail:
cdev_del(&leddev->cdev);
cdev_fail:
unregister_chrdev_region(leddev->devid,LEDDRV_CNT);
chrdev_fail:

return ret;
}
int leddev_resume(struct platform_device *pdev){
struct leddev_struct *leddev = platform_get_drvdata(pdev);

device_destroy(leddev->class,leddev->devid);
class_destroy(leddev->class);
cdev_del(&leddev->cdev);
unregister_chrdev_region(leddev->devid,LEDDRV_CNT);

return 0;
}


const struct of_device_id led_device_id[]={
{.compatible="gpio-led"},
{},
};
struct platform_driver led_dev_driver={
.probe=leddrv_probe,
.driver={
.of_match_table=led_device_id,
.name="gpioled",
},
.remove=leddev_resume,
};

static int __init leddrv_init(void){
int ret=0;
platform_driver_register(&led_dev_driver);
return ret;
}

static void __exit leddrv_exit(void){
platform_driver_unregister(&led_dev_driver);
}


module_init(leddrv_init);
module_exit(leddrv_exit);

MODULE_AUTHOR("KEVIN");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO LED Platform Driver");

应用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char **argv) {
int fd;
char input_mode;

if (argc < 3) {
printf("Usage: ./%s /dev/<device> module(0/1)\n", argv[0]);
exit(1);
}

// 打开设备文件(确保有写权限)
fd = open(argv[1], O_WRONLY);
if (fd < 0) {
perror("open failed"); // 显示系统错误
exit(1);
}

// 解析并验证模块参数
input_mode = atoi(argv[2]);
if (input_mode != 0 && input_mode != 1) {
printf("Error: module must be 0 or 1\n");
exit(1);
}

// 写入数据并验证写入成功
if (write(fd, &input_mode, sizeof(input_mode)) != sizeof(input_mode)) {
perror("write failed"); // 显示系统错误
printf("Failed to write mode=%d\n", input_mode);
exit(1);
}
while(1);//让代码一直运行,来验证自旋锁的效果

close(fd); // 显式关闭文件描述符(虽然 exit 会自动关闭,但显式更好)
exit(0);
}

实验效果:

当信号量 sem 为 1 的时候表示 LED 灯还没有被使用,如果应用程序 A 要使用LED 灯,先调用 open 函数打开/dev/gpioled,这个时候会获取信号量 sem,获取成功以后 sem 的值减 1 变为 0。如果此时应用程序 B 也要使用 LED 灯,调用 open 函数打开/dev/gpioled 就会因为信号量无效(值为 0)而进入休眠状态。当应用程序 A 运行完毕,调用 release 函数关闭/dev/gpioled的时候就会释放信号量 sem,此时信号量 sem 的值就会加 1,变为 1。信号量 sem 再次有效,表示其他应用程序可以使用 LED 灯了,此时在休眠状态的应用程序 B 就会获取到信号量 sem,获取成功以后就开始使用 LED 灯。