1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
|
.Dd September 1, 2021
.Dt V6-PWD 7
.Os "Causal Agency"
.
.Sh NAME
.Nm V6 pwd
.Nd deciphering old code
.
.Sh DESCRIPTION
We were talking about
.Xr wall 1
on IRC
and how long it had been annoying users.
My manual page says
.Xr wall 1
appeared in
.At v6 ,
which means that
.Xr wall 1
has been annoying users for 46 years!
.
.Pp
The Wikipedia page links to the source for
.At v6 ,
so I was curious to see how the very first
.Xr wall 1
was implemented.
It's not that surprising,
except that it is hardcoded
to handle only 50 logins,
and it forks to write to each tty,
waiting one second between each.
I think the forking must be to avoid
any of the terminals being opened
from becoming the controlling terminal
of the original
.Xr wall 1
process.
.
.Pp
Then I started looking
at some of the other source files
and found the implementation of
.Xr pwd 1 ,
which was surprising.
There's no
.Xr getcwd 3
function
(the earlier form of which,
.Xr getwd 3 ,
appeared in
.Bx 4.0 ) ,
so
.Xr pwd 1
has to figure out
the path to the working directory itself.
It took me a while to figure out how it works.
.
.Pp
To make it easy to talk about,
I'm just going to include the whole thing here:
.Bd -literal
char dot[] ".";
char dotdot[] "..";
char root[] "/";
char name[512];
int file, off -1;
struct statb {int devn, inum, i[18];}x;
struct entry { int jnum; char name[16];}y;
main() {
int n;
loop0:
stat(dot, &x);
if((file = open(dotdot,0)) < 0) prname();
loop1:
if((n = read(file,&y,16)) < 16) prname();
if(y.jnum != x.inum)goto loop1;
close(file);
if(y.jnum == 1) ckroot();
cat();
chdir(dotdot);
goto loop0;
}
ckroot() {
int i, n;
if((n = stat(y.name,&x)) < 0) prname();
i = x.devn;
if((n = chdir(root)) < 0) prname();
if((file = open(root,0)) < 0) prname();
loop:
if((n = read(file,&y,16)) < 16) prname();
if(y.jnum == 0) goto loop;
if((n = stat(y.name,&x)) < 0) prname();
if(x.devn != i) goto loop;
x.i[0] =& 060000;
if(x.i[0] != 040000) goto loop;
if(y.name[0]=='.')if(((y.name[1]=='.') && (y.name[2]==0)) ||
(y.name[1] == 0)) goto pr;
cat();
pr:
write(1,root,1);
prname();
}
prname() {
if(off<0)off=0;
name[off] = '\en';
write(1,name,off+1);
exit();
}
cat() {
int i, j;
i = -1;
while(y.name[++i] != 0);
if((off+i+2) > 511) prname();
for(j=off+1; j>=0; --j) name[j+i+1] = name[j];
off=i+off+1;
name[i] = root[0];
for(--i; i>=0; --i) name[i] = y.name[i];
}
.Ed
.
.Pp
First, some syntax trivia:
it seems you don't need
.Sy =
to give globals values.
I guess that makes sense.
I also noticed that
it avoids giving
.Va inum
and
.Va jnum
the same name.
I think that's because in old C,
struct field names all shared the same namespace.
The last difference I noticed
is the operator
.Sy =&
rather than
.Sy &= .
Honestly I think the former makes more sense,
but I can see that the one we have now
is less ambiguous.
.
.Pp
To get
.Fn prname
and
.Fn cat
out of the way,
it's building up a path from the bottom.
At first I thought it must be
starting at the end of its buffer
and moving back as it adds components,
but no,
it moves the entire path-so-far over
every time it adds a new component
onto the front.
.Fn cat
is just a bunch of manual string copying.
It also gives up
if the new component
would make the path longer than 511 characters.
Fair enough.
.
.Pp
So how does it build up the path?
The loop in
.Fn main
first calls
.Xr stat 2
on the current directory
.Pa \&.
in order to get its inode number.
I love that
.Vt struct statb
is just declared at the top of this file.
Clearly this code predates the C preprocessor.
.
.Pp
It then opens the parent directory
.Pa ..
and reads directory entries from it.
The inner loop is looking for
a directory entry with the same inode number
as the current directory,
to figure out what the current directory is called.
Curiously,
it reads 16-byte directory entries,
despite declaring a larger struct.
The preprocessor can't be invented soon enough.
.
.Pp
Once it finds the matching directory entry,
it adds the name of the entry
onto the front of the path,
changes directory to
.Pa ..
and starts over.
It stops when the current directory
has an inode number of 1,
which must be the root of a file system,
but then it does something else.
It took me a while to decipher what
.Fn ckroot
is doing.
.
.Pp
The loop in
.Fn main
stops when it gets to the root
of a file system,
but that's not necessarily
.Pa / .
I think what
.Fn ckroot
is doing is trying to figure out
where that file system is mounted.
It starts by checking the device number
that the current directory is on.
Or really it calls
.Xr stat 2
on the name of the directory entry that
.Fn main
just found,
which I think must be
.Pa \&.
at this point anyway since it's at a root.
.
.Pp
Anyway,
it then changes directory to and opens
.Pa /
and starts reading directory entries from that,
calling
.Xr stat 2
on each of them
and checking for a matching device number.
I think this implies that file systems
can only be mounted in
.Pa /
and not at any lower level,
at least not if you want
.Xr pwd 1
to understand it.
I'm not sure what the check for
an inode number of 0 is skipping over
in this loop.
Some kind of special entry in
.Pa /
perhaps.
.
.Pp
Once it finds an entry
with a matching device number,
it checks the flags
to make sure the entry is a directory.
It does so with hardcoded constants,
but it seems they haven't changed
in all these years.
According to
.Xr stat 2 ,
040000 is
.Dv S_IFDIR .
The number of file types
clearly has grown since then though,
since
.Dv S_IFMT
is now 0170000 rather than 060000.
.
.Pp
I think the reason it checks
that the entry is a directory
is because if it actually is
on the root file system already,
then any regular file
would have a matching device number.
If the entry is indeed a directory,
it then checks if the entry is
.Pa \&.
or
.Pa \&.. ,
which indicates that it really is already at
.Pa / .
If it's not,
it adds the mount point that it found
to the front of the path.
.
.Pp
Finally,
it prints
.Pa /
followed by the path it built up.
If it failed at any point before that,
it would print the path it had built so far
with no leading
.Pa / .
Better than nothing!
.
.Pp
So that's how I think
.Xr pwd 1
works in
.At v6 .
It was a fun puzzle to work through,
and it was interesting to see
the assumptions it makes.
How simple things were back then...
Actually I find it really cool
that code from 1975
can still be read and understood
using knowledge of modern C and UNIX-likes.
.
.Sh SEE ALSO
.Lk https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6
.Pp
.Pa pwd.c
appears in
.Pa V6/usr/source/s2 .
.
.Sh AUTHORS
.An june Aq Mt june@causal.agency
.Pp
I regret saying in two previous posts
what I planned to write next,
because this is still not that.
|