一直以来对于execve的解释都是用新的可执行文件覆盖原有的进程地址空间,我一直以来都是这么理解的,man手册的解释: "This causes the program that is currently being run by the calling process to be replaced with a new program, with a newly initialized stack, heap, and (initialized and uninitialized) data segments."
本书中P84中原文也是: "将可执行文件的数据段和代码段载入当前进程的地址空间中".但是这个解释太具有误导性了,让我一直以为是直接覆盖当前现有的进程地址空间,这就导致在理解vfork的时候产生很大的不解,因为在vfork中子进程和父进程是完全共享地址空间的,那么子进程执行exec不会导致父进程的地址空间被覆盖吗?
其实是因为execve并不是覆盖操作,而是新创建一个虚拟地址空间,然后将之前的虚拟地址空间遗弃(引用计数-1),本书的P91中也提到了,但是由于当前几乎所有的解释都用了覆盖这个词语,因此具有很大的误导性,我建议本书在介绍exec的时候能更加详细的介绍其实现原理.

    HUST_Kingdo 感谢你的建议,很有意思的问题!
    结合vfork的部分,exec介绍的“载入当前进程的地址空间”的确可能会产生“地址空间直接覆盖”的误解。
    我简单看了一下three easy pieces里的介绍,写法也是“loads code...and overwrites its current code segment with it”。我认为这种写法可能有一个深层次的原因:如果单单考虑fork和exec两个系统调用,exec并不要求进程创建一个新的、独立的虚拟地址空间,所以即使理解成覆盖也不会有问题;但由于我们的教材介绍了更新的vfork接口,出现了多个进程直接共享同一地址空间的场景,这就对exec的实现有了更多约束。
    由于本书的受众也包括对内核不太熟悉的本科同学,因此我们希望对exec的介绍简单易懂,不引入过多细节。但由于出现了有歧义的地方,我们会考虑增加更多的解释。再次感谢!

    • qwe replied to this.
      a year later

      fork是创建新的页表(页表项跟父进程是一样的),vfork是父子进程共享页表,
      exec是: 如果没有页表要先创建页表再设置页表项,如果有页表则只更新页表项

      以上为个人理解

      3 months later

      书中在 execve 之后紧接着介绍了写时拷贝技术,我觉得误导性解释的说法没有太多根据。execve 使用场景是 fork 之后用来让进程执行其他二进制文件使用的,结合写时拷贝的介绍,我们知道当发生 execve 程序原来共享的虚拟内存出现了写操作(即载入新程序),因此会申请新的内存。载入的说法用词还是比较严谨的,并没有说直接覆盖到原来的内存空间。

      3 months later

      mingyu 我也是没太读明白书的这里。如果按照您所说的,fork()之后直接进行exec()时,这个exec()不会构造一套新的地址映射。
      那么fork()之后exec()和vfork()之后exec()都是共构造了一次地址映射(前者是fork()的时候构造,后者是exec()的时候构造),也没有效率浪费吧

        qwe 感谢你的问题!

        • "fork()之后直接进行exec()时,这个exec()不会构造一套新的地址映射"
          exec()是需要建立新的地址映射的,因为堆栈、代码段、数据段等部分都需要重新映射,而fork()之后建立的那些映射则需要移除。前面争论的焦点主要在于:fork()之后创建的虚拟地址空间数据结构,在exec()的时候要不要直接遗弃,再创建一个新的?但无论是否要遗弃虚拟地址空间,里面的地址映射都是要重新构造的。

        • “fork()之后exec()和vfork()之后exec()都是共构造了一次地址映射(前者是fork()的时候构造,后者是exec()的时候构造)”
          实际上fork()和exec()要构造两次地址映射(但也许虚拟地址空间数据结构只需要创建一次),vfork()和exec()只需要构造一次,这里就降低了开销。

        • qwe replied to this.
          Write a Reply...