本帖最后由 LINUX课程 于 2024-9-11 10:57 编辑
软件版本:vitis2021.1(vivado2021.1) 操作系统:WIN10 64bit 硬件平台:适用XILINX Z7/ZU系列FPGA
1 概述 并发能力是程序运行非常强大的一项能力,本章将会对Linux中如何实现并发,做一个通过原子操作控制并发的实验。 实验目的: - 了解并发的概念。
- 掌握Linux 中并发编程的能力。
- 理解原子操作的概念。
2 系统框图 3 介绍 3.1 原子操作 在Linux中有形形色色的并发关系,处理好这些关系可以大大地提高系统运行的效率,但是同样并发也会带来一些问题,譬如如下一段伪代码: - string a;
- int x;
- void function(string a, int x)
- {
- cout << "name:" << a << endl;
- cout << "age:" << x << endl;
- }
- void main()
- {
- XXX;
- }
复制代码如果我们在主函数里开启两个线程,分别去调用function这个函数,第一个线程设置好a和x的值,然后刚打印完第6行,这时候第二个线程突然中断了一号线程并运行设置了a和x的值。此时一号线程也过来抢占,把二号线程中断了,继续实施了第7行的输出。 不难发现,一号线程虽然运行结束了,但是其x的值实际变成了二号线程设置的值,也就是说这个线程实际和出了bug没有区别。在系统中很明显,这样的抢占是时时刻刻都在发生的,所以就产生了一种需求,那就是在不该中断的时候禁止产生中断。 产生如上现象,总结一下有以下的几个要素需要被同时满足: 其中前两个是我们无法改变的事实,对于第三点,我们可以引入一个概念,那就是原子操作。原子操作顾名思义就是不可分割的步骤,只要开始就一定要执行到结束。如果非要杠一下的话,化学中的原子是还能拆分的,但是我们这里的原子,应该做到不可拆分。譬如一个原子操作有5个步骤,那从第一个步骤开始,就一定是独占地执行到步骤五的,也许你对其过程都知晓,但是无法拆分步骤。 在写驱动的时候,往往会有并发执行的情况,因为驱动是面向对象的,不如说它就是为了并发而生的。所以在写驱动之前,就应该想好那些步骤是不能被中断的,哪些步骤是可以被中断的。 4 搭建工程 该实验使用默认的vivado工程生成的系统镜像,vivado工程文件在附件中可以找到。 4.1 vivado工程搭建 5 程序分析 5.1 驱动程序分析 - //添加头文件
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/cdev.h>
- #include <linux/gpio.h>
- #include <linux/uaccess.h>
- #include <linux/atomic.h>
- #define ZYNQMP_GPIO_NR_GPIOS 118
- #define MIO_PIN_51 (ARCH_NR_GPIOS - ZYNQMP_GPIO_NR_GPIOS + 51)
- // #define MIO_PIN_38 (ARCH_NR_GPIOS - ZYNQMP_GPIO_NR_GPIOS + 38)
- //设置一个设备全局变量
- struct lock_device
- {
- dev_t devno;
- struct cdev cdev;
- struct class *class;
- struct device *device;
- atomic64_t lock;
- } lock_dev;
- int lock_open(struct inode *inode, struct file *filp)
- {
- printk("-lock_open-\n");
- if (!atomic64_read(&lock_dev.lock)) //读取锁的状态
- atomic64_inc(&lock_dev.lock); //把原子变量加 1, 上锁
- else
- return -EBUSY; //若检测到已上锁,则返回设备忙
- return 0;
- }
- ssize_t lock_write(struct file *flip, const char __user *buf, size_t count, loff_t *fops)
- {
- int flag = 0, i = 0;
- flag = copy_from_user(&i, buf, count); //使用copy_from_user读取用户态发送过来的数据
- printk(KERN_CRIT "flag = %d, i = %d, count = %d\n", flag, i, count);
- if (flag != 0)
- {
- printk("Kernel receive data failed!\n");
- return 1;
- }
- if (i == 48)
- {
- gpio_set_value(MIO_PIN_51, 0);
- // gpio_set_value(MIO_PIN_38, 0);
- }
- else
- {
- gpio_set_value(MIO_PIN_51, 1);
- // gpio_set_value(MIO_PIN_38, 1);
- }
- return 0;
- }
- int lock_close(struct inode *inode, struct file *filp)
- {
- printk("-lock_close-\n");
- atomic64_set(&lock_dev.lock, 0); //将变量设为0,意为解锁
- return 0;
- }
- const struct file_operations lock_fops = {
- .open = lock_open,
- .write = lock_write,
- .release = lock_close,
- };
- //实现装载入口函数和卸载入口函数
- static __init int lock_drv_init(void)
- {
- int ret = 0;
- printk("----^v^-----lock drv v1 init\n");
- //动态申请设备号
- ret = alloc_chrdev_region(&lock_dev.devno, 0, 1, "lock_device");
- if (ret < 0)
- {
- printk("alloc_chrdev_region fail!\n");
- return 0;
- }
- //设备初始化
- cdev_init(&lock_dev.cdev, &lock_fops);
- lock_dev.cdev.owner = THIS_MODULE;
- //自动创建设备节点
- //创建设备的类别
- //参数1----设备的拥有者,当前模块,直接填THIS_MODULE
- //参数2----设备类别的名字,自定义
- //返回值:类别结构体指针,其实就是分配了一个结构体空间
- lock_dev.class = class_create(THIS_MODULE, "lock_class");
- if (IS_ERR(lock_dev.class))
- {
- printk("class_create fail!\n");
- return 0;
- }
- //创建设备
- //参数1----设备对应的类别
- //参数2----当前设备的父类,直接填NULL
- //参数3----设备节点关联的设备号
- //参数4----私有数据直接填NULL
- //参数5----设备节点的名字
- lock_dev.device = device_create(lock_dev.class, NULL, lock_dev.devno, NULL, "lock_device");
- if (IS_ERR(lock_dev.device))
- {
- printk("device_create fail!\n");
- return 0;
- }
- //向系统注册一个字符设备
- cdev_add(&lock_dev.cdev, lock_dev.devno, 1);
- //MIO_PIN_51 38申请GPIO口
- ret = gpio_request(MIO_PIN_51, "led1");
- if (ret < 0)
- {
- printk("gpio request led1 error!\n");
- return ret;
- }
- // ret = gpio_request(MIO_PIN_38, "led2");
- // if (ret < 0)
- // {
- // printk("gpio request led2 error!\n");
- // return ret;
- // }
- //GPIO口方向设置成输出
- ret = gpio_direction_output(MIO_PIN_51, 1);
- if (ret != 0)
- {
- printk("gpio direction output MIO_PIN_51 fail!\n");
- }
- // ret = gpio_direction_output(MIO_PIN_38, 1);
- // if (ret != 0)
- // {
- // printk("gpio direction output MIO_PIN_38 fail!\n");
- // }
- //将原子变量置0,相当于初始化
- atomic64_set(&lock_dev.lock, 0);
- return 0;
- }
- static __exit void lock_drv_exit(void)
- {
- printk("----^v^-----lock drv v1 exit\n");
- //释放按键GPIO
- gpio_free(MIO_PIN_51);
- // gpio_free(MIO_PIN_38);
- //注销字符设备
- cdev_del(&lock_dev.cdev);
- //删除设备节点
- device_destroy(lock_dev.class, lock_dev.devno);
- //删除设备类
- class_destroy(lock_dev.class);
- //注销设备号
- unregister_chrdev_region(lock_dev.devno, 1);
- }
- //申明装载入口函数和卸载入口函数
- module_init(lock_drv_init);
- module_exit(lock_drv_exit);
- //添加GPL协议
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("msxbo");
复制代码本章节我们写一个带锁的led驱动,当led被操作后15秒内,其他任何进程都不允许对led进行任何操作。这次选择在驱动打开时上锁,关闭时解锁,当前程序未关闭前可以对led进行任何操作。 行9~15,准备好LED的标号。 行17~25,设置一个关于设备所有描述的全局变量结构体。 行27~35,打开驱动设备时上锁,首先读取原子变量的值。若未上锁则上锁,已上锁则返回设备忙的通知。 行36~65,使用GPIO子系统控制LED,之前的章节已经分析过,不再赘述。 行66~71,关闭设备驱动,同时将原子变量置0来解锁。 行73~77,驱动程序接口函数。 行79~199,驱动的初始化工作,其中前面的几项我们已经是老生常谈了,在完成设备的一系列设置后,GPIO需要先申请到GPIO口的控制权,然后还需将GPIO设置成输出模式。最后,还需要将原子变量通过置0来初始化。可以看到这段代码很长,但是流程都是大同小异,可以写好一个模板以后复制粘贴。 行201~221,驱动的出口函数,在卸载设备前先释放GPIO,原子变量无需释放。 1:atomic64_t结构体 - typedef struct {
- long counter;
- } atomic64_t;
复制代码含义:在type.h头文件中,对于原子变量的结构体的定义。 2:atomic64_read函数 - long long atomic64_read(const atomic64_t *v);
复制代码含义:读取原子变量所使用到的函数。 具体分析: 3:atomic64_set函数 - void atomic64_set(atomic64_t *v, long long i);
复制代码含义:原子变量设置函数,用来设置原子变量的数值。 具体分析: - *v:atomic64_t结构体地址
- i:需要设置的值
- 返回值:空
4:atomic64_add函数 - void atomic64_##op(long long a, atomic64_t *v);
复制代码含义:原子变量加法函数,作用是把指定的atomic64_t结构体的值加上指定的数值。这个函数用op代替了add这个函数,因为其同样也承载了减法函数的功能,使用op来复用了函数。 具体分析: - a:指定的数
- *v:atomic64_t结构体
- 返回值:空
5:atomic64_sub函数 - void atomic64_##op(long long a, atomic64_t *v);
复制代码含义:既然有加,那就也应该有减,这是原子变量的减法函数,使用方法和加法函数一样。 具体分析: - a:指定的数
- *v:atomic64_t结构体
- 返回值:空
6:atomic64_inc函数 - #define atomic64_inc(v) atomic64_add(1, (v))
复制代码含义:原子变量自增函数,调用这个函数可让指定函数自增一。原理就是通过atomic64_add函数加一。 具体分析: 7:atomic64_dec函数 - #define atomic64_dec(v) atomic64_sub(1, (v))
复制代码含义:原子变量自减函数,与自增的原理一样,是通过atomic64_sub函数减一。 具体分析: 5.2 应用程序分析 - #include <stdio.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <string.h>
- int main(int argc, char *argv[])
- {
- int fd, ret = 0;
- char *filename;
- char writebuf[1] = {0};
- filename = argv[1];
- fd = open(filename, O_RDWR); //打开设备
- if (fd < 0)
- {
- printf("Can't open file %s\n", filename);
- return -1;
- }
- memcpy(writebuf, argv[2], 1); //将内容拷贝到缓冲区
- ret = write(fd, writebuf, 1); //写数据
- if (ret < 0)
- {
- printf("Write file %s failed!\n", filename);
- }
- else
- {
- printf("Write file success!\n");
- }
- sleep(15);
- printf("Finish.\n");
- ret = close(fd); //关闭设备
- if (ret < 0)
- {
- printf("Can't close file %s\n", filename);
- return -1;
- }
- return 0;
- }
复制代码在现实情况中,程序的运行的速度是很快的,中断的发生也是十分迅速,为了观察到现象,需要放缓程序运行的速度,并手动模拟中断。 行17~22,打开驱动设备。 行24~33,通过驱动程序接口,发送指令至驱动程序,此处0代表灯灭,1代表灯亮。 行35,将线程暂停15秒,留出时间测试。 行36~43,通过打印通知设备已关闭。 6 程序编译 详细的程序编译部分可以参考第一章程序编译部分。将编译好的文件上传至开发板。 6.1 Makefile文件分析 - #已经编译过的内核源码路径
- KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel
- export ARCH=arm
- export CROSS_COMPILE=arm-linux-gnueabihf-
- #当前路径
- CURRENT_DIR = $(shell pwd)
- MODULE = lock_drv
- APP = lock_app
- all :
- #进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules
- ifneq ($(APP), )
- $(CROSS_COMPILE)gcc $(APP).c -o $(APP)
- endif
- clean :
- make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
- rm $(APP)
- #指定编译哪个文件
- obj-m += $(MODULE).o
复制代码7 演示 7.1 硬件准备 SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,参考Linux基础篇第四章) 将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。 7.2 程序准备 查看文件是否传输成功。在终端输入“ls”命令。可以看到,“lock_drv.ko”和“lock_app”已经被上传到当前的文件夹内了。 使用 chmod 改变 “lock_app”的权限。 然后在root 状态下 insmod 安装驱动。 通过输入“lsmod”,可以查看驱动是否安装成功。 7.3 实验结果 观察LED灯的状态,目前应该均为长亮状态。使用./lock_app /dev/lock_device 0&来将LED熄灭,注意这条命令的结尾有一个&符号,这个符号的意思为将进程运行在后台,如果占用了控制台的话我们就没办法手动模拟中断了。 运行命令的瞬间,可以观察到LED熄灭了,在接下来的15秒内,其他程序都没办法中断它(除非使用kill命令)。 这时候迅速输入./lock_app /dev/lock_device 1命令来打开LED灯: LED灯并没有打开,倒是输出了一行错误:Can't open file /dev/lock_device。意为无法打开驱动设备。 等到后台进程经过15秒后,会打印出Finsih。这时候就说明了已经解锁,再输入./lock_app /dev/lock_device 1,LED灯亮起,同时再次进入锁定状态。 测试完成,使用rmmod移除驱动。 |