- Minix Assignment
- Survey of a simple OS (e.g. Unix Sixth Edition)
- Concurrency from application programmer's perspective
- select system call
- threads
- mutual exclusion
- synchronization
- thread safety and the os
This early version (but not too early) of Unix is relative
simple and elegant.
For its features and the state of the hardware and demands in
1970, it provided "solutions" to many operating system
requirements.
A number of those "solutions" became deficient based on demands
and improvements in and speed of hardware components.
It has had a major influence on modern operating systems.
We have discussed role in operating systems of most of these:
- dual mode processors
- trap instructions
- system calls
- interrupts
- interrupt handlers
Process abstraction:
- address space
- text
- data
- bss
- heap (dynamically allocated: e.g. calling malloc)
- stack
Operating system data structures
- Process control block (e.g. a struct)
- Process Table
- Process File descriptor table
- System Open File table
Included
- process id (pid)
- location to save registers
- parent process
- pending signals
- signal masks
- process "state"
- location in memory of its program
Unix File system was:
- Tree structured with root directory and subdirectores
- Files named by paths separated by "/"
- Files accessed by aquiring a file descriptor with open
1
2 int main()
3 {
4 int fd; /* for file descriptor */
5 char buffer[1024];
6 int count;
7
8 if ( (fd = open("/home/gal/blob", O_RDWR) == -1 ) {
9 /* couldn't be opened */
10 perror("/home/gal/blob"));
11 exit(1);
12 }
13 if ((count = read(fd, buffer, 1024)) == -1) {
14 /* the read failed */
15 perror("read"); /* or printf("%s\n", strerror(errno)); */
16 exit(1);
17 }
18 /* buffer now contains count bytes that were read from the file */
19 ...
20 }
File descriptors 0, 1, and 2 are in use when a process is
created and correspond to standard in, standard out,
and standard error respectively.
These are ordinarily keyboard input, terminal output, and
terminal output.
This can be changed as in this code fragment:
1 if (fork() == 0) {
2 /* set up file descriptor 1 in child process */
3 close(1);
4 if (open("/home/me/blob", O_WRONLY) == -1) {
5 perror("/home/me/blob");
6 exit(1);
7 } else {
8 execlp("/home/me/prog", "prog", "50", (char *) 0);
9 printf("/home/me/prog: Could not execute.\n");
10 exit(1);
11 }
12
13 }
14
15 /* parent continues here */
16 ...
If the child reads from standard input (e.g. with
scanf or fgets(buf, N, stdin)
It will read from the file '/home/me/blob'
A successful open returns the first unused file descriptor:
the first (smallest) index in the descriptor table of the first unused
entry.
- Descriptor table - one per process
- System Open File Table - one for all open files by all processes
- In memory inode table

File descriptors are just subscripts for this table.
Entries are either NULL or else a pointer into the System
File table.

An entry in the system file table is made for each file that
is opened by some current process.
An entry contains
- current file position
- reference count - how many processes are pointing to this entry.
The reference count will be 2 if a parent has an open file
and forks a child.
But if two unrelated processes open the same file, there will
be two separate entries and the reference count in each will be 1.

For each file on disk, there is an inode for that file (also stored on the
disk) is a struct containing file information including
- creation, last modification times
- file owner
- file type (ordinary file, directory, named pipe)
- file access permissions
- location of data blocks on disk
When a file is opened its inode is read into an available slot
in the inode table in memory. The in memory inode table contains
a few more members including
- reference count - how many different file opens reference
this file.
- device (disk) where the inode is located.
Even if more than one process has a file open, there will only
be one entry in the inode table for that file, but the reference
count will reflect the number of uses of that inode.
Pipes provide a convenient communication between a parent
process and a child process.
The file system provides an implementation of pipes/
Redirection ccould be combined with pipes so that ordinary
input/output uses the pipes.
A simple pipe example (no redirection):
1
2 /**
3 /* p[0] and p[1] will hold file descriptors for the pipe
4 * p[0] descriptor is for reading
5 * p[1] descriptor is for writing
6 */
7 int p[2];
8 pipe(p); /* create a pipe (assume no errors) */
9
10 if ( fork() == 0 ) {
11 char buf[80];
12 close(p[1]); /* Child will only read from, not write to the pipe */
13 while( read(p[0], buf, 80) > 0 ) {
14 /* use data from the parent */
15 }
16 exit(0);
17 } else {
18 char buf[80];
19 close(p[0]); /* Parent will only write to, not read from the pipe */
20 int more_data_to_write = 1;
21 while(more_data_to_write) {
22 /* generate data in buf */
23 ...
24 write(p[1], buf, 80);
25 more_data_to_write = moreData();
26 }
27 }
A pipe provides only one-way communication.
If parent and/or child will read AND write, then 2 pipes are
needed.
If a process writes to the 'write' end of a pipe and then reads
from the 'read' end of the same pipe, it could read its own data.
Directories were themselves just very simple files organized
like this example:
/home/gal |
. |
120 |
.. |
18 |
docs |
132 |
projects |
128 |
.bash_profile |
126 |
hello.c |
160 |
/home/gal/docs |
. |
132 |
.. |
120 |
assign1.txt |
180 |
hello.c |
160 |
Each entry in a Unix 6th directory was a fixed size: 16
bytes
- 14 bytes for the name (not null terminate)
- 2 bytes for the inode number
The link system call permits creating a file reference in one
directory to an existing file in another directory. The names
don't have to be the same, but they refer to the same file
contents.
An example above is the file with inode number 160: hello.c
The referenced inode contains all the file information (permissions,
file size, last time of modification, location of blocks on disk,
etc.)
The inode also contains a link count.
Either file hello.c could now be 'deleted', but that would
- remove its entry from the directory where it was
deleted
- decrement the link count to 1
Suppose we call 'open' to open the file
"/home/gal/docs/hello.c"
How many inodes must be consulted?
- get the inode for "/"; then use it to read the entries of the
directory
The entry for home contains its inode number
- get the inode for home; use it to read the entries of /home
The entry for gal contains its inode number
-
get the inode for gal; use it to read the entries of
/home/gal
The entry for docs contains its inode number
- get the inode for docs; use it to read the entries of
/home/gal/docs
The entry for hello.c contains its inode number
-
get the inode for hello.c; finally(!) use it to
read the file hello.c
Ugh! Possibly lots of disk accesses!
The familiar protection is based on the smallest class
to which the executing process belongs: owner, group, other.
Smaller means: owner is 'smaller' than group, which is
'smaller' than other.
Suppose the current directory (".") contains two subdirectories
A and B
Suppose users tom and sue are members of the adm group, but charles
is not.
bash-3.2$ ls -lR
.:
total 8
drwxr-xr-x 2 tom adm 4096 Jul 21 16:49 A/
drwxr----- 2 tom adm 4096 Jul 21 16:50 B/
./A:
total 4
-rw-rw-rw- 1 tom adm 984 Jul 21 16:49 x
./B:
total 8
-r--rw-rw- 1 tom adm 302 Jul 21 16:50 x
-rw----rw- 1 sue adm 445 Jul 21 16:50 y
May charles list the contents of directory A?
May chalres read A/x?
May sue list the contents of directory B?
May sue modify B/y?
May tom modify B/x?
May tom modify B/y?
- Multithreaded Processes (POSIX threads; Windows
threads)
- Virtual memory
- First: 1960
- Unix: 1979
- Windows: 1993
- Mac: 1991 (but optional until ~2001 in OS X)
- Naming
- Files, directories, and devices used the same file
naming conventions
- Linux added the /proc 'directory' or file system that
names other objects including processes, kernel objects
-
Object references
- Unix/Linux uses the file naming extensions and file
system call operations on 'objects'
- Windows provides system calls, some with significant
differences for objects: processes, threads, files, pipes, etc.
-
Support for adding devices!
Some facilities for concurrency are different in an OS than for
the application programmer, but essential concepts are the
same.
Here is an example to get started:
1
2 int main(int argc, char * argv[]) {
3 fd_set rfds;
4 struct timeval tv;
5 int seconds = 5;
6 int retval;
7 const int MAXBUF = 120;
8 char buf[MAXBUF];
9 char magic[] = "select";
10
11
12 /* Get timeout seconds */
13 if ( argc > 1 ) {
14 seconds = atoi(argv[1]);
15 if ( seconds == 0 ) {
16 seconds = 5;
17 }
18 }
19 /* Watch stdin (fd 0) to see when it has input. */
20 FD_ZERO(&rfds);
21 FD_SET(0, &rfds);
22
23 /* Wait up to ? seconds. */
24 tv.tv_sec = seconds;
25 tv.tv_usec = 0;
26
27 setbuf(stdout, NULL);
28 printf("You have %d seconds to enter the magic word.\n", seconds);
29 printf("> ");
30 retval = select(1, &rfds, NULL, NULL, &tv);
31 /* Donb
32 t rely on the value of tv now! */
33
34
35 if (retval == -1)
36 perror("select()");
37 else if (retval) {
38 fgets(buf, MAXBUF, stdin);
39 buf[strlen(buf) - 1] = 0;
40 if (!strcmp(buf, magic)) {
41 printf("Correct!\n");
42 } else {
43 printf("Sorry, '%s' is not the magic word.\n", buf);
44 }
45 }
46
47 /* FD_ISSET(0, &rfds) will be true. */
48 else {
49 printf("\nSorry, %d seconds expired with no guess entered.\n",
50 seconds);
51 }
52
53 return 0;
54 }
The select system call lets you wait for a set of file
descriptors to be ready to use without causing blocking
that would occur if you chose one (say to read from) and it was
the wrong one (no input yet).
The rlogind program is/was excuted to permit remote logins from
a Unix client machine to a Unix server machine.
It gets input from a descriptor for the remote client, writes
to a descriptor for a pseudo terminal.
An application running on the server thinks its input is coming
from the pseudo terminal and so sends its output there.
The rlogind daemon has to also read the descriptor for the
pseudo terminal and write a descriptor to send the output to the
client.
Which of these 4 operations should it try first?
The select system call lets it repeatedly chose one that will
not block.
1
2 void rlogind(int r_in, int r_out, int l_in, int l_out)
3 {
4 fd_set in = 0, out;
5 int want_l_write = 0, want_r_write = 0;
6 int want_l_read = 1, want_r_read = 1;
7 int eof = 0, tsize, fsize, wret;
8 char fubf[BSIZE], tbuf[BSIZE];
9
10 fcntl(r_in, F_SETFL, O_NONBLOCK);
11 fcntl(r_out, F_SETFL, O_NONBLOCK);
12 fcntl(l_in, F_SETFL, O_NONBLOCK);
13 fcntl(l_out, F_SETFL, O_NONBLOCK);
14
15 while(!eof) {
16 FD_ZERO(&in);
17 FD_ZERO(&out);
18 if(want_l_read)
19 FD_SET(l_in, &in);
20 if(want_r_read)
21 FD_SET(r_in, &in);
22 if(want_l_write)
23 FD_SET(l_out, &in);
24 if(want_r_write)
25 FD_SET(r_out, &in);
26
27 select(MAXFD, &in, &out, 0, 0);
28 if (FD_ISSET(l_in, &in)) {
29 if ((tsize=read(l_in, tbuf, BSIZE)) > 0 ) {
30 want_l_read = 0;
31 want_r_write = 1;
32 } else
33 eof = 1;
34 }
35 if (FD_ISSET(r_in, &in)) {
36 if ((tsize=read(l_in, tbuf, BSIZE)) > 0 ) {
37 want_r_read = 0;
38 want_l_write = 1;
39 } else
40 eof = 1;
41 }
42 ...
}
}
void incoming(int r_in, int l_out)
{
int eof = 0;
char buf[BSIZE];
int size;
while(!eof) {
size = read(r_in, buf, BSIZE);
if (size <= 0)
eof = 1;
if (write(l_out, buf, size) <= 0)
eof = 1;
}
}
void outgoing(int l_in, int r_out)
{
int eof = 0;
char buf[BSIZE];
int size;
while(!eof) {
size = read(l_in, buf, BSIZE);
if (size <= 0)
eof = 1;
if (write(r_out, buf, size) <= 0)
eof = 1;
}
}