「ping」によるネットワーク通信機能を実装してみよう:H8マイコンボードで動作する組み込みOSを自作してみよう!(6)(2/3 ページ)
今回は、いよいよネットワーク通信機能を実装していく。ネットワーク通信のような複雑なプログラムを実装すると、“マルチタスク構成にすることの有用性”がこれまで以上に実感できるはずだ。
5.タスク設計
前述したように、ICMPの通信はEthernet、IP、ICMPの階層構造になっています。またMACアドレスの解決のためにはARPが必要です。
これら全てを1つのプログラムで実装することも可能です。しかし、今回はこれらを「タスク」として独立させて実装してみます。
このような機能は役割ごとにタスクに分割し、マルチタスクとして実装した方がプログラム全体の見通しがよくなります。また、マルチタスクにすることで、処理内容の独立性を高めることができます。
例えば、前回説明したように、これらを1つのタスクとして実装してしまうと、ある時間のかかる処理を始めた途端、別の処理が一切実行できなくなってしまうという問題が起こり得ます。それぞれの処理をタスクに分割することで、このような“他の処理に別の処理が引きずられる”というバグを未然に防止することができるわけです。
今回は、以下のタスクを作成します。
- フレームの送受信処理(netdrvモジュール)
- Ethernet関連の処理(ethernetタスク)
- ARP関連の処理(arpタスク)
- IP関連の処理(ipタスク)
- ICMP関連の処理(icmpタスク)
図3は、これらのタスク構成です。
受信したネットワークフレームは、netdrvモジュールからethernetタスクに渡されます(受信を行うのはnetdrvモジュールが持つ割り込みハンドラであるため、netdrvは「タスク」でなく「モジュール」と表記しています)。ethernetタスクは、受信フレームを解析し、ARPならばarpタスクに、IPならばipタスクに転送します。これらのタスク間のデータのやりとりには、KOZOSのメッセージ通信機能を利用します。
受信フレームがIPであった場合、ipタスクによって解析されます。そして、これがICMPの場合には、さらにicmpタスクに転送されます。icmpタスクはICMPヘッダを見て、ICMP Echoであった場合には“応答”として、ICMP Echo Replyを送信する処理を開始します。
送信処理の流れは受信と逆です。icmpタスクはipタスクに送信要求を渡します。ipタスクはIPヘッダを付加してethernetタスクに渡します。ethernetタスクは送信パケットにEthernetヘッダを付加して送信しようとするのですが、その前に、ここでMACアドレス解決の処理が入ります。
ipタスクはMACアドレスの管理をしていないため、送信先のMACアドレスを知りません。このため宛先MACアドレスが“不明”のまま、パケットをethernetタスクに渡します。ethernetタスクは宛先MACアドレスが不明の送信パケットを渡されると、そのままarpタスクに転送します。arpタスクは宛先のIPアドレスを参照して、“MACアドレス解決済み”ならば、そのMACアドレスを補填(ほてん)して、ethernetタスクにパケットを戻します。ethernetタスクは、今度は宛先MACアドレスが補填されているため、Ethernetヘッダを付加してEthernetフレームとし、netdrvモジュールに渡して送信してもらいます。
MACアドレスが“未解決”の場合はもう少し複雑です。arpタスクは送信パケットをいったんキューイングし、代わりにARP Requestパケットをethernetタスクに渡して送信してもらいます。ARP Requestの応答として相手ホストから返ってきたARP Replyは、ethernetタスク経由でarpタスクに渡され、MACアドレスが解決されます。これによって、arpタスクは先程キューイングしていたパケットの宛先MACアドレスを補填し、パケットをethernetタスクに渡して送信してもらいます。
図4は、相手ホストからKOZOSがICMP Echoを受信して、ICMP Echo Replyを応答する場合のシーケンス図です。この場合は、最初のARPのやりとりにより、MACアドレス解決がなされているため、ICMP Echo Replyを送信する際にはARPによるMACアドレス解決は行われていません。
図5は、KOZOS側からICMP Echoを送信して、相手からICMP Echo Replyが応答される場合のシーケンス図です。この場合は、KOZOS側からARP Requestを発行し、ARP Replyを受け取るのを待ってから実際にパケット送信されます。つまり、arpタスクによるパケットの送信保留が行われているわけです。
6.実装の詳細
ここから実装の詳細について説明していきましょう。まずは、受信の流れを追いながら見ていきます。
パケットの受信は、netdrvモジュールによって行われます。netdrvは、デバイスドライバのサービス関数を呼び出すことで、フレームの送受信を行うためのモジュールです。
ここでnetdrvをタスクでなく“モジュール”と呼んでいるのは、前述したように、フレーム受信は割り込みハンドラによって行われるためです。つまり、受信処理はnetdrvタスクのコンテキストで行われるわけではありません。よって、ここでは「netdrvタスク+受信割り込みハンドラ」を「netdrvモジュール」として呼んでいます。
割り込みハンドラは、リスト2のようになっています。フレームを受信すると“MSGBOX_ID_ETHPROC”というメッセージボックスに対して、“ETHERNET_CMD_RECV”というコマンドをメッセージ通信で送信します。
/* 割り込みハンドラ */ static void netdrv_intr(void) { …… if (rtl8019_checkintr(0)) { pkt->cmd = ETHERNET_CMD_RECV; …… pkt->size = rtl8019_recv(0, pkt->top); if (pkt->size > 0) kx_send(MSGBOX_ID_ETHPROC, 0, (char *)pkt); …… }
このメッセージボックスは、ethernetタスクによって受信されます。リスト3は、ethernetタスクの受信部分です。
static int ethernet_recv(struct netbuf *pkt) { …… switch (ethhdr->type) { case ETHERNET_TYPE_ARP: pkt->cmd = ARP_CMD_RECV; kz_send(MSGBOX_ID_ARPPROC, 0, (char *)pkt); break; case ETHERNET_TYPE_IP: pkt->cmd = IP_CMD_RECV; kz_send(MSGBOX_ID_IPPROC, 0, (char *)pkt); break; …… } …… static int ethernet_proc(struct netbuf *buf) { …… switch (buf->cmd) { …… case ETHERNET_CMD_RECV: ret = ethernet_recv(buf); break; case ETHERNET_CMD_SEND: ret = ethernet_send(buf); break; …… } int ethernet_main(int argc, char *argv[]) { …… while (1) { kz_recv(MSGBOX_ID_ETHPROC, NULL, (char **)&buf); ret = ethernet_proc(buf); …… }
ethernetタスクのメイン関数の「ethernet_main()」では、「kz_recv()」によってメッセージの受信待ちをしています。受信したメッセージは「ethernet_proc()」に渡され、フレーム受信ならば「ethernet_recv()」が呼ばれます。ethernet_recv()では、ARPパケットならばarpタスクに、IPパケットならばipタスクに受信したパケットを転送します。
ipタスクの受信部分は、リスト4のようになっています。メイン関数の「ip_main()」では、やはり、kz_recv()によってメッセージ受信待ちをしており、受信すると「ip_proc()」が呼ばれます。ここでパケット受信ならば「ip_recv()」が呼ばれ、登録されているプロトコルに応じて対応するメッセージボックスに転送されます。ICMPの場合は、icmpタスクのメッセージボックスに転送されます。
static int ip_recv(struct netbuf *pkt) { …… pkt->cmd = protoinfo[iphdr->protocol].cmd; …… kz_send(protoinfo[iphdr->protocol].id, 0, (char *)pkt); …… } …… static int ip_proc(struct netbuf *buf) { …… switch (buf->cmd) { …… case IP_CMD_RECV: ret = ip_recv(buf); break; case IP_CMD_SEND: ret = ip_send(buf); break; …… } int ip_main(int argc, char *argv[]) { …… while (1) { kz_recv(MSGBOX_ID_IPPROC, NULL, (char **)&buf); ret = ip_proc(buf); …… }
icmpタスクの受信部分は、リスト5のようになっています。メイン関数の「icmp_main()」では、kz_recv()によってメッセージ受信待ちをしており、受信すると「icmp_proc()」が呼ばれます。ICMPのパケット受信の場合には「icmp_recv()」が呼ばれ、ICMP Echoならば、icmp_recv()内で「icmp_sendpkt()」が呼ばれて応答パケットを作成し、ipタスクに転送することで送信依頼をします。
static int icmp_sendpkt(uint32 ipaddr, uint8 type, uint8 code, uint16 id, uint16 sequence_number, int datasize, char *data) { …… pkt->cmd = IP_CMD_SEND; kz_send(MSGBOX_ID_IPPROC, 0, (char *)pkt); …… } static int icmp_recv(struct netbuf *pkt) { …… switch (icmphdr->type) { …… case ICMP_TYPE_REQUEST: icmp_sendpkt(pkt->option.common.ipaddr.addr, ICMP_TYPE_REPLY, icmphdr->code, icmphdr->param.id, icmphdr->param.sequence_number, pkt->size - sizeof(struct icmp_header), (char *)icmphdr + sizeof(*icmphdr)); …… } …… static int icmp_proc(struct netbuf *buf) { …… switch (buf->cmd) { case ICMP_CMD_RECV: ret = icmp_recv(buf); break; case ICMP_CMD_SEND: ret = icmp_send(buf); break; …… } int icmp_main(int argc, char *argv[]) { …… while (1) { kz_recv(MSGBOX_ID_ICMPPROC, NULL, (char **)&buf); ret = icmp_proc(buf); …… }
Copyright © ITmedia, Inc. All Rights Reserved.