The system call ptrace allows a parent process the control over one of its child processes which must be created be the system call fork. The synopsis of the call is:
int ptrace (int request, int pid, int addr, int data);
The child process is usually executed normally, until it receives
a signal. It then stops and informs the parent process about its
actual state by the system call wait. The controlling process
may then read or write the child process' memory by using ptrace.
The parent can also force the child to either resume or stop execution.
However, the most interesting request for timing analysis is
PTRACE_SINGLESTEP. This command sets the trace-bit within the
child's processor-state-word and enables the parent process to
trace the child's machine instructions. In this case, the child stops
after every machine instruction execution and sends a signal to its
parent. An example program for tracing machine instructions could be
implemented as follows:
#include <stdlib.h>
#incldue <iostream.h>
#include <signal.h>
#include <syscall.h>
#include <sys/ptrace.h>
void main (void) {
long long counter = 1; // machine instruction counter
int wait_val; // child's return value
int pid; // child's process id
switch (pid=fork()) {
case -1: perror("fork"); break;
case 0: // child process starts
ptrace(0,0,0,0);
// must be called in order to allow the
// control over the child process
execl("./testprog","testprog",NULL);
// executes the testprogram and causes
// the child to stop and send a signal
// to the parent, the parent can now
// switch to PTRACE_SINGLESTEP
break;
// child process ends
default: // parent process starts
wait(&wait_val);
// parent waits for child to stop (execl)
if (ptrace(PTRACE_SINGLESTEP,pid,0,0) != 0)
perror("ptrace");
// switch to singlestep tracing and
// release child
wait(&wait_val);
// wait for child to stop at next instruction
while (wait_val == 1407) {
counter++;
if (ptrace(PTRACE_SINGLESTEP,pid,0,0) != 0)
perror("ptrace");
wait(&wait_val);
}
// continue to stop, wait and release until
// the child is finished; wait_val != 1407
// Low=0177L and High=05 (SIGTRAP)
} // end of switch
cout << "Counter: `` << counter << endl;
} // end of main
In order to provide an efficient machine instruction counting, the test
program should be linked statically, like: c++ -static testprog.cpp.
Hence, setting up and dynamically linking the run-time libraries, which
is ``computationally expensive'', is avoided for each run of the test
program.
A Genetic Algorithm must execute the fitness function many times. Hereby,
the test program's parameters must be passed into the fitness function
and the resulting fitness must be passed back. The latter does not
create any particular problems, as the machine instructions are counted
within the GA procedure (parent process). But passing parameters into a
process in order to call the test program (function within the process)
requires some additional installations in the form of Inter Process
Communication (IPC). A simple methodology for IPC is provided by
pipes.
A Unix pipe is a data stream which can be accessed as a file. Consequently, the calling GA process can open a pipe and write its data (parameter) into it. The called process inherits all of the parent's file descriptors and is able to read the data from the pipe. The called process may finally execute the actual test program. The GA fitness function can be implemented as follows:
double fitness (int size,
unsigned char* parameter_string,
char* exename) {
int pid; // child's process id
int pfd[2]; // pipe descriptor
char str_size[10]; // parameter 1 for executable
char str_pfd[10]; // parameter 2 for executable
if (pipe(pfd) == -1) perror("pipe"); // create pipe
switch (pid=fork()) { // create new process
case 0: { // child process
ostrstream os1 (str_size,10); // stream on string
ostrstream os2 (str_pfd,10); // for strcat
if (close(pfd[1]) == -1) perror("close");
os1 << size;
os2 << pfd[0];
ptrace(0,0,0,0);
execlp(exename,exename,str_size,str_pfd,NULL);
break;
}
default: // parent process
if (close(pfd[0]) == -1)
perror("close");
if (write(pfd[1],parameter_string,size) == -1)
perror("write");
wait(&wait_val);
if ptrace(PTRACE_SINGLESTEP,pid,0,0) != 0)
perror("ptrace");
wait(&wait_val);
while (wait_val == 1407) {
counter++;
if (ptrace(PTRACE_SINGLESTEP,pid,0,0) != 0)
perror("ptrace");
wait(&wait_val);
}
return((double)counter);
} // end of switch
} // end of fitness
The test program must access the pipe which was created by the parent process beforehand in order to retrieve the test procedure's input parameter string. It must finally execute the actual test procedure. The source code can be implemented as shown below:
int main (int argc, char* argv[]) {
int size; // size of the parameter string
int pfd[2]; // pipe descriptor
unsigned char* buffer; // buffer for pipe input
istrstream is1 (argv[1],10);
istrstream is2 (argv[2],10);
is1 >> size;
is2 >> pfd;
buffer = new unsigned char [size];
// no open needed, just read the pipe
switch (read(pfd,buffer,size) {
case -1: perror("read"); break;
case 0: perror("EOF"); break;
default: // call actual test procedure with
// the parameter string converted
// to the type of the test function
test_func (*(TYPE*)&buffer);
}
delete buffer;
}