Linux的中断上半部和中断下半部的概念,并利用任务队列(Tasklet)实现中断下半部的处理

news2025/1/24 5:13:17

中断上半部和中断下半部的介绍

在Linux内核中,中断处理机制被设计成“中断上半部(Top Half)”和“中断下半部(Bottom Half)”两个部分,这种设计主要目的是提高系统的中断响应效率,同时减少中断处理对内核其他操作的影响。

中断上半部(Top Half)

  • 概念:中断上半部是指中断发生后,硬件触发的中断处理程序(ISR, Interrupt Service Routine)。

  • 特点

    1. 运行在中断上下文中,不能睡眠。
    2. 优先级高,必须尽快完成,以便处理其他可能的中断。
    3. 通常只完成最必要的工作,例如清除中断源、读取硬件状态等。
    4. 阻塞所有同级或更低优先级的中断。
  • 局限性:由于不能睡眠且运行时间要求短,复杂的工作不能放在上半部。


中断下半部(Bottom Half)

  • 概念中断下半部通常是在中段上半部中指定的任务,当中断上半部完成后,会执行在中断下半部任务,这些任务没有中断上半部那样紧急。它运行在普通进程上下文中,可以执行相对复杂的逻辑。

  • 特点

    1. 在合适的时机由内核调度运行,不会阻塞中断。
    2. 可以睡眠,因为运行在普通的进程上下文中。
    3. 通常处理较为复杂的任务,例如数据处理、通知用户空间等。
  • 实现方式
    Linux 提供了多种机制来实现中断下半部:

    1. 软中断(SoftIRQ)
      • 用于非常高效的下半部实现,主要用于网络包处理等。
      • 不能睡眠。
      • 比如之前在博文 https://blog.csdn.net/wenhao_ir/article/details/145281064 中实现的内核定时器其实就是属于中断下半部的软中断。
    2. 任务队列(Tasklet)
      • 通过软中断实现的高级接口,适合较轻量的任务。
      • 也不能睡眠。
    3. 工作队列(Workqueue)
      • 运行在内核线程中,可以睡眠,适合复杂的任务处理。例子见 https://blog.csdn.net/wenhao_ir/article/details/145321621
    4. 线程化的中断处理
      • 利用函数request_threaded_irq在注册中断请求时,为该中断也注册一个属于这个中断的线程,当中断的处理函数(上半部)运行完毕后,唤醒这个线程,进而处理中断的下半部。例子见 https://blog.csdn.net/wenhao_ir/article/details/145326705

上半部和下半部的关系

  1. 上半部的任务是快速响应硬件,通知内核某个事件发生,并触发下半部的处理。
  2. 下半部完成真正的处理工作,将中断对内核的影响降到最低。

举例说明

  • 情景:按键中断
    • 上半部:读取硬件寄存器,获取按键的状态,并将事件放入缓冲区,同时触发下半部。
    • 下半部:将按键事件处理为具体的逻辑,比如按键消抖、通知用户空间等。

这种分工明确的设计,使得系统既能快速响应中断,又能在合适的时机完成复杂任务,从而提高整体性能和实时性。

本文代码在哪个基础上修改而成?

本文是在博文 https://blog.csdn.net/wenhao_ir/article/details/145228617 的代码基础上修改而成。

完整源代码

驱动程序gpio_key_drv.c中的代码

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>


struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
	struct tasklet_struct tasklet;
} ;

static struct gpio_key *gpio_keys_100ask;

/* 主设备号                                                                 */
static int major = 0;
static struct class *gpio_key_class;

static int g_key = 0;

static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);


/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

#define NEXT_POS(x) ((x+1) % BUF_LEN)

static int is_key_buf_empty(void)
{
	return (r == w);
}

static int is_key_buf_full(void)
{
	return (r == NEXT_POS(w));
}

static void put_key(int key_value)
{
	if (!is_key_buf_full())
	{
		g_keys[w] = key_value;
		w = NEXT_POS(w);
	}
}

static int get_key(void)
{
	int key_value = 0;
	if (!is_key_buf_empty())
	{
		key_value = g_keys[r];
		r = NEXT_POS(r);
	}
	return key_value;
}


/* 实现文件操作结构体中的read函数  */
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	int err;
	int key_value;
	
	wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
	//从缓形缓冲区中取出数据
	key_value = get_key();
	err = copy_to_user(buf, &key_value, 4);

	// 返回值为4表明读到了4字节的数据
	return 4;
}


/* 定义自己的file_operations结构体                                              */
static struct file_operations gpio_key_drv = {
	.owner	 = THIS_MODULE,
	.read    = gpio_key_drv_read,
};


static void key_tasklet_func(unsigned long data)
{
	/* data ==> gpio */
	struct gpio_key *gpio_key = (struct gpio_key *)data;

	int val;

    // 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
	// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
    val = gpiod_get_value(gpio_key->gpiod);

    // 打印中断号、GPIO引脚编号和电平值
    // printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);

	// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
	g_key = (gpio_key->gpio << 8) | val;
	//装按键值放入环形缓冲区
	put_key(g_key);
	wake_up_interruptible(&gpio_key_wait);

	printk("Tasklet_func from GPIO pin number: %d\n", gpio_key->gpio);
}


static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;

	tasklet_schedule(&gpio_key->tasklet);

    return IRQ_HANDLED;  // 表示中断已处理
}


/* 1. 从platform_device获得GPIO
 * 2. gpio=>irq
 * 3. request_irq
 */
static int gpio_key_probe(struct platform_device *pdev)
{
	int err;

	// 获取设备树节点指针
	struct device_node *node = pdev->dev.of_node;

	// count用于存储设备树中描述的GPIO口的数量
	int count;
	
	int i;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
		
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	count = of_gpio_count(node);
	if (!count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}

	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
	if (!gpio_keys_100ask) {
		printk("Memory allocation failed for gpio_keys_100ask\n");
		return -ENOMEM;
	}


	for (i = 0; i < count; i++)
	{
		//  获取GIPO的全局编号及其标志位信息的代码
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
			printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
			return -1;
		}

		// 获取GPIO口的GPIO描述符的代码
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		if (!gpio_keys_100ask[i].gpiod) {
			printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);
			return -EINVAL;
		}

		// 结构体gpio_key的成员flag用于存储对应的GPIO口是否是低电平有效,假如是低电平有效,成员flag的值为1,假如不是低电平有效,成员flag的值为0。
		// 后续代码实际上并没有用到成员flag,这里出现这句代码只是考虑到代码的可扩展性,所以在这里是可以删除的。
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;

		// 每次循环都重新初始化flags
    	flags = GPIOF_IN;

		// 假如GPIO口是低电平有效,则把flags添加上低电平有效的信息
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;

		// 请求一个GPIO硬件资源与设备结构体`pdev->dev`进行绑定
		// 注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定
		// 由`devm`开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

		// 获取GPIO口的中断请求号
		gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);


		tasklet_init(&gpio_keys_100ask[i].tasklet, key_tasklet_func, (unsigned long)&gpio_keys_100ask[i]);
	}

	for (i = 0; i < count; i++)
	{
		char irq_name[32];  // 用于存储动态生成的中断名称

		//使用snprintf()函数将动态生成的中断名称写入irq_name数组
		snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i);  // 根据i生成名称

		//调用函数request_irq()来请求并设置一个中断
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);
	}

	/* 注册file_operations 	*/
	major = register_chrdev(0, "swh_read_keys_major", &gpio_key_drv);  

	gpio_key_class = class_create(THIS_MODULE, "swh_read_keys_class");
	if (IS_ERR(gpio_key_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "swh_read_keys_major");
		return PTR_ERR(gpio_key_class);
	}

	// 由于这里是把多个按键看成是一个设备,你可以想像一个键盘上对应多个按键,但键盘本身是一个设备,所以只有一个设备文件
	device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "read_keys0"); /* /dev/read_keys0 */
        
   
    return 0;
    
}

static int gpio_key_remove(struct platform_device *pdev)
{
    struct device_node *node = pdev->dev.of_node;
    int count;
    int i;

	device_destroy(gpio_key_class, MKDEV(major, 0));
	class_destroy(gpio_key_class);
	unregister_chrdev(major, "swh_read_keys_major");

    count = of_gpio_count(node);
    for (i = 0; i < count; i++) 
	{
        // 只有在irq有效时才释放中断资源
        if (gpio_keys_100ask[i].irq >= 0) {
            // 释放GPIO中断资源,下面这句代码做了下面两件事:
			// 1、解除 `gpio_keys_100ask[i].irq` 中断号和 `gpio_key_isr` 中断处理函数的绑定。
			// 2、解除 `gpio_keys_100ask[i].irq` 中断号和中断处理函数与 `gpio_keys_100ask[i]` 数据结构的绑定。
            free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
        }

        // 释放GPIO描述符
        if (gpio_keys_100ask[i].gpiod) {
            gpiod_put(gpio_keys_100ask[i].gpiod);
        }
    }

    // 释放内存
    kfree(gpio_keys_100ask);
    return 0;
}


static const struct of_device_id irq_matach_table[] = {
    { .compatible = "swh-gpio_irq_key" },
    { },
};

/* 1. 定义platform_driver */
static struct platform_driver gpio_keys_driver = {
    .probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "swh_irq_platform_dirver",
        .of_match_table = irq_matach_table,
    },
};

/* 2. 在入口函数注册platform_driver */
static int __init gpio_key_init(void)
{
    int err;
    
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    err = platform_driver_register(&gpio_keys_driver); 
	
	return err;
}

/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 *     卸载platform_driver
 */
static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&gpio_keys_driver);
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");

测试程序button_test.c中的代码

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

/*
 * ./button_test /dev/100ask_button0
 *
 */

// 打印线程的执行函数
void* print_while_waiting(void* arg)
{
    while (1)
    {
        printf("I am another thread, and while the main thread is waiting for a button to be pressed, I can still run normally.\n");
        sleep(10); // 每隔10秒打印一次
    }
    return NULL;
}

int main(int argc, char **argv)
{
    int fd;
    int val;
    pthread_t print_thread;
    int keystroke = 0; //记录按键次数

    /* 1. 判断参数 */
    if (argc != 2) 
    {
        printf("Usage: %s <dev>\n", argv[0]);
        return -1;
    }

    /* 2. 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (fd == -1)
    {
        printf("Can not open file %s\n", argv[1]);
        return -1;
    }

    // 创建一个线程,每隔一段时间打印输出一条信息表示在等待按键期间,另外的线程在继续正常执行。
    if (pthread_create(&print_thread, NULL, print_while_waiting, NULL) != 0)
    {
        printf("Failed to create print thread\n");
        close(fd);
        return -1;
    }

    while (1)
    {
        /* 3. 读文件 */
        read(fd, &val, 4);
        
        /* 提取 GPIO 编号和逻辑值 */
        int gpio_number = (val >> 8) & 0xFF; // 高8位为 GPIO 编号
        int gpio_value = val & 0xFF;         // 低8位为逻辑值

        keystroke++;

        /* 打印读到的信息 */
        printf("GPIO Number: %d, Logical Value: %d\n", gpio_number, gpio_value);
        printf("keystrokes is %d\n", keystroke);
    }

	//pthread_join的作用是使主线程等待线程print_threa结束后再继续执行剩下的代码。
	//如果主线程在结束时未等待子线程完成,可能会导致未完成的资源清理或意外的程序终止。
	//这里由于主线程中有个条件永远为真的while循环,实际上这句代码没有实际作用。
    pthread_join(print_thread, NULL);

    close(fd);

    return 0;
}

与任务队列(Tasklet)相关的代码分析

任务队列(Tasklet)和上一篇博文用内核定时器实现的使用很类似,所以要是时间充裕,可以先去看下上一篇博文 https://blog.csdn.net/wenhao_ir/article/details/145281064

在本文的代码中,我们把在硬中断处理函数中对按键值的读取功能放到任务队列(Tasklet)的处理函数,而在硬中断处理函数中,只做一件事,就是去添加任务队列(Tasklet)作为中断下半部,这就的修改使得硬中断处理函数变得更加简洁高效,使得硬中断的处理时间变得更短,从而尽量减少对系统的影响,读取按钮值并把按钮值放到环形缓冲区的功能其实并不是那么紧急,所以我们放到任务队列(Tasklet)中,即中断下半部去处理。

首先在按键结构体gpio_key中添加成员struct tasklet_struct tasklet,如下:
在这里插入图片描述
为什么呢?因为每一个GPIO口我们都要为其分配一个tasklet_struct结构体。

然后在platform中的probe操作函数gpio_key_probe对每个GPIO口初始化时,为每个GPIO口初始化一个Tasklet,代码如下:
在这里插入图片描述

tasklet_init(&gpio_keys_100ask[i].tasklet, key_tasklet_func, (unsigned long)&gpio_keys_100ask[i]);

tasklet_init的第1个参数就是一个tasklet_struct结构体的实例,第2个参数key_tasklet_func就是任务队列的处理函数(回调函数),每3个参数是传给处理函数的数据,其类型为unsigned long类型,所以要进行下强制类型转换。

然后在硬中断中我们就把Tasklet添加到任务队列,这样在硬件中断完成后,就去执行Tasklet对应的处理函数。
在这里插入图片描述
代理很简单,这里就不赘述了。
注意:tasklet_schedule函数并一定只能在硬中断的处理函数中被调用,它也可以在别的地方调用。详情在本篇博文后面有说明。

处理函数(回调函数)的代码如下:

static void key_tasklet_func(unsigned long data)
{
	/* data ==> gpio */
	struct gpio_key *gpio_key = (struct gpio_key *)data;

	int val;

    // 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
	// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
    val = gpiod_get_value(gpio_key->gpiod);

    // 打印中断号、GPIO引脚编号和电平值
    // printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);

	// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
	g_key = (gpio_key->gpio << 8) | val;
	//装按键值放入环形缓冲区
	put_key(g_key);
	wake_up_interruptible(&gpio_key_wait);

	printk("Tasklet_func from GPIO pin number: %d\n", gpio_key->gpio);
}

最后,收尾工作不能忘,卸载模块时别忘了删除掉Tasklet占用的相关资源,代码如下:
在这里插入图片描述

注意:tasklet_schedule函数并不一定要运行在硬件中断的处理函数中

tasklet_schedule 函数也不一定非要运行在硬件中断的处理函数中,它可以在任何内核上下文中调用,前提是代码所在的上下文允许执行内核函数。


tasklet_schedule 的调用场景

  1. 硬件中断的处理函数中
    • 这是最常见的场景之一。
    • 中断处理函数中通常需要尽量简化操作,将复杂任务交由更低优先级的机制来处理。tasklet_schedule 可以将任务延迟到 tasklet 的执行上下文中完成。
    • Tasklet 会在软中断上下文中运行,因此对实时性要求更低。

  1. 普通的内核上下文
    • tasklet_schedule 也可以在普通的进程上下文中调用(例如在驱动的 openwriteioctl 等回调中)。
    • 此时调用 tasklet_schedule 后,Tasklet 将在软中断上下文中被调度执行。

  1. 定时器回调函数中
    • 在内核定时器的回调函数中调用 tasklet_schedule 也是合法的。定时器回调函数通常运行在软中断上下文中,而 tasklet_schedule 只是将任务推入 tasklet 的运行队列中。

为什么不一定要在硬件中断上下文中调用?

  • Tasklet 的设计目的:

    • Tasklet 是软中断的延伸,主要用来在软中断上下文中执行较简单的任务。调用 tasklet_schedule 不依赖中断上下文,它只将任务标记为待执行,内核会在适当时机运行它。
  • 上下文限制:

    • Tasklet 本质上运行在软中断上下文中,这意味着:
      1. 它不能进行可能会睡眠或阻塞的操作(例如调用 msleepmutex_lock)。
      2. 它不能访问进程上下文相关的数据(例如用户空间的内存)。
  • 对实时性的需求:

    • Tasklet 的优先级高于工作队列(Workqueue),但低于硬件中断。即便调用 tasklet_schedule 时不在硬件中断上下文中,Tasklet 也会按照优先级被及时调度。
      关于工作队列(Workqueue)的介绍,见博文 https://blog.csdn.net/wenhao_ir/article/details/145321621

tasklet_schedule 函数和 schedule_work 函数的对比
关于工作队列(Workqueue)的介绍,见博文 https://blog.csdn.net/wenhao_ir/article/details/145321621

特性tasklet_scheduleschedule_work
执行上下文软中断上下文kworker(内核线程),进程上下文
是否可阻塞
调用场景限制无限制,可以在任何内核代码中调用无限制,可以在任何内核代码中调用
适用任务类型简单、快速的任务较复杂或可能需要阻塞的任务
优先级高于工作队列,低于硬件中断处理低于 Tasklet,依赖内核线程调度

总结

tasklet_schedule 并不强制要求在硬件中断的处理函数中调用。它可以在任何允许执行内核函数的上下文中调用(硬件中断、软中断、进程上下文)。调用后,它会将任务添加到 tasklet 的待执行队列中,稍后会在软中断上下文中被内核调度执行。关键是确保 Tasklet 中的任务不会进行阻塞操作,因为软中断上下文不允许睡眠。

设备树文件的修改和更新

和下面两篇博文一样:
https://blog.csdn.net/wenhao_ir/article/details/145225508
https://blog.csdn.net/wenhao_ir/article/details/145176361

Makfile文件内容

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
	make -C $(KERN_DIR) M=`pwd` modules
	# 因为测试程序中有线程的创建,所以下面的语句需要添加 -lpthread 链接选项
	$(CROSS_COMPILE)gcc -o button_test_02 button_test.c -lpthread 

clean:
	make -C $(KERN_DIR) M=`pwd` clean
	rm -rf modules.order
	rm -f button_test_02

obj-m += gpio_key_drv.o

交叉编译出驱动模块和测试程序

源码复制到Ubuntu中。

在这里插入图片描述

make

在这里插入图片描述
将交叉编译出的gpio_key_drv.kobutton_test_02复制到NFS文件目录中,备用。
在这里插入图片描述

加载模块

打开串口终端→打开开发板→挂载网络文件系统

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod /mnt/tasklet/gpio_key_drv.ko

在这里插入图片描述

检查设备文件生成没有

ls /dev/

有了:
在这里插入图片描述

运行测试程序

先把内核printk打印的显示打开:

echo "7 4 1 7" > /proc/sys/kernel/printk
cd /mnt/tasklet
./button_test_02 /dev/read_keys0

在这里插入图片描述
可见实现了我们的需求,按键值的读取和放入环形缓冲区放在了任务队列(Tasklet)的处理函数中进行了。

卸载驱动程序模块

rmmod gpio_key_drv.ko

在这里插入图片描述
运行上面命令后,过了较长时间系统仍然能正常运行,说明卸载没有问题。
至于设备文件、设备类、驱动程序还在不在,在之前的博文中已经测试了,这里就不测试了。
这里我主要是要看任务队列(Tasklet)占用的相关资源是不是被正确释放了,如果没有正确释放,系统是会崩溃的。

附完整工程文件

https://pan.baidu.com/s/1RlH8rPN1atJemNIFAtCzYA?pwd=6g77

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

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

相关文章

数学规划问题2 .有代码(非线性规划模型,最大最小化模型,多目标规划模型)

非线性规划模型 FIrst:转化为标准型 在matlab中求非线性规划的函数 练习题: 典型例题: 最大最小化模型 核心思想&#xff1a; matlab的模型求解 经典例题: 多目标规划模型 基本概念 求解思路: 模型构建步骤 经典例题: 非线性规划模型 非线性规划&#xff08;Nonl…

linux 下tensorrt的yolov8的前向推理(c++ 版本)的实现

一、环境搭建 cuda 11.4 ubuntu 20.04 opencv-4.5.2 1.1 配置tensorrt 根据本机的硬件配置及cuda的版本&#xff0c;选择TensorRT-8.6.1.6的版本&#xff0c;下载网址为: TensorRT SDK | NVIDIA Developer 根据官网的说明&#xff0c;下载对应的压缩包即可。解压后&…

VUE elTree 无子级 隐藏展开图标

这4个并没有下级节点&#xff0c;即它并不是叶子节点&#xff0c;就不需求展示前面的三角展开图标! 查阅官方文档如下描述&#xff0c;支持bool和函数回调处理&#xff0c;这里咱们选择更灵活的函数回调实现。 给el-tree结构配置一下props&#xff0c;注意&#xff01; :pr…

windows git bash 使用zsh 并集成 oh my zsh

参考了 这篇文章 进行配置&#xff0c;记录了自己的踩坑过程&#xff0c;并增加了 zsh-autosuggestions 插件的集成。 主要步骤&#xff1a; 1. git bash 这个就不说了&#xff0c;自己去网上下&#xff0c;windows 使用git时候 命令行基本都有它。 主要也是用它不方便&…

Glary Utilities Pro 多语便携版系统优化工具 v6.21.0.25

Glary Utilities是一款功能强大的系统优化工具软件&#xff0c;旨在帮助用户清理计算机垃圾文件、修复系统错误、优化系统性能等。 软件功能 清理和修复&#xff1a;可以清理系统垃圾文件、无效注册表项、无效快捷方式等&#xff0c;修复系统错误和蓝屏问题。 优化和加速&…

【Python使用】嘿马python基础入门全体系教程第12篇:__init__()方法,说明:【附代码文档】

本教程的知识点为&#xff1a;计算机组成 计算机是由什么组成的&#xff1f; 1. 硬件系统&#xff1a; 2. 软件系统&#xff1a; 目标 运算符的分类 1. 算数运算符 2. 赋值运算符 3. 复合赋值运算符 判断语句和循环语句 if嵌套 1. if嵌套的格式 2. if嵌套的应用 if嵌套执行流程…

从入门到精通:RabbitMQ的深度探索与实战应用

目录 一、RabbitMQ 初相识 二、基础概念速览 &#xff08;一&#xff09;消息队列是什么 &#xff08;二&#xff09;RabbitMQ 核心组件 三、RabbitMQ 基本使用 &#xff08;一&#xff09;安装与环境搭建 &#xff08;二&#xff09;简单示例 &#xff08;三&#xff09;…

【Block总结】WTConv,小波变换(Wavelet Transform)来扩展卷积神经网络(CNN)的感受野

论文解读&#xff1a;Wavelet Convolutions for Large Receptive Fields 论文信息 标题: Wavelet Convolutions for Large Receptive Fields作者: Shahaf E. Finder, Roy Amoyal, Eran Treister, Oren Freifeld提交日期: 2024年7月8日arXiv链接: Wavelet Convolutions for La…

Couchbase UI: Indexes

在Couchbase中&#xff0c;索引的这些指标可以帮助你评估索引的性能和状态。下面是每个指标的详细解释&#xff0c;以及如何判断索引的有效性&#xff1a; 1. Index Name&#xff08;索引名称&#xff09; 描述&#xff1a;每个索引都有一个唯一的名称。这个名称通常会包括表…

(3)STM32 USB设备开发-USB存储设备

例程&#xff1a;STM32USBdevice: 基于STM32的USB设备例子程序 - Gitee.com 本篇为使用芯片内部flash作为USB存储设备的例程&#xff0c;没有知识&#xff0c;全是实操&#xff0c;按照步骤就能获得一个STM32的U盘。本例子是在野火F103MINI开发板上验证的&#xff0c;如果代码…

细说STM32F407单片机电源低功耗StopMode模式及应用示例

目录 一、停止模式基础知识 1、进入停止模式 2、停止模式的状态 3、退出停止模式 4、SysTick定时器的影响 二、停止模式应用示例 1、示例功能和CubeMX项目配置 &#xff08;1&#xff09;时钟 &#xff08;2&#xff09;RTC &#xff08;3&#xff09;ADC1 &#xf…

Blazor-Blazor WebAssmbly项目结构(上)

创建项目 今天我们来创建一个BlazorWebAssmbly项目&#xff0c;来看看项目结构是如何得&#xff0c;我们创建带模板得项目&#xff0c;会创建出一个demo&#xff0c;来看看项目结构。 创建的项目可以直接启动运行&#xff0c;首次启动会看见加载的过程&#xff0c;这个过程…

【2024年终总结】我与CSDN的一年

&#x1f449;作者主页&#xff1a;心疼你的一切 &#x1f449;作者简介&#xff1a;大家好,我是心疼你的一切。Unity3D领域新星创作者&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6; &#x1f449;记得点赞 &#x1f44d; 收藏 ⭐爱你们&#xff0c;么么哒 文章目录 …

开篇:吴恩达《机器学习》课程及免费旁听方法

课程地址&#xff1a; Machine Learning | Coursera 共包含三个子课程 Supervised Machine Learning: Regression and Classification | Coursera Advanced Learning Algorithms | Coursera Unsupervised Learning, Recommenders, Reinforcement Learning | Coursera 免费…

推荐一个开源的轻量级任务调度器!TaskScheduler!

大家好&#xff0c;我是麦鸽。 这次推荐一款轻量级的嵌入式任务调度器&#xff0c;目前已经有1.4K的star&#xff0c;这个项目比较轻量化&#xff0c;只有5个源文件&#xff0c;可以作为学习的一个开源项目。 核心文件 项目概述&#xff1a; 这是一个轻量级的协作式多任务处理&…

暑期实习准备:C语言(持续更新)

1.局部变量和全局变量 局部变量的作用域是在变量所在的局部范围&#xff0c;全局变量的作用域是整个工程&#xff1b;局部变量的生命周期是作用域内&#xff0c;全局变量的生命周期是整个程序的生命周期&#xff0c;当两者命名冲突时&#xff0c;优先使用的是局部变量。 2.C语言…

Harmony Next 支持创建分身

应用分身能实现在一个设备上安装多个相同的应用&#xff0c;实现多个账号同时登录使用和运行并且互不影响。主要应用场景有社交账号双开、游戏大小号双开等&#xff0c;无需账号切换&#xff0c;从而省去频繁登录的繁琐。 Harmony Next 很容易就能让 App 支持创建分身。 官方文…

java ,springboot 对接支付宝支付,实现生成付款二维码,退款,查询订单状态等接口

查看文档 支付宝文档地址&#xff1a; 小程序文档 - 支付宝文档中心 使用沙箱环境 沙箱登录地址 登录 - 支付宝 点击查看 才能看钥匙截图写错了。。 问号可以看默认加密方式 点击沙箱帐号 这里我们就具备所有条件了 实战开始 pom文件增加依赖 <dependency> <gro…

深入内核讲明白Android Binder【三】

深入内核讲明白Android Binder【三】 前言一、服务的获取过程内核源码解析1. 客户端获取服务的用户态源码回顾2. 客户端获取服务的内核源码分析2.1 客户端向service_manager发送数据1. binder_ioctl2. binder_ioctl_write_read3. binder_thread_write4. binder_transaction4.1 …

chrome游览器JSON Formatter插件无效问题排查,FastJsonHttpMessageConverter导致Content-Type返回不正确

问题描述 chrome游览器又一款JSON插件叫JSON Formatter&#xff0c;游览器GET请求调用接口时&#xff0c;如果返回的数据是json格式&#xff0c;则会自动格式化展示&#xff0c;类似这样&#xff1a; 但是今天突然发现怎么也格式化不了&#xff0c;打开一个json文件倒是可以格…