ARMLinux驱动开发:信号量实现LED设备互斥访问
一、实验概述
本实验通过信号量机制实现ARM Linux驱动中的并发控制,确保LED设备在多个进程同时访问时的互斥性,避免竞争条件。
二、关键概念
1. 并发与竞争问题
- 竞争条件:多个执行单元同时访问共享资源(如硬件寄存器)
- 临界区:需要互斥访问的代码区域
- 共享资源:本实验中的LED控制寄存器
2. 信号量机制
#include <linux/semaphore.h>
// 信号量结构体
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
// 常用API
void sema_init(struct semaphore *sem, int val); // 初始化
void down(struct semaphore *sem); // 获取信号量(可休眠)
int down_trylock(struct semaphore *sem); // 尝试获取(非阻塞)
void up(struct semaphore *sem); // 释放信号量
三、实验驱动代码实现
1. 驱动框架设计
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/semaphore.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define DEVICE_NAME "led_mutex"
#define LED_NUM 4
#define CLASS_NAME "led_class"
// LED设备结构体
struct led_device {
dev_t devno;
struct cdev cdev;
struct class *class;
struct device *device;
// 硬件相关
void __iomem *gpio_base;
unsigned int gpio_pins[LED_NUM];
// 信号量保护
struct semaphore led_sem;
// 当前状态
unsigned long led_state;
};
static struct led_device *led_dev;
2. 信号量初始化与使用
// 初始化信号量
static int led_semaphore_init(void)
{
// 初始化互斥信号量(初始值为1)
sema_init(&led_dev->led_sem, 1);
// 或者使用互斥信号量宏
// DEFINE_SEMAPHORE(led_dev->led_sem); // 静态定义
return 0;
}
// 带信号量保护的LED控制函数
static void set_led_state(int led_num, int state)
{
unsigned long flags;
unsigned int reg_val;
// 获取信号量(进入临界区)
if (down_interruptible(&led_dev->led_sem)) {
printk(KERN_ERR "Failed to get semaphore, interrupted\n");
return -ERESTARTSYS;
}
// 临界区开始
spin_lock_irqsave(&led_dev->hw_lock, flags);
// 读取当前GPIO状态
reg_val = readl(led_dev->gpio_base + GPIO_DATA_REG);
if (state) {
// 点亮LED
reg_val |= (1 << led_dev->gpio_pins[led_num]);
led_dev->led_state |= (1 << led_num);
} else {
// 熄灭LED
reg_val &= ~(1 << led_dev->gpio_pins[led_num]);
led_dev->led_state &= ~(1 << led_num);
}
// 写回寄存器
writel(reg_val, led_dev->gpio_base + GPIO_DATA_REG);
spin_unlock_irqrestore(&led_dev->hw_lock, flags);
// 临界区结束
// 释放信号量
up(&led_dev->led_sem);
printk(KERN_INFO "LED %d set to %s\n", led_num, state ? "ON" : "OFF");
}
3. 文件操作实现
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
struct led_cmd cmd;
if (count != sizeof(struct led_cmd))
return -EINVAL;
// 从用户空间拷贝命令
if (copy_from_user(&cmd, buf, sizeof(struct led_cmd)))
return -EFAULT;
// 验证参数
if (cmd.led_num >= LED_NUM || cmd.state > 1)
return -EINVAL;
// 获取信号量
if (down_trylock(&led_dev->led_sem)) {
// 非阻塞方式,如果获取失败立即返回
return -EBUSY;
}
// 临界区:控制LED
ret = control_led_hardware(cmd.led_num, cmd.state);
// 释放信号量
up(&led_dev->led_sem);
return ret ? ret : count;
}
static ssize_t led_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
unsigned long state;
if (*ppos > 0)
return 0;
// 获取信号量(可中断)
if (down_interruptible(&led_dev->led_sem)) {
return -ERESTARTSYS;
}
// 读取LED状态
state = led_dev->led_state;
// 释放信号量
up(&led_dev->led_sem);
// 拷贝到用户空间
if (copy_to_user(buf, &state, sizeof(state)))
return -EFAULT;
*ppos = sizeof(state);
return sizeof(state);
}
4. 完整的驱动初始化
static int __init led_mutex_init(void)
{
int ret;
int i;
// 1. 分配设备结构
led_dev = kzalloc(sizeof(struct led_device), GFP_KERNEL);
if (!led_dev)
return -ENOMEM;
// 2. 申请设备号
ret = alloc_chrdev_region(&led_dev->devno, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
goto fail_alloc;
}
// 3. 初始化信号量
sema_init(&led_dev->led_sem, 1); // 初始值为1(互斥信号量)
// 4. 初始化cdev
cdev_init(&led_dev->cdev, &led_fops);
led_dev->cdev.owner = THIS_MODULE;
// 5. 添加cdev到系统
ret = cdev_add(&led_dev->cdev, led_dev->devno, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
goto fail_cdev;
}
// 6. 创建设备类
led_dev->class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(led_dev->class)) {
ret = PTR_ERR(led_dev->class);
goto fail_class;
}
// 7. 创建设备节点
led_dev->device = device_create(led_dev->class, NULL,
led_dev->devno, NULL, DEVICE_NAME);
if (IS_ERR(led_dev->device)) {
ret = PTR_ERR(led_dev->device);
goto fail_device;
}
// 8. 映射硬件寄存器(示例)
led_dev->gpio_base = ioremap(GPIO_BASE_ADDR, SZ_4K);
if (!led_dev->gpio_base) {
ret = -ENOMEM;
goto fail_ioremap;
}
// 9. 配置GPIO引脚
for (i = 0; i < LED_NUM; i++) {
configure_gpio_as_output(led_dev->gpio_base, led_dev->gpio_pins[i]);
}
// 10. 初始化硬件锁
spin_lock_init(&led_dev->hw_lock);
printk(KERN_INFO "LED mutex driver loaded successfully\n");
return 0;
// 错误处理(略)
...
}
四、测试程序
1. 并发测试程序
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#define DEV_PATH "/dev/led_mutex"
struct led_cmd {
int led_num;
int state;
};
void *thread_func(void *arg)
{
int fd;
struct led_cmd cmd;
int thread_id = *(int *)arg;
fd = open(DEV_PATH, O_RDWR);
if (fd < 0) {
perror("Failed to open device");
pthread_exit(NULL);
}
// 随机控制LED
srand(time(NULL) + thread_id);
for (int i = 0; i < 10; i++) {
cmd.led_num = rand() % 4;
cmd.state = rand() % 2;
printf("Thread %d: Setting LED %d to %d\n",
thread_id, cmd.led_num, cmd.state);
if (write(fd, &cmd, sizeof(cmd)) < 0) {
if (errno == EBUSY) {
printf("Thread %d: Device busy, retrying...\n", thread_id);
usleep(1000); // 等待1ms后重试
continue;
}
perror("Write failed");
}
usleep(rand() % 10000); // 随机延迟
}
close(fd);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t threads[5];
int thread_ids[5];
int i;
// 创建5个并发线程
for (i = 0; i < 5; i++) {
thread_ids[i] = i;
if (pthread_create(&threads[i], NULL, thread_func, &thread_ids[i])) {
perror("Failed to create thread");
return 1;
}
}
// 等待所有线程完成
for (i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
printf("All threads completed successfully\n");
return 0;
}
2. 测试脚本
#!/bin/bash
# test_led_mutex.sh
echo "Loading LED mutex driver..."
sudo insmod led_mutex.ko
echo "Testing with single process..."
sudo ./test_single_process
echo "Testing with multiple processes..."
sudo ./test_concurrent_processes
echo "Testing concurrent threads..."
sudo ./test_concurrent_threads
echo "Checking kernel messages..."
dmesg | tail -20
echo "Cleaning up..."
sudo rmmod led_mutex
五、信号量使用注意事项
1. 信号量选择策略
// 1. 互斥访问(推荐)
sema_init(&sem, 1); // 初始值为1的互斥信号量
// 2. 允许多个读者
sema_init(&sem, 5); // 允许最多5个并发访问
// 3. 信号量获取方式
down(&sem); // 不可中断(可能造成死锁)
down_interruptible(&sem); // 可被信号中断(推荐)
down_trylock(&sem); // 非阻塞,立即返回
down_timeout(&sem, HZ); // 带超时等待
2. 避免常见问题
// 问题1:信号量未配对释放
void buggy_function(void)
{
down(&sem);
if (error_condition) {
return; // 错误!未释放信号量
}
up(&sem);
}
// 正确做法:使用goto或提前释放
void correct_function(void)
{
down(&sem);
if (error_condition) {
up(&sem); // 提前释放
return;
}
// 正常处理
up(&sem);
}
// 问题2:嵌套获取
void nested_access(void)
{
down(&sem);
another_function(); // 内部可能再次获取同一个信号量
up(&sem);
}
// 解决方案:使用可重入锁(mutex)或避免嵌套
六、性能优化建议
1. 读写信号量
#include <linux/rwsem.h>
// 读写信号量允许多个读者或一个写者
struct rw_semaphore led_rwsem;
// 初始化
init_rwsem(&led_rwsem);
// 读者
down_read(&led_rwsem);
// 读操作...
up_read(&led_rwsem);
// 写者
down_write(&led_rwsem);
// 写操作...
up_write(&led_rwsem);
2. 完成量机制
#include <linux/completion.h>
// 用于线程间同步
struct completion led_ready;
init_completion(&led_ready);
// 等待完成
wait_for_completion(&led_ready);
// 完成事件
complete(&led_ready);
七、调试技巧
1. 调试输出
// 添加调试信息
#define DEBUG_SEMAPHORE 1
#ifdef DEBUG_SEMAPHORE
#define sem_debug(fmt, args...) \
printk(KERN_DEBUG "LED_SEM: " fmt, ##args)
#else
#define sem_debug(fmt, args...)
#endif
// 在关键位置添加调试
down_interruptible(&led_dev->led_sem);
sem_debug("Semaphore acquired by process %d\n", current->pid);
up(&led_dev->led_sem);
sem_debug("Semaphore released by process %d\n", current->pid);
2. 死锁检测
// 使用调试工具检测死锁
// 1. 打开内核死锁检测
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_PROVE_LOCKING=y
// 2. 使用lockdep工具
#include <linux/lockdep.h>
// 声明锁类
static struct lock_class_key led_lock_key;
// 初始化时注册
lockdep_set_class(&led_dev->led_sem.lock, &led_lock_key);
八、实验总结
通过本实验,我们实现了:
信号量初始化:使用
sema_init()初始化互斥信号量
临界区保护:使用
down()/
up()保护LED硬件访问
并发控制:支持多进程/多线程安全访问
错误处理:正确处理信号量获取失败情况
资源清理:确保模块卸载时释放所有资源
关键知识点:
- 信号量是内核同步机制,可用于进程和中断上下文
- 互斥信号量(初始值为1)保证资源独占访问
down_interruptible()比down()更安全,可被信号中断
- 信号量必须成对使用,避免死锁
- 硬件访问需要同时考虑并发保护和硬件操作原子性
扩展思考:
如何优化信号量性能?
何时使用自旋锁替代信号量?
如何处理优先级反转问题?
如何实现读写信号量优化?
这个实验为理解Linux内核并发控制机制提供了实践基础,是设备驱动开发的重要技能。