linux驱动程序开发指南-字符驱动介绍
概述:
在linux系统中设备驱动程序通常是作为应用层和设备层的中间层软件,驱动程序的主要功能是实现应用层访问硬件设备的具体操作接口,通过调用驱动程序,上层应用程序可以采用统一的接口访问各种硬件设备。由于驱动程序一般位于操作系统内核层,因此驱动也可以作为用户空间与内核空间交互的接口层,用户空间、驱动、内核空间框架如下图所示:
图1
根据设备数据访问方式一般将linux驱动分为字符设备驱动和块设备驱动两大类,字符设备一般按照字节流的方式访问设备,如串口,块设备一般按照固定长度数据块的方式访问设备如磁盘。如果根据驱动所在的子系统进行分类的话,驱动可以划分为终端(tty)驱动,网络设备驱动,USB驱动等。
由于硬件设备种类繁多,且各硬件访问方式各不相同,导致针对不同的硬件需要编写不同的驱动程序,在实际项目中需要结合具体的硬件资料进行相关调试。为了便于理解,本文不包含具体的硬件设备驱动编写方法,尽量不涉及其他子系统的内容,只包含通用字符设备驱动框架介绍。
1 驱动模块基本结构
linux系统驱动程序可以直接编译到内核中,也可以编译成模块的方式,驱动模块可以进行单独安装。为了便于调试,项目中我们一般用模块的形式进行驱动开发,本章介绍驱动模块代码的基本结构。
1.1 驱动模块安装接口
通过宏module_init(function)申明函数function为模块安装时开始调用的接口函数,相当于应用程序main函数。在终端通过insmod命令安装驱动模块时会调用该函数,该函数实现的主要功能实现模块相关初始化工作,比如资源申请。
1.2 驱动模块卸载接口
通过宏module_exit(function)申明函数function为模块卸载时调用的接口函数。当卸载驱动模块是会以该函数为调用入口,在终端通过rmmod命令卸载驱动模块时会调用该函数,该函数实现的主要功能是实现模块相关的清理工作,比如资源释放。
1.3 驱动模块头文件
要编写一个linux驱动模块必须要包含以下头文件:
#include <linux/init.h>
该头文件包含内核初始化相关接口的申明和定义。
#include <linux/module.h>
该头文件包含内核模块相关接口、数据结构的申明和定义,宏module_init、module_exit也是通过这个头文件定义的。
1.4 模块常用宏定义
一般我们的模块程序会使用宏MODULE_LICENSE是用来告知内核, 该模块带有一个自由的许可证; 如果没有这样的说明, 在模块加载时内核打印如下警告信息:
“module verification failed: signature and/or required key missing - tainting kernel”
模块中增加以下语句,可以避免警告:
MODULE_LICENSE("Dual BSD/GPL");
另外以下几个宏可以根据需要使用
MODULE_AUTHOR:申明模块编写人
MODULE_DESCRIPION:模块描述
MODULE_VERSION:模块版本
1.5 驱动模块程序示例
编写一个简单的驱动模块程序test_driver.c如下所示:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int test_init(void)
{
printk("test module init\n");
return 0;
}
static void test_exit(void)
{
printk("test module exit\n");
}
module_init(test_init);
module_exit(test_exit);
以上例子中printk用于在内核空间打印信息,功能和用户空间的printf接口类似。
2 驱动模块编译
2.1 驱动makefile模板格式
驱动模块编译和应用程序编译不同,由于驱动模块一般要调用内核其他接口,所以驱动模块要使用内核编译系统来进行编译。内核驱动模块makefile格式比较固定,模板如下:
ifneq ($(KERNELRELEASE),)
obj-m := module_name.o
module_name -objs := file1.o file2.o …
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -rf *.o .*.cmd *.ko *.mod.c modules.order Module.symvers
运行make命令,makefile会执行两次,第一次执行else分支,设置变量KERNELDIR和PWD,然后调用KERNELDIR所指定目录的内核Makefile。内核Makefile再次执行第一个分支编译当前目录模块。这里makefile中的”clean”目标和应用程序makefile意义相同,都是清除生成文件。驱动模块makefile模板虽然不好理解但实际使用并不需要修改太多,一般只要修改以下两行:
obj-m := module_name.o
该行指定要生成的模块名称
module_name -objs := file1.o file2.o …
该行指定生成模块所依赖的目标文件,file1.o file2.o分别是模块.c文件对应的目标文件。
2.2 驱动模块Makefile实例
编译上节驱动模块只要对Makefile模板文件进行简单修改:将块名改成修改成test,将目标文件指定为test_driver.o。
ifneq ($(KERNELRELEASE),)
obj-m := test.o
test-objs := test_driver.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -rf *.o .*.cmd *.ko *.mod.c modules.order Module.symvers
修改完后在终端输入make命令进行编译,当前目录下生成test.ko文件。通过insmod test.ko安装驱动,dmesg 查看内核打印信息如下:
“test module init”
rmmod test.ko卸载驱动,dmesg 查看内核打印信息如下:
“test module exit”
3 字符驱动与应用层交互
前面两章对内核驱动模块基本结构和编译方式进行了介绍,本章在此基础上通过编写一个完整的字符设备驱动,介绍驱动和应用层交互的相关接口。之所以选字符设备作为例子来讲是由于字符设备驱动比块设备驱动、网络设备等其他驱动要简单,另外字符驱动也能用于很多简单硬件设备驱动,比如:项目中长见的spi、I2C、pcie等设备驱动都是字符设备驱动。
3.1 应用程序访问设备接口
在linux系统中用户层操作一切都是以文件的形式,对于操作设备是通过读写设备文件的方式来实现。在用户层读写设备和读写普通文件没有任何差别,也是通过调用open、lseek、read、write、close等系统API访问设备。
设备文件通常位于/dev目录下。终端输入ls /dev –al可以查看各设备文件属性,下图截取了一部分串口终端设备文件属性:
图2
红框中的两列数字分别表示主设备号和次设备号,主设备号表示某一类设备,次设备号用于区分该类设备中的具体设备。设备号和设备文件如何创建的?如何与驱动程序关联的?下面我们逐步分析。
3.2 设备号分配、释放
3.2.1 设备号申请
主设备号一般在驱动程序中由内核相应接口函数动态分配,也可以手动指定当前系统未使用的主设备号,由于手动分配要指定空闲设备号,为了避免冲突,我一般不用这种方式。下面介绍动态分配的方式。
设备号动态分配接口函数:
int alloc_chrdev_region(dev_t *dev,
unsigned int firstminor,
unsigned int count,
char *name);
接口说明:
dev :申请的第一个设备号
firstminor:起始次设备号
count:连续请求设备号数
name:设备名称
返回值: 生成成功返回0,失败返回错误码
调用该接口成功申请设备号之后,可以通过以下宏定义进行设备号相关操作:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MAJOR(dev):获取主设备号
MINOR(dev):获取次设备号
MKDEV(ma,mi):将主设备号和次设备号组合成一个设备号
从宏定义知道32位的设备号由高12位的主设备号和低20位的次设备号组成。
3.2.2 设备号释放
设备号释放接口函数:
void unregister_chrdev_region(dev_t first, unsigned int count);
参数说明:
first:释放的第一个设备号
count:释放的设备号数
该接口将申请的设备号释放掉,一般用于驱动卸载时。
3.2.3 设备号申请示例
将第一章的例子增加设备号申请接口:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/aio.h> //设备号申请接口相关头文件
MODULE_LICENSE("Dual BSD/GPL");
int test_dev_major = 0;
static int test_init(void)
{
int result = 0;
dev_t dev = 0;
printk("test module init\n");
result = alloc_chrdev_region(&dev, 0, 1, "test_dev");
test_dev_major = MAJOR(dev);
if (result < 0)
{
printk("test_dev: can't get major %d\n", test_dev_major);
return result;
}
printk("test_dev: alloc devid %d major %d minor %d\n", dev, test_dev_major, MINOR(dev));
return 0;
}
static void test_exit(void)
{
printk("test module exit\n");
unregister_chrdev_region(MKDEV(test_dev_major, 0), 1);
}
module_init(test_init);
module_exit(test_exit);
编译后安装,dmesg 查看内核增加了以下打印信息:
说明申请了主设备号243。通过cat /proc/devices命令可以查看当前申请了哪些主设备号:
可以使用rmmod命令卸载驱动,再次查看的话设备号已经释放掉:
3.3 设备文件创建
之前提到应用层访问设备是通过读写/dev目录下的设备文件的方式,本节对设备文件的创建方式进行介绍。
这里为了便于理解不涉及通过内核接口的方式创建设备文件,这里只介绍通过终端输入命令来创建字符设备文件这种简单的方式:
mknod [OPTION]... NAME TYPE [MAJOR MINOR]
为了便于理解,[OPTION]可选参数这里不讨论。其他几个参数内容为:
NAME:创建的设备文件名,如/dev/test_dev
TYPE:创建的设备文件类型,字符设备为‘c’
MAJOR:主设备号
MINOR:次设备号
已上一节的驱动程序为例,创建一个字符设备文件如下所示:
在根用户下输入命令:mknod /dev/test_dev c 243 0
输入命令ls /dev –al | grep test 查看/dev目录确实生成了一个“test_dev”文件。
3.4 设备文件操作接口说明
现在设备文件已经创建了,创建的设备文件和我们的驱动怎么关联起来?当应用层读写设备时如何和设备操作关联起来?本节对相关问题进行讨论。
3.4.1 设备数据结构定义
为了便于理解,创建一个虚拟设备,设备数据结构定义如下:
#define BUF_SIZE 128
struct test_dev {
char buf[BUF_SIZE];
struct cdev cdev;
};
buf数组模拟设备数据空间。
cdev 字符设备结构体,表面虚拟设备是一个字符设备。
3.4.2 字符设备初始化
void cdev_init(struct cdev *cdev, struct file_operations *fops);
参数说明:
cdev:字符设备指针,表示一个字符设备。结构在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;
};
fops:文件操作接口,包含文件操作的多个接口,结构在文件include/linux/fs.h中定义如下:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
该结构比较复杂,为了便于理解,下面只介绍几个经常要用到的成员。
struct module *owner
是一个指向拥有这个结构的模块的指针。这个成员用来在它的操作还在被使用时阻止模块被卸载一般初始化为THIS_MODULE, 该宏在include/linux/module.h文件中定义。
3.4.3 文件打开
int (*open)(struct inode *inode, struct file *filp);
参数说明:
inode :该参数包含 struct cdev 成员地址,通过cdev成员地址可以进一步获取设备数据结构地址。
filp:文件结构指针,表示当前打开的一个文件,该结构比较复杂,在include/linux/fs.h文件中定义,这里只说明一个下面要使用到的成员void *private_data,该指针指向驱动定义的私有数据,这样就可以通过文件指针和驱动定义的数据联系起来。
3.4.4 文件读取
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
参数说明:
file:同open接口
__user:用户空间数据缓存地址
size_t:读取文件数据长度
loff_t:文件偏移位置
返回值:返回负值表示错误,非负值表示正确读出的数据长度
3.4.5 文件写入
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
参数说明:
file:同open接口
__user:用户空间数据缓存地址
size_t:待写入文件的数据长度
loff_t:文件偏移位置
返回值:返回负值表示错误,非负值表示正确写入的数据长度
3.5 设备文件操作接口实现
上一节对字符设备操作接口进行了说明,本节通过在之前的例子中实现相关接口,实现一个完整的字符驱动。
3.5.1 open接口实现
在介绍该接口实现前,先说明一下宏定义container_of,在内核头文件/include/linux/kernel.h中定义:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
该宏定义的功能是根据结构体成员地址获取结构体地址。
接口实现如下:
int test_open (struct inode *inode, struct file *filp)
{
struct test_dev *pdev;
/*这里根据cdev地址获取test_dev 地址*/
pdev = container_of(inode->i_cdev, struct test_dev, cdev);
/* 文件私有数据指针指向字符设备地址*/
filp->private_data = pdev;
return 0; /* success */
}
3.5.2 read接口实现
用户空间程序数据拷贝可以直接使用C库函数memcpy,但是驱动程序将内核空间数据拷贝到用户空间不能直接调用memcpy接口,需要用到以下接口:
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
参数说明:
to:用户空间目的地址
from:内核空间源地址
count:拷贝数据长度
返回值:0 成功, 非0 未拷贝的数据
read接口实现如下:
ssize_t test_read (struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
//通过文件指针私有数据获取字符设备地址
struct test_dev *dev = filp->private_data;
ssize_t retval = 0;
loff_t pos = *f_pos;
if((count > BUF_SIZE) || (pos >= BUF_SIZE))
{
return -EFAULT;
}
/*拷贝内核数据到用户空间*/
if (copy_to_user (buf, dev->buf + pos, count)) {
retval = -EFAULT;
goto out;
}
return count;
out:
return retval;
}
3.5.3 write接口实现
类似于read接口,用户空间程序数据拷贝可以直接使用C库函数memcpy,但是驱动程序将用户空间数据拷贝到内核空间不能直接调用memcpy接口,需要用到以下接口:
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
参数说明:
to:内核空间目的地址
from:用户空间源地址
count:拷贝数据长度
返回值:0 成功, 非0 未拷贝的数据
write接口实现:
ssize_t test_write (struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
{
struct test_dev *dev = filp->private_data;
ssize_t retval = 0;
loff_t pos = *f_pos;
if((count > BUF_SIZE) || (pos > BUF_SIZE))
{
return -EFAULT;
}
if (copy_from_user (dev->buf + pos, buf, count))
{
retval = -EFAULT;
goto out;
}
return count;
out:
return retval;
}
3.5.4 文件操作数据结构初始化
基本的文件操作接口实现后就可以对文件操作结构体变量进行初始化了:
struct file_operations test_fops = {
.owner = THIS_MODULE,
.read = test_read,
.write = test_write,
.open = lpc_test_open,
};
3.6 字符设备添加
设备初始化后需要将字符设备添加到内核:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
参数说明:
dev:已初始化的字符设备
num:设备响应的第一个设备号
count:关联到设备的设备号的数目, 一般是 1
3.7 字符设备删除
当驱动卸载时需从内核中删除字符设备:
void cdev_del(struct cdev *dev)
参数说明:
dev:已添加的字符设备
4 字符设备驱动完整实现
通过前几章的了解,本章通过将上章实现的接口添加都前面的示例代码中实现一个完整的简单驱动程序,添加相关接口的驱动代码如下:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h> /* container_of */
#include <linux/fs.h> /* file opt */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/aio.h> /* alloc_chrdev_region */
#include <linux/uaccess.h> /* copy_to_user */
#include <linux/cdev.h> /* struct cdev */
MODULE_LICENSE("Dual BSD/GPL");
int test_dev_major = 0;
#define BUF_SIZE 128
struct test_dev {
char buf[BUF_SIZE];
struct cdev cdev;
}test_dev;
int test_open (struct inode *inode, struct file *filp)
{
struct test_dev *pdev;
/*这里根据cdev地址获取test_dev 地址*/
pdev = container_of(inode->i_cdev, struct test_dev, cdev);
/* 文件私有数据指针指向字符设备地址*/
filp->private_data = pdev;
printk("test_dev open success\n");
return 0;
}
ssize_t test_read (struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
//通过文件指针私有数据获取字符设备地址
struct test_dev *dev = filp->private_data;
ssize_t retval = 0;
loff_t pos = *f_pos;
if((count > BUF_SIZE) || (pos >= BUF_SIZE))
{
return -EFAULT;
}
/*拷贝内核数据到用户空间*/
if(copy_to_user(buf, dev->buf + pos, count))
{
retval = -EFAULT;
goto out;
}
printk("test_dev read success\n");
return count;
out:
return retval;
}
ssize_t test_write (struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
{
struct test_dev *dev = filp->private_data;
ssize_t retval = 0;
loff_t pos = *f_pos;
if((count > BUF_SIZE) || (pos > BUF_SIZE))
{
return -EFAULT;
}
if(copy_from_user(dev->buf + pos, buf, count))
{
retval = -EFAULT;
goto out;
}
printk("test_dev write success\n");
return count;
out:
return retval;
}
struct file_operations test_fops = {
.owner = THIS_MODULE,
.read = test_read,
.write = test_write,
.open = test_open,
};
static int test_init(void)
{
int result = 0;
dev_t dev = 0;
printk("test module init\n");
result = alloc_chrdev_region(&dev, 0, 1, "test_dev");
test_dev_major = MAJOR(dev);
if (result < 0)
{
printk("test_dev: can't get major %d\n", test_dev_major);
return result;
}
printk("test_dev: alloc devid %d major %d minor %d\n", dev, test_dev_major, MINOR(dev));
cdev_init(&test_dev.cdev, &test_fops);
result = cdev_add(&test_dev.cdev, dev, 1);
if (result)
{
printk("Error %d adding test_dev\n", result);
}
return 0;
}
static void test_exit(void)
{
printk("test module exit\n");
cdev_del(&test_dev.cdev);
unregister_chrdev_region(MKDEV(test_dev_major, 0), 1);
}
module_init(test_init);
module_exit(test_exit);
5 字符驱动程序测试
本章通过一个简单的应用程序对上章编写的虚拟字符设备进行操作,测试程序如下:
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_NAME "/dev/test_dev"
int main(int argc, char **argv)
{
int fd = 0;
char *testStr = "test dev opt";
char buf[64] = {0};
if((fd = open(DEV_NAME, O_RDWR|O_NONBLOCK)) < 0)
{
return -1;
}
write(fd,testStr,strlen(testStr));
read(fd,buf,strlen(testStr));
printf("read:%s\n",buf);
close(fd);
}
终端输入:gcc -o test.exe main.c编译乘车test.exe执行程序。
输入sudo ./test.exe运行结果如下:
read:test dev opt
dmesg命令查看内核驱动打印信息如下:
[2522609.557786] test_dev open success
[2522609.557792] test_dev write success
[2522609.557795] test_dev read success
(注:设备文件默认只有根权限才能读写)
总结:本文通过一个简单的字符驱动介绍了字符驱动框架、用户空间和内核空间的数据交互接口。为了便于理解,除了字符驱动必须接口外文中的例子尽量没有涉及内核的其他功能接口,如果想进一步了解驱动编写建议在此基础上对一个真实设备驱动进行学习。
linux驱动程序开发指南-字符驱动介绍相关推荐
- Linux TWI开发指南
文章目录 Linux TWI开发指南 1 前言 1.1 文档简介 1.2 目标读者 1.3 适用范围 2 模块介绍 2.1 模块功能介绍 2.2 相关术语介绍 2.2.1 硬件术语 2.2.2 软件术 ...
- VxWorks驱动程序开发指南--驱动程序的组织结构
8D Spaces Reliability & Stability & Efficiency 目录视图 摘要视图 订阅 VxWorks驱动程序开发指南(四)--驱动程序的组织结构 20 ...
- 嵌入式Linux驱动程序开发
嵌入式Linux驱动程序开发 1.设备驱动程序的概念... 2 2.处理器与设备间数据交换方式... 2 21.查询方式... 2 2.2.中断方式... 3 2.3.直接访问内存(DMA)方式... ...
- Linux SID 开发指南
Linux SID 开发指南 1 前言 1.1 编写目的 介绍Linux 内核中基于Sunxi 硬件平台的SID 模块驱动的详细设计,为软件编码和维护提供基 础. 1.2 适用范围 内核版本Linux ...
- VxWorks设备驱动程序开发指南---驱动程序的分类
8D Spaces Reliability & Stability & Efficiency 目录视图 摘要视图 订阅 VxWorks设备驱动程序开发指南(三)---驱动程序的分类 2 ...
- Linux 汇编语言开发指南
Linux 汇编语言开发指南 肖文鹏 (xiaowp@263.net), 北京理工大学计算机系硕士研究生 本文作者 肖文鹏是北京理工大学计算机系的一名硕士研究生,主要从事操作系统和分布式计算环境的研究 ...
- poll接口《来自Linux驱动程序开发实例》
您所在的位置:读书频道 > 操作系统 > Linux > 1.2.7 poll接口 1.2.7 poll接口 2012-05-22 13:38 冯国进 机械工业出版社 我要评论(0) ...
- 异步通知《来自Linux驱动程序开发实例》
您所在的位置:读书频道 > 操作系统 > Linux > 1.2.8 异步通知 1.2.8 异步通知 2012-05-22 13:38 冯国进 机械工业出版社 我要评论(0) 字号: ...
- 全志Tina Linux MPP 开发指南
全志Tina Linux MPP 开发指南支持百问网T113 D1-H哪吒DongshanPI-D1s V853-Pro等开发板 1 简述 整理 MPP sample 使用说明文档的目的是:使 MPP ...
最新文章
- 微软MSN推出新一代Live服务 能离线编辑博客
- 分布式Ehcache Terracotta使用
- 一些极其简易的自动巡线车模
- HTML经典模板总结(地址)
- 专访 Christian Posta:Istio 1.7 将成为生产可用的最稳定版本
- [Qt教程] 第30篇 XML(四)使用流读写XML
- ES6.X,你必须知道的API和相关技巧
- 再等等!iPhone 11和iPhone SE还会继续降价
- javascript:控制一个元素高度始终等于浏览器高度
- 如何彻底关闭windows10自动更新
- ajax的param方法,jQuery ajax - param() 方法
- ppc64,ppc64le,ARM,AMD,X86,i386,x86_64(AMD64),AArch64的概念
- 测试opencl软件,我该如何测试OpenCL的可兼容性?
- 基于边界凹凸点和神经网络的粘连颗粒图像分割算法研究(既然有网友要源代码研究,在此公开绝大部分源代码)
- kernal tch 下载 天正_tch_kernal.arx
- 托管c++ (CLI) String^ 、 std::string 、 std::ostringstream的相互转化
- 从标注好的xml文件中截取坐标点(人脸框四个点坐标)人脸图像并保存在指定文件夹
- Codeforces Round #614 (Div. 2) E. Xenon's Attack on the Gangs(DP记忆化搜索+思维)
- docker 强制使用root进入容器
- 税务系统服务器维护导致逾期申报了,申报更正日期改变会导致逾期申报吗
热门文章
- PC#1 ping PC#2,请描述PC1和PC2之间的通信过程【杭州多测师】【杭州多测师_王sir】...
- 智能制造在汽车行业中如何应用
- android平板是否支持遥控,用手机平板遥控家中PC:Microsoft Remote Desktop
- Magento后台订单跟踪 - 修改订单状态
- Application Performance Management(APM)
- matlab函数之随机函数-randperm,sort,rand,randint
- 关于一个监听、发送QQ消息的插件的使用部署
- 使用USBWriter做U盘启动盘后容量变小的解决办法
- 基于cesium的二三维地图
- java 如何通过年份获取当前年有多少天,具体年份天数