base on redis source code 5.0.3
在redis server启动过程中, 有一个宏和一个函数显得很奇特, 他们是server.c
中main()
函数中的第一个宏和第一个函数, 宏INIT_SETPROCTITLE_REPLACEMENT
和函数spt_init(argc, argv);
。他俩组合在一起的主要功能是在macOS和*nix下设置(修改)redis的各个进程名, 例如redis-aof-rewrite
、redis-rdb-bgsave
等。
那么他们是如何工作的呢?想知道这些, 这得从main函数说起。请看标准的main函数签名:
int main(int argc, char ** argv);
macOS和*nix系统创建进程后会给进程分配一个全局的environment环境变量char ** environ
, 它是一个char*
数组, 里面保存的是类似{k=v, k=v, k=v}
这样的字符串数组。如果我们想使用它可以像下面这样(实际上redis也是这样做的):
#include
extern char ** environ;
int main(int argc, char ** argv){
for (auto idx = 0; idx < argc; ++idx){
std::cout << static_cast(argv[idx]) << " = " << argv[idx] << std::endl;
}
for(auto idx = 0; nullptr != environ[idx]; ++idx){
std::cout << static_cast(environ[idx]) << " = " << environ[idx] << std::endl;
}
}
g++ -Wall -std=c++11 main.cpp -o test
./test
在我的mac下运行结果如下(其实就是系统给进程设置的环境变量):
0x7ffeed1afa80 = ./test // argv[0]
0x7ffeed1afa87 = __CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0 //environ[0]
0x7ffeed1afaad = TMPDIR=/var/folders/5y/c9fbgn3x6p90sl9gbx_y1qnm0000gn/T/
0x7ffeed1afae6 = HOME=/Users/zhangyebai
0x7ffeed1afafd = SHELL=/bin/zsh
0x7ffeed1afb0c = Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.Ey7nIaeRPX/Render
0x7ffeed1afb58 = SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.CT1oMhwGyS/Listeners
0x7ffeed1afb9a = PATH=/usr/local/opt/icu4c/sbin:/usr/local/opt/icu4c/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/opt/icu4c/sbin:/usr/local/opt/icu4c/bin:/usr/local/sbin:/Users/zhangyebai/scripts:/usr/local/mysql/bin:/Users/zhangyebai/scripts:/usr/local/mysql/bin
0x7ffeed1afcb0 = LOGNAME=zhangyebai
0x7ffeed1afcc3 = XPC_SERVICE_NAME=0
0x7ffeed1afcd6 = COMMAND_MODE=unix2003
0x7ffeed1afcec = USER=zhangyebai
0x7fe8f2502450 = XPC_FLAGS=0x0
0x7ffeed1afd0a = TERM_PROGRAM=vscode
0x7ffeed1afd1e = TERM=xterm-256color
0x7ffeed1afd32 = TERM_PROGRAM_VERSION=1.31.1
0x7ffeed1afd4e = TERM_SESSION_ID=3B84E1B2-C0C2-426C-8AE3-00C01A9F6D5B
0x7ffeed1afd83 = ZSH=/Users/zhangyebai/.oh-my-zsh
0x7ffeed1afda4 = PAGER=less
0x7ffeed1afdaf = LSCOLORS=Gxfxcxdxbxegedabagacad
0x7ffeed1afdcf = PWD=/Users/zhangyebai/code/cpp/redis
0x7ffeed1afdf4 = SHLVL=1
0x7ffeed1afdfc = LESS=-R
0x7ffeed1afe04 = LC_CTYPE=en_US.UTF-8
0x7ffeed1afe19 = SECURITYSESSIONID=186a9
0x7ffeed1afe31 = APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL=true
0x7ffeed1afe61 = OLDPWD=/Users/zhangyebai/code/cpp/redis/testdwnakdihjwaijdiwaljdiowadwadwadwa.dSYM
0x7ffeed1afeb4 = LANG=en_US.UTF-8
0x7ffeed1afec5 = _=/Users/zhangyebai/code/cpp/redis/./test
environ
跟int main(int argc, char ** argv)
有着什么样的关系呢? 如下所示:
'argv[0][content]\0' 'argv[1][content]\0' ... 'argv[n][content]\0' nullptr 'environ[0][content]\0' 'environ[1][content]\0' ... 'envrion[x][content]\0'
可以看到他们是一段连续的内存, 下面我们举个例子把它实例化看一下:
argv[0] argv[1] environ[0] environ[1]
a \0 b \0 d=2\0 e=3\0
0 1 2 3 4567 891011
base = 0x7ffeeaf21a80
offset = 0, 1, 2 ... 11
说完argv
、environ
的内存布局, 我们就可以开始看redis是如何设置进程名的了, 这里先直接给出答案: argv[0]里面对应的就是进程名。但是先别着急, 想要修改它可也没那么容易。因为什么? 上面我们说了, argv
、environ
可是内存连续的, 如果你设置了一个新的进程名长度比原来的长, 那么悲剧即将发生。它将覆盖argv[0]后面的缓冲区, 这将是致命的 。argv
、environ
在进程运行过程中随时可能会用到,它们很重要。好了, 下面我们看redis怎么做的:
// server.c line 4035 version 5.0.3
int main(int argc, char ** argv){
//....more
/* We need to initialize our libraries, and the server configuration. */
#ifdef INIT_SETPROCTITLE_REPLACEMENT
spt_init(argc, argv);
#endif
//...more
}
在setproctitle.c
中line 152
展开spt_init(argc, argv);
, 我们一行一行来看(这里点名表扬redis的函数尾注释):
void spt_init(int argc, char *argv[]) {
char **envp = environ;
char *base, *end, *nul, *tmp;
int i, error;
/*
注意这里的base, 被赋值argv[0],由于上面说了argv和environ
是内存连续的, 在经历下面的操作以后argv 和 environ这边连续
的内存将退化成char*类型的base
*/
if (!(base = argv[0]))
return;
// nul表示argv[0], 也就是base字符串,
// 也就是进程名字符串的结束位置(不包括\0)
nul = &base[strlen(base)];
end = nul + 1;
/*
虽然argv和environ是连续的内存, 但是其中包含\0, 而且不知道
environ的长度,所以得遍历二者, 将这片内存退化成char*,
也就是base
step 1: 遍历argv
note: for循环的条件判断很奇怪, 我暂时没遇到满足这样奇怪条件
的启动参数, 可能redis的开发人员在不同的操作系统上遇到这么怪异
的问题, 有待探究。
*/
for (i = 0; i < argc || (i >= argc && argv[i]); i++) {
if (!argv[i] || argv[i] < end)
continue;
end = argv[i] + strlen(argv[i]) + 1;
}
/*
step 2: 遍历 environ
note: for循环中的这个if判断同样很奇怪
*/
for (i = 0; envp[i]; i++) {
if (envp[i] < end)
continue;
end = envp[i] + strlen(envp[i]) + 1;
}
/**
SPT是一个全局变量结构体:
static struct {
// original value
const char *arg0;
// title space available
char *base, *end;
// pointer to original nul character within base
char *nul;
_Bool reset;
int error;
} SPT;
*/
// 这一步很关键, 将原进程名备份
if (!(SPT.arg0 = strdup(argv[0])))
goto syerr;
#if __GLIBC__
/**
释放跟argv有关的内存
*/
if (!(tmp = strdup(program_invocation_name)))
goto syerr;
program_invocation_name = tmp;
if (!(tmp = strdup(program_invocation_short_name)))
goto syerr;
program_invocation_short_name = tmp;
#elif __APPLE__
/**
释放跟argv有关的内存
*/
if (!(tmp = strdup(getprogname())))
goto syerr;
setprogname(tmp);
#endif
/**
重新设置了env, 具体怎么实现我们等会跳进去看。
只需要知道, 这个函数执行过以后, 将在新的内存区域
产生新的environ, 原来的environ内存区域将归base所有
*/
if ((error = spt_copyenv(envp)))
goto error;
/**
重新设置了除argv[0]以外的所有argv, 具体怎么实现我们等会跳进去看。
只需要知道, 这个函数执行过以后, 将在新的内存区域
产生新的除argv[0]以外的argv, 原来的argv[1-n]内存区域将归base所有
*/
if ((error = spt_copyargs(argc, argv)))
goto error;
/**
至此, 原来argv environ共有的那片连续内存全部被转换成base, 即
char*, 也是argv[0], 也就是进程名。argv和environ将不再连续, 新的
内荣全部由strdup生成
我们修改进程名, 只需要改SPT->base就可以了。
*/
SPT.nul = nul;
SPT.base = base;
SPT.end = end;
return;
syerr:
error = errno;
error:
SPT.error = error;
} /* spt_init() */
现在我们在setproctitle.c
的line 103
展开spt_copyenv(envp)
:
static int spt_copyenv(char *oldenv[]) {
extern char **environ;
char *eq;
int i, error;
/**
如果environ != oldenv则说明environ已经被设置过了
直接返回成功
*/
if (environ != oldenv)
return 0;
/**
让老的environ失效,但是不释放那片内存
这个函数有一段血泪史, 等下我们展开
*/
if ((error = spt_clearenv()))
goto error;
/**
setenv会生成新的environ, 不必手动设置新environ的内存
注意setenv中取值的写法, 这是c语言优秀的精华
*/
for (i = 0; oldenv[i]; i++) {
if (!(eq = strchr(oldenv[i], '=')))
continue;
*eq = '\0';
error = (0 != setenv(oldenv[i], eq + 1, 1))? errno : 0;
*eq = '=';
if (error)
goto error;
}
return 0;
/**
如果设置过程中出问题了, 则还原environ的设置
*/
error:
environ = oldenv;
return error;
} /* spt_copyenv() */
现在我们来述说那段血泪史, 我们在setproctitle.c
的line 83
展开spt_clearenv();
:
这里面是redis作者对cleanrenv()最深沉的吐槽
/*
* For discussion on the portability of the various methods, see
* http://lists.freebsd.org/pipermail/freebsd-stable/2008-June/043136.html
*/
/**
看上面这段注释中的链接, redis作者写了好几种实现方法来兼容macOS和*nix系统, 至今未果
现在的这段实现里面macOS是问号,但是我亲测it works
step 1: 扔给系统一个空environ, 让原来的environ失效
step 2: 再设置新的environ
*/
static int spt_clearenv(void) {
#if __GLIBC__
clearenv();
return 0;
#else
extern char **environ;
static char **tmp;
/**
及其怪异的写法,其实相当于:
char * arr[1] = {nullptr};
temp = static_cast(arr);
environ = temp;
它的这个写法, 我思考了好久。这里也不得不吐槽, c语言灵活的没边了...
void *一时爽, 回看火葬场。
*/
if (!(tmp = malloc(sizeof *tmp)))
return errno;
tmp[0] = NULL;
environ = tmp;
return 0;
#endif
} /* spt_clearenv() */
现在我们在setproctitle.c
的line 103
展开spt_copyargs(argc, argv);
:
static int spt_copyargs(int argc, char *argv[]) {
char *tmp;
int i;
/**
除argv[0]以外的所有argv都由strdup重新生成
注意由strdup生成的char*是需要free的
redis里由于就在这里用到, 并且这些参数是给系统使用,
而且所有函数都只调用一次所以就没有free
*/
for (i = 1; i < argc || (i >= argc && argv[i]); i++) {
if (!argv[i])
continue;
if (!(tmp = strdup(argv[i])))
return errno;
argv[i] = tmp;
}
return 0;
} /* spt_copyargs() */
现在我们在setproctitle.c
的line 103
展开setproctitle(const char fmt, ...)
:
//SPT_MAXTITLE = 255;
void setproctitle(const char *fmt, ...) {
char buf[SPT_MAXTITLE + 1]; /* use buffer in case argv[0] is passed */
va_list ap;
char *nul;
int len, error;
if (!SPT.base)
return;
if (fmt) {
va_start(ap, fmt);
len = vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
} else {
len = snprintf(buf, sizeof buf, "%s", SPT.arg0);
}
if (len <= 0)
{ error = errno; goto error; }
if (!SPT.reset) {
memset(SPT.base, 0, SPT.end - SPT.base);
SPT.reset = 1;
} else {
memset(SPT.base, 0, spt_min(sizeof buf, SPT.end - SPT.base));
}
/**
一顿操作base nul end。终于通过memcpy将我们的新进程名设置进去了
其实我觉得nul用的没有意义...
也可能是有序的代码里面会用到?(如果有的话以后更新)
*/
len = spt_min(len, spt_min(sizeof buf, SPT.end - SPT.base) - 1);
memcpy(SPT.base, buf, len);
nul = &SPT.base[len];
if (nul < SPT.nul) {
*SPT.nul = '.';
} else if (nul == SPT.nul && &nul[1] < SPT.end) {
*SPT.nul = ' ';
*++nul = '\0';
}
return;
error:
SPT.error = error;
} /* setproctitle() */
总结起来就是:
1. 重新修改全局environ指针指向
2. 重新修改除argv[0]以外的argv指针指向
3. 扩容argv[0]
4. 通过修改argv[0]的内容设置进程名
不过有一点很费解的是, 理论上来说重新修改argv[0]的指针指向也可以做到修改进程名, 但是
我测试了一下发现不行, 猜测可能系统kernel在创建进程时缓存了argv的const指针,所以在修改
argv[0]的指针值时不会生效, 这段验证只是猜测, 未经证实, 有待探究。
最后, 从redis中偷师, 用c++11写了一个header only的设置进程名的小库:
setproctitle
同时该工程下还有以下两个小玩意儿:
c++11线程池 c++11 std::functional lambda
c++11时间库 跨平台, 几乎能支持所有你的习惯来操作时间~
部分setproctitle
:
#ifndef _UTIL_H_
#define _UTIL_H_
#include //strlen(2) strdup(1)
#include //min(1)
/**
* origin: 进程原始名
* base: char数组, 存储修改后的进程名
* nul:
* end:
*
*/
typedef struct _PROC_TITLE_INFO{
const char * origin;
char * base, *nul, *end;
}PTI, *PPTI;
extern char ** environ;
class Util{
private:
Util() = default;
~Util() = default;
public:
/**
* gloabl init function, keep it be called only once
*
*/
static PPTI init_proc_title_info(int argc, char ** argv){
auto ** envp = environ;
auto base = argv[0];
if(nullptr == base){
return nullptr;
}
char * end = (&base[strlen(base)]) + 1; // +1 for '\0'
for (auto idx = 0; idx < argc || (idx >= argc && nullptr != argv[idx]); ++idx){
// 不知道redis为什么这样写, 我猜可能是遇到特殊的argv, 待探索
if(nullptr == argv[idx] || argv[idx] < end){ //注意这里比较的都是指针,不涉及到数据
continue;
}
end = argv[idx] + strlen(argv[idx]) + 1; // +1 for '\0'
}
for (auto idx = 0; nullptr != envp[idx]; ++idx){
if (envp[idx] < end){ //注意这里比较的都是指针,不涉及到数据
continue;
}
end = envp[idx] + strlen(envp[idx]) + 1; // +1 for '\0'
}
PPTI ppti = new PTI();
ppti->origin = strdup(argv[0]);
if(nullptr == ppti->origin){
delete ppti;
ppti = nullptr;
return ppti;
}
#ifdef __GLIBC__
//TODO
#elif __APPLE__
auto name = strdup(getprogname());
if(nullptr == name){
free(const_cast(ppti->origin));
delete ppti;
ppti = nullptr;
return ppti;
}
setprogname(name);
#endif
if( !set_new_env(environ) ){
free(const_cast(ppti->origin));
delete ppti;
ppti = nullptr;
return ppti;
}
if( !set_new_argv(argc, argv) ){
free(const_cast(ppti->origin));
delete ppti;
ppti = nullptr;
return ppti;
}
ppti->base = base;
ppti->end = end;
return ppti;
}
static bool set_proc_title(const PPTI ppti, const char * fmt, ...){
if(nullptr == ppti){
return false;
}
if(nullptr == ppti->base){
return false;
}
char buf[256] = {0};
va_list ap;
auto len = 0;
if(nullptr != fmt){
va_start(ap, fmt);
len = vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
}else{
len = snprintf(buf, sizeof buf, "%s", ppti->origin);
}
if(len <= 0){
return false;
}
memset(ppti->base, 0, len + 1);
memcpy(ppti->base, buf, len);
return true;
}
private:
template
static T min(T && l, T && r){
return l > r ? r : l;
}
static bool set_new_argv(int argc, char ** argv){
for(auto idx = 1; idx < argc || (idx >= argc && nullptr != argv[idx]); ++idx){
if(nullptr == argv[idx]){
continue;
}
auto arg = strdup(argv[idx]);
if(nullptr == arg){
return false;
}
argv[idx] = arg;
}
return true;
}
static bool set_new_env(char ** old_env){
extern char ** environ;
if( environ != old_env){
return true;
}
if(! clear_env()){
environ = old_env;
return false;
}
char * eq = nullptr;
for (auto idx = 0; nullptr != old_env[idx]; ++idx){
eq = strchr(old_env[idx], '=');
if(nullptr == eq){
continue;
}
*eq = '\0';
int result = setenv(old_env[idx], eq + 1, true);
*eq = '=';
if(0 != result){
environ = old_env;
return false;
}
}
return true;
}
static bool clear_env(void){
#ifdef __GLIBC__
//TODO
#else
extern char ** environ;
/**
* 相当于 char * arr[1] = {nullptr};
* static char ** temp_env = statc_cast(arr);
* 其主要目的是为了使environ变成空置,从而使environ失效
* 关于这个函数的实现请点击下面的链接查看redis作者对其的说明和吐槽
* For discussion on the portability of the various methods, see
* http://lists.freebsd.org/pipermail/freebsd-stable/2008-June/043136.html
*/
static char ** temp_env = static_cast(malloc(sizeof *temp_env));
if(nullptr == temp_env){
return false;
}
temp_env[0] = nullptr;
environ = temp_env;
return true;
#endif
}
};
#endif
测试代码:
#include
#include "./common/Util.hpp"
extern char **environ;
//#define UNUSED(x) (void)x;
int main(int argc, char ** argv){
auto ppti = Util::init_proc_title_info( argc, argv);
std::cout << ppti->origin << "-" << ppti->base << std::endl;;
std::cout << Util::set_proc_title(ppti, "%s-%d", "named", 1) << std::endl;
std::cout << getprogname() << std::endl;
std::cout << getprogname() << std::endl;
std::cout << ppti->end - ppti->base << std::endl;
std::cin.get();
}
g++ -Wall -std=c++11 -O3 main.cpp -o test
./test
结果:
ps -ef | grep named
501 99907 50309 0 1:49AM ttys002 0:00.00 named-1
q
quit