全部乾物ですよ~
序文
前回の記事では Tcp サーバーを実装しましたが、マルチプロセス、マルチスレッドの効果を実証するために、サーバーとクライアント間の通信を無限ループとして記述し、あるユーザーがサーバーと通信しない限り、他のユーザーはサーバーと通信できないようにしました。
1. TCPサーバーマルチプロセス版
前回の記事で書いたserviceIOインターフェースは無限ループになっていて複数のユーザーが通信できないので、マルチプロセスを使ってこの問題を解決するにはどうすればよいでしょうか?実際、これは非常に簡単です。子プロセスを作成するだけで済みます。子プロセスは親プロセスのファイル記述子を継承するため、同じ sock ファイルをポイントできる必要があります。その後、子プロセスに serviceIO を呼び出させます。親プロセスはブロックして子プロセスを待ちます。
pid_t id = fork();
if (id == 0)
{
// 子进程
// 子进程不需要listensock,既然不需要我们就关闭
close(_listensock);
serviceID(sock);
close(sock);
exit(0);
}
// 父进程
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
cout << "waitsuccess: " << ret << endl;
}
まず、子プロセスも親プロセスの listensock ファイル記述子を継承しますが、このファイル記述子は親プロセスが監視するために使用します。子プロセスの仕事はクライアントとの通信のみなので、未使用のファイル記述子を閉じる必要があります。これは、外出前にタイヤやその他の装備が良好かどうかを毎回チェックする老ドライバーのようなものです。クライアントが終了すると、子プロセスは sock ファイル記述子を閉じてから子プロセスを終了します。これは、親プロセスが子プロセスを待機するようにしているためです。そのため、子プロセスが終了して孤立プロセスになることを恐れる必要はありませんが、このようなコードは常に間違っていると感じます。問題はありますか? それは正しい!親プロセスを子プロセスを待たせると、このコードはシリアルのままではないでしょうか? マルチユーザー通信を実現したい場合、親プロセスは複数の子プロセスを作成する必要があります。startが無限ループになるため、親プロセスは毎回子プロセスを作成してユーザーと通信することになります。親プロセスを子プロセスを待たせておけば、これまでと同じになります。子プロセスが1人のユーザーの通信を処理し終わって初めて、別のユーザーがサーバーと通信できるようになります。では、この問題を解決するにはどうすればよいでしょうか? 以下のコードを見てください。
pid_t id = fork();
if (id == 0)
{
// 子进程
// 子进程不需要listensock,既然不需要我们就关闭
close(_listensock);
// 子进程创建孙进程,如果成功将子进程关闭让孙进程处理任务,由于孙进程的父进程退出所以变成孤儿进程最终会被
// 操作系统领养,不需要进程等待
if (fork() > 0)
{
exit(0);
}
serviceID(sock);
close(sock);
exit(0);
}
// 父进程
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
cout << "waitsuccess: " << ret << endl;
}
子プロセスに未使用のファイル記述子を閉じさせた後、すぐに子プロセスに別の子プロセスを作成させます。作成が成功したら、元の子プロセスを終了させ、元の子プロセスの子プロセスにクライアントと通信するコードを実行させます。この利点は、元の子プロセスの子プロセスを待つ必要がないことです。元の子プロセスの子プロセスが終了したため、このプロセスは孤立プロセスになります。プロセスが孤立プロセスになると、そのプロセスはオペレーティング システムに採用されることは誰もが知っています。そのため、この孤立プロセスの終了について心配する必要はありません。特定のクライアントが終了した場合にのみ、孫プロセスが終了し、オペレーティング システムに採用されます。これで今の問題は解決します。それで、実行して見てみましょう:
今回はどのクライアントが終了しても他のユーザーに影響を与えず、問題がないことがわかります。上記のログの印刷は変更されています。これについては後ほど説明します。ファイル記述子が 4 と 5 であることがわかります。次に、ファイル記述子が正しく閉じられているかどうかを確認するために再起動してみましょう。
再起動後もファイル記述子が 4 と 5 のままであることがわかります。これは、以前にファイル記述子を正しく閉じたことを意味します。他の番号から開始する場合は、以前のファイル記述子がリークしたはずです。
もちろん、上記の子プロセスを頻繁に作成するのは明らかに良くないので、2 番目の方法があります: signalignore version:
void start()
{
//忽略17号信号
signal(SIGCHLD,SIG_IGN);
for (;;)
{
//4.server获取新链接 未来真正使用的是accept返回的文件描述符
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock是和client通信的fd
int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
//accept失败也无所谓,继续让accept去获取新链接
if (sock<0)
{
logMessage(ERROR,"accept error,next");
continue;
}
logMessage(NORMAL,"accept a new link success");
cout<<"sock: "<<sock<<endl;
pid_t id = fork();
if (id==0)
{
close(_listensock);
serviceID(sock);
close(sock);
exit(0);
}
//父进程
//已经对17号信号做忽略,父进程不用等待子进程,子进程会自动退出,但是需要父进程关闭文件描述符
close(sock);
}
}
まず、17番の信号SIGCHLDを無視しますが、17番の信号とは何でしょうか?子プロセスが終了するとき、親プロセスに終了することを知らせるシグナル17番を送り、このシグナルを無視すると、親プロセスは子プロセスを待たずに、子プロセスに不要なファイル記述子を閉じさせて、クライアントと通信するコードを実行させます。クライアントが終了しない場合、子プロセスはファイル記述子を閉じないので、親プロセスがファイル記述子を閉じても子プロセスには影響しないのでしょうか?実際にはそうではありません。子のプロセスと親プロセスの両方がファイル記述子を指し、このファイル記述子の参照カウントは2であり、参照カウントが0に縮小された場合にのみ、親プロセスによってファイル記述子を閉じない場合、子プロセスは参照カウントを1に減らすだけで、ファイル記述書は閉じません。
ファイルディスクリプタがすべて 4 であることがわかりますが、これは CPU の動作速度が速すぎるためで、accept インターフェイスで 4 番のファイルディスクリプタを申請し、子プロセスを作成し (子プロセスは 4 番のディスクリプタを継承します)、親プロセスが直接ソケットを閉じています。
2、Tcp サーバーのマルチスレッド バージョン
プロセス作成の負荷が非常に大きいため、マルチスレッドを利用してユーザーにサービスを提供しています。マルチスレッドの原理はマルチプロセスの原理と同じです。スレッドを作成し、この新しいスレッドにクライアントと通信するコードを実行させるだけで済みますが、クライアントとの通信にはファイル記述子とserviceIOメソッドを使用する必要があり、serviceIOはメンバー関数です。マルチスレッド実行のコールバック関数は静的である必要があるため、 this ポインタとソケットを格納するクラスを作成し、次に静的メンバー関数を作成して、関数内でコールバック メソッドを呼び出すことができます。
class TcpServer;
struct ThreadData
{
ThreadData(TcpServer* self,int sock)
:_self(self)
,_sock(sock)
{
}
TcpServer* _self;
int _sock;
};
static void* threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData*>(args);
td->_self->serviceID(td->_sock);
close(td->_sock);
delete td;
return nullptr;
}
上の図からわかるように、最初にスレッドを作成し、次に struct ThreadData クラスを実装しました。クラス内のメンバーには、Tcpserver と sock ファイル記述子へのポインターがあります。次に、ThreadData へのポインターを作成し、this と sock で初期化し、スレッドの作成時にこのポインターをコールバック関数に渡します。コールバック関数に入ると、スレッドは最初に分離されます。一度分離されると、新しいスレッドを待つ必要はありません (新しいスレッドを待つと、再びシリアルになります)。ファイル記述子を閉じてから、null へのポインタを解放します。ここで、マルチスレッドではメインスレッドがファイル記述子を閉じる必要がないのはなぜですか? マルチプロセスとは異なり、すべてのスレッドがプロセスのファイル記述子を共有するため、子プロセスは親プロセスのファイル記述子をポイントします。一度ポイントされると、ファイル記述子の参照カウントは +1 されますが、マルチスレッドから見えるファイル記述子はプロセス内で開かれたファイル記述子であるため、スレッドにファイル記述子をクローズさせます。つまり、プロセス内のファイル記述子もクローズされます。以下で実行してみましょう (実行前に必ず makefile でサーバーに -pthread オプションを追加してください。追加しないとコンパイルされません)。
ファイル記述子が正しく閉じられていることを確認し、サーバーを再度開きます。
画面をクリアした後、再度開いたところ、ファイル記述子がまだ 4 から始まっていることがわかりました。これは、ファイル記述子が漏洩していないことを意味します。
3. TCP サーバーのスレッド プールのバージョン
以前に書いたスレッド プールを思い出してください。スレッド プールの利点は、一度に複数のスレッドを作成してタスクを実行できることですが、今日のタスクはクライアントと通信することなので、スレッド プールの以前のコードを使用します。
#include <pthread.h>
#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include <mutex>
#include "lockguard.hpp"
#include "log.hpp"
using namespace std;
const int gnum = 5;
template <class T>
class ThreadPool
{
public:
static ThreadPool<T>* getInstance()
{
if (_tp == nullptr)
{
_mtx.lock();
if (_tp == nullptr)
{
_tp = new ThreadPool<T>();
}
_mtx.unlock();
}
return _tp;
}
static void* handerTask(void* args)
{
ThreadPool<T>* threadpool = static_cast<ThreadPool<T>*>(args);
while (true)
{
T t;
{
// threadpool->lockQueue();
LockGuard lock(threadpool->getMutex());
while (threadpool->IsQueueEmpty())
{
threadpool->condwaitQueue();
}
// 获取任务队列中的任务
t = threadpool->popQueue();
}
t();
}
return nullptr;
}
void Push(const T& in)
{
LockGuard lock(&_mutex);
//pthread_mutex_lock(&_mutex);
_task_queue.push(in);
pthread_cond_signal(&_cond);
//pthread_mutex_unlock(&_mutex);
}
void start()
{
for (const auto& t: _threads)
{
pthread_create(t,nullptr,handerTask,this);
logMessage(DEBUG,"线程%p创建成功",t);
}
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for (auto& t: _threads)
{
delete t;
}
}
public:
void lockQueue()
{
pthread_mutex_lock(&_mutex);
}
void unlockQueue()
{
pthread_mutex_unlock(&_mutex);
}
void condwaitQueue()
{
pthread_cond_wait(&_cond,&_mutex);
}
bool IsQueueEmpty()
{
return _task_queue.empty();
}
T popQueue()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
pthread_mutex_t* getMutex()
{
return &_mutex;
}
private:
ThreadPool(const int &num = gnum)
: _num(num)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_cond,nullptr);
for (int i = 0;i<_num;i++)
{
_threads.push_back(new pthread_t);
}
}
ThreadPool(const ThreadPool<T>& tp) = delete;
ThreadPool<T>& operator=(const ThreadPool<T>& tp) = delete;
int _num;
vector<pthread_t *> _threads;
queue<T> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool<T>* _tp;
static mutex _mtx;
};
template <class T>
ThreadPool<T>* ThreadPool<T>::_tp = nullptr;
template <class T>
mutex ThreadPool<T>::_mtx;
シングルトン モードのスレッド プールに加えて、ロックもあります。
#include <iostream>
#include <pthread.h>
class Mutex //自己不维护锁,有外部传入
{
public:
Mutex(pthread_mutex_t *mutex)
:_pmutex(mutex)
{
}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{
}
private:
pthread_mutex_t *_pmutex;
};
class LockGuard //自己不维护锁,由外部传入
{
public:
LockGuard(pthread_mutex_t *mutex)
:_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
次に、スレッド プールのバージョンの記述を開始します。最初にスレッド プールを開始する必要があります。開始インターフェイスで通信するため、開始インターフェイスでスレッド プールを開始します。
もちろん、まだタスクを記述する必要がありますが、このタスクも非常に単純で、以前に説明したコードです。
void start()
{
// 4.线程池初始化
ThreadPool<Task>::getInstance()->start();
logMessage(NORMAL,"ThreadPool init success");
for (;;)
{
//4.server获取新链接 未来真正使用的是accept返回的文件描述符
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock是和client通信的fd
int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
//accept失败也无所谓,继续让accept去获取新链接
if (sock<0)
{
logMessage(ERROR,"accept error,next");
continue;
}
logMessage(NORMAL,"accept a new link success,get new sock: %d",sock);
//5.用sock和客户端通信,面向字节流的,后续全部都是文件操作
//serviceID(sock);
//对于一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致文件描述符泄漏
//close(sock);
// 4.线程池版本
ThreadPool<Task>::getInstance()->Push(Task(sock,serviceID));
}
}
前回スレッド プールをシングルトン モードに変更したため、シングルトンとして起動し、起動後に複数のスレッドを作成して、タスクを作成しました。
#include <iostream>
class Task
{
using func_t=std::function<void(int)>;
public:
Task()
{
}
Task(int sock,func_t func)
:_sock(sock)
,_callback(func)
{
}
void operator()()
{
_callback(_sock);
}
~Task()
{
}
private:
int _sock;
func_t _callback;
};
このタスクは、スレッドによって呼び出されるコールバック メソッドとファイル記述子を知る必要があるだけで、その後、ファンクターのオーバーロード () シンボルを記述します。
タスクでは、匿名オブジェクトを直接構築し、タスクをプッシュします。次の図に示すように、スレッド プールでは、serviceIO 関数がファンクターを通じて直接呼び出されます。
次にスレッド プールを実行します。
上記は Tcp サーバーの 3 つのバージョンです。先ほど示したデータの印刷など、さらに興味深い機能をログに追加する方法を説明しましょう。
4. Tcpサーバーログの改善
元のログコードに基づいて可変パラメータリストを追加しますが、可変パラメータを抽出するにはどうすればよいでしょうか?
可変パラメータを使用するには、まず va_last とは何か、次に va_last の使用方法を知る必要があり、va_last を使用するには、va_start()、va_arg()、va_end() の 3 つのマクロを使用する必要があります。
上の図に示すように、va_last は上記のパラメータ 3.14、10、および 'c' を指す必要があります。たとえば、今、va_last は最初のパラメータ 3.14 を指します。2 番目のパラメータ 10 を指すには、va_last ポインタを特定のバイト数だけオフセットするだけで済みます。では、va_last が最初のパラメータを指すようにするにはどうすればよいでしょうか? va_start(start) を直接使用して、va_last が最初のパラメータを指すようにします。va_arg() は、ポインタを特定の型で後方に移動させることができます。たとえば、3.14 から 10 を指したいだけの場合は、va_arg(start, int) だけが必要で、va_end() は開始ポインタを nullptr にします。
vsprintf インターフェイスを使用して次のことを示してみましょう。
void logMessage(int level,const char* format, ...)
{
//[日志等级][时间戳/时间][pid][message]
//std::cout<<message<<std::endl;
char logprefix[1024]; //日志前缀
snprintf(logprefix,sizeof(logprefix),"[%s][%ld][pid:%d]",to_levelstr(level),(long int)time(nullptr),getpid());
char logcontent[1024]; //日志内容
va_list arg;
va_start(arg,format);
vsprintf(logcontent,format,arg);
std::cout<<logprefix<<logcontent<<std::endl;
}
まず第一に、ログは固定形式です。書き込むログのプレフィックスは [ログ レベル][][][] でなければならず、ログの内容はログに表示される文字列であるため、2 つのバッファが必要です。プレフィックスはプレフィックスを表し、コンテンツはログの内容を表します。プレフィックスでは、レベル、時刻、pid を出力する必要があり、メッセージはログの内容です。va_last を定義する必要があり、arg ポインタが format の位置を指すようにします。vsprintf はパラメータ内の文字列とバッファに出力されるパラメータを読み取ることができ、次に 2 つのバッファを 1 つの文字列に結合して、ログ内のパラメータを使用して出力を完了します。たとえば、上記のデモでは、ログ内に作成されたファイル記述子の数を直接出力します。これは変数を使用することで実現されます。パラメータ。
5. Tcpサーバーをデーモン化する
現在作成している TCP サーバーは、xshell クライアントの影響を受けます。xshell クライアントを終了すると、サーバーも終了します。実際、サーバーは影響を受けません。まず、Linux のフォアグラウンドとバックグラウンドの原理について説明し、デーモン プロセスを実現しましょう。
上の図に示すように、まず、xshell にログインすると、Linux はフォアグラウンド プロセスと複数のバックグラウンド プロセスを含むセッションを提供します。コマンドを入力するコマンド ラインは bash で、バックグラウンド プロセスを表示するコマンドは jobs です。
たとえば、Sleep 10000 タスクを作成すると、シリアル番号 1 が与えられます。これは、これがジョブ No. 1 であることを意味します。また、さらにいくつかを追加することもできます。
プログラムの後に & 記号を追加すると、プロセスがバックグラウンドに置かれることになります。
まず、作成したスリープの ppid がすべて 29324 であることがわかります。コマンド ラインで開始したため、親プロセスはすべて bash であり、次に PGID を観察します。I と一緒に作成したスリープ プロセスは、同じ PGID を持っています。同じ PGID は、それらが同じプロセス グループに属していることを意味し、先ほどの 2 番目と 3 番目のジョブと同様に、同じプロセス グループがジョブを完了する必要があり、同じ PGID を持つ最初のプロセスがリーダーですこのプロセスグループの。SID はセッション ID を表し、セッション ID はこれらのプロセスがすべて 1 つのセッション内にあることも意味します。
このとき、誰もがこの現象を確認できるように、フロントデスクに No.1 を置きました。
fg コマンドは、タスクをフォアグラウンドに移動することを表します。スリープ タスクをフォアグラウンドに配置した後、bash が機能しないことがわかりました。これにより、次の図に示すように、各セッションがフォアグラウンド タスクを 1 つだけ持つことができることが確認されます。
では、bash を元に戻すにはどうすればよいでしょうか? ctrl + z、ctrl + z だけでタスクを一時停止できます。タスクが一時停止されると、バックグラウンドに置かれます。
一時停止後にプロセスを実行し続けるにはどうすればよいですか? bg コマンドを使用します。
上記の原則を理解した後、デーモン プロセスを実装できます。
先ほどのデモンストレーションから、タスクが一時停止されるとバックグラウンドに切り替わることがわかります。サーバーがフォアグラウンドであろうとバックグラウンドであろうと、誰かが xshell にログインすると、bash はデフォルトでフォアグラウンド プロセスに切り替わります。このとき、サーバーはユーザーのログインによって影響を受ける可能性があります。また、プロセスがそれ自体でセッションを形成し、私たちが独自のプロセス グループのリーダーである場合、次の図に示すように、私たちが言ったことの影響を受けません。
デーモンの実装を開始しましょう。
上記のインターフェイスを覚えておいてください。このインターフェイスは、プロセス グループのリーダーではないプロセスを独立したセッションに変えることができます。注意: プロセス グループのリーダーであってはなりません。通常のチーム メンバーのみにすることができます。後ほど、チーム リーダーをチーム リーダー以外にする方法が提供されます。
デーモン プロセスを実装するには 3 つの手順があります。
1. 呼び出しプロセスに異常なシグナルを無視させる
たとえば、私たちのサーバーでは、クライアントがファイル記述子を閉じていて、サーバーがまだファイル記述子に書き込んでいる場合、オペレーティング システムはこの時点でプロセスに SIGPIP シグナルを送信します (パイプラインへの書き込み異常を示します)。これは、オペレーティング システムの影響でプロセスを終了させることができないため、このシグナルを無視します。
2. 自分自身をこのプロセス (setsid()) のリーダーにしないでください。
この手順は実際には非常に簡単で、サブプロセスを作成するだけでよく、作成が成功すると元のプロセスが終了します。原則として、自分がグループリーダーである場合、作成された子プロセスはグループメンバーでなければなりません。元のグループリーダーだったプロセスが終了すると、新しいグループリーダーは元のプロセスグループリーダーの次のプロセスになります。このとき、作成した子プロセスにsetid()を行うと、このプロセス自体がプロセスグループになり、PID、PGID、SIDは同じになります。
3. デーモン プロセスは端末から切り離されているため、xshell を閉じても、リモート サーバーがシャットダウンしていない限り、kill -9 を使用してプロセスを強制終了しない限り、デーモン プロセスは実行され続けます。デーモン プロセスは端末から切り離されているため、デフォルトで開かれている 3 つのファイル記述子を閉じる必要があります。そのため、このパスにリダイレクトできます。
4. (オプション) プロセスはデフォルトで cwd コマンドを開き、プロセスの現在のパスを記録します。これにより、パスを指定しないときにデフォルトで作成されたファイルが現在のパスにある理由も証明でき、実際にこのパスに変更を加えることができます。たとえば、デーモン プロセスは現在のパスに配置する必要はありませんが、他のパスに配置することもできます。
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char* currPath = nullptr)
{
//1.让调用进程忽略掉异常的信号
signal(SIGPIPE,SIG_IGN);
//2.如何让自己不是组长,setsid
if (fork()>0)
{
exit(0);
}
//只剩子进程
pid_t n = setsid();
assert(n != -1);
//3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
int fd = open(DEV,O_RDWR);
if (fd>0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
else
{
close(0);
close(1);
close(2);
}
//4.可选:进程执行路径发生更改
if (currPath) chdir(currPath);
}
dev が正常に開かれた場合はリダイレクトされ、失敗した場合は 0、1、2 のファイル記述子が閉じられます。リダイレクト関数について述べたように、最初のパラメータは古く、2 番目のパラメータは新しいので、0、1、2 記述子を dev/null にリダイレクトしたいので、dev/null が配置されているファイル記述子が古いです。この言い訳は非常に複雑であるため、当時私たちは次のように言いました。最初のパラメータはリダイレクトの宛先であることを覚えておいてください。リダイレクトが完了したら、ファイル記述子の漏洩を防ぐために、前のファイル記述子を閉じます。
chdir は、デフォルトのパスを変更するためのインターフェイスです。実演してみましょう:
最初にヘッダー ファイルをインクルードします。サーバーが初期化された後、サーバーをデーモン プロセスに変えます。次に、それを実行します。
上の図に示すように、PID と PGID は SID と同じであり、デーモン化されているので、サーバーにメッセージを送信してみましょう。
メッセージがエコーされている限り、サーバーが稼働していることを意味するため、問題がないことがわかります。
サーバーがデーモン化されると、デフォルトで開かれたファイル記述子に元々書き込まれていたログ メッセージは dev/null にリダイレクトされるため、サーバーの初期化のログ情報のみが表示され、サーバーが開始されると表示されなくなります。次に、ログを変更し、ログ情報を 2 つのファイルに直接出力します。
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG_ERR "log.error"
#define LOG_NORMAL "log.txt"
const char* to_levelstr(int level)
{
switch(level)
{
case DEBUG:return "DEBUG";
case NORMAL:return "NORMAL";
case WARNING:return " WARNING";
case ERROR:return "ERROR";
case FATAL:return "FATAL";
default : return nullptr;
}
}
void logMessage(int level,const char* format, ...)
{
//[日志等级][时间戳/时间][pid][message]
char logprefix[1024]; //日志前缀
snprintf(logprefix,sizeof(logprefix),"[%s][%ld][pid:%d]",to_levelstr(level),(long int)time(nullptr),getpid());
char logcontent[1024]; //日志内容
va_list arg;
va_start(arg,format);
vsprintf(logcontent,format,arg);
//文件版
FILE* log = fopen(LOG_NORMAL,"a");
FILE* err = fopen(LOG_ERR,"a");
if (log!=nullptr && err!=nullptr)
{
FILE* tep = nullptr;
if (level==DEBUG || level==NORMAL || level==WARNING)
{
tep = log;
}
else
{
tep = err;
}
if (tep)
{
fprintf(tep,"%s%s\n",logprefix,logcontent);
}
fclose(log);
fclose(err);
}
}
まずログを分類し、ログ ストレージ レベル 0、1、2、ログ ストレージ レベル 4、5 を読み取り、これら 2 つのファイルを開きます。すべて開いている場合は、ファイル ポインタを定義します。ログ レベルが 0、1、2 の場合、新しいファイル ポインタはファイル log.txt を指します。どのファイルに書き込むかを確認した後、このファイルのように前のログ プレフィックス + ログの内容を書き込みます。書き込み後、これら 2 つのファイルをオフにします。
結果を見てみましょう:
新しいユーザーがログインしている限り、ログはアップロードされるため、問題がないことがわかります。
要約する
この記事で最も重要なことは、ネットワークの知識とシステムの知識の統合です。たとえば、マルチプロセス版とマルチスレッド版では、マルチプロセスではファイル記述子を 2 回閉じる必要がありますが、マルチスレッドでは 1 回だけ閉じる必要があります。これらの概念を理解するには、プロセスとスレッドの概念を知る必要があり、ネットワークを学ぶことはシステムの基礎スキルを試すことになります。