1. 22
  1.  

  2. 4

    Very interesting, but the question remains: Why did 4BSD add a stack size limit?

    My guess: 4BSD also introduced some kind of shared memory threading. Unbounded stack growth is incompatible with multithreading in C (since threads in the same address space would run into each other’s stacks), so they would need to limit stack sizes.

    The fact that Unix didn’t have a stack size limit early on is a good illustration of how shared memory threading is counter to the Unix model. (the Unix model is processes passing messages)

    1. 12

      The fact that Unix didn’t have a stack size limit early on is a good illustration of how shared memory threading is counter to the Unix model. (the Unix model is processes passing messages)

      I feel like people put too much stock in there being a single, coherent “UNIX model” that hasn’t chiefly been assembled to justify the limitations of early machines and early software. Even if processes had been limited to a single thread forever, you would also have had to eschew all memory mappings other than brk and the thread stack. Also, there was a stack limit: when you bumped into the brk moving the other way in what was by modern standards an unimaginably small address space.

      1. 1

        I feel like people put too much stock in there being a single, coherent “UNIX model” that hasn’t chiefly been assembled to justify the limitations of early machines and early software

        Ah, actually operating systems before Unix (such as Multics) had shared memory. Unix deliberately chose to not have shared memory, it wasn’t a restriction of the available hardware and software.

        1. 3

          Ah, actually operating systems before Unix (such as Multics) had shared memory. Unix deliberately chose to not have shared memory, it wasn’t a restriction of the available hardware and software.

          Multics was, as far as I recall, developed on and for machines with a larger address space and in general more capacity than the original PDP-7 and PDP-11 systems that were the target for UNIX. I’d love to see a citation for choosing not to have shared memory on purpose as a permanent design goal, and not merely as an artefact of an early software implementation that had not yet grown a number of the features that make it useful. Recall that early UNIX also used whole-process swapping as a means for time sharing, which is also relatively simple to implement but not desirable as an end state.

          1. 3

            The Multics design of sharing memory through segments could work on any system, it wasn’t expensive in terms of hardware resources. I don’t have a citation on hand, but these facts:

            • segments (shared memory) were the core abstraction of Multics
            • Unix was famously designed in reaction to the failure of Multics
            • Unix does the exact opposite of Multics when it comes to sharing memory (i.e. Unix doesn’t have it at all)

            are pretty suggestive. Note also that Unix only got shared memory many years later after the original designers stopped working on it, and also that Unix was already deployed and used at many sites long before it got shared memory. (so shared memory is not really in the class of “features that make it useful”)

      2. 4

        Although I don’t know for sure, I think it matters that 4BSD was one of the earliest Unixes that ran on 32-bit architectures, which permitted large memory spaces, and ran in a semi-hostile environment (undergraduate students running student programs). You hardly needed memory limits with V7 on a PDP-11, which had at most 64 KB for your stack and data; a VAX provided far much more room for unfortunate memory usage events. For the record, 4BSD still had no threads and as far as I know no explicit shared memory of any form (whether System V shared memory or the modern mmap). Many things about V7 Unix are very simple and minimal compared to today, so not having process limits is not much of a surprise.

        (I’m the author of the linked-to entry.)

        1. 3

          Unbounded stack growth with a single thread just means it runs into the heap. AFAIK this was a “feature” of hardware of the time - on x86 you’d have SS==DS, both grow from opposite ends of the same segment, and programs can decide for themselves how to apportion that memory. But without some kind of boundary enforced by memory protection, when they collide the result is memory corruption that the kernel can’t protect.

          So I’ll bet the limit is because it’s desirable to have a boundary, and the limit was chosen to be extremely large, indicating runaway stack consumption more than an actual constraint. 4Mb is a lot of stack, particularly of that era.

          1. 3

            As I recall, 4BSD was the first UNIX system to support a paged MMU. It introduced the mmap system call, which was used to create anonymous memory mappings, shared memory mappings, and perform memory-mapped I/O. This meant that you had a fragmented address space for the first time.

            On traditional UNIX (note: the following paragraph contains gross oversimplifications), your memory abstraction was comprised of three / four segments in the address space (which may or may not have been enforced by an MMU, depending on the target). These were code (+ data from globals, sometimes in a separate segment sometimes part of the code segment), heap, and stack. The binary was loaded at the bottom of the address space. The stack started at the top of the address space. The heap grew from the top of the binary. On systems with a segmented MMU, each of these was one (or more) segments with permissions. The brk and sbrk system calls moved the line for ‘memory that can be used for heap’ up and for ‘memory that can be used for the stack’ down. Without an MMU, these told the kernel how much memory it needed to write out on context switch and allowed it to raise an error if they stack and heap segments overlapped.

            With a paged MMU, the address space could contain arbitrary mappings at arbitrary locations. You could still grow the stack with sbrk, but you might fail long before you ran out of address space because some other mapping was situated below the stack. The kernel therefore needed to track the size of the address space reservation for the stack, to prevent anything else being mapped there. Once you have a thing whose size you need to track, the obvious next step is to allow the size to be configurable. You already need to be able to track dynamic sizes for other kinds of VM object (e.g. file-backed mappings) so you don’t lose any space (and you do gain on generality) by making the stack something with a per-process configurable size.

            1. 2

              While BSD did introduce paged memory as part of their VAX port, the mmap() system call only came years later, in SunOS 4 (as far as I can determine). 4BSD still had a linear data space map with code / data / heap at the bottom, growing up, and stack at the top, growing down. Also, sbrk() doesn’t change the stack; both it and brk() affect the heap.