这两天有同学使用数据校验工具时发现进程hang住了,也不知道什么原因,我简单看了看进程堆栈,问题虽然很简单,但能导致程序hang住,也一定不是小问题。简单说明下程序组件的结构,程序由两部分构成,dbchk和dbchk_inner,dbchk采用python代码实现,dbchk_inner采用c语言实现。dbchk负责并发控制,dbchk_inner则负责具体的校验任务。用户通过运行dbchk命令即可达到校验的目的。进程关系如下:
$ pstree 18649 dbchk─┬─sh───dbchk_inner───2*[{scandiff}] └─{dbchk} |
回到问题本身,我用测试用例复现了hang住了场景,查看了dbchk和dbchk_inner的堆栈信息,信息如下:
dbchk进程18649堆栈信息:
$ pstack 18649 thread 2 (thread 0x7f4343fff700 (lwp 18658)): #0 0x000000346f80f09d in waitpid () from /lib64/libpthread.so.0 #1 0x000000347190ff8a in ?? () from /usr/lib64/libpython2.6.so.1.0 #2 0x00000034718de706 in pyeval_evalframeex () from /usr/lib64/libpython2.6.so.1.0 #3 0x00000034718e0797 in pyeval_evalcodeex () from /usr/lib64/libpython2.6.so.1.0 |
dbchk_inner进程18660堆栈信息:
pstack 18660 #0 0x000000346f4da3dd in write () from /lib64/libc.so.6 #1 0x000000346f470fd3 in _io_new_file_write () from /lib64/libc.so.6 #2 0x000000346f470e9a in _io_new_file_xsputn () from /lib64/libc.so.6 #3 0x000000346f46705d in fwrite () from /lib64/libc.so.6 #4 0x00000000004136f0 in scanner::run(unsigned int) () |
可以看到父进程dbchk在卡在waitpid()函数,这个容易理解,它应该在等待子进程dbchk_inner结束;再看子进程dbchk_inner,dbchk_inner卡在fwrite()函数,这个就有点奇怪了,为啥写会被阻塞呢?首先想到的是磁盘空间不够了?看了下磁盘空间还有很大的剩余,那还有什么可能导致write卡住,还有一种可能就是缓冲区满了,写不下去。
基于这个思考,回头看看dbchk的代码
pio = subprocess.popen(command, shell=true, stdout=subprocess.pipe, stderr=subprocess.pipe).wait() |
可以看到程序里使用了popen的wait函数,这可以解释父进程为啥会卡住,因为子进程没有执行完;注意popen的参数,将stdout和stderr输出重定向到了subprocess.pipe,这个值表示父子进程之间的管道。那么子进程写缓冲区卡住,应该就是因为pipe的缓冲区满了。为啥会满呢,一是产生的数据太多;另一方面是没有进程去缓冲区去取数据,导致缓冲区只进不出。pipe缓冲区默认值大小4096个字节,这个可以通过ulimit -a得到,8*512=4096字节,并且这个值不可以修改的,因为值是定义在linux的头文件里面,除非你重新编译linux内核。
$ ulimit -a core file size (blocks, -c) 0 …… pipe size (512 bytes, -p) 8 |
好了问题找到了,pipe缓冲区满是罪归祸首,如何解这个问题?
1.不将stdout和stderr重定向管道,直接输出
2.程序控制输出到管道数据的大小
管道在进程间通信(ipc)使用很广泛,shell命令就使用的很广泛。比如:
ps –aux | grep mysqld |
上述命令表示获取mysqld进程相关的信息。这里ps和grep两个命令通信就采用了管道。管道有几个特点:
1. 管道是半双工的,数据只能单向流动,ps命令的输出是grep的输出
2. 只能用于父子进程或兄弟进程通信,这里可以认为ps和grep命令都是shell(bash/pdksh/ash/dash)命令的子进程,两者是兄弟关系。
3. 管道相对于管道两端的进程而言就是一个文件,并且只存在于内存中。
4. 写入端不断往管道写,并且每次写到管道末尾;读取端则不断从管道读,每次从头部读取。
到这里大家可能会有一个疑问,管道两端的进程,写入进程不断的写,读取进程不断的读,那么什么时候结束呢?比如我们刚刚这个命令很快就结束了,它的原理是怎么样的呢?对于管道,这里有两个基本原则:
1.当读一个写端已经关闭的管道时,在所有数据被读取后,read返回0,以指示达到文件结束处。
2.当写一个读端已经关闭的管道时,会产生sigpipe信息。
结合这个例子,当ps写管道结束后,就会自动关闭,此时grep进程read就会返回0,然后自动结束。
参考文档:
《unix环境高级编程》
http://blog.chinaunix.net/uid-26833883-id-3227144.html