Έλεγχος Διεργασιών <stdlib.h>,<unistd.h>


Μια διεργασία (process) είναι στην ουσία ένα εκτελούμενο πρόγραμμα. Μπορεί να είναι πρόγραμμα του συστήματος (π.χ. login, update, csh) ή ένα πρόγραμμα που ξεκινά ο χρήστης (vi, mail ή ένα πρόγραμμα γραμμένο από το χρήστη).

Κάθε διεργασία στο UNIX/Linux κατά την εκκίνησή της λαμβάνει (μεταξύ των άλλων) από το σύστημα ένα μοναδικό αριθμό - τον αριθμό διεργασίας (process ID), pid.  Η εντολή ps εμφανίζει τις τρέχουσες διεργασίες με τα στοιχεία τους.

Η συνάρτηση της C int getpid() επιστρέφει το pid της διεργασίας που κάλεσε τη συνάρτηση.

Συνήθως τα προγράμματά μας κατά την εκτέλεσή  τους αποτελούνται από μια διεργασία. Αργότερα θα δούμε πώς μπορούμε να γράψουμε προγράμματα που κατά την εκτέλεσή τους δημιουργούν αρκετές επικοινωνούσες διαργασίες.

Εκτέλεση Εντολών Φλοιού από τη C

Μπορούμε να εκτελέσουμε εντολές φλοιού (όπως και σενάρια ή συναρτήσεις φλοιού) UNIX/Linux μέσα από ένα πρόγραμμα C ακριβώς όπως και από τη γραμμή εντολών, με τη βοήθεια της συνάρτησης system().

Σημείωση: αυτό μπορεί να μας γλιτώσει από πολύ κόπο, αφού μπορούμε πολύ εύκολα να ενσωματώσουμε ότι εντολή, σενάριο, υπηρεσία, εφαρμογή κλπ διατίθεται στη γραμμή εντολών του φλοιού UNIX/Linux.

int system(char *string) -- όπου το string μπορεί αν ειναι το όνομα διαδρομής οποιουδήποτε εκτελέσιμου προγράμματος από τη γραμμή εντολών του φλοιού UNIX/Linux. Η συνάρτηση ορίζεται στη βιβλιοθήκη stdlib.h

Παράδειγμα: Κλήση της ls από ένα πρόγραμμα:


#include <stdio.h>
#include <stdlib.h>
main()
{
printf("Files in Directory are:\n");
system("ls -l");
}

Η συνάρτηση επιστρέφει τη κατάσταση εξόδου της εντολής που εκτελέστηκε (στην επιτυχία συνήθως είναι 0). Σε περίπτωση αποτυχίας επιστρέφει -1. 

Μια άλλη μέθοδος εκτέλεσης εντολών φλοιού είναι η παρακάτω:

#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

...

int rc;

rc = syscall(SYS_chmod, "/etc/passwd", 0444);

if (rc == -1)
fprintf(stderr, "chmod failed, errno = %d\n", errno);

Η κλήση της συνάρτησης system περιλαμβάνει τη κλήση 3 άλλων συναρτήσεων: execl(), wait() και fork() (που ορίζονται στη βιβλιοθήκη unistd).

execl()

To execl αποτελεί σύντμηση των λέξεων execute(εκτέλεση) και leave(απομάκρυσνη) που σημαίνει οτι η καλούμενη με την execl()  διεργασία θα εκτελεστεί και θα τερματιστεί.

Η συνάρτηση ορίζεται στη βιβλιοθήκη unistd.h ως εξής:

int execl(char *path, char *arg0,...,char *argn, 0);

Η τελευταία παράμετρος πρέπει πάντα να είναι NULL (0, μηδέν). Αντιστοιχεί στον NULL terminator (τερματισμό). Αφού  ο αριθμός των ορισμάτων στην λίστα είναι μεταβλητός πρέπει να υπάρ ο τερματισμός NULL.

Το path είναι το όνομα διαδρομής της προς εκτέλεση εντολής, το argo είναι το ίδιο με το path ή τουλάχιστο με το τελευταίο τμήμα του ονόματος διαδρομής (θυμηθείτε οτι στην επεξεργασία της γραμμής εντολών στο φλοιό του UNIX/Linux το μηδενικό όρισμα της εντολής είναι το όνομά της).

Τα arg1 ... argn είναι τα ορίσματα που λαμβάνει η εντολή, ενώ το μηδέν (0) σημειώνει το τέλος των ορισμάτων.

Έτσι το προηγούμενο παράδειγμά μας θα μπορούσε να είναι:

#include <stdio.h>
#include <unistd.h>
main()
{
printf("Files in Directory are:\n");
execl("/bin/ls","/bin/ls"', "-l",NULL);
}

Η execl() έχει αρκετές παραλλαγές, δείτε σχετικά το εγχειρίδιο της πρότυπης βιβλιοθήκης της C. Για παράδειγμα η συνάρτηση  execlp() επιτρέπει το σύστημα να αναζητήσει την προς εκτέλεση εντολή σε όλους τους καταλόγους που υπάρχουν στην τρέχουσα τιμή της μεταβλητής περιβάλλοντος φλοιού PATH. H execle() επιτρέπει τον επιπλέον οσριμό τιμών για μεταβλητές περιβάλλοντος του φλοιού κατά την εκτέλεση, κλπ.

H execv()λειτουργεί execl() όμως είναι χρήσιμη όταν ο αριθμός των παραμέτρων δεν είναι γνωστός εκ των προτέρων. Η εισαγωγή των ορισμάτων γίνεται με τη τη λογική του πίνακα ορισμάτων. Επίσης η execvp()αντιστοιχεί στην execlp() και η execve()στην execle().

H execl()επιστρέφει τη κατάσταση εξόδου της εντολής που εκτελέστηκε (στην επιτυχία συνήθως είναι 0). Σε περίπτωση αποτυχίας επιστρέφει -1. Επίσης θέτει το errno, με τιμές σφαλμάτων που αναλύονται στο εγχειρίδιο.

fork()

Η int fork() μετατρέπει μια διεργασία σε 2 ίδιες διεργασίες, γνωστές ως γονέας ή γονική διεργασία (parent)  και  παιδί ή θυγατρική διεργασία (child). Στην επιτυχή εκτέλεση, η fork() επιστρέφει 0 στη θυγατρική διεργασία και το pid  της θυγατρικής διεργασίας στη γονική. Σε αποτυχία, η fork() επιστρέφει -1 στη γονική διεργασία, θέτει το errno σε κατάλληλη τιμή, και, φυσικά, δεν υπάρχει θυγατρική διεργασία.

Σημείωση: Η θυγατρική διεργασία κληρονομεί το περιβάλλον φλοιού της γονικής διεργασίας αλλά έχει το δικό της pid. 

Το παρακάτω πρόγραμμα δείχνει μια απλή χρήση της fork(), όπου δύο αντίγραφα του ίδιου προγράμματος εκτελόυνται 'ταυτόχρονα' (πολυεργασία, multitasking)

#include <stdio.h>
#include <unistd.h>
main() 
{
int some_value;

printf("Forking process\n");
fork();

/* This part of the program is executed by two different proceses */
printf("The process id is %d \n", getpid());

some_value = getpid() + 10;
printf("Some value is %d", some_value);

execl("/bin/ls","/bin/ls","-l",NULL);

/* This line is not executed because of th execl function */
printf("This line is not printed\n");
}

Το αποτέλεσμα θα είναι κάτι σαν :


Forking process
The process id is 6753
Some value is 6763
The process id is 6754
Some value is 6764
θα εκτελεστούν δύο ls -l


Σημείωση: τα pid αλλάζουν σε κάθε εκτέλεση. Επίσης η σειρά εμφάνισης των αποτελεσμάτων μεταβάλλεται σε κάθε εκτέλεση. Οι εντολές μετά την execl() δεν εκτελούνται επειδή έχουμε τερματισμό του προγράμματος. Η μεταβλητή some_value (όπως και όλα τα άλλα στοιχεία των δύο διεργασιών) είναι ιδιωτικά της κάθε διεργασίας.

Όταν δημιουργούμε 2 με fork() μπορούμε εύκολα να αναγνωρίσουμε τη θυγατρική διεργασία γιατί η fork() επιστρέφει μηδέν (0) στη θυγατρική διεργασία. Μπορούμε να παγιδεύσουμε σφάλματα αφού η fork() επιστρέφει -1. π.χ:


int pid; /* process identifier */
 
pid = fork();
if ( pid < 0 ) { printf("Cannot fork!!'\n");
exit(1);
}
if ( pid == 0 )
{ /* Child process */
......
}
else
{ /* Parent process value of pid variable is child's pid */
....
}

wait()

int wait (int *status_location) 

αναγκάζει τη γονική διεργασία να αναμένει το τερματισμό της θυγατρικής διεργασίας. Η συνάρτηση wait() επιστρέφει το pid της θυγατρικής διεργασίας ή -1 για σφάλμα. Η κατάσταση εξόδου της θυγατρικής διεργασίας βρίσκεται στο status_location

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main()

  int pid, status;
 
  
  printf("Forking process\n");

  pid = fork();

  wait(&status);
  printf("The process id is %d \n", getpid());
  printf("value of pid is %d\n", pid);
  if (pid == 0)
    printf("I am the child, status is %d\n", status);
  else
    printf("I am the parent, status is %d\n", status);
  execl("/bin/ls","/bin/ls","-l", NULL);

  printf("This line is not printed\n");
}

Μέσω της τιμής τηςμεταβλητής pid ελέγχουμε τις διεργασίες. Λόγω της κλήσης της συνάρτησης wait() η γονική διεργασία θα εκτελείται πάντα μετά τη θυγατρική. Επίσης η σειρά εμφάνισης των αποτελεσμάτων είναι δεδομένη αφού θα εμφανιστούν πρώτα όλα τα αποτελέσματα της θυγατρικής διεργασίας. Επίσης το status λαμβάνει τιμή μέσω της wait() μετά τον τερματισμό της θυγατρικής διεργασίας.

exit()

void exit(int status) 

Τερματίζει τη διεργασία που καλεί αυτή τη συνάρτηση και επιστρέφει το τιμή status.  Η τιμή αυτή μπορεί να διαβαστεί τόσο από το UNIX/Linux όσο και από προγράμματα C, που έχει ξεκινήσει τη τερματισθείσα διεργασία με fork().

Η σύμβαση είναι οτι τιμή status ίση με 0 σημαίνει κανονικό τερματισμό. Κάθε άλλη τιμή σημαίνει σφάλμα ή κάτι ασυνήθιστο (συνήθως η τιμή είναι -1). Πολλές συναρτήσεις της πρότυπης βιβλιοθήκης C  έχουν κωδικούς σφάλματος που ορίζονται στο αρχείο κεφαλής sys/stat.h

Ακολουθούν δύο πλήρη παραδείγματα χρήσης των παραπάνω συναρτήσεων:

Στο πρώτο παράδειγμα προσομοιώνεται η χρήση της συνάρτησης system(). Ουσιαστικά ισοδυναμεί με εκτέλεση στη γραμμή εντολών της bash -c command.

#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

/* Execute the command using this shell program. */
#define SHELL "/bin/bash"

int my_system (const char *command)
{
int status;
pid_t pid;

pid = fork ();
if (pid == 0)
{
/* This is the child process. Execute the shell command. */
execl (SHELL, SHELL, "-c", command, NULL);
/* If this point is reached execl failed */
exit (EXIT_FAILURE);
}
else
if (pid < 0)
/* The fork failed. Report failure. */
status = -1;
else
/* This is the parent process. Wait for the child to complete. */
if (waitpid (pid, &status, 0) != pid)
status = -1;
return status;
}

Στο δεύτερο παράδειγμα ο χρήστης εισάγει εντολές φλοιού με τα ορίσματά τους. Το πρόγραμμα χωρίζει τη συμβολοσειρά σε tokens που μπαίνουν σε πίνακα συμβολοσειρών και οι εντολές εκτελούνται σε θυγατρική διερασία με την συνάρτηση exccvp().

/* fork.c - example of a fork in a program */
/* The program asks for UNIX commands to be typed and inputted to a string*/
/* The string is then "parsed" by locating blanks etc. */
/* Each command and sorresponding arguments are put in a args array */
/* execvp is called to execute these commands in child process */
/* spawned by fork() */

/* cc -o fork fork.c */

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

main()
{
char buf[1024];
char *args[64];

for (;;) {
/*
* Prompt for and read a command.
*/
printf("Command: ");

if (gets(buf) == NULL) {
printf("\n");
exit(0);
}

/*
* Split the string into arguments.
*/
parse(buf, args);

/*
* Execute the command.
*/
execute(args);
}
}

/*
* parse--split the command in buf into
* individual arguments.
*/
parse(buf, args)
char *buf;
char **args;
{
while (*buf != NULL) {
/*
* Strip whitespace. Use nulls, so
* that the previous argument is terminated
* automatically.
*/
while ((*buf == ' ') || (*buf == '\t'))
*buf++ = NULL;

/*
* Save the argument.
*/
*args++ = buf;

/*
* Skip over the argument.
*/
while ((*buf != NULL) && (*buf != ' ') && (*buf != '\t'))
buf++;
}

*args = NULL;
}

/*
* execute--spawn a child process and execute
* the program.
*/
execute(args)
char **args;
{
int pid, status;

/*
* Get a child process.
*/
if ((pid = fork()) < 0) {
perror("fork");
exit(1);

/* NOTE: perror() produces a short error message on the standard
error describing the last error encountered during a call to
a system or library function.
*/
}

/*
* The child executes the code inside the if.
*/
if (pid == 0) {
execvp(*args, args);
perror(*args);
exit(1);

/* NOTE: The execv() vnd execvp versions of execl() are useful when the
number of arguments is unknown in advance;
The arguments to execv() and execvp() are the name
of the file to be executed and a vector of strings contain-
ing the arguments. The last argument string must be fol-
lowed by a 0 pointer.

execlp() and execvp() are called with the same arguments as
execl() and execv(), but duplicate the shell's actions in
searching for an executable file in a list of directories.
The directory list is obtained from the environment.
*/
}

/*
* The parent executes the wait.
*/
while (wait(&status) != pid)
/* empty */ ;
}

Ασκήσεις

Άσκηση 12725

Γράψτε ένα πρόγραμμα C που να δημιουργεί με fork() δύο διεργασίες οι οποίες να εκτελούνται 'ταυτόχρονα' κάνοντας κάποια διαφορετική μη-διαλογική εργασία, π.χ. η μία δημιουργεί μια αρχειοθήκη ενώ η άλλη καθαρίζει ένα κατάλογο από παλιά αρχεία. Οι δύο εργασίες υλοποιούνται με εντολές ή σενάρια φλοιού.

Άσκηση 12726

Γράψτε ένα πρόγραμμα C που με τη βοήθεια menu και διαλόγων να επιτρέπει την ταυτόχρονη εκτέλεση ορισμένων σεναρίων φλοιού (ως διεργασίες), αφού εισαχθούν τα κατάλληλα ορίσματα. Μετά από κάθε εκτέλεση επιστρέφουμε στο menu.

Άσκηση 12727

Γράψτε ένα πρόγραμμα C που με τη χρήση της popen() να περνά την έξοδο της εντολής UNIX/Linux rwho στην εντολή UNIX/Linux more.



Dave Marshall
1/5/1999
μετάφραση και προσαρμογή στα Ελληνικά Κ.Γ. Μαργαρίτης
14/3/2008