記事のナビゲーション
Redisソースコードシリーズの本来の目的は、Redisをよりよく理解し、Redisをよりよく理解するのに役立つことですが、それを理解する方法は十分ではありません。この記事に従って環境を構築し、フォローアップしてソースコードを自分で読むことをお勧めします。私と一緒に読んでください。私は数年前にCを使用したので、いくつかのエラーは避けられないので、読者が指摘できることを願っています。
Cao Gong氏は、Redisソースコード(1)-redisデバッグ環境のビルド、clionの使用、Javaのデバッグと同じ効果を達成すると述べた
補足知識
プロジェクトの構造と入り口
大学のおもちゃに加えて、実際のプロジェクトは、プロジェクトを形成する多数のソースコードファイルで構成されています。Javaでは、Javaファイルが他のJavaファイルの関数、型、変数などを使用する場合、インポートステートメントを使用してインポートする必要があります。C言語でも同様で、C言語で他のファイルの関数をインクルードするには、include文を使用する必要があります。
たとえば、redisのメインエントリでは、redis.cファイルに次のステートメントの束が含まれています。
#include "redis.h"
#include "cluster.h"
#include "slowlog.h"
#include "bio.h"
#include <time.h>
#include <signal.h>
その中で、<time.h>のように、<で始まるものは標準ライブラリのヘッダーファイルであり、システムによって指定されたパスの下で検索され、jdk
公式クラスと比較できます。「bio.h」は「」で囲まれています。プロジェクトでカスタマイズされます。
たとえば、time.hは、Linuxの次のパスで見つかりました。
[root@mini1 src]# locate time.h
/usr/include/time.h
その他の関連知識については、以下を参照してください。
https://www.runoob.com/cprogramming/c-header-files.html
ヘッダーファイルについての私の理解
一般的に言えば、ビジネスロジックメソッドは.cファイルに記述します。これらのメソッドの中には、Javaクラスのプライベートメソッドと同様に、このファイル内でのみ使用できるものと、他の外部ソースコードファイルで使用する必要があります。これらのメソッドを外部で使用するにはどうすればよいですか?
ヘッダーファイルメカニズムを介して主要な高級言語のインターフェイスとして理解できます。Javaではクラスが定義されます。メソッドを直接publicに設定できますが、他のクラスに直接アクセスできますが、通常のビジネス開発では、通常、実装クラスに直接アクセスするのではなく、実装クラスを介して実装します。適切な実装クラスでは、インターフェースで定義されていないメソッドをパブリック権限に設定しないでください。
たとえば、ヘッダーファイルといえば、test.c
次のようなソースファイルがあります。
long long ustime(void) {
struct timeval tv;
long long ust;
gettimeofday(&tv, NULL);
ust = ((long long)tv.tv_sec)*1000000;
ust += tv.tv_usec;
return ust;
}
/* Return the UNIX time in milliseconds */
// 返回毫秒格式的 UNIX 时间
// 1 秒 = 1 000 毫秒
long long mstime(void) {
return ustime()/1000;
}
このファイルでは2つのメソッドが定義されていますが、mstime(void)
メソッドを外部に公開するだけでよいとすると、ヘッダーファイルtest.h
は次のようになります。
long long mstime(void);
この場合、私たちの他の方法であるustimeは外の世界からは見えません。
つまり、ヘッダーファイルは、外界に公開される実装クラスのインターフェースとして理解できます。私の例えは不適切だと思われるかもしれません。なぜcファイルを実装クラスとして呼び出すのですか?実際、Huaweiにいたときは、c ++を使用していました。 C言語を書くという、オブジェクト指向の考え方。
私はインターネットで記事を見ました、それをここに引用してください(https://zhuanlan.zhihu.com/p/57882822):
Redisとは対照的に、彼は純粋なCコーディングですが、オブジェクト指向のアイデアを取り入れています。上記の見方とは逆に、「C ++での設計とCでのコード」と表現できます。もちろん、この記事の目的は言語紛争を引き起こすことではありません。さまざまな言語にはそれぞれ長所と短所があります。オープンソースプロジェクトの言語選択は、主にプロジェクトの作成者の個人的な経験と主観的な希望によるものです。
ただし、C言語のヘッダーファイルはJava言語のインターフェースとは異なり、Javaでは、インターフェースと実装クラスが最終的に独立したクラスファイルにコンパイルされます。
C言語では、実装クラスをコンパイルする前に、前処理プロセスがあります。前処理プロセスは、includeステートメントをインクルードされたヘッダーファイルのコンテンツで直接置き換えることです。たとえば、新人チュートリアルの例を例にとります:
header.h
char *test (void);
以下ではprogram.c
、試験方法としては、上記がheader.hの使用が必要とされる必要です。
int x;
#include "header.h"
int main (void)
{
puts (test ());
}
前処理後(つまり、単純な置き換え)の効果は次のとおりです。
int x;
char *test (void);
int main (void)
{
puts (test ());
}
次のコマンドを使用して、このプロセスを示すことができます。
[root@mini1 test]# gcc -E program.c
int x;
# 1 "header.h" 1
char *test (void);
# 3 "program.c" 2
int main (void)
{
puts (test ());
}
上記からわかるように、置き換えられていますが、2回含めるとどうなりますか?
[root@mini1 test]# gcc -E program.c
int x;
# 1 "header.h" 1
char *test (void);
# 3 "program.c" 2
# 1 "header.h" 1
char *test (void);
# 4 "program.c" 2
int main (void)
{
puts (test ());
}
このヘッダーのコンテンツが2回表示され、繰り返されていることがわかります。ただし、上記の場合、エラーは報告されませんが、メソッドは2回定義されています。
ヘッダーファイルにifndefが必要な理由
ヘッダーファイルを見ると、redis.hなどにある次のステートメントが見つかります。
#ifndef __REDIS_H
#define __REDIS_H
#include "fmacros.h"
#include "config.h"
...
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
...
#endif
ご覧のとおり、最初に文があります。
#ifndef __REDIS_H
#define __REDIS_H
最後に文があります:
#endif
これは、次の問題を解決するためのものです。
ヘッダーファイルが繰り返しインポートされる場合(間接的または直接、2回含まれる)、これが追加されない場合、ヘッダーファイルのコンテンツが2回インポートされます。これを追加した後、含まれていても2回、プログラムの実行中、最初に、__REDIS_H
このマクロが定義されていないことがわかり、それを定義します。プログラムが2番目のインクルードのコンテンツに遭遇すると、__REDIS_H
このマクロが定義されていることがわかり、直接スキップされます。同じヘッダーファイルを使用すると、たとえ複数回インクルードされても、その内容は一度しか解析できません。
さらに、メソッド宣言のように、定義は複数回うまくいく場合がありますが、ヘッダーファイルに次の型定義がある場合:
typedef char my_char;
char *test (void);
同じヘッダーファイルを繰り返しインクルードすると、型定義が繰り返されます。ただし、centos 7.3.1611、gccバージョンで試したのは非常に奇妙ですgcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-16)
が、エラーは報告されませんでした。私の以前のC言語の知識は自宅では学んでいないようです。
インターネット上で重複するインクルードが一時的に見つかりませんでした。具体的な害は何ですか?インターネット上で2つの回答が見つかりました:
- グローバル変数はヘッダーファイルで定義されます。
- コンパイル時間の無駄
ただし、企業は通常、ヘッダーファイル内の変数の定義を禁止しているため、厳密に言えば、最初の答えは存在しません。
あなたが知っている問題があります、あなたは見ることができます:ヘッダーファイルを繰り返しインクルードすることの危険は何ですか?
HuaweiのC言語プログラミング仕様では、ヘッダーファイルの規定の一部
自分で検索できます:Huawei Technology Co.、Ltd. c言語プログラミング仕様
ここでは一部のみ傍受します。
规则1.6 禁止在头文件中定义变量。
说明: 在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。
规则1.7 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部
函数接口、变量。
说明:若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c
中通过#include <b.h>来使用foo。禁止通过在a.c中直接写extern int foo(int input);来使用foo,
后面这种写法容易在foo改变时可能导致声明和定义不一致。
ここでの1.7も、私たちの理解と一致しています。ヘッダーファイルは、モジュールを実装する外部インターフェイスです。通常、次のコンテンツのみが許可されます:
- タイプ定義
- マクロ定義
- 関数宣言(実装を含まない)
- 変数宣言(定義ではない)
この最後のポイントを追加したいと思います。ヘッダーファイルで変数を定義することを禁止しているため、変数はcファイルで定義されています。たとえば、redis.cでは、グローバル変数が定義されています。
/* Global vars */
struct redisServer server; /* server global state */
このような重要なグローバル変数は、基本的にredis-serverのインスタンスのすべての状態値を維持し、独自のredis.cでのみ使用されますが、これは不可能です。他のファイルでの使用方法は、次のステートメントをredis.hヘッダーファイルで作成する必要があります。
/*-----------------------------------------------------------------------------
* Extern declarations
*----------------------------------------------------------------------------*/
extern struct redisServer server;
タイプ定義について
一般に、高水準言語のクラスと同様に、構造体を使用して構造体を定義します。
たとえば、redisの文字列は通常、データ構造sdsを使用して格納されます。構造の定義は次のとおりです。
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
さらに、C言語では、typedefは型エイリアスを定義するために広く使用されます。
詳細については、このチュートリアルを参照してください。
https://www.runoob.com/cprogramming/c-typedef.html
ポインタについて
基本的な知識:https : //www.runoob.com/cprogramming/c-pointers.html
ここで、ポインタの理解についてお話します。ポインタは通常、メモリアドレスを指します。最初にこのポインタのタイプを無視できます。実際、ポインタが指すアドレスのデータタイプを気にしない場合は、直接次のように定義できます。 void * ptr。
このポインターはアドレスAを指すと見なされます。上記がcharとして格納されていると考える場合、char型は1つしか占有しないため、このポインターをvoid *からchar *型にキャストしてから、ポインターを逆参照できます。バイトなので、ポインタが指す位置から開始して、このバイトの現在の内容を取得し、それをcharとして解析するだけで、このアドレスでchar値を取得できます。
強制的にvoid *をint *に変換すると、それを逆参照すると、整数が4バイトを占めるため、現在のポインター位置から4バイトを取得し、それを整数に変換します。
一般に、ポインターを逆参照する場合、最初に、int *ポインターなどの現在のポインターのデータ型を確認し、次にintを指すと、逆参照に4バイトかかります。構造を指す場合は、構造体が占めるバイト数を計算し、対応するバイト数を取得して、構造体型の変数を逆参照します。
この部分では、これを見てみましょう:
https://www.runoob.com/cprogramming/c-data-types.html
https://www.runoob.com/cprogramming/c-pointer-arithmetic.html
redis起動プロセス中の構成アイテムの初期化
前のセクションで多くのことを述べましたが、この講義では、redisの起動プロセス全体について話すだけでは不十分です。2つの講義があるかもしれません。この講義では、最初にその一部について説明します。
起動エントリはredis.cのmainメソッドにあります。私のコードを使用してデバッグ環境を構築する場合は、redis-serverを直接起動できます。
int main(int argc, char **argv) {
struct timeval tv;
/**
* 1 设置时区
*/
setlocale(LC_COLLATE,"");
/**
*2
*/
zmalloc_enable_thread_safeness();
// 3
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
// 4
srand(time(NULL)^getpid());
// 5
gettimeofday(&tv,NULL);
// 6
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
// 检查服务器是否以 Sentinel 模式启动
server.sentinel_mode = checkForSentinelMode(argc,argv);
// 7 初始化服务器
initServerConfig();
-
1ヶ所、タイムゾーン設定
-
2か所、メモリ割り当てのスレッド数を設定します。ここでは1に設定されます
-
3つの場所で、oomが発生したときに関数ポインターを設定します。関数ポインターは、ラムダ式でjava 8と同様の関数を指し、ストリームへのメソッド参照をスローします;関数ポインターは、通常、oomのときに呼び出されます。これは、Javaのテンプレートデザインパターンまたは戦略パターンに似ています。
-
4か所、乱数の種を設定
-
5時、現在の時刻を取得し、に設定
tv
変数ここではtvのアドレスが渡されます。これは、Javaでオブジェクト参照を渡すのと同様に、C言語での一般的な使用方法です。その後、メソッド内で、オブジェクトの内部フィールドが変更されます。
-
6箇所、ハッシュ関数をシード
-
7時に、サーバーが初期化されます。
ここに7つのポイントがあります:
void initServerConfig() {
int j;
// 服务器状态
// 设置服务器的运行 ID
getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE);
// 设置默认配置文件路径
server.configfile = NULL;
// 设置默认服务器频率
server.hz = REDIS_DEFAULT_HZ;
// 为运行 ID 加上结尾字符
server.runid[REDIS_RUN_ID_SIZE] = '\0';
// 设置服务器的运行架构
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
// 设置默认服务器端口号
server.port = REDIS_SERVERPORT;
// tcp 全连接队列的长度
server.tcp_backlog = REDIS_TCP_BACKLOG;
// 绑定的地址的数量
server.bindaddr_count = 0;
// UNIX socket path
server.unixsocket = NULL;
server.unixsocketperm = REDIS_DEFAULT_UNIX_SOCKET_PERM;
// 绑定的 TCP socket file descriptors
server.ipfd_count = 0;
server.sofd = -1;
// redis可使用的redis db的数量
server.dbnum = REDIS_DEFAULT_DBNUM;
// redis 日志级别
server.verbosity = REDIS_DEFAULT_VERBOSITY;
// Client timeout in seconds,客户端最大空闲时间;超过这个时间的客户端,会被强制关闭
server.maxidletime = REDIS_MAXIDLETIME;
// Set SO_KEEPALIVE if non-zero. 如果设为非0,则开启tcp的SO_KEEPALIVE
server.tcpkeepalive = REDIS_DEFAULT_TCP_KEEPALIVE;
// 打开这个选项,会周期性地清理过期key
server.active_expire_enabled = 1;
// 客户端发来的请求中,查询缓存的最大值;比如一个set命令,value的大小就会和这个缓冲区大小比较,
// 如果大了,就根本放不进缓冲区
server.client_max_querybuf_len = REDIS_MAX_QUERYBUF_LEN;
// rdb保存参数,比如每60s保存,n个键被修改了保存,之类的
server.saveparams = NULL;
// 如果为1,表示服务器正在从磁盘载入数据: We are loading data from disk if true
server.loading = 0;
// 日志文件位置
server.logfile = zstrdup(REDIS_DEFAULT_LOGFILE);
// 开启syslog等机制
server.syslog_enabled = REDIS_DEFAULT_SYSLOG_ENABLED;
server.syslog_ident = zstrdup(REDIS_DEFAULT_SYSLOG_IDENT);
server.syslog_facility = LOG_LOCAL0;
// 后台运行
server.daemonize = REDIS_DEFAULT_DAEMONIZE;
// aof状态
server.aof_state = REDIS_AOF_OFF;
// aof的刷磁盘策略,默认每秒刷盘
server.aof_fsync = REDIS_DEFAULT_AOF_FSYNC;
// 正在rewrite时,不刷盘
server.aof_no_fsync_on_rewrite = REDIS_DEFAULT_AOF_NO_FSYNC_ON_REWRITE;
// Rewrite AOF if % growth is > M and...
server.aof_rewrite_perc = REDIS_AOF_REWRITE_PERC;
// the AOF file is at least N bytes. aof达到多大时,触发rewrite
server.aof_rewrite_min_size = REDIS_AOF_REWRITE_MIN_SIZE;
// 最后一次执行 BGREWRITEAOF 时, AOF 文件的大小
server.aof_rewrite_base_size = 0;
// Rewrite once BGSAVE terminates.开启该选项时,BGSAVE结束时,触发rewrite
server.aof_rewrite_scheduled = 0;
// 最近一次aof进行fsync的时间
server.aof_last_fsync = time(NULL);
// 最近一次aof重写,消耗的时间
server.aof_rewrite_time_last = -1;
// Current AOF rewrite start time.
server.aof_rewrite_time_start = -1;
// 最后一次执行 BGREWRITEAOF 的结果
server.aof_lastbgrewrite_status = REDIS_OK;
// 记录 AOF 的 fsync 操作被推迟了多少次
server.aof_delayed_fsync = 0;
// File descriptor of currently selected AOF file
server.aof_fd = -1;
// AOF 的当前目标数据库
server.aof_selected_db = -1; /* Make sure the first time will not match */
// UNIX time of postponed AOF flush
server.aof_flush_postponed_start = 0;
// fsync incrementally while rewriting? 重写过程中,增量触发fsync
server.aof_rewrite_incremental_fsync = REDIS_DEFAULT_AOF_REWRITE_INCREMENTAL_FSYNC;
// pid文件
server.pidfile = zstrdup(REDIS_DEFAULT_PID_FILE);
// rdb 文件名
server.rdb_filename = zstrdup(REDIS_DEFAULT_RDB_FILENAME);
// aof 文件名
server.aof_filename = zstrdup(REDIS_DEFAULT_AOF_FILENAME);
// 是否要密码
server.requirepass = NULL;
// 是否进行rdb压缩
server.rdb_compression = REDIS_DEFAULT_RDB_COMPRESSION;
// rdb checksum
server.rdb_checksum = REDIS_DEFAULT_RDB_CHECKSUM;
// bgsave失败,停止写入
server.stop_writes_on_bgsave_err = REDIS_DEFAULT_STOP_WRITES_ON_BGSAVE_ERROR;
// 在执行 serverCron() 时进行渐进式 rehash
server.activerehashing = REDIS_DEFAULT_ACTIVE_REHASHING;
server.notify_keyspace_events = 0;
// 支持的最大客户端数量
server.maxclients = REDIS_MAX_CLIENTS;
// bpop阻塞的客户端
server.bpop_blocked_clients = 0;
// 可以使用的最大内存
server.maxmemory = REDIS_DEFAULT_MAXMEMORY;
// 内存淘汰策略,也就是key的过期策略
server.maxmemory_policy = REDIS_DEFAULT_MAXMEMORY_POLICY;
server.maxmemory_samples = REDIS_DEFAULT_MAXMEMORY_SAMPLES;
// hash表的元素小于这个值时,使用ziplist 编码模式;以下几个类似
server.hash_max_ziplist_entries = REDIS_HASH_MAX_ZIPLIST_ENTRIES;
server.hash_max_ziplist_value = REDIS_HASH_MAX_ZIPLIST_VALUE;
server.list_max_ziplist_entries = REDIS_LIST_MAX_ZIPLIST_ENTRIES;
server.list_max_ziplist_value = REDIS_LIST_MAX_ZIPLIST_VALUE;
server.set_max_intset_entries = REDIS_SET_MAX_INTSET_ENTRIES;
server.zset_max_ziplist_entries = REDIS_ZSET_MAX_ZIPLIST_ENTRIES;
server.zset_max_ziplist_value = REDIS_ZSET_MAX_ZIPLIST_VALUE;
server.hll_sparse_max_bytes = REDIS_DEFAULT_HLL_SPARSE_MAX_BYTES;
// 该标识打开时,表示正在关闭服务器
server.shutdown_asap = 0;
// 复制相关
server.repl_ping_slave_period = REDIS_REPL_PING_SLAVE_PERIOD;
server.repl_timeout = REDIS_REPL_TIMEOUT;
server.repl_min_slaves_to_write = REDIS_DEFAULT_MIN_SLAVES_TO_WRITE;
server.repl_min_slaves_max_lag = REDIS_DEFAULT_MIN_SLAVES_MAX_LAG;
// cluster模式相关
server.cluster_enabled = 0;
server.cluster_node_timeout = REDIS_CLUSTER_DEFAULT_NODE_TIMEOUT;
server.cluster_migration_barrier = REDIS_CLUSTER_DEFAULT_MIGRATION_BARRIER;
server.cluster_configfile = zstrdup(REDIS_DEFAULT_CLUSTER_CONFIG_FILE);
// lua脚本
server.lua_caller = NULL;
server.lua_time_limit = REDIS_LUA_TIME_LIMIT;
server.lua_client = NULL;
server.lua_timedout = 0;
//
server.migrate_cached_sockets = dictCreate(&migrateCacheDictType,NULL);
server.loading_process_events_interval_bytes = (1024*1024*2);
// 初始化 LRU 时间
server.lruclock = getLRUClock();
// 初始化并设置保存条件
resetServerSaveParams();
// rdb的默认保存策略
appendServerSaveParams(60*60,1); /* save after 1 hour and 1 change */
appendServerSaveParams(300,100); /* save after 5 minutes and 100 changes */
appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */
/* Replication related */
// 初始化和复制相关的状态
server.masterauth = NULL;
server.masterhost = NULL;
server.masterport = 6379;
server.master = NULL;
server.cached_master = NULL;
server.repl_master_initial_offset = -1;
server.repl_state = REDIS_REPL_NONE;
server.repl_syncio_timeout = REDIS_REPL_SYNCIO_TIMEOUT;
server.repl_serve_stale_data = REDIS_DEFAULT_SLAVE_SERVE_STALE_DATA;
server.repl_slave_ro = REDIS_DEFAULT_SLAVE_READ_ONLY;
server.repl_down_since = 0; /* Never connected, repl is down since EVER. */
server.repl_disable_tcp_nodelay = REDIS_DEFAULT_REPL_DISABLE_TCP_NODELAY;
server.slave_priority = REDIS_DEFAULT_SLAVE_PRIORITY;
server.master_repl_offset = 0;
/* Replication partial resync backlog */
// 初始化 PSYNC 命令所使用的 backlog
server.repl_backlog = NULL;
server.repl_backlog_size = REDIS_DEFAULT_REPL_BACKLOG_SIZE;
server.repl_backlog_histlen = 0;
server.repl_backlog_idx = 0;
server.repl_backlog_off = 0;
server.repl_backlog_time_limit = REDIS_DEFAULT_REPL_BACKLOG_TIME_LIMIT;
server.repl_no_slaves_since = time(NULL);
/* Client output buffer limits */
// 设置客户端的输出缓冲区限制
for (j = 0; j < REDIS_CLIENT_LIMIT_NUM_CLASSES; j++)
server.client_obuf_limits[j] = clientBufferLimitsDefaults[j];
/* Double constants initialization */
// 初始化浮点常量
R_Zero = 0.0;
R_PosInf = 1.0/R_Zero;
R_NegInf = -1.0/R_Zero;
R_Nan = R_Zero/R_Zero;
// 初始化命令表,比如get、set、hset等各自的处理函数,放进一个hash表,方便后续处理请求
server.commands = dictCreate(&commandTableDictType,NULL);
server.orig_commands = dictCreate(&commandTableDictType,NULL);
populateCommandTable();
server.delCommand = lookupCommandByCString("del");
server.multiCommand = lookupCommandByCString("multi");
server.lpushCommand = lookupCommandByCString("lpush");
server.lpopCommand = lookupCommandByCString("lpop");
server.rpopCommand = lookupCommandByCString("rpop");
/* Slow log */
// 初始化慢查询日志
server.slowlog_log_slower_than = REDIS_SLOWLOG_LOG_SLOWER_THAN;
server.slowlog_max_len = REDIS_SLOWLOG_MAX_LEN;
/* Debugging */
// 初始化调试项
server.assert_failed = "<no assertion failed>";
server.assert_file = "<no file>";
server.assert_line = 0;
server.bug_report_start = 0;
server.watchdog_period = 0;
}
上記のすべてはコメント化されているため、最初に見ることはできません。コピー、クラスター、luaなど、最初に他を見てください。
まとめ
cに触れて久しぶりに忘れてしまいましたが、一般的には難しくありません。難しいのはメモリリークですが、デバッグに使用するだけなので、これらの問題については心配していません。
ポインター部分には少し土台が必要で、誰もがそれを学ぶために少し時間を取ることができます。
ご質問やご提案がございましたら、お知らせください。