Linux并发与竞争实验,原子操作实现 LED 互斥访问的核心逻辑,并给出完整、规范、可直接用于开发的代码模板,同时补充其余三种并发控制机制的核心差异。
一、原子操作实验核心逻辑
原子操作的本质,是让变量的读写操作“不可被打断”,本实验用一个值为 1的原子变量当“设备使用许可证”。
应用想操作 LED,必须先尝试把许可证从 1减成 0(只有能减成功,才算拿到许可证)。
如果此时许可证已经是 0,说明其他应用正在用,本应用拿不到,直接报错。
应用用完后,再把许可证从 0加回 1,让出设备控制权。
这种机制保证同一时刻只有一个应用能操作 LED,实现互斥。
二、完整开发代码模板
以下代码是可直接用于开发的规范版本,分为驱动(atomic.c)和测试应用(atomicApp.c)两部分。
1. 驱动代码(atomic.c)
#include<linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/atomic.h>
#defineGPIOLED_CNT 1 // 设备号数量:1个LED对应1个设备
#defineGPIOLED_NAME "gpioled"// 设备名
#define LED_OFF 0
#define LED_ON 1// LED设备结构体
structgpioled_dev {dev_t devid; // 设备号
struct cdev cdev; // 字符设备结构体
struct class *class; // 设备类
struct device *device;// 设备实例int major;
// 主设备号
int minor; // 次设备号
struct device_node *nd; // 设备树节点int led_gpio; // LED对应GPIO编号atomic_tlock; // 原子变量:作为设备互斥锁(初始值=1)
};
static struct gpioled_dev gpioled;// 打开设备:尝试获取设备控制权
static int led_open(struct inode *inode, struct file *filp) {
// 原子减1,判断是否能成功拿到锁(许可证)// 如果减1后结果是0,返回true;否则返回false(说明已被占用)
if(!atomic_dec_and_test(&gpioled.lock)) {
// 拿不到锁,把锁加回1,保持原样,返回“忙”
atomic_inc(&gpioled.lock);return -EBUSY;}
// 拿到锁,设置文件私有数据为设备结构体(方便后续操作访问)
filp->private_data = &gpioled;return 0;}
// 从设备读数据(本实验读操作仅确认状态,不做实质功能)
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {return 0;}
// 向设备写数据:控制LED亮灭
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
int ret;unsigned char user_cmd;struct gpioled_dev *dev = filp->private_data;
// 把用户态数据拷贝到内核态
ret = copy_from_user(&user_cmd, buf, cnt);if (ret <0) {return -EFAULT;}// 根据指令控制
LED GPIOif (user_cmd == LED_ON) {
gpio_set_value(dev->led_gpio, 0); // 假设GPIO拉低点亮LED
} else if (user_cmd == LED_OFF) {
gpio_set_value(dev->led_gpio, 1); // 假设GPIO拉高熄灭LED
}return 0;}
// 关闭设备:释放设备控制权
static int led_release(struct inode *inode, struct file *filp) {
struct gpioled_dev *dev = filp->private_data;// 把锁从0加回1,释放设备
atomic_inc(&dev->lock);return 0;}// 设备操作函数集合
static struct file_operations gpioled_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release = led_release,};// 驱动初始化函数
static int __init led_init(void) {int ret = 0;// 初始化原子变量,初始值为1(表示设备空闲)
atomic_set(&gpioled.lock, 1);// 从设备树获取LED节点
gpioled.nd = of_find_node_by_path("/gpioled");
if (!gpioled.nd) {printk(KERN_ERR "gpioled node not found!\n");
return -ENODEV;}
printk(KERN_INFO "gpioled node found!\n");// 获取设备树中led-gpio属性的GPIO编号
gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
if (gpioled.led_gpio <0) {
printk(KERN_ERR "get led-gpio failed!\n");
return -EINVAL;}// 申请GPIO并设置为输出模式
ret = gpio_request(gpioled.led_gpio, "led_gpio");
if (ret) {
printk(KERN_ERR "gpio request failed!\n");return ret;}
gpio_direction_output(gpioled.led_gpio, 1); // 初始熄灭LED// 分配主设备号(动态分配)
ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
if (ret <0) {printk(KERN_ERR "alloc_chrdev_region failed!\n");goto err_gpio_free;}
gpioled.major = MAJOR(gpioled.devid);gpioled.minor = MINOR(gpioled.devid);
// 初始化cdev
cdev_init(&gpioled.cdev, &gpioled_fops);gpioled.cdev.owner = THIS_MODULE;// 添加cdev到系统
ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);if (ret <0) {printk(KERN_ERR "cdev_add failed!\n");goto err_unreg_region;}
// 创建设备类
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
if (IS_ERR(gpioled.class)) {ret = PTR_ERR(gpioled.class);printk(KERN_ERR "class_create failed!\n");goto err_cdev_del;}// 创建设备节点(自动生成 /dev/gpioled)
gpioled.device = device_create(gpioled.class,NULL,MKDEV(gpioled.major, gpioled.minor),NULL, GPIOLED_NAME);if (IS_ERR(gpioled.device)) {
ret = PTR_ERR(gpioled.device);printk(KERN_ERR "device_create failed!\n");goto err_class_del;}printk(KERN_INFO "atomic driver init success!\n");
return 0;
// 错误处理:逆序释放资源err_class_del:class_destroy(gpioled.class);
err_cdev_del:cdev_del(&gpioled.cdev);
err_unreg_region:unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
err_gpio_free:gpio_free(gpioled.led_gpio);return ret;}// 驱动卸载函数
static void __exit led_exit(void) {// 逆序释放所有资源
device_destroy(gpioled.class, MKDEV(gpioled.major, gpioled.minor));
class_destroy(gpioled.class);
cdev_del(&gpioled.cdev);unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
gpio_free(gpioled.led_gpio);printk(KERN_INFO "atomic driver exit!\n");}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
MODULE_INFO("version", "1.0");
2. 测试应用代码(atomicApp.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>// 用法提示
static void usage(char *argv[]) {
printf("Usage: %s <cmd>rn", "1:openLED", "0: close LED");
printf("%s 0 /dev/gpioled\n", argv[0]);printf("%s 1 /dev/gpioled\n", argv[0]);}
int main(int argc, char *argv[]) {
if (argc != 3) {usage(argv);return -1;}
// 打开设备文件
int fd = open(argv[2], O_RDWR);if (fd <0) {
perror("open device failed!");return -1;}// 获取用户输入的控制指令(亮/灭)
unsigned charcmd = atoi(argv[1]);int ret = write(fd, &cmd, 1); // 向驱动写入指令
if (ret <0) {perror("write failed!");close(fd);return -1;}// 模拟应用占用设备25秒(5次睡眠,每次5秒)
int cnt = 0;while (1) {sleep(5);cnt++;printf("App running times: %d\n", cnt);
if (cnt >= 5) break;}// 关闭设备文件(释放驱动的原子锁)
close(fd);
return 0;}
3. 配套Makefile文件
指定内核源码路径(需替换为你实际的内核路径)KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
当前模块名(和驱动源文件名 atomic.c 对应)obj-m := atomic.o
当前路径CURRENT_PATH := $(shell pwd)all:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
三、其余三种并发控制机制的核心差异。
除了原子操作,还有自旋锁、信号量、互斥体三种常用并发控制机制,它们各有适用场景,核心区别如下:
1. 自旋锁(spinlock)
工作方式:如果申请不到锁,内核会在原地“忙等待”,不停循环检查锁是否被释放,直到拿到锁为止,期间不会睡眠、不会让出CPU。
适用场景:临界区代码执行时间极短(比如几行代码,微秒级别),且只能用于非中断上下文(比如进程上下文)。
优点:开销极小,不需要切换上下文,拿到锁的速度最快。
缺点:如果临界区时间长,CPU会被一直占用,浪费资源;且不能用于中断上下文(中断中忙等待会导致系统死锁)。
2. 信号量(semaphore)
工作方式:如果申请不到锁,内核会让当前进程进入睡眠状态,把进程挂到等待队列上,等锁被释放时,再由持有锁的进程唤醒等待的进程。
适用场景:临界区代码执行时间较长(比如秒级),允许进程睡眠,既可用于进程上下文,也可用于中断上下文(需特殊处理)。
优点:不会占用CPU空转,适合处理长时间任务,让CPU去做其他事。
缺点:有睡眠开销,申请和释放锁的速度比自旋锁慢;资源占用比互斥体略高。
3. 互斥体(mutex)
工作方式:本质是一种特殊的二值信号量(初始值为1),专门用于实现进程间互斥。申请不到锁时,进程会睡眠等待,锁释放后自动唤醒。
适用场景:主要用于进程之间的互斥访问,对访问顺序有严格要求(谁拿到锁谁用,严格互斥)。
优点:比信号量更轻量、高效,代码可读性更好;系统提供了严格的错误检查,避免误用(比如重复加锁、解锁未加锁的互斥体等)。
缺点:只能用于进程上下文,不能用于中断上下文(因为涉及到睡眠和唤醒,中断中不能睡眠);也不能实现资源计数(只能控制单一资源互斥,不像信号量可以控制多个同类资源)。
四、四者核心对比
机制
核心特点
睡眠行为
适用场景
原子操作
针对变量,操作绝对不可打断
不睡眠
变量级保护,简单互斥
自旋锁
忙等待,死磕不放
不睡眠
极短临界区,非中断上下文
信号量
睡眠等待,支持多资源计数
申请不到则睡眠7
长时间临界区,资源计数场景
互斥体
专用互斥工具,严格二值控制
申请不到则睡眠
进程间单一资源互斥,高效安全
简言之:
变量级简单保护选原子操作;
短任务、不能睡眠选自旋锁;
长任务、需要睡眠选信号量或互斥体;
纯进程互斥、追求安全高效优先选互斥体。