itop-3568开发板驱动学习笔记(7)高级字符设备(一)阻塞 IO 和 非阻塞 IO

news2024/11/24 17:43:47

《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记

文章目录

  • 阻塞 IO
  • 非阻塞 IO
  • IO 实验(使用等待队列)
    • 等待队列
    • 阻塞 IO 实验
    • 非阻塞 IO 实验

阻塞 IO

I/O输入/输出(Input/Output),分为IO设备和IO接口两个部分。 在POSIX兼容的系统上,例如Linux系统 ,I/O操作可以有多种方式,比如DIO(Direct I/O),AIO(Asynchronous I/O,异步I/O),Memory-Mapped I/O(内存映射I/O)等,不同的I/O方式有不同的实现方式和性能,在不同的应用中可以按情况选择不同的I/O方式。

——百度百科

阻塞 IO:当用户线程发出 IO 请求后,内核会去查看数据是否就绪,如果没就绪,线程便进入阻塞状态,让出 CPU。当数据就绪后,内核才会将数据拷贝到用户空间,最后返回用户线程,开始处理数据。

非阻塞 IO

非阻塞 IO:当用户进程发出 IO 请求后,不进行等待,立刻返回一个结果,如果返回错误,表示数据没有准备好,这时用户进程可以选择再次发出 IO 请求。如果内核数据已经准备好,那么数据会马上被拷贝到用户线程,然后返回用户线程处理数据。

IO 实验(使用等待队列)

等待队列

等待队列,是指linux系统中进程所组成的队列,就是需要其他事件的发生才会自己本身被唤醒的进程,也就是说这些进程本身是在等待其他某些进程为他们提供进程发生的条件。他们是属于消费者的,但是他们要消耗的东西还没有产生,这些就是处于等待状态的进程,组成了等待队列。等待队列很容易使用, 尽管它的设计很是微妙, 但不需要知道它的内部细节。

——百度百科

等待队列的原型是双循环链表,它有一个队列头,等待队列头结构体定义:

struct _wait_queue_head{
	spinlock_t lock; //自旋锁
	struct list_head task_list //链表头
};
typefef struct _wait_queue_head wait_queue_head_t;

等待队列除队列头外,其他便是等待队列项,即等待队列的队列元素,它的结构体定义如下:

struct _wait_queue{
	unsigned int flags;
	void *private;
	wait_queue_func_t func;
	struct list_head task_list;
};
typedef struct _wait_queue wait_queue_t;

等待队列 API 函数:

函数描述
DECLARE_WAIT_QUEUE_HEAD(name)定义并初始化一个等待队列头
init_waitqueue_head(q)初始化等待队列头,q 为队列头指针
DECLARE_WAITQUEUE(name, tsk)创建并初始化一个等待队列项,tsk 一般设置为 current(一个全局变量)相当于当前进程
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)添加等待队列项,q 为等待队列头指针,wait 为等待队列项指针
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)删除添加等待队列项,q 为等待队列头指针,wait 为等待队列项指针
wait_event(wq_head, condition)不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列 wq_head 里睡眠直到 condition 变为真
wait_event_interruptible(wq_head, condition)可中断的阻塞等待,让调用进程进入可中断的睡眠状态,在等待队列 wq_head 里睡眠直到 condition 变为真
wake_up(wait_queue_head_t *q)唤醒等待队列 q 所有休眠进程
wake_up_interruptible(wait_queue_head_t *q)唤醒等待队列 q 可中断的休眠进程

等待队列使用方法:

  1. 初始化等待队列头,并将条件 condition 设置为假(0)
  2. 在需要阻塞的地方调用 wait_event(),让进程进入休眠状态
  3. 想要唤醒线程时,将条件 condition 置为 1,然后调用 wake_up() 唤醒等待队列中的休眠进程

阻塞 IO 实验

驱动核心代码

在驱动的 read() 中,先运行 wait_event_interruptible(),让调用 read() 的线程进入睡眠状态,直到 condition 的值变为 1,这一步骤便是阻塞 IO 的核心(数据还未就绪,线程让出 CPU);当驱动的 write() 被执行时,数据就绪,condition 置为 1,然后调用 wake_up_interruptible() 唤醒第一个线程(不调用唤醒函数,第一个线程并不会自己唤醒)。

// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue); 

// read()
static  ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;
	int ret = 0;
	
	// 可中断的阻塞等待,进程进入休眠状态
	wait_event_interruptible(my_wait_queue, tmp_dev->condition); 
	
	// 向应用空间拷贝数据
	ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf)); 
	if(ret != 0)
	{
		printk("copy_to_user error.\r\n");
		return -1;
	}
	
	printk("chrdev_read.\n");
	return 0;
}

// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
	if(ret != 0)
	{
		printk("copy_from_user error.\r\n");
		return -1;
	}

	tmp_dev->condition = 1; // 将条件置 1
	wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
	return 0;
}


完整驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/wait.h>

// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue); 

// 定义一个私有数据结构体
struct my_device
{
	dev_t dev_num; // 设备号
	int major;     // 主设备号
	int minor;     // 次设备号
	struct cdev st_cdev;
	struct class *st_class;
	struct device *st_device;
	char kbuf[32];
	int condition; // 条件标志
};

// 定义一个全局私有数据结构体
struct my_device dev1;

// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
	file->private_data = &dev1; // 设置私有数据
	printk("chrdev_open.\n");
	return 0;
}

// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
	printk("chrdev_release.\n");
	return 0;
}

// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
	if(ret != 0)
	{
		printk("copy_from_user error.\r\n");
		return -1;
	}

	tmp_dev->condition = 1; // 将条件置 1
	wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
	return 0;
}

// read()
static  ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;
	int ret = 0;
	
	// 可中断的阻塞等待,进程进入休眠状态
	wait_event_interruptible(my_wait_queue, tmp_dev->condition); 
	
	// 向应用空间拷贝数据
	ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf)); 
	if(ret != 0)
	{
		printk("copy_to_user error.\r\n");
		return -1;
	}
	
	printk("chrdev_read.\n");
	return 0;
}

static struct file_operations chrdev_fops = {
	.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = chrdev_open,  //将 open 成员指向 chrdev_open()函数
	.read = chrdev_read,  //将 read 成员指向 chrdev_read()函数
	.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
	.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
	int ret;

	// 自动获取设备号(只申请一个,次设备号从 0 开始)
	ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
	if(ret < 0)
	{
		goto err_alloc;
	}
	printk("alloc chrdev region successfully.\n");
	dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
	dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
	printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
	
	dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
	cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备	
	ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
	if(ret < 0)
	{
		goto err_cdev_add;
	}
	printk("cdev add successfully.\n");
	
	dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
	if(IS_ERR(dev1.st_class))
	{
		ret = PTR_ERR(dev1.st_class); // 返回错误码
		goto err_class_create;
	}
	
	dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
	if(IS_ERR(dev1.st_device))
	{
		ret = PTR_ERR(dev1.st_device); // 返回错误码
		goto err_device_create;
	}

	
	return 0;

err_device_create:
	class_destroy(dev1.st_class); // 删除类

err_class_create:
	cdev_del(&dev1.st_cdev); // 删除 cdev

err_cdev_add:
	unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号

err_alloc:
	return ret; // 返回错误号

}

// 驱动出口函数
static void __exit chrdev_exit(void)
{
	device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
	class_destroy(dev1.st_class); //删除设备类
	cdev_del(&dev1.st_cdev); // 删除字符设备
	unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
	
	printk("chrdev_exit.\n");
}

module_init(chrdev_init);  //注册入口函数
module_exit(chrdev_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");  //同意GPL协议
MODULE_AUTHOR("xiaohui");  //作者信息

“读”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEV_FILE "/dev/chrdev_device"

int main(int argc, char** argv)
{
	int fd, tmp;
	int ret = 0;
	char buf[32] = {0};
	
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);
	
	// 读数据
	printf("app will read data.\n");
	ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
	if(ret == 0)
		printf("app read data successfully\ndata: %s\n", buf);
	else
		printf("app read data failed.\n");
	// 关闭设备文件
	close(fd);
	return 0;
}

“写”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEV_FILE "/dev/chrdev_device"

int main(int argc, char** argv)
{
	int fd, tmp;
	int ret = 0;
	char buf[32] = "Blocking IO";
	
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);
	
	// 读数据
	printf("app will write data.\n");
	ret = write(fd, buf, sizeof(buf)); // 向设备文件写数据
	if(ret == 0)
		printf("app write data successfully\n");
	else
		printf("app write data failed.\n");
	// 关闭设备文件
	close(fd);
	return 0;
}

Makefile 文件

#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o

#架构平台选择
export ARCH=arm64

#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-

#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/
#KDIR := /lib/modules/$(shell uname -r)/build

#编译模块
all:
	make -C $(KDIR) M=$(shell pwd) modules
	$(CROSS_COMPILE)gcc read.c -o read
	$(CROSS_COMPILE)gcc write.c -o write

#清除编译文件
clean:
	make -C $(KDIR) M=$(shell pwd) clean
	rm read write

实验结果

先运行“读”程序,“读”程序进入阻塞,需要等待“写”程序向设备文件写数据,“写”程序运行后,“读”程序被唤醒,继续运行,

在这里插入图片描述

非阻塞 IO 实验

在应用程序打开文件调用 open() 函数时,如 fd = open(DEV_FILE, O_RDWR) ,默认是阻塞式的,如果要采用非阻塞式,需要修改 open() 的参数,例如:fd = open(DEV_FILE, O_RDWR|O_NONBLOCK)

驱动核心代码

在驱动的 read() 中,先判断 file->f_flags 是否带有 O_NONBLOCK,即是否为非阻塞 IO,如果是,则继续判断数据就绪条件 condition 的值,数据没有就绪就直接退出 read(),并返回 -EAGAIN,此时“读”程序所在进程便不会被阻塞,但应用层需要不停循环判断 read 的返回值。(此实验的 wait_event_interruptible() 和 wake_up_interruptible() 没有实质作用,但如果应用层打开设备文件时没有指定 O_NONBLOCK 参数,则本实验依然为阻塞 IO 实验 )

// read()
static  ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;
	int ret = 0;
	
	// 如果 open() 的 flags 参数带 O_NONBLOCK
	if(file->f_flags& O_NONBLOCK)
	{
		// 如果数据没就绪,直接退出 read()
		if(tmp_dev->condition == 0)
			return -EAGAIN;
	}
	
	// 可中断的阻塞等待,进程进入休眠状态
	wait_event_interruptible(my_wait_queue, tmp_dev->condition); 
	
	// 向应用空间拷贝数据
	ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf)); 
	if(ret != 0)
	{
		printk("copy_to_user error.\r\n");
		return -1;
	}
	
	printk("chrdev_read.\n");
	return 0;
}

// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
	if(ret != 0)
	{
		printk("copy_from_user error.\r\n");
		return -1;
	}

	tmp_dev->condition = 1; // 将条件置 1
	wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
	return 0;
}

完整驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/wait.h>

// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue); 

// 定义一个私有数据结构体
struct my_device
{
	dev_t dev_num; // 设备号
	int major;     // 主设备号
	int minor;     // 次设备号
	struct cdev st_cdev;
	struct class *st_class;
	struct device *st_device;
	char kbuf[32];
	int condition; // 条件标志
};

// 定义一个全局私有数据结构体
struct my_device dev1;

// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
	file->private_data = &dev1; // 设置私有数据
	printk("chrdev_open.\n");
	return 0;
}

// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
	printk("chrdev_release.\n");
	return 0;
}

// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
	if(ret != 0)
	{
		printk("copy_from_user error.\r\n");
		return -1;
	}

	tmp_dev->condition = 1; // 将条件置 1
	wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
	return 0;
}

// read()
static  ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;
	int ret = 0;
	
	// 可中断的阻塞等待,进程进入休眠状态
	wait_event_interruptible(my_wait_queue, tmp_dev->condition); 
	
	// 向应用空间拷贝数据
	ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf)); 
	if(ret != 0)
	{
		printk("copy_to_user error.\r\n");
		return -1;
	}
	
	printk("chrdev_read.\n");
	return 0;
}

static struct file_operations chrdev_fops = {
	.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = chrdev_open,  //将 open 成员指向 chrdev_open()函数
	.read = chrdev_read,  //将 read 成员指向 chrdev_read()函数
	.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
	.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
	int ret;

	// 自动获取设备号(只申请一个,次设备号从 0 开始)
	ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
	if(ret < 0)
	{
		goto err_alloc;
	}
	printk("alloc chrdev region successfully.\n");
	dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
	dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
	printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
	
	dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
	cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备	
	ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
	if(ret < 0)
	{
		goto err_cdev_add;
	}
	printk("cdev add successfully.\n");
	
	dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
	if(IS_ERR(dev1.st_class))
	{
		ret = PTR_ERR(dev1.st_class); // 返回错误码
		goto err_class_create;
	}
	
	dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
	if(IS_ERR(dev1.st_device))
	{
		ret = PTR_ERR(dev1.st_device); // 返回错误码
		goto err_device_create;
	}

	
	return 0;

err_device_create:
	class_destroy(dev1.st_class); // 删除类

err_class_create:
	cdev_del(&dev1.st_cdev); // 删除 cdev

err_cdev_add:
	unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号

err_alloc:
	return ret; // 返回错误号

}

// 驱动出口函数
static void __exit chrdev_exit(void)
{
	device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
	class_destroy(dev1.st_class); //删除设备类
	cdev_del(&dev1.st_cdev); // 删除字符设备
	unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
	
	printk("chrdev_exit.\n");
}

module_init(chrdev_init);  //注册入口函数
module_exit(chrdev_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");  //同意GPL协议
MODULE_AUTHOR("xiaohui");  //作者信息

“读”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEV_FILE "/dev/chrdev_device"

int main(int argc, char** argv)
{
	int fd, tmp;
	int ret = 0;
	char buf[32] = {0};
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR|O_NONBLOCK);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);
	
	// 读数据
	printf("app will read data.\n");
	while(1)
	{
		ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
		sleep(3); // 内核 printk() 和 printf() 同时打印,终端有乱码(可能还是串口调试工具的问题)
		if(ret == 0)
			printf("app read data successfully\ndata: %s\n", buf);
		else
			printf("app read data failed.\n");
		//sleep(3);
		
	}	
	
	// 关闭设备文件
	close(fd);
	return 0;
}

“写”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEV_FILE "/dev/chrdev_device"

int main(int argc, char** argv)
{
	int fd, tmp;
	int ret = 0;
	char buf[32] = "Non_Blocking IO";
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR|O_NONBLOCK);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);
	
	// 读数据
	printf("app will write data.\n");
	ret = write(fd, buf, sizeof(buf)); // 向设备文件写数据
	if(ret == 0)
		printf("app write data successfully\n");
	else
		printf("app write data failed.\n");
	// 关闭设备文件
	close(fd);
	return 0;
}

Makefile 文件

#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o

#架构平台选择
export ARCH=arm64

#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-

#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/
#KDIR := /lib/modules/$(shell uname -r)/build

#编译模块
all:
	make -C $(KDIR) M=$(shell pwd) modules
	$(CROSS_COMPILE)gcc read.c -o read
	$(CROSS_COMPILE)gcc write.c -o write

#清除编译文件
clean:
	make -C $(KDIR) M=$(shell pwd) clean
	rm read write

实验结果

运行“读”程序后,由于数据没有就绪,read 直接返回 -EAGAIN(非 0 的错误号),而不像阻塞 IO 一样挂起等待,“写”程序运行后,内核数据就绪,“读”程序中的 read() 返回 0(读取成功)。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/401619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python学习——【第四弹】

前言 上一篇文章 python学习——【第三弹】 中学习了python中的流程控制语句&#xff0c;这篇文章我们接着学习python中的序列。先给大家介绍不可变序列 字符串和可变序列 列表&#xff0c;下一篇文章接着补充元组&#xff0c;集合和字典。 序列 指的是一块可以存放多个值的…

多线程的风险 --- 线程安全

✨个人主页&#xff1a;bit me&#x1f447; ✨当前专栏&#xff1a;Java EE初阶&#x1f447; ✨每日一语&#xff1a;低头赶路&#xff0c;敬事如仪&#xff1b;自知自心&#xff0c;其路则明。 目 录&#x1f378;一. 线程不安全&#x1f379;二. 线程不安全的原因&#x1f…

【C语言】每日刷题 —— 牛客语法篇(1)

前言 大家好&#xff0c;今天带来一篇新的专栏c_牛客&#xff0c;不出意外的话每天更新十道题&#xff0c;难度也是从易到难&#xff0c;自己复习的同时也希望能帮助到大家&#xff0c;题目答案会根据我所学到的知识提供最优解。 &#x1f3e1;个人主页&#xff1a;悲伤的猪大…

Java的jar包打包成exe应用

将springboot项目使用maven打出的jar包&#xff0c;打成windows平台下exe应用程序包&#xff08;自带jre环境&#xff09;。 工具&#xff1a;1、exe4j 2、Inno Setup 工具放到网盘&#xff0c;链接&#xff1a;https://pan.baidu.com/s/1ZHX8P7u-7GBxaC6uaIC8Ag 提取码&#x…

VMware16pro虚拟机安装全过程

很多时候需要用到Linux系统&#xff0c;简单的一种方式可以是&#xff1a;Windows系统运行Linux&#xff08;Windows Subsystem for Linux&#xff09;不过有些时候还是需要虚拟机来运行Linux&#xff0c;也更方便点&#xff0c;比如在做嵌入式系统的烧录等操作都需要Linux环境…

人的高级认知:位置感

你知道吗&#xff1f;人有个高级认知&#xff1a;位置感 位置感是啥&#xff1f;咋提高位置感&#xff1f; 趣讲大白话&#xff1a;知道自己几斤几两 【趣讲信息科技99期】 ******************************* 位置感 就是对自己所处环境和自身存在的领悟 属于人生智慧 来源于阅历…

设计模式——创建型模型——单列模式(8种实现)

前言&#xff1a; &#x1f44f;作者简介&#xff1a;我是笑霸final&#xff0c;一名热爱技术的在校学生。 &#x1f4dd;个人主页&#xff1a;个人主页1 || 笑霸final的主页2 &#x1f4d5;系列专栏&#xff1a;计算机基础专栏 &#x1f4e7;如果文章知识点有错误的地方&#…

保姆级使用PyTorch训练与评估自己的EfficientNetV2网络教程

文章目录前言0. 环境搭建&快速开始1. 数据集制作1.1 标签文件制作1.2 数据集划分1.3 数据集信息文件制作2. 修改参数文件3. 训练4. 评估5. 其他教程前言 项目地址&#xff1a;https://github.com/Fafa-DL/Awesome-Backbones 操作教程&#xff1a;https://www.bilibili.co…

eps文件删除了能恢复吗?恢复误删eps文件的三种方法

eps文件格式专为矢量图像和图形而设计。虽然没有被广泛使用&#xff0c;但它仍然受到各种插画家和平面设计师的钟爱。eps文件十分适合创建徽标和商标设计&#xff0c;主要应用见于广告牌、海报和横幅。可是在使用设备过程中&#xff0c;难免会遇到数据丢失问题&#xff0c;如果…

KaiwuDB 时序引擎数据存储内存对齐技术解读

一、理论1、什么是内存对齐现代计算机中内存空间都是按照 byte 划分的&#xff0c;在计算机中访问一个变量需要访问它的内存地址&#xff0c;从理论上看&#xff0c;似乎对任何类型的变量的访问都可以从任何地址开始。但在实际情况中&#xff0c;通常在特定的内存地址才能访问特…

并发编程---java锁

java锁一 多线程锁synchronized案例分析1.1synchronized介绍1.2 synchronized案例分析1.2.1.标准访问&#xff0c;请问先打印邮件还是短信&#xff1f;1.2.2.邮件⽅法暂停4秒钟&#xff0c;请问先打印邮件还是短信&#xff1f;分析1.2.3.新增⼀个普通⽅法hello&#xff08;&…

Django系统开发

Django系统开发 1.新建项目 创建Django项目 删除templates目录 删除settings.py里templates -> DIRS的列表数据 2.创建app 在Pycharm中 注册app 在settings.py中找到 INSTALLED_APPS 加入对应路径 app01.apps.App01Config 3.表结构 from django.db import modelsclas…

32位处理器AM6528BACDXEA、AM6548BACDXEAF基于Arm Cortex-A53内核【工业4.0嵌入式产品应用】

AM6528BACDXEA、AM6548BACDXEAF 处理器是专为满足工业4.0嵌入式产品对处理性能的复杂需求而设计的Arm应用处理器。AM654x和AM652x器件将四个或两个Arm Cortex-A53内核与一个双Arm Cortex-R5F MCU子系统结合在一起。这些包含的功能旨在帮助客户实现终端产品的功能安全目标。它还…

绘制CSP的patterns矩阵图

最近在使用FBCSP处理数据,然后就想着看看处理后的样子,用地形图的形式表现出来,但是没有符合自己需求的函数可以实现,就自己尝试的实现了一下,这里记录一下,方便以后查阅。 绘制CSP的patterns矩阵图 对数据做了FBCSP处理,但是想画一下CSP计算出来的patterns的地形图,并…

成功的项目管理策略:减少成本,提高质量

项目管理是一项具有挑战性的任务&#xff0c;项目团队需要合理的规划和策略&#xff0c;以确保项目的成功和达成预期。为了实现项目的成功&#xff0c;项目经理必须采用正确的策略&#xff0c;才能以最大限度地减少成本并提高项目质量。本文将探讨成功的项目管理策略&#xff0…

Django实践-05Cookie和Session

文章目录Django实践-05Cookie和SessionDjango实践-05Cookie和Session用户登录的准备工作1. 创建用户模型。2. 正向工程生成数据库表3.写utils.py文件&#xff0c;密码转md54.给数据表tb_user中插入测试数据5.编写用户登录的视图函数和模板页。6.编写urls.py。6.增加login.html模…

数据仓库相关概念的解释

数据仓库相关概念的解释 文章目录数据仓库相关概念的解释1 ETL是什么&#xff1f;ETL体系结构2 数据流向何为数仓DW3 ODS 是什么&#xff1f;4 数据仓库层DWDWD 明细层DWD 轻度汇总层&#xff08;MID或DWB&#xff0c;data warehouse basis&#xff09;DWS 主题层&#xff08;D…

MySQL8读写分离集群

文章目录前言MySQL读写分离原理搭建MySQL读写分离集群MySQL8.0之前MySQL8.0之后后记前言 上一期介绍并实现了MySQL的主从复制&#xff0c;由于主从复制架构仅仅能解决数据冗余备份的问题&#xff0c;从节点不对外提供服务&#xff0c;依然存在单节点的高并发问题 所以在主从复…

【Vue】10分钟带你读懂Vue的过滤器

一、什么是过滤器&#xff1f;过滤器提供给我们的一种数据处理方式。过滤器功能不是必须要使用的&#xff0c;因为它所实现的功能也能用计算属性或者函数调用的方式来实现。Vue.js 允许你自定义过滤器&#xff0c;可被用于一些常见的文本格式化。二、过滤器声明与使用过滤器应该…

重构SeleniumeDownloader底层浏览器驱动

一、解决bug&#xff1a;Selenium with PhantomJS&#xff0c;重构SeleniumeDownloader底层浏览器驱动 0、小背景&#xff1a; 想爬取外网steam的数据&#xff0c;但是steam官网在海外&#xff0c;加上steam处于反爬考虑&#xff0c;对于异步数据-json数据进行处理&#xff0…