Skip to content

Comment on The Unix-Haters Handbook (1994) [pdf]parent

Comments

Norton Utilities for DOS came with a program called NCD (Norton Change Directory). It was a full-screen replacement for the CD command, you could browse through the filesystem to get to the directory you wanted, then it would exit and return you to COMMAND.COM's prompt, with you already in that directory. That worked because under DOS, the current directory was system-wide, [0] not per-process, so a subprocess could change the parent process' current directory. On Unix systems, and the Windows NT family, the current directory is per-process, so that doesn't work any more.

Now, you can implement the same idea on Unix–but it is more complex. You need to define a shell function in your .profile/.bash_profile/.zprofile/whatever. That shell function then executes the external change-directory program, and passes the destination directory back to the shell function somehow. The shell function then actually changes the directory.

What if shells had exposed some kind of standard API to their subprocesses? Maybe something as simple as an environment variable containing the path to a Unix domain socket which accepts a simple text-based protocol. You could then implement an NCD-style command, by having it send the shell which called it a request to change its directory. That way, you would not have to install such a command by modifying your profile and restarting your shell, the command would just work the first time you ran it.

Similarly, there are a lot of tools out there for supporting multiple versions of development tools concurrently (Environment Modules, venv for Python, nvm for Node, conda, etc.) Most of these tools work by manipulating your shell environment. Since, on Unix, environment variables are per-process, and a process cannot modify the environment of its parent, you have to install some shell function in your profile and restart your shell before using one of them. Once again, if shells exported some kind of "get/set shell environment" API to their subprocesses, these kind of tools would work the first time you ran them, without any need to modify your shell profile.

[0] Actually, in MS-DOS and Windows 3.x/9x/Me, there is both a system-wide current drive, and a separate system-wide current directory for each drive – Windows NT family's cmd.exe simulates this by using a hidden environment variable per a drive to store its current directory, but that is a convention which only cmd.exe supports, the rest of Windows knows nothing about it.

That worked because under DOS, the current directory was system-wide, [0] not per-process, so a subprocess could change the parent process' current directory.

For people who don't know DOS: DOS didn't really have multiple processes. COMMAND.COM made part of it resident (TSR), which is how it "survived" invoking other applications. At any given point, there was exactly one process running, and all the state you'd typically associate with a process was "system-wide".

For people who don't know DOS: DOS didn't really have multiple processes.

Well, compared to CP/M, it did. CP/M only supported a single program being loaded into memory at a time [0]–because every program was loaded into memory at the same absolute memory address (0x100). By contrast, DOS could have multiple processes in memory simultaneously, even though only one of them at a time could be the active foreground process. DOS processes can launch child processes; the parent will be suspended (while remaining in memory) while the child process executes, but will then resume execution once the child process finishes. Under CP/M, that was impossible, there was no API to do that, the architecture didn't support the existence of one.

Indeed, while the main execution of the parent process would be suspended while the child process ran, any interrupt handlers installed by the parent would still be invoked, so some of the parent's code could still run while the child was executing. A parent process can even expose an API to its children – which is exactly what COMMAND.COM does (such as INT 2E which I mentioned)

That's what makes TSRs possible. A TSR is essentially just an ordinary program, the only difference is that when it exits, it tells the OS to leave the process in memory rather than unloading it. By contrast, the CP/M equivalent to TSRs, RSXs (Resident System Extensions), are completely different from ordinary CP/M programs. They are loaded at a variable address near the top of memory, instead of a fixed address at the bottom.

COMMAND.COM made part of it resident (TSR), which is how it "survived" invoking other applications.

COMMAND.COM isn't a TSR. COMMAND.COM's "residence" is essentially the same as any other program which spawns a child program under DOS, the parent always remains in memory while the child runs. Unlike a TSR, when a COMMAND.COM instance terminates (the initial COMMAND.COM instance won't, but inferior instances will), it doesn't stay in memory, it is unloaded.

What makes COMMAND.COM somewhat unusual, is that it splits itself into two portions, a "resident" portion and a "transient" portion. The resident porition is loaded at the bottom of memory, the transient portion is loaded at the top of free memory, without being officially allocated. Since DOS normally allocates memory in a bottom-up manner, if the child program doesn't use much memory, the transient portion will not be overwritten. When the chlid program returns, COMMAND.COM tests the integrity of the transient portion – if it finds it has been overwritten (because the child program needed that much memory), it reloads it from disk before continuing. That's quite unusual behaviour, but still rather different from how TSRs behave.

At any given point, there was exactly one process running, and all the state you'd typically associate with a process was "system-wide".

That's not true. When you spawn a child process, you can either let it inherit your environment variables, or you can provide it with a new environment segment – thus each process in the system can potentially have different environment variables. Similarly, file handles are per-process. Similar to Unix – indeed, the DOS 2.x file handle design was copied from Unix – every handle has an "inherit" flag, which determines whether child processses inherit that handle from their parent. DOS keeps track of which processes have which files open, so when the last process with that file open terminates, the file is closed. Likewise, allocated memory blocks are per-process, so when a process is unloaded, DOS frees its memory blocks, but not those of ancestor processes or TSRs.

If DOS has per-process environment variables, file handles and memory allocations – why not current directories as well? I don't know, but I can speculate: DOS 2.0 copied the idea of a current directory from Unix, so like Unix they probably would have initially planned to make it per-process. However, requirements for backward compatiblity with DOS 1.x programs pushed them towards having a separate current directory per each drive. Keeping a separate current directory per-process might have been feasible if it was just a single current directory, but having to do so separately for each drive would have made it a lot more complex and wasteful of memory. I think that is why they made it system-wide instead of per-process.

Microsoft always planned to make MS-DOS a multi-tasking operating system, and much of its internal architecture was designed to support gradual evolution towards multi-tasking [1] – which makes its architecture somewhat closer to a multi-tasking operating system than that of a true single-tasking OS such as CP/M. In fact, they even developed a multi-tasking version of MS-DOS (the so-called "European MS-DOS 4.0" [2]) but it was largely unsuccessful. Eventually Microsoft gave up on the idea of making MS-DOS multitasking, but much of the ideas and experience they developed in trying to do so ended up going into OS/2 and Windows instead.

[0] CP/M later evolved into MP/M, which was a true multi-tasking operating system, but I'm not talking about that here

[1] See for example this design document in the MS-DOS 2.x source code: https://github.com/microsoft/MS-DOS/blob/master/v2.0/bin/DEV...

[2] https://www.pcjs.org/software/pcx86/sys/dos/microsoft/4.0M/

That's what makes TSRs possible. A TSR is essentially just an ordinary program, the only difference is that when it exits, it tells the OS to leave the process in memory rather than unloading it.

Yeah, I remember programming a bunch of those in TurboPascal. There were compiler directives to limit the heap size and some inline assembly was needed to save/restore the stack inside the interrupt handler, but once it worked, the rest was like a normal program.

It was very useful to have a calculator, taking notes, etc. Now we take multitasking for granted, of course.

On Unix systems, and the Windows NT family, the current directory is per-process, so that doesn't work any more.

But still it bites me everyonce in a while with cd's per-drive curent dir. pushd doesn't have this issue but I need to remember to use it. Usually after a failed cd on the other drive.

If you have a program 'finddir' that just prints out the directory, you could simply to 'cd `finddir`' or make that a shell alias 'ncd'.

AboutSource Built by g1lg1l

Hackerly is an independent reader for Hacker News, built on the public HN API. Not affiliated with Y Combinator.