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

itop-3568开发板驱动学习笔记(8)高级字符设备(二)IO 多路复用

《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记

文章目录

  • IO 多路复用简介
  • IO 多路复用实验
    • poll 机制
    • select 机制
    • epoll 机制

IO 多路复用简介

IO 多路复用可以实现一个进程监控多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件操作符就绪时就会阻塞应用程序,从而释放出 CPU 资源。

——原文

Linux 提供了三种实现 IO 多路复用的模型,分别是 select、poll 和 epoll,它们功能本质上相同:监听多个文件描述符,当指定的事件(如读、写事件)发生时,就通知线程事件到达。

IO 多路复用实验

poll 机制

poll()接受一个指向结构’struct pollfd’列表的指针,其中包括了你想测试的文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前的结构在调用后将被填写并在事件发生后返回。在SVR4(可能更早的一些版本)中的 "poll.h"文件中包含了用于确定事件的一些宏定义。事件的等待时间精确到毫秒 (但令人困惑的是等待时间的类型却是int),当等待时间为0时,poll()函数立即返回,-1则使poll()一直挂起直到一个指定事件发生。

——百度百科

(应用层)poll 函数原型:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds 为要监听的文件描述符集合,是一个数组,该结构体定义见下文;nfds 为要监听的文件描述符数量;timeout 为超时时间,单位为 ms,timeout 大于 0,poll 等待时间直到指定的时间,timeout 为 0,poll 立即返回,timeout 为 -1,一直等待,直到时间发生。
poll 返回值小于等于 0 表示没有任何事件发生,大于 0 时为事件发生个数。

poll 结构体定义:

struct pollfd {
	int fd;
	short events;  //事件
	short revents; //返回事件
}

poll 支持的事件类型:

事件名说明
POLLIN普通数据可读
POLLRDNORM普通数据可读
POLLPRI高优先级数据可读
POLLOUT普通数据可写
POLLWRNORM普通数据可写
POLLERR发生错误
POLLHUP发生挂起
POLLNVAL描述符不是一个打开的文件

驱动层 poll 函数定义:

unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);

wait 对应用户层 poll 函数的 timeout 参数,在驱动中调用的休眠函数为 poll_wait(),wait 便是 poll_wait() 的一个参数。

该函数的返回值就是应用层 struct pollfd 的 revents 成员。

poll_wait() 定义如下,主要用于 poll 驱动函数中的休眠操作,

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

该函数的第二个参数 wait_queue_head_t 为上篇笔记提到的等待队列头,即 poll_wait 依靠等待队列来实现阻塞线程的操作。

实验程序

驱动核心代码

在上一份驱动代码的基础上添加 chrdev_poll() 函数,当驱动运行该函数时,调用 poll_wait() 函数,让线程进入阻塞,同时设定超时时间(由用户层程序决定超时时间),当等待队列 my_wait_queue 被唤醒或到达超时时间, poll 继续往下运行,如果驱动数据已经准备好(tmp_dev->condition 变为 1),则返回 POLLIN,表示数据可读。

// poll
static unsigned int chrdev_poll(struct file *file, struct poll_table_struct *wait)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	poll_wait(file, &my_wait_queue, wait); // 阻塞
	
	if(tmp_dev->condition == 1)
	{
		return POLLIN; // 返回事件类型
	}
	return 0;
}

static struct file_operations chrdev_fops = {
	.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = chrdev_open,  //将 open 成员指向 chrdev_open()函数
	.read = chrdev_read,  //将 read 成员指向 chrdev_read()函数
	.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
	.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
	.poll = chrdev_poll,  //将 poll 字段指向 chrdev_poll()函数
};

完整驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/wait.h>
#include <linux/poll.h>

// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue); 

// 定义一个私有数据结构体
struct my_device
{
	dev_t dev_num; // 设备号
	int major;     // 主设备号
	int minor;     // 次设备号
	struct cdev st_cdev;
	struct class *st_class;
	struct device *st_device;
	char kbuf[32];
	int condition; // 条件标志
};

// 定义一个全局私有数据结构体
struct my_device dev1;

// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
	file->private_data = &dev1; // 设置私有数据
	printk("chrdev_open.\n");
	return 0;
}

// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
	printk("chrdev_release.\n");
	return 0;
}

// read()
static  ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;
	int ret = 0;
	
	// 如果 open() 的 flags 参数带 O_NONBLOCK
	if(file->f_flags& O_NONBLOCK)
	{
		// 如果数据没就绪,直接退出 read()
		if(tmp_dev->condition == 0)
			return -EAGAIN;
	}
	
	// 可中断的阻塞等待,进程进入休眠状态
	wait_event_interruptible(my_wait_queue, tmp_dev->condition); 
	tmp_dev->condition = 0; // 条件标志复位
	
	// 向应用空间拷贝数据
	ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf)); 
	if(ret != 0)
	{
		printk("copy_to_user error.\r\n");
		return -1;
	}
	
	printk("chrdev_read.\n");
	return 0;
}

// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
	if(ret != 0)
	{
		printk("copy_from_user error.\r\n");
		return -1;
	}

	tmp_dev->condition = 1; // 将条件置 1
	wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
	return 0;
}

// poll
static unsigned int chrdev_poll(struct file *file, struct poll_table_struct *wait)
{
	struct my_device *tmp_dev = (struct my_device*)file->private_data;

	poll_wait(file, &my_wait_queue, wait); // 阻塞
	
	if(tmp_dev->condition == 1)
	{
		return POLLIN; // 返回事件类型
	}
	return 0;
}

static struct file_operations chrdev_fops = {
	.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = chrdev_open,  //将 open 成员指向 chrdev_open()函数
	.read = chrdev_read,  //将 read 成员指向 chrdev_read()函数
	.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
	.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
	.poll = chrdev_poll,  //将 poll 字段指向 chrdev_poll()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
	int ret;

	// 自动获取设备号(只申请一个,次设备号从 0 开始)
	ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
	if(ret < 0)
	{
		goto err_alloc;
	}
	printk("alloc chrdev region successfully.\n");
	dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
	dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
	printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
	
	dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
	cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备	
	ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
	if(ret < 0)
	{
		goto err_cdev_add;
	}
	printk("cdev add successfully.\n");
	
	dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
	if(IS_ERR(dev1.st_class))
	{
		ret = PTR_ERR(dev1.st_class); // 返回错误码
		goto err_class_create;
	}
	
	dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
	if(IS_ERR(dev1.st_device))
	{
		ret = PTR_ERR(dev1.st_device); // 返回错误码
		goto err_device_create;
	}

	
	return 0;

err_device_create:
	class_destroy(dev1.st_class); // 删除类

err_class_create:
	cdev_del(&dev1.st_cdev); // 删除 cdev

err_cdev_add:
	unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号

err_alloc:
	return ret; // 返回错误号

}

// 驱动出口函数
static void __exit chrdev_exit(void)
{
	device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
	class_destroy(dev1.st_class); //删除设备类
	cdev_del(&dev1.st_cdev); // 删除字符设备
	unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
	
	printk("chrdev_exit.\n");
}

module_init(chrdev_init);  //注册入口函数
module_exit(chrdev_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");  //同意GPL协议
MODULE_AUTHOR("xiaohui");  //作者信息

“读”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>

#define DEV_FILE "/dev/chrdev_device"

int main(int argc, char** argv)
{
	int fd, tmp;
	int ret = 0;
	char buf[32] = {0};
	struct pollfd fds[1];
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);

	//初始化 fbs[0]
	fds[0].fd = fd;
	fds[0].events = POLLIN; // 事件类型为可读事件
	
	// 读数据
	printf("app will read data.\n");
	while(1)
	{
		ret = poll(fds, 1, 5000); // 监听文件是否有事件发生
		if(ret <= 0)
		{
			printf("poll wait timeout.\n");
		}
		else if(fds[0].revents == POLLIN) // 如果返回事件为数据可读取
		{
			ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
			if(ret == 0)
				printf("app read data successfully\ndata: %s\n", buf);
			else
				printf("app read data failed.\n");
			sleep(3);	
		}
	}	
	
	// 关闭设备文件
	close(fd);
	return 0;
}

“写”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEV_FILE "/dev/chrdev_device"

int main(int argc, char** argv)
{
	int fd, tmp;
	int ret = 0;
	char buf[32] = "poll test";
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);
	
	// 读数据
	printf("app will write data.\n");
	ret = write(fd, buf, sizeof(buf)); // 向设备文件写数据
	if(ret == 0)
		printf("app write data successfully\n");
	else
		printf("app write data failed.\n");
	// 关闭设备文件
	close(fd);
	return 0;
}

Makefile 文件

由于我打算在 X86 平台测试,所我屏蔽了交叉编译和平台的平台的环境变量,内核目录改为本机内核地址,

#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o

#架构平台选择
#export ARCH=arm64

#编译器选择
#export CROSS_COMPILE=aarch64-linux-gnu-

#内核目录
#KDIR := /home/topeet/Linux/rk356x_linux/kernel/
KDIR := /lib/modules/$(shell uname -r)/build

#编译模块
all:
	make -C $(KDIR) M=$(shell pwd) modules
	$(CROSS_COMPILE)gcc read.c -o read
	$(CROSS_COMPILE)gcc write.c -o write

#清除编译文件
clean:
	make -C $(KDIR) M=$(shell pwd) clean
	rm read write

实验结果

先在后台运行 read,read 先通过 poll 监听设备文件是否有数据可读,每次监听最长等待 5 秒,超过 5 秒没有等到 POLLIN 事件,打印超时提示,再次循环监听,直到 POLLIN 事件发生(调用 write() 向驱动写数据后,数据就绪,poll 返回 POLLIN)。

在这里插入图片描述

select 机制

select 与 poll 机制类似,都是轮询监听,但 select 最大的文件描述符个数是有限制的,默认为 1024,而 poll 没有这个限制。

select 函数接口

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数:

  • nfds: 被监控的三类文件描述符集合中最大的文件描述符 + 1
  • readfds: 读事件文件描述符集合
  • writefds: 写事件文件描述符集合
  • exceptfds: 异常文件描述符集合
  • timeout: 超时时间,为 NULL 时, select() 进入阻塞状态,一直等待,直到有事件发生;超时时间为 0 时,不阻塞,立刻返回;超时时间大于 0,select() 便会在超时时间内监测事件是否发生,超时后则退出 select()。

返回值:
返回值等于 0 表示没有任何事件发生,大于 0 时为事件发生个数,小于 0 表示出错。

struct timeval 结构体

struct timeval{
	long  tv_sec;  //秒
	long  tv_usec; //毫秒
};

文件描述符集合的操作:

void FD_CLR(int fd, fd_set *set);   // 将 set 中与 fd 对应的标志位清除
int FD_ISSET(int fd, fd_set *set);  // 判断 set 里与 fd 对应的标志是否为 1
void FD_SET(int fd, fd_set *set);   // 把 set 里的与 fd 对应的标志位置为 1
void FD_ZERO(fd_set *set);          // 把 set 里所有文件描述符标志位置 0

【注意】:调用完 select() ,需要重新给 timeout 赋值(向下计数,计数值到 0 表示超时),同时也要调用 FD_ZERO() 将 3 种文件描述符数组清空。

实验程序

驱动文件、“写”程序、Makefile 都和 poll 实验相同,只有 “读“程序(监听程序)不同,

“读”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>

#define DEV_FILE "/dev/chrdev_device"
#define FDS_NUM  5

int main(int argc, char** argv)
{
	int fd, tmp, i;
	int ret = 0;
	char buf[32] = {0};
	int fds[FDS_NUM] = {0};
	int max_fd = 0;
	fd_set readfds;
	fd_set writefds;
	fd_set errorfds;
	struct timeval timeout;
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);

	// 初始化 fbs[0]
	fds[0] = fd;

	// 获取最大 fd 
	for(i = 0; i < FDS_NUM; i++)
	{
		max_fd = fds[i] > max_fd ? fds[i] : max_fd;
	}
	
	// 读数据
	printf("app will read data.\n");
	while(1)
	{
		// 将 fds[0] 添加到文件描述符集合
		FD_SET(fds[0], &readfds);
		FD_SET(fds[0], &writefds);
		FD_SET(fds[0], &errorfds);

		// 赋值超时时间
		timeout.tv_sec = 3;
		timeout.tv_usec = 1000;

		// 监听文件是否有事件发生
		ret = select(max_fd + 1, &readfds, &writefds, &errorfds, &timeout);
		if(ret <= 0)
		{
			printf("select wait timeout.\n");
		}
		else if(FD_ISSET(fds[0], &readfds)) // 如果返回事件为可读
		{
			ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
			if(ret == 0)
				printf("app read data successfully\ndata: %s\n", buf);
			else
				printf("app read data failed.\n");
			sleep(3);	
		}
		else if(FD_ISSET(fds[0], &errorfds)) // 如果返回事件为异常事件
		{
			printf("excepted event.\n");
		}
		else if(FD_ISSET(fds[0], &writefds)) // 如果返回事件为可写
		{
			printf("writable event.\n");
		}

		// 清空集合里所有文件描述符
		FD_ZERO(&readfds);
		FD_ZERO(&writefds);
		FD_ZERO(&errorfds);
	}	
	
	// 关闭设备文件
	close(fd);
	return 0;
}

实验结果

seletc 实验结果与 poll 实验结果相同:

在这里插入图片描述

epoll 机制

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

——百度百科

epoll 相关变量

typedef union epoll_data {
 void *ptr;
 int fd;
 uint32_t u32;
 uint64_t u64;
} epoll_data_t;

struct epoll_event {
 uint32_t events; /* Epoll events */
 epoll_data_t data; /* User data variable */
};

常见事件类型

事件名描述
EPOLLIN文件描述符可读
EPOLLOUT文件描述符可写
EPOLLPRI文件描述符有紧急数据可读
EPOLLERR文件描述符产生错误
EPOLLHUP文件描述符被挂断
EPOLLET文件描述符有事务产生

epoll相关的系统调用有:epoll_create, epoll_ctl 和epoll_wait,头文件为 sys/epoll.h

epoll_create()

原型:

int epoll_create(int size);

功能:epoll_create() 用于生成一个 epoll 文件描述符,size 为 epoll 文件描述符可支持的文件描述符个数。

epoll_wait()

原型:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能:监听 epoll 文件描述符

参数:

  • epfd:epoll_create 生成的 epoll 文件描述符
  • epoll_event:用来回传成功检测的事件数组
  • maxevents:每次监听的最大事件个数
  • timeout:timeout 为 -1,表示一直阻塞监听;timeout 为 0,表示非阻塞监听;timeout 为正数时,代表监听超时时间(单位为毫秒)

返回值:成功时返回有事件发生的文件描述符个数,超时返回 0,发生错误时返回 -1 并设置 errno。

epoll_ctl()

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能:控制 epoll 文件描述符上的事件,包括注册事件、修改事件和删除事件。

参数:

  • epfd:epoll_create 生成的 epoll 文件描述符
  • op:要进行的操作,可取值见下文
  • fd:要操作的文件描述符
  • event:epoll_event 指针
op说明
EPOLL_CTL_ADD1注册操作
EPOLL_CTL_DEL2删除操作
EPOLL_CTL_MOD3修改操作

返回值:成功返回 0,发生错误返回 -1 并设置 errno。

实验程序

驱动文件、“写”程序、Makefile 都和 poll 实验相同,只有 “读“程序(监听程序)不同,

“读”程序代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>

#define DEV_FILE "/dev/chrdev_device"
#define FDS_NUM  5
#define MAX_EVENTS 10

int main(int argc, char** argv)
{
	int fd, tmp, i;
	int ret = 0;
	char buf[32] = {0};
	int epollfd;
	struct epoll_event event;
	struct epoll_event events[MAX_EVENTS];
	
	// 打开设备文件
	fd = open(DEV_FILE, O_RDWR);
	if(fd < 0)
	{
		printf("%s open failed.\n", DEV_FILE);
		return 0;
	}
	printf("%s open successfully.\n", DEV_FILE);

	// 创建一个 epollfd
	epollfd = epoll_create(FDS_NUM);
	if(epollfd == -1)
	{
		printf("epoll_create error.\n");
		return 0;
	}

	// event 结构体初始化
	event.events = EPOLLIN; // 读事件
	event.data.fd = fd;     // 要监听的文件描述符

	// 设置 epollfd
	if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
	{
		printf("epoll_ctl error.\n");
		return 0;
	}

	// 读数据
	printf("app will read data.\n");
	while(1)
	{
		// 等待 events
		ret = epoll_wait(epollfd, events, MAX_EVENTS, 3000);
		if(ret < 0)
		{
			printf("epoll_wait error.\n");
			return 0;
		}
		else if(ret == 0)
		{
			printf("epoll wait timeout.\n");	
		}
		else
		{

			// 根据监测到的事件作相应处理
			for(i = 0; i < ret; i++)
			{
				if(events[i].data.fd == fd)
				{
					ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
					if(ret == 0)
						printf("app read data successfully\ndata: %s\n", buf);
					else
						printf("app read data failed.\n");
					sleep(3);		
				}
			}
		}
	}		
	// 关闭设备文件
	close(fd);
	return 0;
}

实验结果

epoll 实验结果与 poll 实验 和 select 实验结果相同:

在这里插入图片描述


http://www.kler.cn/a/6867.html

相关文章:

  • 掌握SEO提升网站流量的关键在于长尾关键词的有效运用
  • k8s上部署redis高可用集群
  • 《通往人工智能深度学习专家之路:全面解析学习路线图》
  • 退款成功订阅消息点击后提示订单不存在
  • android 如何获取当前 Activity 的类名和包名
  • 【会话文本nlp】对话文本解析库pyconverse使用教程版本报错、模型下载等问题解决超参数调试
  • Java 基本数据类型
  • yshop代码生成器遇到的问题 eFrom.vue没生成
  • 从零开始实现一个C++高性能服务器框架----配置模块
  • 【华为机试真题详解JAVA实现】—整数与IP地址间的转换
  • plt常用绘图方法总结
  • 【游戏策划】消消乐游戏策划案
  • Windows11之QT开发框架超详细下载安装与使用教程
  • 【Java贪心】P1208 [USACO1.3]混合牛奶 Mixing Milk
  • mySql的配置文件 .ini
  • 办公工具-latex
  • (不打广告)推荐这款永久免费内网穿透软件-神卓互联
  • C#中的异常
  • 【趣味杂谈】ChatGPT,不止是第四次工业革命的开始
  • 网站都变成灰色了,怎么实现的?
  • 函数的返回值
  • ToBeWritten之Radare2 使用教程
  • [oeasy]python0129_unicode_中文字符序号_十三道大辙_字符编码解码_eval_火星文
  • Linux系统(Ubuntu) 安装和使用MySQL (5.0) ---- 保姆级教程
  • OD-求字符串中所有整数的最小和(Python)
  • 新加坡量子软件公司Horizon完成1810万美元A轮融资