/首页
/开源
/关于
聊一道口头面试题
发表@2019-10-12 10:00:21
更新@2023-04-27 22:55:42
我是老李,大家好!一来是最近比较忙,二来是预祝大家春节愉快! 鉴于今天文章内容可能会比较正规一些,所以封面图就也跟着一起正规起来一下。封面人物:Dennis MacAlistair Ritchie,即丹尼斯里奇,或称D.M.R,Ken Tom的好盆友,C语言与UNIX发明人之一,大爷肉身已不在人世,精神依然长流! 众所周知,很久很久很久很久之前,我曾经去某东讨BD西伐TX南征AL北战快手的宇宙级公司挑战(折磨)过,让我暗爽的是历经摧残苦尽甘来后我依旧主动抛弃了他们,代价据说是(据说是)会被锁定半年,看意思大概就是该简历进入了一个半年的生理不应期。 昨晚我在写山寨Redis的时候就联想到了当时的一道面试题,仔细琢磨了一下当时虽然回答出来了,但其实并不全面和深入。题面是这样的,你们感受一下(并不是用笔和纸回答的面试题,就是和面试官沟通交流中他随意且随机地问):Linux下如何为程序设定新的进程名称。 那个,这问题听起来是不是很沙雕?但实际上这个问题背后内涵相当丰厚,在我看来他至少涉及到了如下两个面: - *NIX环境变量 - exec运行时程序内存分配图 - 命令行参数 我先说下当时我的回答,脱口而出的那种,一共用了不到3秒钟:直接修改argv[0]参数。 我当时就是这么回答的,这么回答没有错,只是不全面。记得当时我俩就大眼瞪小眼,他问我:完了?我怂怂地点了点头... ...面试官既然问你这个问题,想必是想了解更多【关于你对这些东西的掌握广度面以及深度】,这么聊聊一句现在想来也显得颇为尴尬,主要是我确实只能想到这个啊。 现如今,我打算着手解决一下这个看起来【平淡无奇】的问题,而且根据以往的经验看,我必须也要说下PHP... 一般说来我们,我们输入个ps -ef,就会看到下面这种东西,我截几个图你们感受一下: 比如nginx的进程名 ![](https://ti-node.com/static/upload/PNP/a/img_102.png#width-full) 比如Postgres的进程名 ![](https://ti-node.com/static/upload/PNP/a/img_103.png#width-full) 比如Swoole服务(可以配置自定义) ![](https://ti-node.com/static/upload/PNP/a/img_104.png#width-full) 比如Workerman的进程名 ![](https://ti-node.com/static/upload/PNP/a/img_105.png#width-full) 这么做好处很多,一是识别度很高,二是在grep的时候会很方便,三可能会看起来比较正规(我感觉正规这个词快被我用坏了)... ... 那,我们到直接CVS(Ctrl+C、Ctrl+V、Ctrl+S)阶段? ### 可能是世界上最好的语言 PHP里非常粗暴地提供了一个叫做cli_set_process_title的函数,不过文档上也明确指出了:此函数用于处理top或ps命令后查看到的进程名,而且此函数只能在PHP cli模式下使用。鉴于我等众雕都是大量使用php-fpm而不是php-cli的人,所以没准真的有很多PHPer压根都没听过cli_set_process_title这个函数。我码个demo吧,你们感受下: ```php =php 5.5 if (\function_exists('cli_set_process_title')) { \cli_set_process_title($title); } // Need proctitle when php<=5.5 . elseif (\extension_loaded('proctitle') && \function_exists('setproctitle')) { \setproctitle($title); } \restore_error_handler(); } ``` 嗯,长见识了,合着PHP还有一个叫做proctitle的扩展? ### 可能是圈里较为古老的语言 这个就比较恶心麻烦了,但实际上也【可能是较为标准】的答案。好了,你们准备一下,我要开始表演了。首先我可以尝试随便瞎写一个C语言程序,比如helloworld: ```php #include
#include
int main() { printf( "sleep me...\n" ); sleep( 1000 ); return 0; } ``` 编译一下一跑,大概就是下图这么个结果,我们的任务就是要改动这个玩意: ![](https://ti-node.com/static/upload/PNP/a/img_108.png#width-full) 在Linux中有一个叫做prctl的标准函数,据man页说明这个函数可以调整【调用进程或线程的名称】,可以先尝试一下: ```php #include
#include
int main( int argc, char * argv[] ) { // 试图把当前进程名调整为 tidis-server char * process_name = "tidis-server"; prctl( PR_SET_NAME, process_name, NULL, NULL, NULL ); // 保证进程不会退出...不然ps -ef看不到 sleep( 100000 ); return 0; } ``` 这个编译搞定后(gcc默认的文件名a.out)我们用ps -ef | grep tidis,然而实际上结果为空,去掉grep才注意到,此时进程的名字依然为./a.out;我们用top命令查看,发现进程名也依然是./a.out~~~改名失败了? 后来看手册才知晓,这个函数修改的是*NIX的/procs目录下的一些内容(/procs目录的作用自己手动查下哈),怎么查看下呢? 首先看下./a.out的进程pid是什么,然后直接cat查看/proc/[pid]/stat和/proc/[pid]/status两个文件即可,注意其中的Name应该已经是tidis-server了。 ![](https://ti-node.com/static/upload/PNP/a/img_109.png#width-full) 这个函数并不能调整进程在ps和top命令中的进程名,应该是只能调整在/proc/[pid]下的一些数据信息。而且man页上也额外指出如下内容:“ If the length of the string, including the terminating null byte,exceeds 16 bytes, the string is silently truncated. ”,就是说这个函数接受的进程名参数长度最长应该是16字节而且包括末尾的null byte(C语言中字符串最后一个元素是null byte)。但不能说这个函数没用,因为一些工具命令获取信息就是从/proc目录中获取的,比如vmstat等,如果一个查看进程的工具获取数据就是从/proc目录中获取数据的,那么我们就会达到我们想要的结果。 那么,ps和top这两个常用命令中显示的进程名如何修改?此时不得不引入一下命令行参数的概念,实际上cli中启动一个程序都是会默认带入命令行参数的,做个实验你们复制走试一下: ```php #include
#include
// argc就表示命令参数的个数 // argv是一个指针数组,就是一个数组,里面全是一坨指针,而且是字符串指针 int main( int argc, char * argv[] ) { printf( "一共收到%d个命令行参数:\n", argc ); for( int i = 0; i < argc ; i++ ) { printf( "%s\n", argv[ i ] ); } return 0; } ``` 运行命令我们用这个尝试下./a.out -h 127.0.0.1 -p 3306,运行结果入下图: ![](https://ti-node.com/static/upload/PNP/a/img_110.png#width-full) 这会儿你在结合我当初那个虎批的回答:直接修改argv[0]参数~上图中的./a.out就是argv[0],同时也是显示在ps、top中的进程名,所以理论上我们修改argv[0]指针指向的字符串内容就应该可以修改进程名,let's rock~千万不要错过代码中的注释! ```php #include
#include
#include
int main( int argc, char * argv[] ) { printf( "argv[0]的内存地址:%p\n", argv[ 0 ] ); // 修改argv[0]指针指向的内存中的字符串的内容 // 我知道,一定有,很多人想用 argv[0] = "tidis";但是,这么写, // 连编译都过不了的,因为argv[0]实际上是一个“指针常量”, // 你修改不了的~实在get不到这个点,就多想想多看《C与指针》多写! strcpy( argv[ 0 ], "tidis-server-master-process" ); // 保证进程不会退出 sleep( 100000 ); return 0; } ``` 编译后run一下,然后结合ps -ef | grep tidis查看一下: ![](https://ti-node.com/static/upload/PNP/a/img_111.png#width-full) ![](https://ti-node.com/static/upload/PNP/a/9.gif#width-full) 好像成功了???当然没有!不然这文章还怎么编下去... ...我总不能搁一句【老铁们,我实在编不下去了】就全剧终吧?然而现在就是到了尴尬的境地,就是看起来成功了实际上并没有成功,我还得装作什么都不知道继续编下去,你们说难受不难受?好了,改代码!按照下面的COPY: ```php #include
#include
#include
int main( int argc, char * argv[] ) { printf( "argv[0]的内存地址:%p\n", argv[ 0 ] ); strcpy( argv[ 0 ], "tidis-server-master-process" ); for ( int i = 0; i < argc; i++ ) { printf( "argv[%d] : %s\n", i, argv[ i ] ); } sleep( 100000 ); return 0; } ``` 编译成功后我们使用./a.out -h 127.0.0.1 -p 3306执行,感受下? ![](https://ti-node.com/static/upload/PNP/a/img_112.png#width-full) 怎么会是这样?!!! ![](https://ti-node.com/static/upload/PNP/a/img_113.png#width-full) 而且你们仔细观察下,好像还有规律。我再改下文件名,估计你们能慢慢回过味儿来,我们把a.out文件名直接修改为tidis-server-master-process,然后再用./tidis-server-master-process -h 127.0.0.1 -p 3306跑一下,怎么样?没问题了吧?图我就不贴了。事情到这里我们得出一个结论:直接修改argv[0]可以实现目标,但是有个缺陷就是新的进程名长度不可以超过程序的文件名。 我们当然可以通过在代码里做条件检测来约束这个问题,但终究不是彻底解决方案。为了能搞明白这个问题,是时候引入程序运行时地址分配图和环境变量的概念了。这里的环境变量就是说你们平时经常从网上复制粘贴的那些linux环境变量,其实就是字符串,配置什么Java环境Golang环境时候你们一定都搞过这个,就是key=value。再一次翻阅APUE,里面倒是给出了命令行参数和环境变量的数据结构,其实就是一大坨char *然后形成了一个char **: ![](https://ti-node.com/static/upload/PNP/a/img_114.png#width-full) 无论是argv还是environ,他们的指针数组最后一个元素一定是NULL;然后是指针指向的内存空间是连续紧挨着的,具体说就是argv在前面,environ环境紧跟在后。看到这里,你们知道为啥前面长长的进程名会出现那个【有规律】的现象了吧?因为argv内存空间是连续,太长会直接覆盖后面单元中数据,如果你愿意动手试下,可以用如下代码读取出一下你的argv和environ,各位看官,请copy下面代码: ```php #include
extern char **environ; int main( int argc, char *argv[] ) { for ( int i = 0; i < argc; i++ ) { printf( "%s\n", argv[ i ] ); } printf( "=============================\n" ); int i = 0; // 注意此处用 ++i 而不是 i++,不然你换下,有惊喜~ while ( environ[ ++i ] ) { printf( "%s\n", environ[ i ] ); } return 0; } ``` 那程序运行时地址分配图是什么玩意?当一个程序在命令行跑起来的时候,实际上相当于exec族系统调用执行了一个程序,就是TA把命令行参数和环境变量透传给main函数的,一个程序的运行时地址分配是这样shai儿的: ![](https://ti-node.com/static/upload/PNP/a/img_115.png#width-full) 注意,这个里的堆和数据结构中那个堆不是一回事,C中malloc获取内存空间就是从堆内存中获取的。我们的argv们就和环境变量们在一起,一起拥挤在最最最最上面,位于堆内存和栈内存的顶上。现如今我们要在程序运行后调整argv[0]的值,如果新的进程名长度超过了原文件名长度,我们就只能申请新的空间,但是如果想让程序在运行后整体向下移动堆栈、正文段几乎是不可能的,所以我们就可以像Nginx那样这样来实现一下: - 申请一块儿新的内存 - 修改argv[0]内容 - 把argv[1]一直到最后一个argv[x]以及环境变量放到申请的新内存中 NOTICE:下面这段可供copy的代码,虽然大概率可能存在bug,不过用于演示俨然是没有问题的,即便如此,对于一些新手来说可能还是会非常绕弯儿 ```php #include
#include
#include
#include
#include
#define BUF_SIZE 1024 extern char **environ; int main( int argc, char * argv[] ) { char ** origin_argv; char ** origin_environ; char * last_argv; char * process_name = "tidis-master-process"; int index; char new_argv[ BUF_SIZE ]; size_t buf_len; origin_environ = environ; origin_argv = argv; // 将argv中除了argv[ 0 ]之外的全部保存到new_argv中 // 以字符串的形式将argv[ 1 ]-argv[ x ]的参数保存到new_argv中 memset( new_argv, '\0', BUF_SIZE ); for ( index = 1; index < argc; index++ ) { strcat( new_argv, argv[ index ] ); strcat( new_argv, " " ); } // 将envrion中环境变量保存到new_environ中 int environ_count = 0; for ( environ_count = 0; environ[ environ_count ] != NULL; environ_count++ ) continue; // 这句可能需要好好理解一下... environ = ( char ** )malloc( sizeof( char * ) * ( environ_count + 1 ) ); // 将老的environ逐一copy到新的environ中 for ( index = 0; index < environ_count; index++ ) { // 这两句的意思就是:先分配好内存空间,然后复制过去 environ[ index ] = ( char * )malloc( sizeof( char ) * strlen( origin_environ[ index ] ) ); strcpy( environ[ index ], origin_environ[ index ] ); } // 确保environ环境变量最后一个指针为空. environ[ index ] = NULL; // 这个逻辑也比较绕,目的是为了获取最后一个argv参数. // index一般说来,肯定都大于0,因为一定会有环境变量的... last_argv = index > 0 ? origin_environ[ index - 1 ] + strlen( origin_environ[ index - 1 ] ) : origin_argv[ argc - 1 ] + strlen( argv[ argc - 1 ] ); // 同时设定argv[ 0 ]和prctl,双重保险. // 这里意味着,只要命令行参数不超2048即可,如果更长,该这个数值即可... char argv_buffer[ 2048 ]; size_t argv_buffer_length; strcpy( argv_buffer, process_name ); strcat( argv_buffer, " " ); strcat( argv_buffer, new_argv ); argv_buffer_length = strlen( argv_buffer ); strcpy( origin_argv[0], argv_buffer ); char * pt_last = &origin_argv[0][ argv_buffer_length ]; while ( pt_last < last_argv ) // 那个..看到这句有崩溃的么... // 说下哈,++优先级比*高,所以这句就是先产生pt_last的一个拷贝然后执行++,然后* // 作为左值的含义就是给某一个内存位置存储数值,也就是'\0' *pt_last++ = '\0'; origin_argv[ 1 ] = NULL; prctl( PR_SET_NAME, process_name, NULL, NULL, NULL ); printf( "\n\ntidis-server start!\n\n" ); sleep( 100000 ); return 0; } ``` 好了,这坨代码也TM折腾的我筋疲力尽,不过好在能用,你们感受下: ![](https://ti-node.com/static/upload/PNP/a/img_116.png#width-full) 完美! 如果要搞明白涉及上面的内容,三本书离不开:APUE、C与指针、c primer plus。不过话说回来,搞不搞明白这些问题实际上也没有太大意义,不影响砌砖头赚钱 ~ 至于这几本书,他们不属于那种快速阅读快速理解的那种,对付这几本书籍,你需要参考下毛泽东同志的《论持久战》,如果想快速21天精通的,好像不大行... ... 参考链接与资料: 1. http://lxr.nginx.org/source/src/os/unix/ngx_setproctitle.c 2. https://blog.csdn.net/duyiwuer2009/article/details/8447802 3. APUE第七章节部分内容