Alright, I know that the submission deadline has passed, but I just spent the past couple of nights playing with this assignment —
as I understand it— and your professor is an a**.
I spent more time screwing around parsing that dependency graph than anything else.
The point of this homework is to
initialize and instantiate a static dependency tree, where nodes are processes and leaves are pipes. The flow of data through the pipes is automatically managed by the dependencies themselves. The hard part is connecting the pipes before creating subprocesses.
A very simple example:
input_var a,b;
internal_var p0;
a -> p0;
+ b -> p0;
write(a,b,p0). |
That's three nodes: a, b, and p0. (Four nodes if you count the parent process.)
But that's also five pipes:
+---+ +---+
| a | | b |
+---+ +---+
| | +----+ | |
| +------->| p0 |<------+ |
| +----+ |
| | |
| v |
| +------+ |
+-------->|parent|<-------+
+------+ |
Remember, your parent process also gets output from each node. Notice how 'b' has an output pipe to both 'p0' and to the parent process?
Had the 'write' statement read
write(a,p0).
you would not have that second pipe leading back to the parent process:
+---+ +---+
| a | | b |
+---+ +---+
| | +----+ |
| +------->| p0 |<------+
| +----+
| |
| v
| +------+
+-------->|parent|
+------+ |
There are a few caveats, too. You were not pointed to the
fdopen()
function, but you probably want that. Likewise, you must make sure to flush your output every time you write to pipe or it will not be readable on the other end. Finally, and this is important:
all pipes must be created before any subprocess is forked.
That is, you should have the entire tree figured out and ready before you begin forking.
I designed my tree with a simple struct, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
typedef struct var
{
char name[ 10 ]; // The name, like 'a' and 'p0'.
vartype type;
FILE* inputs [ 10 ]; // read end of a pipe
int ninputs;
FILE* outputs[ 10 ]; // write end of a pipe
int noutputs;
// input_var
double value;
// internal_var
char mathops[ 10 ];
}
var;
|
In main(), I keep a static array of var:
1 2
|
var vars[ 30 ];
int nvars = 0;
|
Finish that with a couple of functions to create and find a var:
1 2
|
var create_var( const char* name, vartype type );
var* find_var( const char* name, var* vars, int nvars );
|
Now I have a variable lookup table, and I can identify what kind of statement I got the var from: 'input_var', 'internal_var', or 'write'.
I have a function that takes a line of text and
strtok()
s it into a list of names and creates a var for each name with the appropriate type. I call it once each for the first line (input_var), the second line (internal_var), and the last line (write).
Next is to process the dependencies. I have a (not-so-little, about 40 lines long) function that takes a line of input and decodes the stupid dependency syntax into three things:
1 2 3
|
char mathop
char* varname0
char* varname1
|
Once found, I look up the names to find the vars, complain if they don't exist or are not the correct type, and then create the pipe that connects the two vars:
1 2 3 4 5 6 7 8 9
|
int fds[ 2 ];
if (!pipe( fds )) fooey();
v0->outputs[ v0->noutputs ] = fdopen( fds[ 1 ], "w" );
v0->noutputs += 1;
v1->inputs [ v1->ninputs ] = fdopen( fds[ 0 ], "r" );
v1->mathops[ v1->ninputs ] = mathop;
v1->ninputs += 1;
|
This same hookup is done with the list of 'write' vars.
So far, my main() looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
char s[ 1000 ];
var vars[ 30 ];
int nvars = 0;
// input_var
if (!readline( f, s, sizeof s, ';' )) fooey();
parse_varnames( s, vars, &nvars, input_var );
// internal_var
if (!readline( f, s, sizeof s, ';' )) fooey();
parse_varnames( s, vars, &nvars, internal_var );
// dependencies
while (readline( f, s, sizeof s, ';' ))
if (!parse_dependency( s, vars, nvars ))
break;
// write()
int nwritevars = nvars;
parse_varnames( s, vars, &nwritevars, write_var );
|
1The readline() function is a utility function I wrote to properly input a line ending on a given separator. You could just use fgets() directly in only a couple more lines of code each.)
2Also, I wanted to keep the write vars separate, hence the separate counter.
At this point we are ready to create our subprocesses.
The forking is pretty simple. The new process does not use main(). It should instead use a simple function and then terminate. I wrote a little procedure to help me fork. Here's a version that is fairly easy to read:
1 2 3 4 5 6 7 8 9 10 11 12
|
void fork_var( var* v )
{
switch (fork())
{
case 0:
if (v->type == input_var) exit( input_var_proc( var ) );
if (v->type == internal_var) exit( internal_var_proc( var ) );
case -1:
fooey();
}
}
|
If the fork fails, we just crash hard (fooey()).
The child determines which function to call, calls it, and terminates.
The parent simply returns from the function and pretends not to care that it just spawned a child.
Those two little functions are the core of your subprocesses. Here's my input var proc in all its glory:
1 2 3 4 5 6 7
|
int input_var_proc( var* v )
// An 'input_var' proces simply prints its stored value to all of its output pipes and terminates
{
while (v->noutputs--)
fprintf( v->outputs[ v->noutputs ], "%g\n", v->value ); // '\n' is important!
return 0;
}
|
That is, an 'input_var' process does nothing but print its stored value to all its output pipes and then terminates.
The 'internal_var' process is only slightly more complicated: it only needs to read from all its input pipes, in order, and apply the mathematical operation to a local variable. When done, it needs to print the resulting value to all its output pipes and terminate.
tl;dr: This is basically a loop with a switch statement for the read + mathop, followed by another loop just like in the input_var_proc.
Back to main(), we can effect all this forking and pipe I/O with a simple loop:
1 2
|
for (int n = 0; n < nvars; n++)
fork_var( vars + n );
|
The only thing remaining to do is print all the variables asked for in the 'write' statement:
1 2
|
for (int n = nvars; n < nwritevars; n++)
printf( "%g\n", read_double( vars[ n ].inputs[ 0 ] ) );
|
A good programmer would also be encouraged to go back and close all those pipe ends, but terminating the process does that automatically, so I didn't bother for either the parent or any subprocess. ;->
There is one other thing I did to make my life a lot simpler: utility functions. Like the that 'read_double()' function you see me using above. And the 'readline()' function to get a line of text from the dataflow graph. It makes it so much easier to program.
Really, the hardest part was parsing the dataflow graph. Once the whole thing is parsed, forking the child processes and letting them sort themselves with pipe I/O is easy.
I hope this is helpful.