一、我为什么不喜欢system和popen

要说到我为什么不喜欢system和popen这两个函数,这个说来就话长了。最开始,我还是很喜欢用这两个函数的,直到后来发现了太多因为滥用导致的程序异常后,它们就逐渐被我打入了冷宫。我认为一个设计良好的程序,完全是可以避开这两个函数的。这不,一周之内,我就收到了两个因为它们导致的BUG。

与其说我不喜欢这两个函数,倒不如说是不喜欢代码作者在不完全考虑好异常的情况下就使用它们^_^。

问题的现象是:线上程序功能失效,经过一番排查后,发现很多popen和system调用命令报错的日志。错误码是12,12的宏定义是ENOMEM,说明是无法分配足够的内存导致命令执行失败了。

strerror(ENOMEM) = "Alloc memory fail"

但是实际上设备的剩余内存还有好几百兆,所以这个错误有点让人摸不着头脑了。在网上查询了一波后,几乎全是说创建子进程时子进程完全拷贝了父进程的内存导致的。虽然我比较认同这个观点,因为这两个函数底层实现都是创建子进程执行命令,但是并没有一个人能讲述清楚为什么创建子进程时会拷贝父亲内存。

因为在学习多进程时听到最多的一句话是“读时共享,写时复制”,也就是说,子进程创建的时候,是共享父亲的内存的,不会完全拷贝内存到子进程,直到有数据修改才复制。这前后就逻辑就相违背了。

为了搞清楚这个问题,在google查了很久,也没有找到满意的答案。最后动手实践了一下,跟踪程序调用,发现是执行system时,程序并没有调用mmap来映射父进程的内存地址,想来system确实是不支持这个机制(虽然不知道结论是否正确,但从现象和调试过程来看,比较靠谱)。

二、strace调试

编写了一个简单的system调用程序:

#include <stdlib.h>

int main() {
    system("echo helloworld >> ~/test.txt");
    return 0;
}

编译,通过strace调用:

输出使用两个框框起来了,其中第一个框里面出现了大量的mmapmunmap,这些都是strace调用system命令产生的,不是system程序中的system()函数产生的。因为strace调试程序也是fork() + execve()来实现的(这个从第一行的输出就能看出来),这些mmap调用就是父子进程在映射共享内存,也就说明了正常情况下创建子进程确实是遵循“读时共享,写时复制”的。

但是重点是第二个红框标出来的部分,这里才是system程序执行部分。可以看到,这里的system()函数是直接通过clone来创建新进程的,创建完成后父进程就调用wait等待子进程退出了,并没有执行mmap这些函数来共享父进程内存,因此也就不支持COW原则。

三、参考

ENOMEM from popen() for system(), while there is enough memory

最后修改:2020 年 03 月 22 日
如果觉得我的文章对你有用,请随意赞赏