宋宝华: 论一个程序员问问题的自我修养(修订版)


问问题如打麻将


麻场如战场,麻品如人品,检验一个人的最好方法就是带他去搓麻将,一叶而知秋。同样地,通过程序员问问题的情状也基本可以看出一个程序员的治学态度。如果他问问题的时候,看得出他经过深思熟虑,经过自己的debug和剖析,这个程序员多半还是一个不错的程序员,或者正在走向“不错”。反之,一眼看出,其问的问题就没有经过大脑,甚至连问题的现场都描述不清楚,这样的程序员多半在编程领域难有大的建树,很可能只是在长年累月地低水平重复建设。

一个好的程序员,不断通过解决工程问题,查阅资料,总结知识,提升认知来逐步把自己变成高手。一个不好的程序员,有点什么事情就开始到处问,也从来不深入地去自己研究,其实,就算是工作20年,水平恐怕也还是不行,因为他陷入了简单的经验式重复。高手不完全是知识面广或深,很大程度上高在其本身的认知层面,而认知层面的提高就在于他从来都是在思考。

 

我是一个码农,也号称是一老湿,接触的码农比较多,平生阅码农无数(哈哈,吹牛的,阅人莫道千千万,识得真心在最初)。 好的程序员,基本上,问问题的时候,给你春风拂面的感觉,让你心里生出一些敬佩;不好的程序员,很多时候问的问题,你根本无言以对,心里一万个草泥马奔涌而过。

麻品有问题


下面我们来总结一些不好的程序员问问题的时候的几个典型的“问题”。

不调试就问

之前有时候坐在office的时候,有些人问我这样的问题,“我的程序hang死了,请教一下hang死在哪里了?”我这个时候只想说,程序是你的,你都不知道哪里hang死了,我怎么就会知道?能不能拿gdb attach上去看啊?你拿gdb attach上去看,最后程序是拿不到mutex呢?还是进while(1)了呢?还是怎么的?

任何时候,问问题之前,先问自己一个问题,“我调试过没有?”没有的话,赶紧先闭嘴。

下面我们用一个最简单的例子来解释这个过程,看下面一个程序,2个线程拿2个锁,但是顺序相反,程序肯定会死锁。死锁之后,gdb attach上去看,不要问! 

pthread_mutex_tmutex_1;

pthread_mutex_tmutex_2;


void *child1(void*arg)

{

       while(1) {

              pthread_mutex_lock(&mutex_1);

              sleep(3);

              pthread_mutex_lock(&mutex_2);

              printf("thread 1 get running\n");

              pthread_mutex_unlock(&mutex_2);

              pthread_mutex_unlock(&mutex_1);

              sleep(5);

       }

}


void *child2(void*arg)

{

       while(1) {

              pthread_mutex_lock(&mutex_2);

              pthread_mutex_lock(&mutex_1);

              printf("thread 2 get running\n");

              pthread_mutex_unlock(&mutex_1);

              pthread_mutex_unlock(&mutex_2);

              sleep(5);

       }

}


int main(int argc,char *argv[])

{

       int tid1,tid2;


       pthread_mutex_init(&mutex_1,NULL);

       pthread_mutex_init(&mutex_2,NULL);

       pthread_create(&tid1,NULL,child1,NULL);

       pthread_create(&tid2,NULL,child2,NULL);

       ...

}

上述程序跑着一定会死锁,之后我们gdb attach它的pid上去调试就可以分析出来它是怎么样在死锁。我们首先看到它有3个线程:

(gdb) i threads

  Id  Target Id         Frame

  3   Thread 0xb75e9b40 (LWP 15809) "a.out" ...

  2   Thread 0xb7deab40 (LWP 15808) "a.out"...

* 1    Thread 0xb7deb700 (LWP 15804)"a.out"...

切到线程2,看一下backtrace:

(gdb) thread 2

[Switching to thread 2 (Thread 0xb7deab40 (LWP 15808))]

(gdb) bt

...

#4 0x08048627 in child1 (arg=0x0) at deadlock.c:12

...

这个PID为15808的线程在代码的第12行,child1()这个函数,等一个mutex,等谁呢?

(gdb) l child1

...

12                 pthread_mutex_lock(&mutex_2);

...

看出代码12行在等mutex_2,看下mutex_2的owner是谁?

(gdb) p mutex_2

$1 = {__data ={__lock = 2, __count = 0, __owner = 15809, __kind = 0, ...

我们目前看出来,15808线程在等一个mutex,这个mutex目前被15809拿着。我们现在切到线程3,然后看backtrace:

(gdb)thread 3

[Switchingto thread 3 (Thread 0xb75e9b40 (LWP 15809))]

(gdb) bt

...

#4  0x08048677 in child2 (arg=0x0) at deadlock.c:24

...

同样看出15809在等一个mutex,在代码的第24行。第24行在等谁呢?

(gdb) l child2

...

24                 pthread_mutex_lock(&mutex_1);

...

第24行在等mutex_1,它的owner又是谁呢?

(gdb) p mutex_1

$2 = {__data ={__lock = 2, __count = 0, __owner = 15808, __kind = 0...

由此可见,15808在等一个mutex,该mutex被15809拿着;15809在等一个mutex,该mutex被15808拿着。显然是死锁了。

上述例子非常简单,但足够说明我们调试问题的方法和思维问题的出发点。

很多时候,我们依赖于backtrace(栈回溯),比如kernel的lockup、kernel的OOPS(哎呀!)、应用软件的hang死不动。


没有回溯崩溃或hang死现场的问问题都是耍流氓!

无背景就问

任何问题,都必有它的特定背景和再现流程,不然就反证了它是一个general或者common的问题,general的问题本身不是问题,不存在需要debug的问题,基本你通过无数渠道都可以获得答案。


所以,任何时候,问问题,应该描述清楚你所有的平台背景,以及你是如何再现的,缺乏足够信息的问题,就和缺乏足够信息的报bug是一回事。比如,作为一个测试工程师,你报的bug是说手机会死机,但是没有说什么手机,什么安卓版本,哪天的release,再现的流程,这种bug的报法,属于无效bug,只会被RD直接ignore。


下面来看一段真实的对话,它突显了我霸气和焦躁的不良品质 :-)



问题情况都说不清楚的情况下,你问问题,显然就是讨骂。比如你说RAMDISK在板子上面跑CRC错误,那么你先有没有在自己电脑上面解开过,比如执行mount -o loop或者cpio提取?


如果你电脑上面自己都没有试过,都不确定RAMDISK是不是OK的,问板子还有什么意义?另外,问板子的问题,就应该描述清楚自己板子的内存大小,RAMDISK大小,存放的内存为位置,RAMDISK读出的介质如NAND/SD驱动有无问题等一系列情况,首先排除一些最基本的可能性。

骂完了我心理也有点难受了,感觉自己有点过了。但是我的好基友说,好老湿和好医生是一样的,医者父母心。上来就骂同学,肯定是招人恨啊,但是一辈子让你不修正自己的思维方式,那么岂不是更加地不负责任?


没有背景信息和再现流程的问问题都是耍流氓!

不学习就问

很多问题,你其实稍微学习一下就没有必要问了。比如,“请问,一个Linux普通进程多久被调度到?”这样的问题本来就是很吐血,认真学习后,不会问这样的问题。明确地说,不知道!装逼的说法,就是“depend on …”,依赖的东西太多。再装逼的说法,就是“一言难尽”,但这也是大实话。

普通的Linux进程为人比较善良,我们一般用nice来形容它们的优先级,nice越高,优先级越低(你越nice,就越喜欢在地铁让座,当然越坐不到座位)。普通进程的跑法,并不是nice低的一定堵着nice高的(要不然还说什么“善良”),它是按照如下公式进行:

vruntime=  pruntime*NICE_0_LOAD / weight

其中NICE_0_LOAD是1024,也就是NICE是0的进程的weight。vruntime是进程的虚拟运行时间,pruntime是物理运行时间,weight是权重,权重完全由nice决定,如下表:

在RT进程都睡过去之后(有一个特例就是RT没睡也会跑普通进程,那就是RT加起来跑地实在太久太久,普通进程必须喝点汤了),Linux开始跑NORMAL的,它倾向于调度vruntime(虚拟运行时间)最小的普通进程,根据我们小学数学知识,vruntime要小,要么分子小(喜欢睡,I/O型进程,pruntime不容易长大),要么分母大(nice值低,优先级高,权重大)。这样一个简单的公式,就同时照顾了普通进程的优先级和CPU/IO消耗情况。

比如有4个普通进程,如下表,目前显然T1的vruntime最小(这是它喜欢睡的结果),然后T1被调度到。


pruntime

Weight

vruntime

T1

8

1024(nice=0)

10*1024/1024=8

T2

10

526(nice=3)

10*1024/526 =19

T3

20

1024(nice=0)

20*1024/1024=20

T4

20

820(nice=1)

20*1024/820=24

然后,我们假设T1被调度再执行12个pruntime,它的vruntime将增大delta*1024/weight这里delta是12,weight是1024),于是T1的vruntime成为20,那么这个时候vruntime最小的反而是T2(为19),此后,Linux将倾向于调度T2(尽管T2的nice值大于T1,优先级低于T1,但是它的vruntime现在只有19)。

所以,普通进程的调度,是一个综合考虑你喜欢干活还是喜欢睡和你的nice值是多少的结果。鉴于此,我们去问一个普通进程的调度延迟究竟有多大,这个问题,本身意义就不是特别大,它完全取决于当前的系统里面还有谁在跑,取决于你唤醒的进程的nice和它前面喜欢不喜欢睡觉。

类似的知识性问题还有很多很多,这些问题,完全可以通过提前学习来解答。没有必要非得问。


知识性的问题,多半也很容易在网上找到资料。比如,搞不清楚PHP、node.js和Python做后端谁好,随便一百度,知乎网友回答的一大堆。如果再去问google,那会拿到更多的答案。“敏而好学,不耻下问”固然重要,但“敏而好学,耻于上问”也同样重要。


没有经过基本的学习就问问题都是耍流氓!


做一个好麻友


我的带头大哥,现在跟好朋友打麻将,都是直接支付宝付款,每局谁胡牌后,支付宝直接转账,麻品那是相当地好。然后打2个小时的牌,产生了几千上万的GDP,为国家和人民做了很大的贡献。


下面建议Linux程序员建立如下好习惯,做一个愿赌服输的好男人:

有问题,找男人

有问题,先man。“请问fread的第二个参数怎么设置?”鬼晓得?你man啊!

$ man fread

我man过了,我man open的时候出来的居然不是open(),而是openvt:

于是就要

$ man 2 open

这个时候出来的才是open,请问这个2是个什么鬼?那么,可以man一下man。

$ man man

所以,有问题,找男人,男人如果有问题,继续找男人

不知道找哪个男人,只记得函数里面有个单词叫timer。那么先定位,可以用类似apropos的工具:

baohua@baohua-VirtualBox:~/develop$ apropos timer

getitimer (2)        - get or set value of an interval timer

IPC::Run::Timer (3pm) - - Timer channels for IPC::Run.

setitimer (2)        - get or set value of an interval timer

time (7)             - overview of time and timers

timer_create (2)     - create a POSIX per-process timer

timer_delete (2)     - delete a POSIX per-process timer

...

再man那个函数即可。

提示


欢迎单位的女程序员有问题后直接请教男同事,

美女程序员直接忽略本文的所有条款,

美女程序员不受任何问问题规则约束  :-)



有问题,找Google

有的程序员又跳出来了,“报告,我google上不去”,相信我,一个不会翻墙的程序员,多半不会是一个好程序员。你连墙都不会翻(或者懒得翻的可能性更大),又如何查阅到最合适你的资料?

“报告,翻墙要花钱”,那么,吃饭要不要花钱?对于程序员来说,翻墙的需求,基本和吃饭的需求是一样的。翻墙越多,水平越高(翻墙看YY网站除外,那无非是多认识几个老师)。


我们不是嘲笑baidu一定不行,而是很多时候,工作的时候,碰到的kernel panic,碰到的segmentation fault,很多的其他的现场,它都是以英文的形式来呈现,很可能你在google里面能直接搜索到别人跟你碰到同样的问题。抛开baidu卖狗皮膏药的道德层面问题不谈,baidu在英文方面的搜索能力显然是远远落后google的。


下面我们搜索Kernel hard lockup,百度显示如下(应该说还不错):

Google显示如下(精准地定位到了Linux kernel官网的文档):

我们不是崇洋媚外,我们只是正视血淋淋的现实。

感恩回答者

很多程序员问别人问题,觉得别人天经地义应该回答你。而且问了很stupid的问题,别人没有好好回答,还觉得回答者在装逼。其实反过来想,一定要明白,回答我们问题的人,其实真的是在给我们工作。几年前,我在LKML问Greg K.H.下一个LTSI(工业级稳定版Linux) 版本是什么,他给我的回答非常简单直接,体现了一个正确的回答问题的风范:


“you are asking me to do work for you, right?”,你问别人问题的时候,你在让别人为你工作(而且这个工作还是免费的),对不对?

 

最好的问问题的方法,就是尽量不要问问题。如果要问题,要问能打动被提问者的问题。如何打动?你问问题,显示你已经经过足够的思辨。建议每一个人,可以深度学习一下《How-To-Ask-Questions-The-Smart-Way》这个文档,它是一个git项目哦:

https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way


最后的话

他回忆说:“我的名字是妈妈取的,我问过她为什么给我取这么奇怪的名字,她说是王勃的《滕王阁序》'雄州雾列,俊采星驰,台隍枕夷夏之交,宾主尽东南之美'这句来的。”


哪怕是一个龙套,我们也要认真地去做。不然就会永远是个龙套。因为,这是一个演员的自我修养。

不会问问题(意味着从来不试图自己去解决问题),我们恐怕永远是一个龙套。当你碰到一个bug,你去问别人,然后别人帮你快速解决了,自己的所学是有限的。而自己陷入思考,真正去debug,去查阅资料,去绞尽脑汁,这个过程的获得,除了这个修复bug本身以外,更可能学到了很多的知识,获取了很多的经验,提升了自己的认知。而这种认知,是你问一万个问题,并且立即获得了答案,也无法取得的。


小贴士


三个耍流氓


不调试就问

无背景就问

不学习就问



三个好习惯


有问题,找男人

有问题,找Google

感恩回答者



苹果手机打赏



安卓手机打赏


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值