- パート1(ハードウェア)
- パート2(サーバー)
家庭用コンポーネントから組み立てられたサーバーの欠点のいくつかを排除するために、最近共有したいデバイスを開発しました。 その詳細な説明は、スキームとソースコードとともに、 最初のパートにあります。
記事のこのパートでは、カーネル空間のシリアルポートとやり取りする方法と、LinuxのRS232を介して複数のデバイスサブシステムで作業を整理する方法について説明します。
デバイスには、次のサブシステムが含まれます。
-
watchdog
デーモンで動作するハードウェアウォッチドッグ。 - 真の乱数ジェネレーター。
- 自律センサーからデータを収集するためのnRF24L01 +無線モジュール。
相対的に言えば、シリアルポートは2つのエンドポイント(エンドポイント)です。 この場合、少なくとも4つ必要です。
- コマンドを転送します。
- 回答を読む;
- 乱数のストリームを受信します。
- センサーからデータを受信します。
この問題は、コマンドがデバイスに直接発行され、ディスパッチャを使用してデバイスからのトラフィックが逆アセンブルされる場合に解決できます。 アイデアは、いくつかの思考の後、次の形式を取りました。
ここで、バックグラウンドプロセス(デーモン) wrnd
はディスパッチャとして機能します。その目的は、3つのFIFOチャネルでトラフィックをフィルタリングすることです。
- rng.fifo-乱数のストリーム。
- nrf.fifo-センサーからのデータストリーム。
- cmd.fifo-コマンドによって返されるデータ。
ダイアグラムには以下も示されています。
- wrn_wdt-ウォッチドッグタイマーを制御するキャラクターデバイスドライバー
/dev/watchdog
開発/dev/watchdog
。 - wrnctrl-ファームウェア、監視、およびデバイス管理用のユーティリティ。
- オレンジは、プロジェクトが対話するサービスを示します。
コマンドは、 [C|W|R|N][0-99]:[1]
の形式でテキスト形式でデバイスに送信されます。最初の文字はサブシステムの識別子であり、コマンド番号と引数はコロンの後に続きます。
基本的に、ソフトウェアは純粋なCで記述されており、BashおよびMakefileにスクリプトが含まれています。 インストーラーはGentoo用に設計されていますが、必要に応じて他のディストリビューションに簡単に適合させることができます。
プロジェクトのソースコードは、 wrndディレクトリのGitHub alexcustos / wrn-projectで入手できます。 コードには、センサーからの処理データが含まれています。このデータは、 ATtiny85:プロトタイプワイヤレスセンサーにあります。
すべてのコンポーネントについてさらに詳しく説明します。
運転手
記事の公開後、wdt.fifoチャネルがプロジェクトに追加されました。これは、 wrnd
デーモンによって提供され、デバイスファイル/dev/watchdog
を複製します。 両方のオプションは、 watchdog
デーモンを使用するのに適しており、一般的に似ています。 したがって、以下の情報は関連性があり、ウォッチドッグタイマードライバーの動作方法を知りたい場合に役立ちます。
プログラミングの観点から見ると、Linuxドライバーは非常に単純です。 それらの開発は、多くのよく文書化された例がカーネルソースツリーで利用可能であるという事実によって簡素化されています。 計画をコンパイルおよびデバッグしようとする場合にのみ、いくつかの問題が発生する可能性があります。 事実、カーネル空間では、glibcライブラリの使い慣れた関数は使用できず、カーネルで提示された関数のみを使用できます。 さらに、インタラクティブなコードのデバッグは非常に困難です。
しかし、この場合、キャラクターデバイス/dev/watchdog
を実装するのはタスクが非常に簡単なので、怖いことはありません( コードは完全に wrn_wdt.cです )。 デバイスはドライバー番号10(その他のデバイス)によってサービスされるため、最初に適切なデータ構造を決定する必要があります。
static struct miscdevice wrn_wdt_miscdev = { .minor = WATCHDOG_MINOR, // watchdog .name = "watchdog", // /dev .fops = &wrn_wdt_fops, // };
次に、ハンドラーを設定します。
static const struct file_operations wrn_wdt_fops = { .owner = THIS_MODULE, // .llseek = no_llseek, // .write = wrn_wdt_write, // .unlocked_ioctl = wrn_wdt_ioctl, // ioctl .open = wrn_wdt_open, // .release = wrn_wdt_release, // };
ここで、unlocked_ioctl、これはioctl()を介してデバイス記述子にアクセスするための通常のハンドラです。しばらくの間、呼び出しは非ブロッキングになりました。したがって、必要に応じて同期するために追加の対策が必要です
次に、モジュールのロードおよびアンロード操作のハンドラーを定義する必要があります。
module_init(wrn_wdt_init); // module_exit(wrn_wdt_exit); //
必要に応じて、module_param()、MODULE_PARM_DESC()を介してモジュールにパラメーターを追加し、順序を指定できます。
MODULE_DESCRIPTION("..."); // MODULE_AUTHOR("..."); // MODULE_LICENSE("..."); //
次のコマンドでこの情報を表示できます。
modinfo [path]/[module].ko
このステップで、ドライバーはほとんど準備ができています。 これを行うには、少なくともシリアルポートにデータを送信する機能が必要です。 これは、カーネルスペースではAPIが提供されていないため、直接行うことはできません。 標準ドライバーの削除と独自のドライバーの開発に関連するオプションは、イデオロギー的に正しくないものとしてすぐに除外できます。 したがって、2つのオプションが残ります。
- カーネルスペースから、ユーザースペースのファイルシステムにアクセスします。
- LDISCカーネルスペースに登録して(ラインディシプリン)、シリアルポートトラフィックをインターセプトおよび制御します。
最初のオプションを選択したことはすでに明らかであり、これについて言い訳はできません。 真剣に、回線制御を使用して、トラフィックマネージャをカーネル空間に配置する必要があります。 ただし、既に述べたように、この空間でのプログラミングとデバッグは簡単な問題ではなく、可能であれば、ユーザー空間で実行できるI / O操作と同様に、回避する必要があります。
しかし、そのようなドライバーの不適切な開発の主な理由は、あたかもそれがUSBドライバーであるかのように独立性を達成することができないことです。 ラインディシプリンは、ポートパラメータを設定し、必要なラインディシプリンを設定するために、ユーザー空間にコードを必要とする単なるレイヤーです。
可能であれば、選択が行われ、メインドライバーコードは次のようになります。
static int __init wrn_wdt_init(void) { int ret; // , wrnd filp_port = filp_open(serial_port, O_RDWR | O_NOCTTY | O_NDELAY, 0); if (IS_ERR(filp_port)) ... // else { ret = misc_register(&wrn_wdt_miscdev); // miscdevice if (ret == 0) wdt_timeout(); // } return ret; } static void wdt_enable(void) { ... // spin_lock(&wrn_wdt_lock); // ( ) // watchdog keep-alive interval, // WRN , getnstimeofday(&t); time_delta = (t.tv_sec - wdt_keep_alive_sent.tv_sec) * 1000; // sec to ms time_delta += (t.tv_nsec - wdt_keep_alive_sent.tv_nsec) / 1000000; // ns to ms if (time_delta >= WDT_MIN_KEEP_ALIVE_INTERVAL) { // cmd_keep_alive fs = get_fs(); // FS set_fs(get_ds()); // KERNEL_DS ( ) ret = vfs_write(filp_port, cmd_keep_alive, strlen(cmd_keep_alive), &pos); set_fs(fs); // FS if (ret != strlen(cmd_keep_alive)) ... // getnstimeofday(&wdt_keep_alive_sent); } spin_unlock(&wrn_wdt_lock); // ( ) } static long wrn_wdt_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { ... // switch (cmd) { case WDIOC_KEEPALIVE: wdt_enable(); ret = 0; break; case WDIOC_SETTIMEOUT: ret = get_user(t, (int *)arg); // ... // timeout = t; wdt_timeout(); wdt_enable(); /* */ case WDIOC_GETTIMEOUT: ret = put_user(timeout, (int *)arg); // break; ... // } return ret; }
上記のコードスニペット以外では、MAGICCLOSEサポートが残ります。 これは、デバイスファイルを閉じるときにドライバーがウォッチドッグを無効にするために必要です。 したがって、システムによってファイルが閉じられるwatchdog
デーモンの異常なシャットダウンを認識することが重要です。 この場合、MAGICCLOSEメカニズムは再起動を保証するのに役立ちます。 そのサポートは、特殊文字(通常はV)を受信した直後にデバイスファイルが閉じられた場合にのみ、ウォッチドッグタイマーの非アクティブ化を提供します。
ドライバーは、 make driver
コマンドでMakefileを使用して構築され、その一部は次のことを行います。
TARGET_WDT = wrn_wdt ifneq ($(KERNELRELEASE),) # , obj-m := $(TARGET_WDT).o else # make KERNEL := $(shell uname -r) # driver: $(MAKE) -C /lib/modules/$(KERNEL)/build M=$(PWD) # install: driver $(MAKE) -C /lib/modules/$(KERNEL)/build M=$(PWD) modules_install endif
システムの起動時にインストール後にモジュールをロードするには、次の行を/etc/conf.d/modules
ファイルに追加する必要があります。
modules="wrn_wdt"
手動モジュールは、コマンドmodprobe wrn_wdt
またはinsmod ./wrn_wdt.ko
modprobe wrn_wdt
insmod ./wrn_wdt.ko
ます。 アップロード: modprobe -r wrn_wdt
またはrmmod wrn_wdt
; モジュールがロードされていることを確認してください: lsmod | grep wrn_wdt
lsmod | grep wrn_wdt
。
watchdog
デーモンのデバイスファイルとしてwdt.fifoチャネルを使用する場合、依存関係が正しく設定され、 wrnd
デーモンがより早く起動し、後でwatchdog
を停止することを確認することが重要です。 そうしないと、それに応じて、FIFOチャネルがまだ作成されていないか、タイマーが非アクティブにならないため、不要な再起動が発生する可能性があります。
鬼
wrnd
デーモンの目的は、シリアルポートからのバイナリデータのストリームをソートし、それらをサービスに便利な形式に変換することです。
シリアルポート設定はデフォルトでテキスト端末向けに最適化されているため、それらをラインに入れ、デバイスと動作モードを調整する必要があります( 完全なコード: serialport.c ):
// termios / struct termios2 ttyopts; memset(&ttyopts, 0, sizeof ttyopts); // if (ioctl(fd, TCGETS2, &ttyopts) != 0) ... // // ttyopts.c_cflag &= ~CBAUD; ttyopts.c_cflag |= BOTHER; ttyopts.c_ispeed = speed; // unsigned int, B9600 ttyopts.c_ospeed = speed; ... // // ttyopts.c_cc[VMIN] = vmin; // ( ) ttyopts.c_cc[VTIME] = vtime; // if (ioctl(fd, TCSETS2, &ttyopts) != 0) ... //
ここで最適なVMINおよびVTIME値を選択することが重要です。 それらがゼロである場合、ポートはシステムリソースを不必要に消費する遅延なしでポーリングされます。 データがない場合、ゼロ以外のVMIN値は、フローを無制限にブロックできます。
この場合、データはメインプログラムストリームで1バイト読み込まれます。 長時間ブロックするのは良くないので、VMINは常にゼロであり、VTIMEはパラメーターで変更できます。デフォルトでは5(最大遅延0.5秒)です。
受信したバイトは、固定サイズのバッファに到着します。 予想されるバイト数を受け取ると、バッファーは対応するデータ構造(構造)に変換されます。 この方法は優れていますが、覚えておく必要がある機能があります。 コンパイラは、フィールド間にスペースを追加し、通常は単語の境界に合わせてフィールドを配置することにより、データ構造を最適化します。 データは異なるプラットフォーム間で送信されるため、ワードサイズとバイト順(エンディアン)が異なるため、不整合が発生する可能性があります。
アライメントを解消するには、データ構造をパックとして宣言する必要があります。 AtmelStudioのプロジェクトはデフォルトで-fpack-struct
スイッチを使用して-fpack-struct
れるため、このキーのキャンセルに関する警告がないことを確認してください。 データアクセスの速度のためにメモリを節約するタスクがないため、このキーを使用してwrndプロジェクトをアセンブルすることはお勧めできません。 必要に応じて、対応する属性を指定するだけで十分です。次に例を示します。
struct payload_header { ... // } __attribute__ ((__packed__));
プロセスは、 daemon
機能によってバックグラウンドで開始されdaemon
。
if (arguments->daemonize && daemon(0, 0) < 0) ... //
その結果、新しいPIDを持つプロセス(フォーク)のコピーが作成され、それがさらに機能し続け、現在のプロセスが終了します。 関数の引数は、新しいプロセスでは、ルートディレクトリを機能するように設定し、標準入力、出力、およびエラーストリームを/dev/null
リダイレクトする必要があることを示してい/dev/null
。
リーダーが接続されていないFIFOに書き込もうとしたときにデーモンがシャットダウンしないようにするには、SIGPIPEシグナルを無視する必要があります。
signal(SIGPIPE, SIG_IGN);
コードの残りの目的は、次のようにリストできます。
- 転送されたパラメーターの分析。
- 必要なファイルを開始、作成、開くための条件を確認します。
- ログファイルを操作し、
logrotate
からのSIGHUPシグナルを使用して再検出しlogrotate
。 - 正しいシャットダウンのためにSIGINT、SIGTERMを処理するシグナル。
- デバイスとの同期とその初期化。
- 着信データの処理と、対応するFIFOチャネルでの結果の記録。
チャンネルrng.fifo
データは、約636バイト/秒の速度でバイナリ形式でチャネルに直接送信されます。 エントロピーを/dev/random
に追加するには、 rngd
デーモンが使用されます。 必要なソースのみを使用するには、パラメータ " --no-tpm=1 --no-drng=1 --rng-device /run/wrnd/rng.fifo
"を彼に渡す必要があります。
ここで、エントロピーの欠如の問題を解決するために、真の乱数ジェネレーターを使用する必要がないことに注意する価値があります。 パラメーター " --rng-device /dev/urandom
"を指定してrngd
を実行するだけで十分です。 /dev/urandom
使用されるアルゴリズムは十分であり、そうしないことを推奨することは通常完全に正当化されていません。 比較テストの結果は、出版物の終わり近くの最初の部分で見ることができます。
真に乱数を生成することを選択したのは簡単です-同様のデバイスを組み立てたかったのですが、それに対する議論は見つかりませんでした。
チャンネルnrf.fifo
センサーからのデータは処理され、SQLクエリの形式でチャネルに送信され、テーブルにレコードが挿入されます。 wrnsensors.shの例は、SQLite3データベースでの作業を示していますが、INSERTクエリは普遍的であり、どのSQLデータベースでも機能するはずです。
チャンネルcmd.fifo
チャネルは、以下でwrnctrl
管理wrnctrl
によって使用されます。
make daemon
コマンドでMakefileを使用してwrnd
をwrnd
できmake daemon
。 デバッグバージョンをビルドするためのデバッグターゲットが提供されます。
管理ユーティリティ
ユーティリティwrnctrl
はcmd.fifoチャネルからデバイスからデータを受け取るため、 wrnd
デーモンをwrnctrl
する必要がwrnd
ます。
リーダーはすべてのソースからデータを受信しますが、FIFOを同時に開くと、予測可能な結果が得られますが、記録のみが可能です。 複数のリーダーが1つのFIFOを開く場合、どのリーダーがデータを受信するかを予測することは不可能です。 この動作は定義上正しいですが、望ましくないため、cmd.fifoへのアクセスを同期する必要があります。
Linuxではflock
を使用してファイルがビジーであることを宣言できるため、コード内でのみ同時にファイルを処理することを除外できます。 このメカニズムは名前付きパイプでは機能しないため、 /tmp
内の追加ファイルを使用する必要があります( 完全なコード: wrnctrl )。
function device_cmd() { cmd=$1 # # (subshell), 4 ( # 4, if ! flock -w ${FIFO_MAX_WAIT} 4; then return; fi # # , if [ -O ${LOCK_NAME} ]; then chmod 666 ${LOCK_NAME}; fi # cmd.fifo 3, exec 3< <(timeout ${FIFO_MAX_LOCK} cat ${WRND_CMDFIFO}) sleep 0.2 # if [ -r /dev/fd/3 ]; then echo "$cmd" >${WRND_DEVICE} # # , FIFO timeout while read log_line <&3; do echo "$log_line" # done exec 3>&- # 3 fi ) 4>${LOCK_NAME} # 4 subshell, }
デバイスファームウェアの場合、 wrnctrl flash [firmware].hex
ます。 開始する前に、 watchdog
およびwrnd
停止する必要がありwrnd
。 このコマンドはavrdude
ユーティリティを使用します。たとえば、パッケージマネージャーからインストールできます。
emerge -av dev-embedded/avrdude
上記のファイルに加えて、プロジェクトインストールパッケージには次のものも含まれています。
- デーモン設定を含むファイル。
- デーモンを制御するOpenRCスクリプト。
- logrotateの設定ファイル。
- cronを介して実行されるスクリプトは、デバイスのすべてのサブシステムのステータスログをログに書き込みます。
プロジェクトの組み立てとインストールは、 make install
コマンドで行います。 インストールは、スーパーユーザー特権(root)で実行する必要があります。 ファイルはシステムユーティリティinstall
によってコピーされます。これにより、ターゲットファイルの権限と所有者をすぐに設定できます。
おわりに
このデバイス用のUSBインターフェースが確実に望ましいでしょう。 しかし、サーバーに使用可能なUSBポートがないため、この形式でプロジェクトが表示されました。 それにもかかわらず、それはかなりシンプルで安定した作業デバイスであることが判明したので、再生に強くお勧めします。