[X]关闭

[米联客-XILINX-H3_CZ08_7100] LINUX驱动篇连载-21 Linux并发程序

文档创建者:LINUX课程
浏览次数:227
最后更新:2024-09-10
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-SOC » 2_LINUX应用开发
本帖最后由 LINUX课程 于 2024-9-11 10:57 编辑

软件版本:vitis2021.1(vivado2021.1)
操作系统:WIN10 64bit
硬件平台:适用XILINX Z7/ZU系列FPGA
登录“米联客”FPGA社区-www.uisrc.com视频课程、答疑解惑!

1 概述
并发能力是程序运行非常强大的一项能力,本章将会对Linux中如何实现并发,做一个通过原子操作控制并发的实验。
实验目的:
  • 了解并发的概念。
  • 掌握Linux 中并发编程的能力。
  • 理解原子操作的概念。
2 系统框图
图片6.jpg
3 介绍
3.1 原子操作
在Linux中有形形色色的并发关系,处理好这些关系可以大大地提高系统运行的效率,但是同样并发也会带来一些问题,譬如如下一段伪代码:
  1. string a;
  2. int x;

  3. void function(string a, int x)
  4. {
  5.     cout << "name:" << a << endl;
  6.     cout << "age:" << x << endl;
  7. }

  8. void main()
  9. {
  10.     XXX;
  11. }
复制代码
如果我们在主函数里开启两个线程,分别去调用function这个函数,第一个线程设置好a和x的值,然后刚打印完第6行,这时候第二个线程突然中断了一号线程并运行设置了a和x的值。此时一号线程也过来抢占,把二号线程中断了,继续实施了第7行的输出。
不难发现,一号线程虽然运行结束了,但是其x的值实际变成了二号线程设置的值,也就是说这个线程实际和出了bug没有区别。在系统中很明显,这样的抢占是时时刻刻都在发生的,所以就产生了一种需求,那就是在不该中断的时候禁止产生中断。
产生如上现象,总结一下有以下的几个要素需要被同时满足:
  • 存在共享资源
  • 存在中断机制
  • 缺乏临界区保护
其中前两个是我们无法改变的事实,对于第三点,我们可以引入一个概念,那就是原子操作。原子操作顾名思义就是不可分割的步骤,只要开始就一定要执行到结束。如果非要杠一下的话,化学中的原子是还能拆分的,但是我们这里的原子,应该做到不可拆分。譬如一个原子操作有5个步骤,那从第一个步骤开始,就一定是独占地执行到步骤五的,也许你对其过程都知晓,但是无法拆分步骤。
在写驱动的时候,往往会有并发执行的情况,因为驱动是面向对象的,不如说它就是为了并发而生的。所以在写驱动之前,就应该想好那些步骤是不能被中断的,哪些步骤是可以被中断的。
4 搭建工程
该实验使用默认的vivado工程生成的系统镜像,vivado工程文件在附件中可以找到。
4.1 vivado工程搭建
image.jpg
5 程序分析
5.1 驱动程序分析
  1. //添加头文件
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. #include <linux/cdev.h>
  5. #include <linux/gpio.h>
  6. #include <linux/uaccess.h>
  7. #include <linux/atomic.h>

  8. #define ZYNQMP_GPIO_NR_GPIOS 118
  9. #define MIO_PIN_51 (ARCH_NR_GPIOS - ZYNQMP_GPIO_NR_GPIOS + 51)
  10. // #define MIO_PIN_38 (ARCH_NR_GPIOS - ZYNQMP_GPIO_NR_GPIOS + 38)

  11. //设置一个设备全局变量
  12. struct lock_device
  13. {
  14. dev_t devno;
  15. struct cdev cdev;
  16. struct class *class;
  17. struct device *device;
  18. atomic64_t lock;
  19. } lock_dev;

  20. int lock_open(struct inode *inode, struct file *filp)
  21. {
  22. printk("-lock_open-\n");
  23. if (!atomic64_read(&lock_dev.lock)) //读取锁的状态
  24.   atomic64_inc(&lock_dev.lock); //把原子变量加 1, 上锁
  25. else
  26.   return -EBUSY; //若检测到已上锁,则返回设备忙
  27. return 0;
  28. }
  29. ssize_t lock_write(struct file *flip, const char __user *buf, size_t count, loff_t *fops)
  30. {
  31. int flag = 0, i = 0;
  32. flag = copy_from_user(&i, buf, count); //使用copy_from_user读取用户态发送过来的数据
  33. printk(KERN_CRIT "flag = %d, i = %d, count = %d\n", flag, i, count);
  34. if (flag != 0)
  35. {
  36.   printk("Kernel receive data failed!\n");
  37.   return 1;
  38. }
  39. if (i == 48)
  40. {
  41.   gpio_set_value(MIO_PIN_51, 0);
  42.   // gpio_set_value(MIO_PIN_38, 0);
  43. }
  44. else
  45. {
  46.   gpio_set_value(MIO_PIN_51, 1);
  47.   // gpio_set_value(MIO_PIN_38, 1);
  48. }
  49. return 0;
  50. }
  51. int lock_close(struct inode *inode, struct file *filp)
  52. {
  53. printk("-lock_close-\n");
  54. atomic64_set(&lock_dev.lock, 0); //将变量设为0,意为解锁
  55. return 0;
  56. }

  57. const struct file_operations lock_fops = {
  58. .open = lock_open,
  59. .write = lock_write,
  60. .release = lock_close,
  61. };

  62. //实现装载入口函数和卸载入口函数
  63. static __init int lock_drv_init(void)
  64. {
  65. int ret = 0;
  66. printk("----^v^-----lock drv v1 init\n");

  67. //动态申请设备号
  68. ret = alloc_chrdev_region(&lock_dev.devno, 0, 1, "lock_device");
  69. if (ret < 0)
  70. {
  71.   printk("alloc_chrdev_region fail!\n");
  72.   return 0;
  73. }

  74. //设备初始化
  75. cdev_init(&lock_dev.cdev, &lock_fops);
  76. lock_dev.cdev.owner = THIS_MODULE;

  77. //自动创建设备节点
  78. //创建设备的类别
  79. //参数1----设备的拥有者,当前模块,直接填THIS_MODULE
  80. //参数2----设备类别的名字,自定义
  81. //返回值:类别结构体指针,其实就是分配了一个结构体空间
  82. lock_dev.class = class_create(THIS_MODULE, "lock_class");
  83. if (IS_ERR(lock_dev.class))
  84. {
  85.   printk("class_create fail!\n");
  86.   return 0;
  87. }

  88. //创建设备
  89. //参数1----设备对应的类别
  90. //参数2----当前设备的父类,直接填NULL
  91. //参数3----设备节点关联的设备号
  92. //参数4----私有数据直接填NULL
  93. //参数5----设备节点的名字
  94. lock_dev.device = device_create(lock_dev.class, NULL, lock_dev.devno, NULL, "lock_device");
  95. if (IS_ERR(lock_dev.device))
  96. {
  97.   printk("device_create fail!\n");
  98.   return 0;
  99. }

  100. //向系统注册一个字符设备
  101. cdev_add(&lock_dev.cdev, lock_dev.devno, 1);

  102. //MIO_PIN_51 38申请GPIO口
  103. ret = gpio_request(MIO_PIN_51, "led1");
  104. if (ret < 0)
  105. {
  106.   printk("gpio request led1 error!\n");
  107.   return ret;
  108. }
  109. // ret = gpio_request(MIO_PIN_38, "led2");
  110. // if (ret < 0)
  111. // {
  112. //  printk("gpio request led2 error!\n");
  113. //  return ret;
  114. // }

  115. //GPIO口方向设置成输出
  116. ret = gpio_direction_output(MIO_PIN_51, 1);
  117. if (ret != 0)
  118. {
  119.   printk("gpio direction output MIO_PIN_51 fail!\n");
  120. }
  121. // ret = gpio_direction_output(MIO_PIN_38, 1);
  122. // if (ret != 0)
  123. // {
  124. //  printk("gpio direction output MIO_PIN_38 fail!\n");
  125. // }

  126. //将原子变量置0,相当于初始化
  127. atomic64_set(&lock_dev.lock, 0);

  128. return 0;
  129. }

  130. static __exit void lock_drv_exit(void)
  131. {
  132. printk("----^v^-----lock drv v1 exit\n");

  133. //释放按键GPIO
  134. gpio_free(MIO_PIN_51);
  135. // gpio_free(MIO_PIN_38);

  136. //注销字符设备
  137. cdev_del(&lock_dev.cdev);
  138. //删除设备节点
  139. device_destroy(lock_dev.class, lock_dev.devno);
  140. //删除设备类
  141. class_destroy(lock_dev.class);
  142. //注销设备号
  143. unregister_chrdev_region(lock_dev.devno, 1);
  144. }

  145. //申明装载入口函数和卸载入口函数
  146. module_init(lock_drv_init);
  147. module_exit(lock_drv_exit);

  148. //添加GPL协议
  149. MODULE_LICENSE("GPL");
  150. 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结构体
  1. typedef struct {
  2.     long counter;
  3. } atomic64_t;
复制代码
含义:在type.h头文件中,对于原子变量的结构体的定义。
2:atomic64_read函数
  1. long long atomic64_read(const atomic64_t *v);
复制代码
含义:读取原子变量所使用到的函数。
具体分析:
  • atomic64_t结构体
  • 返回值:读取到的数值
3:atomic64_set函数
  1. void  atomic64_set(atomic64_t *v, long long i);
复制代码
含义:原子变量设置函数,用来设置原子变量的数值。
具体分析:
  • *v:atomic64_t结构体地址
  • i:需要设置的值
  • 返回值:空
4:atomic64_add函数
  1. void  atomic64_##op(long long a, atomic64_t *v);
复制代码
含义:原子变量加法函数,作用是把指定的atomic64_t结构体的值加上指定的数值。这个函数用op代替了add这个函数,因为其同样也承载了减法函数的功能,使用op来复用了函数。
具体分析:
  • a:指定的数
  • *v:atomic64_t结构体
  • 返回值:空
5:atomic64_sub函数
  1. void  atomic64_##op(long long a, atomic64_t *v);
复制代码
含义:既然有加,那就也应该有减,这是原子变量的减法函数,使用方法和加法函数一样。
具体分析:
  • a:指定的数
  • *v:atomic64_t结构体
  • 返回值:空
6:atomic64_inc函数
  1. #define atomic64_inc(v)   atomic64_add(1, (v))
复制代码
含义:原子变量自增函数,调用这个函数可让指定函数自增一。原理就是通过atomic64_add函数加一。
具体分析:
  • v:atomic64_t结构体
  • 返回值:空
7:atomic64_dec函数
  1. #define atomic64_dec(v)   atomic64_sub(1, (v))
复制代码
含义:原子变量自减函数,与自增的原理一样,是通过atomic64_sub函数减一。
具体分析:
  • v:atomic64_t结构体
  • 返回值:空
5.2 应用程序分析
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <string.h>

  8. int main(int argc, char *argv[])
  9. {
  10.     int fd, ret = 0;
  11.     char *filename;
  12.     char writebuf[1] = {0};

  13.     filename = argv[1];

  14.     fd = open(filename, O_RDWR); //打开设备
  15.     if (fd < 0)
  16.     {
  17.         printf("Can't open file %s\n", filename);
  18.         return -1;
  19.     }

  20.     memcpy(writebuf, argv[2], 1); //将内容拷贝到缓冲区
  21.     ret = write(fd, writebuf, 1); //写数据
  22.     if (ret < 0)
  23.     {
  24.         printf("Write file %s failed!\n", filename);
  25.     }
  26.     else
  27.     {
  28.         printf("Write file success!\n");
  29.     }

  30.     sleep(15);
  31.     printf("Finish.\n");

  32.     ret = close(fd); //关闭设备
  33.     if (ret < 0)
  34.     {
  35.         printf("Can't close file %s\n", filename);
  36.         return -1;
  37.     }

  38.     return 0;
  39. }
复制代码
在现实情况中,程序的运行的速度是很快的,中断的发生也是十分迅速,为了观察到现象,需要放缓程序运行的速度,并手动模拟中断。
行17~22,打开驱动设备。
行24~33,通过驱动程序接口,发送指令至驱动程序,此处0代表灯灭,1代表灯亮。
行35,将线程暂停15秒,留出时间测试。
行36~43,通过打印通知设备已关闭。
6 程序编译
详细的程序编译部分可以参考第一章程序编译部分。将编译好的文件上传至开发板。
6.1 Makefile文件分析
  1. #已经编译过的内核源码路径
  2. KERNEL_DIR = /home/uisrc/uisrc-lab-xlnx/sources/kernel

  3. export ARCH=arm
  4. export CROSS_COMPILE=arm-linux-gnueabihf-

  5. #当前路径
  6. CURRENT_DIR = $(shell pwd)

  7. MODULE = lock_drv
  8. APP = lock_app

  9. all :
  10. #进入并调用内核源码目录中Makefile的规则, 将当前的目录中的源码编译成模块
  11. make -C $(KERNEL_DIR) M=$(CURRENT_DIR) modules

  12. ifneq ($(APP), )
  13. $(CROSS_COMPILE)gcc $(APP).c -o  $(APP)
  14. endif

  15. clean :
  16. make -C $(KERNEL_DIR) M=$(CURRENT_DIR) clean
  17. rm $(APP)

  18. #指定编译哪个文件
  19. obj-m += $(MODULE).o
复制代码
7 演示
7.1 硬件准备
SD2.0 启动 01 而模式开关为 ON OFF(7100 需要先将系统烧录进qspi,然后才能从qspi启动sd卡,参考Linux基础篇第四章)
2f5038eb9880afd532753935815b079.jpg
将 PS 端串口线连接电脑,如果要使用 ssh 登录,将网口线同样连接至电脑,最后给开发板通电。每次重新上电,需要重新插拔 PS 串口,否则会登录失败。
image.jpg
7.2 程序准备
查看文件是否传输成功。在终端输入“ls”命令。可以看到,“lock_drv.ko”和“lock_app”已经被上传到当前的文件夹内了。
image.jpg
使用 chmod 改变 “lock_app”的权限。
image.jpg
然后在root 状态下 insmod 安装驱动。
image.jpg
通过输入“lsmod”,可以查看驱动是否安装成功。
7.3 实验结果
观察LED灯的状态,目前应该均为长亮状态。使用./lock_app /dev/lock_device 0&来将LED熄灭,注意这条命令的结尾有一个&符号,这个符号的意思为将进程运行在后台,如果占用了控制台的话我们就没办法手动模拟中断了。
image.jpg
运行命令的瞬间,可以观察到LED熄灭了,在接下来的15秒内,其他程序都没办法中断它(除非使用kill命令)。
这时候迅速输入./lock_app /dev/lock_device 1命令来打开LED灯:
LED灯并没有打开,倒是输出了一行错误:Can't open file /dev/lock_device。意为无法打开驱动设备。
image.jpg
等到后台进程经过15秒后,会打印出Finsih。这时候就说明了已经解锁,再输入./lock_app /dev/lock_device 1,LED灯亮起,同时再次进入锁定状态。
image.jpg
测试完成,使用rmmod移除驱动。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则