Linuxネットワークプログラミング(シングルプロセス、シングルスレッドで多重化)
目次
サーバ環境
| 製品名 | OpenBlockS 600/R |
| CPU | 600MHz(AMCC PowerPC 405EX) |
| メモリ | 1GB(DDR2 SDRAM) |
| ストレージ | 8GB(Compact Flash) |
前回までのコードの問題点
Linuxネットワークプログラミング(初級編 その1)で説明したサーバでのシステムコールの使用順序は以下の通りです。
socket → bind → listen → accept → recv/send → shutdown → close
この流れでプログラムを組むと以下のようになります。
for (;;) {
accept ... ;
for (;;) {
recv ... ;
send ... ;
}
}まずacceptで処理がブロックされます。
クライアントからの接続があると処理が先に進み、次にrecv/sendにてループ処理を行います。
recv/sendが終了するまでforループが続くため、新規クライアントは接続できません。
ディスクリプタをノンブロッキングにしてrecv/sendする方法もありますが、forループが無限ループとなりCPUを激しく消費することになります。
あるクライアントと送受信を行う間も別のクライアントから接続できるようにするには、IOの多重化が必要になります。
I/Oの多重化の各方式の比較
多重化には以下の3つの方式があります。(Linuxネットワークプログラミングバイブルから引用)
方式1:シングルプロセスシングルスレッド(select()、poll()、EPOLLのいずれかを使用)
| メリット | リソース消費が少ない 処理全体が1つのレベルで管理できる 送受信のデータ共有が容易 |
| デメリット | 時間のかかる処理がある場合に並列性を確保しにくい プログラムが複雑になることがある プログラム異常時に全体が影響を受ける 1プロセスのリソース制限を受ける |
■方式2:マルチプロセス(fork()を使用)
| メリット | 時間のかかる処理がある場合に並列性を確保しやすい プログラム異常時に他の処理に影響が出にくい プログラムがシンプルで分かりやすい 各処理の終了時にリソースが確実に解放される 1プロセスのリソース制限を受けない |
| デメリット | リソースを多く必要とする 子プロセス生成時の処理が重たい 送受信の連携が面倒 UNIX系OS以外では利用が難しい 終了処理の考慮が面倒 |
■方式3:マルチスレッド(pthreadを使用)
| メリット | マルチプロセスに比較してリソースの処理が少ない 時間のかかる処理がある場合に並列性を確保しやすい 他の処理系でも使える場合が多い プログラムがシンプルで分かりやすい |
| デメリット | 送受信の連携がやや面倒 デッドロックなどの問題を引き起こしやすい 終了処理の考慮が面倒 プログラム異常時に全体が影響を受ける 各処理の終了時のリソース解放に注意が必要 1プロセスのリソース制限を受ける |
本ページでは、上記のうち、方式1:シングルプロセスシングルスレッドについて紹介します。
方式2についてはLinuxネットワークプログラミング(マルチプロセスで多重化) 、 方式3についてはLinuxネットワークプログラミング(マルチスレッドで多重化)を見て下さい。
方式1にはselect()、poll()、EPOLL()がありますが、一つずつ取り上げると長くなるため、select()のみ取り上げます。
無限ループの中で上記システムコールを呼び出し新規クライアントからの接続、または既存クライアントからの受信を待ち受けるという基本的な使い方は三つとも同じです。
簡単にselect()との違いを挙げておきますと、poll()は監視中のディスクリプタの変化を指定できる分、select()よりも細かいコントロールができる点で優れています。
EPOLL()も基本的にpoll()と同じ優位性がありますが、数千コネクションが接続された際にselect(),poll()で問題になるパフォーマンスの問題を改善する工夫がされている点で3方式のなかで最も優れた方式です。
ただしEPOLL()はLinuxでしか使用できないため、移植性には難有りです。
poll()とEPLL()についてはLinuxネットワークプログラミングバイブルに詳細な説明がありますので、気になる方はぜひ読んでみて下さい。
シングルプロセス、シングルスレッド(select())について
select()はaccept()やrecv()同様、処理をブロックしますが、複数のディスクリプタを同時に見張ることができるため、クライアントからの新規接続や接続中クライアントからの受信など、記述子の変化があった場合にどのような変化かに関わらず処理を先に進められます。
また、各ディスクリプタに変化があるまでブロックするため、CPUパワーを無駄に消費することもありません。
select()
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);| 引数readfds | 読みが可能になったら教えて欲しい記述子群。接続待ちソケットもこれに含める。 |
| 引数writefds | 書きが可能になったら教えて欲しい記述子群。 |
| 引数exceptfds | エラーが起きたら教えて欲しい記述子群。 |
| 引数nfds | 見張るべき記述子のうち最大の値のものに1を足した値を指定する。 readfds=3,5 / writefds=4,6 の場合、6 + 1 = 7になる。 |
| 引数timeval | ここで指定した時間が経過するとブロックが解除される。NULLを指定すると監視対象がレディになるまで待ち続ける。 |
返り値が-1の場合はエラー。0の場合はタイムアウト。1以上の場合はレディ状態の記述子が1つ以上ある場合です。
サーバ用途であればnfdsとreadfdsのみ指定し、残りはNULLで事足りることが多いです。
サーバでのシステムコールの使用順序は以下の通りです。
socket→bind→listen→select→accept
(socket→bind→listen→select)→recv→shutdown→close
selectで変化を検知した記述子が接続待ちソケット(通常は3)であればaccept、それ以外であればすでに接続済みクライアントからのデータ受信であるため、処理を分岐します。
selectと共に使用することになるシステムコールも合わせて紹介します。
FD_ZERO, FD_SET, FD_ISSET
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ISSET(int fd, fd_set *fdset);| FD_ZERO | fdsetに指定されたfd_set型のデータの全ビットを0にする。 |
| FD_SET | fdをfdsetに追加する。 |
| FD_ISSET | fdsetにfdが含まれていれば非0を返し、含まれていなければ0を返す。 |
上記システムコールの典型的な使い方は以下の通りです。
- FD_ZEROでreadfdsを0にする。
- FD_SETで監視する記述子をセットする。
- select()を呼び出す。
- FD_ISSETでどの記述子が変化したのかを調べて処理を分岐させる。
ソース例を以下に載せるので、これまでの説明と見比べてみてください。
socket→bind→listenについては割愛しますが、変数socが接続待ちソケットと仮定します。
ソースを分かりやすくするため、接続できるクライアント数を3つまでと仮定します。
int child[3];
struct sockaddr_storage from;
int acc, width, i, count, pos, ret;
socklen_t len;
fd_set mask;
/* child[0]、child[1]、child[2]は接続中クライアントがいれば記述子が入る。いなければ-1。 */
for (i = 0; i < 3; i++) {
child[i] = -1;
}
for (;;) {
/* select()でmaskが書き換えられるためループするごとに初期化 */
FD_ZERO(&mask);
/* 接続待ちソケットを監視対象に追加 */
FD_SET(soc, &mask);
/* select()の第一引数に使用。この時点では接続待ちソケット + 1 */
width = soc + 1;
/* 監視対象の追加とselect()の第一引数の更新 */
for (i = 0; i < 3; i++) {
if (child[i] != -1) {
/* 接続中のクライアントがいれば記述子を監視対象に追加 */
FD_SET(child[i], &mask);
/* select()の第一引数を更新 */
if (child[i] + 1 > width) {
width = child[i] + 1;
}
}
}
switch (select(width, (fd_set *) &mask, NULL, NULL, NULL)) {
/* エラーの場合 */
case -1:
perror("select");
break;
/* タイムアウトの場合 */
case 0:
break;
/* 新規接続または接続済ソケットからの受信 */
default:
/* 新規接続の場合 */
if (FD_ISSET(soc, &mask)) {
/* accept処理 */
len = (socklen_t) sizeof(from);
if ((acc = accept(soc, (struct sockaddr *)&from, &len)) == -1) {
if(errno != EINTR) {
perror("accept");
}
} else {
/* すでに3接続がある場合は受け入れ拒否 */
if (child[0] && child[1] && child[2]) {
(void) fprintf(stderr, "child is full : cannot accept\n");
(void) close(acc);
/* まだ接続可能な場合 */
} else {
if (child[0] == -1) {
child[0] = acc;
} else if(child[1] == -1) {
child[1] = acc;
} else {
child[2] = acc;
}
}
}
}
/* 接続済ソケットからのデータ受信かのチェック */
for (i = 0; i < 3; i++) {
if (child[i] != -1) {
/* 接続済ソケット(child[i])からの受信の場合 */
if (FD_ISSET(child[i], &mask)) {
/* recv処理などをここで実施。*/
recv ...
/* 切断された場合は以下の処理を実施 */
(void) close(child[i]);
child[i] = -1;
}
}
}
/* case文のためのbreak */
break;
}
}次回以降でマルチプロセス、マルチスレッドを紹介します。