Exercise 2: A Grandchild Inherits From Its Own Parent, Not the Original Ancestor — Possible Solution ==================================================================== THE TEST ------------------------------ gp_fd = syscalls.invoke(grandparent.pid, 'open', '/grandparent_file') parent_pid = syscalls.invoke(grandparent.pid, 'fork') parent_fd = syscalls.invoke(parent_pid, 'open', '/parent_own_file') grandchild_pid = syscalls.invoke(parent_pid, 'fork') # fork the PARENT RESULT ------------------------------ grandparent's own fds: {0: '/grandparent_file'} parent's own fds: {0: '/grandparent_file', 1: '/parent_own_file'} grandchild's own fds: {0: '/grandparent_file', 1: '/parent_own_file'} The grandchild ends up with BOTH files -- the one originally opened by the grandparent, AND the one opened later by the parent. The grandparent's own table is unaffected by the grandchild's own existence. WHY THE GRANDCHILD GETS BOTH FILES ------------------------------ _do_fork()'s own implementation copies from whatever pid was actually passed in as the argument: def _do_fork(self, pid): child_pcb = self.kernel.create_process(num_pages=0) self.open_fds[child_pcb.pid] = dict(self.open_fds.get(pid, {})) ... The FIRST fork() call passed grandparent.pid, so the parent's own table starts as a copy of {0: '/grandparent_file'}. After the parent then opens its own second file, its own table becomes {0: ..., 1: '/parent_own_file'}. The SECOND fork() call passes parent_pid, not grandparent.pid -- so it copies from the PARENT's own CURRENT table at that moment, which already contains both files. The grandchild therefore inherits both, correctly reflecting everything its own real ancestry chain had accumulated by the time it was created. WHY IT INHERITS FROM ITS OWN PARENT, NOT A FIXED ANCESTOR ------------------------------ fork() has no concept of "the original ancestor" anywhere in its own implementation -- it only ever knows about the ONE pid it was given for this specific call. Each fork() is a completely independent, self-contained operation: copy from whoever you're told to copy from, right now, as that process's own table stands at this exact moment. This is exactly how real process trees work -- a grandchild's own environment reflects everything its own direct lineage did, not just whatever the very first ancestor happened to set up before any intermediate generations made their own changes. WHY THIS WORKS AS AN ANSWER ------------------------------ Having the parent open a SECOND file specifically between the two fork() calls -- rather than forking twice in a row with no changes in between -- creates a genuine test of whether inheritance is "snapshot from the original ancestor" or "snapshot from the immediate parent, whatever state it's currently in." The grandchild ending up with both files, not just the grandparent's original one, confirms it's the latter.