Linux内核驱动:cdev、misc以及device三者之间的联系和区别

背景

我想在cdev中使用dev_err等log打印函数,但是跟踪了一下cdev中的原型,发现并不是我想要的。

常见的驱动是这样子使用dev_err的:

// 某个驱动,这里是电池有关的
static int32_t oz8806_read_byte(struct oz8806_data *data, uint8_t index, uint8_t *dat)
{
struct i2c_client *client = data->myclient; // ... dev_err(&client->dev, "%s: err %d, %d times\n", __func__, ret, i); // ...
}

i2c_client原型是这样子的,dev就是一个device:

// include/linux/i2c.h
struct i2c_client {
// ...
struct device dev; /* the device structure */
// ...
};

那么,我想只要找到cdev中的dev,也可以这样子用,对吧?但是:

// include/linux/cdev.h
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;

dev_t长这个样子:

// include/linux/types.h
typedef u32 __kernel_dev_t; typedef __kernel_dev_t dev_t;

我在困惑dev_t是什么东西以后,找到了下面这篇文章。

原文(有删改):https://blog.csdn.net/leochen_career/article/details/78540916

从/dev目录说起

从事Linux嵌入式驱动开发的人,都很熟悉下面的一些基础知识, 比如,对于一个char类型的设备,我想对其进行read wirte 和ioctl操作,那么:

1、我们通常会在内核驱动中实现一个file_operations结构体,然后分配主次设备号,调用cdev_add函数进行注册。

2、从/proc/devices下面找到注册的设备的主次设备号,在用mknod /dev/char_dev c major minor 命令行创建设备节点。

3、在用户空间open /dev/char_dev这个设备,然后进行各种操作。

OK,字符设备模型就这么简单,很多ABC教程都是一个类似的实现。

然后我们去看内核代码时,突然一脸懵逼。。。怎么内核代码里很多常用的驱动的实现不是这个样子的?没看到有file_operations结构体,我怎么使用这些驱动?看到了/dev目录下有需要的char设备,可是怎么使用呢?

Linux驱动模型的一个重要分界线

linux2.6版本以前,普遍的用法就像我上面说的一样。但是linux2.6版本以后,引用了Linux设备驱动模型,开始使用了基于sysfs的文件系统,出现让我们不是太明白的那些Linux内核驱动了。

也就是说,我们熟悉的那套驱动模式是2.6版本以前的(当然这是基础,肯定要会的)

我们不熟悉的驱动模型是2.6版本以后的。

cdev、misc以及device

cdev和device的区别和联系

struct  cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
}; struct device {
struct device *parent;
struct device_private *p;
struct kobject kobj;
const char *init_name; /* initial name of the device */
struct device_driver *driver; /* which driver has allocated this device */
void * driver_data ; /* Driver data, set and get with dev_set/get_drvdata */
dev_t devt; /* dev_t, creates the sysfs "dev" */
u32 id; /* device instance */
void (*release)( struct device *dev);
// ...
};

通过看这两个结构体发现,其实cdev和device之间没啥直接的联系,只有一个 dev_t kobject 是相同的。 dev_t 这个是联系两者的一个纽带了 。

通常可以这么理解: cdev是传统驱动的设备核心数据结构,device是Linux设备驱动模型中的核心数据结构。

miscdevice和device的区别和联系

struct  miscdevice  {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group groups;
const char *nodename;
umode_t mode;
};

从定义可以看出,miscdevice是 device的子类,是从device派生出来的结构体,也是属于device范畴的,也就是该类设备会统一在/sys目录下进行管理了。

miscdevice和cdev的区别和联系

通过上面的数据结构可以看到,两者都有一个file_operations dev_t(misdevice由于主设备号固定,所以结构体里只有一个minor)。

从数据结构上看,miscdevice是device和cdev的结合。

注册device与cdev的不同

要注册一个device设备,需要调用核心函数device_register()(或者说是device_add()函数) ;

要注册一个cdev设备,需要调用核心函数register_chrdev()(或者说是cdev_add()函数)

device_register函数与cdev、misc以及device

为了方便理解cdev、misc以及device这3者的关系,我们看看device_register()的实际调用。

有关的代码位于:drivers/base/core.c

device_register
device_add // 其中包含2个关键函数
// 将相关信息添加到/sys文件系统中(略)
device_create_file
// 将相关信息添加到/devtmpfs文件系统中
devtmpfs_create_node

device_create_file不做详细解析,因为devices本来就是/sys文件系统中的重要概念

关键是devtmpfs_create_node

什么是Devtmpfs 的概念

Devtmpfs lets the kernel create a tmpfs very early at kernel initialization , before any driver core device is registered . Every device with a major / minor will have a device node created in this tmpfs instance . After the rootfs is mounted by the kernel , the populated tmpfs is mounted at / dev .

devtmpfs_create_node()函数的核心是调用了内核的 vfs_mknod()函数,这样就在devtmpfs系统中创建了一个设备节点,当devtmpfs被内核mount到/dev目录下时,该设备节点就存在于/dev目录下,比如/dev/char_dev之类的。

vfs_mknod函数中会调用init_special_inode.

init_special_inode

如果node是一个char设备,会给i_fop 赋值一个默认的def_chr_fops,也就是说对该node节点,有一个默认的操作。在open一个字符设备文件时,最终总会调用chrdev_open,然后调用各个char设备自己的file_operations 定义的open函数。

void  init_special_inode( struct  inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
" inode %s:%lu\n" , mode, inode->i_sb->s_id,
inode->i_ino);
} /*
* Dummy default file-operations: the only thing this does
* is contain the open that then fills in the correct operations
* depending on the special file...
*/
const struct file_operations def_chr_fops = {
.open = chrdev_open ,
.llseek = noop_llseek,
}; static int chrdev_open( struct inode *inode, struct file *filp)
{
ret = -ENXIO;
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put; replace_fops (filp, fops);
if (filp->f_op->open) {
ret = filp->f_op-> open (inode, filp);
if (ret)
goto out_cdev_put;
} return 0;
out_cdev_put:
cdev_put(p);
return ret;
}

上面分析的核心意思是:device_register()函数除了注册在/sys下面外;

还通过devtmpfs_create_node()在/dev目录下创建了一个设备节点(inode),这个设备节点有一个默认的file_operations

cdev_add函数的实质

/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add( struct cdev *p, dev_t dev, unsigned count)
{
int error; p->dev = dev;
p->count = count; error = kobj_map (cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error; kobject_get(p->kobj.parent); return 0;
}

kobj_map() 函数:用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里 。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数, 根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段 。

此处只是简单的一个保存过程,并没有将cdev和inode关联起来。

有了这个关联之后,当我们使用mknod 命令,就会创建一个inode节点,并且通过 dev_tinodecdev_map里面的cdev联系起来了。

dev_t是联系inode/cdev/device三者的纽带

在创建device设备时,如果定义了dev_t,那么会创建一个inode节点,并且绑定一个默认带有一个file_operations的cdev。

如果针对该dev_t,定义了我们自己的file_operations,再调用cdev_add(),那么就会用自己定义的file_operations替换掉默认的file_operations

这样,devicecdev以及自己定义的file_operations就关联起来了。

struct   class  *my_class;
struct cdev my_cdev[N_MINORS];
dev_t dev_num; static int __init my_init( void )
{
int i;
dev_t curr_dev; /* Request the kernel for N_MINOR devices */
alloc_chrdev_region(&dev_num, 0, N_MINORS, "my_driver" ); /* Create a class : appears at /sys/class */
my_class = class_create(THIS_MODULE, "my_driver_class" ); /* Initialize and create each of the device(cdev) */
for (i = 0; i < N_MINORS; i++) { /* Associate the cdev with a set of file_operations */
cdev_init(&my_cdev[i], &fops); /* Build up the current device number. To be used further */
curr_dev = MKDEV(MAJOR(dev_num), MINOR(dev_num) + i); /* Create a device node for this device. Look, the class is
* being used here. The same class is associated with N_MINOR
* devices. Once the function returns, device nodes will be
* created as /dev/my_dev0, /dev/my_dev1,... You can also view
* the devices under /sys/class/my_driver_class.
*/
device_create (my_class, NULL, curr_dev , NULL, "my_dev%d" , i); /* Now make the device live for the users to access */
cdev_add (&my_cdev[i], curr_dev , 1);
} return 0;
}

misdevice

在misdevice的实现中,就将devicecdev关联了起来,并定义自己的file_operations

因此,misdevice同时具有device的标准设备模型,也定义了自己的file_operations

按照上面的思路,推测出misdevice的注册过程应该是如下流程:

  • 调用核心device注册函数device_add()
  • 调用核心cdev注册函数cdev_add()

但是分析misc_register()函数,发现只调用了device_add()函数,并没有调用cdev_add(),那么自己定义的file_operations是如何和cdev关联起来的呢?

其实misc.c中用了另外一套思路,但是原理是一样的。

创建cdev_map

在misc_init()中, 通过register_chrdev()函数将主设备号为0,次设备号为0-255的所有cdev都通过cdev_add()进行了注册;

也就是说将256个cdev放入到了cdev_map中,然后绑定的file_operations为默认的misc_open操作函数;

static   int  __init misc_init( void )
{
int err; if ( register_chrdev (MISC_MAJOR, "misc" ,&misc_fops))
goto fail_printk;
misc_class->devnode = misc_devnode;
return 0;
// ...
} int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
cd = __register_chrdev_region(major, baseminor, count, name);
cdev = cdev_alloc();
cdev->ops = fops;
err = cdev_add (cdev, MKDEV(cd->major, baseminor), count);
// ...
}

在misc_register中进行链接

当调用misc_register()时,内核通过维护一个 misc_list 链表,misc设备在misc_register注册的时候链接到这个链表。

/**
* misc_register - register a miscellaneous device
* @misc: device structure
*
* Register a miscellaneous device with the kernel. If the minor
* number is set to %MISC_DYNAMIC_MINOR a minor number is assigned
* and placed in the minor field of the structure. For other cases
* the minor number requested is used.
*
* The structure passed is linked into the kernel and may not be
* destroyed until it has been unregistered. By default, an open()
* syscall to the device sets file->private_data to point to the
* structure. Drivers don't need open in fops for this.
*
* A zero is returned on success and a negative errno code for
* failure.
*/ int misc_register( struct miscdevice * misc)
{
dev_t dev;
int err = 0; INIT_LIST_HEAD(&misc->list); dev = MKDEV(MISC_MAJOR, misc->minor); misc->this_device =
device_create_with_groups(misc_class, misc->parent, dev,
misc, misc->groups, "%s" , misc->name); /*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
list_add (&misc->list, &misc_list); }

查找并替换新的file_operations

通过步骤2,可以操作对应dev_t的某个cdev的inode了,但此时的open为绑定的file_operations提供的默认的misc_open;

在此函数中,会根据dev_t在内核的misc_list中进行查找,然后用自己定义的file_operations替换到misc提供的那个默认的file_operations。

static   int  misc_open( struct  inode * inode,  struct  file * file)
{
int minor = iminor(inode);
struct miscdevice *c;
int err = -ENODEV;
const struct file_operations *new_fops = NULL; mutex_lock(&misc_mtx); list_for_each_entry(c, &misc_list, list) {
if (c->minor == minor) {
new_fops = fops_get(c->fops);
break ;
}
} if (!new_fops) {
mutex_unlock(&misc_mtx);
request_module( "char-major-%d-%d" , MISC_MAJOR, minor);
mutex_lock(&misc_mtx); list_for_each_entry(c, &misc_list, list) {
if (c->minor == minor) {
new_fops = fops_get(c->fops);
break ;
}
}
if (!new_fops)
goto fail;
} /*
* Place the miscdevice in the file's
* private_data so it can be used by the
* file operations, including f_op->open below
*/
file->private_data = c; err = 0;
replace_fops(file, new_fops);
if (file->f_op->open)
err = file->f_op->open(inode,file);
fail:
mutex_unlock(&misc_mtx);
return err;
}

因此, miscdevice就是通过上述的步骤,实现了device cdev和自定义 file_operations的关联。

一个简单的小测试

通过命令mknod建立一个misc设备,然后去打开该设备,看下返回值,如下

 root@imx6qsabresd:/dev# mknod test c 10 100
root@imx6qsabresd:/dev# cat /dev/test
cat: can't open '/dev/test': No such device

通过上面的分析,我们知道cat时,调用的open函数会最后调到misc_open(),该函数中返回的错误就是 -ENODEV,和看到的现象一致。

通过mknod建立一个设备,主次设备号随便,看下返回值,如下

 root@imx6qsabresd:/dev# mknod aaa c 1 0
root@imx6qsabresd:/dev# cat /dev/aaa
cat: can't open '/dev/aaa': No such device or address

通过上面的分析,我们知道,调用的open函数会最后调用到chrdev_open,该函数中返回的错误就是-ENXIO,和看到的错误提示一致。

Linux设备驱动模型下的cdev

通过上面的分析,我们知道当device_add()注册device时,会调用devtmpfs_create_node()

但是这个调用是有个约束条件的, 这个约束条件是device中必须定义了devt这个设备号。

所以,对于很多的平台设备platform_devices(也就是那些在dts文件中定义的devices),在进行platform_device_add()时,并不会在/dev下面出现inode节点。

但是i2c控制器,作为一个平台设备,确在/dev下面出现了设备节点,比如/dev/i2c-0 /dev/i2c-1

这是什么原因的?分析内核代码,我们发现i2c-dev.c这个文件,其实这个文件就是用来创建/dev/i2c-0这些设备的,它是一个我们熟悉的cdev设备驱动。

i2c_dev_init()函数中使用了一种 内核通知机制,通过回调,在i2cdev_attach_adapter()中创建了一个带devt的device,那么自然会出现/dev/i2c-0节点了。内核通知机制的具体原理没有研究。

Linux设备驱动模型下的eeprom驱动

我们传统的用法是习惯在/dev下注册eeprom设备,也就是所谓的cdev设备,然后操作。

但是内核中用的是基于/sys系统的devices驱动模型,使用这个模型时,我们在dts文件中,在相应的i2c控制器下配置好好,就能在通过/sys文件系统进行访问了。

比如如下的dts配置,我再i2c4下挂了一个eeprom芯片。内核会在初始化时将device于at24的驱动进行绑定(具体过程是dts以及platform设备模型的相关知识)

&i2c4{
eerpom: at24c04@54{
compatible = "24c04";
reg = <0x54>;
status = "okay";
};
};

设备注册成功后,我们能看到如下信息

root@imx6qsabresd:/sys/class/i2c-dev/i2c-3/device/3-0054# ls
driver modalias of_node subsystem eeprom name power uevent

该目录下有很多属性文件,如果我们想读写eeprom,那么我们可以通过操作eeprom这个文件实现读写,这些都是设备驱动模型和sys文件系统的相关知识了。

那么,如果我想基于这个架构,增加一个/dev/eeprom设备,该怎么实现呢?

首先,需要清楚,在内核调用at24_probe()函数之前,eprom作为devices,已经被注册到了系统中(这个过程是内核在解析dts配置文件时自动完成的,不是使用者自己注册的)。

由于在内核注册devices时,并没有分配dev_t这个设备号,所以不会在/dev下面创建设备节点。

在没有设备号的情况下如何在/dev中创建设备

那么现在想创建一个cdev设备,但是这个设备号内核注册时并没有分配给这个device,怎么办?

有两种方案实现:

方案一:注册misc设备

at24_probe()函数中,直接注册一个misc设备,实现file_operations,从而能够建/dev/eeprom节点。

使用该方法时,需要理解一点,其实你是将eeprom这个芯片注册了两次设备(devices),一次是作为i2c的下挂的设备,由平台自动注册的;还有一次是自己注册的misc设备。这两个设备都在/sys系统下存在。这两个设备对sys来说是完全独立的,只是因为我们自己将他们的驱动写在了一起实现。

root@imx6qsabresd:/sys/class/i2c-dev/i2c-3/device/3-0054# ls
driver modalias of_node subsystem eeprom name power uevent
root@imx6qsabresd:/sys/devices/virtual/misc/eeprom# ls
dev power subsystem uevent
root@imx6qsabresd:/# ls -al /dev/eeprom
crw------- 1 root root 10, 100 Jan 1 1970 /dev/eeprom

方案二: 手动分配设备号

at24_probe()函数中,给入参i2c_client里面的device里的dev_t分配一个设备号,然后通过cdev_init()cdev_add()函数将该设备号与file_operations进行关联,再通过mknod创建节点。

使用此方法,实现了在/sys系统中只注册了一个device,但既能够通过sys系统访问,也能够通过/dev/eeprom设备节点的形式访问了。

下面是自己尝试在at24.c里面增加的代码

static int eeprom_open( struct inode *inode, struct file *filp) {
unsigned int major, minor;
major = imajor(inode);
minor = iminor(inode);
printk( "%s, major = %d, minor = %d!\n" , func, major, minor);
return 0;
}
static ssize_t eeprom_read( struct file *filp, char *buf,
size_t len, loff_t *offset) {
printk( "%s, len = %d\n" , func, len);
return 0;
}
static ssize_t eeprom_write( struct file *filp, const char *buf,
size_t len, loff_t *offset) {
printk( "%s, len = %d\n" , func, len);
return 0;
}
static int eeprom_close( struct inode *inode, struct file *filp) {
printk( "%s\n" , func);
return 0;
}
static struct file_operations eeprom_fops = {
.open = eeprom_open,
.read = eeprom_read,
.write = eeprom_write,
.release = eeprom_close,
};
static int at24_probe( struct i2c_client *client, const struct i2c_device_id *id)
{
// ...
/*
* add the char device to system
*/
dev = &client->dev;
dev->devt = MKDEV(100, 0);
err = register_chrdev_region(dev->devt, 1, "eeprom" );
if (err < 0) {
dev_err(&client->dev, "Can't static register chrdev region!\n" );
return err;
}
cdev_init(&eeprom_cdev, &eeprom_fops);
err = cdev_add(&eeprom_cdev, dev->devt, 1);
if (err < 0) {
dev_err(&client->dev, "Can't add char device!\n" );
return err;
}
dev_err(&client->dev, "add char eeprom device!\n" );
return 0;
}

测试如下。

root@imx6qsabresd:/sys/class/i2c-dev/i2c-3/device/3-0054# ls
driver modalias of_node subsystem eeprom name power uevent
root@imx6qsabresd:/# ls -al /dev/eeprom
crw------- 1 root root 100, 0 Jan 1 1970 /dev/eeprom

Linux内核驱动:cdev、misc以及device三者之间的联系和区别的更多相关文章

  1. linux 内核驱动--Platform Device和Platform_driver注册过程

    linux 内核驱动--Platform Device和Platform_driver注册过程 从 Linux 2.6 起引入了一套新的驱动管理和注册机制 :Platform_device 和 Pla ...

  2. linux内核驱动模型

    linux内核驱动模型,以2.6.32内核为例.(一边写一边看的,有点乱.) 1.以内核对象为基础.用kobject表示,相当于其它对象的基类,是构建linux驱动模型的关键.具有相同类型的内核对象构 ...

  3. 【引用】Linux 内核驱动--多点触摸接口

    本文转载自James<Linux 内核驱动--多点触摸接口>   译自:linux-2.6.31.14\Documentation\input\multi-touch-protocol.t ...

  4. Linux内核驱动开发之KGDB原理介绍及kgdboe方式配置

    接博文<Linux内核驱动开发之KGDB单步调试内核(kgdboc方式)>.上篇博文中,仅简单介绍使用串口的Kgbd的流程(kgdboc方式),本文将重点介绍KGDB调试Linux内核的原 ...

  5. OMAP4之DSP核(Tesla)软件开发学习(二)Linux内核驱动支持OMAP4 DSP核

    注:必须是Linux/arm 3.0以上内核才支持RPMSG,在此使用的是.config - Linux/arm 3.0.31 Kernel Configuration.(soure code fro ...

  6. Linux内核驱动学习(八)GPIO驱动模拟输出PWM

    文章目录 前言 原理图 IO模拟输出PWM 设备树 驱动端 调试信息 实验结果 附录 前言 上一篇的学习中介绍了如何在用户空间直接操作GPIO,并写了一个脚本可以产生PWM.本篇的学习会将写一个驱动操 ...

  7. Linux内核驱动学习(六)GPIO之概览

    文章目录 前言 功能 如何使用 设备树 API 总结 前言 GPIO(General Purpose Input/Output)通用输入/输出接口,是十分灵活软件可编程的接口,功能强大,十分常用,SO ...

  8. Unix/Linux环境C编程新手教程(12) openSUSECCPP以及Linux内核驱动开发环境搭建

    1. openSUSE是一款优秀的linux. watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaXRjYXN0Y3Bw/font/5a6L5L2T/font ...

  9. Unix/Linux环境C编程入门教程(12) openSUSECCPP以及Linux内核驱动开发环境搭建

    1. openSUSE是一款优秀的linux. 2.选择默认虚拟机 3.选择稍后安装操作系统 4.选择linux  opensuse 5. 选择默认虚拟机名称 6.设置处理器为双核. 7.内存设置为2 ...

  10. 嵌入式C语言自我修养 02:Linux 内核驱动中的指定初始化

    2.1 什么是指定初始化 在标准 C 中,当我们定义并初始化一个数组时,常用方法如下: ] = {,,,,,,,,}; 按照这种固定的顺序,我们可以依次给 a[0] 和 a[8] 赋值.因为没有对 a ...

随机推荐

  1. Git——分支管理(2)

    Git--分支管理(2) 提示:图床在国外且动图比较多的情况下,需要时间加载. 目录: 目录 Git--分支管理(2) 提示:图床在国外且动图比较多的情况下,需要时间加载. 目录: Git基础 Git ...

  2. 01.Markdown 语法

    标题 # 一级标题 ## 二级标题 ### 三级标题 ...(最多六级标题) 字体 **hello**:粗体 *hello*:斜体 三个*:粗体+斜体 ~~hello~~:删除线 引用 > 引用 ...

  3. docker-compose 安装 mysql:5.7.31

    目录 一.新建一个启动服务的目录 二.新建文件docker-compose.yml 三.新建角本文件 init-mysql.sh 四.实使化目录和配置文件 启动服务 登陆mysql 其它操作 参考文档 ...

  4. leaflet 加载geojson叠加显示

    geojson需要先制作shp,然后导入下面网站生成geojson. https://mapshaper.org/ geojson,最好放后台,前台通过异步请求去加载json,然后显示. getGeo ...

  5. IPv6 — 协议头

    目录 文章目录 目录 前文列表 IPv6 协议头格式 扩展报头 前文列表 <IPv6 - 网际协议第 6 版> <IPv6 - 地址格式与寻址模式> IPv6 协议头格式 IP ...

  6. C# 如何获取本机IP

    百度搜索的方案 如果你去百度C#如何获取本机IP,那么大概率的你会得到以下的几段代码,第一种就是这样: string name = Dns.GetHostName(); IPAddress[] ipa ...

  7. 防患未然 | AIRIOT城市管廊智能运维解决方案

      城市管廊构建复杂,管道内部传感器和附属设备居多,且近年来事故频发,地下空间属性人员进出管理不便,紧急情况应急调度措施有限.传统人工管理模式,运营成本高,且管理水平和质量也无法得到有利保障.因此在管 ...

  8. opencv-python 实现鱼眼矫正 棋盘矫正法

    .htmledit_views address, .htmledit_views cite, .htmledit_views dfn, .htmledit_views em, .htmledit_vi ...

  9. Qt开发技术:Q3D图表开发笔记(四):Q3DSurface三维曲面图颜色样式详解、Demo以及代码详解

    前言   qt提供了q3d进行三维开发,虽然这个框架没有得到大量运用也不是那么成功,性能上也有很大的欠缺,但是普通的点到为止的应用展示还是可以的.  其中就包括华丽绚烂的三维图表,数据量不大的时候是可 ...

  10. MySQL 导出一条数据的插入语句

    1.MySQL 导出一条数据的插入语句的方法 在MySQL中,如果我们想要导出一条数据的插入语句,我们可以使用SELECT ... INTO OUTFILE语句(但这通常用于将整个表或查询结果导出到一 ...