Systemprogrammierung - Teil 5

Identitätssuche

von Wolfgang Hetzler


Systemprogrammierung gilt als schwierig, da ihre Inhalte dem Benutzer sehr oft abstrakt gegenüber treten. Daß diese Abstraktheit nicht sein muß und die Systemcalls eine wohldefinierte Schnittstelle zum Kernel darstellen, will diese Serie zeigen. Heute geht es um Prozeß-IDs.

Jeder Prozeß unter Linux ist mit einer Anzahl spezifischer Kenndaten verbunden, die für die Ausführung von Bedeutung sind, z.B. der Zugriff auf die eigenen Dateien und/oder auf Dateien, die anderen Eigentümern zuzuordnen sind, oder die Kenntnisse der eigenen Ressourcen.

Diese Informationen können mit einem Set von Systemcalls abgerufen werden oder man ermittelt sie mit dem bekannten Kommando ps. Ich will heute dieses Set von Aufrufen vorstellen, mit dem man diese Informationen im Rahmen von Programmen eruieren kann - zu welchem Zwecke auch immer. Ich beschränke mich wie stets auf die meiner Meinung nach wichtigsten, diese sind schon umfangreich genug.

Zunächst ein paar Informationen über die Identität: jeder Prozeß ist mit einer spezifischen Nummer verknüpft, der PID, die beim Starten des Prozesses vergeben wird. Doch die PID ist nicht die alleinige Information, die eine Identität stiftet, desweiteren werden Informationen über den user aufgbaut, der ja als Prozeß agiert.

Hier haben wir zunächst die UID und die GID (auch als reelle uid und gid bezeichnet), die nach erfolgreichem login um die effektiven Werte erweitert werden. Jeder User hat also zusätzlich die EUID und seine EGID, die benutzt werden, um erweiterte Zugriffsrechte auf Dateien in spezifischen Fällen zu realisieren.

Zunächst sind die rellen Werte und die effektiven identisch, letztere können bei Bedarf jedoch kurzfristig geändert werden (wie ließe sich sonst ein schreibender Zugriff auf die passwd erklären, die ja für den normalen User tabu ist!). Von Bedeutung sind weiterhin die Identifiktion der Prozeßgruppe, die PGRP und die Nummer des Elternprozesses, die PPID. Daraus resultiert die folgende verrwirrende Menge von Systemcalls.

Prototypen der Systemcalls

int getpid();      // liefert die eigene PID 
int setuid(int);   // setzt neue UID 
int getuid();      // liefert UID  
int setgid(int);   // setzt GID  
int getgid();      // liefert GID  
int geteuid();     // liefert EUID  
int getegid();     // liefert EGID 
int getppid();     // liefert PPID  
int getpgrp();     // liefert PGRP 
int setsid();      // setzt neue PGRP, falls PID nicht PGRP 
int setreuid(int reuid,int euid); // setzt UID und EUID  
int setregid(int regid,int egid); // setzt GID und EGID 
int getpgid(int pid);             // liefert PGRP von pid 
  
int setfsuid(int); // setzt UID für Zugriffschecks 
                   // des Kernels auf Dateiensysteme  
int setfsgid(int); // setzt GID für Zugriffschecks 
                   // des Kernels auf Dateiensysteme                     
                   // die beiden letzten sind lediglich für NFS 

int getsid();      // liefert Sessiongroup 

// alle Systemcalls in der Reihenfolge ihrer Nennung in der asm/unistd.h

Zur Demonstration schreiben wir ein kleines Programm, das die Ids auf dem Bildschirm ausgibt

Listing 1: get_your_ids.cpp

#include <iostream.h> 
#include <unistd.h> 

int get_your_ids(); 

int main(){ 
  get_your_ids(); 
} 

int get_your_ids() { 
 int pid,uid,gid,euid,egid,ppid,pgid,pgrp; 

 pid = getpid();   // die eigene pid 
 uid = getuid();   // die eigene uid 
 gid = getgid();   // die eigene Gruppe 
 euid = geteuid(); // die effektive eigene uid 
 egid = getegid(); // die effektive eigene Gruppe 
 ppid = getppid(); // die Eltern pid 
 pgid = getpgid(getppid()); // die Prozeßgruppe des Arguments 
 pgrp = getpgrp(); // die eigene Prozeßgruppe 

 cout << "relevante Informationen dieses Prozesses\n" 
      << "\nPID = " << pid               
      << "\nUID = "  << uid              
      << "\nGID = "  << gid            
      << "\nEUID= " << euid 
      << "\nEGID = " << egid 
      << "\nPPID = " << ppid 
      << "\nPGRP von parent = " << pgid             
      << "\nPGRP = "  << pgrp; 
} 
Ausgabe des Listings 1

relevante Informationen dieses Prozesses 

PID = 22653 
UID = 500 
GID = 100 
EUID= 500 
EGID = 100 
PPID = 19936 
PGRP von parent = 19936 
PGRP = 22653 

Die effektiven IDs laufen mit den reellen identisch, die Prozeßgruppe ist mit der pid identisch, die pid des Eltenprozeses ist die pid der Shell, von der dieses kleine Programm gestartet wird. Geändert werden können die pid und die gid mit

int setuid(int);
int setgid(int);

die natürlich bei einer Erweiterung des Rechtekatalogs root-Rechte benötigen.

Die Prozeßgruppe ist von Bedeutung für einen potentiellen Sitzungsleiter, von dem aus weitere Prozesse gestartet werden, man bewerkstelligt das mit

int setsid();

Der Rückgabewert ist die neue Prozessgruppe (ich komme auf die Prozeßgruppe bei der Behandlung der Signale zurück).

Das Ändern von Identitäten ist aber auch an anderer Stelle von zentraler Bedeutung - der login-Prozeß läuft unter der Identität von root, das sollte sich aber bei einem normalen User schleunigst ändern, denn sonst wäre jeder User root. Wir schreiben eine Variante von login.

Listing 2

#include <sys/types.h> 
#include <unistd.h> 
#include <iostream.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <pwd.h> 
#define MAX_PWD_LENGHT 124 
int set_new_ids_and_exec_a_shell(); 

struct passwd *pw; 

/*  Name wird bei Aufruf übergeben 
 *  Passwort wird überprüft 
 *  und anschließend eine shell für
 * den neuen user mit leicht ! 
 * geändertem Environment aufgebaut 
 */ 

int main(int argc,char **argv) {

  char passwd[MAX_PWD_LENGTH]; 

  pw = getpwnam(argv[1]); 

  if(!pw) { 
               
    cout << "password: ";          
    system("stty -echo");                
    cin >> passwd;              
    system("stty echo");              
    cout << "login incorrect"; 
               
    exit(1); 
  } 

  if(pw->pw_passwd != 0) { 
               
    cout << "password: ";             
    system("stty -echo");               
    cin >> passwd;                 
    system("stty echo");                
    if(!strcmp(pw->pw_passwd,crypt(passwd,"99")) {                              
      cout << "login incorrect";                              
      exit(1); 
    } 
 
  } 
  set_new_ids_and_exec_a_shell(); 
} 

int set_new_ids_and_exec_a_shell() { 
 
  setuid(pw->pw_uid);  // setzen der neuen ids 
  setgid(pw->pw_gid); 
 
  char env_home[512];  // Aufbereitung des neuen Environment 
  char *envp[2]; 
  envp[0] = env_home; 
  envp[1] = 0; 
  strcpy(env_home,"HOME="); 
  strcat(env_home,pw->pw_dir); 
 
  execle(pw->pw_shell,pw->pw_shell,0,envp); 
  // Starten der shell für den neuen user 

  /* never reached */ 
  return 1; 
}

Getty (wird später mit ioctl vorgestellt) startet via exec login und übergibt den eingelesenen Usernamen, in main holen wir uns mit der Funktion getpwnam die aktuellen Einträge aus der /etc/passwd für den User (wir überlassen shadow einer korrekten Version), prüfen, ob es den User gibt und ob er ein Paßwort hat und holen uns dieses, anschließend wird die Funktion set_new_ids_and_exec_a_shell aufgerufen, die die neuen ids setzt, ein neues Environment beispielhaft aufbaut und anschließend die Shell startet.

Insgesamt sind natürlich die IDs nicht übermäßig interessant - sie werden ja sowieso gesetzt und wir wenden uns anderen Informationen zu, die ebenfalls zu einem Prozeß gehören.

Zunächst weden wir uns den Prioritäten eines Prozesses zu, die ja auch unter anderem dafür verantwortlich sind, in welcher Häufigkeit ein Prozeß über die Ressource Prozessor verfügt.

Die Priorität eines Prozesses kann geändert werden mit dem bekannten Systemkommando nice, das ebenfalls als Systemcall existiert. Hier sei schon die kleine Anmerkung gemacht, daß nice seinen Namen zur Recht trägt, denn wenn man seine Priorität ändert, ist man nett zu anderen, denn man kann sie nur herabsetzen, eine Heraufsetzen der Priorität ist lediglich dem Superuser vergönnt. Um die Priorität zu erfahren, kann man den Systemcall getpriority einsetzen, zum Setzen benöigt man setpriority oder nice.

Wir schauen uns dazu ein kleines Programm an, das zunächst die Priorität checkt und dann verändert. Zunächst wieder die Prototypen, und dann das Programm.

Prototypen

int getpriority(int which, int who);  
// liefert nach Maßgabe von which Prioritäten 

int setpriority(int which, int who);  
// setzt nach Maßgabe von which Prioritäten 
 
int nice(int); // ändert Priorität 

folgende Makros stehen zur Verfüung:
 
PRIO_PROCESS  -> who ist eine PID 
PRIO_PGRP     -> who ist eine Prozeßgruppe 
PRIO_USER     -> who ist eine UID

Priorität ist ein Wert zwischen -20 und 20 

Listing 3

#include <unistd.h> 
#include <iostream.h> 
#include <sys/time.h> 
#include <sys/resource.h> 

int main() 
{ 
  int which,who; 

  cout << getpriority(PRIO_PROCESS,getpid())<<"\n"; 

  setpriority(PRIO_PROCESS,getpid(),10); 
  cout << getpriority(PRIO_PROCESS,getpid()) << "\n"; 

  nice(-10);  // root only 
  cout << getpriority(PRIO_PROCESS,getpid()); 
} 

Set- und getpriority erwarten zwei Parameter, which und who, wobei which who interpretiert (s. obige Makros). Setpriority erwartet einen zusätzlichen niceval, der zwischen -20 und 20 liegen muß, der auf die aktuelle Priorität aufaddiert wird - hoher Prioritätswert gleich geringe effektive Priorität und geringer Prioritätswert gleich hohe effektive Priorität. Der Aufruf von nice mit -10, also das Erhöhen der Priorität , ist lediglich dem Superuser vorbehalten.

Von Interesse sind natürlich auch die übrigen Ressourcen, die einem Prozeß zu Verfügung stehen, wie

die sich mit den Systemcalls getrlimit und setrlimit manipulieren lassen. Zunächst die Protypen:

Prototypen

#include <sys/types.h> 
#include <sys/resource.h> 

int getrlimit(unsigned int, struct rlimit *); 
int setrlimit(unsigned int, struct rlimit *); 

struct rlimit { 
  int rlim_cur; /* weiches Limit, kann bis zu rlim_max verschoben werden */ 
  int rlim_max; /* hartes Limit, kann bis auf rlim_cur gesenkt werden */ 
}; 

// der erste Parameter von get- und setrlimit kann folgende Werte annehmen: 
 
// RLIMIT_CPU     maximale CPU-Zeit 
// RLIMIT_FSIZE   maximale Dateigröße 
// RLIMIT_DATA    maximale Größe des Datensegments 
// RLIMIT_STACK   maximale Stackgröße 
// RLIMIT_CORE    maximale Größe der core-Datei 
// RLIMIT_RSS     maximale Größe für Argumente und Umgebung 
// RLIMIT_NPROC   maximale Anzahl der direkten Kindsprozesse 
// RLIMIT_NOFILE  maximale Anzahl der geöffneten Dateien 
// RLIMIT_MEMLOCK maximale Größe des Speicherplatzes, 
//                den ein Prozeß blockieren kann 
// RLIMIT_AS      maximale Größe des Adressraums 

Wir schreiben wieder ein kleines Beispielprogramm:

Listing 4
 
#include <sys/resource.h> 
#include <iostream.h> 

int main() { 

  struct rlimit limits; 

  getrlimit(RLIMIT_NOFILE,&limits); 

  cout << limits.rlim_cur << "----" 
       <<  limits.rlim_max; 
  limits.rlim_cur = 80; 
  setrlimit(RLIMIT_NOFILE,&limits); 

  getrlimit(RLIMIT_NOFILE,&limits); 

  cout << "\n"   << limits.rlim_cur 
       << "----" <<  limits.rlim_max; 
} 
...                       

// mit folgender Ausgabe
// 256----256
// 80----256

Mit getrlimit holen wir uns die aktuellen Werte für die maximale Anzahl der geöffneten Dateien. Die struct limits enthält zwei Komponenten - rlim_cur verweist auf das aktuelle und rlim_max auf das absolut mögliche Limit. Anschließend setzen wir das aktuelle neu und rufen setrlimit auf, der erneute Aufruf von getrlimit dient lediglich zur Kontrolle. So kann mit jedem möglichen Limit verfahren werden. Überschreitet ein Prozeß sein aktuelles Limit, so wird er abgebrochen.

Weitere Informationen über einen Prozeß liefert der Call getrusage in die struct rusage (s iehe unten), da dies aber noch unvollständig geschieht, lassen wir ihn lediglich erwähnt.

Da zur Identität eines Prozesses nicht nur die eigene Nabelschau gerechnet werden kann, sondern auch die Möglichkeit der Kommunikation mit anderen Prozessen gehört, sei für heute zum Abschluß anhand der beiden Systemcalls wait und exit demonstriert, die schon in der Folge 3 etwas stiefmütterlich behandelt worden sind. Zunächst die Prototypen für wait und exit, wobei ich für wait die wohl endgültige Variante des wait4 gewählt habe, der wesentlich mächtiger ist als seine traditionellen Vorgänger.

Prototypen

#include <sys/types.h> 
#include <sys/resource.h> 
#include <sys/wait.h> 

int wait4(int pid, int status, int options, struct rusage *ru); 
// wartet auf das Ende nach verschiedenen  Kriterien 

int exit(int status); 

// an Optionen für wait4 sind möglich 

__WCLONE    nur auf mit clone erzeugte Prozesse warten, 
            von Bedeutung für threads 
WUNTRACED   Berücksichtigung von Prozessen ohne PF_PTRACE (Debugging) 
WNOHANG     kein Blockieren 

struct  rusage { 
  struct timeval ru_utime; /* user time used */ 
  struct timeval ru_stime; /* system time used */ 
  long    ru_maxrss;       /* maximum resident set size */ 
  long    ru_ixrss;        /* integral shared memory size */ 
  long    ru_idrss;        /* integral unshared data size */ 
  long    ru_isrss;        /* integral unshared stack size */ 
  long    ru_minflt;       /* page reclaims */ 
  long    ru_majflt;       /* page faults */ 
  long    ru_nswap;        /* swaps */ 
  long    ru_inblock;      /* block input operations */ 
  long    ru_oublock;      /* block output operations */ 
  long    ru_msgsnd;       /* messages sent */ 
  long    ru_msgrcv;       /* messages received */ 
  long    ru_nsignals;     /* signals received */ 
  long    ru_nvcsw;        /* voluntary context switches */ 
  long    ru_nivcsw;       /* involuntary " */ 
}; 
// nicht alles wird genutzt, aber man hat es 

// An Werten für die pid beim Aufruf von wait4 sind möglich 
 
//   < -1  warte auf alle Prozesse mit der absoluten pid 
//     -1  warte auf alle Prozesse 
//      0  warte auf alle mit der gleichen PGRP 
//   >  0   warte auf pid

Wir schauen uns die Zusammenhänge wieder an einem kleinen Programm an.

Listing 5

#include <unistd.h> 
#include <linux/resource.h> 
#include <iostream.h> 

int wait(int *); 

int main() { 
  int pid,status,rpid; 

  struct rusage ru; 

  if((pid=fork()) == 0) 
    exit(5);  // ist hier willkürlich 
  else{ 
    cout << "I created a child with" << pid; 
    rpid = wait(&status); 
  } 
        
  if((status & 0xff)==0) 
    if((status >> 8 & 0xff) == 5) 
      cout << "\nthis is my child with " << rpid; 

} 

int wait(int *status) { 
  struct rusage ru; 
  return wait4(-1,status,0,&ru); 
} 

In main kreieren wir einen Kindsprozeß, der lediglich seinen exit-call durchführt und den willkürlichen Wert 5 als exit-Wert zurückliefert. Im Elternprozeß warten wir auf das Ende von Kindsprozessen, wobei wir die Funktion wait verwenden, die im Source-Code wiedergegeben ist.

Die Willkür des exit-Wertes 5 bedeutet lediglich, daß der Kindsprozeß dem Elternprozeß signalisieren kann, wie und warum er terminierte - ein Plus an zusäztlicher Kommunikation, die anwendungsbezogen genutzt werden kann.

In wait rufen wir den Systemcall wait4 auf und liefern die pid eines gestorbenen Kindsprozesses zurück, weitere Informationen zu dem Ende eines Kindsprozesses werden traditionell in status abgelegt (exit-Wert oder Signal werden differenziert in zwei Bytes angezeigt), neuere Informationen gehen in die struct rusage, deren Komponeneten oben wiedergegeben sind.

Wir holen uns in main den exit-Wert aus dem höherwertigen Byte des niederwertigen Halbwortes, für diese Operationen stehen auch einige vorbereitete Makros zur Verfügung(siehe dazu die Manuals [2]), die dann benutzt werden sollten, wenn Gedankengänge nach höher- oder niederwertig Schwierigkeiten bereiten.

Diese Form des Wartens auf das Ende eines Prozesses mit Nachfrage nach dem exit-Wert kann durchaus als eine low-level Form der Prozeßkommunikation verstanden werden, besseren wollen wir uns in den nächsten Folgen widmen.

Literatur

[1] Linux-Kernel-Programmierung, Addison-Wesley, 1997
[2] Rochkind, Unix-Programmierung für Fortgeschrittene, Hanser, 1985
[3] Bentson,R., Inside Linux,Seattle:SSC 1996
[4] M.Bach, The Design of the Unix-Operating System
[5] Linux Man, RedHat Software Inc.

Der Autor

Wolfgang Hetzler arbeitet seit 1985 als Dozent an einem privaten Institut für Fort- und Ausbildung im EDV-Bereich und als Lehrbeauftragter an der FH-Frankfurt im Bereich Ingenieur-Informatik. Linux ist ihm seit der Version 0.9xx bekannt und eine Quelle ständiger Auseinandersetzung. Zu erreichen ist er unter het@het.gg.eunet.de.

Copyright © 1998 Linux-Magazin Verlag