Linux驱动开发笔记(七)——并发与竞争(下)——自旋锁&信号量&互斥体

视频:第10.2讲 Linux并发与竞争试验-自旋锁、信号量与互斥体_哔哩哔哩_bilibili


1. 自旋锁

文档:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》47.3节

函数

cpp 复制代码
// include/linux/spinlock_types.h
typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
		struct {
			u8 __padding[LOCK_PADSIZE];
			struct lockdep_map dep_map;
		};
#endif
	};
} spinlock_t;

API函数的定义在include/linux/spinlock.h

线程与线程之间:

表中的自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间。被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则可能导致死锁:

自旋锁会自动禁止抢占,也就是当线程A得到锁以后会暂时禁止内核抢占。如果线程A在持有锁期间进入休眠,那么线程A会自动放弃CPU,此时线程B开始运行,线程B要获取锁,但是此时锁被A线程持有,而且内核还禁止抢占,此时便死锁了。

线程与中断之间:

如上图,线程A先运行并获得lock。当线程A运行functionA时候,中断发生并抢走了CPU使用权。右边的中断服务函数也要获取lock,但lock被A占有,中断一直等待。但是在中断服务函数执行完之前,线程A无法执行,此时便死锁了。

最好的解决方法就是获取锁之前关闭本地中断(本CPU中断),Linux内核提供了相应的API函数:

使用spin_lock_irq/spin_unlock_irq时用户需要能够确定加锁之前的中断状态,但实际上很难做到,因此不推荐使用这两个函数。

建议使用spin_lock_irqsave/ spin_unlock_irqrestore,这组函数会保存中断状态 / 恢复中断状态。一般在线程中使用spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock,示例代码如下所示:

cpp 复制代码
DEFINE_SPINLOCK(lock);  // 定义并初始化一个自旋锁

/* 线程A */
void functionA() {
    unsigned long flags;  // 用于保存中断状态

    // 获取锁并关闭本地中断,保存中断状态到flags
    spin_lock_irqsave(&lock, flags);

    /* 临界区 */
    // 在此执行共享资源操作

    // 释放锁并恢复中断状态
    spin_unlock_irqrestore(&lock, flags);
}

/* 中断服务函数 */
void irq() {
    // 获取锁
    spin_lock(&lock);

    /* 临界区 */
    // 在此执行共享资源操作

    // 释放锁
    spin_unlock(&lock);
}

自旋锁注意事项

《开发指南》47.3.4节

①、等待自旋锁时处于"自旋",因此锁的持有时间不能太长,否则会降低系统性能。如果临界区比较大、运行时间比较长,要选择其他的并发处理方式,如信号量和互斥体。

②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的API函数,否则可能导致死锁。

③、不能递归申请自旋锁,否则死锁。

④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管用的是单核还是多核SOC,都将其当做多核SOC来编写驱动程序。

2. 信号量

详见《开发指南》47.4节

相较于自旋锁,信号量的特点:

①、信号量会以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

②、信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

③、如果共享资源的持有时间比较短,不适合使用信号量,因为频繁休眠、切换线程的开销要远大于信号量带来的优势。

函数

cpp 复制代码
struct semaphore { 
    raw_spinlock_t      lock; 
    unsigned int        count; 
    struct list_head    wait_list; 
}; 

|-------------------------------------------------|--------------------------|
| DEFINE_SEAMPHORE(name) | 定义一个二值信号量,信号量的值为1 |
| void sema_init(struct semaphore *sem, int val) | 设置信号量sem的值为val |
| void down(struct semaphore *sem) | 获取信号量,信号量-1,导致休眠,不可被信号打断 |
| void up(struct semaphore *sem) | 释放信号量,信号量+1,导致休眠,不可被信号打断 |
| int down_trylock(struct semaphore *sem); | 获取信号量,信号量-1,导致休眠,可被信号打断 |
| int down_interruptible(struct semaphore *sem) | 释放信号量,信号量+1,导致休眠,可被信号打断 |

3. 互斥体

详见《开发指南》47.5节

互斥访问:一次只能有一个线程可以访问共享资源。

虽然用二值信号量就可以进行互斥访问,但是Linux提供了更专业的机制------互斥体mutex,其特点如下:

①、mutex可以导致休眠,因此不能在中断中使用mutex。中断中只能使用自旋锁。

②、和信号量一样,mutex保护的临界区可以调用引起阻塞的API函数。

③、因为一次只有一个线程可以持有mutex,因此,必须由mutex的持有者释放mutex。并且mutex不能递归上锁和解锁。

mutex互斥体定义如下:

cpp 复制代码
struct mutex { 
    /* 1: unlocked, 0: locked, negative: locked, possible waiters */ 
    atomic_t    count; 
    spinlock_t  wait_lock; 
}; 

函数

4. 自旋锁实验

第10.4讲 Linux并发与竞争试验-自旋锁、信号量以及互斥体操作实验_哔哩哔哩_bilibili

4.1 文件结构

cpp 复制代码
9_SPINLOCK (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 9_spinlock.code-workspace
├── Makefile
├── spinlockAPP.c
└── spinlock.c

4.2 Makefile

cpp 复制代码
CFLAGS_MODULE += -w

KERNELDIR := /home/for/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子"01、例程源码"中直接搜,cp到虚拟机里面)

CURRENT_PATH := $(shell pwd)	# 当前路径

obj-m := spinlock.o			# 编译文件

build: kernel_modules			# 编译模块

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean	

4.3 spinlock.c

主要有以下更新:

增加了 头文件引用 #include <linux/spinlock_types.h>

增加了 设备结构体中 自旋锁和设备状态 两个变量

增加了 驱动入口函数中 对自旋锁的初始化

增加了 led_open中 加锁解锁和临界区操作

增加了 led_release中 加锁解锁和临界区操作

备注:

实际使用过程中可能会有中断的问题,因此使用spin_lock_irqsave和spin_unlock_irqrestore更好。

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/spinlock_types.h>

#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1

#define LEDON 1
#define LEDOFF 0


/* 设备结构体 */
struct gpioled_dev{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct device *device;
    struct class *class;
    struct device_node *nd;
    int led_gpio;

    int dev_status;  /* 0表示设备可以使用,1不可使用*/
    spinlock_t lock;
};

struct gpioled_dev gpioled;

/* 操作集 */
static int led_release(struct inode *inode, struct file *filp){
    unsigned long irqflag;
    struct gpioled_dev *dev = filp->private_data;

    spin_lock_irqsave(&dev->lock, irqflag); // 加锁=================
    if(gpioled.dev_status){
        gpioled.dev_status = 0; // 设备标记为被空闲
    }
    spin_unlock_irqrestore(&dev->lock, irqflag); // 解锁===============

    return 0;
}
static int led_open(struct inode *inode, struct file *filp){
    unsigned long irqflag;
    filp->private_data = &gpioled;

    spin_lock_irqsave(&gpioled.lock, irqflag); // 加锁=================

    if(gpioled.dev_status){ /* 设备不可使用 */
        spin_unlock(&gpioled.lock); //解锁
        printk("BUSY!!!\r\n");
        return -EBUSY;
    }
    gpioled.dev_status = 1; // 设备标记为被占用

    spin_unlock_irqrestore(&gpioled.lock, irqflag); //解锁====================
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    int ret;
    unsigned char databuf[1];
    struct gpioled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if(ret < 0){
        return -EINVAL;
    }
    if(databuf[0] == LEDON){ //开灯
        gpio_set_value(dev->led_gpio, 0); // 低电平开灯
    } else if(databuf[0] == LEDOFF){
        gpio_set_value(dev->led_gpio, 1); // 高电平关灯
    }

    return 0;
}
static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .write = led_write,
    .open = led_open,
    .release = led_release,
};


/* 驱动入口 */
static int __init led_init(void){
    int ret = 0;

    /* 初始化自旋锁 */
    spin_lock_init(&gpioled.lock);
    gpioled.dev_status = 0;


    /* 1.注册字符设备驱动 */
    gpioled.devid = 0;
    if(gpioled.devid){
        gpioled.devid = MKDEV(gpioled.devid, 0);
        register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
    } else {
        alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }

    /* 2.初始化cdev */
    gpioled.cdev.owner = THIS_MODULE;  
    cdev_init(&gpioled.cdev, &led_fops);

    /* 3.添加cdev */
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); // 错误处理先略过了

    /* 4.创建类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if(IS_ERR(gpioled.class)){
        return PTR_ERR(gpioled.class);
    }

    /* 5.创建设备 */
    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if(IS_ERR(gpioled.device)){
        return PTR_ERR(gpioled.device);
    }

    /* 1.获取设备节点 */
    gpioled.nd = of_find_node_by_path("/gpioled");  // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点
    if(gpioled.nd == NULL){
        ret = -EINVAL;
        goto fail_findnode;
    }

    /* 2.获取LED对应的GPIO */  // 也就是节点中led-gpios那一行内容
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);
    if(gpioled.led_gpio < 0){
        printk("can't find led_gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;
    }
    printk("led_gpio num = %d\r\n",gpioled.led_gpio);

    /*  3.申请IO */
    ret = gpio_request(gpioled.led_gpio, "led-gpios");
    if(ret){
        printk("Failed to request the led gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;        
    }

    /* 4.使用IO,设置为输出 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if(ret){
        goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO
    }

    /* 5.输出高电平,关灯 */
    gpio_set_value(gpioled.led_gpio, 1);

    return 0;

fail_setoutput:
    gpio_free(gpioled.led_gpio);
fail_findnode:
    return ret;
}

/* 驱动出口 */
static void __exit led_exit(void){
    gpio_set_value(gpioled.led_gpio, 1); // 高电平 关灯

    /* 注销字符设备驱动 */
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);

    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);

    gpio_free(gpioled.led_gpio);

}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

4.4 spinlockAPP.c

主要有以下更新:

完全没有更新:( 直接贴过来

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用   ./atomicAPP <filename> <0:1>  0关灯,1开灯
 * ./atomicAPP /dev/gpioled 0  关灯
 * ./atomicAPP /dev/gpioled 1  开灯
 */ 


#define LEDOFF 0
#define LEDON  1

int main(int argc, char *argv[]){
    if(argc != 3){  // 判断用法是否错误
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename;
    int fd = 0;
    unsigned char databuf[1];
    int retvalue = 0;
    int cnt = 0;

    filename = argv[1];
    fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filename

    if(fd <0){
        printf("file %s open failed!\r\n");
        return -1;
    }
    
    databuf[0] = atoi(argv[2]);  // char 2 int

    retvalue = write(fd, databuf, sizeof(databuf));   // 根据驱动里的操作集.write = led_write,执行led_write()函数
    if(retvalue <0){
        printf("LED Control Failed!\r\n");
        close(fd);
        return -1;
    }

    /* 模拟应用占用 */
    while(1){
        sleep(3);
        cnt++;
        printf("APP Runing times: %d\r\n",cnt);
        if(cnt >= 5)break;
    }
    printf("App finished!\r\n");

    close(fd);
    return 0;
}

4.5 测试

cpp 复制代码
# VSCODE终端
make
arm-linux-gnueabihf-gcc spinlockAPP.c -o spinlockAPP
sudo cp spinlock.ko spinlockAPP /..../nfs/rootfs/lib/modules/4.1.15/


# 串口

和原子操作实验8一样,如果一个应用程序没有执行完,再执行下一个应用程序会报错。

5. 信号量实验

懒得重新创新的文件夹,直接在上面的代码改了。

spinlock.c主要有以下更新:

增加了 头文件引用 #include <linux/semaphore.h>

增加了 设备结构体中 semaphore类型信号量

增加了 驱动入口函数中 对信号量的初始化

增加了 led_open中 信号量加1

增加了 led_release中 信号量减1。信号量的特点是能让应用休眠,因此led_release中不需要其他操作,信号量不为0时会自动唤醒休眠的应用。

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>

#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1

#define LEDON 1
#define LEDOFF 0


/* 设备结构体 */
struct gpioled_dev{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct device *device;
    struct class *class;
    struct device_node *nd;
    int led_gpio;

    struct semaphore sem;
};

struct gpioled_dev gpioled;

/* 操作集 */
static int led_release(struct inode *inode, struct file *filp){
    struct gpioled_dev *dev = filp->private_data;

    up(&gpioled.sem); //信号量加1

    return 0;
}
static int led_open(struct inode *inode, struct file *filp){
    filp->private_data = &gpioled;

    down(&gpioled.sem); // 信号量减1。信号量的特点是能让应用休眠,因此此处不需要其他操作,信号量不为0时会自动唤醒休眠的应用。

    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    int ret;
    unsigned char databuf[1];
    struct gpioled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if(ret < 0){
        return -EINVAL;
    }
    if(databuf[0] == LEDON){ //开灯
        gpio_set_value(dev->led_gpio, 0); // 低电平开灯
    } else if(databuf[0] == LEDOFF){
        gpio_set_value(dev->led_gpio, 1); // 高电平关灯
    }

    return 0;
}
static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .write = led_write,
    .open = led_open,
    .release = led_release,
};


/* 驱动入口 */
static int __init led_init(void){
    int ret = 0;

    /* 初始化信号量 */
    sema_init(&gpioled.sem, 1); // 二值信号量


    /* 1.注册字符设备驱动 */
    gpioled.devid = 0;
    if(gpioled.devid){
        gpioled.devid = MKDEV(gpioled.devid, 0);
        register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
    } else {
        alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }

    /* 2.初始化cdev */
    gpioled.cdev.owner = THIS_MODULE;  
    cdev_init(&gpioled.cdev, &led_fops);

    /* 3.添加cdev */
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); // 错误处理先略过了

    /* 4.创建类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if(IS_ERR(gpioled.class)){
        return PTR_ERR(gpioled.class);
    }

    /* 5.创建设备 */
    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if(IS_ERR(gpioled.device)){
        return PTR_ERR(gpioled.device);
    }

    /* 1.获取设备节点 */
    gpioled.nd = of_find_node_by_path("/gpioled");  // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点
    if(gpioled.nd == NULL){
        ret = -EINVAL;
        goto fail_findnode;
    }

    /* 2.获取LED对应的GPIO */  // 也就是节点中led-gpios那一行内容
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);
    if(gpioled.led_gpio < 0){
        printk("can't find led_gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;
    }
    printk("led_gpio num = %d\r\n",gpioled.led_gpio);

    /*  3.申请IO */
    ret = gpio_request(gpioled.led_gpio, "led-gpios");
    if(ret){
        printk("Failed to request the led gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;        
    }

    /* 4.使用IO,设置为输出 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if(ret){
        goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO
    }

    /* 5.输出高电平,关灯 */
    gpio_set_value(gpioled.led_gpio, 1);

    return 0;

fail_setoutput:
    gpio_free(gpioled.led_gpio);
fail_findnode:
    return ret;
}

/* 驱动出口 */
static void __exit led_exit(void){
    gpio_set_value(gpioled.led_gpio, 1); // 高电平 关灯

    /* 注销字符设备驱动 */
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);

    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);

    gpio_free(gpioled.led_gpio);

}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

spinlockAPP.c不需要改,和自旋锁的一致。不同的是,一个程序在执行时再开始一个应用,并不会像前两次一样出现报错,而是自动休眠,待第一个应用执行结束后自动被唤醒并执行:

6. 互斥体实验

懒得重新创新的文件夹,直接在上面的代码改了。

spinlock.c主要有以下更新:

增加了 头文件引用 #include <linux/mutex.h>

增加了 设备结构体中 互斥体

增加了 驱动入口函数中 对互斥体的初始化

增加了 led_open中 互斥体上锁

增加了 led_release中 互斥体解锁。互斥体同样可以使自动休眠。

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/mutex.h>

#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1

#define LEDON 1
#define LEDOFF 0


/* 设备结构体 */
struct gpioled_dev{
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct device *device;
    struct class *class;
    struct device_node *nd;
    int led_gpio;

    struct mutex lock;
};

struct gpioled_dev gpioled;

/* 操作集 */
static int led_release(struct inode *inode, struct file *filp){
    struct gpioled_dev *dev = filp->private_data;

    mutex_unlock(&dev->lock); // 互斥体 解锁

    return 0;
}
static int led_open(struct inode *inode, struct file *filp){
    filp->private_data = &gpioled;

    mutex_lock(&gpioled.lock); // 互斥体 上锁

    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    int ret;
    unsigned char databuf[1];
    struct gpioled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if(ret < 0){
        return -EINVAL;
    }
    if(databuf[0] == LEDON){ //开灯
        gpio_set_value(dev->led_gpio, 0); // 低电平开灯
    } else if(databuf[0] == LEDOFF){
        gpio_set_value(dev->led_gpio, 1); // 高电平关灯
    }

    return 0;
}
static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .write = led_write,
    .open = led_open,
    .release = led_release,
};


/* 驱动入口 */
static int __init led_init(void){
    int ret = 0;

    /* 初始化互斥体 */
    mutex_init(&gpioled.lock);


    /* 1.注册字符设备驱动 */
    gpioled.devid = 0;
    if(gpioled.devid){
        gpioled.devid = MKDEV(gpioled.devid, 0);
        register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
    } else {
        alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }

    /* 2.初始化cdev */
    gpioled.cdev.owner = THIS_MODULE;  
    cdev_init(&gpioled.cdev, &led_fops);

    /* 3.添加cdev */
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); // 错误处理先略过了

    /* 4.创建类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if(IS_ERR(gpioled.class)){
        return PTR_ERR(gpioled.class);
    }

    /* 5.创建设备 */
    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if(IS_ERR(gpioled.device)){
        return PTR_ERR(gpioled.device);
    }

    /* 1.获取设备节点 */
    gpioled.nd = of_find_node_by_path("/gpioled");  // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点
    if(gpioled.nd == NULL){
        ret = -EINVAL;
        goto fail_findnode;
    }

    /* 2.获取LED对应的GPIO */  // 也就是节点中led-gpios那一行内容
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);
    if(gpioled.led_gpio < 0){
        printk("can't find led_gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;
    }
    printk("led_gpio num = %d\r\n",gpioled.led_gpio);

    /*  3.申请IO */
    ret = gpio_request(gpioled.led_gpio, "led-gpios");
    if(ret){
        printk("Failed to request the led gpio\r\n");
        ret = -EINVAL;
        goto fail_findnode;        
    }

    /* 4.使用IO,设置为输出 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if(ret){
        goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO
    }

    /* 5.输出高电平,关灯 */
    gpio_set_value(gpioled.led_gpio, 1);

    return 0;

fail_setoutput:
    gpio_free(gpioled.led_gpio);
fail_findnode:
    return ret;
}

/* 驱动出口 */
static void __exit led_exit(void){
    gpio_set_value(gpioled.led_gpio, 1); // 高电平 关灯

    /* 注销字符设备驱动 */
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);

    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);

    gpio_free(gpioled.led_gpio);

}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

实验同5信号量实验一致。