一、我为什么不喜欢 system 和 popen
要说到我为什么不喜欢 system 和 popen 这两个函数,这个说来就话长了。最开始,我还是很喜欢用这两个函数的,直到后来发现了太多因为滥用导致的程序异常后,它们就逐渐被我打入了冷宫。我认为一个设计良好的程序,完全是可以避开这两个函数的。这不,一周之内,我就收到了两个因为它们导致的 BUG 。
与其说我不喜欢这两个函数,倒不如说是不喜欢代码作者在不完全考虑好异常的情况下就使用它们^_^。
问题的现象是:线上程序功能失效,经过一番排查后,发现很多 popen 和 system 调用命令报错的日志。错误码是 12,12 的宏定义是 ENOMEM,说明是无法分配足够的内存导致命令执行失败了。
strerror(ENOMEM) = "Alloc memory fail"
但是实际上设备的剩余内存还有好几百兆,所以这个错误有点让人摸不着头脑了。在网上查询了一波后,几乎全是说创建子进程时子进程完全拷贝了父进程的内存导致的。虽然我比较认同这个观点,因为这两个函数底层实现都是创建子进程执行命令,但是并没有一个人能讲述清楚为什么创建子进程时会拷贝父亲内存。
因为在学习多进程时听到最多的一句话是 「读时共享,写时复制」,也就是说,子进程创建的时候,是共享父亲的内存的,不会完全拷贝内存到子进程,直到有数据修改才复制。这前后就逻辑就相违背了。
为了搞清楚这个问题,在 google 查了很久,也没有找到满意的答案。最后动手实践了一下,跟踪程序调用,发现是执行 system 时,程序并没有调用 mmap 来映射父进程的内存地址,想来 system 确实是不支持这个机制 (虽然不知道结论是否正确,但从现象和调试过程来看,比较靠谱) 。
二、 strace 调试
编写了一个简单的 system 调用程序:
1 2 3 4 5 6 |
#include <stdlib.h> int main() { system("echo helloworld >> ~/test.txt"); return 0; } |
编译,通过 strace 调用:
输出使用两个框框起来了,其中第一个框里面出现了大量的 mmap
和 munmap
,这些都是 strace 调用 system 命令产生的,不是 system 程序中的 system()
函数产生的。因为 strace 调试程序也是 fork() + execve()
来实现的 (这个从第一行的输出就能看出来),这些 mmap
调用就是父子进程在映射共享内存,也就说明了正常情况下创建子进程确实是遵循 「读时共享,写时复制」 的。
但是重点是第二个红框标出来的部分,这里才是 system 程序执行部分。可以看到,这里的 system()
函数是直接通过 clone 来创建新进程的,创建完成后父进程就调用 wait 等待子进程退出了,并没有执行 mmap 这些函数来共享父进程内存,因此也就不支持 COW 原则。
2020-04-05 追加:研究了一段时间后发现可能不是这个原因导致的。真实原因应该是系统连续内存不足导致的,内核分配新进程需要物理连续的内存空间,设备目前剩余的的内存都是碎片,虚拟内存系统中可能是连续的,但实际上物理内存并不连续,导致分配空间失败。
三、参考
ENOMEM from popen() for system(), while there is enough memory
评论