Computer Science from the Bottom Up
bottomupcs.com
bottomupcs.com
The fork+exec has many unobvious advantages over CreateProcess, but also disadvantages, and one of them is that it's, in my opinion, a completely unexpected approach: I can't think of any other system object that can only be created by first copying another already existing object and then overwriting the newly created one if needed. Files of any kind (folder/pipe/socket/etc)? Memory mappings? Signal handling? Process groups/sessions? Users/groups? The processes seem to be unique in this regard. How did people come up with this approach? That would be an interesting discussion, I think.
Instead, it's just described as the completely self-justified, with description of "zombies" thrown in the end for boots: and the zombie processes are pretty much an accidental artifact of early UNIX design; if the fork was returning not only globally visible (and therefore unstable) PID but also an fd associated with the child process, neither wait/waitpid syscalls nor reaping duties of PID 1 would have been necessary.
But with fork/exec, all of that setup becomes just normal code you run after fork but before exec. You want the child not to have a certain file descriptor? Just close it after fork. You want to drop privileges when running the child? Same thing, just setuid/setgid/setgroups after fork. You want to set up resource limits for the child? Again just setrlimit after fork.
It avoids a lot of complexity in the system call itself. (Naturally, it adds some other complexity elsewhere.)
Oh, and by the way: we have open()/fcntl() for opening files and then fiddling with their settings instead of one open() call with 20 arguments; we could easily have had launch() with the same parameters as execve() that would launch the new process suspended, then we could use... I dunno, even fcntl() on the process's descriptor to configure all those things and then send it SIGCONT when we're done setting it up.
Do we optimize our system call design for fresh students or for professionals?
(That's not why Unix developed with fork, though.)
It's quite clearly defined what gets duplicated and what gets shared on clone()/fork(). There really is no ambiguity.
Another possibility (pretty clearly not the case here) is when there are lots of replies: either you just miss it among the others, or ypu just can't be bothered to scroll down half a mile.
... the separation of fork() and exec() is essential in building a UNIX shell,
because it lets the shell run code after the call to fork() but before the call
to exec(); this code can alter the environment of the about-to-be-run program,
and thus enables a variety of interesting features to be readily built.
...
The separation of fork() and exec() allows the shell to do a whole bunch of
useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
In the example above, the output of the program wc is redirected into the output
file newfile.txt (the greater-than sign is how said redirection is indicated).
The way the shell accomplishes this task is quite simple: when the child is
created, before calling exec(), the shell closes standard output and opens the
file newfile.txt. By doing so, any output from the soon-to-be-running program wc
are sent to the file instead of the screen.
[1] https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-api.pdfI do remember reading the real explanation -- it was easier to implement fork. That's it.
UNIX fork/exec is also significantly faster than CreateProcess, but I'm not sure how much of that is necessary rather than contingent, or results of an ever growing list of "security" programs insisting on a veto. https://stackoverflow.com/questions/47845/why-is-creating-a-...
Fork+IPC architecture - effectively multithreading but where the processes don't automatically share memory and can crash separately - also requires fork(). However this has very much fallen out of fashion.
() I suppose you can emulate fork by calling CreateProcess with an empty executable and CREATE_SUSPENDED, then use various debug APIs to overwrite its memory map with your own, but this is very messy.
What are the advantages you see, that are not rooted in the fact that it's very difficult to create any remotely sane declarative API in a language as imperative and primitive as C? In other words which of these advantages would still apply if you wrote your OS in, say, Ocaml?
Splitting fork and exec allows you to roughly hew the process environment of a clone of the parent into shape, process state-wise, before you sacrifice it to birth the child-process which will inherit many of these desired (and generally a few undesired) traits. So one big advantage is that it allows you to re-use an existing (and growing!) set of imperative commands to modify aspects of a running process to specify the same aspects for a child process,and that piecemeal mutation is basically the only way to express anything of any complexity in C, particularly if you don't want to break API compatibility all the time.
The downside is that you generally end up inheriting a bunch of cruft that you really didn't want to, that the imperative and piecemeal nature opens up problems with race conditions, and that there is a lot of overhead only partially mitigated by various complex hacks (COW, various more "leightweight" fork alternatives, special flags to general purpose system calls that are only there to control behavior upon forking, ...).
> you don't want to break API compatibility all the time.
Indeed you don't, unless you're okay with forcing your users to rewrite all the libraries and/or applications every couple of years. That's true of any programming environment, imperative or not.
> various complex hacks (COW, various more "leightweight" fork alternatives
Copy-on-write is as much a hack as persistent data structures are: it's just more coarse-grained.
It comes from a hack in Unix for PDP-11 systems. This was before paged virtual memory. "Fork" worked by swapping the process out to disk. Then it duplicated the process table entry, with one entry set to the swapped-out copy and one set to the in-memory copy. This was simple to implement, and would still work if you invoked something big enough that both sides of the fork could not both fit in memory. Thus, a shell could invoke a big compiler. That's the real reason.
There are lots of other ways to do it. Some other systems have "run" as a primitive. Plan 9 offers all the options - share code, share file handles, share data. Windows has more of a "run" primitive. QNX has a system where a new process starts empty but attached to a shared object, with control starting in the shared object. The shared object then does the work of loading the program to be executed, so the OS doesn't have to.
Unfortunately, with threads, you're now stuck with locks that are potentially held by threads that no longer exist. So you can't call any function that may potentially acquire a lock, like printf, or your program may randomly hang. This feels... inelegant.
After a fork(2) in a multithreaded process returns in the child,
the child should call only async-signal-safe functions (see
signal-safety(7)) until such time as it calls execve(2) to
execute a new program.
And since any process can be multithreaded thanks to shared libraries being able to internally do whatever the hell they want to... yeah. Python, for example, in its implementation of subprocess module, had to shift a lot of work into pre-forked parent (such as disabling the GC and allocating all of the memory for exec()'s arguments), to add explicit "call_setsid" argument to reduce the usage of "preexec_fn", and even then, it still has lovely comments such as /* We'll be calling back into Python later so we need to do this.
* This call may not be async-signal-safe but neither is calling
* back into Python. The user asked us to use hope as a strategy
* to avoid deadlock... */
Well, hope is usually not a valid thread-safety strategy, but there is really not much else the Python implementation can do.Still seems like a good resource, just perhaps mislabeled.
Definitely not computer science.
We had another called "Operating Systems" that focused on things like concurrency, building a really basic memory pager, etc (mix of C and Java I think)
And we had one other called "Networking" that was sort of along the same lines as "Operating Systems" but focused on, well, networking. Mostly Java; we wrote our own application-layer protocols, that sort of thing
But yeah pretty much everything else - including the more "concrete" programming courses like Data Structures and Algorithms - was fairly abstracted away from any specific system
I'd agree with this, but it fits with how a certain set of schools teaches their core. They will teach a set of related classes around the topics here: computer architecture, operating systems, and networking, because this is often central to their research interests. Others will scatter this information across a number of unrelated courses to check the box, or skip it entirely because their research doesn't touch as heavily on these topics.
Further research on the author suggests they are from New Zealand. I'm from the UK and live in the US. What I see taught in US CS degrees is somewhat different from what I saw in the UK, and heard about from friends in continental Europe. I imagine the author's experience is similar.
A further bias from the author, and I'm making big assumptions, is that see value here because they've worked in places that do lot of system programming. Mentions of VMware and Red Hat on https://www.wienand.org.
I think this is a good system - and also indicates that we are speaking somewhat past each other on what a “computer science” education looks like.
1. BSc, BEng, MSc monikers to be meaningless across universities, but to be used consistently at the establishment.
2. Multiple degree paths to be available that went from something advertised as computer science to computer engineering.
3. A straightforward path directly into master's level computer science education.
You couldn't make an assessment based on BS vs BA because the curriculum may be similar in both cases. Oxford used BA whereas Durham used BS with both producing solid graduates that can't be differentiated in most work (further research or industry.)
My experience in the US is that hiring has kind of perverted the nature of discussions like these. The focus is on a few select algorithms and maths courses whereas the folks who've done the systems work are arguably better prepared for many real world tech roles. The last half of your degree has to be more interesting and reflective of the individual, right?
MA versus MS is different (I think similar to the difference between a M.Phil versus an M.Sc). But I’ve never seen anyone dwell much on BA/BS.
In fact, it seems to specifically be trying to have a different focus than a cs degree:
> This book aims to move in completely the opposite direction, working from operating systems fundamentals through to how those applications are complied and executed.
> In a nutshell, what you are reading is intended to be a shop class for computer science. Young computer science students are taught to "drive" the computer; but where do you go to learn what is under the hood?
And “Shop Class For Computer Scientists” or “Systems Engineering From The Bottom Up” would have been better titles. It’s simply misleading to suggest a course comprehensively covers “computer science” without covering algorithms or complexity, among many other things.
Computer science is about algorithms, complexity, possibly foundations of programming languages, cryptography, database theory, those things. Not about Unix or file formats.
Those are important too, but not "computer science".
But I guess that would be computer science from the top down.
One could argue it's from the very bottom up, not much practicum lies in that text.
Baffingly enough: All this theory is easily understood with 7th-grade math.
Approaching from some specific piece of engineering is surely top-down.
The "bottom" of computer science would be mathematically explaining what it means to compute, and building up to the abstraction of a Turing machine, and eventually getting to a concrete implementation of what we call programming and code. This book has code on page 2 - but code is nowhere near the bottom.
Specifically "mathematically explaining what it means to compute, and building up to the abstraction of a Turing machine"
Thanks
It's pretty much "computation theory for the everyday programmer". It uses Ruby to implement things like Turing Machines and the lambda calculus. Extremely accessible, and a lot of fun.
He proposes a file clerk that gets progressively dumber and faster until they get so dumb that they can be simulated by an electronic circuit.
https://news.ycombinator.com/item?id=13249675 (2016, 157 comments)
https://news.ycombinator.com/item?id=21903007 (2019, 77 comments)
Computer Science from the Bottom Up - https://news.ycombinator.com/item?id=7611093 - April 2014 (44 comments)
These are all engineering concerns, not science.
Of course, Computer Science is almost no science, most degrees are some blend of math and engineering.
no no no no no no no no
It is sort of like RISC on the inside (uOps), but x86 is __not__ RISC on the outside, which means it has to have a thumping great decoder and a bunch of microcode using power all the time.
Locally even if the microarchitecture could be considered a RISC, the architecture of an X86 computer cannot be considered RISC both on account of the irregularity of the instruction set and the limits to instruction-decode throughput because reasons already mentioned.
> Even the most common architecture, the Intel Pentium, whilst having an instruction set that is categorised as CISC, internally breaks down instructions to RISC style sub-instructions inside the chip before executing.
Issue-width isn't everything, but ARM chips are really showing the limitations of x86.
Successful modern architectures have adopted features of both. Even the most RISC architecture these days likely has AES instructions. Even the most CISC architecture is using RISC like micro ops.
AFAIK these sorts of instructions are typically things that would be trivial to implement in hardware but extremely cumbersome to implement in software, like shuffling a bunch of bits around (ARM's "JavaScript instruction" is another famous example). These sorts of things would only require a single micro-operation (or a very small number of uops).
My understanding is that the big distinction between CISC/RISC comes from things like addressing modes, by which a CISC processor lets you cram many hardware operations into a single software instruction. For instance, on x86 you can write an instruction like 'ADD rax, rbx, [rcx + 0x1234 + 8*rdx]' that performs several additions, a bit shift, and a memory access. Whereas on ARM, you would have to split those multiple hardware operations into multiple instructions.
> Even the most CISC architecture is using RISC like micro ops
Yes, but as the parent comment points out, that requires a complicated decoder with limited throughput. Using a RISC architecture throughout therefore moves some (but not all) of that instruction-decoding work into the compiler.
(I'm not at all knowledgeable in the field of processor design, so I would be happy to be proven wrong.)
Also, Computer Science is a much broader field than what this website addresses including very little science but actually a mixture of math (e.g. P vs NP stuff) and engineering (both hardware and software), this website addressing a subset of the engineering part.
* TI58C programmable calculator, a handfull of registers, load store, conditional jump, that's it, essentially a very simple assembly-like language. * then CBM basic, two letter variable names, only globals, line numbers and goto/gosub * then (turbo) pascal with scopes, still single process unicore everything * then a little 6502 assembly (not my cup of tea) * then OS/2 and unix, and Linux when it started, multiprocessing (and multi user) on unicore * ...and proper computer science (as in von Neuman, Turing, Chomski hierarchy, compilers, sorting searching and complexity, graphs, etc) * then chroot * then virtualization (on the mainframe lol, but with Linux) * then containers and in parallel to the virtualization axis threads and green threads
I'm grateful this allowed me to grow into the concepts step by step and grow an intuition for what's happening.
and I wonder if there is some kind of "bottom up" curriculum somewhere that follows such a path?
CE largely extends its focus on software up to networking and O/Ses and down to compilers and instruction sets. And on the hardware side, from motherboards, interconnects, and peripherals down to CPUs, with side trips into non-PC architectures like SoCs (e.g. Raspberry Pi) and GPUs.
The CE emphasis seems to be on the engineering tradeoffs of computer hardware design and implementation (at the symbolic level) and how it interacts with software. However, CE doesn't seem to take the next step down into VLSI or power or heat management or chip failure or transistor microelectronics, largely which remain the purview of EEs.
Not all CE programs are strict about these emphases, however. Some adopt the name CE without moving appreciably away from mainstream CS. I suspect the name 'CE' better serves some professors' priorities and helps them sell their work as being rooted in engineering rather than in the abstractions and symbols rooted in math, from whence CS came.
Are the terms describing the two approaches here "empirical" versus "rational"? I'm talking about building bit by bit as opposed to getting an overview and then drilling down. I definitely feel like I benefit from the second. And I almost feel like it may not be too strong to suggest that I'm actively harmed by the first.
Something like the author's approach is great, for my purposes, when I already pretty much understand something. But I can't start here.
Again, I feel like I'm in a minority. But anyone else have this turn of mind, too? How do you cope? Do you just look for other materials?
Top-down takes time and reflection, and re-packaging.
Some earlier discussion:
1 year ago https://news.ycombinator.com/item?id=21903007
4 years ago https://news.ycombinator.com/item?id=13249675
7 years ago https://news.ycombinator.com/item?id=7611093
Anyway, learning is basically just a long conversation where we learn about the feedback loops about whatever concept or system we are trying to understand and interact with. For me this author's style (and others like him) works a million times better to do that in an intuitive way.
>Computer Science is no more about computers than astronomy is about telescopes.
IMO relays are a bit easier to understand than transistors because it's a bit easier to understand how to implement various logic gates with them with only very basic knowledge of electronics.
Also, very satisfying clicking sounds: https://www.youtube.com/watch?v=JZyFSrNyhy8&t
Atomic Operations
Explain what it is.
and in case anyone is curious: https://wiki.osdev.org/Atomic_operation
“Computer science” is a terrible name. Astronomy is not called “telescope science”, and biology is not called “microscope science”.
The "computer" in "computer science" is referring to turing machines, automata, etc, which are subjects of theory of computation, which studies the nature of computable functions.
These are the 'bottom up' of computer engineering, which are implementation details (my course included semiconductor physics as well). Computer science tends to look at theoretical constructs like Turing machines instead of these. The remaining topics which are indeed computer science.
If you want to learn from bottom up, then learning what programs are seems like an obvious approach.