記事の素材は私のものから引用しました
データ移動メカニズム
- データブロック dblk_t
- メッセージmblk_t
- メッセージを操作するための関数 mblk_t
- キュー queue_t
- キューを操作するための関数 queue_t
- フィルターの接続
- データ処理グラフの信号点
- ティッカーの舞台裏の活動
- バッファライザー (MSBufferizer)
- MSBufferizer を操作するための関数
過去に
データ移動メカニズム
メディア ストリーマーでのデータの移動は、次の構造で記述されたキューを使用して実行されます。 キュー_t。のようなメッセージの文字列 mblk_t、それ自体には信号データは含まれておらず、前、次のメッセージ、およびデータ ブロックへのリンクのみが含まれています。さらに、同じタイプのメッセージへのリンク用のフィールドもあり、単一リンクされたメッセージのリストを整理できることを特に強調したいと思います。このようなリストでまとめられたメッセージのグループをタプルと呼びます。したがって、キューの任意の要素を単一のメッセージにすることができます。 mblk_t、おそらくメッセージタプルの先頭 mblk_t。各タプル メッセージは独自のワード データ ブロックを持つことができます。なぜタプルが必要なのかについては、後ほど説明します。
前述したように、メッセージ自体にはデータのブロックは含まれておらず、代わりに、ブロックが保存されているメモリ領域へのポインタのみが含まれています。この部分では、メディア ストリーマーの仕事の全体像は、漫画「モンスターズ インク」のドア倉庫を思い出させます。ドア (データへのリンク - 部屋) が頭上のコンベアに沿って異常な速度で移動する一方で、部屋自体は動かないままです。
次に、階層に沿って下から上に移動して、メディア ストリーマー内のデータ送信メカニズムのリストされたエンティティを詳細に検討してみましょう。
データブロック dblk_t
データブロックはヘッダーとデータバッファーで構成されます。ヘッダーは以下の構造で記述されます。
typedef struct datab
{
unsigned char *db_base; // Указатель на начало буфер данных.
unsigned char *db_lim; // Указатель на конец буфер данных.
void (*db_freefn)(void*); // Функция освобождения памяти при удалении блока.
int db_ref; // Счетчик ссылок.
} dblk_t;
構造体のフィールドには、バッファの先頭、バッファの末尾、およびデータ バッファを削除する関数へのポインタが含まれています。ヘッダーの最後の要素 db_ref — 参照カウンタ。ゼロに達すると、このブロックをメモリから削除する信号として機能します。データブロックが関数によって作成された場合 datab_alloc() 、データ バッファーはヘッダーの直後にメモリに配置されます。それ以外の場合はすべて、バッファーは別の場所に配置できます。データ バッファーには、フィルターで処理する信号サンプルまたはその他のデータが含まれます。
データ ブロックの新しいインスタンスは、次の関数を使用して作成されます。
dblk_t *datab_alloc(int size);
入力パラメータとして、ブロックが格納するデータのサイズが指定されます。割り当てられたメモリの先頭にヘッダー構造体を配置するために、より多くのメモリが割り当てられます。 データベース。ただし、他の関数を使用する場合、これは常に起こるわけではなく、場合によっては、データ バッファーがデータ ブロック ヘッダーとは別に配置される場合があります。構造体を作成するとき、フィールドは、そのフィールドが データベースベース データ領域の先頭を指し、そして db_lim その終わりまで。リンク数 db_ref は 1 に設定されます。データクリア関数ポインタはゼロに設定されます。
メッセージ mblk_t
前述したように、キュー要素のタイプは次のとおりです。 mblk_t、 それは次のように定義されます。
typedef struct msgb
{
struct msgb *b_prev; // Указатель на предыдущий элемент списка.
struct msgb *b_next; // Указатель на следующий элемент списка.
struct msgb *b_cont; // Указатель для подклейки к сообщению других сообщений, для создания кортежа сообщений.
struct datab *b_datap; // Указатель на структуру блока данных.
unsigned char *b_rptr; // Указатель на начало области данных для чтения данных буфера b_datap.
unsigned char *b_wptr; // Указатель на начало области данных для записи данных буфера b_datap.
uint32_t reserved1; // Зарезервированное поле1, медиастример помещает туда служебную информацию.
uint32_t reserved2; // Зарезервированное поле2, медиастример помещает туда служебную информацию.
#if defined(ORTP_TIMESTAMP)
struct timeval timestamp;
#endif
ortp_recv_addr_t recv_addr;
} mblk_t;
構造 mblk_t 先頭にポインタが含まれています b_前, b_next、二重リンクリスト (キュー) を整理するために必要です。 キュー_t).
次にポインターが来ます b_続き、メッセージがタプルの一部である場合にのみ使用されます。タプル内の最後のメッセージでは、このポインタは null のままです。
次に、データ ブロックへのポインターが表示されます。 b_datap、メッセージが存在します。その後に、ブロック データ バッファー内の領域へのポインターが続きます。分野 b_rptr バッファからデータを読み取る場所を指定します。分野 b_wptr バッファへの書き込みが実行される場所を示します。
残りのフィールドはサービスの性質のものであり、データ転送メカニズムの動作には関係しません。
以下は名前を含む単一のメッセージです m1 そしてデータブロック d1.
次の図は、3 つのメッセージのタプルを示しています。 m1, m1_1, m1_2.
メッセージ機能 mblk_t
新しいメッセージ mblk_t 関数によって作成されました:
mblk_t *allocb(int size, int pri);
彼女はメモリに新しいメッセージを置きます mblk_t 指定されたサイズのデータ ブロックを持つ サイズ、第 2 引数 - プリ このバージョンのライブラリでは使用されません。ゼロのままでなければなりません。関数の操作中に、新しいメッセージの構造にメモリが割り当てられ、関数が呼び出されます。 mblk_init()これにより、作成された構造体のインスタンスのすべてのフィールドがリセットされ、その後、上記のメソッドを使用します。 datab_alloc()、データバッファを作成します。その後、構造内のフィールドが構成されます。
mp->b_datap=datab;
mp->b_rptr=mp->b_wptr=datab->db_base;
mp->b_next=mp->b_prev=mp->b_cont=NULL;
出力では、初期化されたフィールドと空のデータ バッファーを含む新しいメッセージを受け取ります。メッセージにデータを追加するには、データ ブロック バッファーにデータをコピーする必要があります。
memcpy(msg->b_rptr, data, size);
どこ データ データソースへのポインタであり、 サイズ - そのサイズ。
次に、バッファ内の空き領域の先頭を再び指すように、書き込みポイントへのポインタを更新する必要があります。
msg->b_wptr = msg->b_wptr + size
コピーせずに既存のバッファからメッセージを作成する必要がある場合は、次の関数を使用します。
mblk_t *esballoc(uint8_t *buf, int size, int pri, void (*freefn)(void*));
この関数は、メッセージとデータ ブロックの構造を作成した後、次のアドレスのデータへのポインターを構成します。 BUF。それらの。この場合、関数を使用してデータ ブロックを作成した場合とは異なり、データ バッファーはデータ ブロックのヘッダー フィールドの後に配置されません。 datab_alloc()。関数に渡されたデータを含むバッファーは元の場所に残りますが、ポインターの助けを借りて、データ ブロックの新しく作成されたヘッダーに添付され、それに応じてメッセージにも添付されます。
一つのメッセージに mblk_t 複数のデータ ブロックを連続して連結できます。これは次の関数によって行われます。
mblk_t * appendb(mblk_t *mp, const char *data, int size, bool_t pad);
mp — 別のデータ ブロックが追加されるメッセージ。
データ — ブロックへのポインタ。そのコピーがメッセージに追加されます。
サイズ — データサイズ。
パッド — 割り当てられたメモリのサイズが 4 バイト境界に沿って整列する必要があることを示すフラグ (パディングはゼロで行われます)。
既存のメッセージ データ バッファーに十分なスペースがある場合は、新しいデータが既存のデータの後ろに貼り付けられます。メッセージ データ バッファーの空き領域が少ない場合は、 サイズ、その後、十分なバッファ サイズで新しいメッセージが作成され、データがそのバッファにコピーされます。これは新しいメッセージであり、ポインタを使用して元のメッセージにリンクされています b_続き。この場合、メッセージはタプルに変わります。
タプルに別のデータ ブロックを追加する必要がある場合は、次の関数を使用する必要があります。
void msgappend(mblk_t *mp, const char *data, int size, bool_t pad);
彼女はタプル内の最後のメッセージを見つけます (彼は b_続き null になります)、このメッセージの関数を呼び出します appendb().
次の関数を使用して、メッセージまたはタプル内のデータのサイズを確認できます。
int msgdsize(const mblk_t *mp);
タプル内のすべてのメッセージをループし、それらのメッセージのデータ バッファー内のデータの総量を返します。各メッセージのデータ量は次のように計算されます。
mp->b_wptr - mp->b_rptr
2 つのタプルを結合するには、次の関数を使用します。
mblk_t *concatb(mblk_t *mp, mblk_t *newm);
彼女はタプルを追加します ニューム タプルの末尾まで mp そして、結果のタプルの最後のメッセージへのポインタを返します。
必要に応じて、タプルを 1 つのデータ ブロックを含む 1 つのメッセージに変換できます。これは次の関数によって行われます。
void msgpullup(mblk_t *mp,int len);
引数の場合 LEN が -1 の場合、割り当てられるバッファのサイズは自動的に決定されます。もし LEN が正の数の場合、このサイズのバッファが作成され、そこにタプル メッセージ データがコピーされます。バッファがなくなると、コピーはそこで停止します。タプルの最初のメッセージは、コピーされたデータを含む新しいサイズのバッファーを受け取ります。残りのメッセージは削除され、メモリがヒープに戻されます。
構造物を削除する場合 mblk_t 呼び出し時に、データ ブロックの参照カウントが考慮される フリーブ() ゼロであることが判明した場合、データ バッファはインスタンスとともに削除されます。 mblk_t、それを指します。
新しいメッセージのフィールドを初期化します。
void mblk_init(mblk_t *mp);
メッセージに別のデータを追加します。
mblk_t * appendb(mblk_t *mp, const char *data, size_t size, bool_t pad);
新しいデータがメッセージ データ バッファの空き領域に収まらない場合は、必要なサイズのバッファを持つ別途作成されたメッセージがメッセージに添付されます (追加されたメッセージへのポインタが最初のメッセージに設定されます)。メッセージはタプルに変わります。
データの一部をタプルに追加します。
void msgappend(mblk_t *mp, const char *data, size_t size, bool_t pad);
この関数はループ内で appendb() を呼び出します。
2 つのタプルを 1 つに結合します。
mblk_t *concatb(mblk_t *mp, mblk_t *newm);
メッセージ ニューム に付属します mp.
単一メッセージのコピーを作成する:
mblk_t *copyb(const mblk_t *mp);
すべてのデータ ブロックを含むタプルのコピーを完了します。
mblk_t *copymsg(const mblk_t *mp);
タプルの要素は関数によってコピーされます コピーb().
メッセージの簡単なコピーを作成する mblk_t。この場合、データ ブロックはコピーされませんが、その参照カウンターは増加します。 db_ref:
mblk_t *dupb(mblk_t *mp);
タプルの軽量コピーを作成します。データ ブロックはコピーされず、参照カウンターのみがインクリメントされます。 db_ref:
mblk_t *dupmsg(mblk_t* m);
タプルのすべてのメッセージを 1 つのメッセージに結合します。
void msgpullup(mblk_t *mp,size_t len);
引数の場合 LEN が -1 の場合、割り当てられるバッファのサイズは自動的に決定されます。
メッセージの削除、タプル:
void freemsg(mblk_t *mp);
データ ブロックの参照カウントは 1 つ減ります。ゼロになると、データ ブロックも削除されます。
メッセージまたはタプル内のデータの合計量の計算。
size_t msgdsize(const mblk_t *mp);
キューの最後尾からメッセージを取得します。
mblk_t *ms_queue_peek_last (q);
あるメッセージの予約フィールドの内容を別のメッセージにコピーします (実際、これらのフィールドにはメディア ストリーマーによって使用されるフラグが含まれています)。
mblk_meta_copy(const mblk_t *source, mblk *dest);
待ち行列 キュー_t
メディア ストリーマのメッセージ キューは、循環二重リンク リストとして実装されます。各リスト要素には、信号サンプルを含むデータ ブロックへのポインターが含まれています。データ ブロックへのポインタだけが順番に移動し、データ自体は動かないことがわかります。それらの。それらへのリンクのみが移動されます。
キューを説明する構造 キュー_t、 下に示された:
typedef struct _queue
{
mblk_t _q_stopper; /* "Холостой" элемент очереди, не указывает на данные, используется только для управления очередью. При инициализации очереди (qinit()) его указатели настраиваются так, чтобы они указывали на него самого. */
int q_mcount; // Количество элементов в очереди.
} queue_t;
構造体にはフィールド、つまりポインタが含まれています _q_ストッパー *mblk_t と入力すると、キュー内の最初の要素 (メッセージ) を指します。この構造体の 2 番目のフィールドは、キュー内のメッセージのカウンターです。
次の図は、1 つのメッセージ m4、m1、m2、m3 を含む q4 という名前のキューを示しています。
次の図は、1 つのメッセージ m4、m1、m2、m3 を含む q4 という名前のキューを示しています。メッセージ m2 は、さらに 2 つのメッセージ m1_2 および m2_XNUMX を含むタプルの先頭です。
キューを操作するための関数 queue_t
キューの初期化:
void qinit(queue_t *q);
フィールド _q_ストッパー (以下「ストッパー」と呼びます)は関数によって初期化されます mblk_init()、その前の要素と次の要素のポインターは、それ自体を指すように調整されます。キュー要素カウンターはゼロにリセットされます。
新しい要素 (メッセージ) を追加します。
void putq(queue_t *q, mblk_t *m);
新しい要素 m がリストの最後に追加されると、ストッパーがその次の要素になり、ストッパーが前の要素になるように要素ポインターが調整されます。キュー要素カウンターがインクリメントされます。
キューから要素を取得します。
mblk_t * getq(queue_t *q);
ストッパーの後に来るメッセージが取得され、要素カウンターがデクリメントされます。キューにストッパー以外の要素がない場合は、0 が返されます。
メッセージをキューに挿入する:
void insq(queue_t *q, mblk_t *emp, mblk_t *mp);
要素 mp 要素の前に挿入される EMP。 もし EMP=0 の場合、メッセージはキューの最後尾に追加されます。
キューの先頭からメッセージを取得します。
void remq(queue_t *q, mblk_t *mp);
要素カウンターがデクリメントされます。
キュー内の最初の要素へのポインターを読み取ります。
mblk_t * peekq(queue_t *q);
要素自体を削除しながら、キューからすべての要素を削除します。
void flushq(queue_t *q, int how);
引数 の 使用されていない。キュー要素カウンターはゼロに設定されます。
キューの最後の要素へのポインターを読み取るマクロ:
mblk_t * qlast(queue_t *q);
メッセージ キューを操作するときは、次の点に注意してください。 ms_queue_put(q, m) メッセージへの null ポインターを使用すると、関数はループします。プログラムがフリーズしてしまいます。同様に動作します ms_queue_next(q, m).
フィルターの接続
上で説明したキューは、あるフィルターから別のフィルターへ、または 1 つから複数のフィルターへメッセージを渡すために使用されます。フィルターとその接続は有向グラフを形成します。フィルタの入力または出力を総称して「ピン」と呼びます。フィルターが互いに接続される順序を説明するために、メディア ストリーマーは「信号ポイント」の概念を使用します。信号点は構造物です _MSCポイント、これにはフィルターへのポインターとそのピンの番号が含まれており、それに応じてフィルターの入力または出力の 1 つの接続を記述します。
データ処理グラフの信号点
typedef struct _MSCPoint{
struct _MSFilter *filter; // Указатель на фильтр медиастримера.
int pin; // Номер одного из входов или выходов фильтра, т.е. пин.
} MSCPoint;
フィルター ピンには 0 から始まる番号が付けられます。
メッセージキューによる 2 つのピンの接続は、次の構造で記述されます。 _MSキューこれには、メッセージ キューと、それが接続する 2 つの信号ポイントへのポインタが含まれています。
typedef struct _MSQueue
{
queue_t q;
MSCPoint prev;
MSCPoint next;
}MSQueue;
この構造を信号リンクと呼びます。各メディア ストリーマ フィルタには、入力リンクのテーブルと出力リンクのテーブルが含まれています (MSキュー)。テーブルのサイズはフィルターの作成時に設定されます。これは、次のタイプのエクスポートされた変数を使用してすでに行っています。 MSフィルター記述、独自のフィルターを開発したとき。以下は、メディア ストリーマー内のフィルターを記述する構造です。 MSフィルター:
struct _MSFilter{
MSFilterDesc *desc; /* Указатель на дескриптор фильтра. */
/* Защищенные атрибуты, их нельзя сдвигать или убирать иначе будет нарушена работа с плагинами. */
ms_mutex_t lock; /* Семафор. */
MSQueue **inputs; /* Таблица входных линков. */
MSQueue **outputs; /* Таблица выходных линков. */
struct _MSFactory *factory; /* Указатель на фабрику, которая создала данный экземпляр фильтра. */
void *padding; /* Не используется, будет задействован если добавятся защищенные поля. */
void *data; /* Указатель на произвольную структуру для хранения данных внутреннего состояния фильтра и промежуточных вычислений. */
struct _MSTicker *ticker; /* Указатель на объект тикера, который не должен быть нулевым когда вызывается функция process(). */
/*private attributes, they can be moved and changed at any time*/
MSList *notify_callbacks; /* Список обратных вызовов, используемых для обработки событий фильтра. */
uint32_t last_tick; /* Номер последнего такта, когда выполнялся вызов process(). */
MSFilterStats *stats; /* Статистика работы фильтра.*/
int postponed_task; /*Количество отложенных задач. Некоторые фильтры могут откладывать обработку данных (вызов process()) на несколько тактов.*/
bool_t seen; /* Флаг, который использует тикер, чтобы помечать что этот экземпляр фильтра он уже обслужил на данном такте.*/
};
typedef struct _MSFilter MSFilter;
計画に従って C プログラムでフィルターを接続した後 (ティッカーは接続しませんでした)、それによって有向グラフを作成しました。そのノードは構造のインスタンスです。 MSフィルター、エッジはリンクのインスタンスです MSキュー.
ティッカーの舞台裏の活動
ティッカーはダニの発生源をフィルタリングするものであるとお話しましたが、それはすべて真実ではありませんでした。ティッカーは、時計に応じて機能を実行するオブジェクトです 処理する() 接続されている回路 (グラフ) のすべてのフィルター。 C プログラムでティッカーをグラフ フィルターに接続すると、今後オフにするまで制御するグラフがティッカーに表示されます。接続後、ティッカーは、管理を委託されたグラフの検査を開始し、グラフを含むフィルターのリストを作成します。同じフィルターを 2 回「カウント」しないように、検出されたフィルターにチェックボックスを入れてマークします。 見て。検索は各フィルタが持つリンクテーブルを利用して行われます。
グラフの紹介ツアー中に、ティッカーはフィルターの中にデータ ブロックのソースとして機能するものが少なくとも 1 つあるかどうかをチェックします。何もない場合、グラフは正しくないとみなされ、ティッカーがクラッシュします。
グラフが「正しい」と判明した場合、見つかったフィルターごとに初期化のために関数が呼び出されます。 前処理()。次の処理サイクルの瞬間が来るとすぐ (デフォルトでは 10 ミリ秒ごと)、ティッカーは関数を呼び出します。 処理する() 以前に見つかったすべてのソース フィルターに対して、次にリスト内の残りのフィルターに対して。フィルターに入力リンクがある場合、関数を実行します。 処理する() 入力リンク キューが空になるまで繰り返します。この後、リスト内の次のフィルタに移動し、入力リンクにメッセージがなくなるまでフィルタを「スクロール」します。ティッカーは、リストが終了するまでフィルターからフィルターへと移動します。これでサイクルの処理が完了します。
ここでタプルに戻り、そのようなエンティティがメディア ストリーマーに追加された理由について説明します。一般に、フィルター内で動作するアルゴリズムが必要とするデータの量は一致せず、入力で受信されるデータ バッファーのサイズの倍数ではありません。たとえば、高速フーリエ変換を実行するフィルターを作成していますが、定義上、サイズが 512 のべき乗であるデータ ブロックのみを処理できます。 160カウントとしましょう。データが電話チャネルによって生成された場合、入力側の各メッセージのデータ バッファーから XNUMX 個の信号サンプルが得られます。必要な量のデータが存在するまでは、入力からデータを収集しないように誘惑されます。ただしこの場合、ティッカーとの衝突が発生し、入力リンクが空になるまでフィルターをスクロールしようとしても失敗します。以前は、このルールをフィルターの XNUMX 番目の原則として指定しました。この原則に従って、フィルターの process() 関数は入力キューからすべてのデータを取得する必要があります。
さらに、ブロック全体、つまりブロック全体しか取得できないため、入力から 512 個のサンプルのみを取得することはできません。フィルターは 640 個のサンプルを取得し、そのうちの 512 個を使用し、データの新しい部分を蓄積する前に残りを使用する必要があります。したがって、フィルターは、その主な機能に加えて、入力データの中間ストレージのための補助アクションを提供する必要があります。メディア ストリーマとこの一般的な問題の解決策の開発者は、タプルを使用してこの問題を解決する特別なオブジェクト MSBufferizer (バッファラー) を開発しました。
バッファライザー (MSBufferizer)
これは、フィルター内に入力データを蓄積し、フィルター アルゴリズムを実行するのに十分な情報量が得られるとすぐに、処理のためにデータの送信を開始するオブジェクトです。バッファがデータを蓄積している間、フィルタはプロセッサの処理能力を使い果たさずにアイドル モードで動作します。ただし、バッファラーからの読み取り関数がゼロ以外の値を返すとすぐに、フィルターの process() 関数はバッファラーから必要なサイズのデータを取得し、データが使い果たされるまで処理を開始します。
まだ必要でないデータはタプルの最初の要素としてバッファ内に残り、入力データの後続のブロックがこの要素に添付されます。
バッファを記述する構造体は次のとおりです。
struct _MSBufferizer{
queue_t q; /* Очередь сообщений. */
int size; /* Суммарный размер данных находящихся в буферизаторе в данный момент. */
};
typedef struct _MSBufferizer MSBufferizer;
MSBufferizer を操作するための関数
新しいバッファインスタンスを作成します。
MSBufferizer * ms_bufferizer_new(void);
メモリが割り当てられ、初期化されます ms_bufferizer_init() そしてポインタが返されます。
初期化関数:
void ms_bufferizer_init(MSBufferizer *obj);
キューは初期化中です q、 分野 サイズ はゼロに設定されます。
メッセージを追加する:
void ms_bufferizer_put(MSBufferizer *obj, mblk_t *m);
メッセージ m がキューに追加されます。計算されたデータ ブロックのサイズが追加されます。 サイズ.
すべてのメッセージをリンク データ キューからバッファに転送する q:
void ms_bufferizer_put_from_queue(MSBufferizer *obj, MSQueue *q);
リンクからメッセージを転送する q バッファ内の処理は関数を使用して実行されます ms_bufferizer_put().
バッファからの読み取り:
int ms_bufferizer_read(MSBufferizer *obj, uint8_t *data, int datalen);
バッファに蓄積されたデータのサイズが要求されたサイズより小さい場合 (データレン)、関数はゼロを返し、データはデータにコピーされません。それ以外の場合は、バッファ内にあるタプルからのデータの順次コピーが実行されます。コピー後、タプルは削除され、メモリが解放されます。 datalen バイトがコピーされた時点でコピーは終了します。データ ブロックの途中でスペースが足りなくなった場合、このメッセージでは、データ ブロックはコピーされていない残りの部分に減らされます。次回電話をかけるときは、この時点からコピーが続行されます。
現在バッファ内で利用可能なデータ量を読み取る:
int ms_bufferizer_get_avail(MSBufferizer *obj);
フィールドを返します サイズ バッファラー。
バッファ内のデータの一部を破棄します。
void ms_bufferizer_skip_bytes(MSBufferizer *obj, int bytes);
指定されたバイト数のデータが取得され、破棄されます。最も古いデータは破棄されます。
バッファ内のすべてのメッセージを削除します。
void ms_bufferizer_flush(MSBufferizer *obj);
データカウンターはゼロにリセットされます。
バッファ内のすべてのメッセージを削除します。
void ms_bufferizer_uninit(MSBufferizer *obj);
カウンタはリセットされません。
バッファを削除してメモリを解放します。
void ms_bufferizer_destroy(MSBufferizer *obj);
バッファラーの使用例は、いくつかのメディア ストリーマー フィルターのソース コードにあります。たとえば、MS_L16_ENC フィルターでは、サンプル内のバイトをネットワーク順からホスト順に再配置します。
次の記事では、ティッカーの負荷を見積もる問題と、メディア ストリーマーの過度のコンピューティング負荷に対処する方法について説明します。
出所: habr.com