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

80. Linux内核定时器实验

一、Linux内核定时器原理

1.1、内核时间管理
1、Cortex-M内核使用systick作为系统定时器。
2、硬件定时器、软件定时器,原理是依靠系统定时器来驱动。
3、linux内核频率可以配置,图形化界面配置。
4、重点,HZ表示系统节拍率,
在这里插入图片描述

在这里插入图片描述

1.2、节拍率高低的缺陷

1.3、jiffies

1.4、内核定时器
1、软件定时器不像硬件定时器一样,直接给周期值。设置期满以后的时间点。
2、定时处理函数。
3、内核定时器不是周期性的,一次定时时间到了以后就会关闭,除非重新打开。

定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。 Linux 内核定时器采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。 Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。 Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件include/linux/timer.h 中,定义如下(省略掉条件编译):
在这里插入图片描述

void init_timer(struct timer_list *timer)
函数参数和返回值含义如下: timer:要初始化定时器。返回值: 没有返回值。
2、 add_timer 函数
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)函数参数和返回值含义如下: timer:要注册的定时器。
返回值: 没有返回值。
3、 del_timer 函数
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。 del_timer 函数原型如下:
int del_timer(struct timer_list * timer)函数参数和返回值含义如下: timer:要删除的定时器。
返回值: 0,定时器还没被激活; 1,定时器已经激活。
4、 del_timer_sync 函数
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除, del_timer_sync 不能使用在中断上下文中。 del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)函数参数和返回值含义如下: timer:要删除的定时器。
返回值: 0,定时器还没被激活; 1,定时器已经激活。
5、 mod_timer 函数
mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)函数参数和返回值含义如下: timer:要修改超时时间(定时值)的定时器。expires:修改后的超时时间。
返回值: 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活。
关于内核定时器常用的 API 函数就讲这些,内核定时器一般的使用流程如下所示:
在这里插入图片描述

二、编写试验驱动

1、定义一个定时器,结构体timer_list

2、应用ioctl函数-》unlocked_ioctl和compat_ioctl。

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
ioctl的命令是自己定义的,但是要符合linux规则。构建命令:
#define _IO(type,nr) /没有参数的命令
#define _IOR(type,nr,size) //该命令是从驱动读取数据
#define _IOW(type,nr,size) //该命令是向驱动写入数据
#define _IOWR(type,nr,size) //双向数据传输
type是幻数,nr是序号,size是大小。

//Makefile

KERNELDIR := /home/mk/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)obj-m := timer.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

//timer.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
文件名     : timer.c
版本      : V1.0
描述      : Linux内核定时器实验
其他      : 无
***************************************************************/
#define TIMER_CNT       1       /* 设备号个数    */
#define TIMER_NAME      "timer" /* 名字       */
#define CLOSE_CMD       (_IO(0XEF, 0x1))    /* 关闭定时器 */
#define OPEN_CMD        (_IO(0XEF, 0x2))    /* 打开定时器 */
#define SETPERIOD_CMD   (_IO(0XEF, 0x3))    /* 设置定时器周期命令 */
#define LEDON           1       /* 开灯 */
#define LEDOFF          0       /* 关灯 *//* timer设备结构体 */
struct timer_dev{dev_t devid;            /* 设备号   */struct cdev cdev;       /* cdev     */struct class *class;    /* 类        */struct device *device;  /* 设备    */int major;              /* 主设备号   */int minor;              /* 次设备号   */struct device_node  *nd; /* 设备节点 */int led_gpio;           /* key所使用的GPIO编号        */int timeperiod;         /* 定时周期,单位为ms */struct timer_list timer;/* 定义一个定时器*/spinlock_t lock;        /* 定义自旋锁 */
};struct timer_dev timerdev;  /* timer设备 *//** @description : 初始化LED灯IO,open函数打开驱动的时候*                初始化LED灯所使用的GPIO引脚。* @param       : 无* @return      : 无*/
static int led_init(void)
{int ret = 0;timerdev.nd = of_find_node_by_path("/gpioled");if (timerdev.nd== NULL) {return -EINVAL;}timerdev.led_gpio = of_get_named_gpio(timerdev.nd ,"led-gpio", 0);if (timerdev.led_gpio < 0) {printk("can't get led\r\n");return -EINVAL;}/* 初始化led所使用的IO */gpio_request(timerdev.led_gpio, "led");     /* 请求IO     */ret = gpio_direction_output(timerdev.led_gpio, 1);if(ret < 0) {printk("can't set gpio!\r\n");}return 0;
}/** @description     : 打开设备* @param - inode   : 传递给驱动的inode* @param - filp    : 设备文件,file结构体有个叫做private_data的成员变量*                    一般在open的时候将private_data指向设备结构体。* @return          : 0 成功;其他 失败*/
static int timer_open(struct inode *inode, struct file *filp)
{int ret = 0;filp->private_data = &timerdev; /* 设置私有数据 */timerdev.timeperiod = 1000;     /* 默认周期为1s */ret = led_init();               /* 初始化LED IO */if (ret < 0) {return ret;}return 0;
}/** @description     : ioctl函数,* @param - filp    : 要打开的设备文件(文件描述符)* @param - cmd     : 应用程序发送过来的命令* @param - arg     : 参数* @return          : 0 成功;其他 失败*/
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{struct timer_dev *dev =  (struct timer_dev *)filp->private_data;int timerperiod;unsigned long flags;switch (cmd) {case CLOSE_CMD:     /* 关闭定时器 */del_timer_sync(&dev->timer);break;case OPEN_CMD:      /* 打开定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));break;case SETPERIOD_CMD: /* 设置定时器周期 */spin_lock_irqsave(&dev->lock, flags);dev->timeperiod = arg;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));break;default:break;}return 0;
}/* 设备操作函数 */
static struct file_operations timer_fops = {.owner = THIS_MODULE,.open = timer_open,.unlocked_ioctl = timer_unlocked_ioctl,
};/* 定时器回调函数 */
void timer_function(unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)arg;static int sta = 1;int timerperiod;unsigned long flags;sta = !sta;     /* 每次都取反,实现LED灯反转 */gpio_set_value(dev->led_gpio, sta);/* 重启定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); }/** @description : 驱动入口函数* @param       : 无* @return      : 无*/
static int __init timer_init(void)
{/* 初始化自旋锁 */spin_lock_init(&timerdev.lock);/* 注册字符设备驱动 *//* 1、创建设备号 */if (timerdev.major) {       /*  定义了设备号 */timerdev.devid = MKDEV(timerdev.major, 0);register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);} else {                        /* 没有定义设备号 */alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME); /* 申请设备号 */timerdev.major = MAJOR(timerdev.devid); /* 获取分配号的主设备号 */timerdev.minor = MINOR(timerdev.devid); /* 获取分配号的次设备号 */}/* 2、初始化cdev */timerdev.cdev.owner = THIS_MODULE;cdev_init(&timerdev.cdev, &timer_fops);/* 3、添加一个cdev */cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);/* 4、创建类 */timerdev.class = class_create(THIS_MODULE, TIMER_NAME);if (IS_ERR(timerdev.class)) {return PTR_ERR(timerdev.class);}/* 5、创建设备 */timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);if (IS_ERR(timerdev.device)) {return PTR_ERR(timerdev.device);}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */init_timer(&timerdev.timer);timerdev.timer.function = timer_function;timerdev.timer.data = (unsigned long)&timerdev;return 0;
}/** @description : 驱动出口函数* @param       : 无* @return      : 无*/
static void __exit timer_exit(void)
{gpio_set_value(timerdev.led_gpio, 1);   /* 卸载驱动的时候关闭LED */del_timer_sync(&timerdev.timer);        /* 删除timer */
#if 0del_timer(&timerdev.tiemr);
#endif/* 注销字符设备驱动 */cdev_del(&timerdev.cdev);/*  删除cdev */unregister_chrdev_region(timerdev.devid, TIMER_CNT); /* 注销设备号 */device_destroy(timerdev.class, timerdev.devid);class_destroy(timerdev.class);
}module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("mk");

//timerApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
/***************************************************************
文件名     : timerApp.c
版本      : V1.0
描述      : 定时器测试应用程序
其他      : 无
使用方法    :./timertest /dev/timer 打开测试App
***************************************************************//* 命令值 */
#define CLOSE_CMD       (_IO(0XEF, 0x1))    /* 关闭定时器 */
#define OPEN_CMD        (_IO(0XEF, 0x2))    /* 打开定时器 */
#define SETPERIOD_CMD   (_IO(0XEF, 0x3))    /* 设置定时器周期命令 *//** @description     : main主程序* @param - argc    : argv数组元素个数* @param - argv    : 具体参数* @return          : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd, ret;char *filename;unsigned int cmd;unsigned int arg;unsigned char str[100];if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0) {printf("Can't open file %s\r\n", filename);return -1;}while (1) {printf("Input CMD:");ret = scanf("%d", &cmd);if (ret != 1) {             /* 参数输入错误 */gets(str);              /* 防止卡死 */}if(cmd == 1)                /* 关闭LED灯 */cmd = CLOSE_CMD;else if(cmd == 2)           /* 打开LED灯 */cmd = OPEN_CMD;else if(cmd == 3) {cmd = SETPERIOD_CMD;    /* 设置周期值 */printf("Input Timer Period:");ret = scanf("%d", &arg);if (ret != 1) {         /* 参数输入错误 */gets(str);          /* 防止卡死 */}}ioctl(fd, cmd, arg);        /* 控制定时器的打开和关闭 */   }close(fd);
}

三、测试

输入如下命令编译出驱动模块文件: make -j32
编译成功以后就会生成一个名为“timer.ko”的驱动模块文件。
输入如下命令编译测试 timerApp.c 这个测试程序:
arm-linux-gnueabihf-gcc timerApp.c -o timerApp编译成功以后就会生成 timerApp 这个应用程序。
在这里插入图片描述

相关文章:

80. Linux内核定时器实验

一、Linux内核定时器原理 1.1、内核时间管理 1、Cortex-M内核使用systick作为系统定时器。 2、硬件定时器、软件定时器&#xff0c;原理是依靠系统定时器来驱动。 3、linux内核频率可以配置&#xff0c;图形化界面配置。 4、重点&#xff0c;HZ表示系统节拍率&#xff0c; 1.…...

Java 可变参数全解析:动态参数传递的实践指南

Java 可变参数全解析&#xff1a;动态参数传递的实践指南 一、可变参数&#xff1a;Java 方法的灵活扩展 在狂神说 Java 第 49 集课程中&#xff0c;我们系统学习了 Java 可变参数的核心原理。作为 Java SE 5 引入的重要特性&#xff0c;可变参数允许方法接受动态数量的输入&…...

C++类与对象(上):从入门到实践

目录 一、引言 二、面向过程和面向对象初步认识 2.1 面向过程编程 2.2 面向对象编程 三、类的引入 四、类的定义 4.1 定义格式 4.2 定义方式 4.3 成员变量命名规则建议 五、类的访问限定符及封装 5.1 访问限定符 5.2 封装 六、类的作用域 七、类的实例化 7.1 概念…...

Lumerical ------ Edge coupler design

Lumerical ------ Edge coupler design 引言正文无 Si Substrate 的仿真步骤有 Si Substrate 的仿真步骤引言 本文,我们将使用官方提供的 Edge coupler 设计教程,但是中间会带有作者本人的设计的感悟。 正文 无 Si Substrate 的仿真步骤 打开 Edge_Coupler_No_Substrate.l…...

大语言模型本质上还是自动化,而不是智能化

大语言模型本质上仍然是自动化或高级自动化&#xff0c;而非真正的智能化&#xff0c;原因可以从以下几个方面进行分析&#xff1a;1、自动化与智能化的本质区别自动化&#xff1a;大语言模型通过预训练和微调&#xff0c;基于大量数据和规则生成输出。它的行为是基于输入数据的…...

cmake 中的命令

命令描述示例capabilities报告 CMake 内置的功能&#xff0c;以 JSON 格式输出cmake -E capabilitiescat连接文件并将其内容打印到标准输出cmake -E cat file1.txt file2.txtchdir在指定目录中运行命令cmake -E chdir /path/to/dir commandcompare_files比较两个文件是否相同cm…...

python数据结构——链表、栈、队列

一、思维梳理&#xff1a; 二、双向循环链表&#xff1a; class Node:def __init__(self,data):self.data dataself.next Noneself.prev Noneclass DoubleLink:def __init__(self):self.size 0self.head Nonedef is_empty(self):return self.size 0def add_end(self,dat…...

centos操作系统如何更换yum镜像源

CentOS Linux 是一个免费提供的、社区支持的Linux发行版,由CentOS项目社区贡献者开发、分发和维护。2020年CentOS项目宣布将把全部投资转移到CentOS Stream,作为即将发布的 Red Hat Enterprise Linux版本的上游开发平台。因此,CentOS Linux更新和发布将在2021年至2024年期间…...

【Linux篇】自主Shell命令行解释器

&#x1f4cc; 个人主页&#xff1a; 孙同学_ &#x1f527; 文章专栏&#xff1a;Liunx &#x1f4a1; 关注我&#xff0c;分享经验&#xff0c;助你少走弯路&#xff01; 文章目录 1. 获取用户名的接口2. 等待用户输入接口3. 将上述代码进行面向对象式的封装4. 命令行解析5.…...

Tomcat 部署 Jenkins.war 详细教程(含常见问题解决)

在Tomcat中部署Jenkins.war文件是一个相对简单的过程&#xff0c;以下是详细步骤&#xff1a; 1. 准备工作 确保已安装JDK&#xff1a;Jenkins需要Java环境&#xff0c;建议安装JDK 8或更高版本。 下载Jenkins.war&#xff1a;https://pan.quark.cn/s/c4fd7711a1b3 下载Tomc…...

我的创作纪念日-一周年

目录 机缘 收获 日常 成就 憧憬 机缘 时光荏苒&#xff0c;转行计算机已经是第5个年头了。从Python入门&#xff0c;到C入土&#xff0c;兜兜转转&#xff0c;发现自己也只是初窥门径&#xff0c;习得皮毛。我从6年前开始潜水CSDN&#xff0c;学习各路大佬的技术经验&…...

多线程代码案例 - 1

目录 单例模式 1. 饿汉模式 2. 懒汉模式 单例模式与多线程 问题1 问题2 问题3 完&#xff01; 单例模式 单例模式是一种设计模式。 设计模式&#xff0c;是我们在编写代码时候的一种软性的规定&#xff0c;也就是说&#xff0c;我们遵守设计模式&#xff0c;代码的下限…...

display:none与visibility的区别

1. 是否占据空间 display: none&#xff1a;元素完全从文档流中移除&#xff0c;不占据任何布局空间。后续元素会“填补”它的位置。visibility:hidden&#xff1a;元素仍占据布局空间&#xff0c;但内容不可见&#xff08;类似透明占位符&#xff09;。 2.渲染与性能 displ…...

算法 | 基于蜣螂优化算法求解带时间窗的车辆路径问题(VRPTW)研究(附matlab代码)

基于蜣螂优化算法求解带时间窗的车辆路径问题(VRPTW)研究 🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏🍏 摘要 带时间窗的车辆路径问题(VRPTW)是物流配送中的核心优化难题。本文提出一种基于蜣螂优化算法(Dung Beetle Optimizer, DBO)的求解方法,通过…...

开发体育赛事直播系统主播认证功能技术实现方案

该体育直播系统系统由东莞梦幻网络科技开发&#xff0c;使用 ThinkPHP 作为后端&#xff0c;Vue.js 作为 PC/H5 端框架&#xff0c;Java 和 Objective-C 分别用于安卓和 iOS 开发。 1、前端实现 (Vue.js) <template><div class"anchor-certification">…...

国产三维CAD「皇冠CAD」在汽车零部件领域建模教程:刹车片

本教程深度融合三维皇冠CAD&#xff08;CrownCAD&#xff09;的MBD&#xff08;Model-Based Definition&#xff09;设计理念&#xff0c;通过参数化建模、智能约束管理、动态装配验证等功能&#xff0c;实现数据驱动设计&#xff0c;精准解决了汽车制动系统中精密制动组件的设…...

基于指针的线程池

使用原线程池 当 push 和 pop的对象过大时&#xff0c;消耗时延过高&#xff0c;需优化线程池 采用 std::move&#xff08;&#xff09; unique_ptr的方法&#xff0c;能极大的减少时延&#xff0c; 实际就是避免了多次拷贝&#xff0c;直接使用指针。 代码实现 ThreadPool…...

GitHub与Gitee各是什么?它们的区别与联系是什么?

李升伟 整理 GitHub 介绍 GitHub 是一个基于 Git 的代码托管平台&#xff0c;主要用于版本控制和协作开发。它支持多人协作&#xff0c;提供代码托管、问题跟踪、代码审查、项目管理等功能。GitHub 是全球最大的开源社区&#xff0c;许多知名开源项目都在此托管。 主要功能&…...

SpringMvc获取请求数据

基本参数 RequestMapping("save5") ResponseBody public User save5(String name, int age) {User user new User();user.setName(name);user.setAge(age);return user; } 在url中将name与age进行编写&#xff0c;通过框架可以提取url中的name与age&#xff0c;这…...

简述竞赛经历在考研复试中的作用

文章目录 前言拟录取情况baichuicxyAshy佬齐总结回首展望 前言 随着就业形式的不断变化&#xff0c;竞赛奖项在就业中能起到的作用在逐步减弱。想拿到头部大厂实习面试资格&#xff0c;最低要区域赛银牌起步&#xff0c;当然这也仅仅是面试资格。 那么&#xff0c;竞赛经历在…...

大语言模型开发框架——LangChain

什么是LangChain LangChain是一个开发由语言模型驱动的应用程序的框架&#xff0c;它提供了一套工具、组件和接口&#xff0c;可以简化构建高级语言模型应用程序的过程。利用LangChain可以使应用程序具备两个能力&#xff1a; 上下文感知 将语言模型与上下文&#xff08;提示…...

Nginx 核心配置详解与性能优化最佳实践

1.什么是 Nginx&#xff1f; Nginx 是一个高性能的 Web 服务器和反向代理服务器。它轻量、高效&#xff0c;被广泛用于现代 Web 开发中。 2.为什么前端需要了解 Nginx&#xff1f; ★ 了解 本地开发&#xff1a;可以模拟生产环境 部署前端项目&#xff1a;作为静态文件服务器…...

机器学习的一百个概念(7)独热编码

前言 本文隶属于专栏《机器学习的一百个概念》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见[《机器学习的一百个概念》 ima 知识库 知识库广场搜索&…...

用LLama factory时报类似Process 2504721 got signal: 1的解决方法

之前用nohup来远程跑LLama factory微调脚本&#xff0c;是没有问题的&#xff0c;但今天发现运行类似下面这个命令时&#xff0c; nohup llamafactory-cli train examples/train_qlora/qwen_lora.yaml 只要一关闭ssh session&#xff0c;就会终止训练&#xff0c;报类似&…...

解决 Git 通过 SSH 克隆仓库时自动转换为 HTTPS 的问题

解决 Git 通过 SSH 克隆仓库时自动转换为 HTTPS 的问题 在使用 Git 通过 SSH 协议克隆私有仓库时&#xff0c;如果遇到类似以下错误&#xff1a; fatal: unable to access https://itlab.stack.net:stack.git/: Could not resolve host: gitlab.stack这通常是因为 Git 配置错…...

从实用的角度聊聊Linux下文本编辑器VIM

本文从实用的角度聊聊Vim的常用命令。何为实用&#xff1f;我举个不实用的例子大家就明白了&#xff0c;用vim写代码。;) “vim是从 vi 发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富&#xff0c;在程序员中被广泛使用&#xff0c;和Emacs并列成…...

多电机显示并排序

多电机显示并排序 要实现根据后端传递过来的驱动电机数据的数量来显示不同数量的数据列表&#xff0c;我们可以使用 Vue 的 v-for 指令来遍历 driveMotorData 数组&#xff0c;并为每个驱动电机生成一个数据列表。这样&#xff0c;无论后端传来多少个驱动电机的数据&#xff0…...

佳能imageRUNNER 2206N基本参数及管理员密码

基本参数&#xff1a; 产品类型 激光数码复合机 颜色类型 黑白 涵盖功能 复印/打印/扫描 速度类型 低速 最大原稿尺寸 A3 复印/打印方式 激光静电转印方式 感光材料 OPC 显影系统 干式单组分显影 定影…...

社交类 APP 设计:打造高用户粘性的界面

在当今数字化时代&#xff0c;社交类APP已成为人们日常生活中不可或缺的一部分。然而&#xff0c;随着市场竞争的加剧&#xff0c;如何通过设计提升用户粘性成为社交类APP成功的关键。本文将从设计的关键要素、用户界面优化、功能创新、个性化体验以及持续优化等方面&#xff0…...

数据编排与Dagster:解锁现代数据管理的核心工具

在数据驱动的时代&#xff0c;如何高效管理复杂的数据管道、确保数据质量并实现团队协作&#xff1f;本文深入探讨数据编排的核心概念&#xff0c;解析其与传统编排器的差异&#xff0c;并聚焦开源工具Dagster如何以“资产为中心”的理念革新数据开发流程&#xff0c;助力企业构…...