Linux 内核:设备驱动模型(6)设备资源管理

背景

不要总是用Linux 2.6的风格来写驱动代码了,也该与时俱进一下。

参考:http://www.wowotech.net/device_model/device_resource_management.html

前言

每当driver probe一个具体的device实例的时候,都需要建立一些私有的数据结构来保存该device的一些具体的硬件信息。

在过去,驱动工程师多半使用kmalloc或者kzalloc来分配内存,但这会带来一些潜在的问题。例如:在初始化过程中,有各种各样可能的失败情况,这时候就依靠driver工程师小心的撰写代码,释放之前分配的内存。

当然,初始化过程中,除了memory,driver会为probe的device分配各种资源,例如IRQ 号,io memory map、DMA等等。当初始化需要管理这么多的资源分配和释放的时候,很多驱动程序都出现了资源管理的issue。

而且,由于这些issue是异常路径上的issue,不是那么容易测试出来,更加重了解决这个issue的必要性。

内核解决这个问题的模式(所谓解决一类问题的设计方法就叫做设计模式)是Devres,即device resource management模块。

先看一个例子

// drivers/media/platform/soc_camera/mx1_camera.c
static int __init mx1_camera_probe(struct platform_device *pdev)
{
// ... res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
irq = platform_get_irq(pdev, 0);
if (!res || (int)irq <= 0) {
err = -ENODEV;
goto exit;
} clk = clk_get(&pdev->dev, "csi_clk");
if (IS_ERR(clk)) {
err = PTR_ERR(clk);
goto exit;
} pcdev = kzalloc(sizeof(*pcdev), GFP_KERNEL);
if (!pcdev) {
dev_err(&pdev->dev, "Could not allocate pcdev\n");
err = -ENOMEM;
goto exit_put_clk;
} // ... /*
* Request the regions.
*/
if (!request_mem_region(res->start, resource_size(res), DRIVER_NAME)) {
err = -EBUSY;
goto exit_kfree;
} base = ioremap(res->start, resource_size(res));
if (!base) {
err = -ENOMEM;
goto exit_release;
}
// ... /* request dma */
pcdev->dma_chan = imx_dma_request_by_prio(DRIVER_NAME, DMA_PRIO_HIGH);
if (pcdev->dma_chan < 0) {
dev_err(&pdev->dev, "Can't request DMA for MX1 CSI\n");
err = -EBUSY;
goto exit_iounmap;
}
// ... /* request irq */
err = claim_fiq(&fh);
if (err) {
dev_err(&pdev->dev, "Camera interrupt register failed\n");
goto exit_free_dma;
} // ...
err = soc_camera_host_register(&pcdev->soc_host);
if (err)
goto exit_free_irq; dev_info(&pdev->dev, "MX1 Camera driver loaded\n"); return 0; exit_free_irq:
disable_fiq(irq);
mxc_set_irq_fiq(irq, 0);
release_fiq(&fh);
exit_free_dma:
imx_dma_free(pcdev->dma_chan);
exit_iounmap:
iounmap(base);
exit_release:
release_mem_region(res->start, resource_size(res));
exit_kfree:
kfree(pcdev);
exit_put_clk:
clk_put(clk);
exit:
return err;
}

相信每一个写过Linux driver的工程师,都在probe函数中遇到过上面的困惑:

  • 要顺序申请多种资源(IRQ、Clock、memory、regions、ioremap、dma、等等),只要任意一种资源申请失败,就要回滚释放之前申请的所有资源。
  • 于是函数的最后,一定会出现很多的goto标签(如上面的exit_free_irq、exit_free_dma、等等),并在申请资源出错时,小心翼翼的goto到正确的标签上,以便释放已申请资源。

正像上面代码一样,整个函数被大段的、重复的“if (condition) { err = xxx; goto xxx; }”充斥,浪费精力,容易出错,不美观。

最终,Linux设备模型借助device resource management(设备资源管理),帮我们解决了这个问题。driver你只管申请就行了,不用考虑释放,我设备模型帮你释放。既然你驱动需要用的资源都是是设备的资源,那么资源的管理归于device,也就是说不需要driver过多的参与。当device和driver detach的时候,device会自动的释放其所有的资源。

最终,我们的driver可以这样写:

static int __init mx1_camera_probe(struct platform_device *pdev)
{
// ... res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
irq = platform_get_irq(pdev, 0);
if (!res || (int)irq <= 0) {
return -ENODEV;
} clk = devm_clk_get(&pdev->dev, "csi_clk");
if (IS_ERR(clk)) {
return PTR_ERR(clk);
} pcdev = devm_kzalloc(&pdev->dev, sizeof(*pcdev), GFP_KERNEL);
if (!pcdev) {
dev_err(&pdev->dev, "Could not allocate pcdev\n");
return -ENOMEM;
} // ... /*
* Request the regions.
*/
if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res), DRIVER_NAME)) {
return -EBUSY;
} base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
if (!base) {
return -ENOMEM;
}
// ... /* request dma */
pcdev->dma_chan = imx_dma_request_by_prio(DRIVER_NAME, DMA_PRIO_HIGH);
if (pcdev->dma_chan < 0) {
dev_err(&pdev->dev, "Can't request DMA for MX1 CSI\n");
return -EBUSY;
}
// ... /* request irq */
err = claim_fiq(&fh);
if (err) {
dev_err(&pdev->dev, "Camera interrupt register failed\n");
return err;
} // ...
err = soc_camera_host_register(&pcdev->soc_host);
if (err)
return err; dev_info(&pdev->dev, "MX1 Camera driver loaded\n"); return 0;
}

怎么做到呢?注意上面“devm_”开头的接口,答案就在那里。

不要再使用那些常规的资源申请接口,用devm_xxx的接口代替。

为了保持兼容,这些新接口和旧接口的参数保持一致,只是名字前加了“devm_”,并多加一个struct device指针。

devm_xxx接口

下面列举一些常用的资源申请接口,它们由各个framework(如clock、regulator、gpio、等等)基于device resource management实现。

使用时,直接忽略“devm_”的前缀,后面剩下的部分,driver工程师都很熟悉。

只需记住一点,driver可以只申请,不释放,设备模型会帮忙释放。

不过如果为了严谨,在driver remove时,可以主动释放(也有相应的接口,这里没有列出)。

extern void *devm_kzalloc(struct device *dev, size_t size, gfp_t gfp);

void __iomem *devm_ioremap_resource(struct device *dev,
struct resource *res);
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
unsigned long size); struct clk *devm_clk_get(struct device *dev, const char *id); int devm_gpio_request(struct device *dev, unsigned gpio,
const char *label); static inline struct pinctrl * devm_pinctrl_get_select(
struct device *dev, const char *name) static inline struct pwm_device *devm_pwm_get(struct device *dev,
const char *consumer); struct regulator *devm_regulator_get(struct device *dev, const char *id); static inline int devm_request_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, unsigned long irqflags,
const char *devname, void *dev_id); struct reset_control *devm_reset_control_get(struct device *dev,
const char *id);

设备资源

一个设备能工作,需要依赖很多的外部条件,如供电、时钟等等,这些外部条件称作设备资源(device resouce)。

对于现代计算机的体系结构,可能的资源包括:

  • power,供电。
  • clock,时钟。
  • memory,内存,在kernel中一般使用kzalloc分配。
  • GPIO,用户和CPU交换简单控制、状态等信息。
  • IRQ,触发中断。
  • DMA,无CPU参与情况下进行数据传输。
  • 虚拟地址空间,一般使用ioremap、request_region等分配。

而在Linux kernel的眼中,“资源”的定义更为广义,比如PWM、RTC、Reset,都可以抽象为资源,供driver使用。

在较早的kernel中,系统还不是特别复杂,且各个framework还没有成型,因此大多的资源都由driver自行维护。但随着系统复杂度的增加,driver之间共用资源的情况越来越多,同时电源管理的需求也越来越迫切。

于是kernel就将各个resource的管理权收回,基于“device resource management”的框架,由各个framework统一管理,包括分配和回收。

device resource management的软件框架

位于“drivers/base/devres.c”中,它的实现非常简单,为什么呢?因为资源的种类有很多,表现形式也多种多样,而devres不可能一一知情,也就不能进行具体的分配和回收。

因此,devres能做的(也是它的唯一功能),就是:

  • 提供一种机制,将系统中某个设备的所有资源,以链表的形式,组织起来,以便在driver detach的时候,自动释放。

而更为具体的事情,如怎么抽象某一种设备,则由上层的framework负责。这些framework包括:regulator framework(管理power资源),clock framework(管理clock资源),interrupt framework(管理中断资源)、gpio framework(管理gpio资源),pwm framework(管理PWM),等等。

其它的driver,位于这些framework之上,使用它们提供的机制和接口,开发起来就非常方便了。

device原型中的devres_head

先从struct device开始吧!该结构中有一个名称为“devres_head”的链表头,用于保存该设备申请的所有资源。

devres device resource

struct device {
// ...
spinlock_t devres_lock;
struct list_head devres_head;
//...
}

devres原型

devres代表了资源的数据结构。

不知道您是否注意到,devres有关的数据结构,是在devres.c中定义的。

换句话说,是对其它模块透明的。这真是优雅的设计(尽量屏蔽细节)!

当然了,有关的接口还是公开的

// drivers/base/devres.c
struct devres {
struct devres_node node;
/* -- 3 pointers */
unsigned long long data[]; /* guarantee ull alignment */
};

咋一看非常简单,一个struct devres_node的变量node,一个零长度数组data,但其中有无穷奥妙,让我们继续分析。

  • data是一个零长数组,用于存放所申请的不定长内存;因为整个memory空间是连续的,因此可以通过释devres指针,释放所有的空间,包括data所指的那片不定长度的、具体资源所用的空间。

  • 而node用于将devres组织起来,方便插入到device结构的devres_head链表中

devres_node原型

// base/devres.c
struct devres_node {
struct list_head entry;
dr_release_t release;
#ifdef CONFIG_DEBUG_DEVRES
const char *name;
size_t size;
#endif
};

entry:刚刚说了,devres使用node用于将devres组织起来,方便插入到device结构的devres_head链表中

release:资源的存在形式到底是什么,device resource management并不知情,因此需要上层模块提供一个release的回调函数,用于release资源。

抛开用于debug的变量不说,也很简单,一个entry list_head,一个release回调函数。看不出怎么抽象资源啊!别急,奥妙都在data这个零长度数组上面呢。

向上层framework提供的接口

其实有两对:

  • devres_alloc/devres_free
  • devres_add/devres_remove

devres_alloc/devres_free

// drivers/base/devres.c
/**
* devres_alloc - Allocate device resource data
* @release: Release function devres will be associated with
* @size: Allocation size
* @gfp: Allocation flags
*
* Allocate devres of @size bytes. The allocated area is zeroed, then
* associated with @release. The returned pointer can be passed to
* other devres_*() functions.
*
* RETURNS:
* Pointer to allocated devres on success, NULL on failure.
*/
void * devres_alloc(dr_release_t release, size_t size, gfp_t gfp)
{
struct devres *dr; dr = alloc_dr(release, size, gfp | __GFP_ZERO);
if (unlikely(!dr))
return NULL;
return dr->data;
}
EXPORT_SYMBOL_GPL(devres_alloc); /**
* devres_free - Free device resource data
* @res: Pointer to devres data to free
*
* Free devres created with devres_alloc().
*/
void devres_free(void *res)
{
if (res) {
struct devres *dr = container_of(res, struct devres, data); BUG_ON(!list_empty(&dr->node.entry));
kfree(dr);
}
}
EXPORT_SYMBOL_GPL(devres_free);

devres_alloc调用alloc_dr,分配一个struct devres类型的变量,并返回其中的data指针(data变量实际上是资源的代表)。

在alloc_dr中初始化

static __always_inline struct devres * alloc_dr(dr_release_t release,
size_t size, gfp_t gfp)
{
size_t tot_size = sizeof(struct devres) + size;
struct devres *dr; dr = kmalloc_track_caller(tot_size, gfp);
if (unlikely(!dr))
return NULL; memset(dr, 0, offsetof(struct devres, data)); INIT_LIST_HEAD(&dr->node.entry);
dr->node.release = release;
return dr;
}

看第一句就可以了,在资源size之前,加一个struct devres的size,就是total分配的空间。

除去struct devres的,就是资源的(由data指针访问)。

之后是初始化struct devres变量的node,可以看到,devres_alloc指定的release方法,便于在适当的时机执行。

devres_add/devres_remove

void devres_add(struct device *dev, void *res)
{
struct devres *dr = container_of(res, struct devres, data);
unsigned long flags; spin_lock_irqsave(&dev->devres_lock, flags);
// 将资源添加到设备的资源链表头(devres_head)中。
add_dr(dev, &dr->node);
spin_unlock_irqrestore(&dev->devres_lock, flags);
}

从资源指针中,取出完整的struct devres指针,调用add_dr接口。

使用add_dr挂入devers链表中

将资源添加到设备的资源链表头(devres_head)中。

add_dr也很简单,把struct devres指针挂到设备的devres_head中即可

static void add_dr(struct device *dev, struct devres_node *node)
{
devres_log(dev, node, "ADD");
BUG_ON(!list_empty(&node->entry));
list_add_tail(&node->entry, &dev->devres_head);
}

devres_destroy

/**
* devres_destroy - Find a device resource and destroy it
* @dev: Device to find resource from
* @release: Look for resources associated with this release function
* @match: Match function (optional)
* @match_data: Data for the match function
*
* Find the latest devres of @dev associated with @release and for
* which @match returns 1. If @match is NULL, it's considered to
* match all. If found, the resource is removed atomically and freed.
*
* Note that the release function for the resource will not be called,
* only the devres-allocated data will be freed. The caller becomes
* responsible for freeing any other data.
*
* RETURNS:
* 0 if devres is found and freed, -ENOENT if not found.
*/
int devres_destroy(struct device *dev, dr_release_t release,
dr_match_t match, void *match_data)
{
void *res; res = devres_remove(dev, release, match, match_data);
if (unlikely(!res))
return -ENOENT; devres_free(res);
return 0;
}
EXPORT_SYMBOL_GPL(devres_destroy);

从设备中找出对应的资源,并摧毁。

以IRQ模块为例看看如何使用资源管理

先看一个使用device resource management的例子(IRQ模块):

/* include/linux/interrupt.h */
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
{
return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
devname, dev_id);
} /* kernel/irq/devres.c */
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id)
{
struct irq_devres *dr;
int rc; // 申请设备资源
dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres),
GFP_KERNEL);
if (!dr)
return -ENOMEM; // 使用设备资源做自己的事情
rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname,
dev_id);
// 如果失败,可以通过devres_free接口释放资源占用的空间
if (rc) {
devres_free(dr);
return rc;
}
dr->irq = irq;
dr->dev_id = dev_id; // 注册所使用的设备资源
devres_add(dev, dr); return 0;
}
EXPORT_SYMBOL(devm_request_threaded_irq); void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id)
{
struct irq_devres match_data = { irq, dev_id }; WARN_ON(devres_destroy(dev, devm_irq_release, devm_irq_match,
&match_data));
free_irq(irq, dev_id);
}
EXPORT_SYMBOL(devm_free_irq);

前面我们提过,上层的IRQ framework,会提供两个和request_irq/free_irq基本兼容的接口,这两个接口的实现非常简单,就是在原有的实现之上,封装一层devres的操作。

irq_devres原型

用于保存和resource有关的信息(对中断来说,就是IRQ num)

/*
* Device resource management aware IRQ request/free implementation.
*/
struct irq_devres {
unsigned int irq;
void *dev_id;
};

devm_irq_release

用于release resource的回调函数(这里的release,和memory无关,例如free IRQ)

static void devm_irq_release(struct device *dev, void *res)
{
struct irq_devres *this = res; free_irq(this->irq, this->dev_id);
}

因为回调函数是由devres模块调用的,由它的参数可知,struct irq_devres变量就是实际的“资源”,但对devres而言,它并不知道该资源的实际形态,因而是void类型指针。也只有这样,devres模块才可以统一的处理所有类型的资源。

申请设备资源

    dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres),
GFP_KERNEL);
if (!dr)
return -ENOMEM;

以回调函数、resource的size为参数,调用devres_alloc接口,为resource分配空间。

使用设备资源做自己的事情

    // 使用设备资源做自己的事情
rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname,
dev_id);
// 如果失败,可以通过devres_free接口释放资源占用的空间
if (rc) {
devres_free(dr);
return rc;
}

调用原来的中断注册接口(这里是request_threaded_irq),注册中断。该步骤和device resource management无关。

如果失败了,可以通过devres_free接口释放资源占用的空间。

注册所使用的设备资源

注册成功后,以设备指针(dev)和资源指针(dr)为参数,调用devres_add,将资源添加到设备的资源链表头

    devres_add(dev, dr);

到这里,设备资源管理框架就可以:用来在不需要使用的时候摧毁资源了。

用完以后摧毁资源

irq系统中,我们会调用devm_free_irq来释放中断。

void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id)
{
struct irq_devres match_data = { irq, dev_id }; WARN_ON(devres_destroy(dev, devm_irq_release, devm_irq_match,
&match_data));
free_irq(irq, dev_id);
}

而其中就会调用devres_destroy接口,将devresdevres_head中移除,并释放资源。

向设备模型提供的接口

向设备模型提供的接口:devres_release_all

这里是重点,用于自动释放资源。

devres_release_all

int devres_release_all(struct device *dev)
{
unsigned long flags; /* Looks like an uninitialized device structure */
if (WARN_ON(dev->devres_head.next == NULL))
return -ENODEV;
spin_lock_irqsave(&dev->devres_lock, flags);
return release_nodes(dev, dev->devres_head.next, &dev->devres_head,
flags);
}

以设备指针为参数,直接调用release_nodes

static int release_nodes(struct device *dev, struct list_head *first,
struct list_head *end, unsigned long flags)
__releases(&dev->devres_lock)
{
LIST_HEAD(todo);
int cnt;
struct devres *dr, *tmp; // 将设备所有的`devres`从设备的`devres_head`中移除
cnt = remove_nodes(dev, first, end, &todo); spin_unlock_irqrestore(&dev->devres_lock, flags); /* Release. Note that both devres and devres_group are
* handled as devres in the following loop. This is safe.
*/ list_for_each_entry_safe_reverse(dr, tmp, &todo, node.entry) {
devres_log(dev, &dr->node, "REL");
// 调用所有资源的release回调函数(例如上面`devm_irq_release`),
// 回调函数会回收具体的资源(如`free_irq`)。
dr->node.release(dev, dr->data);
// 最后,调用free,释放devres以及资源所占的空间
kfree(dr);
} return cnt;
}

调用时机

先回忆一下设备模型中probe的流程,devres_release_all接口被调用的时机有两个:

  • really_probe失败
  • 设备与驱动分离时:deriver dettach时(就是driver remove时)

really_probe失败

probe调用过程为(就不详细的贴代码了):__driver_attach/__device_attach-->driver_probe_device—>really_probe

really_probe调用driver或者bus的probe接口,如果失败(返回值非零,可参考本文开头的例子),则会调用devres_release_all

static int really_probe(struct device *dev, struct device_driver *drv)
{
int ret = 0; atomic_inc(&probe_count); dev->driver = drv; /* If using pinctrl, bind pins now before probing */
ret = pinctrl_bind_pins(dev);
if (ret)
goto probe_failed; if (driver_sysfs_add(dev)) {
printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n",
__func__, dev_name(dev));
goto probe_failed;
} if (dev->bus->probe) {
ret = dev->bus->probe(dev);
if (ret)
goto probe_failed;
} else if (drv->probe) {
ret = drv->probe(dev);
if (ret)
goto probe_failed;
} driver_bound(dev);
ret = 1;
pr_debug("bus: '%s': %s: bound device %s to driver %s\n",
drv->bus->name, __func__, dev_name(dev), drv->name);
goto done; probe_failed:
devres_release_all(dev);
// ...
return ret;
}

设备与驱动分离时

另外一个时机是在,deriver dettach时(就是driver remove时):driver_detach/bus_remove_device-->__device_release_driver-->devres_release_all

Linux 内核:设备驱动模型(6)设备资源管理的更多相关文章

  1. Linux设备驱动模型底层架构及组织方式

    1.什么是设备驱动模型? 设备驱动模型,说实话这个概念真的不好解释,他是一个比较抽象的概念,我在网上也是没有找到关于设备驱动模型的一个定义,那么今天就我所学.所了解 到的,我对设备驱动模型的一个理解: ...

  2. Linux设备驱动模型简述(源码剖析)

    1. Linux设备驱动模型和sysfs文件系统 Linux内核在2.6版本中引入设备驱动模型,简化了驱动程序的编写.Linux设备驱动模型包含设备(device).总线(bus).类(class)和 ...

  3. Linux内核驱动学习(四)Platform设备驱动模型

    Linux platform设备驱动模型 文章目录 Linux platform设备驱动模型 前言 框架 设备与驱动的分离 设备(device) 驱动(driver) 匹配(match) 参考 前言 ...

  4. Linux中总线设备驱动模型及平台设备驱动实例

    本文将简要地介绍Linux总线设备驱动模型及其实现方式,并不会过多地涉及其在内核中的具体实现,最后,本文将会以平台总线为例介绍设备和驱动程序的实现过程. 目录: 一.总线设备驱动模型总体介绍及其实现方 ...

  5. Linux 字符设备驱动模型

    一.使用字符设备驱动程序 1. 编译/安装驱动 在Linux系统中,驱动程序通常采用内核模块的程序结构来进行编码.因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块 2. 创建设备文件 通 ...

  6. linux设备驱动模型

    尽管LDD3中说对多数程序员掌握设备驱动模型不是必要的,但对于嵌入式Linux的底层程序员而言,对设备驱动模型的学习非常重要. Linux设备模型的目的:为内核建立一个统一的设备模型,从而又一个对系统 ...

  7. linux设备驱动模型(kobject与kset)

    Linux设备模型的目的:为内核建立一个统一的设备模型,从而又一个对系统结构的一般性抽象描述.换句话说,Linux设备模型提取了设备操作的共同属性,进行抽象,并将这部分共同的属性在内核中实现,而为需要 ...

  8. Linux 总线、设备、驱动模型 与 设备树

    1.总线.设备.驱动模型 本着高内聚.低耦合的原则,Linux 把设备驱动模型分为了总线.设备和驱动三个实体,这三个实体在内核里的职责分别如下: 设备和驱动向总线进行注册,总线负责把设备和对应的驱动绑 ...

  9. Linux驱动之平台设备驱动模型简析(驱动分离分层概念的建立)

    Linux设备模型的目的:为内核建立一个统一的设备模型,从而有一个对系统结构的一般性抽象描述.换句话说,Linux设备模型提取了设备操作的共同属性,进行抽象,并将这部分共同的属性在内核中实现,而为需要 ...

  10. Linux设备驱动模型之platform(平台)总线详解

    /********************************************************/ 内核版本:2.6.35.7 运行平台:三星s5pv210 /*********** ...

随机推荐

  1. XAMPP安装与部署使用

    #注:本文章资料借鉴自于Sunny王维,地址:https://blog.csdn.net/qq_36595013/article/details/80373597 [一]XAMPP介绍 XAMPP. ...

  2. sqli-labs-master 第一关

    Sql注入 基础知识: 一··系统函数; 1. version()--MySQL 版本 2. user()--数据库用户名 3. database()--数据库名 4. @@datadir--数据库路 ...

  3. gprMax电磁波正演模拟方法

    文章首发于:https://blog.zhaoxuan.site/archives/37.html: 第一时间获取最新文章请关注博客个人站:https://blog.zhaoxuan.site. 目录 ...

  4. CompletableFuture学习总结

    CompletableFuture 简介 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通 ...

  5. ajax递归发送请求

    简介 大家都知道浏览器在处理http网络请求的时候,不同的浏览器会有不一样的并发限制,下表是一些主流浏览器对 HTTP 1.1 和 HTTP 1.0 的最大并发连接数目: Browser HTTP/1 ...

  6. Teamviewer 再次涨价,太贵了,有没有平替软件?

    今天打开 Teamviewer 网站,吓一跳,商业版基础款价格直接翻倍. 作为行业龙头,又是德国产品,Teamviewer 一直保持着高价格的特色.这两年 Teamviewer 的价格还逐年上涨,从每 ...

  7. C 语言编程 — 宏定义与预处理器指令

    目录 文章目录 目录 前文列表 宏 预处理器 预处理器指令 预处理器指令示例 预定义宏 预处理器指令运算符 宏延续运算符 字符串常量化运算符 标记(Token)粘贴运算符 defined() 运算符 ...

  8. Pageoffice6 实现后台批量生成Word文档

    在实际项目开发中经常会遇到后台动态生成文档的需求,目前网上有一些针对此需求的方案,如果您想要了解这些方案的对比,请查看后台生成单个Word文档中的"方案对比". 如果一次只生成一份 ...

  9. 西门子PLC设备如何接入AIRIOT物联网低代码平台 ?

    西门子PLC设备广泛应用于工业控制领域,高性能和稳定是它最大的优势.下面我们要把西门子300 1200 1500 PLC设备连接到AIRIOT物联网低代码平台,具体操作如下所示: 西门子驱动配置(配套 ...

  10. webapi添加添加websocket中间件

    添加位置 我按照MSDN的例子添加了一个复述客户端响应的中间件.需要注意的时,中间件采用那种方式添加,添加在哪. 哪种方式 我选择创建一条管道分支,只要时ws的连接请求,就转到这个分支 因此,我们需要 ...