目录

hotplug/uevent机制分析

修改mdev配置支持U盘自动挂载


hotplug/uevent机制分析

字符设备驱动中可以实现自动创建设备节点也就是以下函数

class_device_create(),为了让mdev(udev的简化版本)根据这些信息来创建设备节点,分析自动创建设备节点的过程,class_device_create函数最终会调用到kobject_uevent_env函数

struct class_device *class_device_create(struct class *cls,struct class_device *parent,dev_t devt,struct device *device,const char *fmt, ...)
{
...va_end(args);retval = class_device_register(class_dev);if (retval)goto error;return class_dev;error:kfree(class_dev);return ERR_PTR(retval);
}...
int class_device_register(struct class_device *class_dev)
{class_device_initialize(class_dev);return class_device_add(class_dev);
}...int class_device_add(struct class_device *class_dev)
{struct class *parent_class = NULL;struct class_device *parent_class_dev = NULL;struct class_interface *class_intf;int error = -EINVAL;...kobject_uevent(&class_dev->kobj, KOBJ_ADD);/* notify any interfaces this device is now here */down(&parent_class->sem);list_add_tail(&class_dev->node, &parent_class->children);list_for_each_entry(class_intf, &parent_class->interfaces, node) {if (class_intf->add)class_intf->add(class_dev, class_intf);}up(&parent_class->sem);...return error;
}...
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{return kobject_uevent_env(kobj, action, NULL);
}...int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,char *envp_ext[])
{char **envp;char *buffer;char *scratch;const char *action_string;const char *devpath = NULL;const char *subsystem;struct kobject *top_kobj;struct kset *kset;struct kset_uevent_ops *uevent_ops;u64 seq;char *seq_buff;int i = 0;int retval = 0;int j;pr_debug("%s\n", __FUNCTION__);action_string = action_to_string(action);if (!action_string) {pr_debug("kobject attempted to send uevent without action_string!\n");return -EINVAL;}.../* environment values */buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);if (!buffer) {retval = -ENOMEM;goto exit;}/* complete object path */devpath = kobject_get_path(kobj, GFP_KERNEL);if (!devpath) {retval = -ENOENT;goto exit;}/* event environemnt for helper process only */envp[i++] = "HOME=/";envp[i++] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";/* default keys */scratch = buffer;envp [i++] = scratch;scratch += sprintf(scratch, "ACTION=%s", action_string) + 1;envp [i++] = scratch;scratch += sprintf (scratch, "DEVPATH=%s", devpath) + 1;envp [i++] = scratch;scratch += sprintf(scratch, "SUBSYSTEM=%s", subsystem) + 1;
.../* call uevent_helper, usually only enabled during early boot */if (uevent_helper[0]) {char *argv [3];argv [0] = uevent_helper;argv [1] = (char *)subsystem;argv [2] = NULL;call_usermodehelper (argv[0], argv, envp, 0);}exit:kfree(devpath);kfree(buffer);kfree(envp);return retval;
}

分析kobject_uevent_env函数,其中action_string为action_to_string返回值,而action传进的参数为KOBJ_ADD,因此action_string就等于add

static char *action_to_string(enum kobject_action action)
{switch (action) {case KOBJ_ADD:return "add";case KOBJ_REMOVE:return "remove";case KOBJ_CHANGE:return "change";case KOBJ_OFFLINE:return "offline";case KOBJ_ONLINE:return "online";case KOBJ_MOVE:return "move";default:return NULL;}
}

分配保存环境变量的内存,后面对环境变量进行设置,而环境变量是什么东西,在开发板中敲env命令,这些就是环境变量,而这些环境变了是sh程序的环境变量,每一个应用程序都有环境变量

buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);

# env
USER=root
HOME=/
TERM=vt102
PATH=/sbin:/usr/sbin:/bin:/usr/bin
SHELL=/bin/sh
PWD=/
#

在kobject_uevent_env中uevent_helper就是/sbin/mdev,证明uevent_helper就是/sbin/mdev,在其面前添加打印语句,对于call_usermodehelper,调用用户模式应用程序,创建一个进程,传入环境变量,应用程序就会根据这些环境变量来创建设备节点

     printk("xiaoma:uevent_helper = %s\n", uevent_helper);for(i = 0; i < envp[i]; i++){printk("envp[%d] = %s\n", i, envp[i]);}argv [0] = uevent_helper;  argv [1] = (char *)subsystem;argv [2] = NULL;call_usermodehelper (argv[0], argv, envp, 0);

编译新内核加载一个按键驱动程序

# insmod fifth_drv.ko
xiaoma :uevent_helper = /sbin/mdev
envp[0] = HOME=/
envp[1] = PATH=/sbin:/bin:/usr/sbin:/usr/bin
envp[2] = ACTION=add
envp[3] = DEVPATH=/module/fifth_drv
envp[4] = SUBSYSTEM=module
envp[5] = SEQNUM=711
xiaoma :uevent_helper = /sbin/mdev
envp[0] = HOME=/
envp[1] = PATH=/sbin:/bin:/usr/sbin:/usr/bin
envp[2] = ACTION=add
envp[3] = DEVPATH=/class/fifthdrv/buttons
envp[4] = SUBSYSTEM=fifthdrv
envp[5] = SEQNUM=712
envp[6] = MAJOR=252
envp[7] = MINOR=0

分析busybox的mdev.c,在mdev_main会判断-s选项,在开发板查看mdev信息,加-s选项是当系统启动的时候扫描sys目录(根据里面的信息创建各种设备节点)产生/dev目录,这是启动脚本中有加入mdev -s,而加载驱动应该跳过此选项,执行make_device函数,其path参数就是/sys/env_path,而env_path如上为/class/fifthdrv/buttons,对着相对应的文件,在make_device中执行strcat(path, "/dev"),查看/sys/class/fifthdrv/buttons/dev,存放这节点的信息,在后面device_name = bb_basename(path)确定设备文件名,类型,主次设备号,而type = path[5]=='c' ? S_IFCHR : S_IFBLK判断第五个字符是否为c,是的话则为字符设备驱动,接着执行fd = open("/etc/mdev.conf", O_RDONLY)如果有配置文件的话,会优先执行配置,最终调用到mknod(device_name, mode | type, makedev(major, minor)创建设备节点

# mdev
BusyBox v1.7.0 (2020-12-27 14:56:30 CST) multi-call binary

Usage: mdev [-s]

-s      Scan /sys and populate /dev during system boot

Called with no options (via hotplug) it uses environment variables
to determine which device to add/remove.

# ls /sys/class/fifthdrv/buttons
dev        subsystem  uevent
# cat /sys/class/fifthdrv/buttons/dev
252:0

int mdev_main(int argc, char **argv)
{char *action;char *env_path;RESERVE_CONFIG_BUFFER(temp,PATH_MAX);xchdir("/dev");/* Scan */if (argc == 2 && !strcmp(argv[1],"-s")) {struct stat st;xstat("/", &st);root_major = major(st.st_dev);root_minor = minor(st.st_dev);recursive_action("/sys/block",ACTION_RECURSE | ACTION_FOLLOWLINKS,fileAction, dirAction, temp, 0);recursive_action("/sys/class",ACTION_RECURSE | ACTION_FOLLOWLINKS,fileAction, dirAction, temp, 0);/* Hotplug */} else {action = getenv("ACTION");env_path = getenv("DEVPATH");if (!action || !env_path)bb_show_usage();sprintf(temp, "/sys%s", env_path);if (!strcmp(action, "remove"))make_device(temp, 1);else if (!strcmp(action, "add")) {make_device(temp, 0);if (ENABLE_FEATURE_MDEV_LOAD_FIRMWARE)load_firmware(getenv("FIRMWARE"), temp);}}if (ENABLE_FEATURE_CLEAN_UP) RELEASE_CONFIG_BUFFER(temp);return 0;
}
..../* mknod in /dev based on a path like "/sys/block/hda/hda1" */
static void make_device(char *path, int delete)
{const char *device_name;int major, minor, type, len;int mode = 0660;uid_t uid = 0;gid_t gid = 0;char *temp = path + strlen(path);char *command = NULL;/* Try to read major/minor string.  Note that the kernel puts \n after* the data, so we don't need to worry about null terminating the string* because sscanf() will stop at the first nondigit, which \n is.  We* also depend on path having writeable space after it. */if (!delete) {strcat(path, "/dev");len = open_read_close(path, temp + 1, 64);*temp++ = 0;if (len < 1) return;}/* Determine device name, type, major and minor */device_name = bb_basename(path);type = path[5]=='c' ? S_IFCHR : S_IFBLK;/* If we have a config file, look up permissions for this device */if (ENABLE_FEATURE_MDEV_CONF) {char *conf, *pos, *end;int line, fd;/* mmap the config file */fd = open("/etc/mdev.conf", O_RDONLY);if (fd < 0)goto end_parse;len = xlseek(fd, 0, SEEK_END);conf = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);close(fd);if (!conf)goto end_parse;line = 0;/* Loop through lines in mmaped file*/for (pos=conf; pos-conf<len;) {int field;char *end2;line++;/* find end of this line */for (end=pos; end-conf<len && *end!='\n'; end++);/* Three fields: regex, uid:gid, mode */for (field=0; field < (3 + ENABLE_FEATURE_MDEV_EXEC);field++){/* Skip whitespace */while (pos<end && isspace(*pos)) pos++;if (pos==end || *pos=='#') break;for (end2=pos;end2<end && !isspace(*end2) && *end2!='#'; end2++);if (field == 0) {/* Regex to match this device */char *regex = xstrndup(pos, end2-pos);regex_t match;regmatch_t off;int result;/* Is this it? */xregcomp(&match,regex, REG_EXTENDED);result = regexec(&match, device_name, 1, &off, 0);regfree(&match);free(regex);/* If not this device, skip rest of line */if (result || off.rm_so|| off.rm_eo != strlen(device_name))break;}if (field == 1) {/* uid:gid */char *s, *s2;/* Find : */for (s=pos; s<end2 && *s!=':'; s++);if (s == end2) break;/* Parse UID */uid = strtoul(pos, &s2, 10);if (s != s2) {struct passwd *pass;char *_unam = xstrndup(pos, s-pos);pass = getpwnam(_unam);free(_unam);if (!pass) break;uid = pass->pw_uid;}s++;/* parse GID */gid = strtoul(s, &s2, 10);if (end2 != s2) {struct group *grp;char *_grnam = xstrndup(s, end2-s);grp = getgrnam(_grnam);free(_grnam);if (!grp) break;gid = grp->gr_gid;}}if (field == 2) {/* mode */mode = strtoul(pos, &pos, 8);if (pos != end2) break;}if (ENABLE_FEATURE_MDEV_EXEC && field == 3) {// Command to runconst char *s = "@$*";const char *s2;s2 = strchr(s, *pos++);if (!s2) {// Force errorfield = 1;break;}if ((s2-s+1) & (1<<delete))command = xstrndup(pos, end-pos);}pos = end2;}/* Did everything parse happily? */if (field > 2) break;if (field) bb_error_msg_and_die("bad line %d",line);/* Next line */pos = ++end;}munmap(conf, len);end_parse:  /* nothing */ ;}umask(0);if (!delete) {if (sscanf(temp, "%d:%d", &major, &minor) != 2) return;if (mknod(device_name, mode | type, makedev(major, minor)) && errno != EEXIST)bb_perror_msg_and_die("mknod %s", device_name);if (major == root_major && minor == root_minor)symlink(device_name, "root");if (ENABLE_FEATURE_MDEV_CONF) chown(device_name, uid, gid);}if (command) {/* setenv will leak memory, so use putenv */char *s = xasprintf("MDEV=%s", device_name);putenv(s);if (system(command) == -1)bb_perror_msg_and_die("cannot run %s", command);s[4] = '\0';unsetenv(s);free(s);free(command);}if (delete) unlink(device_name);
}

卸载的时候最终也会导致kobject_uevent_env调用call_usermodehelper这个应用程序被调用,以前是add,卸载的时候就变成了remove,mdev就会根据这些环境变量来删除设备节点

# rmmod fifth_drv
xiaoma :uevent_helper = /sbin/mdev
envp[0] = HOME=/
envp[1] = PATH=/sbin:/bin:/usr/sbin:/usr/bin
envp[2] = ACTION=remove
envp[3] = DEVPATH=/class/fifthdrv/buttons
envp[4] = SUBSYSTEM=fifthdrv
envp[5] = SEQNUM=713
envp[6] = MAJOR=252
envp[7] = MINOR=0
xiaoma :uevent_helper = /sbin/mdev
envp[0] = HOME=/
envp[1] = PATH=/sbin:/bin:/usr/sbin:/usr/bin
envp[2] = ACTION=remove
envp[3] = DEVPATH=/class/fifthdrv
envp[4] = SUBSYSTEM=class
envp[5] = SEQNUM=714
xiaoma :uevent_helper = /sbin/mdev
envp[0] = HOME=/
envp[1] = PATH=/sbin:/bin:/usr/sbin:/usr/bin
envp[2] = ACTION=remove
envp[3] = DEVPATH=/module/fifth_drv
envp[4] = SUBSYSTEM=module
envp[5] = SEQNUM=715

为什么uevent_helper就是/sbin/mdev,是因为调用应用程序: 比如mdev,启动脚本,在脚本中echo /sbin/mdev > /proc/sys/kernel/hotplug,设置了uevent_helper为"/sbin/mdev"

修改mdev配置支持U盘自动挂载

当我们插上U盘后,自动创建了设备节点,其中sda表示整个U盘,sda1表示第一个主分区,这样就可以手动来mount挂载,而我们想实现自动挂载

# usb 1-1: new full speed USB device using s3c2410-ohci and address 2
usb 1-1: configuration #1 chosen from 1 choice
scsi0 : SCSI emulation for USB Mass Storage devices

# scsi 0:0:0:0: Direct-Access     SMI      USB DISK         1100 PQ: 0 ANSI: 4
sd 0:0:0:0: [sda] 61440000 512-byte hardware sectors (31457 MB)
sd 0:0:0:0: [sda] Write Protect is off
sd 0:0:0:0: [sda] Assuming drive cache: write through
sd 0:0:0:0: [sda] 61440000 512-byte hardware sectors (31457 MB)
sd 0:0:0:0: [sda] Write Protect is off
sd 0:0:0:0: [sda] Assuming drive cache: write through
 sda: sda1
sd 0:0:0:0: [sda] Attached SCSI removable disk

# ls -l /dev/sda*
brw-rw----    1 0        0          8,   0 Mar  1 15:49 /dev/sda
brw-rw----    1 0        0          8,   1 Mar  1 15:49 /dev/sda1

在分析uevent机制的时候,分析busybox的mdev.c中如果有配置文件的话,会优先执行配置,通过配置文件让mdev这个应用程序来执行我们的命令,在busybox中有个mdev.txt,告诉我们怎么用mdev

其中mdev.conf的格式如下,device regex:正则表达式,表示哪一个设备,uid:owner,gid主ID,octal permissions:以八进制表示的属性,@:创建设备节点之后执行命令,$:删除设备节点之前执行命令,*: 创建设备节点之后 和 删除设备节点之前 执行命令,command:要执行的命令(例如写入自动挂载的命令)

<device regex> <uid>:<gid> <octal permissions> [<@|$|*> <command>]

其中表示默认属性为660,而我们可以修改其属性,mdev根据device regex判断哪一个设备,根据这个设备创建设备节点,再根据这个配置文件来创建属性

The file has the format:
    <device regex> <uid>:<gid> <octal permissions>
For example:
    hd[a-z][0-9]* 0:3 660

The config file parsing stops at the first matching line.  If no line is
matched, then the default of 0:0 660 is used.  To set your own default, simply
create your own total match like so:
    .* 1:1 777

而device regex正则表达式是什么意思,有一个30分钟入门教程,在windows下查看文件例如*.c,*表示通配符,任意字符,如果想更精确的话,就用正则表达式,举例一些简单的字符规则

. 表示任意字符(换行符除外)
* 重复0次或更多次
+ 重复1次或更多次
重复0次或1次
[-] 中括号里一系列字符,表示这些字符里的某一个

例如,以下有一个led驱动程序,以次设备号分别创建,默认属性为660,写出mdev.conf来改变属性

# insmod myleds.ko
leds initialized
# ls -l /dev/led*
crw-rw----    1 0        0        231,   1 Mar  1 16:25 /dev/led1
crw-rw----    1 0        0        231,   2 Mar  1 16:25 /dev/led2
crw-rw----    1 0        0        231,   3 Mar  1 16:25 /dev/led3
crw-rw----    1 0        0        231,   0 Mar  1 16:25 /dev/leds

修改配置文件,重新加载驱动程序,查看驱动信息, 属性为改为了777

# vi /etc/mdev.conf
leds 0:0 777
led1 0:0 777
led2 0:0 777
led3 0:0 777

# rmmod myleds
# insmod myleds.ko
leds initialized
# ls -l /dev/led*
crwxrwxrwx    1 0        0        231,   1 Mar  1 16:29 /dev/led1
crwxrwxrwx    1 0        0        231,   2 Mar  1 16:29 /dev/led2
crwxrwxrwx    1 0        0        231,   3 Mar  1 16:29 /dev/led3
crwxrwxrwx    1 0        0        231,   0 Mar  1 16:29 /dev/leds

这样写麻烦利用正则表达式进行修改,修改为leds?[123]?,?问号表示前面的字符重复0次或者1次,一样的效果

# vi /etc/mdev.conf
leds?[123]? 0:0 777
# rmmod myleds
# insmod myleds.ko
leds initialized
# ls -l /dev/led*
crwxrwxrwx    1 0        0        231,   1 Mar  1 16:29 /dev/led1
crwxrwxrwx    1 0        0        231,   2 Mar  1 16:29 /dev/led2
crwxrwxrwx    1 0        0        231,   3 Mar  1 16:29 /dev/led3
crwxrwxrwx    1 0        0        231,   0 Mar  1 16:29 /dev/leds

增加命令,打印增加某个设备,在文档中有说明用$MDEV环境变量来表示哪个设备节点

leds?[123]? 0:0 777 @ echo create /dev/$MDEV > /dev/console

For your convenience, the shell env var $MDEV is set to the device name.  So if
the device 'hdc' was matched, MDEV would be set to "hdc".

# vi /etc/mdev.conf
leds?[123]? 0:0 777 @ echo create /dev/$MDEV > /dev/console

# rmmod myleds
# insmod myleds.ko
leds initialized
# create /dev/leds
create /dev/led1
create /dev/led2
create /dev/led3

用*符号来加载卸载前后都打印,而我们怎么分辩是加载还是卸载,以shell脚本命令利用环境变量来实现

# vi /etc/mdev.conf
leds?[123]? 0:0 777 * if [ $ACTION = "add" ]; then echo create /dev/$MDEV > /dev/console; else echo remove /dev/$MDEV > /dev/console; fi

# rmmod myleds
# remove /dev/led1
remove /dev/leds
remove /dev/led2
remove /dev/led3
remove /dev/leds
# insmod myleds.ko
leds initialized
# create /dev/leds
create /dev/led2
create /dev/led1
create /dev/led3

把命令写入一个脚本给脚本添加执行权限chmod +x /bin/add_remove_led.sh,一样的效果

leds?[123]? 0:0 777 * /bin/add_remove_led.sh

add_remove_led.sh:

#!/bin/sh
if [ $ACTION = "add" ]; 
then 
    echo create /dev/$MDEV > /dev/console; 
else 
    echo remove /dev/$MDEV > /dev/console; 
fi

因此实现U盘自动挂载就很容易了,也可以把命令写入一个脚本,过程如上,+表示重复1次或者更多次,至少1次

sda[1-9]+ 0:0 777 * if [ $ACTION = "add" ]; then mount /dev/$MDEV /mnt; else umount /mnt; fi

效果如下,插上U盘后自动挂载

# usb 1-1: USB disconnect, address 2
usb 1-1: new full speed USB device using s3c2410-ohci and address 3
usb 1-1: configuration #1 chosen from 1 choice
scsi1 : SCSI emulation for USB Mass Storage devices
scsi 1:0:0:0: Direct-Access     SMI      USB DISK         1100 PQ: 0 ANSI: 4
sd 1:0:0:0: [sda] 61440000 512-byte hardware sectors (31457 MB)
sd 1:0:0:0: [sda] Write Protect is off
sd 1:0:0:0: [sda] Assuming drive cache: write through
sd 1:0:0:0: [sda] 61440000 512-byte hardware sectors (31457 MB)
sd 1:0:0:0: [sda] Write Protect is off
sd 1:0:0:0: [sda] Assuming drive cache: write through
 sda: sda1
sd 1:0:0:0: [sda] Attached SCSI removable disk

# ls /dev/sda*
/dev/sda   /dev/sda1
# ls /dev/sda* -l
brw-rw----    1 0        0          8,   0 Mar  1 17:13 /dev/sda
brwxrwxrwx    1 0        0          8,   1 Mar  1 17:13 /dev/sda1
# cd /mnt/
# ls
1.txt                      autorun.inf
System Volume Information  eaget.ico

Linux驱动之热拔插相关推荐

  1. 韦东山二期驱动视频-热拔插驱动——RK3399自制linux系统不支持HDMI热拔插问题分析

    背景: 公司的板子,对于HDMI的显示器热拔插不支持,只能在插入HDMI时启动才能输出,而当开机之后,再插入HDMI显示器则无输出,不知道原因. 推测如下: 1.设备树的引脚配置有误,导致插入HDMI ...

  2. 嵌入式linux pcie网卡配置,[嵌入式linux]PCIe 热拔插(rescan)

    linux下可通过/sys/bus/pci/devices/0000\:[bus number]\:[device number].[function number]/ 目录下的节点进行热拔插操作. ...

  3. 【Orangepi Zero2 全志H616】语音刷抖音 / 手机连接Linux热拔插相关

    目录 一.手机连接Linux步骤 二.adb控制指令 三.基于Linux串口实现语音刷抖音 1.语音模块控制详情 2.代码实现 一.手机连接Linux步骤 1.把手机接入开发板 2.安装adb工具,在 ...

  4. U盘的热拔插/自动挂载跟linux2.6 kernel、 udev、 hal、 dbus 、gnome-mount 、thunar的关系...

    U盘的热拔插/自动挂载跟linux2.6 kernel. udev. hal. dbus .gnome-mount .thunar的关系 博客分类: System About Linux配置管理网络应 ...

  5. Linux驱动移植USB网卡r8156驱动(详细)总结

    目录 一.简介 二.驱动移植 2.1 驱动源码解压 2.2 驱动Kconfig和Makefile配置 2.2.1 驱动上层目录识别驱动文件 2.2.2 驱动目录新建驱动Kconfig和Makefile ...

  6. linux 驱动 device,driver ,bus 关系

    对于Linux驱动开发来说,设备模型的理解是根本,顾名思义设备模型是关于设备的模型,设备的概念就是总线和与其相连的各种设备了. 设备是通过总线连到计算机上的,需要对应的驱动才能用,可是总线是如何发现设 ...

  7. 嵌入式Linux驱动笔记(十六)------设备驱动模型(kobject、kset、ktype)

    ###你好!这里是风筝的博客, ###欢迎和我一起交流. 前几天去面试,被问到Linux设备驱动模型这个问题,没答好,回来后恶补知识,找了些资料,希望下次能答出个满意答案. Linux早期时候,一个驱 ...

  8. Linux 驱动 | SPI子系统

    SPI子系统 这些驱动的共同点: 主机端驱动和外设端驱动分离 通过一个核心层将某种总线协议进行抽象 外设端驱动通过核心层API间接调用主机驱动提供的传输函数进行收发数据 IIC.SPI等不支持热拔插的 ...

  9. 【linux驱动】USB子系统分析

    本文针对Linux内核下USB子系统进行分析,主要会涉及一下几个方面: USB基础知识:介绍USB设备相关的基础知识 Linux USB子系统分析:分析USB系统框架,USB HCD/ROOT HUB ...

最新文章

  1. 中国最大AI预训练模型发布:113亿参数!北京智源研究院、阿里、清华等联手打造...
  2. 用setResult回传intent参数的时候,接收方activity闪退
  3. Android 监听 Android中监听系统网络连接打开或者关闭的实现代码
  4. 论文笔记:Distilling the Knowledge
  5. 全球及中国食品行业发展潜力与投资机会评估报告2022版
  6. 美国伊利诺伊大学香槟分校计算机专业,伊利诺伊大学香槟分校计算机科学排名第7(2020年TFE美国排名)...
  7. 闪烁点击效果css,CSS3自定义闪烁动画效果实例
  8. Java for循环的几种用法
  9. 利用云功能和API监视Google表格中的Cloud Dataprep作业状态
  10. Hemberg-lab单细胞转录组数据分析(三)
  11. SecureCRT上传bash: rz: command not found
  12. nginx.conf文件配置及nginx重启脚本
  13. ipad上的html编辑器,它让我开始尝试在 iPad 上写作:MWeb for iOS 使用体验
  14. 具体案例 快速原型模型_快速原型模型
  15. 万字长文!多图预警!46张图彻底搞懂 IP 基础知识!
  16. IMO 2017 T4解答
  17. PIKA trouble02 -- (error) ERR Syntax error, try CLIENT (LIST [order by [addr|idle]| KILL ip:port)
  18. 基于微信小程序的单词记忆系统(Java+SSM+MySQL)
  19. Axure自定义Echarts交互图
  20. 机器学习-47-ML-03-Metric-based Approach Train+Test as RNN(元学习-support set和query set用于同一网络的方法)

热门文章

  1. java下标越界怎么解决_java 程序运行会出现 下标越界 如何处理?
  2. 【JAVA】穷词——基于嵌入式的数据库derby+BeautyEye的单词字典应用
  3. 新人第一天报到注意的事项
  4. 英语学习:onset and rime;声母与韵母;元音字母
  5. error 错误页面
  6. try-cathc-finally
  7. ComponentOne Studio WPF部署功能完全兼容
  8. 【解决】在 IPMONTR.DLL 中初始化函数 INITHELPERDLL 启动失败,错误代码为 10107
  9. PHP的PSR推荐规范,PSR-1,PSR-2,PSR-3,PSR-4详解
  10. 腾讯前端特工通关攻略