Cygwin DLL をリンクしたバイナリが実行されると、Cygwin DLL はアプリケーションのテキストセグメントをロードします。 Cygwin DLL の元で動作する全てのプロセスがアクセスすることになる UNIX カーネルをエミュレートするために、Cygwin DLL はまず、 DLL の個々のインスタンスを使用するプロセスがアクセス可能な共有メモリ領域を作成します。 特に、オープンされたファイルのディスクリプタの保持と、 fork 及び exec の補助のために使われます。 全てのプロセスは、追加された共有メモリ領域中にプロセス ID、ユーザ ID、 シグナルマスク、そして他のプロセス独自の情報を格納するための per_process 構造をも保持します。
Cygwin DLL は Win32 API を使用して実装されており、 全ての Win32 ホスト上で動作します。 プロセスは標準 Win32 サブシステムの元で動作するので、 これらのプロセスは Win32 API 呼び出しと同様に Cygwin が提供する UNIX 互換の呼び出しを扱うことが出来ます。 これはプログラマに対し、Win32 API を使用して作られたプログラム構造のデザインに完全なる柔軟性を与えます。 例えば、彼らは Cygwin を使用することによって UNIX バックエンド上に、 Win32 API 呼び出しを使用した Win32 特有の GUI を書くことが出来るのです。
開発工程の初期に、我々はそれが不可能な場合、又は Win32 プラットフォーム上でのツールの使い勝手を極めて悪くしてしまう場合、 POSIX.1 のような既存の UNIX 標準の存在に対して厳格に忠実である必要はないのではないかという、重要な設計の議論を行いました。 多くの場合は、ある環境変数によってデフォルトの振る舞いを上書きしたり、 標準への準拠を強制出来るようにしました。
Cygwin を実装する上では、Windows 95 と Windows 98 はその差を安全に無視出来る程度に、お互い十分に似ています。 Windows NT は極端に異なったオペレーティングシステムです。 そのため、適切に動作出来るよう、 Cygwin DLL はロード時にどちらのオペレーティングシステム上で使用されているのかを常にチェックします。
幾つかの場合、Win32 API は歴史的な理由によってのみ差異が存在します。 この場合、同様の基本的な機能は Windows 9x と NT で利用可能ですが、 その機能を得るために使われる方法は異なっています。 些細な例を挙げましょう。我々の uname の実装においては、 ライブラリは Windows 9x の元ではプロセッサのタイプを得るために sysinfo.dwProcessorType 構造体メンバを調査します。 このフィールドは NT ではサポートされておらず、 sysinfo.wProcessorLevel と呼ばれるオペレーティングシステム独自の構造体メンバに格納されています。
NT と 9x の間におけるその他の違いは、その生い立ちによるものです。 最も良い例は、セキュリティモデルは NT のみが備えているということでしょう。
Windows NT はアクセス制御リスト(ACL)をベースにした、 洗練されたセキュリティモデルを備えています。 デフォルトでは、Cygwin は Win32 ファイルの所有権とパーミッションを、 より標準的な古い UNIX のモデルに対応付けます。 Cygwin バージョン 1.1 は、新しいバージョンの Solaris が使用しているシステムコールによって ACL のサポートを取り入れています。 「ntsec」機能と共に使用されるこの機能については別章で説明されています。 chmod 呼び出しは、Win32 の同等な機能から UNIX スタイルのパーミッションを割り当てます。 多くのプログラムは /etc/passwd と /etc/group ファイルの存在を仮定しているため、 オペレーティングシステムによって提供されるユーザとグループの情報からこれらのファイルを構築するユーティリティが提供されています。
Windows NT 環境下では、administrator はファイルの chown が出来ます。 Cygwin バージョン 1.1.2 以来、setuid のコンセプト又は API 呼び出しのメカニズムはありません。 バージョン 1.1.3 では、Cygwin は Windows NT/2000 環境下での実 UID 及び実効 UID の設定メカニズムを取り入れました。 これについては ntsec の章で説明されています。
Windows 9x 環境下では、この状況は相当に異なるものとなります。 セキュリティモデルが準備されていないため、Cygwin は ファイルがデフォルトのユーザ ID とグループ ID によって所有されているように見せかけることで、 ファイルの所有権を偽装します。 NT 環境下では、ファイルパーミッションはファイルの読み / 書き / 実行状態を調べることによって決定することが出来ます。 Windows 9x 環境下での chown 呼び出しは、 実装されていないというエラーを返すのではなく、 何らの処理をも行わずに直ちに成功を返します。 ファイルの所有権という概念が全く存在しない場合、 全てのユーザはファイルを一緒に所有していることになるので、 これは本質的に適切です。
Cygwin プロセスに関する情報を蓄えた共有メモリを使用する「カーネル」 の密接な関係に関する議論は重要です。 それらの領域にはまだ何らの保護もなされていないため、 Cygwin プロセスが予期しない動作を起こすように、 悪意のあるユーザがそれを書き換えることが原則的に可能です。 (オペレーティングシステムによるセキュリティが欠けているがための) Windows 9x 環境下での新しい問題ではなく、 Windows NT 環境下でもセキュリティホールとなり得るものです。 なぜなら、あるユーザによって実行された Cygwin プログラムに対し、 他のユーザが通常の Windows NT のプログラムでは不可能な方法によって共有メモリ情報を変更することにより、 そのプログラムに影響を与えることが出来るからです。 このため、高いセキュリティが要求されるアプリケーションにおいて Cygwin を使用することは適切ではありません。 実際のところ、これはライブラリの大部分の用途に対しては大きな問題とはなりませんが。
Cygwin は、 ディレクトリの区切りとしてスラッシュあるいはバックスラッシュを使用する、 Win32 と POSIX の両方のパス形式をサポートしています。 DLL が受け取ったパスは必要に応じて Win32 パスから POSIX パスへと変換されます。結果として、ライブラリはファイルシステムが POSIX 準拠のものであると考え、Win32 API 関数を呼び出すときは常にパスを Win32 パスへと変換します。UNC パス名(二つのスラッシュから始まる) もサポートされています。
Windows ファイルシステム空間にあるこの POSIX ビューのレイアウトは、Windows レジストリ中に蓄えられます。 デフォルトでは、スラッシュ(「/」)ディレクトリはシステムパーティションを示しますが、 これは Cygwin の mount ユーティリティによって簡単に変更出来ます。 スラッシュパーティションの選択に加えて、このユーティリティは任意の Win32 パスを POSIX ファイルシステム空間へとマウントします。多くの人々は、各ドライブ文字をスラッシュパーティションへと割り当てる(例えば、C:\ を /c に、D:\ を /d に、など)ためにこのユーティリティを使用しています。
Cygwin DLL は、 Win32 のパス又はパスのリストから POSIX パスへの変換あるいはその逆を行う外部プログラムによって使用される、 様々な Cygwin 独自の関数をエクスポートしています。 シェルスクリプトや Makefile はこれらの関数を直接呼び出すことは出来ません。 代わりに、それらの中では Cygwin に付属する cygpath ユーティリティを実行することによって、同様のパス変換が行えます。
Win32 のファイルシステムは大文字小文字の区別を保存しますが、 大文字小文字の区別は行いません。 Cygwin は現在のところ、大文字小文字の区別はサポートしていません。 なぜなら、実際には大文字小文字の区別に頼っている UNIX プログラムはごく僅かだからです。 大文字小文字の区別のサポートによってファイル名を押し延ばすことが出来ますが、 これはライブラリに対して不要なオーバーヘッドを発生させ、 それらのファイルに対する非 Cygwin アプリケーションのアクセスをより難しいものにしてしまうでしょう。
シンボリックリンクは、リンク位置を示すパスに続くマジッククッキーを含んだファイルによってエミュレートされます。 それらのファイルにはシステム属性が付与されます。 システム属性を持つファイルだけが、 そのファイルがシンボリックリンクであるか、 そうでないかの判定のために用いられるからです。 ハードリンクは Windows NT 環境上の NTFS ファイルシステムでは完全にサポートされています。FAT ファイルシステムでは、 link 呼び出しは単純にファイルのコピーを行いますが、 この方針は多くの場合問題なく動きます。
ファイルに対する i ノード番号は、完全な Win32 パスをハッシュして計算されます。stat 呼び出しによって生成される i ノード番号は、dirent 構造体中の d_ino に格納される値と常に合致します。 この方法で生成される番号はユニークである保証はない、ということに注意して下さい。 しかし重複した i ノード番号が生成される可能性は低いため、 このことが重大な問題となったことはありません。
chroot はリリース 1.1.3 からサポートされました。chroot は本来、Windows ではサポートされていないという点に注意して下さい。これは幾つかの制限が存在することを意味します。第一に、chroot 呼び出しは特権呼び出しではなく、個々のユーザが呼び出すことが可能です。 第二に、chroot 環境はネイティブの Windows プロセスに対して安全ではありません。 chroot 環境をサポートしたいのであれば、例えば、制限付きのアクセスを備えた匿名 FTP をサポートしたいのであれば、ネイティブの Cygwin アプリケーションだけが chroot 環境の内部にアクセス出来るように注意する必要があります。 アプリケーションがファイルシステムに対するアクセスに Cygwin の POSIX API だけを使用するのであれば、そのアプリケーションは意図した通りに制限されます。 これは POSIX パスだけでなく、Win32 パス(ドライブ文字とバックスラッシュを含む)と CIFS パス(//server/share 又は \\server\share)もまた含まれます。
テキストエディタのような他の Win32 プログラムとの相互運用性は、 開発ツールの移植を成功させるためには重要でした。 かつての DOS をホストとした toolchain からアップグレードした Red Hat の顧客の多くは、 Win32 をホストとした新しい toolchain でも、 従来の開発ソースが利用できることを期待していたからです。
不幸なことに、テキストファイルの各行の終端文字は UNIX と Win32 では異なっています。 その結果として、テキストモードで読み込むときは、Cygwin は速やかに復帰改行を一つの改行へと変換しなければなりません。
この対応策は、テキストとバイナリのモードは等しいと明言している POSIX 標準に対する違反の代償として、互換性に対する要求に注意を向けたものです。 その結果、テキストファイルに対する lseek を行うプロセスは、 ファイル中の正確な位置を示す値として読み込んだバイト数を信頼することは出来ない、ということになります。 このため、CYGWIN 環境変数によってこの動作を変更することが出来ます。
我々は libc と数学関数の全てをスクラッチから書くよりも、 Red Hat が所有している既存の ANCI C ライブラリ「newlib」を Cygwin DLL の一部分として含めることを選びました。 Newlib は BSD から派生した ANCI C ライブラリであり、 それまでは組込みシステム開発時のクロスコンパイルにのみ使用されていました。
glob、regexp そして getopt ライブラリのような既存のフリーの実装を再利用することで、 かなりの作業量を減らすことが出来ました。 加えて、Cygwin では速度とサイズの両面でバランスが取れた、 Doug Lea によるフリーの malloc の実装が使われています。 Cygwin DLL はエクスポート関数へのポインタを使用して malloc 呼び出しを行います。これによって、 もし望むのであれば、Cygwin のプロセスは自前の malloc を用意することが可能となっています。
Cygwin での fork 呼び出しは、 Win32 API の最上位に対してうまく割り当てることが出来ないという点で、 特に興味深いものです。 そのため、この呼び出しを正しく実装することはたいへん難しい作業です。 現在、Cygwin の fork は初期の UNIX でよく使用されたような、 non-copy-on-write 実装を用いています。
親プロセスが子プロセスを fork する際に最初に発生する作業は、 親プロセスが子プロセスのために Cygwin のプロセステーブル内の領域を初期化するすることです。 それから、Win32 の CreateProcess 呼び出しを使用して、 サスペンド状態の子プロセスを作成します。 次に、親プロセスは自分自身のコンテキストを保持するために setjmp を呼び出し、Cygwin の共有メモリ領域 (Cygwin の全てのタスクの間で共有される) にこのコンテキストへのポインタを設定します。 そして、親プロセス自身のアドレス領域からサスペンド状態の子プロセスのアドレス 領域へとコピーすることで、 子プロセスの .data セクション及び .bss セクションを埋めます。 子プロセスのアドレス領域が初期化された後、 子プロセスが実行され、その間親プロセスはミューテックスを使用して待機します。 子プロセスは自分自身が fork されたことを知ると、保存されたジャンプバッファを使用して longjump します。 子プロセスは親プロセスが待機しているミューテックスをセットし、 もう一つのミューテックスをブロックします。 これは親プロセスに対して、 子プロセスが親プロセスのスタックとヒープをコピーし終わったことを知らせるシグナルとなり、 その後、親プロセスは子プロセスが待機していたミューテックスをリリースして fork 呼び出しから復帰します。 最後に、子プロセスは最後のミューテックスのブロックから復帰し、 共有領域を使用して渡されたメモリマップ済み領域を再生成して、 自分自身の fork から復帰します。
親プロセスと子プロセスの間で発生するコンテキストスイッチの数を減らすことで、 いかにして fork の実装を高速化するかということに対する幾つかのアイデアを得たとしても、 その後 fork はほぼ間違いなく常に Win32 の元では役に立たないものとなりました。 幸運なことに、多くの状況下では Cygwin が提供する spawn ファミリの呼び出しが、多少の作業だけで fork/exec の代わりを務めることが出来ます。これらの呼び出しは Win32 API の最上位に対して正しくマップされています。この結果として、 それらはより効果的なものとなりました。 fork の代わりに spawn 呼び出しを使うようにコンパイラのドライバプログラムを変更するという作業は些細な変更であり、 我々のテストではコンパイル速度が 20% から 30% も上昇しました。
しかしながら、spawn と exec にはそれぞれ独自の問題があります。 Win32 の元では実際の exec を行う方法が存在しないため、 Cygwin は独自のプロセス ID(PID)を考案しました。 この結果として、プロセスが複数の exec 呼び出しを実行した場合、 それらは一つの Cygwin PID に対して割り当てられた複数の Windows PID となります。幾つかの場合、これらの Win32 プロセスの個々のスタブが残ってしまい、 それらを呼び出した Cygwin プロセスの終了を待つことになります。
Cygwin のプロセスの開始時に、Cygwin DLL はシグナルハンドリング用に第二のスレッドを生成します。 このスレッドはシグナルをプロセスに渡すために使用される Windows イベントを待ち受けています。 プロセスに対してシグナルが送られた時点で、 このスレッドは自分自身が持つシグナルのビットマスクを調べ、 適切な方法でシグナル処理を行います。
実装における様々な複雑化は、 シグナルハンドラは実行プログラムとして同じアドレス空間で処理を行うという事実からきています。 直接の結論は、Cygwin システム関数は、 それを避けるための特別な処理がなければ中断され得るということです。 シグナルを送信する sig_send 関数が中断されるという事態を避けるため、 幾らかの長さに行きます(原文: We go to some lengths to prevent the sig_send function that sends signals from being interrupted.)。 プロセスが別のプロセスに対してシグナルを送る場合、 sig_send がシグナルの送信を完全に完了する前に中断されぬよう、 sig_send の周りにミューテックスが置かれます。
プロセスが自分自身に対してシグナルを送る場合、 ミューテックスの代わりにセマフォ及びイベントのペアを使用します。 sig_send はイベントによって開始され、 そのシグナルを処理するシグナルハンドラに立てられたセマフォをインクリメントします。 シグナルが処理されると、シグナルハンドラによっては既に実行されたイベントをシグナル状態にします。 このプロセスは POSIX によって要求されているプロセス間 (intraprocess)シグナル同期を管理します。
多くの標準的な UNIX のシグナルが提供されています。 ジョブコントロールは、シェルがサポートしていれば動作します。
Cygwin におけるソケット関連の呼び出しは、単に Microsoft によるバークレーソケットの実装である Winsock にある同名の関数を呼び出します。 UNIX でのセマンティクスに合致するよう、 幾つかの修正が必要なだけでした。 最も厄介な違いの一つは、最初のソケット関数が呼び出される前に Winsock が初期化されていなければならないというものです。 結果として、適切な時点でこの初期化を Cygwin が実行する必要があります。 fork 呼び出しを跨ったソケットをサポートするため、 継承したファイル記述子がソケットであった場合、 子プロセスは Winsock を初期化します。
不幸なことに、プロセスの開始時における DLL の暗黙のロードは時間のかかる作業となります。 多くのプロセスはソケットを使用しないので、 Cygwin は Winsock 初期化ルーチンが呼ばれた時点で Winsock DLL を明示的にロードします。 この単純な変更は、GNU configure の時間を 30% スピードアップさせます。
UNIX の select 関数は、 Win32 API の最上位に正しくマップ出来ないもう一つの呼び出しです。 たいへんがっかりさせられたことに、Winsock 中の Win32 の select はソケットハンドルに対してしか動作しません。 我々の実装では、select は異なるタイプのファイルディスクリプタ (ソケット、パイプ、ハンドル、そして独自の Windows メッセージ仮想デバイスである /dev/windows) に対しても通常は機能します。
select 関数に入る時点で最初に行われる処理は、 ファイル記述子を異なるタイプごとに整列することです。 ここで、二つの場合が考えられます。 最も単純なのは、最低一つのファイル記述子が、 可能状態にあることを常に知ることが出来るタイプ (ディスク上のファイルのような)である場合です。 この場合、select は可能状態にあるかどうかを確認するために他のタイプのファイル記述子をポーリングするやいなや、速やかに復帰します。より複雑なのは、 ソケット又はパイプへのファイル記述子が可能状態かどうかを待っている場合です。 これはメインスレッドが自分自身をサスペンドさせ、その後、 ファイル記述子のタイプごとに一つのスレッドを開始することによって実現されています。 個々のスレッドは自分が担当しているタイプのファイルディスクリプタを、 それに対応する Win32 API を呼び出すことによってポーリングします。 スレッドが可能状態になったファイルディスクリプタを発見するやいなや、 そのスレッドはメインスレッドに復帰するよう通知します。 全てのファイル記述子を一度にポーリングした後、select がリターンします。