-
Notifications
You must be signed in to change notification settings - Fork 3
/
systemcall.re
233 lines (160 loc) · 15.5 KB
/
systemcall.re
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
={systemcall} binderドライバを扱う三つのシステムコール - open, mmap, ioctl
//lead{
Binderの仕組みのうち、一番下のレイヤとなるのがbinderドライバです。
binderドライバ自体はとても単純な物で、提供している機能もかなり原始的です。
この節から続く三つの節で、binderドライバについて解説していきます。
この節ではbinderドライバを扱う基本的なシステムコールである、open(), mmap(), ioctl()について、使い方の例を見ていきます。
次の@<chapref>{driver_message}ではこれらのシステムコールを使ってどのようにメッセージを送受信するか、その内容と使い方についてみていきます。
@<chapref>{flat_binderobj}ではこれらのメッセージで送信できるものについて、ドライバの内部実装と合わせて見ていきます。
なお、本章でサービスと言った場合はシステムサービスの事だとします。
//}
== binderドライバの特徴
binderドライバはプロセス間通信の仕組みです。
サービスというプログラミングモデルでシステムを組む為に、それ専用のプロセス間通信の仕組みを作った、それがbinderドライバです。
以下のような特徴があります。
1. ローカルに特化している
2. ドライバとして実装されている為、Linuxカーネルの内部データ構造を用いる事が出来る
3. サービスというプログラミングモデルを前提としていて、スレッドプールやリファレンスカウントのサポートが最初から入っている
4. 呼び出し元のプロセスのuidを正確に把握している
5. ファイルディスクリプタを送信出来る
以上を@<hd>{intro|Binderの特徴}と比較すると、Binderの特徴の多くはそのドライバで実現されている事に気づきます。
世の中にはたくさんのプロセス間通信の仕組みやその上の分散オブジェクトの仕組みがありますが、
分散オブジェクトシステムの為だけに作られたプロセス間通信と、そのプロセス間通信だけで動く分散オブジェクトシステム、というのは珍しいのではないでしょうか。
少なくとも私はBinderしか知りません。
binderドライバはプロセス間通信の仕組みとしては、TCP/IPやUDPのような本格的なプロトコルスタックを持つ物と比較すると、かなりシンプルな物と言えます。
一方で相手のスレッドやリファレンスカウントといったものをサポートしているという点で、パイプなどの原始的な仕組みに比べるとやや複雑と言えます。
多くの分散オブジェクトではIPCのレベルではスレッドやリファレンスカウントの概念を持たない物が多いので、
その上の分散オブジェクトのシステムでその仕組みを持つ事になります。
一方BinderはIPCのレベルでそのサポートを持つので、上のオブジェクトシステムが複雑になるのを防いでいます。
また、デバイスドライバとして直接カーネルモードで動くように実装されているのも特徴的です。
相手のスレッドを起こすのも、カーネルのタスク構造体に直接アクセスして、まるで通常のブロック型デバイスのファイルreadと同じように起こす為、単純明快です。
カーネルのデータ構造に直接アクセスできるため、相手のuidを直接参照したり、ファイルディスクリプタテーブルを書き換えたり、と言った事も簡単に出来ます。
uidを正確に把握している、というのは、地味ですがリモートにも送る事が出来るメッセージング機構だと素直には作りづらい所があります。
uidというのはシステムローカルな物だからです。
ですがシステム内でしか使われない事を前提としているBinderでは変に抽象化せずに直接uidを使う事が出来ます。
4章でも触れた通り、Androidはアプリのプロセスを別々のuidに設定する事でセキュリティを確保している為、uidを調べる事は頻繁に発生します。
カーネルモジュールはcurrentという参照を通じて呼び元のプロセスの情報にアクセス出来るので、
カーネルモジュールのドライバとしてプロセス間通信を実装するなら、呼び元のプロセスのuidを調べる、
という機能は、ストレートに実装出来ます。
== binderドライバの使い方
binderドライバの実装の細かい話に入る前に、実際にbinderドライバを使用するコードの全体像がどうなるかを見てみましょう。
binderドライバは /dev/binder というファイルとして存在していて、binderドライバを使うプロセスはこれをopenしたりioctlしたりします。
#@# TODO: コラムに参照
binderドライバはプロセス間通信の仕組みです。何かしらのデータをプロセスを越えて送る物、と言えます。
送る物の詳細は後に回して、ここではdataというデータで長さがlenの物を送る、という前提でのコードを見てみましょう。
データ送信の単純化したコードの全体像を示すと以下のようになります。
//list[driverall][binderドライバを用いた呼び出し全体]{
// 1. binderドライバをオープン
fd = open("/dev/binder", O_RDWR);
// 2. binderドライバをメモリ領域にmmap。サイズは128K Bytes
mmap(NULL, 128*1024, PROT_READ, MAP_PRIVATE, fd, 0);
// 3. readとwriteに使う引数の初期化。readとwriteは同時に行える
struct binder_write_read bwr;
// write関連初期化
bwr.write_size = len;
bwr.write_consumed = 0;
bwr.write_buffer = (uintptr_t) data;
// read関連初期化、今回はreadはしないので0を入れておく。
bwr.read_size = 0;
bwr.read_consumed = 0;
bwr.read_buffer = 0;
// 4. binderドライバのioctl呼び出し
res = ioctl(fd, BINDER_WRITE_READ, &bwr);
//}
上記のコードはデータを送信するのに必要な全コードを最初から最後まで書いています。
個々のブロックの意味についてはこれから説明していきますが、まずは全体がこんな感じになる、という事を見てください。
上記のコードにもあるように、binderドライバを使用する典型的な手続きとしては以下のようになります。
1. open("/dev/binder", O_RDWR) を呼び出す
2. 1で得られたfdをmmapする
3. binder_write_readという構造体に送りたいデータを指定してBINDER_WRITE_READでioctl
実用の場面では、openとmmapはプロセスの初期化で一回行い、以後はioctlの所だけを繰り返し呼び出して通信を行います。
上記コードのコメントで言う所の1と2はプロセスの初期化の所で一回行うだけ、3と4はメッセージの呼び出しの都度行うという事です。
ioctlは送信と通信をどちらも担当します。
このioctlを呼び出したあとに、bwrのwrite_consumedやread_consumedの値を見て、書き込み、読み込みのどちらが処理されたかを判断します。
以下では上記のそれぞれのコードについての詳細を説明していきたいと思います。
== binderドライバのopenとmmap
zzzこのパラグラフはボツ
#@# TODO: ↑何か導入のお話は置きたい
ユーザープロセスのメモリ空間はそれぞれ異なる為、通常の方法で他人のプロセスのデータを触ったりコードを呼んだりは出来ません。
そこでカーネルの力を借りる必要が出てきます。
プロセスが切り替わってもカーネルのメモリ空間はそのままの為、カーネル空間にデータを書けば、それを別のプロセス空間でもアクセスする事が出来ます。
ですがユーザープロセスは直接はカーネルのメモリはアクセス出来ない為、何らかのシステムコールが必要となります。
binderドライバを使うプロセスは、まずデバイスファイルである/dev/binderファイルをopenします。
//list[open][binderドライバのオープン]{
// 1. binderドライバをオープン
fd = open("/dev/binder", O_RDWR);
//}
openはファイルディスクリプタを返します。
以後の操作はこのopenで返ってくるファイルディスクリプタに対して行います。
binderドライバを使う時には、openした後にこのデバイスファイルをmmapする事になっています。
//list[mmap][binderドライバをmmap]{
// 2. binderドライバをメモリ領域にmmap
mmap(NULL, 128*1024, PROT_READ, MAP_PRIVATE, fd, 0);
//}
mmapはフラグが多いので全てを説明はしませんが、ポイントとなる一番目と二番目の引数だけ簡単に説明しておきます。
先頭の引数はユーザーのアドレス空間のどこにマップするかを指定します。NULLを指定するとカーネルが勝手に選びます。
二番目の引数はマップするサイズです。ここでは128K Bytesの範囲をmmapするように指示しています。
普通mmapはファイルの中身をメモリ空間にマップする為のAPIです。
ですがmmapの呼び出しは内部ではドライバに処理が委譲されていて、ドライバごとに違う処理を行う事も出来ます。
実際binderドライバはただファイルの中身をメモリにマップしている、というのとは、だいぶ異なる挙動をしています。
binderドライバファイルをmmapすると、内部でドライバはカーネル空間に指定されたサイズくらいの送受信用のバッファを確保します。
そしてそのカーネル空間に割り当てたメモリと同じ物理メモリを、ユーザー空間にもマップします。
そしてこのカーネル空間にマップされたメモリは、以後ドライバ側で送受信に使われます。
ユーザー空間にマップされている領域は、ioctlの戻りなどで使われていますが、直接コードの中でそこのアドレスをあらわに参照する事はありません。
何故こういう事が必要か、というのは、少しカーネルのコンテキストスイッチに慣れていないと想像しにくいかもしれません。
仮想メモリから実メモリへの参照、というのは、基本的にはハードウェアで自動的に行われています。
カーネルはハードウェアに仮想メモリと実メモリの対応表をセットしたり、といった操作はしますが、
実際にカーネルなどのソフトウェアが仮想メモリのアドレスから実メモリをたどって値を読んだりはしません。
ドライバにとっても、アクセス出来るメモリは現在のプロセスのメモリ空間とカーネルのメモリ空間だけなのです。
そのどちらでも無い、例えば送信先のプロセスのメモリを参照する方法はありません。
そこで送信先プロセスにどのようにデータを渡すか、というと、送信先プロセスが「現在のプロセス」の時に、
送信先プロセスのメモリ空間とカーネルのメモリ空間で同じ物理メモリを指すようにマップするのです。
この時は送信先プロセスがまだ現在のプロセスなので、こういう操作がドライバから可能です。
そして送信元のプロセスにコンテキストスイッチしてしまっても、このカーネル空間のメモリに書きこんでおけば、後で送信先のプロセスにコンテキストスイッチした時も、ユーザー空間にコピーする事無くそのまま参照出来ます。
コンテキストスイッチした時にマップされる場所をカーネル側にも置いておく事で、
コンテキストスイッチせずにコンテキストスイッチした後のメモリ空間にデータを置いて置ける訳です。
//image[3_3_1][同一物理メモリをマップする]
== binderドライバのioctlと読み書き
Binderを用いたメッセージの送受信には、ioctl システムコールを使います。
ioctlについては「ioctlシステムコール」を参照ください。
#@#ioctlについては@<column>{ioctlシステムコール}を参照ください。
#@# TODO: ioctlシステムコールコラムを持ってくる
メッセージの送受信に使うリクエストIDはBINDER_WRITE_READです。
呼び出しは以下のようなコードとなります。
//list[ioctl][binderドライバのioctl]{
// 4. binderドライバのioctl呼び出し
res = ioctl(fd, BINDER_WRITE_READ, &bwr);
//}
三番目の引数の&bwrというのは、送受信に使う構造体、binder_write_readのポインタです。
BINDER_WRITE_READは送信と受信を一度に出来るAPIとなっている為、
binder_write_readには読み取り関連の設定と書き込み関連の設定を行う事になっています。
書き込み関連は以下のようにバッファと長さ、そして現在どこまで書いたかを表すconsumedを初期化します。
//list[bwr_write][binder_write_readのwrite側の初期化]{
struct binder_write_read bwr;
// write関連初期化
bwr.write_size = len;
bwr.write_consumed = 0;
bwr.write_buffer = (uintptr_t) data;
//}
ここでdataはドライバに送りたいデータの入ったポインタ、lenはそのデータの長さです。
読み込み関連は読み込みに使うバッファとそのサイズですが、読み込みをしない場合はread_sizeに0を入れておきます。
//list[bwr_read][binder_write_readのread側初期化]{
// read関連初期化、今回はreadはしないので0を入れておく。
bwr.read_size = 0;
bwr.read_consumed = 0;
bwr.read_buffer = 0;
//}
このように初期化したbwrをioctlに渡します。再掲すると以下のコードです。
//list[ioctl_again][binderドライバのioctl 再掲]{
// 4. binderドライバのioctl呼び出し
res = ioctl(fd, BINDER_WRITE_READ, &bwr);
//}
このようにioctlを呼ぶ事で、ドライバにデータを送ったり、逆にドライバからデータを受け取ったり出来ます。
以上で@<hd>{binderドライバの特徴}で示した、binderドライバを使ったAPI呼び出しの標準的なコードについて、一通りの説明を行いました。
しかし、ここまでの説明では、bwrのread_bufferの中身やwrite_bufferの中身、
つまりドライバにどういう種類のデータを書き込んで、ドライバからはどういう種類のデータが読み取れるのか、
という話は一切していません。
#@# TODO: ↑ここで言うデータについて、コードで言うとどこかという事を指示
binderドライバと送受信するデータの中身は、bwrのwrite_bufferやread_bufferの中でさらに決まりがあります。
今回の例で言うとbwr.write_bufferに渡しているデータの中で、さらに決まりがあるのです。
以後ではこの送受信のデータの詳細から、binderドライバという物の動きを具体的に見ていきましょう。