#简单的驱动程序
本文档是[Driver Development Kit教程](ddk-tutorial.md)文档的一部分。
##概述
在本章中,我们将了解驱动程序的基础知识。
我们将从简单到稍微复杂,每个驱动程序说明一组具体的概念如下:
`dev/misc/demo-null` 和 `dev/misc/demo-zero`:
*小的,“no-state”的接收器/源驱动程序,用于解释基本知识,比如,
如何处理client端的** read()**和** write()**请求。
`dev/misc/demo-number`:
*一个返回ASCII码的驱动程序,说明了每个设备的上下文,一次性**读取()**
operation,并介绍基于FIDL的控制operation。
`dev/misc/demo-multi`:
*具有多个子设备的驱动程序。
`dev/misc/demo-fifo`:
*显示更复杂的设备状态,检查部分** read()**和** write()**operation,以及
引入状态信令以启用阻塞I/O.
作为参考,所有这些驱动程序的源代码都在`//zircon/system/dev/sample`目录。
##注册
一个叫设备管理器的系统进程(后文称`devmgr`),负责设备驱动程序。
在初始化期间,它会在`/boot/driver`和`/system/driver`目录中搜索驱动程序。
<! - @@@ TODO Brian说,当我们切换到基于包管理的世界时,/system将要消失
。此时这些驱动程序将由garnet中的包管理器提供 - >
这些驱动程序实现为动态共享对象(** DSO **),并提供两个有意思的Items:
*一组用于评估驱动程序绑定的`devmgr`指令
*一组绑定函数。
让我们看一下`dev/sample/null`目录中`demo-null.c`的底部:
```C
static zx_driver_ops_t demo_null_driver_ops = {
.version = DRIVER_OPS_VERSION,
.bind = null_bind,
};
ZIRCON_DRIVER_BEGIN(demo_null_driver,demo_null_driver_ops,“zircon”,“0.1”,1)
BI_MATCH_IF(EQ,BIND_PROTOCOL,ZX_PROTOCOL_MISC_PARENT),
ZIRCON_DRIVER_END(demo_null_driver)
```
<! - @@@ alainv sez这些宏将被弃用,转而使用驱动绑定语言(Driver Binding Language) - >
C预处理器宏`ZIRCON_DRIVER_BEGIN`和`ZIRCON_DRIVER_END`限定在DSO中创建的ELF note section。
本section包含一个或多个由`devmgr`评估(evaluated)的语句。
在上面,如果设备的“BIND_PROTOCOL”等于`ZX_PROTOCOL_MISC_PARENT`,宏`BI_MATCH_IF`是一个评估为'true`的条件。
`true`评估会导致`devmgr`随后使用在`ZIRCON_DRIVER_BEGIN`宏中提供的绑定operation绑定驱动程序。
我们现在可以忽略这个“glue”,只需注意这部分代码:
*告诉`devmgr`,这个驱动程序可以绑定到需要`ZX_PROTOCOL_MISC_PARENT`protocol的设备,同时它包含一个指向`zx_drivers_ops_t`表的指针,该表列出了此DSO提供的功能。
要初始化设备,`devmgr`通过`.bind`成员(也在`demo-null.c`中),调用绑定函数** null_bind()**:
```C
static zx_protocol_device_t null_device_ops = {
.version = DEVICE_OPS_VERSION,
.read = null_read,
.write = null_write,
};
zx_status_t null_bind(void * ctx,zx_device_t * parent){
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name =“demo-null”,
.ops =&null_device_ops,
};
return device_add(parent,&args,NULL);
}
```
绑定功能负责通过调用** device_add()**(入参:指向父设备的指针、参数结构体)来“publishing”设备。
新设备被绑定到父路径名&mdash;
注意我们如何传递上面`.name`中的`demo-null`成员。
`.ops`成员是`zx_protocol_device_t`结构的指针,它列出了适用于该设备operation。
我们将在下面看到这些函数,** null_read()**和** null_write()**。
在调用** device_add()**后,设备名称已注册,并且参数传递的`.ops`成员方法也已经绑定到设备。
从** null_bind()**成功返回,告诉'devmgr'驱动程序现在已经与设备关联起来了。
此时,我们的`/dev/misc/demo-null`设备已准备好处理client端请求,
这意味着它必须:
*支持** open()**和** close()**
*提供一个** read()**处理程序,它立即返回文件结尾(** EOF **)
*提供一个** write()**处理程序,丢弃发送给它的所有数据
无需其他功能。
##从设备读取数据
在`zx_protocol_device_t`结构的`null_device_ops`成员中,提供了我们支持的operation:** null_read()**和** null_write()**。
** null_read()**函数提供读功能:
```C
static zx_status_t
null_read(void * ctx,void * buf,size_t count,zx_off_t off,size_t * actual){
* actual = 0;
return ZX_OK;
}
```
请注意,有两个与大小相关的参数传递给处理程序:
参数 | 含义
------------ | ------------------------------------- -------------------
`count` | client端可以接受的最大字节数
`actual` | 发送到client端的实际字节数
下图说明了这种关系:
图1 TODO
也就是说,client端缓冲区的可用大小(此处为`sizeof(buf)`)作为`count`参数传递给** null_read()**。
类似地,当调用** null_read()**函数,表示它读取的字节数(在我们的例子中为0)时,这个
显示为client端** read()**函数的返回值。
> **注意:
>处理程序应始终立即返回。
>按照惯例,在'* actual`中指示零字节表示EOF **
当然,有些情况下设备没有立即可用的数据,这时它不是EOF情况。
例如,串行端口可能正在等待从远程端到达更多字符。
这是由特殊通知处理的,我们将在下面的`/dev/misc/demo-fifo`设备中看到。
###将数据写入设备
将数据从client端写入设备几乎是相同的,并提供 ** null_write()**函数:
```C
static zx_status_t
null_write(void * ctx,const void * buf,size_t count,zx_off_t off,size_t * actual){
* actual = count;
return ZX_OK;
}
```
与** read()**一样,** null_write()**由client端调用** write()**触发:
图2 TODO
client端在其** write()**函数中指定了他们希望传输的字节数,这将在设备的** null_write()**函数中显示为`count`参数。
该设备可能已满(不是我们的`/dev/misc/demo-null`的情况,尽管—它永远不会填满),因此设备需要告诉client端它实际上写了多少字节。
这是通过`actual`参数完成的,该参数显示为client端** write()**函数的返回值。
请注意,我们的** null_write()**函数包含以下代码:
```C
* actual = count;
```
这告诉client端他们的所有数据都已写入。
当然,因为这是`/dev/misc/demo-null`设备,数据实际上并没有*去*任何地方。
> **注意:
>就像** ** null_read()** **情况一样,处理程序不能阻塞。**
##如何** open()**和** close()**?
我们没有提供** open()**或** close()**处理程序,但我们的设备支持这些operation。
这是可能的,因为任何未提供的operation回调函数都具有默认值。
大多数默认值只返回“不支持”,但在** open()**和** close()**的情况下
默认设置为简单设备提供足够的支持。
##`/dev/misc/demo-zero`
正如您可能想象的那样,`/dev/misc/demo-zero`设备的源代码几乎与`/dev/misc/demo-null`完全相同。
从operation的角度来看,`/dev/misc/demo-zero`应该会返回无穷无尽个零 —只要client去read。
我们不支持write。
考虑`/dev/misc/demo-zero`的** zero_read()**函数:
```C
static zx_status_t
zero_read(void * ctx,void * buf,size_t count,zx_off_t off,size_t * actual){
memset(buf,0,count);
* actual = count;
return ZX_OK;
}
```
代码将整个缓冲区`buf`设置为零(长度由client端给出的`count`参数确定),并告诉client端有多少字节可用(通过按client要求,将`* actual`设置为相同的数字)。
##`/dev/misc/demo-number`
让我们根据上面学到的概念构建一个更复杂的设备。
我们将其称为`/dev/misc/demo-number`,其作用是返回以ASCII字符串形式显示的下一个数字。
例如,以下可能是使用该设备的典型命令行会话:
```shell
$ cat /dev/misc/demo-number
0
$ cat /dev/misc/demo-number
1
$ cat /dev/misc/demo-number
2
```
`/dev/misc/demo-null`立即返回EOF,`/dev/misc/demo-zero`返回永无止境的零,而`/dev/misc/demo-number`是中间的一种:它需要返回一个短数据序列,然后返回EOF。
在现实世界中,client端可以一次读取一个字节,或者它可以请求大缓冲区来存下足够的数据。
对于我们的初始版本,我们假设client端要求一个“大”的缓冲区,足以立即获取所有数据。
这意味着我们可以采取捷径。
有一个偏移参数(`zx_off_t off`)作为第4个参数传递给** read()**处理函数:
```C
static zx_status_t
number_read(void * ctx,void * buf,size_t count,zx_off_t off,size_t * actual)
```
这表明client希望开始(或继续)read的位置。
我们在这里进行的简化是,如果client端的偏移量为零,则意味着它从头开始读,所以我们返回的数据与client端可以处理的数据一样多。
但是,如果偏移量不为零,则返回“EOF”。
让我们讨论代码(请注意,我们最初提供的版本比在源目录中版本稍微简单一些):
```C
static int global_counter; //好与坏,见下文
static zx_status_t
number_read(void * ctx,void * buf,size_t count,zx_off_t off,size_t * actual){
//(1)我们为什么在这里?
if(off == 0){
//(2)第一次read;尽可能多地返回数据
int n = atomic_add(&global_counter);
char tmp [22]; // 2^64是20位+ \ n + nul = 22个字节
* actual = snprintf(tmp,sizeof(tmp),“%d \ n”,n);
if(* actual> count){
* actual = count;
}
memcpy(buf,tmp,* actual);
} else {
//(3)不是第一次 - 返回EOF
* actual = 0;
}
return ZX_OK;
}
```
我们做出的第一个决定是在步骤(1)中,我们确定client是否是第一次read字符串。
如果偏移量为零,那么这是第一次。
在这种情况下,在步骤(2)中,我们从`global_counter`中获取一个值,将其放入一个字符串中,并告诉client端我们返回了一些字节数。
我们返回的字节数限制为以下判断的较小值:
*client端缓冲区的大小(由`count`给出)
*生成的字符串的大小(从** snprintf()**返回的)。
但是,如果偏移量不为零,则意味着它不是client端的第一次从该设备读取数据。
在这种情况下,在步骤(3)中,我们只需设置我们返回的字节数('* actual`)为零,这具有向client端指示“EOF”的效果(就像它在上面的`null`驱动程序中完成了一样。
### Globals(全局变量)很糟糕
我们使用的`global_counter`对驱动程序来说是全局的。
这意味着最终调用** number_read()**的每个会话都以增加这个数字结束。
这是预期的&mdash;毕竟,`/dev/misc/demo-number`的工作是“分发增长的数字给client”。
可能没有预期的是,如果驱动程序被多次实例化(例如,使用真实的硬件驱动程序,就可能会发生),然后这个全局变量就会在不同的实例中共享。
通常,这不是您想要的真正的硬件驱动程序(因为每个驱动程序实例是独立的)。
解决方案是创建“per-device”上下文块;这个上下文块将包含每个设备唯一的数据。
为了创建每个设备的上下文块,我们需要调整我们的绑定例程。
回想一下,绑定例程是在设备和其protocol ops之间建立关联的地方。
如果我们要在绑定例程中创建上下文块,那么我们就可以了在我们稍后的读处理程序中使用它:
```C
typedef struct {
zx_device_t * zxdev;
uint64_t counter;
} number_device_t;
zx_status_t
number_bind(void * ctx,zx_device_t * parent){
//分配和初始化每个设备的上下文块
number_device_t * device = calloc(1,sizeof(* device));
if(!device){
return ZX_ERR_NO_MEMORY;
}
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name =“demo-number”,
.ops =&number_device_ops,
.ctx =device,
};
zx_status_t rc = device_add(parent,&args,&device-> zxdev);
if(rc!= ZX_OK){
free(device);
}
return rc;
}
```
这里我们已经分配了一个上下文块并将其存储在`device_add_args_t`的`ctx`成员中,`args`我们传递给** device_add()**。
现在,在绑定时创建的唯一的上下文块实例与每个绑定的设备实例相关联,并可使用所有的通过** number_bind()**绑定的protocol函数。
请注意,虽然我们不使用上下文块中的`zxdev`设备,但这是一个很好的做法,如果我们以后需要它用于任何其他设备相关operation,请坚持下去。
图 TODO
上下文块可以在`number_device_ops`定义的所有protocol函数中使用,例如
我们的** number_read()**函数:
```C
static zx_status_t
number_read(void * ctx,void * buf,size_t count,zx_off_t off,size_t * actual){
if(off == 0){
number_device_t * device = ctx;
int n = atomic_fetch_add(&device-> counter,1);
// ------------------------------------------------
//其他一切与以前的版本相同
// ------------------------------------------------
char tmp [22]; // 2^64是20位+ \ n + \ 0
* actual = snprintf(tmp,sizeof(tmp),“%d \ n”,n);
if(* actual> count){
* actual = count;
}
memcpy(buf,tmp,* actual);
} else {
* actual = 0;
}
return ZX_OK;
}
```
请注意我们如何使用上下文块中的值替换原始版本的`global_counter`。
使用上下文块,每个设备都有自己独立的计数器。
###清理上下文
当然,每次我们** calloc()**的时候,我们都要在某处** free()**。
这是在我们的** number_release()**处理程序中完成的,我们将它存储在`zx_protocol_device_t中
number_device_ops`结构:
```C
static zx_protocol_device_t
number_device_ops = {
//其他初始化...
.release = number_release,
};
```
** number_release()**函数很简单:
```C
static void
number_release(void* ctx) {
free(ctx);
}
```
在卸载驱动程序之前调用** number_release()**函数。
###控制您的设备
有时,需要向设备发送控制消息。
这是不通过** read()** / ** write()**接口传播的数据。
例如,在`/dev/misc/demo-number`中,我们可能想要一种将计数预设为给定数字的方法。
在传统的POSIX环境中,这是通过client端上的** ioctl()**调用完成的
在驱动程序端,以及适当的** ioctl()**处理程序。
在Fuchsia下,通过FIDL语言来编组数据
([** FIDL **](https://fuchsia.googlesource.com/fuchsia/+/master/docs/development/languages/fidl/README.md))。
有关FIDL本身的更多详细信息,请参阅上面的参考。
对于我们这里的目的,FIDL:
*由类似C语言描述,
*用于定义控件功能的输入和输出参数,
*为client端和驱动程序端生成代码。
>如果您已经熟悉Google的“Protocol Buffers”,那么您使用FIDL会非常舒服。
FIDL有许多优点。
因为输入和输出参数是明确定义的,结果是在client端和驱动程序上生成具有严格类型安全性和检查的代码。
通过从其实现中抽象出消息的定义,FIDL代码生成器可以生成多种不同语言的代码,无需额外的工作。
这特别有用,例如,当client需要用您不一定熟悉的语言的API时。
####使用FIDL
在大多数情况下,您将使用设备已提供的FIDL API,并且很少需要创建自己的。
但是,了解机制是一个好主意,端到端。
使用FIDL进行设备控制很简单:
*在“.fidl”文件中定义输入,输出和接口,
*编译FIDL代码并生成client端函数
*将消息处理程序添加到驱动程序以接收控制消息。
我们将通过为我们的`/dev/misc/demo-number`驱动程序实现“预设值计数器”控制功能,来查看这些步骤。
####定义FIDL接口
我们需要做的第一件事是定义interface。
因为我们要做的就是将计数预设为用户指定的值,我们的interface非常简单。
这就是“`.fidl`”文件的样子:
```FIDL
library zircon.sample.number;
[Layout="Simple"]
interface Number {
// set the number to a given value
SetNumber(uint32 value) -> (uint32 previous);
};
```
第一行,`library zircon.sample.number;`为生成的库提供了一个名称。
接下来,`[Layout =“Simple”]`生成[简单的C绑定](https://fuchsia.googlesource.com/fuchsia/+/master/docs/development/languages/fidl/languages/c.md#simple-bindings).
最后,`interface`部分定义了所有可用的接口。
每个接口都有编号,有名称,并指定输入和输出。
在这里,我们有一个名为** SetNumber()**的接口函数,它接受一个`uint32`(即FIDL等效于C标准整数`uint32_t`类型)作为输入,并返回一个`uint32`结果(计数器更改的前一个值)。
我们将在下面看到更多高级示例。
####编译FIDL代码
FIDL代码由构建系统自动编译;你只需要添加一个依赖项
进入`rules.mk` makefile。
假设调用“.fidl`”文件,这就是独立的`rules.mk`的样子
`demo_number.fidl`:
```makefile文件
LOCAL_DIR:= $(GET_LOCAL_DIR)
MODULE:= $(LOCAL_DIR)
MODULE_TYPE:= fidl
MODULE_PACKAGE:= fidl
MODULE_FIDL_LIBRARY:= zircon.sample.number
MODULE_SRCS + = $(LOCAL_DIR)/demo_number.fidl
include make / module.mk
```
编译完成后,接口文件将显示在构建输出目录中。
确切的路径取决于构建目标(例如,...`/zircon/build-x64/`... x86 64位版本),以及包含FIDL文件的源目录。
对于此示例,我们将使用以下路径:
<dl>
<dt>...`/zircon/system/dev/sample/number/demo-number.c`
<dd>source file for `/dev/misc/demo-number` driver
<dt>...`/zircon/system/fidl/zircon-sample/demo_number.fidl`
<dd>source file for FIDL interface definition
<dt>...`/zircon/build-x64/system/fidl/zircon-sample/gen/include/zircon/sample/number/c/fidl.h`
<dd>generated interface definition header include file
</dl>
查看由FIDL编译器生成的接口定义头文件是有益的。
在这里,它会进行注释和编辑,以显示亮点:
```C
//(1)前向声明
#define zircon_sample_number_NumberSetNumberOrdinal((uint32_t)0x1)
//(2)外部声明
extern const fidl_type_t zircon_sample_number_NumberSetNumberRequestTable;
extern const fidl_type_t zircon_sample_number_NumberSetNumberResponseTable;
//(3)声明
struct zircon_sample_number_NumberSetNumberRequest {
fidl_message_header_t hdr;
uint32_t value;
};
struct zircon_sample_number_NumberSetNumberResponse {
fidl_message_header_t hdr;
uint32_t result;
};
//(4)client端绑定原型
zx_status_t
zircon_sample_number_NumberSetNumber(zx_handle_t _channel,
uint32_t value,
uint32_t * out_result);
//(5)FIDL消息operation结构
typedef struct zircon_sample_number_Number_ops {
zx_status_t(* SetNumber)(void * ctx,uint32_t value,fidl_txn_t * txn);
} zircon_sample_number_Number_ops_t;
//(6)调度原型
zx_status_t
zircon_sample_number_Number_dispatch(void * ctx,fidl_txn_t * txn,fidl_msg_t * msg,
const zircon_sample_number_Number_ops_t * ops);
zx_status_t
zircon_sample_number_Number_try_dispatch(void * ctx,fidl_txn_t * txn,fidl_msg_t * msg,
const zircon_sample_number_Number_ops_t * ops);
//(7)回复原型
zx_status_t
zircon_sample_number_NumberSetNumber_reply(fidl_txn_t * _txn,uint32_t result);
```
>请注意,此生成的文件包含与client端*和*驱动程序相关的代码。
简而言之,生成的代码表示:
1.命令编号的定义(“NumberOrdinal`”,回想一下我们使用命令
** SetNumber()**)的数字`1`,
2.表的外部定义(我们不使用这些),
3.请求和响应消息格式的声明;这些包括FIDL开销标题和我们指定的数据,
4.client端绑定原型&mdash;我们将看到client端如何使用以下内容,
5. FIDL消息operation结构;这是您在驱动程序中提供的功能列表
处理“.fidl`”文件中定义的每个FIDL接口,
6.显示原型&mdash;这是由我们的FIDL消息处理程序调用的,
7.回复原型&mdash;当我们想要回复client端时,我们在驱动程序中调用它。
####client端
让我们从一个基于命令行的小型client端开始,称为`set_number`,使用上面的FIDL接口。
它假定我们控制的设备称为`/dev/misc/demo-number`。
该程序只需要一个参数&mdash;设置当前计数器的数字。
这是程序operation的示例:
```bash
$ cat /dev/misc/demo-number
0
$ cat /dev/misc/demo-number
1
$ cat /dev/misc/demo-number
2
$ set_number 77
Original value was 3
$ cat /dev/misc/demo-number
77
$ cat /dev/misc/demo-number
78
```
完整的程序如下:
```C
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <zircon/syscalls.h>
#include <lib/fdio/util.h>
// (1) include the generated definition file
#include <zircon/sample/number/c/fidl.h>
int main(int argc, const char** argv)
{
static const char* dev = "/dev/misc/demo-number";
// (2) get number from command line
if (argc != 2) {
fprintf(stderr, "set_number: needs exactly one numeric argument,"
" the value to set %s to\n", dev);
exit(EXIT_FAILURE);
}
uint32_t n = atoi(argv[1]);
// (3) establish file descriptor to device
int fd = open(dev, O_RDWR);
if (fd == -1) {
fprintf(stderr, "set_number: can't open %s for O_RDWR, errno %d (%s)\n",
dev, errno, strerror(errno));
exit(EXIT_FAILURE);
}
// (4) establish handle to FDIO service on device
zx_handle_t num;
zx_status_t rc;
if ((rc = fdio_get_service_handle(fd, &num)) != ZX_OK) {
fprintf(stderr, "set_number: can't get fdio service handle, error %d\n", rc);
exit(EXIT_FAILURE);
}
// (5) send FDIO command, get response
uint32_t orig;
if ((rc = zircon_sample_number_NumberSetNumber(num, n, &orig)) != ZX_OK) {
fprintf(stderr, "set_number: can't execute FIDL command to set number, error %d\n", rc);
exit(EXIT_FAILURE);
}
printf("Original value was %d\n", orig);
exit(EXIT_SUCCESS);
}
```
这与使用POSIX ** ioctl()**的方法非常相似,不同之处在于:
*我们建立了FDIO服务的句柄(步骤4),和
* API对特定类型operation是type-safe 和 prototyped(步骤5)。
请注意,FDIO命令有一个很长的名称:** zircon_sample_number_NumberSetNumber()**
(其中包括大量重复)。
这是FIDL编译器代码生成过程的一个反应&mdash;该
“`zircon_sample_number`”部分来自“`library zircon.sample.number`”
声明,第一个“`Number`”来自“`interface Number`”语句和最后一个
“`SetNumber`”是接口定义语句中接口的名称。
####向驱动程序添加消息处理程序
在驱动方面,我们需要:
*处理FIDL消息
*解复用消息(找出它是哪个控制消息)
*生成回复
结合上面的原型,来处理我们的FIDL控制消息
我们需要绑定一个消息处理函数(就像我们按顺序执行的那样)
处理** read()**,例如):
```C
static zx_protocol_device_t number_device_ops = {
.version = DEVICE_OPS_VERSION,
.read = number_read,
.release = number_release,
.message = number_message,//处理FIDL消息
};
```
在这种情况下,** number_message()**函数是微不足道的;它只是包装发货功能:
```C
static zircon_sample_number_Number_ops_t number_fidl_ops = {
.SetNumber = fidl_SetNumber,
};
static zx_status_t number_message(void * ctx,fidl_msg_t * msg,fidl_txn_t * txn){
zx_status_t status = zircon_sample_number_Number_dispatch(ctx,txn,msg,&number_fidl_ops);
return status;
}
```
生成的** zircon_sample_number_Number_dispatch()**函数接收传入消息
并根据提供的函数表调用适当的处理函数`number_fidl_ops`。
当然,在我们这个简单的例子中,只有一个函数`SetNumber`:
```c
static zx_status_t fidl_SetNumber(void* ctx, uint32_t value, fidl_txn_t* txn)
{
number_device_t* device = ctx;
int saved = device->counter;
device->counter = value;
return zircon_sample_number_NumberSetNumber_reply (txn, saved);
}
```
** fidl_SetNumber()**处理程序:
*建立指向设备上下文的指针,
*保存当前计数值(以便以后可以返回),
*将新值设置到设备上下文中,并且
*调用“回复”函数将值返回给客户端。
请注意,** fidl_SetNumber()**函数具有与FIDL匹配的原型规格,确保类型安全。同样,回复功能,
** zircon_sample_number_NumberSetNumber_reply()**也符合FIDL规范的接口定义结果部分的原型。
####高级用途
FIDL表达式当然可以比我们上面显示的更复杂。
例如,可以使用嵌套结构,而不是简单的`uint32`。
输入和输出都允许多个参数。见
[FIDL参考](https://fuchsia.googlesource.com/fuchsia/+/master/docs/development/languages/fidl/README.md)。
##使用`/dev/misc/demo-multi`注册多个设备
到目前为止,讨论的设备是“单身人士”&mdash;也就是说,一个注册名称做了一件事
(`null`表示空设备,`number`表示数字设备,依此类推)。
如果您拥有一组所有执行类似功能的设备,该怎么办?
例如,您可能有一个具有16个通道的某种多通道控制器。
处理这个问题的正确方法是:
1.创建一个驱动程序实例,
2.创建基本设备节点,和
3.在该基本设备下显示您的子设备。
如上所述,创建驱动程序实例是一种很好的做法,在“Globals is bad”中(我们稍后在这个特定的背景下再讨论一下)。
在这个例子中,我们将创建一个基本设备`/dev/misc/demo-multi`,然后我们将在名为“0”到“15”的情况下创建16个子设备(例如,`/dev/misc/demo-multi/7`)。
```c
static zx_protocol_device_t multi_device_ops = {
.version = DEVICE_OPS_VERSION,
.read = multi_read,
.release = multi_release,
};
static zx_protocol_device_t multi_base_device_ops = {
.version = DEVICE_OPS_VERSION,
.read = multi_base_read,
.release = multi_release,
};
zx_status_t multi_bind(void* ctx, zx_device_t* parent) {
// (1) allocate & initialize per-device context block
multi_root_device_t* device = calloc(1, sizeof(*device));
if (!device) {
return ZX_ERR_NO_MEMORY;
}
device->parent = parent;
// (2) set up base device args structure
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.ops = &multi_base_device_ops, // use base ops initially
.name = "demo-multi",
.ctx = &device->base_device,
};
// (3) bind base device
zx_status_t rc = device_add(parent, &args, &device->base_device.zxdev);
if (rc != ZX_OK) {
return rc;
}
// (4) allocate and bind sub-devices
args.ops = &multi_device_ops; // switch to sub-device ops
for (int i = 0; i < NDEVICES; i++) {
char name[ZX_DEVICE_NAME_MAX + 1];
sprintf(name, "%d", i);
args.name = name; // change name for each sub-device
device->devices[i] = calloc(1, sizeof(*device->devices[i]));
if (device->devices[i]) {
args.ctx = &device->devices[i]; // store device pointer in context
device->devices[i]->devno = i; // store number as part of context
rc = device_add(device->base_device.zxdev, &args, &device->devices[i]->zxdev);
if (rc != ZX_OK) {
free(device->devices[i]); // device "i" failed; free its memory
}
} else {
rc = ZX_ERR_NO_MEMORY;
}
// (5) failure backout
if (rc != ZX_OK) {
for (int j = 0; j < i; j++) {
device_remove(device->devices[j].zxdev);
free(device->devices[j]);
}
device_remove(device->base_device.zxdev);
free(device);
return rc;
}
}
return rc;
}
// (6) release the per-device context block
static void multi_release(void* ctx) {
free(ctx);
}
```
步骤是:
1.建立设备上下文指针,以防多次加载此驱动程序。
2.创建并初始化我们将传递给** device_add()**的`args`结构.
该结构具有基本设备名称,“`demo-multi`”和上下文指针到基本设备上下文块`base_device`。
3.调用** device_add()**添加基本设备。
现在已经创建了`/dev/misc/demo-multi`。
请注意,我们将新创建的设备存储到`base_device.zxdev`中。这稍后将作为子设备的“父”设备。
4.现在创建16个子设备作为基础(“父”)设备的子设备。
请注意,我们将`ops`成员更改为指向子设备协议操作`multi_device_ops`而不是基本版本。
每个子设备的名称只是设备编号的ASCII表示。
请注意,我们将设备编号索引`i`(0 .. 15)存储在`devno`中作为上下文
(我们有一个称为`multi_devices`的上下文数组,我们很快就会看到)。
我们还说明了动态分配每个子设备,而不是在父结构中分配其空间。
对于“热插拔”设备来说,这是一个更现实的用例&mdash;你没有必要分配一个大的上下文结构,或者执行初始化工作,对于尚未存在的设备。
5.如果发生故障,我们需要删除和释放我们已添加的设备,包括基本设备和每设备上下文块。
请注意,我们释放了但不包括失败的设备索引。这就是** device_add()**失败,我们在步骤4中对子设备结构调用** free()**的原因。
6.我们在发布处理程序中发布了每设备上下文块。
###哪个设备是哪个?
我们有两个** read()**函数,** multi_read()**和** multi_base_read()**。
这使我们可以使用不同的行为来读取基本设备.
read 16个子设备中的一个。
读取的基本设备几乎与我们在`/dev/misc/demo-number`中看到的相同:
```C
static zx_status_t
multi_base_read(void * ctx,void * buf,size_t count,zx_off_t off,size_t * actual){
const char * base_name =“base device \ n”;
if(off == 0){
* actual = strlen(base_name);
if(* actual> count){
* actual = count;
}
memcpy(buf,base_name,* actual);
} else {
* actual = 0;
}
return ZX_OK;
}
```
这只是为读取返回字符串“`base device \ n`”,直到client端允许的字节数的最大值。
但是子设备的读取需要知道哪个设备被调用。
我们在单个子设备上下文块中保留一个名为`devno`的设备索引:
```C
typedef struct {
zx_device_t * zxdev;
int devno; //设备号(索引)
} multidev_t;
```
存储16个子设备以及基本设备的上下文块在上面的绑定函数的步骤(1)中创建的每设备上下文块。
```C
//这包含我们的每个设备实例
#define NDEVICES 16
typedef struct {
zx_device_t * parent;
multidev_t * devices [NDEVICES]; //指向我们的16个子设备的指针
multidev_t base_device; //我们的基础设备
} multi_root_device_t;
```
请注意,每个设备的`multi_root_device_t`上下文结构包含1个`ultidev_t`
上下文块(用于基本设备)和16个指向动态分配上下文的子设备块的指针。
这些上下文块的初始化发生在步骤(3)中(对于基本设备)和(4)(在每个子设备的`for`循环中完成)。
图 TODO
上图说明了每设备和和个别设备上下文块之间的关系。
子设备7代表所有子设备。
这就是我们的** multi_read()**函数的样子:
```C
static const char* devnames[NDEVICES] = {
"zero", "one", "two", "three",
"four", "five", "six", "seven",
"eight", "nine", "ten", "eleven",
"twelve", "thirteen", "fourteen", "fifteen",
};
static zx_status_t
multi_read(void* ctx, void* buf, size_t count, zx_off_t off, size_t* actual) {
multidev_t* device = ctx;
if (off == 0) {
char tmp[16];
*actual = snprintf(tmp, sizeof(tmp), "%s\n", devnames[device->devno]);
if (*actual > count) {
*actual = count;
}
memcpy(buf, tmp, *actual);
} else {
*actual = 0;
}
return ZX_OK;
}
```
从命令行执行我们的设备会产生如下结果:
```shell
$ cat /dev/misc/demo-multi
base device
$ cat /dev/misc/demo-multi/7
seven
$ cat /dev/misc/demo-multi/13
thirteen
```
###多个multiple设备
为支持多个设备的控制器创建“每个设备”上下文块可能看起来很奇怪,但它与任何其他控制器没有什么不同。
如果这是一个真正的硬件设备(比如16通道数据采集系统),您当然可以将两个或更多这些插入您的系统。
每个驱动程序都将获得一个唯一的基本设备名称(例如`/dev/daq-0`,`/dev/daq-1`,依此类推),然后以该名称manifest其channel(例如,对于第二数据采集系统上的第8个通道,`/dev/daq-1/7`)。
理想情况下,应根据硬件提供了独特的密钥来为某种类型分配唯一的基本设备名称。
这具有可重复性/可预测性的优点,特别是对于热插拔设备。
例如,在数据采集的情况下,将连接不同的设备到每个控制器通道。
在重新启动或热插拔/重新插入事件之后,希望能够将每个控制器与已知的基本设备名称相关联;它没有让插件/拔出事件之间的设备名称随机更改。
##阻塞读写:`/dev/misc/demo-fifo`
到目前为止,我们检查过的所有设备都会立即返回数据(对于** read()**operation),或(在`/dev/misc/demo-null`的情况下),接受数据而不阻塞(对于** write()**operation)。
我们将讨论的下一个设备`/dev/misc/demo-fifo`如果有可用的数据,将立即返回数据,否则它将阻塞client端,直到数据可用。
同样,对于写入,如果有空间,它将立即接受数据,否则它将阻塞client端,直到空间可用。
read和write的私有处理者必须立即返回(无论是否有数据或空间是否可用)。
但是,他们不必立即返回或接受*数据*;他们可以改为向client表明它应该等待。
我们的FIFO设备通过维护单个32kbyte FIFO来运行。
client端可以读取和写入FIFO,并将展示所讨论的在满条件和空条件下的阻塞行为。
###上下文结构
首先要看的是上下文结构:
```C
#define FIFOSIZE 32768
typedef struct {
zx_device_t* zxdev;
mtx_t lock;
uint32_t head;
uint32_t tail;
char data[FIFOSIZE];
} fifodev_t;
```
这是一个基本的循环缓冲区;数据被写入`head`表示的位置,并从`tail`指示的位置读取。
如果`head == tail`那么FIFO是空的,如果`head`就在`tail`之前(使用环绕数学)表示FIFO已满,否则它有一些数据和一些空间可用。
从更高的层面看,** fifo_read()**和** fifo_write()**函数几乎相同,
所以让我们从** fifo_write()**开始:
```C
static zx_status_t
fifo_write(void* ctx, const void* buf, size_t len,
zx_off_t off, size_t* actual) {
// (1) establish context pointer
fifodev_t* fifo = ctx;
// (2) lock mutex
mtx_lock(&fifo->lock);
// (3) write as much data as possible
size_t n = 0;
size_t count;
while ((count = fifo_put(fifo, buf, len)) > 0) {
len -= count;
buf += count;
n += count;
}
if (n) {
// (4) wrote something, device is readable
device_state_set(fifo->zxdev, DEV_STATE_READABLE);
}
if (len) {
// (5) didn't write everything, device is full
device_state_clr(fifo->zxdev, DEV_STATE_WRITABLE);
}
// (6) release mutex
mtx_unlock(&fifo->lock);
// (7) inform client of results, possibly blocking it
*actual = n;
return (n == 0) ? ZX_ERR_SHOULD_WAIT : ZX_OK;
}
```
在步骤(1)中,我们建立一个指向该设备实例的上下文块的上下文指针。
接下来,我们在步骤(2)中锁定互斥锁。
这样做是因为我们的驱动程序中可能有多个线程,而我们不希望他们互相干涉。
缓冲管理在步骤(3)中执行&mdash;我们稍后会检查实现。
了解在步骤(3)之后需要采取的措施非常重要:
*如果我们写了一个或多个字节(由'n`表示非零),我们需要
将设备标记为“可读”(通过** device_state_set()**
和'DEV_STATE_READABLE`),
这在步骤(4)中完成。我们这样做是因为数据现在可用。
*如果我们还有剩余的字节要写(由'len`表示非零),我们
需要将设备标记为“不可写”(通过** device_state_clr()**和
`DEV_STATE_WRITABLE`),在步骤(5)完成。我们知道FIFO已满,因为
我们无法写出所有数据。
我们可能依赖于执行步骤(4)和(5)中的一个或两个关于write期间发生的事情。
我们将始终至少执行其中一个,因为`n`和`len`不能同时执行为零。
这意味着一个不可能的条件:我们既没有写任何数据(`n`,传输的总字节数,为零),但同时又写入了所有数据(`len`,要传输的剩余字节数也为零)。
在步骤(7)中,做出关于阻塞client端的决定。
如果`n`为零,则意味着我们无法写入任何数据。
在这种情况下,我们返回`ZX_ERR_SHOULD_WAIT`。
此返回值会阻塞client端。
当** device_state_set()**时,client端被解锁,函数在步骤(2)中从** fifo_read()**处理程序调用:
```c
static zx_status_t
fifo_read(void* ctx, void* buf, size_t len,
zx_off_t off, size_t* actual) {
fifodev_t* fifo = ctx;
mtx_lock(&fifo->lock);
size_t n = 0;
size_t count;
while ((count = fifo_get(fifo, buf, len)) > 0) {
len -= count;
buf += count;
n += count;
}
// (1) same up to here; except read as much as possible
if (n) {
// (2) read something, device is writable
device_state_set(fifo->zxdev, DEV_STATE_WRITABLE);
}
if (len) {
// (3) didn't read everything, device is empty
device_state_clr(fifo->zxdev, DEV_STATE_READABLE);
}
mtx_unlock(&fifo->lock);
*actual = n;
return (n == 0) ? ZX_ERR_SHOULD_WAIT : ZX_OK;
}
```
算法的形状与书写案例相同,但有两点不同:
1.我们正在读数据,所以调用** fifo_get()**而不是** fifo_put()**
2.“DEV_STATE”逻辑是互补的:在write情况下,我们设置可读
并且清除可写,在read案例中我们设置了可写和清除可读。
与write案例类似,在`while`循环之后,我们将执行其中一个或两个以下行动:
*如果我们读取一个或多个字节(由'n`表示为非零),我们需要标记设备现在可写(我们消耗数据,因此现在有一些空间可用)。
*如果我们仍然有字节要读(如'len`表示非零),我们标记设备为空(我们没有获得所有数据,所以这一定是因为我们耗尽了装置)。
如在书面案例中,将执行上述动作中的至少一个。
为了使它们都不执行,都是'n`(读取的字节数)和`len`(要读取的字节数)必须为零,这意味着不可能,几乎是形而上学的条件,即同时read任何内容和所有内容。
>此处还有一个额外的微妙之处。
>当`n`为零时,我们*必须*返回`ZX_ERR_SHOULD_WAIT`&mdash;我们不能返回`ZX_OK`。
>将`* actual`设置为零返回`ZX_OK`表示EOF,这绝对不是这里的情况。
###读写交互
正如您所看到的,读取处理程序允许阻塞写入client端来取消阻塞,并且
write handler是允许阻塞读取client端来解除阻塞的原因。
当client端被阻塞时(通过`ZX_ERR_SHOULD_WAIT`返回代码),它被对应的** device_state_set()**唤醒。此唤醒动作使client端再次尝试其读取或写入操作。
请注意,client端被唤醒后无法保证成功。我们可以有多个读者,例如,等待数据。
假设所有这些都被阻塞,因为FIFO是空的。另一个client端出现并写入FIFO。这会导致** device_state_set()**使用`DEV_STATE_READABLE`调用函数。其中一个client端可能会消耗所有可用数据;该其他client端将尝试读取,但将获得`ZX_ERR_SHOULD_WAIT`并将阻塞。
###缓冲管理
正如所承诺的那样,为了完整起见,这里是对缓冲管理的快速检查。这两个例程都很常见。
我们将查看读取路径(写入路径几乎相同)。
在read函数的核心,我们看到:
```c
size_t n = 0;
size_t count;
while ((count = fifo_get(fifo, buf, len)) > 0) {
len -= count;
buf += count;
n += count;
}
```
三个变量`n`,`count`和`len`是相互关联的。
传输的总字节数存储在`n`中。
在每次迭代期间,`count`获取传输的字节数,并将其用作控制`while`循环的基础。
变量`len`表示要传输的剩余字节数。
每次循环时,`len`减少了传输的字节数,`n`相应的增加。
因为FIFO是作为循环缓冲区实现的,所以它意味着一个完整的数据集可能在FIFO中连续定位,也可能是环绕的FIFO的结束回到开头。
底层的** fifo_get()**函数可以获得尽可能多的数据,而无需包装。
这就是`while`循环要“retry” operation的原因;看它能否得到更多的数据可能是由于`tail`回绕到缓冲区的开头。
我们会在调用** fifo_get()**一到三次。
1.如果FIFO为空,我们只需调用一次。
它将返回零,表示没有可用的数据。
2.如果数据连续位于底层FIFO缓冲区中,我们称之为第二次;
第一次获取数据,第二次返回零,表明
没有更多数据可用。
3.如果数据在缓冲区中被循环覆盖,我们将调用它第三次。
一次获得第一部分,第二次获得环绕部分,第三次将返回零,表示没有更多数据可用。