zoukankan      html  css  js  c++  java
  • Linux字符设备驱动框架(四):Linux内核的input子系统

    /************************************************************************************

    *本文为个人学习记录,如有错误,欢迎指正。

    *本文参考资料: 

    *        https://blog.csdn.net/qq_35865125/article/details/80637809

    *        https://blog.csdn.net/yueqian_scut/article/details/48792939

    *        hhttps://www.cnblogs.com/lifexy/p/7542989.html

    *        https://www.cnblogs.com/xiaojiang1025/p/6414746.html

    ************************************************************************************/

    1. input子系统概述

     Linux输入设备总类繁杂,常见的包括有按键、键盘、触摸屏、鼠标、摇杆等;这些输入设备属于字符设备,而linux内核将这些设备的共同性抽象出来,简化驱动开发建立了一个input子系统。input子系统对linux的输入设备驱动进行了高度抽象,最终分成了三层:input设备驱动层、input核心层、input事件处理层。input子系统的框架如下图。

    (1)input设备驱动层:负责操作具体的硬件设备,将底层的硬件输入转化为统一事件形式,向input核心层汇报;

    (2)input核心层:连接input设备驱动层与input事件处理层,向下提供驱动层的接口,向上提供事件处理层的接口;

    (3)input事件处理层:为不同硬件类型提供了用户访问及处理接口,将硬件驱动层传来的事件报告给用户程序。

    2. 相关数据结构

    先了解三个定义在/linux/input.h下重要的结构体input_dev、input_handler、input_handle。

    (1)input_dev

    Linux内核中使用input_dev结构体来描述一个input设备。input_dev结构体包含了一个input设备的所有信息,不同的input设备可能只用到其中的一部分。

    struct input_dev {
        const char *name;
        const char *phys;
        const char *uniq;
        struct input_id id;  //与input_handler匹配使用的id
    
        unsigned long evbit[BITS_TO_LONGS(EV_CNT)];  //设备支持的输入事件位图
        unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];//对于按键事件,设备支持的输入子事件位图
        unsigned long relbit[BITS_TO_LONGS(REL_CNT)];// 对于相对坐标事件,设备支持的相对坐标子事件位图
        unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];//对于绝对坐标事件,设备支持的绝对坐标子事件位图
        unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];//混杂设备的支持的子事件位图
        unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
        unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
        unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
        unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
    
        unsigned int keycodemax;
        unsigned int keycodesize;
        void *keycode;
        int (*setkeycode)(struct input_dev *dev,
                  unsigned int scancode, unsigned int keycode);
        int (*getkeycode)(struct input_dev *dev,
                  unsigned int scancode, unsigned int *keycode);
    
        struct ff_device *ff;
    
        unsigned int repeat_key;
        struct timer_list timer;
    
        int sync;
    
        int abs[ABS_CNT];
        int rep[REP_MAX + 1];
    
        unsigned long key[BITS_TO_LONGS(KEY_CNT)];
        unsigned long led[BITS_TO_LONGS(LED_CNT)];
        unsigned long snd[BITS_TO_LONGS(SND_CNT)];
        unsigned long sw[BITS_TO_LONGS(SW_CNT)];
    
        int absmax[ABS_CNT];//绝对坐标事件的最大键值
        int absmin[ABS_CNT];//绝对坐标事件的最小键值
        int absfuzz[ABS_CNT];
        int absflat[ABS_CNT];
        int absres[ABS_CNT];
    
        int (*open)(struct input_dev *dev);
        void (*close)(struct input_dev *dev);
        int (*flush)(struct input_dev *dev, struct file *file);
        int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
    
        struct input_handle *grab;
    
        spinlock_t event_lock;
        struct mutex mutex;
    
        unsigned int users;
        bool going_away;
    
        struct device dev;
    
        struct list_head    h_list;//该链表头用于链接该设备所关联的input_handle
        struct list_head    node;  //该链表头用于将设备链接到input_dev_list
    };
    struct input_dev

    (2)input_handler

    input_handler表示对输入事件的具体处理,它为输入设备的功能实现了一个接口。

    struct input_handler 
    {
    void *private;
    void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value); struct input_handle* (*connect)(struct input_handler *handler, struct input_dev *dev, struct input_device_id *id); void (*disconnect)(struct input_handle *handle); const struct file_operations *fops; //提供给用户对设备操作的函数指针 int minor; char *name; struct input_device_id *id_table; //与input_dev匹配用的id struct input_device_id *blacklist; //标记的黑名单 struct list_head h_list; //用于链接和该handler相关的handle struct list_head node; //用于将该handler链入input_handler_list };

    (3)input_handle

     input_handle是用来关联input_dev和input_handler。为什么用input_handle来关联input_dev和input_handler而不将input_dev和input_handler直接对应呢?因为一个device可以对应多个handler,而一个handler也可处理多个device。就如一个触摸屏设备可以对应event handler也可以对应tseve handler。

    struct input_handle 
    {
          void *private;
     
          int open;                      //记录设备打开次数
          char *name;
     
          struct input_dev *dev;         //指向所属的input_dev
          struct input_handler *handler; //指向所属的input_handler
     
          struct list_head      d_node;  //用于链入所指向的input_dev的handle链表
          struct list_head      h_node;  //用于链入所指向的input_handler的handle链表
    };

    3. input核心层分析

    input核心层的核心代码:/kernel/drivers/input/input.c。

    input核心层完成的主要工作包括:

    1) 直接跟字符设备驱动框架交互,字符设备驱动框架根据主设备号来进行管理,而input-core则是依赖于次设备号来进行分类管理。Input子系统的所有输入设备的主设备号都是13,其对应input核心层定义的structfile_operations input_fops。驱动架构层通过主设备号13获取到input_fops,之后的处理便交给input_fops进行。

    2) 提供接口供事件处理层(input-handler)和输入设备(input-device)注册,并为输入设备找到匹配的事件处理者。

    3) 将input-device产生的消息(如触屏坐标和压力值)转发给input-handler,或者将input-handler的消息传递给input-device(如鼠标的闪灯命令)。

    (1)input子系统的初始化

    input子系统使用subsys_initcall宏修饰input_init()函数,因此input_init()函数在内核启动阶段被调用。input_init()函数的主要工作是:在sys文件系统下创建一个设备类(/sys/class/input),调用register_chrdev()函数注册input设备。

    static const struct file_operations input_fops =
    
    {
      .owner = THIS_MODULE,
      .open = input_open_file,
    };
    
    
    static int __init input_init(void)
    {
        int err;
    
        input_init_abs_bypass();
    
        err = class_register(&input_class);   //在sys文件系统下创建input_class类,/sys/class/input
        if (err) {
            printk(KERN_ERR "input: unable to register input_dev class
    ");
            return err;
        }
    
        err = input_proc_init();//在/proc下建立相关文件
        if (err)
            goto fail1;
    
        //注册input设备。INPUT_MAJOR是input设备的主设备号,INPUT_MAJOR=13;即,input子系统的所有设备的主设备号相同
        err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
        if (err) {
            printk(KERN_ERR "input: unable to register char major %d", INPUT_MAJOR);
            goto fail2;
        }
    
        return 0;
    
     fail2:    input_proc_exit();
     fail1:    class_unregister(&input_class);
        return err;
    }
    subsys_initcall(input_init);

    (2)input_handler与input_device的匹配

    input核心层提供了input_dev与input_handler的注册接口。从以下代码中可以看出,input_register_handler()与input_register_device中都进行了input_handler与input_device的匹配;由此可知,不管新添加input_dev还是input_handler,都会进入input_attach_handler()判断两者id是否有支持, 若两者支持便进行连接。

    int input_register_handler(struct input_handler *handler)
    {
       ... ...
      list_add_tail(&handler->node, &input_handler_list); //将新的handler放入链表中
      ... ...
      //遍历input_dev_list链表中的所有input_dev,是否支持这个新添加的input_handle;若两者支持,便进行连接
        list_for_each_entry(dev, &input_dev_list, node)
            input_attach_handler(dev, handler);
      ... ...
    }
    
    int input_register_device(struct input_dev *dev)
    {
      ... ...
      list_add_tail(&dev->node, &input_dev_list); //将新的dev放入链表中
      ... ...
      //遍历input_handler_list链表中的所有input_handler,是否支持这个新添加的input_dev;若两者支持,便进行连接
      list_for_each_entry(handler, &input_handler_list, node)
        input_attach_handler(dev, handler); 
      ... ...
    }
    
    static int input_attach_handler(struct input_dev *dev, struct input_handler *handler)
    {
    ... ...
    id = input_match_device(handler->id_table, dev);  //匹配两者
    
    if (!id)                                          //若不匹配,return退出
    return -ENODEV; 
    
    error = handler->connect(handler, dev, id);       //调用input_handler ->connect函数建立连接
    ... ...
    
    }

    若input_handler与input_device匹配成功,就会自动进入input_handler 的connect函数建立连接。以evdev_handler的.connect函数evdev_connect()为例进行分析。

    static struct input_handler evdev_handler = 
    {
        .event        = evdev_event,
        .connect    = evdev_connect,
        .disconnect    = evdev_disconnect,
        .fops        = &evdev_fops,
        .minor        = EVDEV_MINOR_BASE,
        .name        = "evdev",
        .id_table    = evdev_ids,
    };
    
    static int evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id)     
    {
      ... ... 
      for (minor = 0; minor < EVDEV_MINORS && evdev_table[minor]; minor++);//查找驱动设备的子设备号
       if (minor == EVDEV_MINORS)                  // EVDEV_MINORS=32,所以该事件下的驱动设备最多存32个
      { 
            printk(KERN_ERR "evdev: no more free evdev devices
    ");
            return -ENFILE;                                   //没找到驱动设备
        }
      ... ...
      evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);     //分配一个input_handle全局结构体(没有r)
       ... ...
       evdev->handle.dev = dev;                             //指向参数input_dev驱动设备
      evdev->handle.name = evdev->name;
      evdev->handle.handler = handler;                      //指向参数 input_handler驱动处理结构体
      evdev->handle.private = evdev;
      sprintf(evdev->name, "event%d", minor);               //1)保存驱动设备名字, event%d
      ... ...
      devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor),  //2) 将主设备号和次设备号转换成dev_t类型
      cdev = class_device_create(&input_class, &dev->cdev,  //3)在input类下创建驱动设备
      devt,dev->cdev.dev, evdev->name); 
      ... ...
      error = input_register_handle(&evdev->handle);        //4)注册这个input_handle结构体
      ... ...
    }

    1) 是在保存驱动设备名字,名为event%d, 比如下图(键盘驱动)event1: 因为没有设置子设备号,默认从小到大排列,其中event0是表示这个input子系统,所以这个键盘驱动名字就是event1;

    2)是在保存驱动设备的主次设备号,其中主设备号INPUT_MAJOR=13,次此设备号=EVDEV_MINOR_BASE+驱动程序本身子设备号;

    3)会在/sys/class/input类下创建驱动设备event%d,譬如:键盘驱动event1;

    4)最终会进入input_register_handle()函数来注册handle。

    int input_register_handle(struct input_handle *handle)
    {
          struct input_handler *handler = handle->handler; //handler= input_handler驱动处理结构体 
    
          list_add_tail(&handle->d_node, &handle->dev->h_list); //(1)
          list_add_tail(&handle->h_node, &handler->h_list);    // (2)
     
          if (handler->start)
                 handler->start(handle);
          return 0;
    }

    完成匹配后,最终结果如下:

     4.input事件处理层分析

     Linux内核中实现了几个常用的handler,包括keyboard_handler、mouse_handler、joystick_handler、event_handler。这些handler已经满足绝大部分输入设备的需求,下面以event_handler为例对input事件处理层进行分析。

    (1)event_handler初始化

    Linux内核中以模块的形式提供了event_handler,evdev_init被module_init修饰,即当event_handler模块被装载(insmod)时会调用evdev_init()函数。

    evdev_init()函数调用input_register_handler()函数向Linux内核注册event_handler,由input核心层分析可知,在input_register_handler()函数内部注册event_handler时会遍历当前的input_dev以判断是否有input设备支持event_handler,若两者匹配则调用evdev_handler.connect来建立连接。

    static struct input_handler evdev_handler = 
    {
        .event       = evdev_event,
        .connect     = evdev_connect,
        .disconnect  = evdev_disconnect,
        .fops        = &evdev_fops,
        .minor       = EVDEV_MINOR_BASE,
        .name        = "evdev",
        .id_table    = evdev_ids,                       //用以和input_dev进行匹配
    };
    
    static int __init evdev_init(void)
    {
        return input_register_handler(&evdev_handler);  //向Linux内核注册event_handler
    }
    
    module_init(evdev_init);

    (2)应用层open()input设备的过程分析

    假设在注册输入设备过程中生成/dev/input/event0设备文件,我们来跟踪打开这个设备的过程。

    Open(“/dev/input/event0”)

    1)vfs_open打开该设备文件,读出文件的inode内容,得到该设备的主设备号和次设备号;

    2)chardev_open 字符设备驱动框架的open根据主设备号得到输入子系统的input_fops操作集;

    3)进入到input_fops->open, 即input_open_file;

    static int input_open_file(struct inode *inode, struct file *file)
    {
        struct input_handler *handler;
        const struct file_operations *old_fops, *new_fops = NULL;
        int err;
      
      ... ...
    
        if (handler)
            new_fops = fops_get(handler->fops);
    
        old_fops = file->f_op;
        file->f_op = new_fops;
    
        err = new_fops->open(inode, file);
      
      ... ...
    
    }

    4)至此,进入到input_handler层。以evdev_handler为例进行分析,new_fops->open(inode, file)即evdev_open。evdev不仅关联了底层具体的input_dev,而且记录了应用层进程打开该设备的信息。之后input_dev产生的消息可以传递到evdev的client中的消息队列,便于上层读取。

    static int evdev_open(struct inode *inode, struct file *file)
    {
        struct evdev *evdev;
        struct evdev_client *client;
      
      ... ...
    
        client = kzalloc(sizeof(struct evdev_client), GFP_KERNEL);
    
        //根据当前进程产生client的名称
        snprintf(client->name, sizeof(client->name), "%s-%d",
                dev_name(&evdev->dev), task_tgid_vnr(current));
        wake_lock_init(&client->wake_lock, WAKE_LOCK_SUSPEND, client->name);
      //将代表打开该设备的进程相关的数据结构client和evdev绑定
        client->evdev = evdev;
        evdev_attach_client(evdev, client);
      //执行input_dev层的open
        error = evdev_open_device(evdev);
        ... ...
    }

    (3)应用层read()input设备的过程分析

    open获得的fd句柄对应的file_operations是evdev_handler的evdev_fops。因此read接口最终会调用到evdev_fops的read接口,即evdev_read。

    static ssize_t evdev_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos)
    {
        struct evdev_client *client = file->private_data;
        struct evdev *evdev = client->evdev;
        struct input_event event;
        int retval;
      
      //判断读取长度是否小于单个input_event的长度
        if (count < input_event_size())
            return -EINVAL;
    
      //在非阻塞情况下,若消息队列为空,则return
        if (client->head == client->tail && evdev->exist && (file->f_flags & O_NONBLOCK))
            return -EAGAIN;
      
      //等待消息队列不为空的事件
        retval = wait_event_interruptible(evdev->wait, client->head != client->tail || !evdev->exist);
        if (retval)
            return retval;
    
        if (!evdev->exist)
            return -ENODEV;
      
      //将消息队列中的消息取出,并通过input_event_to_user()返回至应用层
        while (retval + input_event_size() <= count && evdev_fetch_next_event(client, &event)) 
      {
            if (input_event_to_user(buffer + retval, &event))
                return -EFAULT;
    
            retval += input_event_size();
        }
    
        return retval;
    }

    在非阻塞情况下, 假设消息队列为空时,则应用层的进程将会睡眠,直到被唤醒再进行消息读取。此时,evdev_read进入休眠状态,等待ecdev->wait变量被唤醒。在evdev_handler.event中,即evdev_event()函数中,ecdev->wait变量被唤醒。

    static void evdev_event(struct input_handle *handle, unsigned int type, unsigned int code, int value)
    {
        ... ...
        wake_up_interruptible(&evdev->wait);
    }

    evdev_event()函数被谁调用?由Linux内核 gpio_keys_isr()函数代码可知,若底层输入设备发生输入事件,将触发硬件中断,在中断服务函数中会调用input_event上报输入事件,在input_event()函数内部调用了evdev_handler.event,即evdev_read将被唤醒。

    static irqreturn_t gpio_keys_isr(int irq, void *dev_id)
    {
       ... ...
      input_event(input, type, button->code, !!state);  //上报事件
      input_sync(input);                                //同步信号通知,表示事件发送完毕
    }
    
    void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
    {
      struct input_handle *handle;
      ... ...
    
      /* 通过input_dev ->h_list链表找到input_handle驱动处理结构体*/
      list_for_each_entry(handle, &dev->h_list, d_node)    
      if (handle->open)                        //如果input_handle之前open 过,那么这个就是我们的驱动处理结构体
          handle->handler->event(handle, type, code, value); //调用evdev_event()的.event事件函数 
    
    }

    5. input子系统的使用

    (1)分配与释放input_dev

    1)分配input_dev

    /*
    *所在文件:/kernel/drivers/input/input.c
    *参数:    
    *            无
    *返回值:    
    *            struct input_dev *:指向申请的input_dev
    */
    struct input_dev *input_allocate_device(void);

    2)释放input_dev

    /*
    *所在文件:/kernel/drivers/input/input.c
    *参数:    
    *            struct input_dev *dev:指向需要释放的input_dev
    *返回值:    
    *            无
    */
    void input_free_device(struct input_dev *dev);

    (2)初始化input_dev

    初始化一个input对象是使用input子系统编写驱动的主要工作,内核在头文件"include/uapi/linux/input.h"中规定了一些常见输入设备的常见的输入事件,这些宏和数组就是我们初始化input对象的工具。这些宏同时用在用户空间的事件解析和驱动的事件注册,可以看作是驱动和用户空间的通信协议,所以理解其中的意义十分重要。在input子系统中,每一个事件的发生都使用事件(type)->子事件(code)->值(value)三级来描述,比如,按键事件->按键F1子事件->按键F1子事件触发的值是高电平1。注意,事件和子事件和值是相辅相成的,只有注册了事件EV_KEY,才可以注册子事件BTN_0,也只有这样做才是有意义的。

    Linux内核定义的事件类型,对应事件对象的type域。每一类事件还有相应的子事件。

    #define EV_SYN            0x00  //同步类型
    #define EV_KEY            0x01  //按键事件
    #define EV_REL            0x02  //相对事件(对应鼠标)
    #define EV_ABS            0x03  //绝对事件(对应触摸屏)
    #define EV_MSC            0x04  //
    #define EV_SW             0x05
    #define EV_LED            0x11
    #define EV_SND            0x12
    #define EV_REP            0x14
    #define EV_FF             0x15
    #define EV_PWR            0x16
    #define EV_FF_STATUS      0x17
    #define EV_MAX            0x1f
    #define EV_CNT            (EV_MAX+1)

    事件位图:在Linux内核中,使用事件位图来描述输入设备所支持的事件类型或其子事件。事件位图使用unsigned long变量来描述所支持的事件类型或其子事件,每一个Bit代表一个事件类型或其子事件,该Bit为1表明支持该事件类型或其子事件。

    #define EV_CNT (EV_MAX+1)  //事件类型的最大个数
    #define KEY_CNT (KEY_MAX+1) //按键事件的子事件最大个数
    #define BITS_PER_BYTE        8
    #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))
    
    //计算需要多少个long变量来描述EV_KEY的子事件(一个Bit描述一个子事件),即计算long类型数组变量的个数
    #define BITS_TO_LONGS(nr)    DIV_ROUND_UP(nr, BITS_PER_BYTE * sizeof(long))
    
    unsigned long evbit[BITS_TO_LONGS(EV_CNT)];  //使用一个unsigned long数组来描述该输入设备的支持的事件类型,在struct input_dev中定义
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];//使用一个unsigned long数组来描述该输入设备的按键事件(EV_KEY)所支持的子事件,在struct input_dev中定义

    Linux内核中还提供了相应的工具将这些事件正确的填充到input对象中描述事件的位图中。

    //第一种
    //这种方式非常适合同时注册多个事件
    button_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL);                                             //填充该设备所支持的事件类型
    button_dev->keybit[BIT_WORD(BTN_MOUSE)] = BIT_MASK(BTN_LEFT) |BIT_MASK(BTN_RIGHT) |BIT_MASK(BTN_MIDDLE);//填充该设备的按键事件所支持的子事件
    
    
    //第二种
    //通常用于只注册一个事件
    set_bit(EV_KEY,button_dev.evbit);//填充该设备所支持的事件类型
    set_bit(BTN_0,button_dev.keybit);//填充该设备的按键事件所支持的子事件

    (3)注册与注销input_dev

    1)注册input_dev

    /*
    *所在文件:/kernel/drivers/input/input.c
    *参数:    
    *            struct input_dev *dev:指向需要注册的input_dev
    *返回值:    
    *            返回值为0,注册成功;否则,注册失败
    */
    int nput_register_device(struct input_dev *dev);

    2)注销input_dev

    /*
    *所在文件:/kernel/drivers/input/input.c
    *参数:    
    *            struct input_dev *dev:指向需要注销的input_dev
    *返回值:    
    *            无
    */
    void input_unregister_device(struct input_dev *dev);

    (4)驱动层报告事件

    1)上报指定的事件

    /*
    *所在文件:/kernel/drivers/input/input.c
    *参数:    
    *            struct input_dev *dev:指向相应的输入设备
    *            unsigned int type:事件类型
    *            unsigned int code:子事件
    *            int value:子事件的值
    *返回值:    
    *            无
    */
    void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);

    2)上报键值

    /*
    *所在文件:/kernel/include/linux/input.h
    *参数:    
    *            struct input_dev *dev:指向相应的输入设备
    *            unsigned int code:键盘事件的子事件
    *            int value:子事件的值
    *返回值:    
    *            无
    */
    static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
    {
        input_event(dev, EV_KEY, code, !!value);
    }

    3)上报绝对事件

    /*
    *所在文件:/kernel/include/linux/input.h
    *参数:    
    *            struct input_dev *dev:指向相应的输入设备
    *            unsigned int code:绝对事件的子事件
    *            int value:子事件的值
    *返回值:    
    *            无
    */
    static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
    {
        input_event(dev, EV_ABS, code, value);
    }

    4)上报相对事件

    /*
    *所在文件:/kernel/include/linux/input.h
    *参数:    
    *            struct input_dev *dev:指向相应的输入设备
    *            unsigned int code:相对事件的子事件
    *            int value:子事件的值
    *返回值:    
    *            无
    */
    static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
    {
        input_event(dev, EV_REL, code, value);
    }

    5)同步所有的上报

    /*
    *所在文件:/kernel/include/linux/input.h
    *参数:    
    *            struct input_dev *dev:指向相应的输入设备
    *返回值:    
    *            无
    */
    static inline void input_sync(struct input_dev *dev)
    {
        input_event(dev, EV_SYN, SYN_REPORT, 0);
    }

     6. input子系统应用实例

    驱动程序实例(四):按键驱动程序(platform + input子系统 + 外部中断方式)。

  • 相关阅读:
    TransportClient基于Elasticsearch6.8.6 X-PACK
    elasticsearch6.8.6配置xpack(生成密钥)
    Java8 List排序
    ssh 免密码登录自动设置脚本
    Linux grep命令用于查找文件里符合条件的字符串
    [译]如何防止elasticsearch的脑裂问题
    APScheduler定时任务使用
    storm本地python开发环境搭建
    关于python反射的getattr,我终于想通了!
    利用sqlalchemy 查询视图
  • 原文地址:https://www.cnblogs.com/linfeng-learning/p/9460459.html
Copyright © 2011-2022 走看看