18

I am running a stripped down version of BSD Unix V6, bkunix on my Elektronika BK 0010-01. It has a very limited range of Unix commands. The contents of /bin are:

  • cal
  • cat
  • clock
  • cp
  • date
  • df
  • echo
  • ed
  • halt
  • ln
  • ls
  • mkdir
  • mount
  • mv
  • od
  • pwd
  • rm
  • rmdir
  • sh
  • stty
  • sync
  • umount
  • wc

and the contents of /etc are:

  • fsck
  • glob
  • init
  • mkfs
  • mknod

There are also some shell built-ins like cd (though errors mention chdir, as per V6).

So, I am trying to run this shell script called hw using sh:

#!/bin/sh
echo "Hello, world!"

If I type

#./hw

I get the error message "./hw: not found"

If I type

sh hw (or sh hw.sh - I tried this too), I get "try again".

demonstration of the problem

I am guessing it is because the shell script is not executable, though unfortunately I do not know how to make it executable, as chmod is not implemented. The system appears to be aware of permissions, as you can see them when you run ls -l.

So my question are: Is "try again" probably connected to missing permissions, and if so, how might I change file permissions with only the shell builtins and the utilities listed above?

harlandski
  • 2,953
  • 14
  • 34
  • 8
    I would be interested to know more about your setup. Would you consider putting something up on your youtube channel? – Omar and Lorraine May 24 '23 at 09:31
  • 3
    Yes I'll definitely get round to the BK at some point, thanks for the encouragement! – harlandski May 24 '23 at 09:36
  • 4
    Your assumption that the #! syntax existed then, or even that # introduces a comment line, is unwarranted. Try making a script consisting just of the commands which must be executed. Also, does "sh" without any arguments work? If an error message is not displayed, try "ps" to confirm. – Leo B. May 24 '23 at 17:44
  • Please post text instead of images. – Barmar May 24 '23 at 22:02
  • You shouldn't use curly quotes in shell scripts. Either ' or ". – Barmar May 24 '23 at 22:04
  • @LeoB. Thanks, I'll try it. sh on its own does not throw an error, there is a small delay then the # prompt appears again. – harlandski May 25 '23 at 00:04
  • 1
    @Barmar the curly quotes are just the styling on this system I think, there are no double straight quotes, and the ones you see work with echo "Hello, world!" outside of a script. I'll experiment with single quotes, and with excluding them altogether (to do something else) – harlandski May 25 '23 at 00:08
  • 2
    I've used old systems where the curly quote was what you got for 0x22, normally ". Possibly the Amstrad PC1512. – Chris H May 25 '23 at 12:53
  • Yeah @Barmar We can rule out Unicode trickery, this is БК-0010-01. The encoding is the ASCII superset, KOI-7N1. – Omar and Lorraine May 26 '23 at 09:12
  • Do you still have the FOCAL on that box? If so I'd love to compare notes. – Maury Markowitz May 26 '23 at 18:29
  • Yes I have both the original FOCAL cartridge and a version on a Flash drive. I'd love to compare notes, how can we connect? – harlandski May 27 '23 at 14:12

2 Answers2

17

When the shell executes an external command (i.e., not something like cd or shift), it forks the process. The fork system call returns twice: once for the parent and once for the child.

  • The parent meddles with the file handles, and then waits for the child to exit.

  • The child does some error handling (checking that the file loads and is executable, etc, this would print any sensible error messages) and then executes the program.

The obtuse "try again" message is what's printed out when the shell fails to fork. This is when fork() returns -1.

As an aside, rm also forks (don't know what for yet) and prints a similar message if it fails to fork.


A few reasons why fork() would fail:

  • The system is out of memory
  • The maximum number of PIDs has been reached
  • At least Linux does not support forking if there is no MMU. I know the БК has no MMU, maybe the bkunix kernel doesn't support it
Omar and Lorraine
  • 38,883
  • 14
  • 134
  • 274
  • 13
    fork() works without MMU on ancient systems. You don't want to know how this was done. – Joshua May 24 '23 at 16:45
  • 7
    @Joshua: Actually, I think it's useful to understand how context switching used to work, since such understanding will make it obvious why fork() was designed as it was. Normally, switching between process #5 and process #12 would entail writing the present contents of process RAM to slot #5 on the swap drive and reading slot #12 from the swap drive into RAM. If process #12 wants to fork a new process which would be assigned #15, that would be accomplished by dumping the current contents of RAM to slot #12 and modifying the "current slot number" to 15 while leaving whatever was in RAM... – supercat May 24 '23 at 18:30
  • 6
    ...from process #12 wherever it happened to be. The next time there's a context switch or another fork, the current contents of RAM will get written out to slot #15. So fork() didn't have to make a copy of RAM for process #12--instead, all it had to do was refrain from loading slot #15 after performing the write that would have been needed when performing any kind of context switch. Of course, that design should have been replaced when it became possible to hold multiple programs in RAM simultaneously, but unfortunately Unix is still saddled with that design and consequent problems. – supercat May 24 '23 at 18:33
  • 4
    @supercat From where I sit, that design looks serendipitous. Separating fork() from exec() is handy, giving the forked child shell a chance to adjust the context before exec(). It also allows either by itself, which can be handy. – John Doty May 24 '23 at 19:03
  • 1
    @supercat: x86 UNIX did it with multiple processes active at a time and no MMU. This used the segmented architecture to move programs around in RAM. It had the drawback of no program could be bigger than 64kb code + 64kb data. No MMU meant memory safety was advisory. There was nothing stopping a program from changing es to point to kernel RAM and stomping stuff. – Joshua May 24 '23 at 19:11
  • 1
    @JohnDoty: The design means that if a process that has allocated 2GB of storage wants to spawn a small process, it will be necessary to either temporarily have an extra 2GB of storage available to commit, or else enable enable autocommit and make it impossible for programs to handle allocation failures. It also creates substantial difficulties with trying to support the existence of multiple threads within a process. I fail to see any substantive upside compared with having the old process create a "new process properties" object and pass that to a spawn() function. – supercat May 24 '23 at 19:48
  • 1
    @supercat That's what COW is for. – John Doty May 24 '23 at 19:51
  • 3
    @JohnDoty: That doesn't help with the needless-commit-or-overcommit issue. On a 4GB system, if a process with 1.6GB of allocations calls fork and another process requests 1GB of storage without the first child process having yet called exec, it's impossible to know whether or not all allocations will fit in the system's 4GB of actual storage. A system would either have to refuse the 1GB allocation even if the total amount of storage processes would actually need would be under 3GB, or report success without any guarantee that the storage will actually be available when needed. – supercat May 24 '23 at 20:08
  • 2
    @JohnDoty: On a spawn-based system, if a process with 1.6GB of allocations spawns a process that never needs more than 100K of storage, the system will know that the two processes combined never have more than 1.7GB of committed storage between them, and should thus pose no obstacle to another application wanting to allocate 1GB. – supercat May 24 '23 at 20:11
  • @JohnDoty: Or to address "that's what COW is for" more directly: COW adds a substantial amount of complexity which could be avoided if there were no need to handle fork+exec without wasting RAM. – supercat May 24 '23 at 20:17
  • @JohnDoty: Doesn't COW need an MMU/MPU? Or are you saying it works with architectures which do have an MPU that doesn't qualify to also be called an MMU? – Ben Voigt May 24 '23 at 21:10
  • 1
    Is Unix really feasible without fork()? If it can't fork, everything is running in one huge process. – Barmar May 24 '23 at 22:06
  • @Barmar- given that Unix existed before it had fork, I think it must have been feasible. Basically, the shell exec'd a command program, and then 'exit' from that program exec'd the shell back again. So, yes one 'process' per teletype, not necessarily 'huge' (mainly because there wasn't enough core to be huge in). – dave May 24 '23 at 22:40
  • 1
    @Barmar: Lots of people have gotten it working with vfork() only. – Joshua May 24 '23 at 23:29
  • 3
    @another-dave How was the process per teletype created? Didn't init have to fork login? – Barmar May 24 '23 at 23:30
  • Anyway, I don't think you can have anything even remotely resembling what we consider Unix, with scripts, pipelines, etc. without something like fork. OTOH, there was some work on emulating it in Multics, where each login session was a single process. – Barmar May 24 '23 at 23:32
  • I'm not sure if it has any bearing on your discussion that the BK0010-01 is a microcomputer with only 32KiB of RAM. I seem to remember the documentation of bkunix saying it can support multitasking for up to three processes. – harlandski May 25 '23 at 00:37
  • @Barmar - processes were created with the assembler :-) The PDP-7 had two teletypes, therefore two processes. See this article from Ritchie under the heading 'process control'. – dave May 25 '23 at 00:42
  • 5
    @Barmar, the alternative to fork() isn't "just a single process, ever", or "no pipes or anything", but something like posix_spawn(), where the parent process specifies the desired fd actions to execute in the child in some more or less declarative format instead of running code in the child's context to execute them manually. – ilkkachu May 25 '23 at 20:18
  • or well, I suppose it would be possible to keep the part where some of the parent code runs within the child process, just we'd need a way to specify which part of the parent code to keep (along with any working memory it needs). That part could be far shorter than the whole process. (Or we could have some bytecode language interpreted by the kernel (or some dedicated userland spawning library) that could be used to specify those actions (like BPF)) – ilkkachu May 25 '23 at 20:22
11

So, now having got the shell script to run, I can say the following:

bkunix does not recognize comments with # or shebang #! Under normal circumstances running:

sh {filename}

where the file starts with a commment or a shebang results in #: not found or #!/bin/sh: not found.

By removing the shebang line and having just the following line in my hw file:

echo "Hello, World!"

means that

sh hw

produces the desired output.

Why this was previously causing the 'try again' I can only guess at, based on @ГероямСлава's response. I had previously been testing out most of the commands I listed as being available in /bin, plus any shell builtins I could think of / draw from the Unix V6 documentation. I can only imagine that I had created too many forks already (I believe bkunix can only support 3 concurrent processes - presumably three additional processes to init and other essentials - though I cannot find the reference to this now). Unfortunately I am unable to thoroughly check this, as there is also no ps command, but I have been able to do the following test.

sh
sh
sh

The first two times produce a new shell with #, the third one produces "try again". This seems to confirm both the hypothesis that bkunix can run a maximum of three additional processes (the default instance of sh plus two more in this case), and that when this limit is exceeded, the error message "try again" is produced.

I am grateful to the various commenters who have led me to this solution, particularly @Leo B. I include a screenshot as evidence, though I have described everything in detail above.

result of typing sh three times

harlandski
  • 2,953
  • 14
  • 34
  • 1
    RE: "maximum of three processes at once": it's a number greater than three, since it must include init, which will have PID 1. There could be still other processes like device drivers or whatnot, though I don't know that. – Omar and Lorraine May 25 '23 at 08:20
  • 1
    Maybe three additional processes? As I said, I unfortunately can't find the reference any more, but 'three additional' does seem to be supported by my experiment above. – harlandski May 25 '23 at 13:36
  • @Героямслава: Possible the initial sh is pid 1? It looks like init doesn't call fork anywhere. – Joshua Jun 03 '23 at 04:12
  • @Joshua It's possible. I know of at least one other Unix-like with an arrangement like that. – Omar and Lorraine Jun 03 '23 at 06:46