当前位置: 首页 > news >正文

linux 设备驱动的分层思想

一、        概述

        

                像这样的分层设计在linux的input、RTC、MTD、I2c、SPI、tty、USB等诸多类型设备驱动中屡见不鲜,下面对这些驱动进行详细的分析。

二、        输入设备驱动

        输入设备(如按键、键盘、触摸屏、鼠标等)是典型的字符设备,其一般的工作机理是底层在按键、触摸等动作发送时产生一个中断(或驱动通过Timer定时查询),然后通过CPU通过SPI、I2c或外部存储器读取键值、坐标等数据,并将它们放入一个缓冲区,字符设备驱动管理该缓冲区,而驱动的read()接口让用户可以读取键值、坐标等数据。

        显然,在这些工作中,只是中断、读键值/坐标值是与设备相关的,而输入事件的缓冲区管理以及字符设备驱动的file_operations接口则对输入设备是通用的。基于此,内核设计了输入子系统,由核心层处理公共的工作。Linux内核输入子系统如图所示:

        

        输入核心提供了底层输入设备驱动程序所需要的API,如分配/释放一个输入设备:

         

         

        input_allocate_device()返回的是1个input_dev的结构体,此结构体用于表征1个输入设备。注册/注销输入设备用的接口如下:

         

        报告输入事件用的接口如下:             

        而对于所有的输入事件,内核都用统一都数据结构来描述,这个数据结构是input_event,如代码:    

        drivers/input/keyboard/gpio_keys.c基于input架构实现了通用的GPIO按键驱动。该驱动是基于platform_driver架构的,名为”gpio-keys“它将硬件相关的信息(如使用GPIO号,按下和抬起时的电平等)屏蔽在板文件platform_device的platform_data中,因此该驱动应用于各个处理器,具有良好的跨平台性。以下是该驱动的probe()函数。

 851 static int gpio_keys_probe(struct platform_device *pdev)852 {853     struct device *dev = &pdev->dev;854     const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);855     struct fwnode_handle *child = NULL;856     struct gpio_keys_drvdata *ddata;857     struct input_dev *input;858     int i, error;859     int wakeup = 0;860 861     if (!pdata) {862         pdata = gpio_keys_get_devtree_pdata(dev);863         if (IS_ERR(pdata))864             return PTR_ERR(pdata);865     }866 867     ddata = devm_kzalloc(dev, struct_size(ddata, data, pdata->nbuttons),868                  GFP_KERNEL);869     if (!ddata) {870         dev_err(dev, "failed to allocate state\n");871         return -ENOMEM;872     }873 874     ddata->keymap = devm_kcalloc(dev,875                      pdata->nbuttons, sizeof(ddata->keymap[0]),876                      GFP_KERNEL);877     if (!ddata->keymap)878         return -ENOMEM;879 880     input = devm_input_allocate_device(dev);881     if (!input) {882         dev_err(dev, "failed to allocate input device\n");883         return -ENOMEM;884     }885 886     ddata->pdata = pdata;887     ddata->input = input;888     mutex_init(&ddata->disable_lock);889 890     platform_set_drvdata(pdev, ddata);891     input_set_drvdata(input, ddata);892 893     input->name = pdata->name ? : pdev->name;894     input->phys = "gpio-keys/input0";895     input->dev.parent = dev;896     input->open = gpio_keys_open;897     input->close = gpio_keys_close;898 899     input->id.bustype = BUS_HOST;900     input->id.vendor = 0x0001;901     input->id.product = 0x0001;902     input->id.version = 0x0100;903 904     input->keycode = ddata->keymap;905     input->keycodesize = sizeof(ddata->keymap[0]);906     input->keycodemax = pdata->nbuttons;907 908     /* Enable auto repeat feature of Linux input subsystem */909     if (pdata->rep)910         __set_bit(EV_REP, input->evbit);911 912     for (i = 0; i < pdata->nbuttons; i++) {913         const struct gpio_keys_button *button = &pdata->buttons[i];914 915         if (!dev_get_platdata(dev)) {916             child = device_get_next_child_node(dev, child);917             if (!child) {918                 dev_err(dev,919                     "missing child device node for entry %d\n",920                     i);921                 return -EINVAL;922             }923         }924 925         error = gpio_keys_setup_key(pdev, input, ddata,926                         button, i, child);927         if (error) {928             fwnode_handle_put(child);929             return error;930         }931 932         if (button->wakeup)933             wakeup = 1;934     }935 936     fwnode_handle_put(child);937 938     error = input_register_device(input);939     if (error) {940         dev_err(dev, "Unable to register input device, error: %d\n",941             error);942         return error;943     }944 945     device_init_wakeup(dev, wakeup);946 947     return 0;948 }
 880     input = devm_input_allocate_device(dev);886     ddata->pdata = pdata;887     ddata->input = input;888     mutex_init(&ddata->disable_lock);889 890     platform_set_drvdata(pdev, ddata);891     input_set_drvdata(input, ddata);892 893     input->name = pdata->name ? : pdev->name;894     input->phys = "gpio-keys/input0";895     input->dev.parent = dev;896     input->open = gpio_keys_open;897     input->close = gpio_keys_close;898 899     input->id.bustype = BUS_HOST;900     input->id.vendor = 0x0001;901     input->id.product = 0x0001;902     input->id.version = 0x0100;903 904     input->keycode = ddata->keymap;905     input->keycodesize = sizeof(ddata->keymap[0]);906     input->keycodemax = pdata->nbuttons;907 925         error = gpio_keys_setup_key(pdev, input, ddata,926                         button, i, child);912     for (i = 0; i < pdata->nbuttons; i++) {913         const struct gpio_keys_button *button = &pdata->buttons[i];914 915         if (!dev_get_platdata(dev)) {916             child = device_get_next_child_node(dev, child);917             if (!child) {918                 dev_err(dev,919                     "missing child device node for entry %d\n",920                     i);921                 return -EINVAL;922             }923         }924 925         error = gpio_keys_setup_key(pdev, input, ddata,926                         button, i, child);927         if (error) {928             fwnode_handle_put(child);929             return error;930         }931 932         if (button->wakeup)933             wakeup = 1;934     }938     error = input_register_device(input);

        上诉代码的第880行分配了1个输入设备,第886~907行初始化了该input_dev的一些属性,第925行注册了这个输入设备。第912~934行则初始化了所用到的GPIO,第23行完成了这个输入设备的注册 。

        在注册输入设备后,底层输入设备驱动的核心工作只剩下在按键、触摸等人为动作发送时报告事件。下方展示了GPIO按键中断发送时的事件报告代码。

  1. GPIO指通用输入输出接口(General-purpose input/output)
  2. Keys表示按键设备
  3. IRQ是中断请求(Interrupt Request)的缩写
  4. ISR即中断服务程序(Interrupt Service Routine)
 469 static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)470 {471     struct gpio_button_data *bdata = dev_id;472     struct input_dev *input = bdata->input;473     unsigned long flags;474 475     BUG_ON(irq != bdata->irq);476 477     spin_lock_irqsave(&bdata->lock, flags);478 479     if (!bdata->key_pressed) {480         if (bdata->button->wakeup)481             pm_wakeup_event(bdata->input->dev.parent, 0);482 483         input_report_key(input, *bdata->code, 1);484         input_sync(input);485 486         if (!bdata->release_delay) {487             input_report_key(input, *bdata->code, 0);488             input_sync(input);489             goto out;490         }491 492         bdata->key_pressed = true;493     }494 495     if (bdata->release_delay)496         hrtimer_start(&bdata->release_timer,497                   ms_to_ktime(bdata->release_delay),498                   HRTIMER_MODE_REL_HARD);499 out:500     spin_unlock_irqrestore(&bdata->lock, flags);501     return IRQ_HANDLED;502 }
 483         input_report_key(input, *bdata->code, 1);484         input_sync(input);

        GPIO按键驱动通过input_report_key()、input_sync()这样的函数来汇报按键事件以及同步事件。从底层的GPIO按键驱动可以看出,该驱动没有任何file_operations的动作,也没有各种I/O模式,注册进入系统也用的是input_register_device()这样与input相关的API。这是由于与linux VFS接口的这一部分代码全部都在drivers/input/evdev.c中实现了。

 558 static ssize_t evdev_read(struct file *file, char __user *buffer,559               size_t count, loff_t *ppos)560 {561     struct evdev_client *client = file->private_data;562     struct evdev *evdev = client->evdev;563     struct input_event event;564     size_t read = 0;565     int error;566 567     if (count != 0 && count < input_event_size())568         return -EINVAL;569 570     for (;;) {571         if (!evdev->exist || client->revoked)572             return -ENODEV;573 574         if (client->packet_head == client->tail &&575             (file->f_flags & O_NONBLOCK))576             return -EAGAIN;577 578         /*579          * count == 0 is special - no IO is done but we check580          * for error conditions (see above).581          */582         if (count == 0)583             break;584 585         while (read + input_event_size() <= count &&586                evdev_fetch_next_event(client, &event)) {587 588             if (input_event_to_user(buffer + read, &event))589                 return -EFAULT;590 591             read += input_event_size();592         }593 594         if (read)595             break;596 597         if (!(file->f_flags & O_NONBLOCK)) {598             error = wait_event_interruptible(client->wait,599                     client->packet_head != client->tail ||600                     !evdev->exist || client->revoked);601             if (error)602                 return error;603         }604     }605 606     return read;607 }1292 static const struct file_operations evdev_fops = {
1293     .owner      = THIS_MODULE,
1294     .read       = evdev_read,
1295     .write      = evdev_write,
1296     .poll       = evdev_poll,
1297     .open       = evdev_open,
1298     .release    = evdev_release,
1299     .unlocked_ioctl = evdev_ioctl,
1300 #ifdef CONFIG_COMPAT
1301     .compat_ioctl   = evdev_ioctl_compat,
1302 #endif
1303     .fasync     = evdev_fasync,
1304     .llseek     = no_llseek,
1305 };

        上诉代码574-576行在检查出是非阻塞访问后,立即返回EAGAIM错误, 而586和598~600行都代码则处理了阻塞的睡眠情况。回过头来想,其实gpio_keys驱动里面调用的input_event()、input_sync()有间接唤醒这个等待队列evdev->wait的功能,只不过这些代码都隐藏在其内部实现里了。

 三、        RTC设备驱动

        RTC(实时钟)借助电池供电,在系统掉电都情况下依然可以正常计时。它通常还具有产生周期性中断以及闹钟(Alarm)中断的能力,是一种典型的字符设备。作为一种字符设备驱动,RTC需要有file_operations中接口函数的实现,如open()、release()、read()、poll()、ioctl()等,而典型的IOCTL包括RTC_SET_TIME、RTC_ALM_READ、RTC_ALM_SET、RTC_IRQP_SET、RTC_IRQP_READ等,这些对于所有的RTC是通用的,只用底层的具体实现是与设备相关的。

        

四、        Framebuffer设备驱动

        Framebuff(帧缓冲)是Linux系统为显示设备提供设备的一个接口,它将显示缓冲器抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。对于帧缓冲设备而言,只要在显示缓冲区中与显示点对应的区域内写入区域颜色值,对应的颜色会自动在屏幕上显示。

        下图为Linux帧缓冲设备驱动的主要结构,帧缓冲设备提供给用户空间file_operations结构体由

drivers/video/fbdev/core/fbmem.c中的file_operations提供,而特定帧缓冲设备fb_info结构的注册、注销以及其中成员的维护,尤其是fb_ops中成员函数的实现则由对应的xxxfb.c文件实现,fb_ops中的函数最终会操作LCD控制其硬件寄存器。

        多数显存的操作方法都是规范的,可以按照像素点格式的要求顺序写帧缓冲区。但是有少量LCD的显存写法可能比较特殊,这时候,在核心层drivers/video/fbdev/core/fb_chrdev.c实现的fb_write()中,实际上可以给底层提供一个重写自己的机会,如下:

 46 static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)47 {48     struct fb_info *info = file_fb_info(file);49 50     if (!info)51         return -ENODEV;52 53     if (info->state != FBINFO_STATE_RUNNING)54         return -EPERM;55 56     if (info->fbops->fb_write)57         return info->fbops->fb_write(info, buf, count, ppos);58 59     return fb_io_write(info, buf, count, ppos);60 }

        第56~57行是一个检查底层LCD有没有实现自己特殊显存写法的代码,如果有,直接调底层的;如果没有,用中间层标准的显存写法就搞定了底层的那个不特殊的LCD。

五、        终端设备驱动

        在linux系统中,终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。对于嵌入式系统而言,最普通采用的是UART(Univer Asynchronous Receiver/Transmitter)串行端口,日常生活中简称串口。

        Linux内核中tty的层次结构如图所示,它包含tty核心tty_io.c、tty线路规程n_tty.c(实现N_TTY线路规程)和tty驱动实例,tty线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这个格式化常常采用一个协议转换的形势。

        tty_io.c本身是一个标准的字符设备驱动,它对上字符设备的职责,实现file_operations成员函数。但是tty核心层对下又定义了tty_driver的架构,这样tty设备驱动的主体工作就变成了填充tty_driver结构体中的成员,实现其中的tty_operations的成员函数,而不再是去实现file_operations这一级的工作。tty_driver结构体和tty_operations的定义分别如下: 

        

        

433 struct tty_driver {
434     struct kref kref;
435     struct cdev **cdevs;
436     struct module   *owner;
437     const char  *driver_name;
438     const char  *name;
439     int name_base;
440     int major;
441     int minor_start;
442     unsigned int    num;
443     short   type;
444     short   subtype;
445     struct ktermios init_termios;
446     unsigned long   flags;
447     struct proc_dir_entry *proc_entry;
448     struct tty_driver *other;
449 
450     /*
451      * Pointer to the tty data structures
452      */
453     struct tty_struct **ttys;
454     struct tty_port **ports;
455     struct ktermios **termios;
456     void *driver_state;
457 
458     /*
459      * Driver methods
460      */
461 
462     const struct tty_operations *ops;
463     struct list_head tty_drivers;
464 } __randomize_layout;
350 struct tty_operations {
351     struct tty_struct * (*lookup)(struct tty_driver *driver,
352             struct file *filp, int idx);
353     int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
354     void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
355     int  (*open)(struct tty_struct * tty, struct file * filp);
356     void (*close)(struct tty_struct * tty, struct file * filp);
357     void (*shutdown)(struct tty_struct *tty);
358     void (*cleanup)(struct tty_struct *tty);
359     ssize_t (*write)(struct tty_struct *tty, const u8 *buf, size_t count);
360     int  (*put_char)(struct tty_struct *tty, u8 ch);
361     void (*flush_chars)(struct tty_struct *tty);
362     unsigned int (*write_room)(struct tty_struct *tty);
363     unsigned int (*chars_in_buffer)(struct tty_struct *tty);
364     int  (*ioctl)(struct tty_struct *tty,
365             unsigned int cmd, unsigned long arg);
366     long (*compat_ioctl)(struct tty_struct *tty,
367                  unsigned int cmd, unsigned long arg);
368     void (*set_termios)(struct tty_struct *tty, const struct ktermios *old);
369     void (*throttle)(struct tty_struct * tty);
370     void (*unthrottle)(struct tty_struct * tty);
371     void (*stop)(struct tty_struct *tty);
372     void (*start)(struct tty_struct *tty);
373     void (*hangup)(struct tty_struct *tty);
374     int (*break_ctl)(struct tty_struct *tty, int state);
375     void (*flush_buffer)(struct tty_struct *tty);
376     void (*set_ldisc)(struct tty_struct *tty);
377     void (*wait_until_sent)(struct tty_struct *tty, int timeout);
378     void (*send_xchar)(struct tty_struct *tty, char ch);
379     int (*tiocmget)(struct tty_struct *tty);
380     int (*tiocmset)(struct tty_struct *tty,
381             unsigned int set, unsigned int clear);
382     int (*resize)(struct tty_struct *tty, struct winsize *ws);
383     int (*get_icount)(struct tty_struct *tty,
384                 struct serial_icounter_struct *icount);
385     int  (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
386     int  (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
387     void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
388 #ifdef CONFIG_CONSOLE_POLL
389     int (*poll_init)(struct tty_driver *driver, int line, char *options);
390     int (*poll_get_char)(struct tty_driver *driver, int line);
391     void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
392 #endif
393     int (*proc_show)(struct seq_file *m, void *driver);
394 } __randomize_layout;

        tty设备发送数据的流程为:tty核心从一个用户获取将要发送给一个tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传递到tty驱动,tty驱动将数据转化为可以发送给硬件的格式。接收数据的流程为:从tty硬件接收到的数据向上交给tty驱动,接着进入tty线程驱动,再进入tty核心,在这里它被一个用户获取。 

        尽管一个特定的底层UART设备驱动完全可以遵循上述tty_driver的方法来设计,即定义tty_driver并实现tty_operations中的成员函数,但是鉴于串口之间的共性,Linux考虑在文件drivers/tty/serial/serial_core.c中实现了UART设备的通用tty驱动层(我们也可以称其为串口核心层)。这样,UART驱动的主要任务就进一步演变成了serial-core.c中定义的一组uart_xxx接口而不是tty_xxx接口,如下图所示,按照面向对象的思想,可以认为tty_driver是字符设备的泛华、serial-core是tty_driver的泛化,而具体的串口驱动又是serial-core的泛化。

         

        串口核心层又定义了新的uart_driver结构体和其操作集uart_ops。一个底层的UART驱动需要创建和通过uart_register_driver注册一个uart_driver而不是tty_driver,代码给出uart_driver的定义。

 732 struct uart_driver {733     struct module       *owner;734     const char      *driver_name;735     const char      *dev_name;736     int          major;737     int          minor;738     int          nr;739     struct console      *cons;740 741     /*742      * these are private; the low level driver should not743      * touch these; they should be initialised to NULL744      */745     struct uart_state   *state;746     struct tty_driver   *tty_driver;747 };

        uart_driver结构体在本质上是派生uart_driver结构体,因此,它的第746行也包含了一个tty_driver的结构体成员。tty_operations在UART这个层面也被进一步泛化为了uart_ops,其定义如代码如下:

 374 struct uart_ops {375     unsigned int    (*tx_empty)(struct uart_port *);376     void        (*set_mctrl)(struct uart_port *, unsigned int mctrl);377     unsigned int    (*get_mctrl)(struct uart_port *);378     void        (*stop_tx)(struct uart_port *);379     void        (*start_tx)(struct uart_port *);380     void        (*throttle)(struct uart_port *);381     void        (*unthrottle)(struct uart_port *);382     void        (*send_xchar)(struct uart_port *, char ch);383     void        (*stop_rx)(struct uart_port *);384     void        (*start_rx)(struct uart_port *);385     void        (*enable_ms)(struct uart_port *);386     void        (*break_ctl)(struct uart_port *, int ctl);387     int     (*startup)(struct uart_port *);388     void        (*shutdown)(struct uart_port *);389     void        (*flush_buffer)(struct uart_port *);390     void        (*set_termios)(struct uart_port *, struct ktermios *new,391                        const struct ktermios *old);392     void        (*set_ldisc)(struct uart_port *, struct ktermios *);393     void        (*pm)(struct uart_port *, unsigned int state,394                   unsigned int oldstate);395     const char  *(*type)(struct uart_port *);396     void        (*release_port)(struct uart_port *);397     int     (*request_port)(struct uart_port *);398     void        (*config_port)(struct uart_port *, int);399     int     (*verify_port)(struct uart_port *, struct serial_struct *);400     int     (*ioctl)(struct uart_port *, unsigned int, unsigned long);401 #ifdef CONFIG_CONSOLE_POLL402     int     (*poll_init)(struct uart_port *);403     void        (*poll_put_char)(struct uart_port *, unsigned char);404     int     (*poll_get_char)(struct uart_port *);405 #endif406 };

        由于drivers/tty/serial/serial_core.c是一个tty_driver,因此在serial_core.c中,存在一个tty_operations的实例,这个实例的成员函数会进一步调用struct uart_ops的成员函数,这样就把file_operations里的成员函数、tty_operations的成员函数和uart_ops的成员函数串起来。

六、        misc设备驱动

        由于Linux驱动倾向与分层设计,所有各个具体的设备都可以找到它归属都类型,从而套到它相应的架构里面去,并且只需要实现最底层的那 一部分。但是,也有部分类似globalmem、globalfifo的字符设备,确实不知道它属于什么类型,一般推荐大家采用miscdevice框架结构。miscdevice本质上也是字符设备,只是在miscdevice核心层的misc_init()函数中,通过register_chrdev(MISC_MAJOR,"misc",&misc_fops)注册了字符设备,而具体miscdevice实例调用misc_register()的时候又自动完成了device_create()、获取动态次设备号的动作。

        miscdevice的主设备号是固定的,MISC_MAJOR定义为10,在linux内核中,大概可以找到200多处使用miscdevice框架结构的驱动。

        miscdevice的结构体定义如下,第82行,指向了一个file_operations的结构体。miscdevice结构体内file_operations中的成员函数实际上是由dirvers/char/misc.c中的misc驱动的核心层的misc_ops成员函数间接调用的,比如misc_open()就会间接调用底层注册的miscdevice的fops->open。

 79 struct miscdevice  {80     int minor;81     const char *name;82     const struct file_operations *fops;83     struct list_head list;84     struct device *parent;85     struct device *this_device;86     const struct attribute_group **groups;87     const char *nodename;88     umode_t mode;89 };

         如果上诉代码第80行的minor为MISC_DYNAMIC_MINOR,miscdevice核心层会自动找一个空闲的次设备号,否则用minor指定的次设备号。第81行的name是设备的名称。

        miscdevice驱动的注册和注销分别用下面两个API:

        

        因此miscdevice驱动的一般结构形如:

         

        在调用misc_register(&xxx_dev)时,该函数内部会自动调用device_create(),而device_create会以xxx_dev作为drvdata参数。其次,在miscdevice核心层misc_open()函数的帮助下,在file_operations的成员函数中,xxx_dev会自动生成为file的private_data(misc_open会完成file->private_data的赋值操作)。

        如果我们用面向对象的封装思想把一个设备的属性、自旋锁、互斥体、等待队列、miscdevice等封装在一个结构体里面:

         

        在file_operations的成员函数中,就可以通过container_of()和file->private_data反推出xxx_dev的实例。  

         

七、        驱动核心层

        分析了上诉多个实例,我们可以归纳出核心层肩负的3大职责:

        1)对上提供接口。file_operations的读、写、ioctl都被中间层搞定,各种I/O模型也被处理掉了。

        2)中间层实现通用逻辑。可以被底层各种实例共享都代码都被中间层搞定,避免底层重复实现。

        3)对下定义框架。底层的驱动不再需要关心linux内核VFS的接口和各种可能的I/O模型,而只需处理与具体硬件相关的访问。

        这种分层有时候还不是两层,可以有更多层,在软件上呈现为面向对象里类继承和多态的状态。上面介绍的终端设备驱动类似下图一样的结果。

         

  

         

         

        

http://www.dtcms.com/a/333816.html

相关文章:

  • MySQL的学习笔记
  • Python 常用库速查手册
  • 小红书帖子评论的nodejs爬虫脚本
  • C++编程学习(第24天)
  • 数据结构与算法p4
  • Eclipse:关闭项目
  • 【121页PPT】锂膜产业MESERP方案规划建议(附下载方式)
  • Git、JSON、MQTT
  • ramdisk内存虚拟盘(一)——前世今生
  • 嵌入式第二十九课!!!回收子进程资源空间函数与exec函数
  • SurperSet柱状图排序失效问题解决
  • 移动板房的网络化建设
  • python中的reduce函数
  • FTP定时推拉数据思考
  • 深入理解 Python 闭包:从原理到实践
  • AI - MCP 协议(一)
  • NY232NY236美光固态闪存NY240NY241
  • Dummy步进电机驱动使用和相关问题
  • 疏老师-python训练营-Day46通道注意力(SE注意力)
  • 高通vendor app访问文件
  • 【使用三化总结大模型基础概念】
  • 淘宝/天猫店铺商品搜索利器:taobao.item_search_shop API返回值详解
  • 【秋招笔试】2025.08.15饿了么秋招机考-第一题
  • 嵌入式linux学习 -- 进程和线程
  • CIAIE 2025上海汽车内外饰展观察:从美学到功能的产业跃迁
  • Redis 启动时出现 “Bad file format reading the append only file“ 错误
  • 【万字精讲】 左枝清减·右枝丰盈:C++构筑的二叉搜索森林
  • office2016常见故障解决方法
  • 第七十一章:AI的“个性定制服务”:微调 LLM vs 微调 Diffusion 模型——谁是“魔改之王”?
  • 展览讯息易天邀您共赴第26届中国国际光电博览会