【linux驱动开发】IO模型之同步IO、异步IO、IO多路复用

文章目录

  • IO的概述
  • IO模型的实现
    • 阻塞IO
    • 非阻塞IO
    • IO多路复用
    • 信号驱动
    • 异步IO
  • 编译与测试说明

IO的概述

io,英文名称为inoputoutput,表示输入与输出。

在冯诺依曼结构计算机中,计算机由 运算器控制器存储器输入输出五部分组成,各个部分的数据流、指令流、控制流的大概流向如图所示:
在这里插入图片描述
在上图中,输入就是指鼠标键盘等设备通过计算机的输入设备向计算机内部输入信息,而输出设备是指控制器将计算机内部需要传递到计算机外部的数据通过输出设备传出,比如传出到显示器中。

一个完整的IO过程需要包含以下三步:

  • 系统调用:用户空间的应用程序向内核发起IO请求;
  • 数据准备:内核准备需要传递的数据,并且将IO设备的数据加载到内核缓冲区中;
  • 拷贝数据:操作系统拷贝数据,将内核缓冲区数据拷贝到用户进程缓冲区中。

IO模型根据实现的功能可划分为:

在这里插入图片描述

IO模型的实现

阻塞IO

阻塞IO,是指用户程序发起一个系统调用后,如果内核中数据未准备好,那么用户程序就会一直阻塞,直到内核数据准备完成。
以用户程序发起read为例,在用户发起读取系统数据后,如果内核数据未准备好,那么用户程序就会一直阻塞;反之,程序就可继续运行。如图示:
在这里插入图片描述

阻塞IO的实现

在linux驱动程序中,阻塞IO可使用等待队列实现。等待队列是linux内核实现阻塞与唤醒的内核机制,其以双向循环链表为基础结构,可借助下图来理解:
在这里插入图片描述

等待队列的使用方法

  • 步骤一:初始化等待队列队头,并将唤醒条件设置成假。
    初始化可使用宏定义DECLARE_WAIT_QUEUE_HEAD静态初始化等待队列,其宏定义原型为:
#define DECLARE_WAIT_QUEUE_HEAD(name) \
	struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

使用时直接传入队列名字即可。
初始化也可使用init_waitqueue_head宏定义动态初始化等待队列,其宏定义原型为:

#define init_waitqueue_head(wq_head)						\
	do {									\
		static struct lock_class_key __key;				\
										\
		__init_waitqueue_head((wq_head), #wq_head, &__key);		\
	} while (0)

使用时,先定义一个struct wait_queue_head类型的变量,然后传入该宏定义用于初始化。

  • 步骤二 :在需要阻塞的地方调用设置等待事件 wait_event,使进程进入休眠。

不可中断等待wait_event,让调用进程进入不可中断的睡眠状态,在等待队列里面睡眠直到condition 变成真,被内核唤醒。

#define wait_event(wq_head, condition)						\
do {										\
	might_sleep();								\
	if (condition)								\
		break;								\
	__wait_event(wq_head, condition);					\
} while (0)

第一个参数 wq: wait_queue_head_t 类型变量。第二个参数 condition : 等待条件,为假时才可以进入休眠。如果 condition 为真,则不会休眠

可中断等待wait_event_interruptible,,让调用进程进入可中断的睡眠状态,直到 ondition 变成真被内核唤醒或信号打断唤醒。


#define wait_event_interruptible(wq_head, condition)				\
({										\
	int __ret = 0;								\
	might_sleep();								\
	if (!(condition))							\
		__ret = __wait_event_interruptible(wq_head, condition);		\
	__ret;									\
})

参数wq :是指等待队列,是wait_queue_head_t 类型变量。参数condition :是等待条件。为假时才可以进入休眠。如果 condition 为真,则不会休眠。

  • 步骤三:当条件满足时,需要解除休眠,先将条件(condition=1),然后调用 wake_upwake_up_interruptible 函数唤醒等待队列中的休眠进程。

使用方法为:直接向wake_upwake_up_interruptible 传入需要唤醒的等待队列即可。

#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

x 表示要唤醒的等待队列的等待队列头。两者的区别就是wake_up唤醒所有休眠进程,而wake_up_interruptible只唤醒可中断的休眠进程。

其他函数

  • 创建等待队列项
    一般使用宏 DECLARE_WAITQUEUE(name,tsk)给当前正在运行的进程创建并初始化一个等待队列项,宏定义如下:
#define DECLARE_WAITQUEUE(name, tsk)						\
	struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)

第一个参数 name 是等待队列项的名字,第二个参数 tsk 表示此等待队列项属于哪个任务(进程),一般设置为 current。在 Linux 内核中 current相当于一个表示当前进程的全局变量。

  • 添加/删除队列
    当设备没有准备就绪(如没有可读数据)而需要进程阻塞的时候,就需要将进程对应的等
    待队列项添加到前面创建的等待队列中,只有添加到等待队列中以后进程才能进入休眠态。当
    设备可以访问时(如有可读数据),再将进程对应的等待队列项从等待队列中移除即可。

添加队列项函数add_wait_queue

  • 函数原型
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry 
  • **函数功能
    向等待队列中添加队列项。
  • 参数含义
    wq_head 表示等待队列项要加入等待队列的等待队列头。
    wq_entry 表示要加入的等待队列项

移除队列项函数add_wait_queue

  • 函数原型
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
  • **函数功能
    向等待队列中删除队列项。
  • 参数含义
    q表示等待队列项要加入等待队列的等待队列头。
    wait表示要函数的等待队列项。

驱动编写

编写驱动完成:当应用程序读取数据时,若内核数据未准备完成,需要阻塞等待内核数据准备完成。

  • 第一步:使用宏定义DECLARE_WAIT_QUEUE_HEAD静态初始化等待队列;
DECLARE_WAIT_QUEUE_HEAD(wqueue)

定义变量char myFlag保存唤醒条件,并且初始化为唤醒条件为假。

  • 第二步:在read中添加等待事件。
wait_event_interruptible(wqueue,test_dev->myFlag);
  • 第三步:在write中设置唤醒条件,并且发出唤醒信号。
	test_dev->myFlag = 1;
	wake_up_interruptible(&wqueue);

完整驱动:

#include <linux/kernel.h>
#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h>             // 系统错误文件
#include <linux/wait.h>

#define CREATE_DEVICE_NUM 1

#define KBUFFSIZE 32         // 缓冲区大小
// 设备结构体
struct device_test{
	dev_t dev_num;           //设备号
	int major;               // 主设备号
	int minor;               // 次设备号
	struct cdev cdev_test;   // cdev
	struct class *class;    // 类
	struct device *device;  // 设备
	char kbuff[KBUFFSIZE];   //缓冲区
	char myFlag;             // 标志位
};

struct device_test dev[CREATE_DEVICE_NUM]; // 定义设备
char *deviceName[] = {"mydevice1","mydevice2","mydevice3","mydevice4"}; // 设备名

DECLARE_WAIT_QUEUE_HEAD(wqueue); // 定义一个等待队列

// 打开设备函数
static int cdev_test_open(struct inode*inode,struct file*file)
{
	// 设置次设备号
	int i;
	for(i = 0;i<CREATE_DEVICE_NUM;++i)
		dev[i].minor = i;

	// 将访问的设备设置成私有数据
	file->private_data = container_of(inode->i_cdev,struct device_test,cdev_test);  
	printk("cdev_test_open is ok\n");

	return 0;
}

// 读取设备数据函数
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	struct device_test *test_dev = (struct device_test*)file->private_data;
	wait_event_interruptible(wqueue,test_dev->myFlag); // 等到标志位
	if(copy_to_user(buf,test_dev->kbuff,strlen(test_dev->kbuff)) != 0)
	{
		printk("copy_from_user error\r\n"); // 应用数据传输到内核错误
		return -1;
	}
	printk("read data from kernel:%s\r\n",test_dev->kbuff);
	return 0;
}

//向设备写入数据函数
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
	struct device_test *test_dev = (struct device_test*)file->private_data;
	if(copy_from_user(test_dev->kbuff,buf,size) != 0)
	{
		printk("copy_from_user error\r\n"); // 应用数据传输到内核错误
		return -1;
	}
	printk("write data to kernel: %s\r\n",test_dev->kbuff);

	// 唤醒等待队列
	test_dev->myFlag = 1;
	wake_up_interruptible(&wqueue);
	
	return 0;
}

// 释放设备(关闭设备)
static int cdev_test_release(struct inode *inode, struct file *file)
{
	printk("This is cdev_test_release\r\n");
	return 0;
}

/*设备操作函数,定义 file_operations 结构体类型的变量 cdev_test_fops*/
struct file_operations cdev_test_fops = {
	.owner = THIS_MODULE, //将 owner 指向本模块,避免在模块的操作正在被使用时卸载该模块
	.open = cdev_test_open, //将 open 字段指向 chrdev_open(...)函数
	.read = cdev_test_read, //将 open 字段指向 chrdev_read(...)函数
	.write = cdev_test_write, //将 open 字段指向 chrdev_write(...)函数
	.release = cdev_test_release //将 open 字段指向 chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
	/*注册字符设备驱动*/
	int ret,i,num;

	printk("------------------------------------\r\n");
	/*1 创建设备号*/
	//动态分配设备号
	ret = alloc_chrdev_region(&dev[0].dev_num, 0, CREATE_DEVICE_NUM, "alloc_name"); 
	if (ret < 0)
	{
		printk("alloc_chrdev_region error\r\n");
		goto errpr_chrdev;
	}
	for(i=0;i<CREATE_DEVICE_NUM;++i)
	{
		// 获取主从设备号
		if(i == 0)
			num=dev[i].dev_num;
		else
			num=dev[i-1].dev_num+1;
		dev[i].major = MAJOR(num);
		dev[i].minor = MINOR(num);
		printk("number:%d major:%d  minor:%d\r\n",num,dev[i].major,dev[i].minor);
		
		// 初始化cdev
		dev[i].cdev_test.owner = THIS_MODULE;
		cdev_init(&dev[i].cdev_test,&cdev_test_fops);

		// 添加cdev设备到内核
		ret = cdev_add(&dev[i].cdev_test,num,1);
		if(ret < 0)
		{
			printk("cdev_add error\r\n");
			goto error_cdev_add;
		}
		// 创建类
		dev[i].class = class_create(THIS_MODULE,deviceName[i]);
		if(IS_ERR(dev[i].class))
		{
			printk("class_create error\r\n");
			ret = PTR_ERR(dev[i].class);
			goto error_class_create;
		}

		// 创建设备
		dev[i].device = device_create(dev[i].class,NULL,num,NULL,deviceName[i]);
		if(IS_ERR(dev[i].device))
		{
			printk("device_create error\r\n");
			ret = PTR_ERR(dev[i].device);
			goto error_device_create;
		}

		// 设置标志位
		dev[i].myFlag = 0;
	}

	return 0;

// 创建设备失败
error_device_create:		
	device_destroy(dev[i].class, num);

// 创建类失败
error_class_create:
	class_destroy(dev[i].class); 

// 添加设备失败
error_cdev_add:
	unregister_chrdev_region(num, 1);

// 字符设备添加出错
errpr_chrdev:
	return ret;
}

// 注销字符设备
static void __exit chr_fops_exit(void) //驱动出口函数
{

	int i,num;
	for(i=0;i<CREATE_DEVICE_NUM;++i)
	{
		if(i == 0)
			num=dev[i].dev_num;
		else
			num=dev[i-1].dev_num+1;
		printk("number:%d \r\n",num);
		//注销设备号
		unregister_chrdev_region(num, 1);

		//删除 cdev
		cdev_del(&dev[i].cdev_test); 

		//删除设备
		device_destroy(dev[i].class, num);

		//删除类
		class_destroy(dev[i].class); 
	}
}

module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");

非阻塞IO

非阻塞IO,恰好是阻塞IO的对立面。使用非阻塞IO后,如果内核数据为准备我那成,内核不会阻塞并会返回一个err错误;若内核数据准备完成,就将该数据返回给用户程序。

这里还是以用户程序发起read操作为例,在发起read操作后,如果内核数据未准备好,那么用户程序不会阻塞,转而执行后面的程序,但是用户程序如果还想获取数据就需要间隔一定时间再次“询问”内核数据是否准备完成。这个过程用图表示如下:
在这里插入图片描述

使用非阻塞IO时,需要现在应用程序中以非阻塞 O_NONBLOCK 模式打开文件,即使用open(“/dev/xxx_dev”, O_RDWR | O_NONBLOCK)打开设备文件,这样就可与内核交换数据时以非阻塞模式实现。

驱动编写

编写驱动完成:当应用程序读取数据时,若内核数据未准备完成,则立刻返回;反之就读取数据。

与阻塞IO相比,这里需要改动的就是read函数,需要判断文件打开模式,若是O_NONBLOCK模式,并且数据还未准备好,就立刻返回。判断语句可这样写:

if((file->f_flags & O_NONBLOCK) && (test_dev->myFlag == 0))
		return -EAGAIN;

判断语句中的变量file struct file类型,其f_flags域表示文件打开的模式,而变量test_dev是自定义的 struct device_test类型,其myFlag域表示文件是否可被唤醒。
具体定义如下:

// 设备结构体
struct device_test{
	dev_t dev_num;           //设备号
	int major;               // 主设备号
	int minor;               // 次设备号
	struct cdev cdev_test;   // cdev
	struct class *class;    // 类
	struct device *device;  // 设备
	char kbuff[KBUFFSIZE];   //缓冲区
	char myFlag;             // 标志位
};

而后驱动的其他部分与阻塞IO一样即可。

IO多路复用

IO多路复用,是指同时监测若干个文件描述符是否可以执行IO操作的能力。

通常情况下使用 select()、poll()、epoll()函数实现 IO 多路复用。这里以 select 函数为例进行讲解,使用时可以对 select 传入多个描述符,并设置超时时间。当执行 select 的时候,系统会发起一个系统调用,内核会遍历检查传入的描述符是否有事件发生(如可读、可写事件)。如有,立即返回,否则进入睡眠状态,使进程进入阻塞状态,直到任何一个描述符事件产生后(或者等待超时)立刻返回。此时用户空间需要对全部描述符进行遍历,以确认具体是哪个发生了事件,这样就能使用一个进程对多个 IO 进行管理,如下图所示:
在这里插入图片描述

信号驱动

信号驱动,是指当信号与处理函数绑定后,一旦系统发出特定信号就会触发处理函数。
例如,在用户进程中,可将某个信号与处理函数绑定,而处理函数的主要功能是read,一旦系统发出特定信号,处理函数就可读取数据,如图:
在这里插入图片描述

异步IO

异步IO,是指用户进程访问内核数据时,如果内核数据未准备就绪,用户进程不会阻塞;反之,就直接将数据从内核空间拷贝到用户空间缓冲区中,然后再执行定义好的回调函数接收数据。如图所示:

在这里插入图片描述

编译与测试说明

由于本文操作未涉及任何硬件,因此我们可尝试直接使用客户机ubuntu来测试。

为了实现在客户机ubuntu中测试,首先我们需要更改Makefile文件,因为之前都是使用的交叉编译环境,如果不改就无法在客户机ubuntu中运行,只能在对应开发板中运行。

更改Makefile需要注意:

  • 首先,更改ARCH所指定的平台;
  • 其次,更改CROSS_COMPILE指定的编译工具,这里可直接接空,后续编译时会加上gcc
  • 最后,需要更改内核源码位置。
    可先试用命令uname -a查看内核版本。

然后再进入/lib/modules下指定版本的linux内核中。

最后需要将内核路径指定到上步骤选中的内核版本下的built目录下。

#!/bin/bash

# 环境变量
export ARCH=x86
export CROSS_COMPILE=

# 目标文件  此处
obj-m += file.o
# ubuntu中内核源码所在的位置
KDIR := /lib/modules/5.4.0-150-generic/build
PWD ?= $(shell pwd)

# 执行编译操作
all: file.c
	make -C $(KDIR) M=$(PWD) modules
	rm -rf *.cmd *.mod.c *.o *.symvers *.order *.mod
	
# 执行删除操作
clean:
	make -C $(KDIR) M=$(PWD) clean

# 编译测试文件
test1:test.c
	gcc test.c -o target

为了编译方便,在上述Makefile文件中,小编特意将测试文件test.c的编译命令加入Makefile文件中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/440997.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

华为云开年采购季Web及移动App上云体验,助力软件行业创新发展

随着云化、智能化浪潮的进一步深入&#xff0c;越来越多的应用软件开发商选择将核心产品从本地IDC机房搬迁到公有云上。但同时&#xff0c;软件开发商们也非常在意公有云厂商的可靠性与安全性&#xff0c;希望能够选择一家更加稳定可靠的云服务商&#xff0c;确保自身业务的连续…

MS2548,自动方向控制、半双工 RS-485 收发器,可替代MAX13487

产品简述 MS2548 是一个 5V 供电、半双工 RS-485 收发器。 芯片具有自动换向控制功能&#xff0c;可用于隔离 485 端口&#xff0c; 驱动器输入与使能信号一起配合控制芯片的状态&#xff0c; 驱动差分总线。 芯片内部集成热插拔保护和过温保护功能。接 收器输入阻抗为 1…

华为OD机试真题-测试用例执行计划

测试用例执行计划 题目描述&#xff1a; 某个产品当前迭代周期内有N个特性({F1,F2,...,FN})需要进行覆盖测试&#xff0c;每个特性都被评估了对应的优先级&#xff0c;特性使用其ID作为下标进行标识。 设计了M个测试用例({T1,T2,...,TM})&#xff0c;每个用例对应了一个覆盖特…

网络工程师笔记9

动态路由 RIP路由协议 配置简单 易于维护 适用于小型网络 周期性是30s发一次

分享关于如何解决系统设计问题的逐步框架

公司广泛采用系统设计面试&#xff0c;因为在这些面试中测试的沟通和解决问题的技能与软件工程师日常工作所需的技能相似。面试官的评估基于她如何分析一个模糊的问题以及如何逐步解决问题。测试的能力还包括她如何解释这个想法&#xff0c;与他人讨论&#xff0c;以及评估和优…

主网NFT的发布合约

1.什么是nft? NFT:Non-fungible-token 非同质化货币 2.新建suimove项目 使用sui move new 项目名命令新建sui move项目 sui move new nft_qyx项目结构如下: 3.写nft合约 module qyx123::nft{use sui::object::{Self, UID};use sui::transfer;use sui::tx_context::{Sel…

C++11_右值引用与移动语义

目录 1、左值的定义 1.1 左值引用 2、右值的定义 2.1 右值引用 3、右值与左值的使用区别 4、右值引用的意义 4.1 左值引用的短板 5、移动语义 5.1 移动构造 5.2 移动赋值 6、万能引用 6.1 右值的别名-左值化 6.2 完美转发 前言&#xff1a; 在C11之前就有了引…

143.和弦是什么?和声是什么?三和弦

内容参考于&#xff1a; 三分钟音乐社 上一个内容&#xff1a;142.音程的构唱练习 和弦的定义&#xff1a; 一个音可以把它称为单音 两个音可以把它称为音程 更多的音&#xff0c;通俗的定义上&#xff0c;三个音或者三个以上的音构成的集体就可以叫做和弦&#xff0c;这些音…

如何将虚拟机设置成固定IP

问题描述&#xff1a; 在VMware虚拟机上部署的项目ip地址和数据库ip地址发生变动&#xff0c;导致mysql,nginx,redis等无法访问&#xff0c;要改配置又特别麻烦&#xff0c;而且下次可能还会变动。 解决方法&#xff1a; 将虚拟机ip地址配置成固定ip 关闭虚拟机&#xff0c;找…

【SpringMVC】快速体验 SpringMVC接收数据 第一期

文章目录 一、SpringMVC 介绍1.1 主要作用1.2 核心组件和调用流程理解 二、快速体验三、SpringMVC接收数据3.1 访问路径设置3.1.1 精准路径匹配3.1.2 模糊路径匹配3.1.3 类和方法级别区别3.1.4 附带请求方式限制3.1.5 进阶注解 与 常见配置问题 3.2 接收参数&#xff08;重点&a…

Vulnhub内网渗透Jangow01靶场通关

详细请见个人博客 靶场下载地址。 下载下来后是 .vmdk 格式&#xff0c;vm直接导入。 M1请使用UTM进行搭建&#xff0c;教程见此。该靶场可能出现网络问题&#xff0c;解决方案见此 信息搜集 arp-scan -l # 主机发现ip为 192.168.168.15 nmap -sV -A -p- 192.168.168.15 # 端…

python的虚拟环境

python的虚拟环境可以为项目创建一个独立的环境&#xff0c;能够解决使用不同版本依赖给项目带来冲突的麻烦。创建虚拟环境的方式有很多种&#xff0c;pipenv会自动帮你管理虚拟环境和依赖文件&#xff0c;并且提供了一系列命令和选项来帮忙你实现各种依赖和环境管理相关的操作…

【动态规划.3】[IOI1994]数字三角形 Number Triangles

题目 https://www.luogu.com.cn/problem/P1216 观察下面的数字金字塔。 写一个程序来查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。 7→3→8→7→5 的路径产生了最大权值。 分析 这是一个动态规划…

VMware虚拟机

1、虚拟机介绍 虚拟机&#xff08;Virtual Machine&#xff09;是一种软件&#xff0c;可以用来模拟具有完整硬件功能的完整的计算机系统的软件&#xff0c;并且可以和主机环境隔离开&#xff0c;互不影响。也就是&#xff0c;在实体计算机中能够完成的工作都可以通过虚拟机实…

哨兵系列数据下载(哨兵2号Sentinel-2下载)

目录 一、介绍 二、哨兵二号介绍 三、数据下载 1、注册账号 2、数据下载 3、相关问题 四、数据预处理 1、大气校正 2、重采样 五、其他问题 一、介绍 哨兵&#xff0d;1卫星是全天时、全天候雷达成像任务&#xff0c;用于陆地和海洋观测&#xff0c;首颗哨兵&#xf…

Python PyQt5 多Tab demo

参考&#xff1a; https://cloud.tencent.com/developer/news/388937 importsysfromPyQt5.QtWidgetsimportQVBoxLayout,QWidget,QFormLayout,QHBoxLayout,QLineEdit,QRadioButton,QCheckBox,QLabel,QGroupBox,QApplication,QTabWidgetclassTabDemo(QTabWidget):def__init__(se…

并查集(蓝桥杯 C++ 题目 代码 注解)

目录 介绍&#xff1a; 模板&#xff1a; 题目一&#xff08;合根植物&#xff09;&#xff1a; 代码&#xff1a; 题目二&#xff08;蓝桥幼儿园&#xff09;&#xff1a; 代码&#xff1a; 题目三&#xff08;小猪存钱罐&#xff09;&#xff1a; 代码&#xff1a; …

OpenCASCADE+Qt创建建模平台

1、建模平台效果 2、三维控件OCCWidget 将V3d_View视图与控件句柄绑定即可实现3d视图嵌入Qt中&#xff0c;为了方便也可以基于QOpenGLWidget控件进行封装&#xff0c;方便嵌入各种窗体使用并自由缩放。 #ifndef OCCTWIDGET_H #define OCCTWIDGET_H#include <QWidget> #i…

云轴科技ZStack荣获证券基金行业信息技术应用创新联盟年度优秀成员奖

近日&#xff0c;由中国证监会科技监管司、上海市经济和信息化委员会及上交所理事会科技发展委员会指导&#xff0c;证券基金行业信息技术应用创新联盟&#xff08;简称信创联盟&#xff09;主办的2023年年度工作会议在上海成功举办。会议汇聚了来自监管机构、政府机构、行业侧…

继深圳后,重庆与鸿蒙展开原生应用开发合作

截至2023年底&#xff0c;开源鸿蒙开源社区已有250多家生态伙伴加入&#xff0c;开源鸿蒙项目捐赠人达35家&#xff0c;通过开源鸿蒙兼容性测评的伙伴达173个&#xff0c;累计落地230余款商用设备&#xff0c;涵盖金融、教育、智能家居、交通、数字政府、工业、医疗等各领域。 …