C言語でsetjmpとlongjmpを使用して、例外のキャプチャとコルーチンを実現します


これはダオ兄弟のオリジナルの017です

I.はじめに

C標準ライブラリには、setjmpとlongjmpの2つの強力な関数があります。コードでそれらを使用したことがあるでしょうか。私は体内の何人かの同僚に尋ねました、何人かの人々はこれらの2つの機能を知らない、何人かの人々はこの機能を知っていますが、それを使ったことがありません。

知識の範囲から判断すると、これら2つの関数の関数は比較的単純であり、単純なサンプルコードでそれを明確にすることができます。ただし、この知識ポイントから分岐して考え、さまざまな次元で、この知識ポイントをこのプログラミング言語の他の同様の知識と関連付けて比較し他のプログラミング言語の同様の概念比較し、次にこの知識ポイントがどこにあるかを考える必要があります。使用することができ、他の人がそれをどのように使用するか。

今日は、これら2つの機能について説明しましょう。一般的なプログラムでは使用できませんが、将来、より特殊なプログラムフローを処理する必要がある場合、予期しない結果が生じる可能性があります。

例:関数の観点からsetjmp / longjmpgotoステートメントと比較し、関数戻り値と比較し言語でのコルーチン使用シナリオと比較します fork Python/Lua

2、関数構文の紹介

1.最小限の例

意味をなさないようにしましょう。この最も単純なサンプルコードをてください。理解していなくてもかまいません。よく知っています。

int main()
{
    // 一个缓冲区,用来暂存环境变量
    jmp_buf buf;
    printf("line1 \n");
    
    // 保存此刻的上下文信息
    int ret = setjmp(buf);
    printf("ret = %d \n", ret);
    
    // 检查返回值类型
    if (0 == ret)
    {
        // 返回值0:说明是正常的函数调用返回
        printf("line2 \n");
        
        // 主动跳转到 setjmp 那条语句处
        longjmp(buf, 1);
    }
    else
    {
        // 返回值非0:说明是从远程跳转过来的
        printf("line3 \n");
    }
    printf("line4 \n");
    return 0;
}

結果:

実行順序は次のとおりです(理解できない場合は、その中には入らないでください。以下の説明を読んだ後、振り返ってください)。

2.機能の説明

まず、次の2つの関数のシグネチャを確認します。

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int value);

それらはすべてヘッダーファイルで宣言されています、ウィキペディアは次のように説明しています: setjmp.h

setjmp:ローカルjmp_bufバッファーをセットアップし、ジャンプ用に初期化します。このルーチンは、後でlongjmpが使用できるように、プログラムの呼び出し環境をenv引数で指定された環境バッファーに保存します。戻りが直接呼び出しからのものである場合、setjmpは0を返します。戻りがlongjmpの呼び出しからのものである場合、setjmpはゼロ以外の値を返します。
longjmp:プログラムの同じ呼び出しでsetjmpルーチンの呼び出しによって保存された環境バッファーenvのコンテキストを復元します。ネストされたシグナルハンドラーからのlongjmpの呼び出しは定義されていません。valueで指定された値は、longjmpからsetjmpに渡されます。longjmpが完了した後、プログラムの実行は、対応するsetjmpの呼び出しがちょうど戻ったかのように続行されます。longjmpに渡される値が0の場合、setjmpは1を返したかのように動作します。それ以外の場合は、値を返したかのように動作します。

私自身の理解を利用して、上記の段落を英語で説明しましょう。

setjmp関数

  1. 関数:この関数を実行するときに、主に一部のレジスタの値など、さまざまなコンテキスト情報を保存します。
  2. パラメータ:コンテキスト情報の保存に使用されるバッファ。これは、現在のコンテキスト情報のスナップショットを取得して保存するのと同じです。
  3. 戻り値:戻り値は2種類あります。setjmp関数を直接呼び出すと戻り値は0になります。longjmp関数を呼び出してジャンプすると戻り値はゼロになりません。ここで関数と比較できます。プロセスを作成するフォーク。

longjmp関数

  1. 機能:パラメータenvバッファに保存されているコンテキスト(スナップショット)にジャンプして実行します。
  2. パラメーター:envパラメーターは、実行のためにジャンプするコンテキスト(スナップショット)を指定します。値は、setjmp関数に戻り判定情報を提供するために使用されます。つまり、longjmp関数が呼び出されると、このパラメーター値は、 setjmp関数の戻り値。
  3. 戻り値:戻り値なし。この関数が呼び出されると、実行のために他の場所のコードに直接ジャンプし、再び戻ってくることはないためです。

概要:これら2つの関数は、プログラムのジャンプを実現するために一緒に使用されます。

3. setjmp:コンテキスト情報を保存します

Cコードがバイナリファイルにコンパイルされた後、実行中にメモリにロードされ、CPUがコードセグメントへの各命令を取り出して実行することがわかっています。CPUには、コードセグメントレジスタCS、命令オフセットレジスタIPなど、現在の実行環境を保存するための多くのレジスタがあります。もちろん、他にも多くのレジスタがあります。この実行環境をコンテキストと呼びます。

次の図に示すように、CPUが次の実行命令を取得するCSとIPの2つのレジスタを介して実行される命令を取得できます。

いくつかの知識ポイントを追加します。

  1. 上の図では、コードセグメントレジスタCSはベースアドレスと見なされます。つまり、CSはメモリ内のコードセグメントの開始アドレスを指し、IPレジスタは次に実行される命令アドレスのオフセットを表します。このベースアドレスから。したがって、命令をフェッチするたびに、これら2つのレジスタに値を追加するだけで、命令のアドレスを取得できます。
  2. 実際、x86プラットフォームでは、コードセグメントレジスタCSはベースアドレスではなく、セレクタです。オペレーティングシステムのどこかにテーブルがあり、このテーブルにはコードセグメントの実際の開始アドレスが格納され、CSレジスタにはインデックス値のみが格納されます。このインデックス値は、このテーブルのテーブルエントリを指します。これには、仮想の関連知識が含まれます。メモリ;
  3. 命令を取得した後、IPレジスタは自動的に次の命令の先頭に移動します。移動するバイト数は、現在フェッチされている命令が占めるバイト数によって異なります。

CPUは大ばかで、わかりません。何をさせても、CPUは何をするのかを実行します。たとえば、フェッチ命令:CSレジスタとIPレジスタを設定している限り、CPUはこれら2つのレジスタの値を使用して命令をフェッチします。これらの2つのレジスタが間違った値に設定されている場合、CPUは愚かな命令をフェッチしますが、実行中にクラッシュします。

これらのレジスタ情報はコンテキスト情報として簡単に理解でき、CPUはコンテキスト情報に従ってそれを実行します。そのため、C言語では、現在のコンテキスト情報を保存して一時的にバッファに保存するためのsetjmpライブラリ関数を用意しました。

保存の目的は何ですか?将来的に現在の場所に復元して実行を継続できるようにするため。

より簡単な例があります:サーバーのスナップショット。スナップショットの目的は何ですか?サーバーにエラーが発生した場合、特定のスナップショットに戻すことができます

4. longjmp:ジャンプを実装します

ジャンプと言えば、すぐに頭に浮かんだのはgotoステートメントです。多くのチュートリアルでgotoステートメントについて多くの意見があり、コードで使用しないようにする必要があると思います。この観点は良い出発点です。gotoを使いすぎると、コードの実行順序の理解に影響します。

しかし、Linuxカーネルのコードを見ると、gotoステートメントがたくさん見つかります。繰り返しますが、コードのメンテナンスと実行効率のバランスを見つけてください

ジャンプすると、プログラムの実行順序が変更されます。gotoステートメントは、関数内でのみジャンプでき、関数と交差した場合は何も実行できません。

したがって、C言語はリモートジャンプを実装するためのlongjmp関数を提供します。これは、その名前からわかります。つまり、関数間をジャンプできます。

CPUの観点から、いわゆるジャンプとは、コンテキスト内のさまざまなレジスタを特定の時間のスナップショットとして設定することです。明らかに、上記のsetjmp関数では、そのときのコンテキスト情報(スナップショット)がに格納されています。一時バッファ。領域内にあり、その場所にジャンプする場合は、実行すると、回線上のCPUに直接通知されます。

CPUに伝える方法は?一時バッファレジスタ情報をCPUで使用されているレジスタに上書きするだけです。

5. setjmp:戻り値の型と戻り値

複数のプロセスを必要とするいくつかのプログラムでは、我々は、頻繁に使用するfork関数をする「インキュベート」新しいプロセスを、現在のプロセス、そして新しいプロセスがされて実行からの次のステートメントフォーク機能

以下の場合、メインプロセス、fork関数を呼び出した後に戻っても、次のステートメントを実行し続け、そのメインプロセスと新しいプロセスを区別するためにどのように?fork関数は、以下を区別するための戻り値提供します

fork関数は0を返します:これは新しいプロセスであることを意味します;
fork関数はゼロ以外を返します:それは元のメインプロセスを意味し、戻り値は新しいプロセスのプロセス番号です。

同様に、setjmp関数にもさまざまな戻り値の型があります。おそらく、戻り値の型を表現するのは正確ではありません。次のように理解できます。setjmp関数から戻ると合計2つのシナリオがあります

  1. setjmpがアクティブに呼び出された場合:0が返されます。アクティブな呼び出しの目的は、コンテキストを保存してスナップショットを作成することです。
  2. longjmpをジャンプする場合:ゼロ以外を返します。このときの戻り値は、longjmpの2番目のパラメーターで指定されます。

上記の2つの異なる値に従って、異なるブランチ処理を実行できます。longjmpジャンプで戻る場合、実際のシーンに応じて異なるゼロ以外の値を返すことができますしている人のために、このような言語スクリプトでの経験をプログラミングとしてPythonとLuaのを、彼らは考えるんでした収量/レジューム機能パラメータと戻り値に対する外部パフォーマンスは同じです!

概要:これまでのところ、基本的に2つの関数setjmp / longjmpの使用を終了しましたが、十分に明確に説明したかどうかはわかりません。この時点で、記事の冒頭にあるサンプルコードを見てください。一目でわかるはずです。

3つ目は、setjmp / longjmpを使用して例外キャプチャを実現することです。

Cライブラリはこのツールを提供するため、特定の使用シナリオが必要です。例外キャプチャは、一部の高級言語(Java / C ++)(通常はtry-catchステートメント)の文法レベルで直接サポートされていますが、C言語で自分で実装する必要があります。

最も単純な例外キャプチャモデルの1つを示しましょう。コードには、合計56行あります。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

typedef int     BOOL;
#define TRUE    1
#define FALSE   0

// 枚举:错误代码
typedef enum _ErrorCode_ {
    ERR_OK = 100,         // 没有错误
    ERR_DIV_BY_ZERO = -1  // 除数为 0
} ErrorCode;

// 保存上下文的缓冲区
jmp_buf gExcptBuf;

// 可能发生异常的函数
typedef int (*pf)(int, int);
int my_div(int a, int b)
{
    if (0 == b)
    {
        // 发生异常,跳转到函数执行之前的位置
        // 第2个参数是异常代码
        longjmp(gExcptBuf, ERR_DIV_BY_ZERO);
    }
    // 没有异常,返回正确结果
    return a / b;
}

// 在这个函数中执行可能会出现异常的函数
int try(pf func, int a, int b)
{
    // 保存上下文,如果发生异常,将会跳入这里
    int ret = setjmp(gExcptBuf);
    if (0 == ret)
    {
        // 调用可能发生异常的哈数
        func(a, b);
        // 没有发生异常
        return ERR_OK;
    }
    else
    {
        // 发生了异常,ret 中是异常代码
        return ret;
    }
}

int main()
{
    int ret = try(my_div, 8, 0);     // 会发生异常
    // int ret = try(my_div, 8, 2);  // 不会发生异常
    if (ERR_OK == ret)
    {
        printf("try ok ! \n");
    }
    else
    {
        printf("try excepton. error = %d \n", ret);
    }
    
    return 0;
}

コードを詳細に説明する必要はありませんコード内のコメントを見て理解してください。このコードは単なる目安であり、より完全なパッケージを備えた本番コードで使用する必要があります。

注意すべき点の1つ:setjmp / longjmpは、プログラムの実行順序のみを変更します。アプリケーションの一部のデータをロールバックする必要がある場合は、手動で処理する必要があります。

第4に、setjmp / longjmpを使用してコルーチンを実装します

1.コルーチンとは

Cプログラムでは、同時に実行する必要のあるシーケンスが一般にスレッドによって実装されている場合、コルーチンとは何ですか?ウィキペディアによるコルーチンの説明は次のとおりです。

このページのコルーチンに関する詳細情報、コルーチンとスレッドについて具体的に説明されているページ、比較ジェネレーター、さまざまな言語実装メカニズム。

コルーチンとスレッドの違いを簡単に理解するために、プロデューサーとコンシューマーを使用します。

2.スレッド内のプロデューサーとコンシューマー

  1. プロデューサーとコンシューマーは2つの並列実行シーケンスであり、通常は2つのスレッドが実行に使用されます。
  2. 生産者が商品を生産するとき、消費者は待機状態(ブロック)になります。生産が完了した後、セマフォは消費者に商品を消費するように通知します。
  3. 消費者が商品を消費するとき、生産者は待機状態(ブロック)になります。消費が終わった後、生産者はセマフォによって商品の生産を継続するように通知されます。

3.コルーチンのプロデューサーとコンシューマー

  1. プロデューサーとコンシューマーは同じ実行シーケンスで実行され、実行シーケンスにジャンプして交互に実行されます。
  2. 生産者が商品を生産した後、CPUを放棄し、消費者に実行させます。
  3. 消費者が製品を消費した後、彼はCPUを放棄し、プロデューサーにそれを実行させます。

4.C言語でのコルーチンの実装

これが最も単純なモデルです。コルーチンのメカニズムはsetjmp / longjmpによって実現されます。主な目的は、パラメーターと戻り値の受け渡しの問題を解決することなく、コルーチンの実行シーケンスを理解することです。

C言語でのコルーチンの実装を研究したい場合は、gotoステートメントとswitchステートメントを使用してブランチジャンプを実装するDuffデバイスの概念を確認できます。使用される構文は奇妙ですが合法です。

typedef int     BOOL;
#define TRUE    1
#define FALSE   0

// 用来存储主程和协程的上下文的数据结构
typedef struct _Context_ {
    jmp_buf mainBuf;
    jmp_buf coBuf;
} Context;

// 上下文全局变量
Context gCtx;

// 恢复
#define resume() \
    if (0 == setjmp(gCtx.mainBuf)) \
    { \
        longjmp(gCtx.coBuf, 1); \
    }

// 挂起
#define yield() \
    if (0 == setjmp(gCtx.coBuf)) \
    { \
        longjmp(gCtx.mainBuf, 1); \
    }

// 在协程中执行的函数
void coroutine_function(void *arg)
{
    while (TRUE)  // 死循环
    {
        printf("\n*** coroutine: working \n");
        // 模拟耗时操作
        for (int i = 0; i < 10; ++i)
        {
            fprintf(stderr, ".");
            usleep(1000 * 200);
        }
        printf("\n*** coroutine: suspend \n");
        
        // 让出 CPU
        yield();
    }
}

// 启动一个协程
// 参数1:func 在协程中执行的函数
// 参数2:func 需要的参数
typedef void (*pf)(void *);
BOOL start_coroutine(pf func, void *arg)
{
    // 保存主程的跳转点
    if (0 == setjmp(gCtx.mainBuf))
    {
        func(arg); // 调用函数
        return TRUE;
    }

    return FALSE;
}

int main()
{
    // 启动一个协程
    start_coroutine(coroutine_function, NULL);
    
    while (TRUE) // 死循环
    {
        printf("\n=== main: working \n");

        // 模拟耗时操作
        for (int i = 0; i < 10; ++i)
        {
            fprintf(stderr, ".");
            usleep(1000 * 200);
        }

        printf("\n=== main: suspend \n");
        
        // 放弃 CPU,让协程执行
        resume();
    }

    return 0;
}

印刷情報は次のとおりです。

五数要約

この記事の焦点は、setjmp / longjmpの構文と使用シナリオを紹介することです。一部の需要シナリオでは、半分の労力で乗数効果を実現できます。

もちろん、シーケンスジャンプを実行することで、想像力を駆使してより凝った機能を実現することもできます。すべてが可能です。


自慢したり、誇大広告したり、誇張したり、すべての記事を注意深く書いたりしないでください。 フォワード
へようこそ。 テクノロジー、コロンビアロードの周りの友達 と共有 し、心からの感謝を表します。転送された 推奨言語 は、あなたがそれを考えるのに役立ちました:

ダオ兄弟によって要約されたこの要約記事は非常に注意深く書かれており、それは私の技術的改善に非常に役立ちます。共有するのに良いこと!

最後に、私はあなたに願っています:コードに直面してもバグはありません;人生に直面して、春の花!


【原文】

OF:コロンビアロード(公開番号:町のもののIOT
はほとんど知っています:コロンビアロード
駅B:シェアコロンビアロード
デンバー:コロンビアロードシェア
CSDN:コロンビアロードシェア


組み込み開発プロジェクトの アウトプットサマリー10年の実務経験を載せ ます!

写真のQRコードを長押ししてフォロー、フォロー+スターパブリックアカウント、各記事には乾物があります。


転載:転載へようこそが、著者の同意なしに、この声明を保持し、元のリンクを記事に記載する必要があります。

推奨読書

C言語ポインター-基本的な原則から高度なスキルまで、徹底的な
ステップバイステップの分析を説明するのに役立つ写真とコード付き-Cを使用してオブジェクト指向プログラミングを実装する方法、
元のgdbデバッグ原則は非常に単純
です。暗号化、証明書、
LUAスクリプト言語の奥深くで、デバッグの原理を完全に理解できます

おすすめ

転載: blog.csdn.net/u012296253/article/details/113543344