【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;   // cdevstruct 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;elsenum=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);// 初始化cdevdev[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;elsenum=dev[i-1].dev_num+1;printk("number:%d \r\n",num);//注销设备号unregister_chrdev_region(num, 1);//删除 cdevcdev_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;   // cdevstruct 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.cmake -C $(KDIR) M=$(PWD) modulesrm -rf *.cmd *.mod.c *.o *.symvers *.order *.mod# 执行删除操作
clean:make -C $(KDIR) M=$(PWD) clean# 编译测试文件
test1:test.cgcc test.c -o target

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

warning: #188-D: enumerated type mixed with another type

警告解释&#xff1a;枚举类型混合了其它的数据类型&#xff1b; 解决方法&#xff1a; 1&#xff1a;检查代码&#xff0c;是不是存在混用&#xff1b;&#xff1b; 2&#xff1a;结构体初始化为 0 报warning&#xff0c;不能将结构体的第一个变量&#xff0c;使用枚举类型&am…

安全防御第七次实验

需求&#xff1a;在FW7和FW8之间建立一条IPSEC通道保证10.0.2.0/24网段可以正常访问到192.168.1.0/24 一、NAT配置 FW4&#xff1a; FW6&#xff1a; 二、在FW4上做服务器映射 三、配置IPSEC FW5&#xff1a; FW6&#xff1a; 四、防火墙上的安全策略 FW4&#xff1a; FW5:…

VBA中类的解读及应用第十讲:限制文本框的输入,使其只能输入数值(上)

《VBA中类的解读及应用》教程【10165646】是我推出的第五套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。 类&#xff0c;是非常抽象的&#xff0c;更具研究的价值。随着我们学习、应用VBA的深入&#xff0…

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

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

抽象版忘记密码模板0.1版本

背景&#xff1a;用户密码忘了&#xff0c;无法登录&#xff0c;怎么办&#xff01;急&#xff01;急&#xff01;急&#xff01; 前置工作——Srpingboot3-maven项目&#xff0c;核心框架如下 <dependency><groupId>org.springframework.boot</groupId><…

Redis核心数据结构之字典(二)

字典 解决键冲突 当有两个或以上数量的键被分配到了一个哈希表数组的同一个索引上面&#xff0c;我们称这些键发生了冲突(collision)。 Redis的哈希表使用链地址法(separate chaining)来解决键冲突&#xff0c;每个哈希表节点都有一个next指针&#xff0c;多个哈希表节点可以…

数据库原理实验课(1)

目录 实验内容 安装头歌中的相关内容 具体过程 完结撒花~ 我也是第一次接触oracle的相关软件和操作&#xff0c;所以是一次傻瓜式教学记录 实验内容 安装头歌中的相关内容 具体过程 这是我在百度网盘中下载解压出来的oracle文件夹内的全部内容&#xff08;可能有因为安装完…

CodeSys通过C函数接口调用Qt

建议先查看之前的文章【CodeSys中调用C语言写的动态库】&#xff0c;了解如何创建一个能够被codesys调用的动态库。 假如想要在函数中使用Qt或者第三方库&#xff08;比如opencv等&#xff09;&#xff0c;可以在其自动生成的makefile文件中设置好相应的参数。 比如我这里就是…

2024037期传足14场胜负前瞻

2024037期售止时间为3月9日&#xff08;周六&#xff09;20点00分&#xff0c;敬请留意&#xff1a; 本期深盘多&#xff0c;1.5以下赔率4场&#xff0c;1.5-2.0赔率5场&#xff0c;其他场次是平半盘、平盘。本期14场整体难度中等。以下为基础盘前瞻&#xff0c;大家可根据自身…

力扣刷题Days13-101对称二叉树(js)

目录 1,题目 2&#xff0c;代码 2.1递归思想 2.2队列--迭代思想 3&#xff0c;学习与总结 1,题目 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 2&#xff0c;代码 2.1递归思想 return dfs(left.left, right.right) && dfs(left.right, right.l…

IDS入侵检测知识整理及lDS入侵检查表

【IDS入侵检测知识整理及lDS入侵检查表】 1. 网络入侵检测IDS概念 2. IDS分类 3. IDS的工作原理 4. IDS在网络中的部署 5. 入侵检查表 项目建设全套资料获取通道&#xff1a;软件开发全套资料_数字中台建设指南-CSDN博客

echarts中toolbox 中文乱码问题

问题描述 本地引用的echarts源文件&#xff0c;页面其他部分编码显示正常&#xff0c;唯独toolbox鼠标悬停在上面时提示信息显示乱码。 如图所示&#xff1a; 尝试过的方法 使用sublime text 3&#xff0c;notepad&#xff0c;记事本更改文件编码为utf-8引入时&#xff0c;在sc…

办公电脑换成MacBookPro半年之后……

小白是从2008年开始接触电脑的&#xff0c;当时朋友给我注册的第一个QQ账号是2008年4月。 从此&#xff0c;小白一直认为电脑全部都是Windows系统。直到上大学那年&#xff0c;看到了外教老师的MacBookPro…… 折腾电脑的开始居然是起源于诺基亚手机&#xff0c;给半智能S40的…

程序员有哪些证书值得考?

证书可以作为第三方机构对于程序员特定技能或知识掌握程度的认可&#xff0c;在求职市场上&#xff0c;一些公司尤其是大型企事业单位或者政府项目可能更看重证书作为衡量应聘者专业能力的标准之一。备考证书的过程&#xff0c;也可以促使程序员系统地学习和巩固相关知识&#…

力扣hot100:438.找到字符串中所有字母异位词(滑动窗口)

26个字符&#xff0c;我复制怎么了&#xff1f;26个字符我比较个数怎么了&#xff1f; 顶多时间复杂度*26 本题用固定窗口大小的滑动窗口每次比较包含26个元素的数组次数&#xff0c;最容易写。 动态窗口大小哈希表存数值&#xff08;双指针差值&#xff09;难想难写。 一、动态…

基于模拟退火算法(SA)的TSP(Python实现)

本篇文章是博主在最化优学习、人工智能等领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅解。文章分类在最优化算…

希腊证券交易所ATHEX计划在Sui上部署融资功能

希腊证券交易所ATHEX计划将其电子订单簿构建&#xff08;Electronic Book Building &#xff0c;EBB&#xff09;功能部署到Sui上&#xff0c;他们是第一个与区块链公司合作开发此技术的证券交易所。其背后的雅典交易所集团Athens Exchange Group将与Sui的开发公司Mysten Labs合…

一键转发朋友圈!微信快速营销推广必备法宝!

在这个“得私域者得天下”的互联网时代&#xff0c;如何能够在微信上进行快速、高效的营销推广成为了摆在许多人面前的一道难题。 幸运的是&#xff0c;随着微信管理系统的出现&#xff0c;一键转发朋友圈的快速营销推广法宝已经变得触手可及。 首先&#xff0c;微信管理系统…

QT 解决在线安装版本下载速度(以国内镜像启动安装器的方式)

关于Fiddler输入steam的教程&#xff0c;本人试过&#xff0c;无效略过… 正确方式 具体操作方法&#xff1a; 1.方式一、未安装过QT 下载好安装器之后&#xff0c;在安装器&#xff08;qt-unified-windows-x64-4.6.0-online.exe&#xff09;的文件夹中空白处&#xff0c;按…

UVA378 Intersecting Lines 题解

UVA378 Intersecting Lines 题解 怎么这么多点斜式邪教啊。 解法 在计算几何中&#xff0c;我们应该尽可能地避免使用浮点数的计算&#xff0c;尽可能地使用向量计算。 本篇题解默认读者具有向量基础。 为了方便讲解&#xff0c;我们将输入的四个点分别记作 A , B , C , …