Interprozess Kommunikation mit Unix98 Terminals

jccTeq

Erfahrenes Mitglied
Introduction
Dies ist mein aller erster Guide! Also möchte ich euch bitten, im Falle von Fehlern, falschen Angaben oder fehlenden/mangelhaften Informationen mich zu kontaktieren, damit ich das korrigieren kann.

Ich bin erreichbar per eMail unter 7.e.Q@35i.net

Was sollte man vorher wissen?
- Wie man C/C++ programmiert
- Was Linux ist und grob wie es aufgebaut ist (daß für Linux alles eine Datei ist etc.)
- Was Device-Nodes sind
- Was Terminals und Konsolen sind
- Was Filedeskriptoren sind
- Was Parent- und Child-Prozesse sind
- Was Interprozess-Kommunikation (IPC) bedeutet

Guide - Interprozess-Kommunikation mit Unix98 Pseudo-Terminals

Für die Kommunikation zwischen zwei Prozessen gibt es unter Linux (sowie auch unter Windows, aber darauf gehe ich hier nicht näher ein) verschiedene Wege, die "Leitung" zwischen den Prozessen zu etablieren. Darunter fallen Begriffe wie Semaphoren, Shared Memory, Pipes, Sockets und ähnliches. Alle diese Konzepte haben aber einen großen Nachteil (außer Sockets, glaub ich, weiß ich aber jetzt nicht so genau gerade):
man kann sie nicht effektiv als Standard-Konsole eines Kind-Prozesses angeben.

Um diesen Nachteil auszumärzen, wurden die sogenannten Pseudo-Terminals geschaffen (vermutlich auch noch aus anderen Gründen, aber dieser ist für mich der plausibelste).

Pseudo-Terminals sind vollwertige Ein-/Ausgabekonsolen, die - im Unterschied zu einer echten Konsole - keine Anbindung an irgendwelche Hardware haben (Monitor, Tastatur, Drucker) und aus einer Device-Node bestehen, sondern rein virtuell bestehen und eine Master- und eine Slave-Seite besitzen (zwei Device-Nodes).

In frühen Linux-Versionen seit Einführung der Pseudo-Terminals war es so, daß man auf Master-Seite (also im Parent-/Eltern-Prozess) zum Beispiel das Device /dev/ptyp0 geöffnet hat und dem Child-Prozess als seine Standart-Ein-/Ausgabe-Konsole die Slave-Seite des Terminals, also /dev/ttyp0 gegeben hat. Dadurch war es im Parent-Prozess möglich, die Ausgaben des Childs (printf und dergleichen) direkt einzulesen, als würde man aus einer Datei lesen oder aus einem Socket. Der große Vorteil dabei ist aber darin zu sehen, daß das Auslesen der Daten "linebuffered" möglich ist. So stehen Ausgaben des Childs dem Parent-Prozess genau so schnell zur Verfügung, wie dem Benutzer eine Ausgabe auf dem Bildschirm. Eben immer nach dem letzten NewLine.

Andernfalls, also mit Pipes und so - jedenfalls hab ich diese Erfahrung machen müssen - sind die Daten blockbuffered, werden also nur alle soundso viele Bytes im Parent als verfügbar gemeldet. Das ist sehr störend, wenn man eine meldungsbasierte Interprozess-Kommunikation herstellen will, weil man durch den Block parsen muss und die Meldungen des Childs nicht in "Echtzeit" gemeldet bekommt.

Mit Pseudo-Terminals ist das somit kein Problem mehr.

Unix98 PTYs

Die sogenannten Unix98 PTYs sind eine Weiterentwicklung der damaligen Pseudo-Terminals. Es gibt dafür nicht mehr etliche masterseitige Device-Nodes (/dev/ptyp? bis ...) und auch keine harten (auf der Festplatte existierenden) Slave-Nodes mehr, sondern nur noch eine einzige "/dev/ptmx". Nach öffnen dieser Datei legt der Kernel in seinem Pseudo-Dateisystem "devpts" (normalerweise nach /dev/pts/ gemountet) eine Datei mit fortlaufender Nummer als Namen an, welche dann die Slave-Seite des Pseudo-Terminals darstellt. Man hat also einen Filedeskriptor (durch öffnen der /dev/ptmx). Wie weiß man aber jetzt, welches /dev/pts/? Device-Node zu diesem Filedeskriptor gehört? In welches Device-Node muss ich schreiben, damit ich das in meinem Parent aus dem nach dem Öffnen gelieferten Filedeskriptor lesen kann?

In neueren Systemen kann man dies mittels der Funktion char *ptsname(int fd) ermitteln. Diese Funktion liefert als Character-Array (String) den vollständigen Pfad der Slave-Seite des Pseudo-Terminals zurück, dessen Master-Seite sich hinter dem Filedeskriptor fd verbirgt.

In älteren Linux-Systemen (abhängig von der GLIBC-Version) ist diese Funktion nicht verfügbar, kann aber nachgebildet werden. Sie macht nichts anderes, als ein ioctl auf dem Filedeskriptor auszuführen. Das sieht in etwa so aus:

Code:
char *ptsname(int fd)
{
	char buf[256];
	int pty;
	ioctl(fd,TIOCGPTN,&pty);
	sprintf(buf,"/dev/pts/%i",pty);
	return buf;
}

Das ioctl schreibt in den Integer pty die Nummer der Slave-Seite des Pseudo-Terminals hinein und ein Zusammensetzen des Pfades bewirkt dann die gleiche Funktionalität, wie die originale ptsname. Man muss nur darauf achten, daß der Pfad korrekt ist.

Wer mag, kann diese Funktion noch um einen Automatismus den Mountpoint von devpts betreffend erweitern. Es ist aber nur dann von Nöten, diese Funktion selbst zu implementieren, wenn man ein älteres System hat und vor einem Update der GLIBC zurückschreckt.

Hat man die Master-Seite geöffnet, so ist es notwendig, die Berechtigungen auf der Slave-Seite zu setzen und das Pseudo-Terminal "aufzuschließen". Dazu gibt es in den aktuellen Systemen die Funktionen grantpt(int fd) und unlockpt(int fd), welchen jeweils der Filedeskriptor der Master-Seite übergeben wird.

Die Funktion grantpt setzt die Besitzer der Slave-Device-Nodes auf den Besitzer des Parent-Prozesses und die Gruppe auf einen undefinierten Wert. Desweiteren setzt sie die Zugriffsrechte auf der Slave-Seite auf "rw-" für den Besitzer und "r--" für die Gruppe. Meine eigene grantpt macht das etwas einfacher, indem einfach jedem Vollzugriff auf die Device-Nodes gewährt wird:

Code:
int grantpt(int fd)
{
	umask(000);
	fchmod(fd,0777);
	chmod(ptsname(fd),0777);
	return 0;
}

Die Funktion unlockpt schließt das Terminal sozusagen auf. Es führt einen weiteren ioctl auf dem Filedeskriptor der Master-Seite aus. Das sieht in etwa so aus:

Code:
int unlockpt(int fd)
{
	int flag=0;
	ioctl(fd,TIOCSPTLCK,&flag);
	return 0;
}

Hat man ptsname nun aufgerufen, als String den Namen des Device-Node zurück bekommen und die Master-Seite freigeschaltet und aufgeschlossen , so kann man im Child-Prozess nun den Slave-Device-Node (bsp. /dev/pts/1329) öffnen und ihn mit beliebigen I/O-Operationen verwenden.

Alle Ausgaben, die auf der Slave-Seite getätigt werden, werden auf der Master-Seite sofort (nach einem Zeilenumbruch) zur Verfügung stehen.

Falls nähere Informationen benötigt werden, so stellt eure Fragen hier.


Gruß,
Hendrik

PS: Um der Vollständigkeit der Funktionen genüge zu tun, hier nochmal meine eigene Variante der Funktion getpt(), welche nichts anderes tut, als /dev/ptmx zu öffnen und den Filedeskriptor zurückzugeben:

Code:
int getpt()
{
	int newfd = open("/dev/ptmx",O_RDWR | O_NOCTTY);
	return newfd;
}
 
Zurück