【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程

文章目录

  • 注册杂项设备
  • 驱动模块传参
  • 注册字符设备

开发环境: windows + ubuntu18.04 + 迅为rk3568开发板

注册杂项设备

相较于字符设备,杂项设备有以下两个优点:

  • 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时,只需使用子设备号进行区分即可。
  • 使用简单:相比如普通的字符设备驱动, misc驱动只需要将基本信息通过结构体传递给相应处理函数即可。

在linxu系统中可使用 cat /proc/misc 命令查看系统中的杂项设备。注册杂项设备的步骤:

  • 1.填充设备操作集结构体struct file_operations

  • 2.填充杂项设备结构体struct miscdevice;

  • 3.使用函数misc_register注册杂项设备;

  • 4.使用函数misc_deregister卸载杂项设备;

上面三步可使用下面函数直观用表达,即:

static struct file_operations xxx_fops{
	.owner = THIS_MODULE, 
	.read = xxx_read, 
	....
};
struct miscdevice xxx_dev{
	.minor = MISC_DYNAMIC_MINOR, 
	.name = "xxx", 
	.fops = &xxx_fops
};
static int __init xxx_init(void) //驱动入口函数
{
	int ret;
	printk(KERN_EMERG "xxx_init\r\n");
	ret = misc_register(&xxx_dev);//注册杂项设备
	if(ret<0)
	{
		printk( "misc_register failed\r\n");
		return -1;
	}
	printk( "misc_register ok\r\n");
	return 0;
}
static void __exit xxx_exit(void) //驱动出口函数
{
	printk(KERN_EMERG "xxx_exit\r\n");
	misc_deregister(&xxx_dev); //卸载杂项设备
}
module_init(xxx_init); //注册入口函数
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");

具体实现注册一个杂项设备的示例代码如下:

#include <linux/kernel.h>
#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体
#include <linux/uaccess.h>

// 打开杂项设备
int _open(struct inode *inode,struct file*file)
{
	printk(KERN_EMERG"hello misc");
	return 0;
}

// 关闭杂项设备
int close(struct inode * inode, struct file *file)
{
	printk(KERN_EMERG"close");
	return 0;
}

// 读取杂项设备中的数据
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
	char kbuff[32] = "kernel";
	if(copy_to_user(buff,kbuff,strlen(kbuff)) != 0) // 将内核中的数据给应用
	{
		printk("copy_to_user error\r\n");
		return -1;
	}	
	printk(KERN_EMERG"copy_to_user is successful\r\n");
	
	return size;
}

// 写入数据到杂项设备中
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
	char kbuff[32] ;
	if(copy_from_user(kbuff,buff,size)!= 0) // 从应用那儿获取数据
	{
		printk("copy_from_user error\r\n");
		return -1;
	}	
	printk(KERN_EMERG"copy_from_user data:%s\r\n",kbuff);

	return size;
}

// 设备文件描述集  
struct file_operations misc_fops ={
	.owner = THIS_MODULE,
	.open = misc_open,
	.release = close,
	.read = misc_read,
	.write = misc_write
};

struct miscdevice misc_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "hello_misc", // 杂项设备名   注册成功后会在 /dev目录下显示
	.fops = &misc_fops
};

// 驱动的入口函数
static int __init misc_init(void)
{
	int ret = 0;
	ret = misc_register(&misc_dev);
	if(ret < 0)
		printk(KERN_EMERG"misc register is error\r\n.");
	else
		printk(KERN_EMERG"misc register is seccussful\r\n.");
	return 0;
}

//驱动的出口函数
static void __exit misc_exit(void)
{
	misc_deregister(&misc_dev);
	printk(KERN_EMERG"baibai\r\n");
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");

编译传送到开发板上后,先试用insmod +驱动名.ko挂载驱动,其结果为:
在这里插入图片描述

上述驱动代码的测试代码如下:

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

int main(int argc,char *argv[])
{
	char filePath[] = "/dev/hello_misc";
	// 打开文件
	int fd = open(filePath,O_RDWR);
	if(fd < 0)
	{
		printf("opening is failed.\n");
		return -1;
	}
	else
		printf("opening is successful.\n");

	// 读取
	char buff1[32],buff2[32] = "hello this is app";
	read(fd,buff1,sizeof(buff1));
	printf("buff1 is %s.\n",buff1);
	
	// 写入
	write(fd,buff2,sizeof(buff2));	

	close(fd);
	return 0;
}

使用./+程序名运行测试代码后,得结果如下:
在这里插入图片描述


驱动模块传参

总所周知,应用程序传参是通过shell终端传,只要将main函数按照下面格式书写即可完成传参操作

int main(int argc ,char *argv[])
{
	return 0;
}

相比之下,驱动模块传递参数需要借助其他函数完成传参操作:

1. 传递单个参数给内核

module_param(name, type, perm)

参数解释:

  • name:参数名,既是外部参数名,又是内部参数名。

  • type:参数的数据类型,可取int、charp等。

  • perm:访问权限。八进制,如:0777。0表示该参数在文件系统中不可见。

注意:传递字符作为参数时数据类为charp,而不是char.

2.传递数组给内核

module_param_array(name, type, nump, perm)
  • module_param_array(name,type,nump,perm)
  • name:数组参数名,既是外部参数,又是内部参数
  • type:参数的数据类型
  • nump:终端传给数组的实际元素个数(指针变量)
  • perm:访问权限,0644。0表示该参数在文件系统中不可见

3.传递字符串给内核

module_param_string(name, string, len, perm)
  • name:参数名,外部参数名
  • string:内部参数名(内部字符数组名)
  • len:数组长度
  • perm:访问权限,0644。0表示该参数在文件系统中不可见

传递参给内核的作用:

  • 1.设置驱动的相关参数,如:设备数量、设备缓冲区大小等等。
  • 2.可进行安全校验,放置驱动被他人盗用。

说明
参数传递的适用时机为 加载驱动到内核时,命令形式为:insmod +驱动名.ko +参数1名=参数值 参数2名=参数值 ……。例如下面示例中使用insmod file.ko date=12传递参数:

#include <linux/module.h>
#include <linux/init.h>

// 保存参数
static int date;
static int date1;
static int data[5];
static int count;
static char str[32];
static char *strData;

// 传递单个参数
module_param(date,int,S_IRUGO|S_IWUSR); // 可读可写
module_param(date1,int,S_IRUGO); // 可读可写
module_param(strData,charp,S_IRUGO); // 可读

// 传递多个参数
module_param_array(data,int,&count,S_IRUGO); // 可读
module_param_string(str,str,sizeof(str),S_IRUGO); // 可读

// 驱动的入口函数
static int __init dev_init(void)
{
	int i=0;
	if(strcmp(str,"myTest")!=0)
	{
		printk("dev_init error\r\n");
		return -1;
	}
	printk("---------------------------------------\r\n");
	printk(KERN_EMERG"dev_init is successful!\r\n");
	for(i=0;i<count;++i)
		printk("data[%d] = %d \r\n",i,data[i]);
	printk("str:%s  strData:%s\r\n",str,strData);
	printk("date:%d  count:%d\r\n",date,count);
	
	data[1] = 111;
	date = 10;
	date1 = 111;
	
	return 0;
}

//驱动的出口函数
static void __exit dev_exit(void)
{
	int i=0;
	for( i=0;i<count;++i)
		printk("data[%d] = %d \r\n",i,data[i]);
	printk("date:%d  count:%d\r\n",date,count);
	
	printk(KERN_EMERG"dev_exit is successful\r\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");

注册字符设备

步骤一:驱动初始化,需要申请设备号,初始化并且注册cdev结构体,初始化硬件;

可使用动态申请或静态申请设备号,其中动态申请设备号一般在235-255,静态申请则一般由用户手动输入。

静态申请的函数原型为:

 int register_chrdev_region(dev_t, unsigned, const char *);

参数含义:

  • from: 自定义的 dev_t 类型设备号。
  • count: 申请设备的数量。
  • name: 申请的设备名称。
    函数返回值:申请成功返回 0,申请失败返回负数。

动态申请的函数原型为:

 int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

参数含义:

  • dev : 会将申请完成的设备号保存在 dev 变量中。
  • baseminor: 次设备号可申请的最小值。
  • count: 申请设备的数量。
  • name: 申请的设备名称。
    函数返回值:申请成功返回 0,申请失败返回负

Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作 file_operations 结构体(设备的打开、读写、关闭等操作接口)等信息:

struct cdev {
	struct kobject kobj; //内嵌的内核对象.
	struct module *owner; //该字符设备所在的内核模块的对象指针. 
	const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结
	构体.struct list_head list; //用来将已经向内核注册的所有字符设备形成链表. 
	dev_t dev; //字符设备的设备号,由主设备号和次设备号构成. 
	unsigned int count; //隶属于同一主设备号的次设备号的个数. 
};

初始化设备描述集cdev结构体的函数原型为:

void cdev_init(struct cdev *, const struct file_operations *);

参数含义:

  • 参数1:表示是抽象设备结构体;
  • 参数2:表示文件操作集;

注册设备到内驱使用下面函数

int cdev_add(struct cdev *, dev_t,  unsigned);

参数含义:

  • 参数1:为要添加的 struct cdev 类型的结构体
  • 参数2:为申请的字符设备号
  • 参数3:为和该设备关联的设备编号的数量
    若函数在内核中添加成功返回 0,添加失败返回负数。

步骤二:构建设备文件操作描述集file_operations

也就是read、write、open、close等函数。

步骤三:生成并且添加设备节点

  • 手动添加设备节点:也就是在加载驱动到内核时添加,即mknod + 路径/设备名 +设备类型 + 主设备号 + 次设备号
  • 自动添加设备节点:初始化内核时,使用函数自动添加。即先试用函数class_create创建一个类,在使用函数device_create创建并且添加设备节点。

class_create函数说明:

#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \ __class_create(owner, name, &__key); \
})

函数作用:
用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进 Linux 内核系统。

参数含义:

  • owner:指向函数即将创建的这个 struct class 的模块。一般为 THIS_MODULE。
  • name:代表即将创建的 struct class 变量的名字。
    返回值:struct class * 类型的类。

device_create函数说明:


struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt,...);

函数作用:
用来在 class 类中下创建一个设备属性文件,udev 会自动识别从而进行设备节点的创建。

参数含义:

  • cls:指定所要创建的设备所从属的类。
  • parent:指定该设备的父设备,如果没有就指定为 NULL。
  • devt:指定创建设备的设备号。
  • drvdata:被添加到该设备回调的数据,没有则指定为 NULL。
  • fmt:添加到系统的设备节点名称。

返回值:struct device * 类型结构体的设备

步骤四:注销字符设备驱动

  • 步骤一:使用函数unregister_chrdev_region释放设备号
void unregister_chrdev_region(dev_t, unsigned)

该函数只有一个参数,为要删除设备的设备号,并且函数无返回值。

  • 步骤二:使用函数cdev_del步骤二:删除设备操作集卸载cdev
void cdev_del(struct cdev *);

该函数只有一个参数,为要删除的 struct cdev 类型的结构体,并且函数无返回值。

  • 步骤三:使用函数device_destroy卸载设备
void device_destroy(struct class *cls, dev_t devt);

用来删除 cls 类中的devt设备属性文件,udev 会自动识别从而进行设备节点的删除。

  • 步骤四:使用函数class_destroy删除类class
 void class_destroy(struct class *cls);

该函数只有一个参数,为要删除的类,并且函数无返回值。

示例代码

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

#define DEVICE_NUMBER 1 // 设备数量
#define DEVICE_SNAME "schrdev" // 静态申请时设备名
#define DEVICE_ANAME "achrdev" // 动态申请时设备名
#define DEVICE_MINOR_NUM 0 // 次设备号起始地址
#define DEVICE_CLASS_NAME "myTestClass" // 类名
#define DEVICE_MYNAME "mytest"

// 打开设备
int chrdev_open(struct inode*inode,struct file*file)
{
	printk("chrdev_open is opened\r\n");

	return 0;
}

// 保存设备号 其中前12位为主设备号 后20位为次设备号
static dev_t dev_num;

// 定义主设备号 次设备号
static int major_num,minor_num; 

// 设备信息描述集
struct cdev cdev;

// 类描述集
struct class *cls;

// 设备描述集
struct device*device;

// 传递单个参数
module_param(major_num,int,S_IRUGO); // 可读
module_param(minor_num,int,S_IRUGO); // 可读

// 文件操作集
struct file_operations chrdev_opr = {
	.owner = THIS_MODULE,
	.open = chrdev_open
};

// 驱动的入口函数
static int __init dev_init(void)
{
	int ret;
	/* 步骤一:申请设备号 */
	// 有传递主设备号就静态申请
	if(major_num)
	{	
		dev_num = MKDEV(major_num, minor_num);//将主设备号 次设备号合并成设备号
		//参数分别表示 设备号 设备数量 设备名称
		ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); 
		if(ret < 0)
		{
			printk("dev_init error\r\n");
			return -1;
		}
	}	
	// 否则就动态申请
	else
	{
		// 参数:设备号 次设备号起始地址 设备数量 设备名称
		ret = alloc_chrdev_region(&dev_num , DEVICE_MINOR_NUM, DEVICE_NUMBER, DEVICE_ANAME);
		if(ret < 0)
		{
			printk("dev_init error\r\n");
			return -1;
		}
		// 获取主设备号 次设备号
		major_num = MAJOR(dev_num);
		minor_num = MINOR(dev_num);
	}
	printk("---------------------------------------\r\n");
	printk("dev_num:%d major_num:%d minor_num:%d\r\n",dev_num,major_num,minor_num);

	/* 步骤二:初始化设备 */
	// 初始化cdev
	cdev.owner = THIS_MODULE;
	// 初始化设备  
	cdev_init(&cdev, &chrdev_opr);

	/* 步骤三:注册设备到内核 */
	//添加(注册)到内核 参数: 设备   设备号  设备数量
	cdev_add(&cdev, dev_num, DEVICE_NUMBER);

	/* 步骤四:先创建类再自动创建添加设备名称*/
	// 创建类 参数1: 类的归属   参数2: 类名
	cls = class_create(THIS_MODULE,DEVICE_CLASS_NAME);
	// 创建设备 参数1: 归属到类   参数2:设备的父设备 参数3:设备号 参数4:添加到设备的回调数据 参数5:设备名
	device = device_create(cls,NULL,dev_num,NULL,DEVICE_MYNAME);
	printk("auto add device name\r\n");
	return 0;
}

//驱动的出口函数
static void __exit dev_exit(void)
{
	/* 步骤一:注销设备号 参数:设备号   设备数量 */ 
	unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
	/* 步骤二:删除设备操作集 */
	cdev_del(&cdev);
	/* 步骤三:删除设备*/
	device_destroy(cls,dev_num);
	/* 步骤四: 删除类*/
	class_destroy( cls);
	
	printk(KERN_EMERG"dev_exit is successful\r\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");

使用加载驱动到内核insmod file.ko命令,得结果
在这里插入图片描述

在测试驱动前需要使用mknod /dev/mytest c 236 0创建设备文件,命令格式为:mknod + /路径/设备名称 +设备类型+主设备号 + 次设备号

然后就可写一个应用程序来测试我们的字符驱动是否成功注册,测试源码如下:

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

int main(int argc,char* argv[])
{
	char name[] = "/dev/mytest";

	int fd = open(name,O_RDONLY); // 仅读
	if(fd < 0)
	{
		printf("open is failed\n");
		return -1;
	}

	close(fd);

	return 0;
}

执行测试程序后结果如下:
在这里插入图片描述

注意:在运行测试程序时,一定要先创建驱动文件,否则就会报段错误,具体如下:
在这里插入图片描述


杂项设备与字符设备的比较

  • 杂项设备的主设备号固定为10,而字符设备的主设备号需要创建。
  • 杂项设备的创建相对简单,只需要填充设备操作集结构体、杂项设备结构体再注册即可。而字符设备需要经过 申请设备号、初始化并且注册cdev结构体、初始化硬件、构建设备文件操作描述集、生成并且添加设备节点。
  • 杂项设备与字符设备都需要构建文件操作描述集。(应用层调用驱动的核心)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JRebel热部署

热部署 什么热部署&#xff0c;简单来说我们正常的java项目需要编写java代码&#xff0c;但电脑执行的可不是java代码&#xff0c;而是转换后的class文件。这也意味着我们对程序进行微调&#xff0c;也要重新编译才能让程序展示我们需要的状态 而且不仅仅是我们手写的java文件…

统计学-R语言-4.3

文章目录 前言直方图茎叶图箱线图练习 前言 本篇介绍的是数值型数据怎么进行数据可视化&#xff0c;本篇介绍的有直方图、茎叶图、箱线图。 直方图 直方图&#xff08;Histogram&#xff09;用于描述连续型变量的频数分布&#xff0c;实际应用中常用于考察变量的分布是否对称…

谷粒商城P139集——云服务器frp内网穿透+nginx完美解决方案

1、修改本地HOST C:\Windows\System32\drivers\etc 目录下 host文件 上面前面是自己的云服务器ip 测试&#xff1a;如域名为gulimall.com 备注如果自己的云服务器nginx端口不是80 访问的时候记得打开 可以访问9200或者nacos尝试 则在浏览器中输入gulimall.com:9200&#xf…

解决“Ubuntu系统与windows系统之间不能执行复制粘贴”之问题

在win11中&#xff0c;发现“Ubuntu系统与windows系统之间不能互相复制粘贴”&#xff0c;只能通过“FPT客户端FileZilla”才能交换文件&#xff0c;但遇到字符串&#xff0c;就没法实现了&#xff0c;因此&#xff0c;在两个系统之间实现互相复制和粘贴字符串&#xff0c;就很…

x86是什么?

x86是一系列CPU架构的统称&#xff0c;这一术语起源于1978年&#xff0c;当时Intel发布了其首款16位微处理器——8086。这款处理器在当时引起了极大的关注&#xff0c;因为它首次引入了许多先进的技术&#xff0c;如寄存器间接寻址和分段内存管理等。随后&#xff0c;Intel又相…

【InternLM 大模型实战】第四课

XTuner 大模型单卡低成本微调实战 FINETUNE简介指令跟随微调增量预训练微调LoRA & QLoRA XTuner简介功能亮点适配多种生态适配多种硬件 8GB 显卡玩转LLMFlash AttentionDeepSpeed ZeRO 动手实战环节环境配置微调准备配置文件模型下载数据集下载修改配置文件开始微调将得到的…

中间人攻击如何进行防护

中间人攻击&#xff08;Man-in-the-Middle Attack&#xff0c;简称 MITM 攻击&#xff09;是一种常见的网络攻击方式&#xff0c;攻击者通过截获两个通信实体之间的通信数据&#xff0c;并在此基础上进行篡改、窃取或伪造等恶意行为。这种攻击方式因其攻击手段的隐蔽性和难以防…

LeetCode讲解篇之47. 全排列 II

文章目录 题目描述题解思路题解代码 题目描述 题解思路 初始化一个nums中元素是否被访问的数组used、记录还需要递归的深度deep&#xff0c;遍历nums&#xff0c;如果当前元素被访问过或者当前元素等于前一个元素且前一个元素没被访问过就跳过该次遍历&#xff0c;否则选择当前…

基于SSM+vue的篮球场预约管理系统(Java毕业设计)

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…

mac上部署单体hbase

1. 简介 HBase 是一个开源的、分布式的、版本化的典型非关系型数据库。它是 Google BigTable 的开源实现&#xff0c;并且是 Apache 基金会的 Hadoop 项目的一部分1。HBase 在 Hadoop Distributed File System (HDFS) 上运行&#xff0c;作为一个列式存储非关系数据库管理系统…

JFinal综合信息管理系统

项目地址&#xff1a;mendianyu/AdvancedManagement: 综合信息管理系统 (github.com) 项目演示地址&#xff1a;软件构造大作业演示视频_哔哩哔哩_bilibili 项目功能 一&#xff1a;基于Jfinal构建信息管理系统&#xff0c;要求包含用户管理&#xff0c;翻译业务模块管理&…

redis复习总结

我的redis 1. redis集群 主从集群【哨兵集群】&#xff1a;主从集群是指中&#xff0c;存在一个master节点和多个slave节点。master节点负责接收客户端的读写&#xff0c;slave节点负责读操作。主节点一旦接收到数据的变更&#xff0c;就会将数据同步至slave节点。 但这样的…

集合(二)Collection集合Set

一、Set介绍&#xff1a; 是一个散列的集合&#xff0c;数据会按照散列值存储的&#xff0c;如两个hello的散列值相同&#xff0c;会存储在同一个地址中&#xff0c;所以看到的就是只有一个hello在集合中了。 1、Set集合有两个主要的实现子类&#xff1a;Hashset和Treeset。ha…

ZooKeeper 实战(四) Curator Watch事件监听

文章目录 ZooKeeper 实战(四) Curator Watch事件监听0.前言1.Watch 事件监听概念2.NodeCache2.1.全参构造器参数2.2.代码DEMO2.3.日志输出 3.PathChildrenCache3.1.全参构造器参数3.2.子节点监听时间类型3.2.代码DEMO 4.TreeCache4.1.构造器参数4.2.代码DEMO4.3.日志输出 ZooKe…

代码随想录算法训练营第四天 |链表总结

1、每次先加判断&#xff1a; if (head null) {return head;} 2、ListNode dummy new ListNode(-1, head);和ListNode dummy new ListNode(-1);区别&#xff1a; 在Java中&#xff0c;ListNode dummy new ListNode(-1, head); 和 ListNode dummy new ListNode(-1); 的主…

软考学习笔记--操作系统-进程管理

进程管理是一个具有独立功能的程序关于数据集合的一次可以并发执行的运行活动&#xff0c;是系统进行资源分配和调度的基本单位。相对于程序&#xff0c;进程是动态的概念&#xff0c;而程序是静态的概念&#xff0c;是指令的集合。进程具有动态性和并发性&#xff0c;需要一定…

LeetCode讲解篇之39. 组合总和

文章目录 题目描述题解思路题解代码 题目描述 题解思路 首先排序数组&#xff0c;然后开始选择数字&#xff0c;当选择数字num后&#xff0c;在去选择大于等于num的合法数字&#xff0c;计算过程中的数字和&#xff0c;直到选数字和等于target, 加入结果集&#xff0c;若数字和…

爬虫案例—表情党图片data-src抓取

爬虫案例—表情党图片data-src抓取 表情党网址&#xff1a;https://qq.yh31.com 抓取心情板块的图片data-src 由于此页面采用的是懒加载技术&#xff0c;为了节省网络带宽和减轻服务器压力。不浏览的图片&#xff0c;页面不加载&#xff0c;统一显示LOADING…。如下图&#x…

tkinter控件中文显示为unicode编码的解决办法

一、背景 最近使用python tkinter编写界面应用时&#xff0c;发现按钮的中文名称在windows上显示正常&#xff0c;但是在linux上显示为中文的unicode编码&#xff1b;文本输入框也是&#xff0c;输入中文输时&#xff0c;text控件上也显示为unicode编码&#xff0c;如下图所示…

【Python数据可视化】matplotlib之设置坐标:添加坐标轴名字、设置坐标范围、设置主次刻度、坐标轴文字旋转并标出坐标值

文章传送门 Python 数据可视化matplotlib之绘制常用图形&#xff1a;折线图、柱状图&#xff08;条形图&#xff09;、饼图和直方图matplotlib之设置坐标&#xff1a;添加坐标轴名字、设置坐标范围、设置主次刻度、坐标轴文字旋转并标出坐标值matplotlib之增加图形内容&#x…