0

Unix ソケットで実行されている ssh サービスがあり、UNIX ソケットのチャネルに向けたいローカル TCP サーバーがあります。

基本的に私がするとき:

$ ssh root@localhost -p 2000

次に、ローカルの TCP サーバーが要求を受け取り、それを Unix ソケットにパイプします。TCP クライアント (この場合は ssh) が Unix ソケットから応答を受け取ります。関連コード:

  let running_tunnel debug (tcp_ic, tcp_oc) () =
    Lwt_io.with_connection a_unix_addr begin fun (mux_ic, mux_oc) ->
      let%lwt _ = some_call with_an_arg
      and _ =

        (* Some setup code *)


      let rec forever () =
        Lwt_io.read_line tcp_ic >>= fun opening_message ->
        Lwt_io.write_from_string_exactly
        mux_oc opening_message 0 (String.length opening_message) >>= fun () ->
        Lwt_io.read_line mux_ic >>= fun reply ->
        Lwt_io.printl reply >>= fun () ->
        Lwt_io.write_line tcp_oc reply >>= fun () ->
        forever ()
      in
      forever ()
      in
      Lwt.return_unit
    end

そして、この種の作品。コマンドラインでsshを呼び出すと「スタック」しますが、反対側のsshヘッダーが正しいため、データを取得していることはわかっていますSSH-2.0-OpenSSH_6.7. また、最初の ssh ハンドシェイクのより多くの部分を出力するように側に指示します。つまり、次の出力が表示されます。

??^?W\zJ?~??curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1ssh-rsa,ssh-dss>aes128-ctr,aes192-ctr,aes256-ctr,chacha20-poly1305@openssh.com>aes128-ctr,aes192-ctr,aes256-ctr,chacha20-poly1305@openssh.com?umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1?umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1none,zlib@openssh.comnone,zlib@openssh.co 

など、正しいようです。ハングの理由は私が使用しているためだと考えたLwt_io.read_lineので、代わりにこれを試しました:

            let rec forever () =
              Lwt_io.read tcp_ic >>= fun opening_message ->
              Lwt_io.write_from_string_exactly
                mux_oc opening_message 0 (String.length opening_message) >>= fun () ->
              Lwt_io.read mux_ic >>= fun reply ->
              Lwt_io.printl reply >>= fun () ->
              Lwt_io.write tcp_oc reply >>= fun () ->
              forever ()
            in
            forever ()

これは実際にはさらに悪化し、最初のハンドシェイクさえ出力しませんでした。専用の ... 関数も試しましたが、{write,read}_into成功は限られていました。strace/dtruce で実行すると、次のような最終結果が表示されます。

read(0x6, "SSH-2.0-OpenSSH_6.9\r\n\0", 0x1000)       = 21 0
write(0x1, "SSH-2.0-OpenSSH_6.9\n\0", 0x14)      = 20 0
read(0x7, "\0", 0x1000)      = -1 Err#35
write(0x7, "SSH-2.0-OpenSSH_6.9\0", 0x13)        = 19 0
select(0x9, 0x7FFF5484F880, 0x7FFF5484F800, 0x7FFF5484F780, 0x0)         = 1 0
read(0x7, "SSH-2.0-OpenSSH_6.7\r\n\0", 0x1000)       = 21 0
write(0x1, "SSH-2.0-OpenSSH_6.7\n\0", 0x14)      = 20 0
read(0x6, "\0", 0x1000)      = -1 Err#35
write(0x6, "SSH-2.0-OpenSSH_6.7\n\0", 0x14)      = 20 0
select(0x9, 0x7FFF5484F880, 0x7FFF5484F800, 0x7FFF5484F780, 0x0)         = 1 0
read(0x6, "\0", 0x1000)      = 1968 0
read(0x6, "\0", 0x1000)      = -1 Err#35
^C

6.9 はローカル マシンの ssh で、6.7 は Unix ソケットの背後にあるリモート マシンです。私にとって奇妙に思えることの 1 つは、 がどのように\rドロップされ、これにより読み取り/書き込みカウントが 1 変化するかということです。これが決定的な違いになるかどうかはわかりません。

理想的には、この読み取り可能なチャネル (TCP ソケット) で利用可能なデータがあるときはいつでも、書き込み可能なチャネル (Unix ソケット) に直接書き込む、またはその逆の Lwt からのある種の抽象化が必要です。

4

1 に答える 1

2

データ ストリームがバイナリであり、テキスト行ベースの入力用であるため、のバリアントはreadline機能しませんでした。オプションのパラメーターを指定しない限り、この関数はすべての入力を最後まで読み取るため、関数readlineを使用した 2 番目のバリアントはLwt_io.read機能しませんでした。これは、リーダー側の EOF の後にのみcount制御が渡されることを意味します。たとえば、いくつかのカウントでwrite使用することは、それほど悪い考えではありません。また、ストリームが有限であることが予想される場合は、戻り値を確認することを忘れないでください。関数とは異なり、要求した正確な量のデータを読み取ることを保証しないため、注意して使用する必要があります。言い換えれば、短い読み取りがあります。同じことは、Lwt_io.readLwt_io.read ~count:1024 mux_icread_intoreadwrite_into関数。この関数の_exactlyバージョンではこの問題が発生しないため、代わりにそれらを使用することをお勧めします。

考慮すべきことがもう 1 つあります。Lwt_ioは、バッファリングされた入出力用のインターフェイスを提供します。つまり、このモジュールのすべての関数は、デバイス記述子を介してオペレーティング システムと直接対話するのではなく、何らかの内部バッファーに対して書き込みおよび読み取りを行っていることを意味します。つまり、あるバッファリングされたソースから別のバッファリングされたソースにデータをパイプすると、両端で予期しない遅延が発生します。したがって、フラッシュを使用してそれらを予測する必要があります。そうしないと、双方向のやり取りがあるときに競合状態が発生する可能性があります。

さらに、バッファリングされた io は物事を大幅に簡素化しますが、代償が伴います。実際、不要なバッファ層がいくつかあります。 を使用しているときはLwt_io、大量の不要なデータも割り当てて、メモリをガベージで浪費します。問題はLwt_io、独自の内部バッファーがあり、カジュアルなユーザーには明らかにされず、データを返したりデータを消費したりするすべての関数は、内部関数との間で追加のコピー操作を実行する必要があることです。たとえば、 を使用Lwt_io.{read,write}すると、次のようになります。

  1. カーネルから内部バッファにデータをコピーする
  2. 文字列を割り当てる
  3. 内部バッファから割り当てられた文字列にデータをコピーします
  4. (現在はwrite一部)文字列から内部バッファにデータをコピーします
  5. 内部バッファからカーネルにデータをコピーします。
  6. (GC 内のどこか、場合によっては後で)割り当てられた文字列をマイナー ヒープからメジャーにコピーする (文字列がマイナー ヒープに収まるほど小さかった場合)、またはある場所から別の場所にコピーする (圧縮アルゴリズムがそれを移動することを決定した場合、およびプロデューサーがコンシューマーを追い越して、読み取りデータの有効期間がかなり長くなった場合、文字列はまだ生きています。

2、3、4、および 6 でコピーを削除できるようです。独自のバッファーを使用して、カーネルからデータをそこにコピーし、このカーネルからデータをカーネルにコピーして戻すことができます。spliceおよびteeシステム コールを使用して、ユーザー空間をまったく使用せずにカーネル バッファー間でデータを直接コピーすることで、1 と 5 のコピーを取り除くことさえできます。しかし、その場合、データを調べる能力が失われ、通常はこれが必要になります。

それでは、カーネル空間からコピー以外のすべてのコピーを削除してみましょう。Lwt_io, likedirect_accessおよび新しく追加された関数で内部バッファーへの低レベルのインターフェイスを使用できますblockが、これには の内部に関する知識が必要であり、Lwt_ioそれほど簡単ではありませんが、それでも実行可能です。代わりに、ライブラリを使用するより単純なアプローチを使用しLwt_unixます。このライブラリは、中間バッファなしでカーネルと直接対話し、バッファリングを独自に残します。

open Lwt.Infix

let bufsiz = 32768

let echo ic oc =
  let buf = Lwt_bytes.create bufsiz in
  let rec loop p =
    let p = p mod bufsiz in
    Lwt_bytes.read ic buf p (bufsiz - p) >>= function
    | 0 -> Lwt.return ()
    | n -> Lwt_bytes.write oc buf p n >>= loop in
  loop 0

これにより、データの単純かつ高速な複製が実装され、プログラムと同じ速度でデータがコピーされますcat。ただし、まだ改善の余地があります。たとえば、ロバスト性 (特にEINTRシグナル) のために、エラー処理を追加する必要があります。また、この関数は、入力と出力が厳密にロックされる同期コピーを実装します。時にはそれはオプションではありません。次の例を考えてみましょう。入力は UDP ソケットであり、消費者を簡単に追い越す可能性があり、平均して生産者が消費者よりも遅い場合でも、データは削除されます。これを処理するには、リーダーとライターを 2 つの個別のスレッドに分割し、エラスティック キューを介して通信する必要があります。

Lwtは、この問題を解決しない、または解決すべきではない、非常に低レベルのライブラリです。特に各ケースのソリューションを構築するために使用できるメカニズムを提供します。いくつかの一般的なパターンのソリューションを提供するライブラリがあり、0MQnanomessagesが良い例です。

アップデート

私はレベルが低すぎるのかもしれませんし、深く掘り下げているのかもしれません。高レベルのアプローチを実際に探している場合は、を使用する必要があります。その場合、次のようLwt_streamに等価なノードをコーディングできますfoo.pipe(bar).pipe(foo)

let echo ic oc = Lwt_io.(write_chars oc (read_chars ic))

もちろん、これははるかに遅くなりますが、タスクによって異なります。

はい、双方向のリダイレクトを実行するには、次のように 2 つのスレッドを実行する必要がありecho ic oc <&> echo ic ocます。ファイル記述子を使用するバージョンでは、どちらも書き込み可能です。Lwt_ioパイプのように一方向のチャネルを使用している場合は、各部分に 2 つのエンドポイントを取得します。それらfiに名前を付けてfo、フロントエンドの入力と出力に対応させbiboバックエンドの部分に を付けます。次に、次のように接続する必要があります:echo fo bi <&> echo bo fiの 2 番目のバージョンのechowith ストリームを使用します。

パフォーマンス コスト

通常、高レベルの抽象化にはパフォーマンス コストが伴います。私たちの特定のケースでは、echo の最初のバージョンを使用すると、1Gb1 秒あたりのスループットが高くなりました。ストリームを含むバージョンのスループットは平均5MB/sです。設定によっては、機能する場合と機能しない場合があります。通常の ssh セッションには十分ですがscp、ローカル ネットワークに影響を与える可能性があります。

于 2015-12-17T15:20:45.350 に答える