嵌入式linux驱动开发教程
目录
- 第二章 内核模块
- 宏内核和微内核
- 内核模块程序的初始化和退出函数原型
- 内核模块的相关工具
- 内核模块基本框架(内核最原始的结构)
- 多个源文件编译生成一个内核模块
- 内核模块参数(参数类型要注意)
- 内核模块依赖
- 内核模块和普通应用程序之间的差异(简答题)
- 第三章 字符设备驱动
- 设备驱动的种类
- 不同设备驱动的特点
- 字符设备驱动基础
- 主设备号和次设备号
- 字符设备驱动框架(编程题)
- 虚拟串口设备操作
- 一个驱动支持多个设备
- 第四章 高级I/O操作
- ioctl 设备操作
- proc 文件操作
- 非阻塞型I/O
- 阻塞型I/O
- I/O多路复用
- 异步I/O
- 对四种I/O模型的总结(问答题)
- 异步通知
- mmap 设备文件操作
- 编程题
第二章 内核模块
宏内核和微内核
宏内核和微内核:linux是宏内核的代表,Windows是微内核的代表。区别是宏内核所有的内核功能被整体编译在一起,形成一个单独的内核镜像文件。
内核模块:linux引入了内核模块。内核模块是单独编译的一段内核代码,在需要的时候动态加载,不需要的时候动态卸载,动态增加内核功能。内核模块有助于减小内核镜像文件的体积,减少内核占用的内存空间。(当然内核模块不一定都是驱动程序,驱动程序也不一定都是内核模块)
内核模块程序的初始化和退出函数原型
// 一个模块程序几乎都要直接或间接使用这三个头文件
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>// 模块的初始化函数原型
int init_module(void)
{printk("模块初始化了");return 0;
}
// 模块的清除函数原型
void cleanup_module(void)
{printk("模块卸载了");
}
内核模块的相关工具
1、模块加载
insmod vser.ko
insmod命令加载指定目录下的.ko文件到内核,需要指定路径,默认是在当前目录找。
depmod
modprobe vser
modprobe命令智能加载,只需要使用加载的模块名称,不需要加后缀,但是需要使用前用depmod命令更新依赖关系,这样可以把有依赖关系的模块一起加载上,而insmod不可以。但是使用这种方式必须将模块放置到指定目录下。
2、模块信息
modinfo vser
查看模块的信息。
3、模块卸载
rmmod vser
如果内核配置为允许卸载模块,将指定的模块从内核中卸载。
内核模块基本框架(内核最原始的结构)
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>// 使用static关键字,因为static关键字修饰的函数的链接属性为内部,解决了内核模块加载到内核之后函数名可能会重复定义的问题。
// 使用 _init 修饰,当函数调用成功后,模块的加载程序会释放掉这部分空间,节约内存
static int _init vser_init(void)
{printk("模块初始化了");return 0;
}
// 使用 _exit 修饰,如果模块不允许卸载,那么这段代码完全不需要加载
static void _exit vser_exit(void)
{printk("模块卸载了");
}
module_init(vser_init);
module_exit(vser_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("NAPO <2661273650@qq.com>");
MODULE_DESCRIPTION("A Module");
MODULE_ALIAS("napo_module");
模块加载可以使用别名,例如:modprobe virtual-serial
多个源文件编译生成一个内核模块
// foo.c文件
extern void bar(void); // 需要使用extern关键字声明该函数实现在外部,否则编译不通过
// .....// bar.c文件
// 在bar文件中实现bar函数
void bar(void)
{printk("我执行了");
}
需要在Makefile中进行修改
obj-m := vser.o # 依然是生成的模块
vser-objs = foo.o bar.o # 表示vser模块是由foo.o和bar.o两个目标文件共同生成的
内核模块参数(参数类型要注意)
已知模块初始化函数在模块加载时被调用,但是函数是不接受参数的,所以需要想办法通过命令行向其传递参数,这就产生了模块参数。(加载模块时通过命令行向其传递参数值)
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>// 声明三个变量,其有默认值,我们的目标是模块加载时改变他们的值
static int baudrate = 9600;
static int port[4] = {0, 1, 2, 3}
static char* name = "vser";// 使用以下的宏代码表示可以在命令行中改变变量的内容
module_param(baudrate, int, S_IRUGO); # S_IRUGO 表示 USER,GROUP,OTHER可读.
module_param_array(port, int, NULL, S_IRUGO);
module_param(name, charp, S_IRUGO);
module_param(name, type, perm); # 单个变量使用的宏.
module_param_array(name, type, nump,perm); # 数组使用的宏.
// name:变量名 type:变量类型 nump:数组元素个数的指针,可选 perm:sysfs文件系统中的文件权限属性
参数的类型有:bool、invbool(反转值bool)、charp(字符指针)、short、int、long、ushort(u表示无符号)、uint、ulong。以及它们对应的数组类型。
指定模块参数的值:modprobe vser baudrate=115200 port=1,2,3,4 name="virtual-serial"
dmesg:获得硬件信息和内核启动时的信息。
内核模块依赖
需要注意的是一个宏:EXPORT_SYMBOL(param)
如同其名字就是导出一个符号,这个符号可以是函数名或者是变量名,将其导出之后可以在别的模块中使用。
/*vser.c*/
....
extern int param;
extern int param_fun(void);
..../*dep.c*/
static int param = 5;
static void param_fun(void)
{printk("我执行了");
}
EXPORT_SYMBOL(param);
EXPORT_SYMBOL(param_fun);
// 两个模块需要一起编译,要不然的话vser.c并不知道dep.c的存在,所以依然找不到符号。
// 卸载时需要先卸载vser模块再卸载dep模块,要不然可能会因为vser依赖dep模块,而导致dep模块不能被卸载。
内核模块和普通应用程序之间的差异(简答题)
(1)内核模块是操作系统内核的一部分,运行在内核空间,应用程序运行在用户空间(主要不同)。
(2)内核模块中的函数是被动的被调用的,比如初始化函数和清除函数分别在模块加载和模块卸载时被调用。
(3)内核模块处于C函数库之下,不能调用C库函数,应用程序可以调用。
(4)内核模块需要做一些清除性的工作,而应用程序有些不需要
(5)内核模块如果产生了非法访问一般导致系统崩溃,应用程序一般只影响自己
第三章 字符设备驱动
设备驱动的种类
(1)字符设备驱动
(2)块设备驱动
(3)网络设备驱动
不同设备驱动的特点
(1)字符设备驱动对数据的处理是按照字节流形式进行的,而块设备驱动是按照若干个块进行的,每个块有固定的大小。
(2)字符设备驱动可以支持随机访问,也可以不支持,块设备驱动都支持随机访问。
(3)字符设备驱动数据流量通常不大,一般没有页高速缓存,而块设备驱动一般有。
(4)网络设备驱动针对网络设备,主要是进行网络数据的收发。
字符设备驱动基础
设备文件通常位于/dev目录下,b 表示块设备,c 表示字符设备
主设备号和次设备号
通常内核用主设备号区别一类设备,次设备号用于区分同一类设备的不同个体或不同分区。路径名则是用户层用于区别设备的。
比如:sda、sda1、sda2、sda3 是块设备 sda表示的整个硬盘,其余的表示的是不同的分区 tty0、tty1是终端设备
使用mknod命令创建一个设备文件:mknod /dev/vser0 c 256 0
指定了设备结点的路径,类型为字符设备,和主次设备号
驱动程序通过主次设备号与设备文件进行关联,通过操作设备结点文件,使用编写的驱动程序中的操作映射到对硬件的操作。
字符设备驱动框架(编程题)
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1
#define VSER_DEV_NAME "vser"static struct cdev vsdev; // 表示了一个具体的字符设备static struct file_operations vser_ops = { // 表示了操作字符设备的一些方法.owner = THIS_MODULE
}static int _init vser_init(void)
{int ret;dev_t dev;// 使用宏MKDEV将主设备号和次设备号合并成一个设备号,高12位为主设备号,低20位为次设备号dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 第一个参数指定起始设备号,第二个参数指定申请个数,第三个参数标记主设备号的名称// 为了解决设备号冲突的问题,动态分配设备号函数:alloc_chrdev_region(dev, baseminor, count, name)ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);if(ret) goto reg_err;// 初始化字符设备,将设备与操作关联,第一个参数为设备地址,第二个参数为操作结构体地址cdev_init(&vsdev, &vser_ops); vsdev.owner = THIS_MODULE;// 将设备添加到散列表中去,第一个参数为设备地址,第二个参数为设备号,第三个参数为cdev对象可以管理多少设备// 一个对象可以管理多个设备ret = cdev_add(&vsdev, dev, VSER_DEV_CNT);if(ret) goto add_err;return 0;
reg_err:return ret;
add_err:// 如果添加对象失败的话注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}static void _exit vser_exit(void)
{dev_t dev;dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 模块卸载时删除掉设备对象cdev_del(&vsdev);// 注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}
module_init(vser_init);
module_exit(vser_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("NAPO <2661273650@qq.com>");
MODULE_DESCRIPTION("A Module");
MODULE_ALIAS("napo_module");
虚拟串口设备操作
内核中有一个结构体 struct kfifo
DEFINE_KFIFO(fifo, type, size); // 初始化一个虚拟串口缓冲区
kfifo_from_user(fifo, from, len, copied);
kfifo_to_user(fifo, to, len, copied);
一个驱动支持多个设备
#define VSER_DEV_CNT 2 // 定义2个设备
static DEFINE_KFIFO(vsfifo0, char, 32);
static DEFINE_KFIFO(vsfifo1, char, 32); // 申请2个空间struct vser_dev {struct kfifo *fifo;struct cdev cdev;
};static struct vser_dev vsdev[2]; // 创建2个对象并为其分配空间switch(MINOR(inode->i_rdev)){case 0:filp->private_data = &vsfifo0;break;case 1:filp->private_data = &vsfifo1;break; } // 根据次设备号的值确定保存哪个FIFO结构的地址到file结构中的private_data成员中。
第四章 高级I/O操作
ioctl 设备操作
ioctl函数原型:int ioctl(int fd, int cmd, ...);
第一个参数为文件描述符,第二个是要对设备操作的命令,第三个是可选的参数
一般在ioctl中使用switch语句对cmd命令进行条件分支,对不同的命令执行不同的操作。
proc 文件操作
proc文件系统是一种伪文件系统。它不存在于磁盘上,只存在于内存中,只有内核运行时才会动态生成里面的内容。
proc_mkdir("vser", NULL); // 建一个vser目录
proc_create_data("info", 0, vsdev.pdir, &proc_ops, &vsdev); // 建一个info文件
非阻塞型I/O
若应用程序以非阻塞型方式打开设备文件,当资源不可用时,驱动应立即返回并返回一个错误码。
int fd = open("/dev/vser0", O_RDWR | O_NONBLOCK); // O_NONBLOCK标志表示以非阻塞方式打开设备文件
阻塞型I/O
若应用程序以非阻塞型方式打开设备文件,当资源不可用时,则进程阻塞,让其他进程运行,有利于提高系统效率。
wait_event_interruptible()
等待阻塞
wake_up_interruptible()
唤醒阻塞
I/O多路复用
可以方便的实现一个进程同时对多个设备进行操作。
I/O多路复用有select、poll、epoll(Linux特有)三种方式
常见事件:
POLLIN:设备可以无阻塞读
POLLOUT:设备可以无阻塞写
异步I/O
异步I/O在提交完I/O操作请求后立即返回,程序不需等到I/O操作完成再去做别的事,具有非阻塞特性。
// 1
struct aiocb aiow, aior; // 定义2个分别用于写/读的异步I/O控制块
memset(&aiow, 0, sizeof(aiow));
memset(&aior, 0, sizeof(aior)); // 初始化这2个控制块
aiow.aio_fildes = fd;
aiow.aio_buf = malloc(32);// 2
strcpy((char *)aiow.aio_buf, "aio test");
aiow.aio_nbytes = strlen((char *)aiow.aio_buf) + 1;
aiow.aio_offset = 0;// 3
aiow.aio_sigevent.sigev_notify = SIGEV_THREAD; //(SIGEV_THREAD:通知信号)
aiow.aio_sigevent.sigev_notify_function = aior_completion_handler;
aiow.aio_sigevent.sigev_notify_attributes = NULL;// 4
aiow.aio_sigevent.sigev_value.sival_ptr = &aiow; // 指向I/O控制块aiow
对四种I/O模型的总结(问答题)
1、阻塞I/O:资源不可用时,进程阻塞,资源可用之后进程被唤醒,阻塞期间不占用CPU,时最常用的方式。(同步阻塞)
2、非阻塞I/O:不管资源是否可用,调用后立即返回,通过返回值判断I/O操作是否成功。(同步非阻塞)
3、I/O多路复用:可以同时监听多个设备的状态,如果所有被监听的设备都没有关心的事件发生,系统调用被阻塞,如果任何一个设备有关心的事件发生,就会唤醒系统调用,系统调用将会再次遍历所监听的设备,获取其事件信息,然后返回。之后可以对设备进行非阻塞的读或写。(异步阻塞)
4、异步I/O:调用者发起异步I/O操作请求,然后立即返回,I/O操作完成后程序会收到通知。(异步非阻塞)
异步通知
异步通知:当设备资源可获取时,由驱动主动通知应用程序,再由应用程序发起访问,类似于中断机制。
异步通知的应用程序实现步骤:
1,注册信号函数
2,打开设备文件
3,设置设备资源可用时驱动向进程发送的信号
4,设置文件的FASYNC标志,使能异步通知机制
mmap 设备文件操作
mmap做为系统调用实现了内存映射,可以实现用户空间和内核空间内存的映射。可以将文件映射到内存中,实现用内存读写代替I/O操作。
remap_pfn_range 函数是其中的核心函数。
编程题
写一个Linux驱动程序,要满足以下要求:
1) 内核中定义一个字符数组iBuf,用来贮存数据;
2) 打开设备时对iBuf清空;
3) 写设备时,把字符串存入iBuf;
4) 读设备时,从iBuf读出所有数据;
5) ioctl控制函数实现四个功能:
a) 命令代码1,无参数,把iBuf中的字母转成小写;
b) 命令代码2,无参数,把iBuf中的字母转成大写;
c) 命令代码3,无参数,按照ASCII码升序排列;
d) 命令代码4,无参数,按照ASCII码降序排列;
6)写出调用驱动示例程序
/*驱动*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1
#define VSER_DEV_NAME "driver"#define LOWWERCASE 0 // 或者用宏 _IO('s', 1)
#define UPPERCASE 1 // _IO('s', 2)
#define ASC 2 // _IO('s', 3)
#define DESC 3 // _IO('s', 4)
#define array_size 128static struct cdev adev; // 表示了一个具体的字符设备static char iBuf[array_size]; // 定义字符数组// 开启设备
static int dev_open(struct inode* inode, struct file* filp)
{memset(iBuf, 0, sizeof(iBuf) / sizeof(char));return 0;
}// 释放设备
static int dev_release(struct inode* inode, struct file* filp)
{return 0;
}// 读出设备函数
static ssize_t dev_read(struct file* filp, char* buf, size_t count, loff_t* pos)
{if(copy_to_user(buf, iBuf, count)) return -1; // 将内核空间的连续内存中的内容复制到用户空间return 0;
}// 写入设备函数
static ssize_t dev_write(struct file* filp, const char* buf, size_t count, loff_t* pos)
{memset(iBuf, 0, sizeof(iBuf) / sizeof(char));if(copy_from_user(iBuf, buf, count)) return -1; // 将用户空间的连续内存中的内容复制到内核空间return 0;
}// 通过冒泡排序为iBuf排序函数
static void bubble_sort(int cmd){int n = sizeof(iBuf);for (int i = 0; i < n - 1; i++){for (int j = 0; j < n - i - 1; j++){int flag = 0;if(iBuf[j] > iBuf[j + 1] && cmd == 1) flag = 1; // 升序if(iBuf[j] < iBuf[j + 1] && cmd == 2) flag = 2; // 降序if(flag){iBuf[j] = iBuf[j] ^ iBuf[j+1];iBuf[j+1] = iBuf[j] ^ iBuf[j+1];iBuf[j] = iBuf[j] ^ iBuf[j+1];}}}
}// 字母转化函数
static void caser(int cmd){for (int i = 0; i < array_size; i++){int flag = 0;if(iBuf[i] >= 'A' && iBuf[i] <= 'Z' && cmd == 1) flag = 1; // 大转小if(iBuf[i] >= 'a' && iBuf[i] <= 'z' && cmd == 2) flag = 2; // 小转大if(flag == 1){iBuf[i] += 32;}if(flag == 2){iBuf[i] -= 32;}}
}// ioctl函数
static long dev_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{if(_IOC_TYPE(cmd) != 's') return -ENOTTY; // 可以不写switch(cmd){case LOWWERCASE:caser(1);break;case UPPERCASE:caser(2);break;case ASC:bubble_sort(1);break;case DESC:bubble_sort(2);break;}return 0;
}// 封装操作到 file_operations 结构体中
static struct file_operations some_ops = {.owner = THIS_MODULE,.open = dev_open,.release = dev_release,.read = dev_read,.write = dev_write,.unlocked_ioctl = dev_ioctl
};// 初始化模块时操作
static int _init dev_init(void)
{int ret;// 使用宏MKDEV将主设备号和次设备号合并成一个设备号,高12位为主设备号,低20位为次设备号dev_t dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 第一个参数指定起始设备号,第二个参数指定申请个数,第三个参数标记主设备号的名称// 为了解决设备号冲突的问题,动态分配设备号函数:alloc_chrdev_region(dev, baseminor, count, name)ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);if(ret) goto reg_err;// 初始化字符设备,将设备与操作关联,第一个参数为设备地址,第二个参数为操作结构体地址cdev_init(&adev, &some_ops); // 将设备添加到散列表中去,第一个参数为设备地址,第二个参数为设备号,第三个参数为cdev对象可以管理多少设备// 一个对象可以管理多个设备ret = cdev_add(&adev, dev, VSER_DEV_CNT);if(ret) goto add_err;return 0;
reg_err:return ret;
add_err:// 如果添加对象失败的话注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}// 卸载模块时操作
static void _exit dev_exit(void)
{// 获取设备号dev_t dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 模块卸载时删除掉设备对象cdev_del(&adev);// 注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}// 为初始化函数和退出函数起别名
module_init(dev_init);
module_exit(dev_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("NAPO <2661273650@qq.com>");
MODULE_DESCRIPTION("A Module");
MODULE_ALIAS("napo_module");
/*调用驱动事例程序*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>#include "dev.h" // 定义有ioctl的命令等等int main(int argc, char* argv){int fd = open("/dev/mydev", O_RDWR);if (fd == -1) return -1;char read_buf[16];char write_buf[16];if(read(fd, read_buf, sizeof(read_buf) / sizeof(char))) printf("读出失败");else printf("读出成功");if(write(fd, write_buf, sizeof(write_buf) / sizeof(char))) printf("写入失败");else printf("写入成功");ioctl(fd, LOWWERCASE); // 转换为小写ioctl(fd, UPPERCASE); // 转换为大写ioctl(fd, ASC); // 升序排列ioctl(fd, DESC); // 降序排列close(fd);return 0;
}
嵌入式linux驱动开发教程相关推荐
- 《嵌入式Linux驱动开发教程》--内核模块
内核模块 绝大多数的驱动都是以内核模块的形式实现. 宏内核和微内核 宏内核(Linux):所有的内核功能都被整体编译在一起,形成单独的内核镜像文件,内核中各功能模块的交互通过直接的函数调用进行. 微内 ...
- 《嵌入式Linux驱动开发教程》--高级I/O操作
高级I/O操作 1.ioctl设备操作 2.proc文件系统 3.非阻塞式IO 4.阻塞式IO 5. IO多路复用 6.异步IO 7.异步通知(信号驱动IO模型) 8.相关代码 8.1非阻塞式IO用户 ...
- 嵌入式linux驱动开发实战教程,嵌入式Linux驱动开发实战视频教程
嵌入式Linux驱动开发实战教程(内核驱动.看门狗技术.触摸屏.视频采集系统) 适合人群:高级 课时数量:109课时 用到技术:嵌入式 Linux 涉及项目:驱动开发.看门狗技术.触摸屏.视频采集 咨 ...
- 【嵌入式Linux】嵌入式Linux驱动开发基础知识之Pinctrl子系统和GPIO子系统的使用
文章目录 前言 1.Pinctrl子系统 1.1.为什么有Pinctrl子系统 1.2.重要的概念 1.3.代码中怎么引用pinctrl 2.GPIO子系统 2.1.为什么有GPIO子系统 2.2.在 ...
- 【嵌入式Linux】嵌入式Linux驱动开发基础知识之按键驱动框架
文章目录 前言 1.APP怎么读取按键值 1.1.查询方式 1.2.休眠-唤醒方式 1.3.poll方式 1.3.异步通知方式 1.5. 驱动程序提供能力,不提供策略 2.按键驱动程序框架--查询方式 ...
- 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED模板驱动程序的改造:设备树
文章目录 前言 1.驱动的三种编写方法 2.怎么使用设备树写驱动程序 2.1.设备树节点要与platform_driver能匹配 2.2.修改platform_driver的源码 3.实验和调试技巧 ...
- 【嵌入式Linux】嵌入式Linux驱动开发基础知识之设备树模型
文章目录 前言 1.设备树的作用 2.设备树的语法 2.1.设备树的逻辑图和dts文件.dtb文件 2.1.1.1Devicetree格式 1DTS文件的格式 node的格式 properties的格 ...
- 【嵌入式Linux】嵌入式Linux驱动开发基础知识之总线设备驱动模型
文章目录 前言 1.驱动编写的三种方法 1.1.传统写法 1.2.总线驱动模型 1.3.设备树驱动模型 2.Linux实现分离:Bus/Dev/Drv模型 2.1.Bus/Dev/Drv模型 2.2. ...
- 【嵌入式Linux】嵌入式Linux驱动开发基础知识之驱动设计的思想:面向对象/分层/分离
文章目录 前言 1.分离设计 驱动程序分析---程序分层 通用驱动程序---面向对象 个性化驱动程序---分离 APP 程序分析 前言 韦东山嵌入式Linux驱动开发基础知识学习笔记 文章中大多内容来 ...
最新文章
- 《BI项目笔记》创建标准维度、维度自定义层次结构
- (005) java后台开发之Mac终端命令运行java
- LeetCode Partition Equal Subset Sum(动态规划)
- php admin允许空密码登陆
- VS2008 ,TFS2008破解序列号
- %date:~0,10%用法
- Android中的Handler的具体用法
- JavaScript对象的理解
- SAP WM 工单完工入库,系统报错- No SU type could be determined -
- 《神经网络与深度学习》课程笔记(3)-- 神经网络基础之Python与向量化
- javascript无提示关闭窗口,兼容IE,Firefox
- 论文准备:基于区块链的一些设计IIoT的最新动向调查【已公开发表】
- 手把手教你做一个2048 上
- linux sdr 2832u软件无线电,使用R820T+RTL2832U玩软件无线电
- Endnote如何添加CAJ格式文件
- 央行降息后六大城市房价有望反弹(名单)
- 微信支付申请相关问题
- linux生成checksum,SF2281修改Lic授权ID生成新checksum生成器
- 细说ItemInfo
- intval()和int()
热门文章
- 新绝代双骄三 (张菁、何露)完美全攻略
- informix数据库unload导出数据Load导入数据
- SVN使用免费外网访问 实现远程办公
- 数字信号处理7——点到向量的距离
- TensorFlow2 -官方教程 :保存和恢复模型
- 【HTML】HTML 列表 ( 无序列表 | 有序列表 | 自定义列表 )
- Linux中cpio是什么?有几种操作模式?
- Joan Baez - Willie Moore
- IBM x3750 M4安装ESXi5.5
- Blood Cousins (dsu on tree + 求第k级祖先)