プロジェクト演習 2 Linux プログラミング

プロジェクト演習 2
Linux プログラミング
山崎
2014 年度 第 3 版
この授業について
これまで授業では,ほとんど Windows を使ってきた.Windows 以外にもさまざまな OS があるが,中でも特
に多く利用されているものが UNIX である.UNIX とはある特定の一つの OS のことではない.さまざまな種類
があり,この授業で取り上げる Linux も UNIX の一つである.例えば,Apple 社の Macintosh 用の OS (MacOS
X) も UNIX の一種である.
Linux は,組込みシステムから大規模なサーバーまで,さまざまに利用されている.この授業では,Linux を組
込んだシステムの開発方法を理解し,自分でも簡単な開発ができるようになることを目標とする.
より具体的な到達目標は,以下の通りである.
• Linux がそれなりに使える
• shell スクリプトが作れる
• Linux 組込みシステムのクロス開発環境の設定やプログラム作成手順を理解し実行できる
• プロセスやドライバの動作を理解し説明できる
• ソケットないしドライバについてある程度の大きさのプログラムを作れるようになる
「プログラミング」などの授業との違いは何か? これまでは,プログラミング言語を習得するための授業だった.
言語を習得するだけでプログラミングできるような問題を解いてきた.例えば,文字の表示は,printf だけを
知っていれば十分だった.
しかし,実際のプログラミングはまったく違う.多くの場合,一つのシステムの中のある一部分を作っていくこ
とになる.OS を呼び出してグラフィクスを描いたり,他のアプリケーションとの間で文字をコピー・ペースト可
能にしたり,他のアプリケーションと同時並行に動作したりといったことが求められる.このためには,システム
の全体の基本的な動作を習得し,必要な API (Application Program Interface) を見つけ,その利用法を理解す
ることに非常に多くの時間を費やす.
また,他人の書いたプログラムを読むということもよくある.現在においては,何かやりたいと思ったときに,
助けになる元のプログラムが存在しないなどということはほとんどない.その分自分は楽になるのだが,その代
わりそのプログラムを読まなければならない.今回は,山崎の書いたプログラムを理解し,それをアレンジしてい
くことで,プログラムを完成させる.
更に,現在ではさまざまなツールがあり,個々の作業を簡単に行うことができる.しかし,何のために何を使う
のが効果的かをよく考えないと,作業効率がかえって悪化する.目的意識をもって,自分で作業環境を工夫してい
くことも,とても重要である.
今回は,組み込み開発環境の中であるソフトウェア (通信アプリまたはドライバ) を作ることで,上記のすべて
を演習していく.
授業の受け方: この授業は,会社で先輩や上司から話を聞いたり教わったりしながら,仕事を進めていくことを
イメージしている.すべてを口頭で説明できないので文書になっているが,故意と情報を散在させたり,情報を不
足させたり,一回説明したことを大幅に省略したりしている.
1
ノートをつける: 先輩 (TA) や上司 (山崎) に同じことを 2 回聞かないようにする (これも訓練).そこで自分で
ノートを作る.社会に出たら,きちんとノートを作ることは必須である.作業の意味を考えながら,どのようにま
とめたら良いかも考える.このノートは採点対象である.
グループについて: この授業は,前述のように新たに習得すべき技術や調べることがとても多いため,グループ
内で相談したり手分けしたりしても構わない.しかし,プログラムやレポートは一人で作成する.
前提となる知識: C プログラミングについては,2 年前期のプログラミング演習程度の能力を前提にしている.忘
れてしまった者や履修してない者は,特に構造体とポインタについてしっかり自習すること.この演習が完了す
れば,社会に出てようやく C のプログラミングができると言っても恥ずかしくないレベルになるので頑張って欲
しい.
また,Linux の最低限の知識は説明するが,上にも書いたように自分が実行したコマンドの意味を一つ一つ確認
してノートを作っていくこと.
備え付けの書籍: この演習では自分で調べることが重要である.Web も便利だが,そもそも何を調べたら良いか
も分からない状態のときは,本を読んで理解するしかない.この演習用に,以下の書籍を本棚に用意している.4
冊ある本は一つの机に一冊ずつ置くことを想定している.2 冊だけの本は利用したら本棚に戻すこと.
• 武藤: Debian GNU/Linux 徹底入門第 3 版, 翔泳社 [4 冊]
Debian という Linux の基本的な使い方と管理方法を紹介した本.3 世代前の本だが基本的なところは変わ
りない.マニュアルというより読むための本.
• 山下: UNIX シェルスクリプトコマンドブック第 2 版, ソフトバンククリエイティブ [4 冊]
シェルスクリプトについてだけ説明した本.マニュアルとしても使える.
• 平田: Linux デバイスドライバプログラミング, ソフトバンククリエイティブ [4 冊]
デバイスドライバの作り方について解説した本.一部古いが,今回の演習でデバイスドライバを作る人には
とても役に立つ.
• 高橋: 詳解 Linux カーネル 第 3 版, オライリー・ジャパン [4 冊]
Linux のカーネルのソースコードの解説.ある関数の意味を知りたいときなどに,マニュアル的に使う.
• スティーブンス: 詳解 UNIX プログラミング, ピアソンエデュケーション [2 冊]
UNIX 特有のプログラミングについての説明.特にマルチプログラミングなどは分かりにくいので,本を
一回読んで整理すると良い.
• スティーブンス: UNIX ネットワークプログラミング Vol.1 第 2 版, ピアソンエデュケーション [2 冊]
他のコンピュータと通信をするためのプログラミングについての解説.
授業の流れと提出物:
1. Linux の設定,C を使った簡単なプログラムの作成
2. ソケットによるプログラミング
3. ドライバの動作の理解 [中間レポート提出]
4. ソフトウェアの設計,コーディング
5. デバッグ・テスト,最終レポートの作成 [ノートチェック,最終レポート提出]
毎回の授業では,授業中に小問題を出題し,これについては授業中に回答を説明する.授業終了直前にやや長い
課題を出題するので,次回までに解いてくること.オフィスアワーは,木曜の 6 限である.木曜の 6 限は残れる
ように時間を空けておくことが望ましい.
提出の必要があるのは,第 3 回の中間レポートと第 5 回の最終レポートだけである.ただし,小問題と課題は
次の回に使うので,必ず完成させること.また同じ理由から,作成したプログラムは必ず保存しておくこと.
採点基準:
2 回のレポートの内容とノートチェックで判断する (1 回目レポート:30%,ノートチェック 10%,2 回目レポー
ト:30%,2 回目プログラム 30%).
2
1 第1回
今回の内容
• PC の設定と Linux のブートができるようになる [0.5 時間]
• Linux の簡単な設定 (特にネットワーク) ができるようになる [0.5 時間]
• Linux に慣れる.shell スクリプトを使えるようになる [1.5 時間]
• C プログラミングの復習.C によるプロセスの生成.[1.5 時間]
1.1 Linux マシンの使い方
1.1.1 立ち上げ方
(ネットの設定が終わっていないので) LAN ケーブルは接続せず,それ以外を接続する.外部 HDD の電源を入
れる.ノート PC (以降ではホストと言う) の電源ボタンを押し,すぐに F12 を押す.(F12 が間に合わなかった
ら Windows が立ち上がるので,Windows をシャットダウンして,また最初から.) しばらくすると,ブートす
るディスクを選択する画面が表示される.ノート PC は 2 種類あり,それぞれ異なる画面が表示される.ここか
babababababababababababababababababababab
ら先は授業中に説明する.特に enshu-608-9∼13 は少しややこしいので注意.
Linux の種類:
Linux には種類がある.今回の演習のホスト側の Linux は,Debian Linux (バージョン 6) である.す
べての Linux では OS のカーネル部分は共通だが,カーネルだけでは何もできない.コマンド類等を
セットにしたものは distribution と呼ばれる.distribution には Debian,Ubuntu,Fedora,Redhat,
Vine (PC 実習室の PC に入っている) などがある.元々が同じ Linux であり,コマンドなどはかなり
共通している.(もちろん,MacOS などの他 UNIX とも似ている.) ただし,管理者として OS の設定
をしようとすると distribution の違いは大きい.本を買って勉強する場合,一般利用者用の本はどれを
買ってもある程度は使える.一方,管理者用の本はどの distribution 用の本かをよく見て買う.
1.1.2 自分のアカウントを作る
enshu でログインする (パスワードは授業中に伝える).自分のアカウントを作るために次を実行する.
「アプリケーション」→「アクセサリ」→「端末」を起動する.
$ su
# adduser 自分のユーザ名
# gpasswd -a ユーザ名 sudo
# exit
$
su のパスワードは口頭で伝える.また,adduser の中でいろいろ聞かれるが,パスワード以外は Enter キーで良
い.最後だけ Y とする.これで,自分のアカウントが作成された.(このアカウントを実際に使うのは,もう少し
後で.)
Linux においては,システムの管理を行うユーザを特権ユーザと呼ぶ.代表的な特権ユーザは root である.一
方,enshu や上記で作成した自分のアカウントは一般ユーザである.上の su は一般ユーザが root になるための
コマンドである.
su のたびに毎回パスワードを入力するのは面倒である.そのために sudo というコマンドもある.sudo の後に
3
任意のコマンドを書くと,そのコマンドは特権モードで実行される.もちろん,誰でも sudo が実行できたら,特
権ユーザを分けている意味がない.上の gpasswd で,ユーザを sudo 可能にしている.
以降では,#のプロンプトで始まるコマンドは,su して実行しても良いし,sudo で実行しても良い.
1.1.3 ネットワークの設定と再起動
ホスト名を確認する.
$ hostname
(PC 本体表面のマシン名と異なる名前が表示された人だけ以下を実行:)
# echo enshu-608-XX > /etc/hostname
← XX はマシン毎に違う
$ cat /etc/hostname
IP アドレスを設定・確認する.
$ ifconfig eth0
eth0 は,LAN を接続するためのハードウェア (デバイス) の名前である.表示される IP アドレス (inet アドレ
スという行) は次の通りでなければならない.
表1
IP アドレス割当て
マシン名
IP アドレス
enshu-608-0
172.28.34.60
enshu-608-1
172.28.34.61
enshu-608-2
172.28.34.62
...
enshu-608-13
172.28.34.73
74∼99 は欠番
armadillo-0
172.28.34.100
armadillo-1
172.28.34.101
...
armadillo-13
172.28.34.113
もし違っていたら,/etc/network/interfaces を修正する (授業中に指示する).
$ cat /etc/network/interfaces
(次は,指示があったときだけ実行する.普通は不要.)
# rm /etc/udev/rules.d/70-persistent-net.rules
1.1.4 落とし方
いきなり電源を切ったりしないこと.次のコマンド (または GUI の「システム」のシャットダウン) を実行して
終了する.
# shutdown -h now
設定確認ができたので,LAN ケーブル*1 を接続し,再度電源を入れる.今度は自分のアカウントでログイン
する.
*1
LAN ケーブルのほかに Ether ケーブル,Ethernet ケーブル,100base-T ケーブルなどの呼び方がある.
4
1.1.5 ファイルの保存について
これから作成するプログラムなどのファイルは,外付けディスクに保存される.ハードディスクは動作中に振動
などの衝撃を与えると容易に故障し,ファイルはすべて読めなくなってしまう.実際,毎年 1 人は出ている.自
己責任でファイルを必ず保存しておくこと.MyVolume へ保存する (付録 A) か,自分の USB メモリに保存する
とよい.後で説明する scp を使って,隣の人のコンピュータに保存したりするのも一案である.
1.1.6 コマンド入力端末の設定
アプリケーション→アクセサリ→「端末」を右クリック→「このランチャをデスクトップへ追加」
デスクトップにアイコンができるので,これをクリックする.これで「端末」アプリケーションが表示される.
この中でコマンドを入力することが,Linux の操作の基本である.Linux では,GUI (Graphical User Interface)
を使うことはあまりない.
矢印キーの上 (↑) を押すと過去に入力したコマンドが表示される.→←を使って移動してコマンドを修正でき
る.Enter を押すと修正後のコマンドが実行される.
ファイル名を途中まで入力して,TAB を押すとファイル名を自動補間してくれる.
1.1.7 マニュアルの引き方
Linux のコマンドが分からないときは,次の 3 つのコマンドが便利である.
• man: そのキーワードのマニュアルを引く
• whatis: そのキーワードのマニュアル一覧を表示する
• apropos: そのキーワードを含むすべてのマニュアル一覧を表示する
特に man は頻繁に使うので,以下の例を実行すること.man から抜けるには q を入力する.このことをよく忘
れるので,必ず一度練習しておく.使用例:
$ man ls
1.1.8 エディタ
著名なエディタには,以下のようなものがある.
• gedit
• vi
• emacs
gedit は Windows のメモ帳のようなもので,誰でも簡単に使える.本格的なエディットには,vi か emacs を使う
(簡単な説明は付録 B).どちらも有名なエディタで,解説した Web ページなどもたくさんあるので,自分で勉強
する.実に多くのコマンドがあり,使いこなせると作業効率が劇的に向上する.将来,ソフトウェア関係の職業に
つくつもりであれば,この機会にまともなエディタに慣れておくことを強くお勧めする.
babababababababababababababababababababab
なお,日本語を入力するときは,「半角/全角」キーを押す
vi と emacs:
emacs の方が高機能であり,できないことはないというくらい強力なエディタである.一方,vi はどん
な Linux (というよりもすべての UNIX) にも入っているので利用範囲は広い.実際,今回のターゲット
マシンには emacs はなく vi しか入っていない.
5
1.1.9 その他
パッドのタップによってマウスクリックをしたい場合は,システム→設定→マウスの中に設定がある.
ウェブブラウザは,アプリケーション→インターネット→ iceweasel ウェブブラウザ.これは,中身は Firefox
というブラウザ.
1.2 shell プログラム
コマンド入力を受けそれを実行するプログラムを,UNIX では shell と言う.shell には沢山の種類 (sh, bash,
csh, tcsh 等) があるが,この演習では bash (多くの Linux のデフォルトの shell) を使う.
まず,shell の基本的な使い方を説明する (詳細は,C.1,C.10 を参照).
shell の特徴の一つは,自分でコマンドを作れるということである.次のような内容の l(小文字のエル) という
ファイルをエディタで作る.
#!/bin/bash
ls -l
これを保存し,中身を確認する.
$ cat l
次を実行する.
$ ls -l
$ chmod a+x l
$ ls -l
$ ./l
chmod の a+x の意味は,このファイルの属性を「全員 (all) が実行可能 (executable) 」なように設定せよ,とい
う意味である.a-x なら,全員が実行不可能という意味である.(これ以外の指定については,自分で調べる.)
ls -l を実行すると,ファイルの一覧が属性付きで表示される.
上の l のような shell 用のプログラムは,スクリプトと呼ばれることが多い.shell の各種機能や shell スクリプ
トについては,付録 C (C.6 など) を参照のこと.
スクリプトを作っても,いちいち./l と実行するのではコマンドらしくない.実は,~/bin の下にあるファイ
ルはコマンドとして実行することになっている.このディレクトリを作って,l をそこに置く.
$ mkdir ~/bin
$ mv l ~/bin
l というコマンドが実行できるか試してみる.
小問題:
• l コマンドはアルファベット順に ls で表示している.これを参考に,タイムスタンプ順に表示するコマンド
lt を作りなさい.
• find はファイルを探すコマンドである.例えば,
$ find . -name ファイル名 -print
などとする.しかし,これは長くて面倒なので,
$ f ファイル名
とするだけでファイルを探してくれるコマンド f を作りなさい.このためには,引数にアクセスする特殊
変数$1 を使う (C.7 参照).
6
1.3 C プログラミング
1.3.1 復習
C のプログラムを復習するために,次のようなプログラム test.c を作成する.何をするプログラムかは見れば
分かると思うが,(C のプログラムがまったく思い出せない人は,/home/enshu/c/の下に幾つかプログラムが置
いてあるので,これを見て思い出す.)
#include <stdio.h>
int main()
{
char buff[100];
scanf("%s", buff);
printf("%s\n", buff);
}
このファイルを test.c に保存して,以下を実行する.
$ cc test.c
$ ./a.out
cc で C コンパイラを実行する.これにより,C プログラムから実行可能ファイルが生成される.実行可能ファイ
ルは,何も指定しなければ a.out という名前である.2 番目の行は,このディレクトリにある a.out を実行せよ,
という意味である.(ついでに,ls -l を実行して,x を確認してみる.cc が勝手に x を立ててくれるので,実行
できるのである)
1.3.2 ファイル記述子を使ったプログラム
ここからが新しいプログラムである.ファイルから文字を入力して,それを画面に表示するプログラムを作る.
ただし,fscanf は OS の機能を直接使えないので,ここでは利用しない.今後のために,ここではファイル記述
子という OS の機能を直接利用する方法を習得する.ファイル記述子を使ってファイルから入力を行うプログラ
ムは,次のようになる.
#include <fcntl.h>
int main(){
int fd;
char buff[100];
fd = open("test.txt", O_RDONLY);
read(fd, buff, 10);
write(1, buff, 10);
close(fd);
}
ファイルをオープンしてファイル記述子を作る関数は open である.ファイル記述子から読み込むには read 関数
を使う.これによりファイルからデータが buff に読み込まれる.
このプログラムでは fd がファイル記述子 (file descriptor) である.見ての通り整数である.ファイル記述子の
0, 1, 2 は特別である.0 は標準入力 (つまりキーボード),1 は標準出力 (つまり画面),2 は標準エラー出力 (これ
も画面) のためのもので,この 3 つのファイル記述子は最初から用意されている.
read(0, buff, 10);
7
などとすると,キーボードからの入力を buff に読み込むことができる.
上の open,read,write のように新しいシステムコールやコマンドが出て来たら,man コマンドを使って自分
で調べる.ただし,コマンド名,システムコール名,ライブラリコール名が重複しているときには,うまく探せな
い.例えば,システムコールの open を調べようとして,man open としても,実はコマンドにも open があるの
で,そちらの説明が表示されてしまう.whatis open の結果を見ると,次のようになっている.
open (1)
- start a program on a new virtual terminal (VT).
open (2)
- open and possibly create a file or device
括弧の中が種類を表しており,1 はコマンド,2 はシステムコール,3 はライブラリコールである.
$ man 2 open
babababababababababababababababababababab
とやると,システムコールの open のマニュアルが表示される.
システムコールとライブラリコール:
システムコールとライブラリコールは,C のプログラムから呼び出す関数のことである.システムコー
ルは OS が用意している機能だが,ライブラリコールは OS とは関係なく,C が用意している関数であ
る.ライブラリコールは普通の C で書かれており,自分が書いたプログラムと一緒にリンクして使って
いる.一方システムコールは,中身は OS の中にある.例えば,printf はライブラリコールなので普通
の関数であり,その中で write システムコールを呼び出している.
プログラムを作る上で両者の違いはあまり意識する必要はないが,前述のように man のマニュアルは 2
がシステムコール,3 章はライブラリコールと別れている.
小問題:
次の 2 つの修正をして,上のプログラムを file.c として完成させなさい.1 つ目の修正は,1 文字だけを読
み込んで表示するようにすることである.2 つめは,エラーをチェックきちんとすることである.open,read,
write, close を man で調べて,そのエラーに対処する.エラーのチェックについては,perror という便利な関数
がある (D.3 を参照).このプログラムは後で使うので,きちんと作っておくこと.
1.3.3 プロセスを作る
Linux では,複数のプログラムを同時に動かすことができる.一つ一つのプログラムが動いている状態を「プロ
セス」と呼ぶ.プロセスを作るには fork 関数を呼ぶ.
pid = fork();
fork 関数を呼ぶと,今実行中のプロセスをコピーした新しいプロセスが作られる.元から実行していたプロセス
を親,新しく作られたプロセスを子と呼ぶ.子は親のコピーなので,親と子はまったく同じプログラムであるが,
fork の戻り値だけが異なる.親には子供のプロセス番号が返され,子には 0 が返される.これを使うと,自分が
親なのか子なのかを区別できる.
プロセスをコピーすると変数もすべてコピーされる.親と子の変数は,プロセスの丸ごとコピーにより同じ値
になるが,変数としては別である.従って,どちらかが変数の値を変えても,もう一方にはその値は伝わらない.
fork を調べると,プロセス番号 (上の pid) は pid_t というデータ型だが,これは実際は int である.
課題:
fork して次のことをするプログラム fork.c を作成せよ.fork のエラーもチェックすること.
8
• 子は,”child”と表示しては 2 秒スリープすることを 10 回繰り返す.
• 親は,”parent”と表示しては 3 秒スリープすることを 10 回繰り返す.
余力があれば,次のようなプログラムも作ってみる.子ごとに違う値とする部分がポイントである.
• 親は,n 個の子プロセスを作る (とりあえずは n=2 でもよい).
• n 番目の子は,n の値を表示しては 1 秒スリープすることを 10 回繰り返す.
• 親は子プロセスを n 個作った後,”parent”と表示しては 1 秒スリープすることを 10 回繰り返す.
子が計算した結果を親が必要な時にはどうしたらよいか.値が 0∼255 (つまり 8 ビット) の範囲で良ければ,
親が wait 関数で待つことで子から値を貰うことができる.(man で wait 関数を調べてみる.) 更に余力がある者
は,上のプログラムを拡張して,各プロセスが n の値を親に返すようにする.親はそれを受け取り,一つずつ表
示する.
なお,もっと様々なデータを親と子の間でやりとりするには,プロセス間通信を行う必要があり,後の演習で取
babababababababababababababababababababab
り上げる.
プロセスを終了させる方法:
fork を使ったプログラムにバグがあると,作成したプロセスが終了しなくなることがある.このような
時は,まず ps コマンドを実行してプロセス番号を調べる.そして,次のコマンドでプロセスを殺す.
kill プロセス番号
babababababababababababababababababababab
コマンドでプロセスを作る方法:
これには & (C.5) を使う.コマンドの後に&を付けると,そのコマンドは別のプロセスで実行される.こ
れを通称「バックグラウンド実行」と呼ぶ.普通のコマンド入力の裏側 (バックグラウンド) で実行する
からである.普通のコマンドとバックグラウンドのコマンドは同時に実行されることになる.
2 第2回
今回の内容
• 組込み開発の基礎 [1.5 時間]
• シグナルによるプロセス間通信 [1.5 時間]
• タイマーとシグナル [1.5 時間]
2.1 ターゲットの設定
2.1.1 コンピュータ間のファイル転送
コンピュータ A から B にファイルを転送するには,scp コマンドを使う.scp コマンドは次のように書く.
$ scp
コピー元 コピー先
コピー元とコピー先が同じホストなら,cp と同じである.別ホストのファイルを指定するときは,ホスト名:ファ
イル名と書く.さらに次のように書けば,あるホストのあるユーザのあるファイル名という指定もできる.
ユーザ名@ホスト名:ファイル
9
scp の仕組みを説明する前に,まずサーバーとクライアントの定義を口頭で説明する.scp のための scp サー
バーが各ホストで動作している.scp コマンドを実行する側が,scp クライアントになる.クライアントとサー
バーの関係と,ファイルを転送する方向とは何の関係もないので注意.
scp クライアントを実行すると,別ホストの scp サーバーにログインする必要が出てくる.このため,scp はパ
スワードを聞いてくる.どのホストの scp サーバーに何という名前でログインしようとしているのか考えて,適
切なパスワードを入力する.
実際に本日の演習に必要なファイルをノート PC に転送する.ノート PC を scp クライアントとして動作させ,
172.28.34.hh (hh の部分は授業中に指示) という scp サーバーから,enshu.tar.gz というファイルを取得せよ.ア
カウントは enshu を使う (パスワードは既に知っているはず).
取得したファイルは,沢山のファイルを一つにまとめたものなので,次のコマンドで解凍する.
$ tar zxvf enshu.tar.gz
これによって,p6 というディレクトリが作成される.
2.1.2 組込みシステムのブート
まず,ターゲットとホストという組込み開発の用語を口頭で説明する.
普通の PC には必ずハードディスクが付いており,そこからプログラムを読み込んでブートする.しかし,組
込みシステムのハードウェアはさまざまなため,ブートの方法も多様である.
Armadillo (以降はターゲットと呼ぶ) には,ブートモードが 3 つある (オート,保守,UART).現在は,オー
トモードに設定済みである (詳細は付録 E を参照のこと).オートモードでは,Linux を立ち上げるために,カー
ネルイメージ (linux.bin.gz) とファイルシステムイメージ (romfs.img.gz) が必要である.前者は OS のプログラ
ムそのものであり,後者は OS が立ち上がるために必要な最小限のファイルをまとめたものである.
これらのイメージを取得する方法としては,以下の 3 つがある.
• 内蔵フラッシュブート: 内蔵フラッシュメモリに置かれたイメージを使う
• microSD ブート: microSD に置かれたイメージを使う
• tftp ブート: サーバー (ホスト) 上に置いたイメージをネットワーク経由で入手する
この演習では,オートモードの tftp ブートを使う.tftp ブートに設定するには,保守モードで setbootdevice コ
マンドを使う.これについては 2.1.4 で説明する.
2.1.3 tftp サーバー側の設定
tftp ブートにおいても tftp 要求を出す側を tftp クライアント,要求を受ける側を tftp サーバーと呼ぶ.tftp
サーバーとしての基本設定は既に完了している.このためやるべきことは,tftp ブートに使う 2 つのファイルを
/srv/tftp というディレクトリに置くことだけである.
$ cd ~/p6/atmark-dist/images
# cp linux.bin.gz romfs.img.gz /srv/tftp
babababababababababababababababababababab
scp サーバーや tftp サーバーなど,通信要求を受け取る側のプログラムはどのように起動されるので
あろうか.実は,/etc/inetd.conf に書いてある.このファイルを見て行くと,tftp dgram udp4 ...
という行がある.これは tftp 要求が来たら/usr/sbin/in.tftpd を動かせという意味である.つまり,
in.tftpd が tftp サーバーのプログラムなのである.
また,tftp という要求は何番のポートを使うかということは,/etc/inetd.conf というファイルに書
いてある.これを見ると,tftp は 69 番ポートであることが分かる.
10
2.1.4 minicom の設定
見ての通り,ターゲットにはキーボードがない.ターゲットにキーボードからコマンドを入力するには,シ
リアル (本当は RS-232C と言う) インタフェースから行う.ターゲットとホストをシリアルケーブルで接続す
る.ホスト側では,ホストのキーボード (と文字表示) をシリアルに入力するソフトを動かす必要がある.これが
minicom というソフトである.
ただし,minicom を直接動かすと日本語表示がうまくできないので,mc というコマンドを使う.mc は Linux
のコマンドではなく,山崎が作成したファイルである.これをコマンドとして実行できるように,自分で設定
せよ.
いよいよターゲットを立ち上げてみる.ただし,まだ LAN ケーブルは接続しないこと.
mc コマンドを実行する.次に,保守モードでターゲットをブートする.保守モードでブートするためには,メ
イン基板のボタンを押しながら電源を入れる.ボタンは,minicom の画面に文字が出たら離して良い.画面には,
hermit>
と出ているはずである.この hermit>という文字列は,ターゲットが表示した文字がシリアルを通ってホストに
送られ,それを mc が画面に表示しているのである.
2.1.5 ターゲットの設定の確認
まず,tftp によるブートの設定を確認する.まず,現在の設定を見るには,単に setbootdevice と入力する.
よく見て設定を確認する.
設定が正しくなかったときには,次のように入力する.
hermit> setbootdevice tftp 172.28.34.tt 172.28.34.hh --kernel=linux.bin.gz --userland=romfs.img.gz
”tftp”の後の最初の IP アドレスは,自分 (ターゲット) の IP アドレスである.次は,tftp サーバ (ホスト) の IP
アドレスである.その次は,2 つのイメージファイルの指定であり,このまま書く.
設定が正しかったら,tftp でブートするための準備をする.まず,ハブの親側のケーブル (床下から立ち上がっ
て来ているケーブル) を抜く.これは,間違った IP アドレスのパケットが,大学のネットワークに流れ出て行か
ないようにするためである.そして,ターゲットとハブを LAN ケーブルで接続する.
リセットボタンを押すと,minicom 画面に Linux がブート過程のメッセージが出るはずである.立ち上がった
ら,root でログインする (パスワードは授業中に伝える).
次に,各マシン毎に正しい IP アドレスが設定されているかを確認する.
# ifconfig eth0
IP アドレスが表示されるはずなので,それが正しいかを確認する.正しい IP アドレスが設定されていたら,ハ
ブを大学のネットワークに接続して良い.(まだ,他の人が IP アドレスの設定中かもしれないので,全員が設定
完了してから接続すること.)
正しい IP アドレスでなかった時は,以下を行う.
11
# vi /etc/config/interfaces
下記の内容を手で入力
auto lo eth0
iface lo inet loopback
iface eth0 inet static
address 172.28.34.tt
netmask 255.255.255.0
gateway 172.28.34.1
# cat /etc/config/interfaces
念のため目で見て確認
# flatfsd -s
リセットボタンを押す.
babababababababababababababababababababab
flatfsd コマンドの仕組み:
Linux の IP アドレスはファイルに書いてある.ということは,romfs.img.gz が同じだと,すべてのマシ
ンの IP アドレスが同じになってしまう.しかし,一台ずつ個別に romfs.img.gz を作るのは手間である.
そこで,armadillo では,個々のマシン毎に設定を個別に保存する機能として,flatfsd コマンドがある.
flatfsd -s コマンドを実行すると,内蔵フラッシュに /etc/config/* を保存する.ブートの時には
/etc/init.d/flatfsd が呼ばれる,その動作は次のようなものである.
1. flatfsd -r をして内蔵フラッシュから/etc/config/* を復活する.
2. /etc/config/*の内容を,/etc/default/*に上書きする.
3. /etc/default/*を /etc/config/* に名前付け替えをする.
2.1.6 終了と再起動の方法
ターゲットは ROM だけで動いているので,いきなり電源を抜いても問題ない.しかし,正しくは halt コマン
ドを実行する.電源を落として良いというメッセージが表示されるので,電源アダプタを抜く.
また,再起動するにはリセットボタンを押しても良いが,正しくは reboot コマンドを実行する.
minicom の終了は,^A,z,q,(Yes か聞かれるので) Enter と順番に入力する.
2.2 クロスコンパイル
簡単なプログラムを作り,これをターゲット用にクロスコンパイルし,ターゲットに転送して実行するという一
連の手順を実行してみる.ホストとターゲット間の転送は,やはり scp を使う.ただし,ターゲットは scp サー
バにはなれないことに注意.
ホストには,既に ARM 用クロスコンパイラ arm-linux-gnueabi-gcc がインストール既みである.クロスコ
ンパイルしたいファイルが main.c だとすると,
$ arm-linux-gnueabi-gcc main.c
とやってみる.a.out ができたはずである.
$ file a.out
と実行すると,ARM 用のコードが出来ていることが分かる.(普通の cc でコンパイルした a.out にも file をし
12
て,その結果を比較してみるとよい.)
小問題:
1∼10 億までの数を加えるプログラムを作り,main.c としなさい.これをまずホストで実行して,実行時間を
測定する.(実行時間を測定するには time コマンドを使ってもよい.) 次に,同じファイルをクロスコンパイルし
て,実行可能ファイルをターゲットに転送し実行せよ.ターゲットでの実行時間はどの程度になったか.
(また,cc でコンパイルした実行可能ファイルをターゲットで実行すると,何が起きるか.)
2.3 シグナルを使ったプロセス間通信
プロセスは独立した存在であって,互いに干渉することは基本的にはできない.プロセス間で何らかのやり取
りをするには,プロセス間通信と呼ばれる仕組みを使う.プロセス間通信には様々な種類があるが,今回は「シグ
ナル」を使う.
シグナルとは,ソフトウェアだけで行う割り込みのことである.割り込みとは,あるプロセスがある処理をして
いるときに,強制的に別の処理を実行させる仕組みである.( 「コンピュータアーキテクチャ」の授業では,割り
込みはハードウェアが掛けるものであったが,シグナルはソフトウェアが掛ける割り込みである.)
シグナルには種類があり,番号で分けられている.シグナル番号の一覧は,/usr/include/bits/signum.h の
80 行目前後にある.
シグナルを受け取る側は,例えば次のように書く*2 .
void sig_handler(int signum){
∼
}
int main()
{
signal(SIGUSR1, sig_handler);
∼
}
signal を実行すると,ある番号のシグナル (SIGUSR1 は既に定義済みの番号なのでこのまま使えばよい) が送ら
れてきた時に実行される関数 (上では sig_handler) を登録する.このような関数をシグナルハンドラ(または
ハンドラ)と呼ぶ.main の∼の部分を実行中にシグナルが発生すると,ハンドラが実行される.ハンドラは,1
引数 (データ型は int) で戻り値は void の関数でなければならない.なお,引数はシグナル番号である.
あるプロセスへシグナルを送るには,次のように書く.
kill(プロセス番号, シグナル番号);
勝手に使ってよいシグナル番号は決まっており,SIGUSR1 と SIGUSR2 である.今回は,SIGUSR1 (実際は 10) を
使う.(なお今回は使わないが,自分自身にシグナルを送るときには,raise 関数を使う.)
小問題:
上の説明を参考にして,シグナルを送信する側のプログラム sig.c,シグナルを受信する側のプログラム rec.c を
作りなさい.幾つか注意が必要である.2 つの異なる実行可能ファイルを作るには,次のようにする.
$ cc -o sig sig.c
$ cc -o rec rec.c
ここで-o は,実行可能ファイルの名前指定である.
rec.c のメインは次のようなプログラムとする.
*2
この書き方は実は古い.今は sigaction という関数を使うことが推奨されているが,使い方が大変なので今回はこちらを使う.もち
ろん,sigaction を自分で調べて使っても良い.
13
for (;;) {
printf("z\n");
sleep(1);
}
すなわち,1 秒に一回”z”を表示する.
rec を別の端末画面で動かし,もう一方で次のコマンドを実行する.
$ ps u
PID の欄を見ると rec のプロセス番号が分かる.この番号を sig に与える必要がある.これには 2 つの方法があ
る.一つは,プロセス番号を scanf で読み込む方法である.もう一つは,次のような形でコマンドのオプション
で指定する方法である(1234 はプロセス番号の例).
$ ./sig 1234
このようにコマンドオプションの値を使う方法は,D.2 を参照のこと.文字列の数字を整数の数値に直すには
atoi が便利である.
babababababababababababababababababababab
sig.c ではシグナルを送った旨を,また rec.c では受け取った旨を表示するだけでよい.
CUI だけで実験する方法:
ここでは端末画面を複数開いて複数のプロセスを実行した.一つの端末画面の CUI の中だけで開発した
い場合は,前回述べた&を使う.
$ ./rec &
これにより./rec は別プロセスで実行される.この時,そのプロセス番号が表示されるので,これを利用
すれば良い.
余力がある人用:
1. signal が SIG_ERR を返したらエラーなので,これをチェックする (このチェックはデータ型が少し難しい
ので,分からなかったら聞く).
2. システムコール getpid を呼び出して,自分のプロセス番号を表示する機能を付ける.(これで ps コマンド
で番号を調べる必要がなくなる.)
3. 例えば,次のような簡単なプログラムを考えてみる.
while(1){
oldv = extv;
newv = extv;
if (oldextv != newextv) printf("error\n");
}
ただし,extv は外部変数,oldv と newv は自動変数である.これに対して,シグナルハンドラを追加する.
シグナルハンドラの中では,extv++ を実行するだけである.次にこのプロセスに対して,外部から繰返し
シグナルを送信してみる.何が起きるだろうか.error が表示されないようにするには,どのようにしたら
良いだろうか.
14
2.4 シグナルとタイマー
ある指定した時間待つプログラムはどのように書けば良いか.一つの方法は,for ループを何回も回すことであ
る.しかし,計算機にただ回数を数えるという仕事をさせるのはもったいないし,電力も消費する.タイマーを使
うと,計算機は別の仕事をしたりスリープしたりできる.
タイマーを使うには,次のようにする.詳細は,man で調べること.
struct itimerval timval;
timval.it_interval.tv_sec=1;
timval.it_interval.tv_usec=0;
timval.it_value.tv_sec=1;
timval.it_value.tv_usec=0;
setitimer(ITIMER_REAL, &timval, NULL);
∼ // 別の仕事
タイマーが発火すると,SIGALRM というシグナルが発生する.(参考までに SIGALRM は実際は 14 である.) シ
グナルハンドラーを書けば,タイマーの発火を知ることができる.
小問題:
タイマーを使って次のようなプログラム timer.c を作れ.必ずエラーチェックをすること.なお,別の仕事が特
に何もない時は,無限ループにすれば良い.(sleep 関数を実行しても良い.)
• 1 秒経過するたびに,何らかのメッセージを出力するプログラムを作れ.
• (少し余力のある人用) クロスコンパイルして,armadillo で実行せよ.先ほどと違い,CPU の性能に関係
なく同じタイミングでメッセージが表示されるはずである.
課題:
シグナルとタイマーを組み合わせて,ストップウォッチを遠隔操作するプログラムを作成する.ストップウォッ
チのメインとなるのは watch.c で,次の動作をする.
• SIGUSR1 が送られてきたら,ストップウォッチを起動する.
• SIGUSR2 が送られてきたら,ストップウォッチを停止する.
• ストップウォッチ動作中は 1 秒に 1 回,経過時間を表示する.
もちろんストップウォッチの実装は,sleep ではなく setitimer を使うこと.(メインで何も処理しないために
sleep を使うのは構わない.)また,ストップウォッチ動作中の SIGUSR1,停止中の SIGUSR2 に対しては適切に
処理する.
ストップウォッチを別画面から起動・停止するプログラムは,sig.c を改造して用いる.とりあえずは,SIGUSR1
を送るだけの sig1.c と SIGUSR2 用の sig2.c を別々に作れば良い.余力があれば,次のような形で起動と停止がで
きるようにする(1234 はプロセス番号の例).
$ ./sig start 1234
$ ./sig stop 1234
3 第3回
今回の内容
15
• クロス開発環境でのドライバとアプリケーションの作成方法を理解する [1 時間]
• make を理解する [1 時間]
• サンプルドライバを動かして動作を理解する [1.5 時間 + レポート]
前回,enshu.tar.gz を解凍したときに,p6/pdf というディレクトリが作成されているはずである.この下にあ
るファイルについて説明する.これらは,これから使う組込みコンピュータ armadillo-440 に添付されていたド
キュメントである.組込みシステムには,このように詳細な情報が付いてくるのが普通である.これまでの授業
の知識を総動員すれば,ほとんど理解できるはずである.
• Armadillo-440 液晶モデル開発セットスタートアップガイド: ハード・ソフトの基本的な使い方.アプリ
ケーションソフトの書き方.
• Armadillo-400 シリーズハードウェアマニュアル: ターゲットのハードウェアに関するマニュアル.演習に
関連があるのはジャンパーや位置や意味など.
• Armadillo-400 シリーズソフトウェアマニュアル: クロス開発環境の説明.ソフトウェアをカスタマイズす
るための情報.ブートローダの情報.
• atmark-dist 開発者ガイド: ターゲットの OS を本格的に開発するための情報.
• Armadillo-400 リビジョン情報: 今回は使わない.
3.1 ターゲット用 Linux の作成
ターゲットマシン (armadillo) では,普通の Linux ではなく,uClinux (http://www.uclinux.org/) とい
う組込み機器用の Linux が動作する.前回は,カーネルイメージ (linux.bin.gz) とファイルシステムイメージ
(romfs.img.gz) について,提供されたものをそのまま利用したが,今回は,それをソースコードからコンパイル
babababababababababababababababababababab
して作ってみる.
Linux と uClinux の違い:
Linux は汎用の OS であり,仮想記憶が可能である.これにより,実メモリよりも大きなプログラムを
実行できる.一方,組込み装置では,それほど巨大なプログラムを動作させることはないため,組込み用
CPU には仮想記憶のためのハードウェア MMU がないものもある.uClinux は,MMU のない CPU
でも動作するのが特徴である.
3.1.1 ソースファイルのコンパイル
カーネルをソースからコンパイルするには,数々のオプションを指定する必要がある.それを実際に体験し
てみる.また,後でドライバーを自分たちで作成する前準備として,標準で入っているドライバー (下の GPIO
Buttons) を削除しなければならない.それについてのオプション設定も行う.
$ cd ~/p6/atmark-dist
$ make menuconfig
メニュー画面が開く.この中では,Enter キーで選択,SPACE キーで ON/OFF.
Vendor/Product Selection を選択
Vendor を選択
「AtmarkTechno」を選択 (かなり上の方にある)
自動的に画面が戻る
2 行下の「AtmarkTechno Products」を選択
Armadillo-440 を選択
16
自動的に画面が戻る
(右矢印キーを押して) Exit を選択
自動的に画面が戻る
Kernel/Library/Defaults Selection を選択
Default all settings を ON
Customize Kernel Settings を ON
Exit を選択
もう一回 Exit を選択
Do you wish to save∼ に対して Yes を選択
いろいろやった後でもう一回メニューが出てくる
Device Drivers を選択
Input device support を選択
Keyboards を選択
GPIO Buttons を OFF
Exit を 4 回
Do you wish to save∼ に対して Yes を選択
しばらく時間がかかる.何もエラーメッセージが出ていなかったら次へ進む.
$ make
OS 全部をコンパイルするので,かなり (15∼20 分) 時間がかかる.待っていても仕方ないので,ほっておいて
次の節に進む.
以降は,エラーなく終わったのを確認してからやる.images/の下に以下の 4 つのファイルができていることを
確認する.
linux.bin, linux.bin.gz, romfs.img, romfs.img.gz
ファイル名の最後に gz とついているのは,圧縮されたファイルである.つまり,linux.bin を圧縮したものが
linux.bin.gz である.(圧縮については man gunzip) ターゲットをブートするためには,前回のように linux.bin.gz
と romfs.img.gz を tftp 用のディレクトリに置く.そして,ターゲットの電源を入れる.
# cat /proc/sys/kernel/version
babababababababababababababababababababab
と実行すると,現在のカーネルがいつコンパイルされたものかが分かる.
大量のメッセージが出力される場合の対処方法:
make は大量のメッセージを表示するので,次のようにしてメッセージをファイルに一旦保存するのが普
通である.(今回は,エラーは出ないはずなので,画面にたれ流しにしている.)
$ make >log 2>&1
ここで 2>&1 というのは,2 番出力を 1 番出力と一緒にせよという意味である.通常,2 番出力はエラー
メッセージ,1 番出力は通常メッセージである.両方とも同じファイル (log) に記録するために,上記
のようにする.
17
babababababababababababababababababababab
/proc の仕組み:
/proc というディレクトリから下は実は本当のファイルではない.OS の内部状態をファイルの形で見
せているだけである.例えば,/proc/1 など数字のついたディレクトリがある.これは,そのプロセス
番号の情報が入っている.そのディレクトリに行って,cat で見てみよう.また,書き込むことで OS
の設定を変えることができるファイルもある.
3.1.2 make
OS のように大きなプログラムは,膨大な数のソースファイルと include ファイルから構成される.その中の一
部を修正した時に,何をコンパイルすべきかを人手で管理するのは大変だし間違いやすい.make は,これを自動
化してくれる.ここでは時間がないので,既に用意してあるファイルを使って,make が何をしてくれるのかを簡
単に説明する.まず,ファイルの中身を確認する.
$ cd ~/p6/maketest
$ cat m.c
$ cat f.c
$ cat g.c
コンパイルして,実行してみる.
$ cc f.c g.c m.c
$ ./a.out
次に,Makefile を見てみる.
m: f.o g.o m.o
f.o: f.c
g.o: g.c
m.o: m.c
Makefile とは,コマンド make が参照するファイルで,実行可能ファイルの作り方が書いてある.上の 1 行目は,
「m は f.o,g.o,m.o に依存している」という意味であり,
「m は f.o と g.o と m.o から作れる」という意味でもあ
る..o というファイルは,.c という C プログラムから作られる中間ファイルである.次の行は,f.o は f.c に依存
しているという意味である.(実は,最初の一行以外は省略できる.C.11 を参照のこと.)
小問題:
まず make を実行して,必要なファイルを全部コンパイルして,m を作ってくれることを確認する.次に,エ
ディタを使って f.c をエディットする (スペースを足す,あるいはメッセージを書き換えるなど).もう一度 make
を実行して,何がコンパイルされるかを確認する.その他,m.c をエディットしたり,m や f.o を削除したりし
て,その都度 make を実行してみる.これにより,必要最小限の動作しかしないことを確認する.
余力のある人は,次を行う.Makefile を次のように修正する.
m: f.o g.o m.o
cc -o m f.o g.o m.o
f.o: f.c
cc -c f.c
g.o: g.c
cc -c g.c
18
m.o: m.c
cc -c m.c
ここで,cc の左はスペースではなく,TAB である (重要).TAB で始まる行は,その上の行の依存関係が満た
されなかったら,TAB の右を実行せよという意味である.最初に実験したように,TAB で始まる行がなくても,
make は適当に作ってくれる.しかし,特殊なことをする場合は,作成方法を TAB の行で指定しなければならな
い.上の Makefile を元に,クロスコンパイルするための Makefile を作成せよ.
(余力のある人) make を使って,2 つのファイルをコンパイルすることもできる.このためには,Makefile の 1
行目を次のように書く.
target: sig rec
説明したように,make は 1 行目のコロンの左側を作成しようとする.そのためには,sig と rec が必要だという
のが,このルールの意味である.しかし,target の作り方が 2 行目に書いてないので,永久に target は作られな
い.つまり,make は実行されるたびに,sig と rec を作るしかないのである.
3.2 ターゲットにファイルを置くには
ターゲットは電源を入れるたびに,ホストから OS とファイル群 (romfs.img.gz) を受け取ってブートする.こ
のため,ターゲットのファイルを変更しても,電源を切ると元に戻ってしまう.ターゲットのファイル状態を永続
的に変更するには,romfs.img.gz そのものを変更するしかない.その方法を説明する.
実は,ホスト側の~/p6/atmark-dist/romfs ディレクトリの下のファイル構成が,そのまま romfs.img.gz に
なる.実験してみるために,vi などを使って romfs/root というディレクトリの下に testfile というファイルを作
る.romfs.img.gz を作るには,先程のように make を実行しても良いが,romfs.img.gz だけを作り直すときは,
次のようにするとずっと早い.
$ cd ~/p6/atmark-dist
$ make image
images の下の romfs.img.gz を適切に設定し,ターゲットを立ち上げてみる.ターゲットの /root の下に testfile
ができているはずである.(時間がある時は実際にやってみる.)
この機能を使って,scp を便利にしてみよう.scp のパスワードを毎回入れるのは面倒だが,実はパスワードの
代わりに「鍵ファイル」を使うとこれを回避できる.次のようにする.
$ ssh-keygen
こうすると,~/.ssh の下に id_rsa と id_rsa.pub という 2 つの鍵ファイルができる.前者を scp クライアン
ト,後者を scp サーバに置けば,パスワードを入力せずに scp が実行できる.ssh-keygen をホストで実行したと
する.サーバに置く鍵のファイル名は authorized keys なので,次のようにする.
$ cd ~/.ssh
$ mv id_rsa.pub authorized_keys
これでサーバ側は設定完了である.
次に id_rsa を scp クライアントに設定する.具体的には,このファイルがクライアント (つまりターゲット)
の/root/.ssh/id_rsa というファイルになるようにすれば良い.ただし,ターゲットの電源が切れてもファイル
が残るようにするには,上のような手順を実行する必要があるのだった.(後は考える.)
クライアントで scp を実行してみると,パスワードが聞かれなくなっているはずである.
19
3.3 サンプルドライバの作成とアプリケーションの作成
~/p6 の下には,bmpread,fbwrite,tactsw の 3 つのディレクトリが存在する.bmpread は,BMP ファイル
を読み出すプログラム,fbwrite はターゲットの画面にグラフィック表示をするプログラム,tactsw はサンプルド
ライバのプログラムである.
まずドライバを用意する.ドライバのプログラムは,tactsw.c という名前でなければならない.サンプルとし
て,tactsw sample.c という山崎が書いたプログラムがあるので,これをコピーする.
$ cd ~/p6/tactsw
$ cp tactsw_sample.c tactsw.c
ドライバを作成する.
$ make modules
エラーメッセージのようなものが出るが,ls -l で tactsw.ko の日付けを見て,今作成されたものであれば問題
ない.
.ko というファイルは何か.Linux では,カーネルの一部分 (例えばドライバ) は,カーネルを動かしたままで
組み入れたり,カーネルから外したりできる.これをカーネルモジュール (略称は km でなく,kernel object で
ko) と言う.今回のドライバもカーネルモジュールの一つである.
tactsw.ko を scp でターゲット側に転送する.次に,カーネルモジュールのドライバを今動作しているカーネル
に組み込んでみる.これには,insmod (insert module) コマンドを使う.
# insmod tactsw.ko
(なお,ドライバモジュールの削除は,rmmod tactsw.ko である.新しくドライバを作ったときには,rmmod を
してから insmod する.)
このときに,このドライバに割り振られた Major 番号が表示される.251 のはずである.これで 251 番という
ドライバがカーネルに入ったのだが,このドライバーに対して,open や read をするには,どうしたら良いだろう
か.Linux では,ドライバも一つのファイルのように見せることで,これを可能とする.
ドライバとやりとりするための疑似的なファイルは,「デバイス」と呼ばれる.次のコマンドで,251 番ドライ
バにデバイスとしてのファイル名を付与する.
# mknod /dev/tactsw c 251 0
と実行する.c というのはドライバの種類で,character 型と block 型がある.今回は c である.(キーボードのよ
うに入力したデータは 1 回限りのデータで消え去っていくタイプがキャラクタ型デバイスで,ハードディスクの
ように同じデータに何回もアクセスできるタイプがブロック型デバイスである.) 最後の 0 は子番号である.251
番ドライバの中に子供のデバイスを作れるが,今回はこの機能は使わないので 0 である.
小問題:
/dev/tactsw をオープンし,5 回だけ文字を read するプログラムを書き,サンプルドライバが動作しているこ
とを確認せよ.ファイル名は read.c とする.(file.c を参考にせよ.)
read する前に,ボタンを複数回押したときの動作と,read した後で押したときの動作の違いは何か?
file.c を作成したときに,read や close の戻り値を調べていない人は,この機会に行う.
3.4 ドライバーの動作の理解
サンプルドライバのソースコードの詳細については,授業中に説明する.
20
ドライバは,大きくトップハーフとボトムハーフに分けて構成される.ボトムハーフは,割り込みハンドラとそ
れに関連する部分を言う.トップハーフは,ユーザプロセスからのシステムコールに対する処理をする部分であ
る.*3
トップハーフ中心で考えると,ドライバの動作は次のように 2 つのパターンがある.
• read システムコールで呼ばれて,もう入力済みの文字があったので,すぐ戻る.
• read システムコールで呼ばれて,まだデータがないので待ちに入る.その後,入力があったら read から
戻る.
ボトムハーフ中心で考えると,次のような 2 つがある.
• 割り込みが入ったときに,それを待っているプロセスがいない.
• 割り込みが入ったときに,それを待っているプロセスがいる.
後述の課題では,それぞれについてどういう順番で動作しているのかを調べてみる.
ドライバを設計するに当たっては,クリティカルリジョンに注意する必要がある.クリティカルリジョンとは,
プログラム上のある一部分のコードであって,その部分を同時に実行してはならない部分である.
サンプルコードでは,msg[] と mlen という配列をトップハーフとボトムハーフが共有しているため,これに関
連するデータ更新には注意が必要である.
一般論としては,注意すべきは次のパターンである.
• トップハーフはプロセススイッチする可能性がある.つまり,2 つのトップハーフが同時に実行される可
能性がある.(今回のドライバは,これがないように作られている.それはどこで保証されているのだろう
か?)
• シングルコアのプロセッサでは,ある瞬間に実行しているコードは一つだけである.しかし,マルチコアの
場合は,異なるプロセッサが同じコードを本当に同時に実行する可能性がある.プロセススイッチは禁止す
ることができるが,物理的に同時に実行されることは防止できない.プロセススイッチからのクリティカル
リジョンの保護と,マルチコア実行からの保護は手段が異なる.マルチコアについては,スピンロックで待
つことが有効である.
• 割り込みはいつでも入ってくる可能性があるため,非同期に割り込みハンドラが走行する.ボトムハーフと
トップハーフが同じデータを共有する場合は,上記と同様の注意が必要である.
• クリティカルリジョンとは関係ないが,割り込みハンドラの記述には,もう一つ注意が必要である.割り込
みハンドラの中では,待ちに入る可能性がある関数を呼んではならないという点である.割り込みハンドラ
は,他の割り込みを禁止して走っているので,その中で待ちに入ると何の割り込みも受けられなくなる.
サンプルドライバを見ると分かるように,msg[] と mlen にアクセスするときには,単純にそのまま操作するこ
とはしていない.これらのデータは,ドライバのトップハーフとボトムハーフが共有しており,同時に操作すると
データが破壊されたり,誤った値を参照したりするためである.
工夫の一つは,次の方法である.
ret = wait_event_interruptible(tactsw_info.wq, (tactsw_info.mlen != 0) );
これは,第 2 引数 (tactsw_info.mlen != 0) が正しくなるまで待ち (スリープ) に入るという関数である (実
際は関数ではない).普通に C で書けるような気もするかもしれないが,tactsw_info.mlen が 0 であることを
検知してから,スリープに入る間に mlen!=0 になると,そのプロセスは 2 度と目を覚まさなくなってしまうだろ
う.つまり,mlen を調べてから寝るまでの間がクリティカルリジョンなのである.
wait_event_interruptible は,ある工夫によってクリティカルリジョンが 2 重に実行されないようになって
いる.なお,そのプロセスがスリープした情報は,tactsw_info.wq に入るので,そのプロセスを起こしたい場
*3
Linux ドライバにおいては,これとは違う意味でトップハーフ,ボトムハーフという言葉を使うこともあるようだが,一般的なドライ
バではここで述べた意味で使う.
21
合は,次を実行する.
wake_up_interruptible(&(tactsw_info.wq));
wake_up_interruptible は起こすだけなので,この呼び出しが待ちに入ることはない.したがって,割り込
みハンドラの中で使っても構わない.
もっと一般的に msg[] や mlen を操作したいときはどうするか.この時には,ロックを使う.ロックを取れた
プログラムだけが,そのデータを操作できるようにすることで,安全に操作可能となる.ここでは,ロックの一つ
であるスピンロックを使っている.
spin_lock_irqsave(&(tactsw_info.slock), irqflags);
∼
spin_unlock_irqrestore(&(tactsw_info.slock), irqflags);
spin_lock_irqsave は 2 つのことを行う.今実行中のプロセッサの割り込みの禁止と,スピンロックの取得であ
る.詳細は,F.5 を参照のこと.∼の部分がクリティカルリジョンであり,これが 2 重に実行されることはない.
irqflags は,禁止前の古い CPU 状態を入れる変数なので,後で使う.slock を取得できれば,もう誰も関連する
コードを実行することはできない (そのようにプログラムを作る).仕事が終わったら,spin_unlock_irqrestore
でロックを解放する.
クリティカルリジョンの実行は,いろいろな意味でシステムに悪影響を与えるので,できるだけ短い時間で実
行しなければならない.また,クリティカルリジョンの中でプロセスがスリープするような関数を呼んではいけ
ない.
課題:
ドライバの動作を解析し,レポートにまとめる.これは採点対象である.少なくとも以下について動作解析を
すること.
• 問 1. 各関数 (tactsw ioctl を除く) がどのようなときに呼ばれるかを調べなさい.
「a.out を実行すると呼ばれる」とかは駄目.(自分で作ったプログラムでしょう?)
• 問 2. あらかじめキー入力がない時に read を実行すると,そのプロセスは待ちに入る.この「待ち」はド
ライバーのプログラムのどの部分で実現されているか調べなさい.まず,どのように調査したかについて述
べ,その後で動作説明をすること.(調査の試行錯誤の過程まで書かないこと.以下同様.)
• 問 3. 問 2 の状態に続けてキー入力が入ると,そのプロセスは待ち状態が解け,実行中となる.何 (ドライ
バー中のプログラムのどの部分) によってプロセスが起こされるのか.そのプロセスが本当に動作を再開す
るのはどのタイミングなのかを調べなさい.まず,どのように調査したかについて述べ,その後で動作説明
をすること.
• 問 4. あらかじめキー入力がある場合において,read を実行したときの動作を調べなさい.まず,どのよう
に調査したかについて述べ,その後で動作説明をすること.
ソースコードは必須ではないが,調査方法の説明等のために付けてもよい.
調査方法のヒント: アプリケーションでは printf,ドライバーでは printk (F.1 を参照) を入れて追い掛けて
いく.
22
babababababababababababababababababababab
【重要】printf とバッファリング:
printf は,実はストリーム (FILE *) に対して出力をしており,中身は fprintf(stdout, ...); であ
る.ストリームに出力すると,OS は,出力回数をできるだけ減らして無駄を防ごうとする.つまり,出
力した文字を OS の中にギリギリまで溜めこむ (これをバッファリングという).printf した文字がすぐ
画面に表示されるとは限らないのである.しかし,デバッグでは,直ちに文字を表示したいことが多い.
一つの方法は,改行を付けることである.多くの場合はこれで表示されるが,必ず表示されるとは限ら
ない.
端末への表示においては,次のようにすると良い.
printf(∼);
tcdrain(1);
ここで tcdrain は,バッファリングされた文字が端末に表示されるまで待つ関数である.
なお,printk は,見かけは printf に似ているが,ストリームを使っていないので,直ちに表示される.
4 第4回
今回の内容:
• 中間レポートの回答を簡単に説明.
• ソケットによる通信プログラム
• 最終製作プログラムの説明
4.1 TCP/IP とソケット
TCP/IP の解説を行う (授業内で説明).コンピュータアーキテクチャの授業で既に簡単に説明したし,情報
ネットワークではもっと深い説明があるので,ソケットを理解するためのごく簡単な説明である.
以下のキーワードを理解すること.
• サーバ
• クライアント
• IP アドレス
• ポート
• ソケット (5 つ組, 5-tuple)
4.2 ソケットによるプロセス間通信
プロセス間通信の一つであるシグナルについては 2.3 で行った.今回は,別のプロセス間通信方法として,ソ
ケットを取り上げる.これまでと同様に,ソケット通信を待ち受ける側をサーバ,通信要求を発行する側をクライ
アントと呼ぶ.サーバ側のやるべきことをプログラムの骨格だけ書くと次のようになる (エラーチェック等いろい
ろと省略している).
#include <arpa/inet.h>
struct sockaddr_in serv_addr;
23
// ソケットを作る
sockfd = socket(PF_INET, SOCK_STREAM, 0);
// アドレスを作る
bzero(&serv_addr, sizeof(struct sockaddr_in));
serv_addr.sin_family = PF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(ポート番号);
// ソケットにアドレスを割り当てる
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in));
// コネクション要求を待ち始めるよう指示
listen(sockfd, 5);
// 要求があったらそれを受け付ける (なければ待つ)
new_sockfd = accept(sockfd, NULL, NULL);
// クライアントからデータを受け取る
read(new_sockfd, buff, 128);
// 1 秒待ってソケットを終了する
sleep(1);
close(new_sockfd);
close(sockfd);
一方,クライアント側のプログラムは,次のようになる.
// ソケットを作る
sockfd = socket(PF_INET, SOCK_STREAM, 0);
// サーバのアドレスを作る
bzero(&serv_addr, sizeof(struct sockaddr_in));
serv_addr.sin_family = PF_INET;
serv_addr.sin_addr.s_addr = inet_addr("172.28.34.∼");
serv_addr.sin_port = htons(ポート番号);
// コネクションを張るための要求をサーバに送る
connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in));
// 128 バイトのデータをソケットで送る
write(sockfd, buff, 128);
// ソケットを終了する
close(sockfd);
各システムコールは次のような意味である.
• socket: ソケット (ファイル記述子) の生成
• bind: ソケットにアドレスをつける
• listen: コネクションの要求を待つ
• connect: コネクションを張る要求を送る
• accept: コネクションの要求を受け入れる
• write, read: データの送受信
24
• close: ソケットを閉じる
各ライブラリ関数の詳細な使い方は,自分で調べること.close の手順には注意が必要である.クライアントが
close してからサーバが close しなければならない.正しく close する方法については,D.5.1 を必ず参照すること
(重要).ここでは,最も手抜きの方法として,サーバが 1 秒待ってから close している.
小問題:
まず,クライアント (ターゲット) から任意の数字をサーバに送り,サーバでそれを表示するプログラムを作成
せよ.
必ずすべてのシステムコールに対して,エラーチェックを入れること.エラーの時にどのような値が返される
かは,man で調べる.
クライアント用プログラムは cli.c,サーバ用プログラムは serv.c とする.ポートは,10000 番を使うものとす
る.クライアント・サーバ間で通信する数字は,最初は char で良い.(char も数字だったことに注意.ただし,範
囲は-128∼127.) つまり,上のプログラムで言えば,buff[0] だけを使ってデータを通信する.
ターゲットに実行可能ファイルを毎回転送するのは大変なので,まず,ホストの上でサーバとクライアントの両
方のプログラムを作り,デバッグまで行う.完成したら,クライアントをクロスコンパイルし,実行可能ファイル
をターゲットに転送して,動作を確認する.
余力がある人は,以下について考えてみる.
• クライアント (ターゲット) から任意の数字をサーバに送り,サーバ (ホスト) 上でそれに 1 を加えてクラ
イアントに戻すようなプログラムを作成せよ.
• サーバが一回で終了せず,何回もクライアント接続が可能なようにする.
• 上のようにするとサーバーが終了しなくなってしまうので,ある特殊なデータ (0 や-1) を送ったら,サー
バーが終了するようにする.
• クライアントとサーバ間の通信を 1 バイトでなく,4 バイト (int) にする.ただし,ソケットで正しい送受
信が保証されるのはバイト列だけである.つまり,buff[0], buff[1], buff[2], buff[3] という 4 つのバイトか
ら int を作り出す関数と,int を 4 つのバイトに分解して buff[0]∼buff[3] に入れる関数を自分で作る必要が
ある.(これを毎回やるのは面倒なので,htonl や ntohl という関数がある.これを自分で調べて使っても
良い.)
• (更に余力がある人へ) 整数や文字列を混在して送信できるようにするには,どうしたら良いだろうか.
4.3 最終課題
2 つのコース,通信アプリコースとドライバーコースがあり,各コース複数の問題がある.どれか一問を選択し
て作成する.自分のオリジナルな機能は勝手に入れてもよい.
4.3.1 通信アプリコース
通信アプリケーションは,複数のマシン上の互いに通信するプロセスによって構成される.現在では,一般のア
プリケーションで通信をしないものなどほとんどないが,今後は組込みシステムでも通信は重要である.既にソ
ケットを使った簡単な通信は実験したが,これをベースにして機能を考えていく.
具体的には,以下のアプリケーションを作成する (どれか一つ).
1. デジタルフォトフレームを作る (難易度低):
サーバー上に置いてある画像ファイル (bmp ファイル) をターゲットの画面に表示する.以下のプログラム
を読んで理解すれば,後は組合せるだけでできるはずである.これらのプログラムについては付録 G を参
照.(特に色の扱いが armadillo は独特なので,必ず読むこと.)
• bmpread.c: ホスト上で bmp ファイルを読み込むプログラム
25
• fbwrite.c: ターゲットに画像を表示するプログラム
• serv.c, cli.c: 今回の演習で作成した,サーバー・クライアント通信プログラム
最初の表示が完成したら,次のようにフォトフレームらしい表示の方法を工夫する.まず,クライアントが
繰返し画像を要求しても良いようにする.つまり,サーバーが一回で終了しないようにする.複数の bmp
ファイルを用意し,リクエストのたびに異なる画像が表示されるようにする.また,下から表示する,右か
ら表示する,ディゾルブ (少しずつ) で表示するなどのパターンを考えてみるのも面白い.
2. 2 つ以上のソケットの同時処理 (難易度高):
今回の演習で作成したサーバー・クライアント通信プログラムにおいて,複数のクライアントが同時接続し
ても対応できるようなサーバーを作成する.ただし,このクライアントはすぐ終了してしまうので,同時に
は動かせない.そこで,クライアントを次のように修整する.値を送信し,サーバーが 1 加算した値を返
して来たら,1 秒スリープして,またサーバーに値を送信することを 10 回ほど繰り返す.つまり,実行に
10 秒かかるようにする.後は,端末を沢山開いてクライアントを次々と起動すれば良い.
このプログラムは見かけは面白くないが,サーバープログラミングの基本中の基本である.もっと面白くし
たい人は,例えば複数の利用者間でチャットをすることなどを考えてみても良い.
ヒント: ホスト側の受信プログラム (メインのプログラム) では,select や poll という関数を使う.
4.3.2 ドライバコース
ドライバコースでは,サンプルドライバに機能を加える.組込みシステムにおいては,新たな独自ハードを接続
することが多い.その場合には,必ずドライバーを作成する必要がある.しかし,ドライバー作成は,OS 自身に
手を入れることを除けば,最高難度のプログラミングの一つと言われており,なかなか自力ではマスターできな
い.ここでは入門レベルではあるが,ドライバープログラミングを体験する.
具体的には,現在のサンプルドライバに,以下のいずれかの機能を追加する.複数の機能を実装しても構わな
い.全ての機能は,一つのドライバ内に共存させることができる.
1. ioctl による状態取得 (難易度低):
今のタクトスイッチの状態を取得するための ioctl を用意する.ioctl のコマンドは適当に自分で定義する.
read では,スイッチが押されるまで待ってしまうが,この機能があれば,キーの状態を瞬時に知ることが
できる.ioctl については,D.4, H.6 を参照のこと.これは簡単にできるはずなので,次の機能と組み合わ
せて作って欲しい.
2. キーの組合せによる文字入力 (難易度低∼中):
複数のタクトスイッチの組合せを検知して,それぞれに異なる文字を出すようにする.例えば,スイッチを
押す個数に応じて,’2’, ’3’, ’4’ などとするのでも良いし,スイッチの組合せを文字に対応させるのでも良
い.3 つのスイッチの組合せで 7 種類の文字を入力できるはずである.(意図的に行うのは極めて困難だが,
ほぼ同時に 2 つのキーを押した場合,割り込みが 2 回でなく 1 回になることがありうる.一つ前の割り込
み処理が完了していないうちに入った次の割り込みは無視されるためである.これを完全に対処するには
工夫が必要で,その場合は難易度高.)
3. シグナル化 (難易度高):
タクトスイッチが押されたらプロセスに signal をかけるような機能を実装する.ioctl を使って,signal を
掛けるように設定する.また,その設定を解除する ioctl も作る (F.7).ioctl については,D.4, H.6 を参照
のこと.
4. オートリピートドライバ (難易度高):
オートリピートというのは,キーボード (今回はスイッチ) を長く押していると,連続して文字が入力され
ることを言う.この機能を実装する.これには,スイッチが押され,ある一定時間のうちに離されなかった
ら,文字が入力されたことにすれば良い.時間検出のためには,カーネルタイマーを使う.キーが押された
らタイマーを起動し,タイマーがタイムアウト (時間切れ) になったら,文字が押されたことにする.この
タイマーをスイッチが離されるまで,繰り返し設定すれば,オートリピートが可能となる.(カーネルタイ
26
マーの使い方は,F.9 を参照すること.)
タイムアウト処理を含めて,トップハーフとボトムハーフで共有データを操作するコードが多くなる.ロッ
クがいい加減でもだいたいは動作するが,厳密に正しい実装にしようとすると,難易度は「超高」である.
いずれも,Linux カーネルが用意する関数の使い方を深く理解する必要がある.一部については,付録に記載し
た.また,準備している本「LINUX デバイスドライバ」,「Linux デバイスドライバプログラミング」を参照した
babababababababababababababababababababab
り,Web を使って自分で調べる.
割り込みハンドラの中で時間のかかる仕事をする方法:
割り込みハンドラの中では,待つ可能性のあることは一切できず,またできるだけ早く終了しなければ
ならない.そこで,ハンドラから抜けた後で仕事をする方法が幾つか用意されている.一つは,カーネ
ルタイマーである.これは,ある時間経過後に関数を実行するためのものであり,その間は別の仕事が
できる.時間指定でなくて,割り込みハンドラから抜けた後,いつでも (暇なときに) 関数を実行させる
こともできる.これがタスクレットである.タスクレットは,適当なタイミングで実行されるカーネル
タイマーのようなものである.
カーネルタイマーもタスクレットも,割り込みハンドラと同様に厳しい制約がある.つまり,その中で
待つことはできないし,またユーザ空間とのデータ転送もできない.もっと自由に何でもやりたいとき
は,ワークキューを使う.ワークキューは,独立したカーネル内スレッドで実現されている.なお,今
回の演習では,タスクレットもワークキューも使う必要はない (使いたければ使っても良い).
まず設計をする.関数を単位として,引数と戻り値の決定をする.また,外部変数を中心としてデータ設計を
し,そのデータを参照する関数と修正する関数を決定する.通信アプリについては,ターゲットとホスト間にどの
ようなデータの通信が発生するかもまとめる.
実装において,よく分からない関数があったら,それを呼び出す簡単なプログラムを作るなどして実験して理解
する.特にカーネル関連の関数は,適当に呼んで動くというものではない.一つ一つきちんと理解して進める.
教員・TA への説明
これは必須ではないが,設計方針が決まったら,教員または TA にそれを説明してみるとよい.自分のシステ
ムを大雑把に人に分かるように説明できることは,とても重要である.また,根本的な間違いがあることもあるの
で,それを早期に知るためにもお勧めする.
よくあるバグ
症状: ターゲットで./a.out を実行すると,いきなり Killed と表示された.
原因: 配列のサイズが大き過ぎる.組み込みシステムでは,メモリに限界があり,また uClinux は仮想記憶ももっ
ていない.配列のサイズを減らす.
症状: ターゲットで./a.out を実行すると,いきなり Segmentation Fault と表示された.
原因: これは,いろいろな原因が考えられる.もしかしたら,自動変数で大きなサイズの配列を宣言しているため
かもしれない.
症状: まっ白い画面が表示された.
原因: 表示しようとしている free1.bmp は,画像の上下に白い帯が入っている.その部分を表示してしまった可
能性がある.なぜ,その部分が表示されてしまったのかは,自分で考えてみること.
27
babababababababababababababababababababab
ソースコードを探して読んでみよう:
ある関数の使い方が分からなかったら,インターネット検索も一つの方法であるが,一番確かなのは
コードを見ることである.例えば,add_timer という関数を使っているコードを見つけたいときは,
$ cd ~/p6/linux-2.6.26-at15/drivers
$ grep add_timer */*.c
または
$ find . -name "*.c" -exec grep add_timer \{\} \; -print
などとする.一方,add_timer の関数定義の方を見たいときには,最も簡単な方法は,
$ cd linux-2.6.6-at14
$ find . -name "*" -exec grep "add_timer(" \{\} \; -print
とする方法である.ただし,沢山出てくる.もう少し本格的にソースコードを読む場合は,
$ cd linux-2.6.6-at14
$ ctags -R
と実行する.かなり時間がかかるが,tags というファイルができる.これをエディタや less で見て,
add_timer を探す.すると,include/linux/timer.h に定義されていることが分かるはずである.(tags
とエディタを連携させて,もっと簡単にファイルを閲覧することもできるが,後は自分で調べる.)
5 第5回
今回の内容
• ノートチェック
• 実装継続
• レポートの書き方
5.1 ノートチェック
どのようにノートを付けているかを確認し,採点する.質問するので,それに関連するノートのページを見せ
る.ノートなど不要という人もいるかもしれないが,その場合は実際に手作業してもらい,そのスムーズさを見て
採点する.
5.2 ドライバの拡張機能の実装とデバッグ・テスト
引き続き,コーディングならびにデバッグをする.ある機能の実装が順調にできたら,別の機能にも挑戦してみ
る.ないしは,データ表現を工夫して高速化したり,プログラムを簡潔にしたりできないか考えてみる.
テストもすること.基本的な動作を確認したら,さまざまなケースについて確認する.もちろんプログラムは,
各種のエラーを判定するようになっていなければならない.システムコールやライブラリコールのエラー判定は
必須である.テスト手順をきちんとメモしておき,最終報告書にて示すこと.
また,時間があったら性能測定もしてみると良い.特に通信アプリについては,例えば,1 バイトずつ送った場
合と,3 バイトずつ送った場合などを測定してみる.時間の測定は,C の中で自分で時間測定関数 (times, clock)
を呼ぶ方法と,shell レベルで time コマンドを使う方法がある.
28
ドライバについては,速すぎて測定は難しい.また,テストも網羅的に行うのは難しい.微妙なタイミングをす
べて再現することはできないためである.少なくとも 3.4 節でやったように,待ちが入るときと,待ちが入らない
ときはテストすべきである.また,msg 配列が溢れないかのテストや,キーを同時に押した時のテスト,キーを
高速に連打した時のテストなどは簡単にできる.その他にもテストができないか考えてみる.
以上の過程を便利にする工夫ができないかも自分で考える.とにかく楽をするように考える.例えば,scp を含
む一連の手順を実行する一つのコマンドを作ってみるなどである.自分が考えて行った工夫もレポートに書いて
babababababababababababababababababababab
良い.
逆アセンブルの方法:
コンパイルされた実行可能ファイルは機械語であり,人間が見ても分からない.これをアセンブリプロ
グラムに戻すことを逆アセンブルと言う.
$ arm-linux-gnueabi-objdump -d ファイル名
とすれば,全逆アセンブルが一気にできる.
デバッガ gdb を使うこともできる.(ただし,クロスデバッグではなく,単にソースを見るだけ.)
$ arm-linux-gnueabi-gdb tactsw.ko
逆アセンブルには,
disass 関数名
と入力する.
5.3 最終報告書
最終報告書を書く.最終報告書は txt, pdf, doc のいずれで提出すること.(txt を Windows にコピーする場合
は,gedit で shift JIS, Windows 改行の設定をする必要がある.授業中に指示する.)
Linux には OpenOffice というソフトが入っているので,これを使っても良い.メニュー→オフィス→
OpenOffice.org Writer を選ぶ.「エキスポート」を使えば pdf に変換できる.
ワードを使いたい場合は,PC 実習室の PC を使う.(ただし,勝手に PC 演習室に行かないで,まず山崎に許
babababababababababababababababababababab
可を得ること.)
整形文書の作成:
Linux において,きちんと整形された文書を書くには,TeX というソフトを使うと良い.この文書も
TeX を使って書かれている.研究論文も TeX のことが多いので,大学院に進む予定の人は,知ってお
いて損はない.この機会に自分で TeX を勉強するのも良いだろう.
最終報告書は例えば次のような章構成とする.A4 で 5 ページ前後とする.
表紙
【学籍番号,氏名,提出年月日】
1. はじめに
【このレポートの目的を書く】
2. 組込み開発環境の概要
29
【今回の組込みシステム開発のための機器,ソフト,役割,手順などについて書く】
3. 動作の概要
【どちらのコースについても複数のプログラムから構成されている】
【これらがどのように連携しているかの概要を書く】
【通信アプリの場合,サーバ側ソフトとクライアント側ソフトの役割と通信について】
【ドライバの場合,アプリケーションとドライバの役割と連携動作について】
4. システムの詳細
【自分の作ったプログラムを掲載し,その説明を書く】
【テストについても書くこと】
【プログラム以外にもツールの使い方の工夫など,自分で考えたことを何でも書く】
【プログラムは全部載せる必要はないが,自分が手を入れた部分は載せること】
5. おわりに
【この演習で習得した知識や技術などを簡単に書く】
【この演習の感想も書く (普通の技術レポートでは感想は書かないが,これは大学の授業なので特別)】
すべてが完了したら,自分のファイルをすべて消去するため,以下を行う.まず,必要なファイルを MyVolume
などに保存する.ログアウトして,enshu でログインする.(自分がログインしたままだと自分は消去できない.)
# userdel -r 自分のアカウント名
30
付録 A
MyVolume への保存
MyVolume のアクセスを準備する
まず,AMI (https://ami.sic.shibaura-it.ac.jp/) にアクセスして,自分の MyVolume のサーバ名を確認してお
く.どの部分がサーバ名かよく見ること (yfx3.sic.shibaura-it.ac.jp などの名前).
以下の手順を行う.
「場所」→「サーバへ接続」をクリック
サービスの種類を「Windows 共有」にする
サーバ名: (AMI で確認したサーバ名)
共有する場所: 空欄のまま
フォルダ: 学籍番号/win/Desktop
ユーザ名: 学籍番号
ドメイン名: sic.shibaura-it.ac.jp
ブックマークを追加するにチェック
ブックマーク名は,「MyVolume」などとする
「接続する」をクリック
パスワードを入力
「ログアウトするまでパスワードを記憶する」をクリック (「期限なし」でも良い)
「接続する」をクリック
2 回目以降は,ブックマークからすぐに開くことができる.
なお,授業資料のある ShareFolders にアクセスするには,上のサーバ名のところを「yshare.sic.shibaura-
it.ac.jp」とし,フォルダを,「ShareFolders」とすれば良い.
付録 B
エディタ: vi と emacs
B.1 vi
vi はファイルを修正するときに起動し,修正が終わったら vi を終了するという使い方をする.
$ vi ファイル名
vi には,入力モードとコマンドモードという 2 つのモードがあり,今どのモードなのかを意識しなければなら
ない.vi を立ち上げたときにはコマンドモードである.コマンドモードにおける主なキーは以下の通り.
h j k l (または←↓→↑):それぞれ左下上右の移動
x (または DELETE):一文字削除 (改行は削除できない)
dd:一行削除
a:入力モードにする (入力モードからコマンドモードにするには ESC)
i:入力モードにする (入力モードからコマンドモードにするには ESC)
ZZ:保存して終了
a と i の違いは,新たな文字を,今いる文字の右に入れるか左に入れるかである.
B.2 emacs
emacs は,vi と異なり立ち上げっぱなしにしておくのが普通である.
$ emacs &
31
とすると,emacs 用のウィンドウが新たに作られる (&の意味は C.5 を参照).そちらのウィンドウに移り,^X^F
でファイルをオープンし,^X^S でセーブするという形でファイルを修正する.
emacs は vi と異なりモードはない.キー入力すると,いつでもそれに対応する動作をする.以下で,^はコン
トロールキーの同時押しを意味する.
^X^F:エディットしたいファイル名を指定する
^B ^P ^N ^F (または←↓→↑):それぞれ左下上右の移動
^D (または DELETE):一文字削除 (改行も削除できる)
^X^S:保存
^X^C:終了 (終了する必要はない)
付録 C
shell プログラミング
本章では,shell でのプログラミングに関して,基本的な考え方を説明する.詳細にはついては自分で調べる.
C.1 ファイル名の指定
shell の基本は,
「コマンド ファイル名」である.コマンドについては C.10 を参照せよ.ファイル名の指定方法
の基本は,
/home/yamazaki/read.c
である.これは,ルート (/) の下の home の下の yamazaki の下の read.c というファイルである.
D2/F
のように”/”で始まらないときは,今いるディレクトリが開始点となる.つまり,今のディレクトリにある D とい
うディレクトリの下の F である.
その他,以下のような記法がある.
.
今いるディレクトリ (ピリオド)
..
一つ上のディレクトリ (ピリオド 2 つ)
~
ホームのディレクトリ
複数のファイルを指定したいときには,?と*を使う.?は任意の一文字,*は任意の文字列という意味である.幾
つか例を挙げる.
*
そこにあるファイルすべて
*z
z という文字で終わるファイル
a*
a という文字で始まるファイル
*m*
中に m という文字を含むファイル
?
任意の 1 文字のファイル
??c
3 文字のファイルで最後は c で終わるファイル
C.2 コマンドの実行
コマンドを入力すると,shell はどのようにしてそれを実行するのだろうか.
$ echo $PATH
と実行すると,コロン (:) で区切られたディレクトリ名が表示される.shell は,これらのディレクトリを順番に
訪問し,入力されたコマンドと同じ名前のファイルがあるかを探す.もし,あればそれを実行する.このコマンド
を調べていくディレクトリ群を実行パスと呼ぶ.Linux のコマンドがどれだけあるのか知りたかったら,実行パ
32
スのディレクトリを一つ一つ ls してみる.(演習用の debian では 1899 個ある.)
なお,
$ /home/enshu/a.out
$ ./a.out
などのようにディレクトリ名も付けて入力した場合は,実行パスとは関係なく,そのファイルをそのまま実行
する.
C.3 リダイレクション
コマンドの入力と出力をキーボードや画面でなく,ファイルに対して行うことを,リダイレクションと言う.リ
ダイレクションの一般形は,
$ コマンド <入力ファイル >出力ファイル
である.
|を使うと,コマンド1 の出力を コマンド2 の入力へリダイレクションできる.このようなリダイレクションの
ことを,パイプと言う.
$ コマンド1 | コマンド2
C.4 コマンドの便利な入力法
↑を入力すると,昔のコマンドが表示され,それを実行できることは本文で説明した.
キー入力の途中で TAB を押すと候補となるファイルを教えてくれる.候補が一つしかないときは,TAB を押
すとそのままそのファイルが表示される.候補が複数あるときは,TAB を 2 回押すと一覧が表示される.(この
ように,途中まで入力すると後を補ってくれる機能を自動補間 (コンプリーション) と呼ぶ.)
C.5 プロセス制御
$ ./aaa
と実行すると,bash は aaa を自分の中から関数のように呼び出して,それが終了するまで待っている.もし,aaa
が無限ループになってしまったりして止めたくなったときには,^C を押す (これは Control キーと C キーを同時
に押すという意味).
aaa を実行したまま,bash が待たないようにすることもできる.
$ ./aaa &
と実行すると,aaa は bash と並行して動作する別プロセスとして実行される.これを裏 (background) での実行
という.bash は bash で動作しているので,別のコマンドを実行したりできる.裏で実行されるプロセスは,キー
ボードとは切り離されているので,何も入力できないし,^C もできない.
裏のプロセスを前面にもってくるには,fg というコマンドを使う (fg は foreground の略).逆に bg というコ
マンドもある.
プロセスにシグナルを送るには,プロセス番号に対して,
$ kill -シグナル番号 プロセス番号
を実行する.プロセス番号は,ps コマンドで見ることができる.
練習: ps コマンドで bash のプロセスを見つけて,それに対して,9 番シグナルを送ってみよう.
33
C.6 shell スクリプト
本文で述べたように,端末アプリで手で入力する内容をそのままファイルに書けば,shell スクリプトとして実
行できる.ただし,以下のことが必要である.
• スクリプトの 1 行目に#!/bin/bash と書く.これにより,沢山種類がある shell のどれを使うかが指定さ
れる.2 行目移行に実行したいコマンドを書く.
• chmod a+x などでファイルを実行可能にする.
• 一般コマンドのように使いたい場合は,~/bin に置く.
C.7 変数と制御
普通のプログラミング言語と同様に変数があり,繰返し実行や条件分岐もできる.これは特に shell スクリプト
を書くときに重要である.
変数への代入は =を使う.また,変数の値を取り出すには$を付ける.
x=3
echo $x
コマンドの引数に書いた値は,第 1 引数は$1 という特殊変数で参照できる (以下同様に$2, $3 である).
echo $1 $2 $3
のようなスクリプトを作って実験してみる.
繰返しは for を使う.例えば,全ファイルを表示するには,次のように入力する.ここで,*は正規表現で,す
べてのファイルを指定している (正規表現は「データ構造とアルゴリズム」の授業で説明した).
for f in *; do
echo $f
done
条件分岐は if を使うが,特殊な書き方なので,なかなか使いにくい.ここでは例を挙げるので,後は自分で調
べる.
if [ $x = $y ]; then
echo same1
elif [ $x -eq $z ]; then
echo same2
else
echo diff
fi
最初の if は文字列としての比較,2 番目の elif は数値としての比較である.スペースも正確に上のように書かなけ
ればならない.
C.8 shell スクリプトの例題
shell スクリプトの勉強には,例題を読んでみるのが一番てっとり早い.Linux のコマンドの中には,shell で書
かれたものが沢山ある.
$ cd /bin
34
$ file * | grep shell
こうすると,shell で書かれたコマンドが見つかる./usr/bin や/etc の下などにも沢山ある.
C.9 .bashrc と alias
bash は立ち上がるときに,~/.bashrc というファイルを読み込む.自分独自の設定はここに書いておく.
特に重要なのは alias である.alias というコマンドを使うと,自分の好きなようにコマンドをカスタマイズで
きる.例えば,.bashrc に
alias ls=’ls -F’
と書いておくと,ls というコマンドを入力すると,自動的に-F オプションが付いて実行される.
C.10 (この授業で) よく使う Linux のコマンド一覧
(*) がついているのは超重要コマンド.
• adduser: 新たなユーザアカウントの追加
• alias: 別名コマンドの設定
• cat: ファイルの中身を見る (*)
• cd: ディレクトリを移動する (*)
• chmod: ファイルの状態を変更する (*)
• cp: ファイルをコピーする (*)
• echo: 画面に文字を表示する
• exit: shell を終了する
• file: ファイルの種類を表示する
• find: ファイルを探す
• ftp: ファイル転送
• gpasswd: グループの管理をする
• grep: ファイルの中から文字列を探す
• ls: ファイルの一覧を表示する (*)
• mkdir: ディレクトリの作成 (*)
• mv: ファイルを移動する (*)
• patch: パッチを当てる
• ps: プロセスの一覧を見る (*)
• pwd: 今いるディレクトリの名前を表示する (*)
• rm: ファイルを削除する (*)
• rmdir: ディレクトリを削除する (*)
• scp: マシン間でのファイル転送
• strace: コマンド実行中のシステムコールを表示する
• tar: ファイル群を一つのファイルにする/元に戻す
• which: コマンドがどこにあるか教えてくれる
C.11 make と Makefile
Makefile の書き方は,かなり複雑である.基本だけ説明する.Makefile は,次のような記述を一つのまとまり
として,それを続けて書いていく.
35
ターゲット : 依存ファイル1 依存ファイル2 依存ファイル...
<TAB> コマンド 1
<TAB> コマンド 2
<TAB> コマンド ...
これはターゲットのファイルは依存ファイル (群) に (時間的に) 依存しているという意味である.この依存関係
が満たされないとき,つまり依存ファイルのどれかがターゲットより新しいとき,コマンドが実行される.(コマ
ンドの頭には,必ずタブ <TAB> が必要である.よく忘れるので注意すること.) コマンドの行は幾つ書いても
良い.
make コマンドを実行すると,Makefile の中の一番上 (最初) の行のターゲットを作ろうとする.(それよりも下
の依存関係は直接は関係ない.)
コマンドの行は省略できることもある.典型的なターゲットの接尾辞については,make は何のコマンドを実行
すべきか知っているからである.例えば,”*.o” を ”*.c” から作るには cc を実行すべきということは知っている
ので,依存関係の行だけを書けば良い.
ターゲットは,存在しないファイルでも良い (依存ファイルも存在しないファイルでも良い).例えば,次の
Makefile を考える.
a: b c
b: b.o
c: c.o
a は b と c に依存するという意味なので,b と c を作ろうとする.b は b.o に依存している.”*.o”から実行ファ
イルを作る方法を make は知っているので,これで b を作ってくれる.c も同様である.
しかし,a をどう作るかは分からない.b も c も一般的なファイルなので,a の作り方はユーザが指定しないと
make には分からないのである.しかし,上の Makefile にはコマンドの行がない.つまり,一番最初の依存関係
は永久に満たされないことになる.したがって,make を実行するたびに,b と c を作ろうとする.これは複数の
ターゲットを作る基本的なテクニックである.
make の使い方
make -n と実行すると,どのようなコマンドを実行するかが表示される.実際には実行されないので,Makefile
のテストに便利である.
単に make と実行すると,一番最初のターゲットを作ろうとする.一方,
make ターゲット
とすると,指定したターゲットを作ろうとする.よくあるのは,
clean:
rm *.o
という行を Makefile に入れておく.そして,make clean と実行する.これにより,すべての”*.o”が消され
る.(上の依存関係には依存ファイルが存在しないので,いつでも必ずコマンドを実行する.)
付録 D
C プログラミング
本章では,アプリケーションを C でプログラミングする際に知っておきたい幾つかの技術を説明する.
D.1 C の超基本
C の構造体とポインタを忘れてしまった人へ.
構造体のメンバへのアクセスは,「構造体変数. メンバ名」と書く.例えば,
36
x.member
ポインタの宣言,ポインタの取得,ポインタの指す先への代入
int i, *p;
p = &i;
// p は i を指すポインタ
*p = 1;
// p の指す先に 1 を代入
*p = *p + 1;
// p の指す先の値に 1 を足して代入
ポインタが構造体を指している場合のメンバのアクセス
struct my_struct{
int x;
};
struct my_struct *p;
p->x = 1;
// (*p).x = 1; と同じ
p->x = p->x + 1;
これだけでは思い出せない人は,かなりヤバイです.自習して下さい.
D.2 コマンドへの引数
$ ./a.out 100 200 300
のように,コマンドに引数を渡すことができる.これには main を次のように書く.
int main(int argc, char *argv[])
{
printf("argc=%d,", argc);
printf("argv[0]=%s,", argv[0]);
printf("argv[1]=%s\n", argv[1]);
}
argc は,引数の個数 + 1 が入っている.上の例なら 4 である.「argv は配列で,その要素はポインタであり,char
を指す」のだった (「プログラミング」でさんざんやったはず).つまり,argv[0] や argv[1] は文字列である.
上の実行結果は,argc=4,argv[0]=./a.out,argv[1]=100
1 個目の引数は argv[1] に入っているので,これを atoi() に与えれば int になる.
D.3 システムコール/ライブラリコールのエラー
システムコール/ライブラリコールを man で見ていると,「エラーの理由は,errno に設定される」というよう
な記述がある.システムコール/ライブラリコールでエラーが起きると,その詳細な理由が errno という外部変数
に代入される.値の意味は,/usr/include/asm-generic/errno-base.h 等に書いてあるが,これをいちいち調べる
のは大変である.
perror を使うと,見易い形でエラー理由を表示してくれる.
val = システムコール名 (∼);
if(val < 0){
perror("コメント:");
exit(1);
}
37
などとやると,とても簡単にエラーの理由が表示できる.
D.4 デバイス関連のシステムコール (特に ioctl)
デバイスに対しては,open, read, write, close などのシステムコールが実行できる.一つ一つの意味は明らか
と思われるが,ioctl は何に使うのかよく分からない.ioctl とは,io control の略で,デバイスを制御するための
関数である.
ioctl(ファイル記述子, コマンド, コマンドパラメタ);
のようにして呼び出す.コマンドは,デバイスごとに独自に決められている.要するに read や write ではできな
い,
「その他なんでも」のための関数である.
D.5 ソケットプログラミング
基本的な説明は本文で行った.ここでは,それ以外の点を幾つか説明する.
D.5.1 ソケットの close
ソケットはどちらから閉じても良いが,クライアントが close をしてから,サーバが close をする方が好まし
い.クライアントが close したことをサーバが知るには,read の戻り値が 0 かを見ればよい.それを判定してか
らサーバが close をする.(read の返す値は,自分で man で確認してみること.)
しかし,デバッグしている最中などは,プログラムがその手順で動作するとは限らない.すると,次回の bind
がエラーとなってしまう.これは,まだそのソケットが使用中と OS が判断してしまうからである.実は,そのま
ま数分待つと自動的にソケットが終了するので,また使えるようになる.しかし,それではデバッグに時間がかか
るので,次のようにすると良い.
int flag=1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(int));
bind(∼);
setsockopt は,ソケットにオプション設定をする.上のオプション SO_REUSEADDR は,そのアドレスが使用中で
あっても,構わず bind せよという指示である.この後 bind を実行すれば,bind はエラーにならない.ただし,
このオプションはデバッグが終わったら外した方が良い.
fork すると,ソケットもコピーされ,どのプロセスもそのソケットを読み書きできるようになる.そのような
ソケットを close する時は注意が必要である.ソケットが fork によりコピーされた場合,すべてのプロセスでそ
のソケットを close する必要がある.一つでも残っていると,ソケットは開いたままになる.つまり,一番最後の
close を実行した時に,本当の close 処理が実行される.
なお,close に似たシステムコールに shutdown がある.fork でコピーされたソケットでもいきなり閉じること
ができるが,詳細は自分で調べる.
D.5.2 複数要求の受け付け
演習の例では,accept した後にすぐ sockfd を close していた.listen の引数に渡したソケットは,クライアン
トからの要求を受けつけるためだけのソケットである.accept は,要求があると新しくソケットを生成しそれ (例
題における new_sockfd) を戻す.実際の通信は new_sockfd に対して行う.
元の sockfd はどうなったのか.実は,コネクション要求は,一つだけでなく複数受けつけることができる.先
の例のように,close(sockfd); とせず,もう一度 accept を呼べば,次の要求を受け付けることができる.
38
D.5.3 複数のファイル記述子の待ち
accept を複数回呼び出して,複数の要求を受け付け,複数のソケットが張られたとしても,複数のマシンと通
信できるわけではない.
while(1){
read(new_sockfd1, buff1, 100);
read(new_sockfd2, buff2, 100);
}
などとしたとすると,もし new_sockfd1 にデータがなければ,read はそこで待ちに入る.従って new_sockfd2
に来ているデータはずっと読み込まれないことになる.
これを回避する方法の 1 つは,一つのソケットに対して一つプロセスを作り,それぞれのプロセス (D.6 参照)
がそれぞれのソケットを read するようにすれば良い.
もう一つの方法は poll (または select) を使う方法である.簡単に説明すると,poll を使うと,複数のソケッ
ト (あらかじめ配列に入れておく) のどれかに入力があったのかを,一度に調べることができる.入力があったソ
ケットだけを read するようにすれば,待ちに入らずにすべての入力を処理できる.
D.6 マルチプログラミング
複数の仕事を並行して実行する方法によってプログラムを作ることを,マルチプログラミングと呼ぶ.Linux で
は,複数のプロセスを同時に実行することができる.
注意 1: ターゲットの Linux (uClinux) は,fork でなく vfork を使うなどの制限がある.ここに書いてあるこ
とは,ホスト PC の Debian 等の普通の Linux でのマルチプログラミングの方法である.
注意 2: 一つのプロセスの中で,複数のスレッドというものを動かすこともできるが,今回は説明しない.
pthread などのキーワードで調べてみる.
D.6.1 プロセスの生成
プロセスの生成には,fork を使う.fork は不思議な動作をするので,簡単に説明する.典型的なプログラムは
次のようになる.
pid=fork();
if (pid == 0) {
子の仕事
} else {
親の仕事
}
form() を実行すると,親プロセス (今実行中のプロセス) から子ができるが,子というのは実は親とまったく同
じである.同じプロセスが 2 つ走ることになる.ただ一つ違うのが,fork からの戻り値である.子には 0 が戻さ
れ,親には子のプロセス番号が戻される.従って上のプログラムでは,「子の仕事」と書いた部分と,「親の仕事」
と書いた部分が並行して動作することになる.
なお,子プロセスは親の完全なコピーなので,その時の変数の値などもそのままコピーされる.ただし,子プロ
セス側で変数に代入したりすると,それが親に伝わるわけではない.その代入は,子プロセスの中だけで有効であ
る.つまり,あくまでもコピーされるだけなのである.
D.6.2 プロセスの終了
子プロセスが main から return すれば,プロセスは終了する.(_exit() を呼んだ方が良いときもあるので,こ
の辺は自分で調べる.)
39
親が,子供が終了したことを確認したいときや,子が main から return した値を知りたいときは,wait を使
う.上の「親の仕事」の中で,
int ret, w_pid;
w_pid = wait(&ret);
とする.ret には子が返した値が入る.w_pid には,終了した子のプロセス番号が入る (親は沢山の子をもてるた
め,これが必要である).
D.6.3 別プログラムの実行
fork では,プログラムの中に「子の仕事」として書いてあることしか実行できない.自分と関係ない別のコマ
ンドを fork して実行するにはどうしたら良いか.これには,execve (その他 execl, execv など沢山ある) を
使う.
fork した後で,子プロセスの中で execve の第一引数にファイル名を指定と呼び出す.すると,今のプロセス
がそのファイルのプログラムで置き換わり,実行が始まる.(execve を実行すると,プログラムが置き換わるの
で,execve はエラーのとき以外はリターンしない.)
付録 E
Armadillo のブートの詳細
計算機のブートの仕組みは,ハードウェアによって少しずつ違うが,大雑把には同じである.電源を入れると,
ROM に入っているごく簡単なプログラムが動き始める.このプログラムがブートローダというプログラムを実行
する.Armadillo では,ブートローダは内蔵フラッシュメモリに入っている.また,Armadillo のブートローダは
hermit という.このブートローダには 3 種類のブートモードがある.モードは下記のジャンパで選ぶ.
表2
ジャンパーピンの設定
JP1
JP2
OPEN
OPEN
OPEN
SHORT
SHORT
-
モード
オートブートモード
保守モード
UART モード
保守モードは,ブートローダだけを立ち上げて,ハードが壊れていないかをテストしたり,手動でブートしたり
するためのモードである.
UART モードは,ブートローダそのものが壊れてしまった時の最後の手段であり,hermit を内蔵フラッシュに
書き込むことができる.
オートブートモードは,一番普通の方法である.ブートローダはオートブートモードで立ち上がると,Linux を
立ち上げるために,カーネルイメージ (linux.bin.gz) とファイルシステムイメージ (romfs.img.gz) を取得する.
前者は OS のプログラムそのものであり,後者は OS が立ち上がるために必要な最小限のファイルをまとめたもの
である.
付録 F
ドライバプログラミング
この章では,ドライバをプログラムする場合に,よく用いられる概念や技術を説明する.
F.1 デバッグ
カーネル (ドライバ) の中では,printf は使えないが,printk という関数が使える.使い方は,printf と同
じである.
ドライバーの中では,とにかくエラーチェックを徹底的にやる.ありえないと思われることでも,一応チェック
40
をしておく.絶対にありえない条件が成り立ったときは,次のように書く.
if (ありえないけど念のためチェック) panic("tactsw: ∼");
panic を呼ぶと,CPU のレジスタ情報などを表示して,ただちに OS が停止する.
F.2 GPIO
General Purpose IO の略である.要するに,信号ピンの現在の電圧を入力 (5V なら 1,0V なら 0) したり,信
号ピンに電圧を出力したり (5V や 0V) するような IO のこと.
GPIO は頻繁に使うのでドライバを書く人のためのライブラリがある.サンプルドライバもこれを使っている.
このため,ハードの IO レジスタにアクセスする部分は,実は見えない.それをするには,かなりハードを理解し
ないと難しいので,今回は GPIO ライブラリを使うことにする.
このライブラリを使うと,各デバイスの個々の信号ピンに割り当てられた gpio 番号という番号さえ分かれば,
そこへの入出力はライブラリ関数を呼ぶだけでよい.
• gpio_to_irq(GPIO): GPIO 番号に対応した IRQ 番号 (割り込み番号) を返す
• gpio_get_value(gpio): GPIO 番号に対応する信号ピンの現在の状態を返す (1 or 0)
• gpio_request(gpio, "tactsw"): この GPIO 番号の利用開始."tactsw"はデバッグやログのために使
うだけで余り意味はない.
• gpio_direction_input(gpio): この GPIO 番号を入力として使うよう設定
• gpio_free(gpio): この GPIO 番号の使用終了
F.3 待ち方 (=寝方),起こし方
[プロセスの待ち方 (=寝方)]
ドライバの中で待ちに入る基本関数は sleep_on().しかし,
while(条件) sleep_on()
だと,条件を見てから寝るまでの間に条件を見落とす可能性がある.
従って,普通は wait_event() を使う.これは,まずプロセスを仮の待ち状態にしてから条件を見るような方
法で,条件見落としを防いでくれる.書き方は,次の通りである (これを見ると分かるように,wait_event は関
数ではない).
wait_event(キュー, 条件);
wait_event には,名前に_interruptible が付いたものと,_timeout が付いたものがある._interruptible
が付いた関数で待ちに入ると,signal があった時に起きあがる.これを付けないと signal (例えば^C) が無視され
る._timeout が付いた関数で待ちに入ると,指定時間経過すると起きる (今回は使わない).
wait_event などを,割り込みハンドラの中で使ってはならない.また,割り込みを禁止した状態で使ってはな
らない.
[プロセスの起こし方]
プロセスを起こすには,wake_up() を使う.引数は,キューである.wake_up() は,キューの中で寝ているプ
ロセスのうち,non-interruptible も interruptible も含めて一つだけを起こす.wake_up_interruptible() は,
interruptible で sleep したプロセスを一つ起こす.
F.4 割り込み
request_irq の最後の引数と free_irq の最後の引数の値が一致するように注意すること.
41
F.5 スピンロック
プロセスと割り込みハンドラがデバイス構造体の情報などを同時書き込みしないように,ロックを取ってから
書き込みをするという手順を守るようにする.ロックを取れるのは一つのプロセスだけなので,同時書き込みは
発生しない.実は,シングルコアであれば割り込みを禁止するだけで良いが,マルチコアのプロプロセッサでは,
物理的に複数のコアが走るので,これも対処しなければならない.このためのロックがスピンロックである.
このロックを取るのに,spin_lock_irqsave() を使う.
spin_lock_irqsave(&(tactsw_info.slock), irqflags);
spin_lock_irqsave は,ロックを取るまで busy wait で待ち,取ったら今そのコードを実行しているプロセッ
サの割り込み (IRQ) を禁止する.(割り込み禁止前の状態は irqflags に入る.) busy wait で待つのは,物理的に
別のプロセッサが,その slock をロックしているかもしれないからである.そのプロセッサ極短時間でロックを
解放するはずなので,解放されるまでループしながら待つ.解放されたら,自分がロックを取る.これで,もう別
のプロセッサがそのロックを取ることはできなくなった.しかし,自分自身に割り込みが入って,割り込みハンド
ラが動き,その中でロックが必要になるかもしれない.そのような事態を避けるために,割り込みを禁止するので
ある.
終了したら,
spin_unlock_irqrestore(&(tactsw_info.slock), irqflags);
これによってロックを解放する.(これにより,別のプロセッサでスピンロックしながら待っていたコードが実行
を開始するかもしれない.) そして,irqflags を使って,CPU を元の状態に戻する.(これによって,割り込み
が許可されるので,割り込みハンドラが動き出したりするかもしれない.)
いつスピンロックを使うかだが,複数のプロセスがある一つのデータ構造を操作する可能性があるときは,いつ
でも使わなければならない.例えば,ドライバ内に何バイトデータが溜ったかを保持するカウンタなどは,割り
込みハンドラがカウンタを増やし,read 時にはカウンタが減る.また,同時に各種ポインタをいじることもある.
そのような操作は,ロックを取って行う必要がある.
なお,count を読み出したり,単にインクリメントしたりする場合などまで,スピンロックをするのは面倒であ
る.その場合は,atomic_inc や atomic_read を使うべきである.ただし,32 ビット境界にあるデータのリード
は,多くのハードでは不可分で実行されるのでロックしなくても良い.サンプルプログラムでもしていない.
F.6 ユーザ空間とのデータのやり取り
ドライバのトップハーフは,ユーザプロセスのまま特権モードで動作している.つまり,トップハーフのメモリ
はユーザ空間の状態である.一方,キーボード (今回はスイッチ) からの入力は,割り込みハンドラによって取得
されるが,割り込みハンドラはカーネルの中にあるので,取得したデータはカーネル空間に入ってくる.カーネル
空間とユーザ空間は分離されており,カーネル空間からユーザ空間へは簡単にデータをコピーすることはできな
い.これを行う関数が,copy_to_user である.引数は以下の通り.
copy_to_user(コピー先アドレス, コピー元アドレス, バイト数);
ただし,コピー先アドレスはユーザ空間のアドレス,コピー元アドレスはカーネル空間のアドレスである.
また,今回は行わないがデータを出力 (write) することを考えると,書き込みたいデータはユーザ空間にあ
り,ドライバからデータを出力させるには,カーネル空間にデータをコピーする必要がある.これを行うのが,
copy_from_user である.
copy_to_user も copy_to_user も,
「待ち」になる可能性のある関数である.従って,割り込みハンドラの中
で使ってはならない.
42
なぜ copy_to_user や copy_from_user は,「待ち」になる可能性があるのだろうか.コピー元やコピー先と
して指定したユーザ空間アドレスが,仮想記憶によってページアウトされている可能性があるからである.その
場合は,ページをメモリに読み込む必要があり,これにはかなりの時間がかかる.その間,そのプロセスはスリー
プし,別のプロセスが実行される.
F.7 シグナル
カーネル (ドライバ) からユーザプロセスにシグナルを送るには,kill_pid() を使う.
kill_pid(pid 番号, シグナル番号, 1);
pid 番号については F.8 で述べる.シグナル番号は,SIGIO, SIGUSR1 などを使うと良い.第 3 引数が 1 のとき
は,シグナル元がカーネルであることを示す.0 のときは,シグナル元がユーザであることを示す.
ドライバからプロセスにシグナルを送る標準的な方法としては,kill_fasync がある.ioctl で FASYNC オプ
ションを指定して設定する.kill_fasync は利用手順が複雑なので今回は避けたが,kill_pid をこのような形
で利用するのは,実は余り適切ではない.
参考: kill_pid, kill_fasync の他に,send_sig という関数もある.これは,struct task_struct *に直
接シグナルを送れる.ただし,相手が受け取れる状態にない (既に exit していたなど) と kernel panic になる.
カーネル関数にはさまざまなものがあり,利用状況にあわせて最適なものを利用する必要があるが,かなりの知識
を要する.
F.8 pid
カーネル (ドライバ) の中でプロセスを指示するには,幾つかの方法がある.
• pid_t
• struct task_struct *
• struct pid *
である (他にもあるかもしれない).
pid_t は,実体は int であり,プロセス番号そのものである.これはアプリケーションレベルではよく使われ
る.ps コマンドを実行すると見ることができる.実は,プロセス番号は 32767 個しかないので再利用される.最
悪の場合,昔の pid_t pid が指すプロセスは,今指しているプロセスとは別という可能性もある.(ドライバプ
ログラミングだけでなく,アプリケーションにおいてもプロセス番号だけを信じるのは実は危険である.)
struct task_struct (include/linux/sched.h) は,プロセスを表現する巨大なデータ構造である.(ここでは
タスクとプロセスは,ほぼ同じだと考えて良い.) struct task_struct *は,そこへの直接のポインタである.
pid_t のように再利用によって問題が生じることはない.しかし,そのプロセスが (exit などして) 存在しなく
なっても,ポインタとしては残る.例えば send_sig で直接 struct task_struct * にシグナルを送るとき,
カーネルは送信先のプロセスが存在するかのチェックをしないので,kernel panic になる可能性がある.カーネル
の中の信頼できるプログラムが使う関数なので,そんなチェックはしないのである.struct task_struct *に
よって指し示すことは,よく分かってやらないと危険である (カーネル内は全部そうだが).
struct pid (include/linux/pid.h) は,pid のハッシュ表の一部である.プロセス番号からプロセス本体の
データを得る場合に,32767 のテーブルを使うと無駄であるからハッシュ表を使う.この構造の一部を指すの
が struct pid *である.これは task_struct の問題も,pid_t pid のような問題もない.struct pid *と
struct task_struct * との間は以下の関数を使って,互いに行き来できる.
• task_pid(struct task_struct *): そのプロセスの pid を返す
• pid_task(struct pid *, enum pid_type): pid に対応するプロセスを返す
43
もし pid に対応するプロセスがもう存在しなくても,エラーチェックをしているので kernel panic になることは
ない.その意味で安心である.kill_pid の第一引数には,これを指定する.
ただし,pid は,その pid の利用者の数を数えて管理をしている.(利用者がいなくなったらデータを捨てる等
の処理を自動的に行う.) このため,利用するときには get_pid を呼び,使い終わったら put_pid() を呼ぶのが
ルールである.次のような使い方だけを知っていれば十分なはずである.
使い始め:
struct pid *my_pid = get_pid(task_pid(current));
使い終わり:
put_pid(my_pid);
なおここで,current という変数のデータ型は struct tast_struct *の変数 (実際は変数ではないが) であ
り,今,実行中のプロセスを指す.つまり,上の一行で,今実行中のプロセスの pid を取得したことになる.
F.9 カーネルタイマー
カーネルタイマーとは,カーネルコードの中で,指定時間経過にある関数を呼び出すための機構である.一般の
アプリケーションなら,sleep を使って指定した時間だけ寝るところだが,割り込みハンドラの中では寝ることは
許されない.そこで,カーネルタイマーを使う.タイマーの宣言は,次のようにする.
struct timer_list my_timer;
関連する関数は以下の通り.
• init_timer(&my_timer) 初期化 (使う前に一回だけ呼び出す)
• add_timer(&my_timer) タイマー開始
• del_timer(&my_timer) タイマー中止
add_timer の前には,必要な設定をする.典型的には次のようになる.
mytimer.expires = jiffies + INTERVAL;
mytimer.function = my_timer_handler;
ここで,jiffies は現在時刻 (ただしミリ秒) が入っている変数である.INTERVAL は何ミリ秒後に起きたいかを
指定する.my_timer_handler は自分で定義する関数の名前である.この関数がどのようなプロトタイプでなけ
ればならないかは,自分で調べる.
なお,同じタイマー構造体を 2 回 add_timer すると,NULL pointer が何たら,という kernel panic になる.
アセンブリソースを調べて,panic が発生した PC のアドレスの少し後に,mod_timer の呼び出しがあったら,
add_timer を 2 回呼んでしまったということである.(add_timer は内部で mod_timer を呼んでいるので,こう
なる.)
付録 G
BMP 関連ソフトの解説
G.1 bmpread.c
BMP ファイルを読み込んで画像データを取り出すプログラムである.BMP ファイルはヘッダと画像データ
の 2 つのパートから構成される.ヘッダは,どのような画像が格納されているかに関する情報で,画像の縦横の
サイズなどが含まれている.関数 readbmp はこのヘッダを解析する関数である.このプログラムでは,あらゆる
BMP ファイルを扱えるわけではない.例えば,圧縮がかかった BMP ファイルは扱えない.readbmp は,ファ
イルを bmpbuff に読み込んだ後,自分が扱えるファイルなのかの確認,ならびに画像の縦横サイズ (height と
width) の取得を行う.
44
関数 showbmp は,画像データを読み込む関数である.多くの場合は,showdot を修正すれば良いはずである.
G.2 fbwrite.c
任意の色をターゲット上の液晶ディスプレイに表示するプログラムである.ハードウェアに依存したプログラ
ムのため,このまま使うしかない部分が多い.特に openfb,closefb は修正の必要はほとんどない.openfb は,
フレームバッファ (/dev/fb0) というデバイスをオープンして,画面の情報を取得する.フレームバッファとは,
表示画像のデータがそのまま入ったメモリであり,そこに書き込めば,そのまま画面に表示される.この関数の中
で最も重要なのは,mmap である.mmap とは,memory mapping の略であり,MMU (コンピューテアーキテク
チャで聞いたはず) を操作するシステムコールである.今回は,フレームバッファというディスプレイ用のメモリ
を,プログラムから普通のポインタを使ってアクセスできるように指定している.
こうすると,後は pset の中を見ると分かるように,ポインタで色を表現するデータを書き込めば,そのまま表
示される.
通常は display 関数 (場合によっては pset も) を修正すれば良いはずである.一つ注意が必要なのは,col と
いう色のデータをどう作るかである.多くの場合,色は RGB の 3 原色であり,各色は 8bit で表現される.実際,
BMP ファイルもそのようになっている.一方,col は unsigned short,つまり 16bit の整数である.この 16bit
の中身は,上位ビットから順に赤 5bit,緑 6bit,青 5bit とする.これはハードで決まっているビット構成なので,
このようにするしかない.8bit のデータをビットを削って 5bit や 6bit にするには,C のビット AND (&) を使っ
て捨ててしまえば良い.また,ビット位置をずらすには,<<や>>を使う.ビット位置が確定した複数のデータを
合成するには,ビット OR (|) を使う.結局,r, g, b から col を計算するプログラムは,次のようになる.
col = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
付録 H
サンプルドライバの解説
H.1 ドライバの構成
ドライバのファイルは,tactsw.c である.このファイルは,次のような順序でプログラムが書かれている.
ドライバの各関数が共通で使う外部変数 (群)
関数定義 (群)
module_init(tactsw_init);
module_exit(tactsw_exit);
MODULE_AUTHOR("Project6");
MODULE_DESCRIPTION("tact switch driver for armadillo-440");
MODULE_LICENSE("GPL");
外部変数で最も重要なのは,struct cdev である.ドライバには,character device と block device があるが,
今回は前者で行くので,cdev を使う.
module_init(tactsw_init); は,モジュールが insmod されたときに実行される関数を登録している.次の
行は,モジュールが削除されたときに実行される関数である.その下の 3 行で,ドライバの説明を登録している
(これは必須である).
従って,tactsw_init() をまず説明する.
H.2 ドライバの初期化
まず,外部変数を説明する.tactsw_buttons[] は,このドライバが扱う複数のボタンの一覧 (配列) である.
GPIO というマクロについては,別途説明する.
tactsw_dev は,OS に登録するデバイス構造体である.
tactsw_info は,各関数が使うさまざまなデータが詰め込んである.特に重要なのは,char msg[] である.
45
このドライバでは,ボタンをキーボードのように取り扱っている.すなわち,ボタンが押されると文字が入力され
たことになるので,それを溜めておくのが,配列 msg である.その他のメンバーについては,その都度説明する.
tactsw_init() は,ほとんどお決まりの書き方である.大雑把に説明すると,新しいキャラクタデバイス用の
メモリを OS から alloc して,major 番号を一つ割当てる.cdev_init で,このデバイスに関係する関数の一覧
表 (配列) である tactsw_fops を登録する.最後に cdev_add でデバイスを OS に登録する.これで OS に対す
る処理は終わりである.
その後,tactsw_setup を呼び出す.ここでは,このデバイス特有の初期化を行う.このドライバが管理する一
つ一つのボタンついて,以下を行う.
1. 変数 gpio に gpio 番号を得る
2. gpio_request() で GPIO のモジュールの利用開始を宣言する
3. GPIO は入力も出力もできるが,今回は入力モードに設定する
4. gpio_to_irq() で,そのボタンの割り込み (IRQ) 番号を得る.その番号に対して,割り込みハンドラ
tactsw_intr を登録する.
tactsw_exit() は,さまざまな登録を逆に解除していく.例えば,gpio_request() を実行したら,終わると
きには gpio_free() を実行しなければならない.
H.3 デバイスの open
static int tactsw_open(struct inode *, struct file *);
現在の実装では 2 つのプロセスが同時にオープンすることに対応していないため,これを防御している.
現在のドライバは,/dev/tactsw にデータを書き込む (writer) のは割り込みハンドラだけであり,読み出す
(reader) のは open をしたプロセス 1 つだけである.これは,single writer, single reader 型のソフトと呼ばれる.
writer (割り込みハンドラ) は,同時に reader が動いているかもしれないことだけに注意してプログラムする.
reader は writer だけに注意してプログラムする.
一方,同時 open が複数回可能とすると,multiple reader ということになる.この場合は,reader を作るとき
には,同時に別の reader がいることにも注意を払わないといけない.具体的にプログラム上のどの部分に注意が
必要になるか,自分で考えてみて欲しい.
H.4 デバイスからのデータ読み込み
static int tactsw_read(struct file *, char *, size_t, loff_t *);
第 1 引数は,file へのポインタである.今回のドライバでは,そもそも入力の元が一つしかないので,この引数
は使わない.
第 2 引数は,read 関数を呼んだユーザが用意したバッファのアドレスである.このバッファに対して,read し
た結果を書き込まなければならない.
第 3 引数は,入力すべきデータの最大数である.
第 4 引数は,入力データのオフセットであるが,今回は使わない.
wait_event_interruptible は,ある条件が立り立つまで,スリープする関数である.第 2 引数が待つべき条
件である.今回は,tactsw_info.mlen != 0 となるまで待つ.メンバー mlen は,tactsw_info.msg[] に入っ
ているデータの個数である.つまり,もし msg[] にデータがなければ,データが入ってくるまで待つということ
を意味する.
これ以降は,データが msg[] にあったので,それを取り出して,read を呼び出したユーザの返すための処理で
ある.まず,mlen を read_size に取り出す.
copy_to_user() は,msg[] の内容を buff に,read_size 個だけコピーする.これは非常に特殊な処理なの
で,よく理解する.
46
read_size 分のデータをユーザに渡したので,そのデータを msg[] から削除する.通常はリングバッファアル
ゴリズムを使うが,ここでは残りのデータをそのまま左にシフトしている.
H.5 割り込みハンドラ
ボタンが押されると GPIO 割り込みが発生する.OS がこれを処理し,request_irq で登録した関数である
tactsw_intr を呼び出す.
static irqreturn_t tactsw_intr(int, void *)
このときの引数は,第 1 引数は IRQ 番号であり,第 2 引数はデバイスの ID である.
すべてのボタンに対して,以下を行う.
1. そのボタンの gpio 番号に対応する IRQ 番号だったときは,以下を行う
2. gpio_get_value(gpio) によって,その GPIO ハードウェアの状態を得る.
3. もし,まだ msg[] に余裕があったら,以下を行う.
4. msg[] に適切な文字コードを追加する.
5. このデバイスに入力があるのを待っているプロセスがあるかもしれないので,wake_up_interruptible
を実行する.
6. 割り込みが正しく処理された場合は,IRQ_HANDLED を戻す.
H.6 デバイスの ioctl
int tactsw_ioctl(struct inode *, struct file *, unsigned int, unsigned long);
第 1 引数は inode,第 2 引数はファイルポインタで,今回は使わない.第 3 引数は ioctl へのコマンドで,第 4
引数はコマンドのパラメタである.
ioctl を拡張するには,この中にプログラムを作っていく.
付録 I
ソースコードの解読
この章では,カーネルソースコードを読む際の注意点,幾つかのテクニック,実際に GPIO 関連の部分を追い
掛けたメモなどをまとめた.興味がある人だけ読めば良い.
I.1 Platform Device
Linux 2.6 からドライバに新たに platform device というモデルが入った (従来の character device, block
device もある).ただし,ドライバとは何かを理解するには,従来のモデルの方が都合がよい.platform device
では,いろいろな抽象化が入るのでイメージをつかみにくい.
今回の演習では使わないが,platform device を簡単に説明する.旧来のデバイスは,デバイス固有の動作の記
述に対して,そのデバイスをシステムがどう扱うかなどが混在していた.platform device では,デバイス,バス,
ドライバを分離して作っていく.ここでバスとは,本当のバスではなくて pseudo bus.
struct platform_device は,name, id, resource (ともちろん dev へのポインタ) だけの構造体で,これが
bus に相当する.resource 構造体の中にアドレス情報などを入れる.
ドライバの中では,platform_get_resource() という関数で,resource を取ってくる.resource->start が
最初の制御レジスタのアドレス.
バスにデバイスがぶらさがり,その先にドライバがあるという構造になる.
47
I.2 割り込み
デバイスドライバにおいて,割り込みハンドラを登録するには,request_irq() を使う.irq とは,interrupt
request,割り込み要求のことである.
本当の割り込みを受け付ける,OS 本体の割り込みハンドラは実は別にある.その割り込みハンドラから,
request_irq() で登録した関数を呼び出して貰う仕組みになっている.
本物のコードは,kernel/irq/handle.c の中の__do_IRQ にある.
ここで,chip の ack メンバが割り込みを受けつけ,chip の end メンバが割り込みの終了 (レベル割り込みであ
れば,信号を落とさせる) 処理をする.
また,__do_IRQ の中から handle_IRQ_event() が呼ばれ,この中で,割り込みハンドラ (action->handler())
が呼ばれている.この handler() が request_irq() で登録した関数である.
なお,登録部分のコードを追い掛けるには,set_irq_handler (または set_irq_chained_handler) を見る.
参考: 元の gpio_key.c の中の request_irq の引数の IRQF_SAMPLE_RANDOM は,割り込み頻度を乱数発生源
として OS が使ってよいかどうかの指定.ドライバの機能には何の関係もない.
I.3 insmod の引数を使いたいとき
MODULE_PARAM を使うと,insmod の引数を xxx_init() に渡すことができる.
I.4 printk の出力先変更
printk (pr_err, pr_info 等でも同じ) は,dmesg 用バッファに出て来ている.dmesg コマンドを使えば見る
ことができる.また,/etc/syslog.conf で指定しておけば,syslog に出力することができる.(要確認)
I.5 コーディングテクニック
if(unlikely(x!=0)){...}
if(likely(x!=0)){...}
意味は,unlikely や likely がない時と同じである.ただし,コンパイラに対して,
• ほぼ偽となるケース (unlikely),
• ほぼ真となるケース (likely)
を伝えることができる.中身は,マクロで__builtin_expect という指示になる.
I.6 gpio 関係
Documentation/gpio.txt は gpiolib を使うためのガイドなので参考になる.
また,drivers/input/keyboard/gpio_keys.c は,GPIO に接続されたタクトスイッチ用ドライバである.
一番気になるのは,物理的なハードウェアであるタクトスイッチの状態をソフトが読み取っている部分であろ
う.これは gpio_get_value() という関数が行っている.全部を説明するのは無理だが,次のような順序で呼び
出しが進む.
gpio_get_value();
↓
#define gpio_get_value __gpio_get_value [include/asm-arm/arch-mxc/gpio.h]
と定義されているので,実は__gpio_get_value が呼ばれる.
↓
48
__gpio_get_value() [drivers/gpio/gpiolib.c] の中で
chip->get(chip, gpio - chip->base);
を実行するが,これは mx25_gpio_get() 関数を呼ぶ.
↓
mx25_gpio_get() [arch/arm/mach-mx25/gpio.c] の中で
mxc_get_gpio_datain();
↓
mxc_get_gpio_datain() [arch/arm/plat-mxc/gpio.c] の中で
return (__raw_readl(port->base + GPIO_DR) >> GPIO_TO_INDEX(gpio)) & 1;
↓
マクロ__raw_read は [include/asm-arm/io.h] において次のように定義されている.
#define __raw_read(a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a))
このマクロは要するに*a と書いたのと同じことである.つまり,port->base+GPIO_DR というアドレスを読ん
でいることになる.GPIO の信号ピンは,このアドレスにメモリマップされており,これにより現在の信号ピン
の状態を取得できる.
以下はメモなので,興味がある人は読んで欲しい.
●各ファイルの概要説明
arch/arm/mach-mx25:
board-armadillo400.h
armadillo400.c : armadillo400 固有.特に初期化.
armadillo400_gpio.c
gpio.c: mx25_xxx という関数はここで定義 (mx25 はハードの名前)
devices.c: いろいろなデバイスの変数宣言と初期値
arch/arm/plat-mxc:
gpio.c: mxc_xxx という関数はここで定義している
drivers/gpio/gpiolib.c
以下の関数 (gpio 共通関数) の定義をしている:
gpio_request()
gpio_direction_input()
// include/asm-arm/arch-mxc/gpio.h の中で,
__gpio_get_value()
// #define gpio_get_value __gpio_get_value
__gpio_set_value()
// #define gpio_set_value __gpio_set_value
__gpio_cansleep
// #define gpio_cansleep __gpio_cansleep
include/asm-arm/arch-mxc: インクルードファイル (あんまり関係ないかも)
●初期化のうち gpio に関連する部分の流れ
arch/.../armadillo400.c の下の方
MACHINE_START(ARMADILLO440, "Armadillo-440")
.init_machine = armadillo440_init,
MACHINE_END
↓
armadillo440_init():
armadillo400_gpio_init();
mxc_gpio_init();
mx25_generic_gpio_init();
armadillo400_key_init();
↓
以下順番に追いかける.
armadillo400_gpio_init() [armadillo440_gpio.c] これはあまり関係ない
mxc_gpio_init() [plat-mxc/gpio.c]
メインは_mxc_gpio_init で,全 GPIO ポートについて以下を行う.
データの初期化:
mxc_gpio_ports[] (in devices.c) の中に GPIO レジスタの物理アドレスなどが入っている.(この
計算は,IO_ADDRESS マクロ (in mx25.h) を使っている.) これを使って gpio_port[] を初期化し
ている.この表は,GPIO 番号を物理アドレスや IRQ 番号に対応付ける重要な表.
ハードの初期化:
割り込み禁止して,irq ハンドラの設定をいろいろやっている.かなり複雑.
49
最終的に登録されるハンドラは,MXC_MUX_GPIO_INTERRUPTS が定義されていたら
mxc_gpio_irq_handler() で,
そうでなければ,
mxc_gpio_mux_irq_handler()
なお,初期化とは関係ないが,これらの中身は:
実際は,mxc_gpio_mux_irq_handler は,multiplex をバラして,一つ一つ
mxc_gpio_irq_handler を呼んでいるだけ.最後は d->handle_irq(); を実行.
これが gpiolib のキモの一つ.
mx25_generic_gpio_init() [gpio.c]
gpiolib.c の中の gpiochip_add() を複数回呼び出している.
add しているのは GPIO の 4 つのチップで,これは配列 mx25_gpio_banks[] に入っている.
chip 固有 (つまりハード固有) の関数はこの構造体を経由して呼ぶことになっている.
chip->get() などのような形 (plat-mxc/gpio.c を参照).
以下の値が関数ポインタとして定義されている.
.direction_input = mx25_gpio_direction_input
.direction_output = mx25_gpio_direction_output
.get
= mx25_gpio_get
.set
= mx25_gpio_set
.dbg_show
= mx25_gpio_dbg_show
これが gpiolib のキモのもう一つ.
armadillo400_key_init() [armadillo400.c]
mxc_register_device(&mx25_gpio_key_device,&armadillo400_gpio_key_data);
ここで,mx25_gpio_key_device は,devices.c で定義されており,名前を見ると "gpio_keys" である.
armadillo400_gio_key_data は構造体の表で,gpio メンバー (GPIO のポート番号),code メンバー
(KEY_ENTER 等のキーコード) などを定義している.gpio_keys.c の中で出てくる button という変数
はこれ.
mxc_register_device は,メンバー platform_data (デバイスのプライベートデータ) に上の第 2 引数
(button 情報) を登録する.そして第 1 引数を platform_device_register する.
これによって,"gpio_keys"という名前のデバイスが登録される.この名前は,gpio_keys.c の中で登録する
platform driver の名前になる.あるドライバ関数を呼ぶときの引数に platform device を渡すがこの対
応付けを名前でやっているのだろう.
なお,gpio_request(GPIO 番号,"gpio_keys") という呼び出しがある.この文字列は,カーネルメッセー
ジ,および sysfs に使われるだけらしい.
●おまじないの部分
module_init(xxx_init);
↓
__define_initcall("6", xxx_init, 6);
↓
static initcall_t __initcall_xxx_init6 __used
__attribute__((__section__(".initcall" level ".init"))) = xxx_init;
__used は,この変数が使われなくても warning を出すなという指示.
__attribute__は,__initcall_xxx_init6 を".initcall6.init"というセクションに置けという指示.
".initcall6.init"という名前のセクションが無条件に呼ばれる.これは,「.initcall.init というセクショ
ンに .initcallX.init をかき集めて入れろ」という指示がリンカースクリプトとして書いてあるから.
50
tactsw_sample.c
1
2
3
4
5
6
/**
* Sample driver for tact switch
* File name: tactsw.c
* Target board: Armadillo 440
* For Project-enshu 6 @ Shibaura Institute of Technology
*/
7
8
9
10
11
12
13
#include
#include
#include
#include
#include
#include
#include
<linux/module.h>
<linux/kernel.h>
<linux/fs.h>
<linux/gpio.h>
<linux/string.h>
<asm/uaccess.h>
<linux/cdev.h>
14
15
#define N_TACTSW
#define MSGLEN
16
17
18
19
20
21
22
23
24
25
26
27
static int tactsw_buttons[] = { // board dependent parameters
GPIO(3, 30), // SW1
#if defined(CONFIG_MACH_ARMADILLO440)
GPIO(2, 20), // LCD_SW1
GPIO(2, 29), // LCD_SW2
GPIO(2, 30), // LCD_SW3
#if defined(CONFIG_ARMADILLO400_GPIO_A_B_KEY)
GPIO(1, 0),
// SW2
GPIO(1, 1),
// SW3
#endif /* CONFIG_ARMADILLO400_GPIO_A_B_KEY */
#endif /* CONFIG_MACH_ARMADILLO440 */
};
28
29
// character device
static struct cdev tactsw_dev;
30
31
32
33
34
35
36
37
38
39
40
41
// Info for the driver
static struct {
int major;
int nbuttons;
int *buttons;
int used;
42
43
44
45
46
47
48
49
50
51
52
53
static int tactsw_open(struct inode *inode, struct file *filp)
{
unsigned long irqflags;
int retval = -EBUSY;
spin_lock_irqsave(&(tactsw_info.slock), irqflags);
if (tactsw_info.used == 0) {
tactsw_info.used = 1;
retval = 0;
}
spin_unlock_irqrestore(&(tactsw_info.slock), irqflags);
return retval;
}
54
55
56
57
58
59
60
static int tactsw_read(struct file *filp, char *buff,
size_t count, loff_t *pos)
{
char *p1, *p2;
size_t read_size;
int i, ret;
unsigned long irqflags;
int mlen;
char msg[MSGLEN];
wait_queue_head_t wq;
spinlock_t slock;
} tactsw_info;
1
256
// number of minor devices
// buffer length
//
//
//
//
//
//
//
//
//
major number
number of tact switchs
hardware parameters
true when used by a process,
this flag inhibits open twice.
buffer filll count
buffer
queue of procs waiting new input
for spin lock
61
if (count <= 0) return -EFAULT;
62
ret = wait_event_interruptible(tactsw_info.wq, (tactsw_info.mlen != 0) );
51
63
if (ret != 0) return -EINTR;
// interrupted
64
65
read_size = tactsw_info.mlen;
// atomic, so needless to spin lock
if (count < read_size) read_size = count;
66
67
68
69
70
if (copy_to_user(buff, tactsw_info.msg, read_size)) {
printk("tactsw: copy_to_user error\n");
// spin_unlock_irqrestore()
return -EFAULT;
}
71
72
73
// Ring buffer is better.
p1 = tactsw_info.msg;
p2 = p1+read_size;
74
75
76
77
78
spin_lock_irqsave(&(tactsw_info.slock), irqflags);
// This subtraction is safe, since there is a single reader.
tactsw_info.mlen -= read_size;
for (i=tactsw_info.mlen; i>0; i--) *p1++=*p2++;
spin_unlock_irqrestore(&(tactsw_info.slock), irqflags);
But we prefer simplicity.
79
80
}
81
82
83
84
int tactsw_ioctl(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg)
{
int retval=0;
85
86
87
88
89
90
91
92
93
94
95
96
return read_size;
switch(cmd){
case 1:
// fetch current status
case 2:
// set signal
case 3:
// clear signal
default:
retval = -EFAULT;
// other code may be better
}
return retval;
}
97
98
99
100
101
static int tactsw_release(struct inode *inode, struct file *filp)
{
tactsw_info.used = 0;
return 0;
}
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
static irqreturn_t tactsw_intr(int irq, void *dev_id)
{
int i;
for (i = 0; i < tactsw_info.nbuttons; i++) {
int gpio = tactsw_info.buttons[i];
if (irq == gpio_to_irq(gpio)) {
int mlen;
unsigned long irqflags;
int val = gpio_get_value(gpio);
int ch = (val == 0)? ’1’:’0’;
// val=0 when key is pushed
spin_lock_irqsave(&(tactsw_info.slock), irqflags);
mlen = tactsw_info.mlen;
if (mlen < MSGLEN) {
tactsw_info.msg[mlen] = ch;
tactsw_info.mlen = mlen+1;
wake_up_interruptible(&(tactsw_info.wq));
}
spin_unlock_irqrestore(&(tactsw_info.slock), irqflags);
return IRQ_HANDLED;
}
}
return IRQ_NONE;
52
126
}
127
128
129
130
131
132
133
134
// .XXX = という書き方は,gcc という特殊な C に独自の機能で,
// XXX という構造体メンバだけを初期化できる.
static struct file_operations tactsw_fops = {
.read = tactsw_read,
.ioctl = tactsw_ioctl,
.open = tactsw_open,
.release = tactsw_release,
};
135
136
137
static int __init tactsw_setup(int major)
{
int i, error, gpio, irq;
138
139
140
141
142
143
144
tactsw_info.major = major;
tactsw_info.nbuttons = sizeof(tactsw_buttons)/sizeof(int);
tactsw_info.buttons = tactsw_buttons;
tactsw_info.used = 0;
tactsw_info.mlen = 0;
init_waitqueue_head(&(tactsw_info.wq));
spin_lock_init(&(tactsw_info.slock));
145
146
for (i = 0; i < tactsw_info.nbuttons; i++) {
gpio = tactsw_info.buttons[i];
147
148
149
150
151
152
error = gpio_request(gpio, "tactsw");
// 2nd arg (label) is used for debug message and sysfs.
if (error < 0) {
printk("tactsw: gpio_request error %d (GPIO=%d)\n", error, gpio);
goto fail;
}
153
154
155
156
157
error = gpio_direction_input(gpio);
if (error < 0) {
printk("tactsw: gpio_direction_input error %d (GPIO=%d)\n", error, gpio);
goto free_fail;
}
158
159
160
161
162
163
irq = gpio_to_irq(gpio);
if (irq < 0) {
error = irq;
printk("tactsw: gpio_to_irq error %d (GPIO=%d)\n", error, gpio);
goto free_fail;
}
164
165
166
167
168
169
170
171
172
error = request_irq(irq, tactsw_intr,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"tactsw",
// used for debug message
tactsw_intr);
// passed to isr’s 2nd arg
if (error) {
printk("tactsw: request_irq error %d (IRQ=%d)\n", error, irq);
goto free_fail;
}
}
// end of for
173
174
175
176
return 0;
free_fail:
gpio_free(gpio);
177
178
179
180
181
182
183
184
fail:
while (--i >= 0) {
gpio = tactsw_info.buttons[i];
free_irq(gpio_to_irq(gpio), tactsw_intr);
gpio_free(gpio);
}
return error;
}
185
186
187
// ・static と付けるとこのファイル内でのみ見えることの指示.
//
付けないと OS 全体から見えるようになる.シンボルテーブルが取られる.
//
OS がリブートしない限りシンボルテーブル上で邪魔になる.
53
188
189
190
191
192
193
// ・init __init とすると .text.init セクションに入る.
//
不要になった時 (=init 終了後) に削除してくれる.
static int __init tactsw_init(void)
{
int ret, major;
dev_t dev = MKDEV(0, 0);
// dev_t は単なる int
194
printk("tactsw: tactsw_init\n");
195
196
197
198
199
200
ret = alloc_chrdev_region(&dev, 0, N_TACTSW, "tactsw");
if (ret < 0) {
return -1;
}
major = MAJOR(dev);
printk("tactsw: Major number = %d.\n", major);
201
202
cdev_init(&tactsw_dev, &tactsw_fops);
tactsw_dev.owner = THIS_MODULE;
203
204
205
206
207
208
ret = cdev_add(&tactsw_dev, MKDEV(major, 0), N_TACTSW);
if (ret < 0) {
printk("tactsw: cdev_add error\n");
unregister_chrdev_region(dev, N_TACTSW);
return -1;
}
209
210
211
212
213
214
215
216
ret = tactsw_setup(major);
if (ret < 0) {
printk("tactsw: setup error\n");
cdev_del(&tactsw_dev);
unregister_chrdev_region(dev, N_TACTSW);
}
return ret;
}
217
218
219
220
221
// init __exit は上と同様に .text.exit セクションに入る.
static void __exit tactsw_exit(void)
{
dev_t dev=MKDEV(tactsw_info.major, 0);
int i;
222
223
224
225
226
227
228
// disable interrupts
for (i = 0; i < tactsw_info.nbuttons; i++) {
int gpio = tactsw_info.buttons[i];
int irq = gpio_to_irq(gpio);
free_irq(irq, tactsw_intr);
gpio_free(gpio);
}
229
230
231
// delete devices
cdev_del(&tactsw_dev);
unregister_chrdev_region(dev, N_TACTSW);
232
233
234
235
236
237
238
// wake up tasks
// This case never occurs since OS rejects rmmod when the device is open.
if (waitqueue_active(&(tactsw_info.wq))) {
printk("tactsw: there remains waiting tasks. waking up.\n");
wake_up_all(&(tactsw_info.wq));
// Strictly speaking, we have to wait all processes wake up.
}
239
240
}
printk("tactsw: exit module\n");
241
242
module_init(tactsw_init);
module_exit(tactsw_exit);
243
244
245
MODULE_AUTHOR("Project6");
MODULE_DESCRIPTION("tact switch driver for armadillo-440");
MODULE_LICENSE("GPL");
54
bmpread.c
1
2
3
4
5
6
7
//
// A sample program to read and parse BMP file
// For Project-enshu 6 @ Shibaura Institute of Technology
//
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
8 // This program does not check whether the bmp file is larger than MAXSIZE
9 #define MAXSIZE 10000000
10 unsigned char bmpbuff[MAXSIZE];
11
12
13
14
15
16
17
// convert 4 bytes to integer
// i: index to bmpbuff[], 1st byte of the consecutive 4 bytes
// return: converted integer (0 ... 2^32-1)
unsigned int get4(int i)
{
return bmpbuff[i+3]<<24 | bmpbuff[i+2]<<16 | bmpbuff[i+1]<<8 | bmpbuff[i];
}
18
19
20
21
22
23
24
// convert 2 bytes to integer
// i: index to bmpbuff[], 1st byte of the consecutive 2 bytes
// return: converted integer (0 ... 2^16-1)
unsigned int get2(int i)
{
return bmpbuff[i+1]<<8 | bmpbuff[i];
}
25 // do something for each dot (it’s up to you)
26 void showdot(int b,int g,int r, int x, int y){
27
if (y<200) return;
28
if (x>70) return;
29
if (x==0) printf("\n");
30
if (b+g+r>300) printf("X");
31
else if (b+g+r>200) printf(".");
32
else printf(" ");
33 }
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// check and read BMP file
// First of all, this function checks the BMP file’s header.
// It confirms BMP mark, Windows bmp, 24b color, uncompressed.
// If OK, it reads the entire BMP file into bmpbuff[].
// return value:
//
start index of image data (when confirmed)
//
-1 when incorrect file
int readbmp(char *filename, unsigned int *height, unsigned int *width)
{
int fd, len;
fd = open(filename, O_RDONLY);
if (fd<0) {printf("Cannot open %s", filename); return -1;}
len = read(fd, bmpbuff, MAXSIZE);
if (len < 0) {printf("Cannot read"); return -1;}
close(fd);
49
50
51
52
53
54
55
56 }
57
58
59
60
61
62
63
if (get2(0) != 0x4d42) {printf("not bmp\n"); return -1;}
if (get4(14) != 40) {printf("not bmp\n"); return -1;}
if (get2(28)!=24) {printf("not 24bit\n"); return -1;}
if (get4(30)!=0) {printf("compressed file\n"); return -1;}
*width = get4(18);
*height = get4(22);
return 54;
// Show each dot in the bmp file
// return value: 0 normal, -1 error
int showbmp(char *filename)
{
unsigned int width, height;
int x, y, i, base, r, g, b, linesize;
base=readbmp(filename, &height, &width);
55
64
if (base<0) return -1;
65
66
67
linesize=width*3;
i=linesize%4;
if (i!=0) linesize=linesize+(4-i);
68
for(y=0; y<height; y++){
69
for(x=0; x<width; x++){
70
b = bmpbuff[base+y*linesize+x*3];
71
g = bmpbuff[base+y*linesize+x*3+1];
72
r = bmpbuff[base+y*linesize+x*3+2];
73
showdot(b,g,r,x,y);
74
}
75
}
76
return 0;
77 }
78 int main()
79 {
80
if (showbmp("free1.bmp")<0) return 1;
81 }
56
fbwrite.c
1
2
3
4
5
6
7
8
9
10
11
12
//
// A sample program to display dots on armadillo’s LCD
// For Project-enshu 6 @ Shibaura Institute of Technology
//
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <limits.h>
13
14
#define FBDEV "/dev/fb0"
15
16
17
18
19
// Color assign is red(5bit), green(6bit), blue(5bit)
// These are sample colors
#define RED
0xf800
#define GREEN 0x07e0
#define BLUE
0x001f
20
21
22
23
24
25
26
struct tagctxt{
int fd;
// file descriptor for frame buffer device
struct fb_var_screeninfo fvsi;
// frame buffer information
struct fb_fix_screeninfo ffsi;
// frame buffer information
long int screensize; // frame buffer size (used by mmap)
char *fb_base ; // base address of mmap
} ctxt;
27
28
29
30
31
// Open the framebuffer
// This function initializes the external variable ctxt.
int openfb()
{
int xres,yres,bpp;
// Name of frame buffer device
// "/dev/fb1" is also possible
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ctxt.fd = open(FBDEV, O_RDWR);
if (ctxt.fd<0) {
fprintf(stderr, "Cannot open");
return -1;
}
if (ioctl(ctxt.fd, FBIOBLANK, FB_BLANK_UNBLANK) != 0) {
fprintf(stderr, "Cannot wakeup LCD");
return -1;
}
if (ioctl(ctxt.fd, FBIOGET_FSCREENINFO, &(ctxt.ffsi)) != 0) {
fprintf(stderr, "Cannot get FSCREENINF");
return -1;
}
if (ioctl(ctxt.fd, FBIOGET_VSCREENINFO, &(ctxt.fvsi)) != 0) {
fprintf(stderr, "Cannot get VSCREENINFO");
return -1;
}
xres = ctxt.fvsi.xres ;
// Resolution for X direction
yres = ctxt.fvsi.yres ;
// Resolution for Y direction
bpp = ctxt.fvsi.bits_per_pixel ;
// Number of bits for each pixel
printf("%d(pixel)x%d(line), %dbpp(bits per pixel)\n",xres, yres, bpp);
53
54
// Calculate the size of frame buffer
ctxt.screensize = xres * yres * bpp / 8;
55
56
57
58
59
60
61
62
63
// Map the frame buffer device to memory.
ctxt.fb_base = (char *)mmap(0, ctxt.screensize, PROT_READ|PROT_WRITE,
MAP_SHARED, ctxt.fd, 0);
if ( (int)ctxt.fb_base == -1 ) {
fprintf(stderr, "Cannot mmap");
return -1;
}
return 0;
}
57
64
65
66
67
68
69
// Close the framebuffer
void closefb()
{
munmap(ctxt.fb_base, ctxt.screensize);
close(ctxt.fd);
}
70
71
72
73
74
75
76
77
78
// Display a dot in the screen
// col is a color whose bit assignment is shown above.
void pset(int x, int y, unsigned short col)
{
int offset;
offset = ((x+ctxt.fvsi.xoffset) * ctxt.fvsi.bits_per_pixel / 8) +
(y+ctxt.fvsi.yoffset) * ctxt.ffsi.line_length ;
*((unsigned short *)(ctxt.fb_base + offset)) = col;
}
79
80
81
82
83
84
85
// Display dots in rectangle area
// This function is just only a sample.
// Modify this function to display something. (it’s up to you)
void display()
{
int x, y;
unsigned short col;
86
87
88
89
90
91
92
col = BLUE;
for (y=0; y<240 ; y++) {
for (x=0; x<320 ; x++) {
pset(x, y, col);
}
}
}
93
94
95
96
97
98
99
int main()
{
if (openfb()<0) return 1;
display();
closefb();
return 0;
}
58