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

「Tech初见」Linux驱动之blkdev

目录

  • 一、Motivation
  • 二、Solution
    • S1 - 块设备驱动框架
      • (1)注册块设备
      • (2)注销块设备
      • (3)申请 gendisk
      • (4)删除 gendisk
      • (5)将 gendisk 加入 kernel
      • (6)设置 gendisk 容量
      • (7)gendisk 引用计数
    • S2 - 定义块设备
    • S3 - 无 I/O 调度的请求队列
  • 三、Result

一、Motivation

在类 Unix OS 的世界里,I/O 设备都是被当作设备文件( device file )这种特殊文件来处理的。例如,用同一 write() 系统调用既可以向普通文件中写入数据,也可以向打印机等外围设备中写数据。根据设备文件的特点可分为字符设备和块设备,

  1. 字符设备一般是不支持随机访问的,例如,鼠标和键盘等;
  2. 块设备是支持随机访问的,例如,硬盘

我们为什么要编写关于设备的驱动程序呢?主要是为了操控插入的外接设备,本文主要是对块设备展开具体讲解,如有字符设备的需求请移步「Tech初见」Linux驱动之blkdev

二、Solution

S1 - 块设备驱动框架

kernel 用 block_device 结构体表示块设备,定义在 include/linux/fs.h 中,结构体内部的成员变量太多了,我决定不展开讲解,等到用到的时候再说。关于 block_device 我们在这里仅仅需要搞明白两点就可以了,

(1)注册块设备

该方法是向 kernel 中注册新的块设备并申请设备号,原型为,

int register_blkdev(unsigned int major, const char* name)

其中,major 为主设备号,name 为块设备名称。若 major 为 0,表示由系统自动分配主设备号;若 major 在 1 ~ 255 之间,表示自定义的主设备号。返回值为 0 则表示注册成功,反之失败。关于主次设备号的内容同样在「Tech初见」Linux驱动之blkdev 中有详细的说明

(2)注销块设备

该方法是在 kernel 中注销指定的块设备,原型为,

void unregister_blkdev(unsigned int major, const char* name)

gendisk 是块设备最重要的结构体,意为通用磁盘,定义在 include/linux/genhd.h,可以理解为 gendisk 是我们创建的块设备节点与 kernel 交互的中间人,同样我们一开始不需要太深入地了解内部的成员变量,只需要记得这几个用于申请和释放 gendisk 的方法即可,

(3)申请 gendisk

在使用 gendisk 之前要先申请,原型为,

struct gendisk* alloc_disk(int minors)

其中,minors 为次设备号的数量,即 gendisk 对应的分区数量

(4)删除 gendisk

原型为,

void del_gendisk(struct gendisk* gdisk)

(5)将 gendisk 加入 kernel

申请到 gendisk 之后,对其进行初始化,然后就可以加入到 kernel 中了,原型为,

void add_disk(struct gendisk* gdisk)

(6)设置 gendisk 容量

初始化 gendisk 时需要设置其容量,原型为,

void set_capacity(struct gendisk* gdisk, sector_t size)

其中的 size 是磁盘容量大小,注意这里指的是 sector 个数,一个 sector 通常为 512 字节

(7)gendisk 引用计数

增加 gendisk 的引用计数,

struct kobject* get_disk(struct gendisk* gdisk)

减少 gendisk 的引用计数,

void put_disk(struct gendisk* disk)

当 kernel 中没人再引用该 gendisk 时,kernel 就可以放心大胆地释放这块空间了
block_device_operations 用来表示块设备的操作集,定义在 include/linux/blkdev.h 中,同样也无需了解太多,记住 open() 和 release() 即可

关于块设备 I/O 请求过程,我先要引入的就是请求队列 request_queue 的概念,它就是一个队列,里面存放着不同的 I/O 请求,我们都知道 I/O 操作比 CPU 操作要慢很多,为了提高系统的利用率和吞吐量,我们一般都是等一等 I/O 操作,待它成势了再一次性地写入磁盘,这样可以减少磁盘的寻道时间,队列和 request 以及包含所需操作的磁块 bio 的关系如下图,

每个 gendisk 都应该有一个请求队列,可以通过,

struct request_queue* blk_alloc_queue(gfp_t gfp_mask)

进行申请,gfp_mask 一般为 GFP_KERNEL。然后通过,

void blk_queue_make_request(struct request_queue* que, make_request_fn* fn)

来为队列绑定请求函数,意思就是说只要是这个队列中的请求,统统按照 fn 的业务逻辑来处理。当然,还需要实现具体的 fn,

void (make_request_fn) (struct request_queue* que, struct bio* bio)

最后,我还想讲解一下 bio 结构体,它保存着最终要读写的数据地址等信息,定义在 include/linux/blk_types.h 中

S2 - 定义块设备

定义一些自己的块设备及对应的操作,struct myblkdev 包含的 struct gendisk 相当重要,透过它才能体现出我们定义的是块设备,buf 指向模拟的磁盘空间,宏 DISKSIZE 是磁盘的大小,默认为 2 MB,宏 NDISKPART 表示磁盘有 3 个 sector,每个 sector 的大小为宏 SECTORSIZE 512,

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/slab.h>
#include <linux/string.h>#define DISKSIZE (2*1024*1024) /* 模拟磁盘的大小为 2 MB */
#define DEVNAME "ramdisk"
#define NDISKPART 3			/* 模拟磁盘有 3 个分区 */
#define SECTORSIZE 512		/* 扇区大小 *//* 自定义的块设备结构 */
struct myblkdev {int major;unsigned char* buf;		/* 指向模拟磁盘的空间 */struct gendisk* gdisk;struct request_queue* que;
};struct myblkdev mydev;int
my_blkdev_open(struct block_device* dev, fmode_t mode)
{printk(KERN_INFO "my_blkdev_open\n");return 0;
}void 
my_blkdev_release(struct gendisk* gdisk, fmode_t mode)
{printk(KERN_INFO "my_blkdev_release\n");
}struct block_device_operations blkops = {.owner = THIS_MODULE,.open = my_blkdev_open,.release = my_blkdev_release,
};

然后,我们的目光就可以转到驱动程序注册的流程当中了,相比于「Tech初见」Linux驱动之blkdev 的字符设备,块设备的初始化稍微复杂了一些,但是本质都是一样的,即是申请 - 初始化 - 加入 kernel 模型。拢共分为五步走,

/* 初始化函数 */
static int __init
my_blkdev_init(void)
{int ok = 0;printk(KERN_INFO "my_blkdev_init\n");/* 注册块设备 */mydev.major = register_blkdev(0, DEVNAME);if(mydev.major < 0) {ok = -EINVAL;goto over;}/* 申请模拟磁盘的内存空间 */mydev.buf = kzalloc(DISKSIZE, GFP_KERNEL);if(mydev.buf == NULL) {ok = -EINVAL;goto alloc_buf_fail;}/* 分配 gendisk */mydev.gdisk = alloc_disk(NDISKPART);if(mydev.gdisk == NULL) {ok = -EINVAL;goto alloc_disk_fail;}/* 分配 request 队列 */mydev.que = blk_alloc_queue(GFP_KERNEL);if(mydev.que == NULL) {ok = -EINVAL;goto alloc_que_fail;}blk_queue_make_request(mydev.que, my_blkdev_make_req_fn);/* 注册 gendisk */mydev.gdisk->major = mydev.major;mydev.gdisk->first_minor = 0;mydev.gdisk->fops = &blkops;mydev.gdisk->queue = mydev.que;mydev.gdisk->private_data = &mydev;strcpy(mydev.gdisk->disk_name, DEVNAME);	/* 给 myblkdev 最核心的组件 kobject 的 gdisk 取名字 */set_capacity(mydev.gdisk, DISKSIZE/SECTORSIZE); /* 设备容量以 sector 为单位 */add_disk(mydev.gdisk);goto over;alloc_que_fail:put_disk(mydev.gdisk);
alloc_disk_fail:kfree(mydev.buf);
alloc_buf_fail:unregister_blkdev(mydev.major, DEVNAME);
over:return ok;
}

(1)注册块设备,为设备申请主设备号并告知 kernel:块设备的名称;(2)申请模拟磁盘的内存空间,通过 kzalloc() 申请空间交给块设备管理;(3)申请 gendisk;(4)为块设备分配 request 队列,用以存放 I/O 请求;(5)初始化 gendisk 并将它注册到 kernel 模型中

大抵就是这些流程,需要注意的是,这里我采用的是不使用 I/O 调度的请求队列。我想大概讲一讲有 I/O 调度和没有的请求队列的区别,对于一些老旧的块设备,比如机械硬盘,有调度的请求队列能够利用请求的特性对其进行重新排序进而减少机械臂移动的次数,从而提高磁盘的工作性能;但是对于现在的 NAND 闪存 OR 固态硬盘,对请求进行排序可能并不会带来实质性的性能提升,反而会增加额外的开销。所以,具体选择哪种请求队列,需要根据实际情况进行判断

关于请求队列的详细用法我在下一小节再展开讲解,我们继续顺着注册模块的流程将驱动程序进行到底,讲解一下模块注销的相关步骤,

/* 卸载函数 */
static void __exit
my_blkdev_exit(void)
{printk(KERN_INFO "my_blkdev_exit\n");/* 删除 gendisk */del_gendisk(mydev.gdisk);/* 减少 gendisk 的引用计数 */put_disk(mydev.gdisk);/* 清空 request 队列 */blk_cleanup_queue(mydev.que);/* 注销块设备 */unregister_blkdev(mydev.major, DEVNAME);/* 释放空间 */kfree(mydev.buf);
}

和注册流程一样,申请了哪些东西,在注销时都要归还,包括 gendisk、请求队列等

S3 - 无 I/O 调度的请求队列

无 I/O 调度的请求队列绑定一个请求处理函数,我取名叫 my_blkdev_make_req_fn,它接受 req_que 请求队列和 bio I/O 操作(页、长度和偏移)作为参数,

/* 制造请求函数 */
void
my_blkdev_make_req_fn(struct request_queue* req_que, struct bio* bio)
{int offset;struct bio_vec bvec;struct bvec_iter iter;unsigned long len = 0;/* 获取要操作的磁盘的起始地址(以字节为单位)*/offset = (bio->bi_iter.bi_sector) << 9;bio_for_each_segment(bvec, bio, iter) {	/* 处理 bio 中的每个段 */char* ptr = page_address(bvec.bv_page) + bvec.bv_offset;len = bvec.bv_len;/* 是读操作 OR 写操作 */if(bio_data_dir(bio) == READ)memcpy(ptr, mydev.buf+offset, len);if(bio_data_dir(bio) == WRITE)memcpy(mydev.buf+offset, ptr, len);offset += len;}bio_endio(bio);
}

bio_for_each_segment 即是遍历 bio 中从当前偏移 offset 开始的未完成的数据段,而宏 bio_for_each_segment_all 是遍历 bio 中所有数据段,不论它们是否已经被完成。在宏的作用域内针对每个数据段做相应的读/写操作。最后,处理完 bio 的所有数据段后透过 bio_endio() 告诉 request_que 目前的 I/O 任务已经完成

三、Result

在 /home/lighthouse/test-linuxdriver/blkdev 目录下,键入 make 命令编译程序,

lighthouse@VM-0-9-ubuntu:~/test-linuxdriver/blkdev$ make
make -C /lib/modules/5.4.0-126-generic/build     M=/home/lighthouse/test-linuxdriver/blkdev modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.0-126-generic'CC [M]  /home/lighthouse/test-linuxdriver/blkdev/blkdev.oBuilding modules, stage 2.MODPOST 1 modulesCC [M]  /home/lighthouse/test-linuxdriver/blkdev/blkdev.mod.oLD [M]  /home/lighthouse/test-linuxdriver/blkdev/blkdev.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-126-generic'

键入,

$ sudo insmod blkdev.ko

挂载驱动程序后,可以在另一个终端键入,

cat /proc/devices

查看该驱动程序向内核申请到的设备号,因为是动态分配,所以每一次的设备号可能不尽相同,

Block devices:2 fd7 loop...251 ramdisk...

可以看到该驱动程序的主设备号为 251,这意味着主设备号为 251 的块设备,以后归该驱动程序管

此时,如果通过 mknod 创建 251 主设备号的设备节点,该驱动程序是可以 handle 一些常规的操作的,例如,cat 和 echo,即读取 OR 写入该设备文件,

$ sudo mknod /dev/myblkdev b 251 0

可以透过,

lighthouse@VM-0-9-ubuntu:~/test-linuxdriver/blkdev$ ls /dev/myblkdev -l
brwxrwxrwx 1 root root 251, 0 Sep  2 15:37 /dev/myblkdev

先查看一下创建的块设备节点,我们会看到,这个块设备权限不太高,可以通过 chmod 提高权限,

$ sudo chmod 777 /dev/myblkdev

之后就可以为所欲为了,可以尝试向 /dev/myblkdev 中写入一串字符,

$ echo "hello myblkdev" > /dev/myblkdev 

可以透过,

$ dmesg | tail -10
$ cat /dev/myblkdev 

查看最近的输出信息,发现字符串内容有写入到 myblkdev 中

相关文章:

「Tech初见」Linux驱动之blkdev

目录 一、Motivation二、SolutionS1 - 块设备驱动框架&#xff08;1&#xff09;注册块设备&#xff08;2&#xff09;注销块设备&#xff08;3&#xff09;申请 gendisk&#xff08;4&#xff09;删除 gendisk&#xff08;5&#xff09;将 gendisk 加入 kernel&#xff08;6&a…...

ssh配置(二、登录服务器)

一. 登录 linux 服务器的两种方式 使用 ssh用户名密码 的方式登录&#xff0c;但这种方式不安全&#xff0c;密码太简单容易被暴力破解&#xff0c;密码太复杂又不容易记。使用 ssh公私钥 的方式登录。 以上两种方式都可以在图形化软件工具中配置&#xff0c;例如 finalshell…...

pytorch异常——RuntimeError:Given groups=1, weight of size..., expected of...

文章目录 省流异常报错异常截图异常代码原因解释修正代码执行结果 省流 nn.Conv2d 需要的输入张量格式为 (batch_size, channels, height, width)&#xff0c;但您的示例输入张量 x 是 (batch_size, height, width, channels)。因此&#xff0c;需要对输入张量进行转置。 注意…...

【FPGA项目】沙盘演练——基础版报文收发

​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ 第1个虚拟项目 前言 点灯开启了我们的FPGA之路&#xff0c;那么我们来继续沙盘演练。 用一个虚拟项目&#xff0c;来入门练习&#xff0c;以此步入数字逻辑的…...

【C++技能树】继承概念与解析

Halo&#xff0c;这里是Ppeua。平时主要更新C&#xff0c;数据结构算法&#xff0c;Linux与ROS…感兴趣就关注我bua&#xff01; 继承 0. 继承概念0.1 继承访问限定符 1. 基类和派生类对象赋值兼容转换2. 继承中的作用域3. 派生类中的默认成员函数4.友元5.继承中的静态成员6.菱…...

计算机网络 第二节

目录 一&#xff0c;计算机网络的分类 1.按照覆盖范围分 2.按照所属用途分 二&#xff0c;计算机网络逻辑组成部分 1.核心部分 &#xff08;通信子网&#xff09; 1.1电路交换 1.2 分组交换 两种方式的特点 重点 2.边缘部分 &#xff08;资源子网&#xff09; 进程通信的方…...

无涯教程-机器学习 - 矩阵图函数

相关性是有关两个变量之间变化的指示&#xff0c;在前面的章节中&#xff0c;无涯教程讨论了Pearson的相关系数以及相关的重要性&#xff0c;可以绘制相关矩阵以显示哪个变量相对于另一个变量具有较高或较低的相关性。 在以下示例中&#xff0c;Python脚本将为Pima印度糖尿病数…...

Redis 高可用与集群

Redis 高可用与集群 虽然 Redis 可以实现单机的数据持久化&#xff0c;但无论是 RDB 也好或者 AOF 也好&#xff0c;都解决 不了单点宕机问题&#xff0c;即一旦单台 redis 服务器本身出现系统故障、硬件故障等问题后&#xff0c; 就会直接造成数据的丢失&#xff0c;因此需要…...

修改文件名后Git仓上面并没有修改

场景&#xff1a; 我在本地将文件夹名称由Group → group ,执行git push 后&#xff0c;远程分支上的文件名称并没有修改。 原因&#xff1a; 是我绕过了git 直接使用了系统的重命名操作。 在 Git 中&#xff0c;对于已经存在的文件或文件夹进行大小写重命名是一个敏感的操作…...

Linux 信号

目录 基本概念信号的分类可靠信号与不可靠信号实时信号与非实时信号 常见信号与默认行为进程对信号的处理signal()函数sigaction()函数 向进程发送信号kill()函数raise() alarm()和pause()函数alarm()函数pause()函数 信号集初始化信号集测试信号是否在信号集中 获取信号的描述…...

深入探讨梯度下降:优化机器学习的关键步骤(二)

文章目录 &#x1f340;引言&#x1f340;eta参数的调节&#x1f340;sklearn中的梯度下降 &#x1f340;引言 承接上篇&#xff0c;这篇主要有两个重点&#xff0c;一个是eta参数的调解&#xff1b;一个是在sklearn中实现梯度下降 在梯度下降算法中&#xff0c;学习率&#xf…...

高频算法面试题

合并两个有序数组 const merge (nums1, nums2) > {let p1 0;let p2 0;const result [];let cur;while (p1 < nums1.length || p2 < nums2.length) {if (p1 nums1.length) {cur nums2[p2];} else if (p2 nums2.length) {cur nums1[p1];} else if (nums1[p1] &…...

Hive-启动与操作(2)

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 个人主页&#xff1a;beixi 本文章收录于专栏&#xff08;点击传送&#xff09;&#xff1a;【大数据学习】 &#x1f493;&#x1f493;持续更新中&#xff0c;感谢各位前辈朋友们支持…...

css transition 指南

css transition 指南 在本文中&#xff0c;我们将深入了解 CSS transition&#xff0c;以及如何使用它们来创建丰富、精美的动画。 基本原理 我们创建动画时通常需要一些动画相关的 CSS。 下面是一个按钮在悬停时移动但没有动画的示例&#xff1a; <button class"…...

LeetCode 面试题 02.05. 链表求和

文章目录 一、题目二、C# 题解 一、题目 给定两个用链表表示的整数&#xff0c;每个节点包含一个数位。 这些数位是反向存放的&#xff0c;也就是个位排在链表首部。 编写函数对这两个整数求和&#xff0c;并用链表形式返回结果。 点击此处跳转题目。 示例&#xff1a; 输入&a…...

一米脸书营销软件

功能优势 JOIN ADVANTAGE HOME PAGE MARKETING 公共主页营销 可同时对多个账户公共主页评论&#xff0c;点赞等 可批量邀请多个好友对Facebook公共主页进行评论点赞等&#xff0c;也可批量登录小号对自己公共主页进行点赞。 GROUP MARKETING 小组营销 可批量针对不同账户进行…...

vue 根据数值判断颜色

1.首先style样式给两种颜色 用:class 三元运算符判断出一种颜色 第一步&#xff1a;在style里边设置两种颜色 .green{color: green; } .orange{color: orangered; }在取数据的标签 里边 判断一种颜色 :class"item.quote.current >0 ?orange: green"<van-gri…...

Hugging Face 实战系列 总目录

PyTorch 深度学习 开发环境搭建 全教程 Transformer:《Attention is all you need》 Hugging Face简介 1、Hugging Face实战-系列教程1&#xff1a;Tokenizer分词器&#xff08;Transformer工具包/自然语言处理&#xff09; Hungging Face实战-系列教程1&#xff1a;Tokenize…...

国标视频云服务EasyGBS国标视频平台迁移服务器后无法启动的问题解决方法

国标视频云服务EasyGBS支持设备/平台通过国标GB28181协议注册接入&#xff0c;并能实现视频的实时监控直播、录像、检索与回看、语音对讲、云存储、告警、平台级联等功能。平台部署简单、可拓展性强&#xff0c;支持将接入的视频流进行全终端、全平台分发&#xff0c;分发的视频…...

HTML <th> 标签

实例 普通的 HTML 表格,包含两行两列: <table border="1"><tr><th>Company</th><th>Address</th></tr><tr><td>Apple, Inc.</td><td>1 Infinite Loop Cupertino, CA 95014</td></tr…...

HTTP/1.1协议中的响应报文

2023年8月30日&#xff0c;周三下午 目录 概述响应报文示例详述 概述 HTTP/1.1协议的响应报文由以下几个部分组成&#xff1a; 状态行&#xff08;Status Line&#xff09;响应头部&#xff08;Response Headers&#xff09;空行&#xff08;Blank Line&#xff09;响应体&a…...

TDengine函数大全-选择函数

以下内容来自 TDengine 官方文档 及 GitHub 内容 。 以下所有示例基于 TDengine 3.1.0.3 TDengine函数大全 1.数学函数 2.字符串函数 3.转换函数 4.时间和日期函数 5.聚合函数 6.选择函数 7.时序数据库特有函数 8.系统函数 选择函数 TDengine函数大全BOTTOMFIRSTINTERPLASTLAS…...

非关系型数据库Redis的安装

一、关系型数据库与非关系型数据库的区别&#xff1a;---------面试高频率问题 1、首先了解一下 什么是关系型数据库&#xff1f; 关系型数据库最典型的数据结构是表&#xff0c;由二维表及其之间的联系所组成的一个数据组织。 优点&#xff1a; 易于维护&#xff1a;都是使用…...

oracle 创建数据库

查询表空间的命令 select t1.name,t2.name from v$tablespace t1,v$datafile t2 where t1.ts# t2.ts#; CREATE TABLESPACE ORM_342_BETA DATAFILE /app/oracle/oradata/sysware/ORM_342_BETA.DBF size 800M --存储地址 初始大小800M autoextend on nex…...

wxWidgets从空项目开始Hello World

前文回顾 接上篇&#xff0c;已经是在CodeBlocks20.03配置了wxWidgets3.0.5&#xff0c;并且能够通过项目创建导航创建一个新的工程&#xff0c;并且成功运行。 那么上一个是通过CodeBlocks的模板创建的&#xff0c;一进去就已经是2个头文件2个cpp文件&#xff0c;总是感觉缺…...

【Apollo学习笔记】——规划模块TASK之SPEED_DECIDER

文章目录 前言SPEED_DECIDER功能简介SPEED_DECIDER相关配置SPEED_DECIDER流程MakeObjectDecisionGetSTLocationCheck类函数CheckKeepClearCrossableCheckStopForPedestrianCheckIsFollowCheckKeepClearBlocked Create类函数 前言 在Apollo星火计划学习笔记——Apollo路径规划算…...

【操作系统】一文快速入门,很适合JAVA后端看

作者简介&#xff1a; 目录 1.概述 2.CPU管理 3.内存管理 4.IO管理 1.概述 操作系统可以看作一个计算机的管理系统&#xff0c;对计算机的硬件资源提供了一套完整的管理解决方案。计算机的硬件组成有五大模块&#xff1a;运算器、控制器、存储器、输入设备、输出设备。操作…...

C++ Primer阅读笔记--allocator类的使用

1--allocator类的使用背景 new 在分配内存时具有一定的局限性&#xff0c;其将内存分配和对象构造组合在一起&#xff1b;当分配一大块内存时&#xff0c;一般希望可以在内存上按需构造对象&#xff0c;这时需要将内存分配和对象构造分离&#xff0c;而定义在头文件 memory 的 …...

【C++历险记】面向对象|菱形继承及菱形虚拟继承

个人主页&#xff1a;兜里有颗棉花糖&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…...

【Locomotor运动模块】攀爬

文章目录 一、攀爬主体“伪身体”1、“伪身体”的设置2、“伪身体”和“真实身体”&#xff0c;为什么同步移动3、“伪身体”和“真实身体”&#xff0c;碰到墙时不同步的原因①现象②原因③解决 二、攀爬1、需要的组件&#xff1a;“伪身体”、Climbing、Climbable及Interacto…...