os.read(0, 1) vs. sys.stdin.read(1) unter select.select()

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

Hallo,

ich bin auf folgendes Verhalten von 'select()' aufmerksam geworden und kann es mir nicht wirklich erklären. Zum Nachvollziehen hier ich ein Beispiel:

Code: Alles auswählen

import os
import select
import sys
import termios
import tty


class Cbreak(object):
    def __init__(self):
        self.old_term_settings = termios.tcgetattr(0)

    def __enter__(self):
        tty.setcbreak(0, termios.TCSADRAIN)
        return self

    def __exit__(self, type_, value, traceback):
        termios.tcsetattr(0, termios.TCSAFLUSH, self.old_term_settings)

    def write_(self, string_):
        sys.stdout.write(string_)
        sys.stdout.flush()


def wait_for_ready(timeout, terminal):
    terminal.write_('wait_for_ready(timeout={0}) >> '.format(timeout))
    if timeout is None:
        ready, _, _ = select.select([0], [], [])
    else:
        ready, _, _ = select.select([0], [], [], timeout)
    terminal.write_('ready: {0} >> '.format(ready))
    return ready

def input_(manner, timeout=None):
    with Cbreak() as terminal:
        while True:
            terminal.write_('\n')
            wait_for_ready(timeout, terminal)
            if manner == 'o':
                terminal.write_('input_: >> ')
                ch = os.read(0, 1)
                terminal.write_(repr(ch))
            else:
                terminal.write_('input_: >> ')
                ch = sys.stdin.read(1)
                terminal.write_(repr(ch))
            if ch == 'q':
                break
input_('o'):
  1. 'select()' wartet, weil nichts zum Auslesen vorhanden ist
    --> drücke <left> ('\x1b', '[', 'D')
  2. 'select()' gibt 'sys.stdin' als bereit zum Auslesen zurück.
    Das geschieht 3 x in Folge, für jedes der 3 Zeichen in der Sequenz.
  3. 'select()' wartet wieder, weil nichts (mehr) zum Auslesen vorhanden ist
  4. ...
input_('s'):
  1. 'select()' wartet, weil nichts zum Auslesen vorhanden ist
    --> drücke <left> ('\x1b', '[', 'D')
  2. 'select()' gibt 'sys.stdin' als bereit zum Auslesen zurück.
    Das geschieht 1 x für '\x1b'
  3. 'select()' wartet, obwohl sich '[' und 'D' noch in 'sys.stdin' befinden.
    --> drücke a
  4. 'select()' gibt 'sys.stdin' als bereit zum Auslesen zurück.
    Das geschieht 3 x in Folge für '[', 'D' und das 'a'
  5. 'select()' wartet wieder, weil nichts (mehr) zum Auslesen vorhanden ist
  6. ...
Ich frage mich nun, auf Grund welchen Zustands 'select()' entscheidet, ob 'sys.stdin' bereit zum Auslesen ist oder nicht. Der Puffer als Indikator kann es nicht sein:
- Descriptoren haben keinen Puffer.
- Sollte 'select()' auf den dahinterliegenden Dateipuffer prüfen, so müssten die restlichen Zeichen '[' und 'D' darin ja auch erkannt und somit ein 'ready' auslösen.

Ich bin ratlos...

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
BlackJack

@mutetella: Was meinst Du mit „Deskriptoren haben keinen Puffer“? Und Du musst immer daran denken, dass auch die „Gegenseite“ einen Puffer haben kann. Über den kann `select()` nichts aussagen.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

BlackJack hat geschrieben:Was meinst Du mit „Deskriptoren haben keinen Puffer“?
Dass ein descriptor letztlich ja nur ein Verweis auf eine Datei und nicht die Datei selbst ist. Und bla bla und brabbel brabbel... Jetzt, wo ich versuche, das zu artikulieren, weiß ich eigentlich auch nicht mehr, was ich damit gemeint habe... :?
BlackJack hat geschrieben:(...), dass auch die „Gegenseite“ einen Puffer haben kann.
Dann müsste mein Beispiel dahingehend

Code: Alles auswählen

def wait_for_ready(timeout, terminal):
    ...
        ready, w, _ = select.select([0], [0], [])
    ...
        ready, w, _ = select.select([0], [0], [], timeout)
geändert werden. Somit verhält sich zwar 'input_("s")' wie gewünscht, aber doch nicht, weil 'select()' jetzt richtiger Weise erkennt, dass 'sys.stdin' bereit zum Auslesen ist, sondern weil 'sys.stdin' bereit zum Beschreiben ist. Aber darum geht es ja nicht...
Ich vermute, Du meinst mit "Gegenseite" was ganz anderes? Geht es in die Richtung: Was in 'sys.stdin' reingeht, landet automatisch in 'sys.stdout'?
--> Aber warum sollte 'select()' darüber nichts aussagen können....?

Ok, ganz andere Richtung:
1. Ich möchte wissen, ob sich noch Zeichen im Puffer befinden.
2. 'select()' kann mir sagen, ob 'sys.stdin' bereit zum Lesen und/oder bereit zum Schreiben ist.
3. Bereit zum Lesen heißt nicht bereit zum Auslesen, sondern bereit zum Lesen vom Eingabegerät. Und diese Bereitschaft ist dann gegeben, wenn der Puffer leer ist.
--> Aber warum meldet mir 'select()' dann eine Bereitschaft, obwohl sich noch restliche Zeichen im Puffer befinden?

Wisst ihr was? Ich kapier's nicht!!

mutetella

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
BlackJack

@mutetella: Bereit zum Lesen heisst doch gerade *nicht* dass der Puffer leer ist, sondern dass da tatsächlich etwas *sofort* gelesen werden kann, ohne dass man warten muss. Das ist ja gerade der Sinn von `select()`, dass man — in der Regel bei mehreren Quellen — die Quelle bekommt, bei der man direkt etwas nicht-blockierend auslesen kann. Und umgekehrt beim Schreiben die Senke bei der man definitiv Daten loswerden kann.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

@BlackJack: Eben! Das war ja meine Grundannahme. Bestätigt sich aber nur, wenn ich 'sys.stdin' über 'os.read()' auslese. Wenn ich 'sys.stdin.read()' verwende geschehen merkwürdige Dinge.
Versuche doch bitte mal mein Beispiel-'input_()' mit Option 'o'. Dabei geschieht, was ich erwarte und Du ja auch so beschreibst.
Aber wie erklärst Du Dir das Verhalten, wenn Du Option 's' verwendest?

Da stimmt doch was nicht!

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

Bin soeben hier auf eine Antwort gestoßen, die genau das 'eigenartige' Verhalten erklärt:
Im Gegensatz zu 'os.read(0, 1)' liest 'sys.stdin.read(1)' nicht nur das 1 Byte, sondern mehrere und puffert diese intern. 'select()' geht dann natürlich davon aus, dass der Puffer leer ist und meldet Bereitschaft, obwohl die noch vorhandenen Zeichen noch im Puffer sind, nur eben nicht mehr in dem, der von 'select()' geprüft wird.

Was haben die Leute früher nur ohne Google gemacht? :wink:

Jetzt kann ich endlich in Frieden schlafen gehen...

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
BlackJack

@mutetella: Vor Google gab es Bücher, Usenet, FidoNet, BBSen, und natürlich kann man auch in den Quelltext schauen. ;-)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

mutetella hat geschrieben:...- Descriptoren haben keinen Puffer. ...
Das ist erstmal richtig, da ein filedescriptor einfach nur ein Integerwert ist. Allerdings freuen wir uns nicht einfach der Zahl, wenn wir diese benutzen, sondern interessieren für das "Konstrukt" hinter dieser Zahl, und dort sind sehr wohl Puffer im Spiel.

Was ist ein filedescriptor unter Linux?
Der filedescriptor ist ein Index auf die n-te geöffnete Datei des gegenwärtigen Prozesses. STDIN, STDOUT und STDERR sind vorbelegt, jede weitere geeöffnete Datei erhält den nächsthöheren Index. Verwaltet wird diese Information vom Kernel in einer fd-Tabelle im struct task_struct, klebt also direkt am Prozess. Das "Dateiobjekt" liegt im Kernel als struct file vor, welches mit einer open-Aktion aus einem Userspace-Prozess vom Kernel kreiiert wird. In diesem Objekt legt der Kernel eine Reihe von Flags ab, z.B. die angefragten Operationen (read, write..) oder die Inode im virtuellen "Dateisystem". Mit ioctl und fnctl kann man diese teilweise einsehen oder manipulieren.
Warum der Kernel hier diesen Aufwand treibt, wird ersichtlich wenn man Dinge wie Speicherverwaltung/-schutz, Prozessmodell usw. bedenkt und dem Unix-Paradigma "alles ist eine Datei" folgt.

Einfachster Anwendungsfall: Prozess A möchte Prozess B Daten übermitteln. Allerdings laufen beide Prozesse im userspace und können nicht ohne weiteres über gemeinsame Speicherbereiche verfügen, um die Daten auszutauschen. Diese Lücke kann der Kernel, der im Ring 0 läuft, schliessen. Grob zusammengefasst, passiert folgendes:
- Prozess A öffnet eine Datei zum Schreiben (Datei kann hier alles mögliche sein - socket, pipe, fifo...)
- Kernel erhält ein syscall open, erstellt ein file-struct mit den gewünschten Operationen, assoziiert eine Inode im vfs und gibt einen fd zurück
- Prozess A schreibt in fd Puffer X aka write(fd, Speicheradresse, Länge) - Puffer X ist Speicheradresse + Länge
- Kernel erhält syscall write mit fd und den Pufferdaten
- über die fd-Tabelle von Prozess A ermittelt der Kernel die zu beschreibende Datei (Inode)
- Kernel sucht Empfänger der Daten (Prozesse mit f_op read für diese Datei)
----> kein Empfänger - Kernel hält Prozess A an (Prozess A wartet nun auf Empfänger - blocking)

- Prozess B öffnet Datei zum Lesen
- Kernel: syscall open --> filestruct + inode
- Prozess B liest von fd in Puffer Y ein - read(fd, Adresse, Länge) - Puffer Y ist Adresse + Länge
- Kernel: syscall read für diesen fd
- Kernel findet write Operation für diese Datei von Prozess A
- Kernel kopiert Daten von Puffer X zu Puffer Y
- Kernel: Ausführung von Prozess A wird wieder aufgenommen

select interagiert mit diesem Modell dergestalt, dass es Informationen über mögliche Lese- und Schreibaktionen aus der kernelinternen Verwaltung hierüber abfragen kann.
Mit select, poll und fds bist Du halt sehr systemnah, was dem Verständnis nicht unbedingt zuträglich ist. Python entfernt sich von dieser Systemnähe und schafft dadurch eine gewisse Plattformunabhängigkeit. Das geht aber auch auf Kosten systemnaher Tools wie select, die dann mitunter nicht mehr nutzbar sind.

Edit: Prozess A <-> B typo verbessert.
Antworten