#簡單的驅動程式
本文檔是[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
$ 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
$ 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.如果資料在緩沖區中被循環覆寫,我們将調用它第三次。
一次獲得第一部分,第二次獲得環繞部分,第三次将傳回零,表示沒有更多資料可用。