Evolution Programming Resources

  Projects

   Anonyme Pipelines unter NT


Einleitung

Unter NT stehen höchst unterschiedliche Formen der Interprozesskommunikation bereit. Die Palette reicht von DDE über Mailslots bis Sockets. Doch auch (oder gerade) die einfachste Form der Kommunikation - die von Unix bekannten Pipes - birgt einige Tücken.
Pipelines existieren unter NT in zwei Formen: zum einen den anonymous Pipes, die hauptsächlich für die Kommunikation zwischen Vater- und Kindprozess verwendet werden und den named Pipes, die die Kommunikation unterschiedlichster Prozesse auch über Rechnergrenzen hinweg ermöglichen.
Während bei meinen Versuchen mit den named Pipes nur selten Probleme aufgetaucht sind, erwiesen sich die anonymous Pipes als wesentlich schwerer zu implementieren. Deshalb beschäftigt sich dieser Artikel nur mit den anonymous Pipes.

Eigenschaften einer anonymous Pipe

Eine anonymous Pipe unterscheidet sich von seinem benannten Vetter nur in einer Eigenschaft: sie hat keinen Namen. Auf sie wird rein über Handles zugegriffen. Und damit offenbart sich schon das erste Problem: wie kommt der Clientprozess an das Handle? Man kann zwar das Handle über einen anderen Interprozesskomunikationmechnismus austauschen (zur Not sogar Dateien), aber im allgemeinen wird man in diesem Fall auf named Pipes zurückgreifen. Wozu sind anonymous Pipes also nütze??
Nun, wie ich oben schon bemerkt habe verwendet man sie vor allem zur Kommunikation zwischen verwandten Prozessen. Dabei verbindet der Vaterprozess die Pipeline mit stdout bzw/und stdin des Kindprozesses. Der Kindprozess kann anschliessend ganz normal von stdin lesen und auf stdout schreiben und damit die Daten der Vaterprozesses übernehmen und Ergebnisse zurückliefern.

Das Vererben von Handles

Eine anonymous Pipe wird durch den Befehl CreatePipe erzeugt. Es werden zwei Handles zurückgeliefert: eines für das Schreibende und eines für das Leseende der Pipeline. Um nun eines dieser Handles dem Clientprozess als stdin oder stdout übergeben zu können, muß eines der Handles als "vererbbar" gekennzeichnet werden.
Vererbbar bedeutet in diesem Zusammenhang, das der Kind-Prozess auf ein vom Vaterprozess geöffnetes Objekt Zugriff hat. Dabei werden alle Eingenschaften des Orginalobjekts ebenfalls vererbt: der Stand der Schreib/Lesezeigers bei einer Datei, der Zustand eines Events etc. In unserem Fall ist es wichtig, das nur das Ende einer Pipeline vererbt wird, das auch wirklich von dem Kindprozess verwendet wird. Wird nämlich ein Handle an einen anderen Prozess als stdin oder stdout übergeben ruft Windows NT intern DuplicateHandle auf. Für jedes Objekt gibt es einen Zähler, der durch ein DuplicateHandle erhöht und durch ein CloseHandle erniedrigt wird. Erst wenn der Zähler auf Null fällt, wird das entsprechende Objekt gelöscht.
Vererben wir also beide Enden einer Pipeline, die mit stdin unseres Kindprozesse verbunden wird, wird intern eine zweite 'virtuelle' Pipeline erzeugt. Schliessen wird dann unser Schreibhandle wird nur dieses virtuelle Objekt zerstört, die Orginalpipeline existiert noch und hängt praktisch mit einem Ende in der Luft. Auf diese Weise, kann der Kindprozess nie das Ende der Transaktion erkennen, denn für ihn signalisiert eine 'zerbrochene' Pipeline das Ende des Files (EOF).

Asynchrones Lesen und Schreiben

Eine weitere unangenehme Eigenschaft der anonymous Pipe ist, das sie nur einen beschränkten Schreib/Lesepuffer zur Verfügung stellen. Ist dieser Puffer voll blockiert ein Schreibbefehl (WriteFile) solange, bis der Kindprozess die Daten aus dem Puffer gelesen hat. Dies ist eigentlich auf den ersten Blick kein Problem. Problematisch wird es erst, wenn man eine Pipeline verwendet um auf stdin der Kindprozesses zu schreiben und eine weitere um von seinem stdout zu lesen. Dabei kommt es, verwendet man ein abwechselndes Lesen und Schreiben (z.B. in einer Schleife) häufig (und unvorhersagbar) zu Deadlocks.
Dies geschieht einfach auf folgende Weise: der Vaterprozess schreibt eine viele Daten (mehr als die Größe des Puffers) in die Pipeline, während der Kindprozess noch das vorhergehende Datenpaket verarbeitet. Der Vaterprozess blockiert. Jetzt ist der Kindprozess mit seiner Verarbeitung fertig und schreibt das Ergebnis das ebenfalls größer ist, als der Puffer auf stdout. Jetzt blockiert auch er. Deadlock. Der Vater wartet darauf, das der Sohn die Daten abholt und umgekehrt.
Deshalb es es nötig, im Vaterprozess das Lesen von stdout und das Scheiben auf stdin als eigenen Thread zu implementieren.

Beispielprogramm

Doch jetzt genug der Theorie. Auf in die Praxis. Das folgende kurze Beispielprogramm zeigt, wie man unter Windows NT den Kommandointerpreter cmd im Hintergrund started, ein kurzes Batchfile ablaufen läßt und die Daten vom Kommandointerpreter übernimmt.



#include <stdio.h>
#include <windows.h>
#include <stdlib.h>

// ReadData
// Diese Funktion wird anschliessen als eigener Thread gestarted und liest Daten aus
// der mit stdout und stderr verbundenen Pipeline und gibt sie einfach auf der
// Konsole aus.
unsigned long __stdcall ReadData(void *pipe)
{
  DWORD x;           // Anzahl der gelesenen Bytes
  char buffer[255];  // Puffer

  // Solange lesen, bis die Pipeline von der anderen Seite geschlossen wird
  while(ReadFile((HANDLE)pipe,buffer,255,&x,NULL) && GetLastError() != ERROR_BROKEN_PIPE)
    fwrite(buffer,x,1,stdout);

  fflush(stdout);

  // eigenes Handle schliessen
  CloseHandle((HANDLE)pipe);

  return 0;
}

// WriteData
// Diese Funktion wird anschliessend als eigener Thread gestarted und schreibt ein
// paar Befehle auf stdin des Kommandointerpreters cmd. Diese werden in einer Art
// Batchbetrieb abgearbeitet.
unsigned long __stdcall WriteData(void *pipe)
{
  DWORD ignore;
  
  // Ein paar Daten schreiben (es ist sicherlich ein wenig overkill, diese paar
  // Zeichen durch einen eigenen Thread schreiben zu lassen - ich moechte
  // hier nur das Prinzip demonstrieren)
  WriteFile((HANDLE)pipe,"dir /s *.exe\n",13,&ignore,NULL);
  WriteFile((HANDLE)pipe,"cd /d C:\\\n",10,&ignore,NULL);
  WriteFile((HANDLE)pipe,"type autoexec.bat\n",18,&ignore,NULL);

  // Pipeline schliessen - damit wird der anderen Seite EOF signalisiert
  CloseHandle((HANDLE)pipe);

  return 0;
}

void main()
{
  // Handles fuer die beiden Pipelines
  HANDLE hClientOut_rd,hClientOut_wr; 
  HANDLE hClientIn_rd,hClientIn_wr;
  
  // Erzeugen der beiden Pipelines - ich gebe NULL fuer den Zeiger auf
  // die SECURITY_ATTRIBUTES an. Dadurch werden beide Handles als nicht
  // vererbbar gekennzeichnet.
  if(!CreatePipe(&hClientOut_rd,&hClientOut_wr,NULL,0)) {
    printf("Can not create PIPE!\n");
    exit(5);
  }

  if(!CreatePipe(&hClientIn_rd,&hClientIn_wr,NULL,0)) {
    printf("Can not create PIPE!\n");
    exit(5);
  }

  // Die Handles, die der Kindprozess benoetigt werden als vererbbar 
  // gekennzeichnet. Dies kann entweder unter NT ueber SetHandleInformation
  // erfolgen oder unter Windows 95 ueber DuplicateHandle
  #ifdef NT
    SetHandleInformation(hClientOut_wr,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);
    SetHandleInformation(hClientIn_rd,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);
  #else
    DuplicateHandle(GetCurrentProcess(),/*source*/hClientOut_wr,
      GetCurrentProcess(),/*dest*/&hClientOut_wr,0,/*bInheritHandle*/TRUE,
      DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE);
    DuplicateHandle(GetCurrentProcess(),hClientIn_rd,GetCurrentProcess(),
      &hClientIn_rd,0,/*bInheritHandle*/TRUE,
      DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE);
  #endif

  STARTUPINFO si;

  memset(&si,0,sizeof(STARTUPINFO));

  // stdin, stdout und stderr des Kindes in STARTUPINFO eintragen
  si.cb         = sizeof(STARTUPINFO);
  si.dwFlags    = STARTF_USESTDHANDLES;
  si.hStdInput  = hClientIn_rd;
  si.hStdOutput = hClientOut_wr;
  si.hStdError  = hClientOut_wr;

  PROCESS_INFORMATION pi;

  // cmd als Kindprozess ohne eigene Konsole starten
  if(!CreateProcess(NULL,"cmd",NULL,NULL,TRUE,NORMAL_PRIORITY_CLASS | DETACHED_PROCESS,
    NULL,NULL,&si,&pi)) {
    printf("Kein Process??\n");
    exit(5);
  }

  // um die Systemresourcen zu schonen koennen wir das nicht benotigte
  // Thread Handle gleich wieder schliessen
  CloseHandle(pi.hThread);

  // Schliessen der nicht mehr benoetigten Handles, die ich an den Prozess
  // uebergeben habe. Nur so kann ich eine 'broken pipe' feststellen, wenn
  // der Kind-Prozess beendet wird.
  CloseHandle(hClientIn_rd);
  CloseHandle(hClientOut_wr);

  HANDLE threads[3];
  unsigned long ignore;

  // Starten des Lese- und Schreibthreads
  threads[0] = CreateThread(NULL,0,WriteData,(void *)hClientIn_wr,0,&ignore);
  threads[1] = CreateThread(NULL,0,ReadData,(void *)hClientOut_rd,0,&ignore);
  
  // Prozess ebenfalls in Warteliste eintragen
  threads[2] = pi.hProcess;

  // Warten bis alle fertig sind
  WaitForMultipleObjects(3,threads,TRUE,INFINITE);

  // und tschuess...
  exit(0);
}


      Top 
 

| © 1997 by 3rd-evolution