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