Funktion, um Datei direkt in Socket zu schreiben



  • Kann sein, dass ich hier gerade massiv auf dem Holzweg bin, deswegen frage ich hier einmal genau nach.

    Nehmen wir an, ich schreibe einen einfachen HTTP-Datenserver. HTTP deswegen, weil das Keep-Alive kann. Nehmen wir ebenfalls an, dass die Verbindung so stabil ist, dass es während der gesamten Kommunikation nicht zu einem Verbindungsabbruch kommt. Auch, dass ich die Datei eventuell locken muss, lassen wir mal komplett außer Acht.

    Wenn eine Datei vom Client angefragt wird, soll diese an den Client in einem Rutsch gesendet werden, ohne chunks. Die Länge der angefragten Datei kann ich über ein stat ermitteln und könnte eigentlich schon im HTTP-Header die Content-Length angeben.

    Jetzt wird's spannend: bisher hatte ich immer geplant, die Datei einfach mit mmap in den Arbeitsspeicher des Prozesses zu mappen, und dann send die Daten zu fressen zu geben. Allerdings gibt es dann das Problem, dass der Kernel aus Selbstschutzgründen den Inhalt des angegebenen Speichers erst in seinen eigenen Kernel-Space kopiert und dann hieraus die Daten versendet.
    Bei jedem send hätte ich also einen Flaschenhals, weil der Buffer beim Kontextwechsel kopiert werden muss.

    Daher einmal ganz allgemein gesprochen: würde ein Kernel-Call, der als Parameter den Namen auf eine Datei und den Socket übernimmt (wahlweise auch mehr) und einfach nur die Daten aus der Datei in den Socket schreibt, Sinn ergeben? Was wäre eure Einschätzung hierzu?



  • Hi,

    das macht auf jeden Fall Sinn. Beziehungsweise statt des Dateinamens würde ich einen Filedeskriptor nehmen. Ich würde die Funktion sendfile nennen 😉 .



  • dachschaden: Sehr guter Gedanke. Wie tntnet schon sagte, gibt es dafür man: sendfile(2).

    Hier ist ein recht guter Artikel darüber, wie nginx das verwendet:
    Nginx Optimization: Understanding sendfile, tcp_nodelay and tcp_nopush



  • tntnet und nman, Danke an euch beiden.
    Den eigentlichen Fall kann ich ja noch abstrahieren - ich sehe nicht ein, jedesmal von Hand einen mmap -Socket zu erstellen, wenn ich auch einfach mit einem Dateinamen arbeiten kann. :p

    Den Link habe ich in meine Favoritenliste gepackt. Den Artikel habe ich zwar durchgelesen, aber ich kenne mein löchriges Gedächtnis.

    Vielen Dank nochmals!
    Wer noch etwas hinzufügen will - ich werde noch über's Wochenende regelmäßig in das Linux/Unix-Forum reinschauen.



  • Boah, jetzt habe ich aber doch noch mal eine Frage bezüglich der Fehlerbehandlung von sendfile .

    Ich habe also die Funktion, die nur Socket und Datenname übernimmt, fertiggeschrieben, einen kleinen Multithreaded-TCP-Server gebaut, und mit 'nem Browser getestet. Dabei ist mir aufgefallen, dass wenn ich im Browser das "Runterladen"-Fenster wegklicke, der Server plötzlich abstürzt. Valgrind gibt mir einen SIGPIPE zurück, also mal Google angeworfen, welcher mir drei Lösungsvorschläge gab:

    1. Eine unter Linux nicht vorhandene Socketoption SO_NOSIGPIPE setzen - die unter Linux halt nicht vorhanden ist. ...
    2. send MSG_NOSIGNAL als Flag übergeben ... aber ich verwende sendfile , nicht send ...
    3. Oder den Signalhandler abstellen: signal(SIGPIPE,SIG_IGN); .

    Weil 1 und 2 nicht möglich sind, ist's dann drei geworden. Wieder getestet, Server stürzt nicht ab - aber nun beschwert sich Valgrind darüber, dass auf dem Stack allozierte, nicht-initialisierte Werte für "Conditional jumps or moves" verwendet wird. 😡

    Was ist passiert? Nun, in meiner Weißheit habe ich mir gedacht: Um zu prüfen, ob die Funktion jetzt einen Fehler geworfen hat, nimmste das hier:

    errno = 0;
    if((sendfile_result = sendfile(w_socket,file_handle,file_begin,file_length)) < 1)
    {
            my_errno = errno;
    }
    

    my_errno wird dann später als Fehlercode der kompletten Funktion interpretiert. Und errno ist zu diesem Zeitpunkt 0. Sprich, die Funktion sendfile speichert nicht in errno einen EPIPE ab. Was wahrscheinlich auch der Grund ist, warum das Signalsystem hier verwendet wurde.

    Das if wegzumachen bringt nichts, das ist nur der ursprüngliche Code gewesen. Inzwischen mache ich nach jedem sendfile das Schreiben von my_errno, aber der Wert ist immer 0

    Gibt es irgendeine Möglichkeit, festzustellen, ob der Fall jetzt erfolgreich war oder nicht? Und der erste, der mir "Dann mach doch 'nen Signalhandler" schreibt, fresse ich mit Haut und Haaren:

    man 2 signal schrieb:

    Die Auswirkungen von signal() in einem Multithread-Prozess sind nicht spezifiziert.

    Und dummerweise IST das ein Multithread-Prozess. Wenn das Signal gesendet wird, hätt' ich nämlich keine Ahnung, in welchem Thread das jetzt passiert ist. Abgesehen davon wüsste ich nicht mal, wie ich nur mit einer Signalnummer bewaffnet innerhalb des Handlers Reparaturen eines mir nicht mehr bekannten Threads durchführen könnte.

    EDIT: "dir frei" zu "mir drei" umgewandelt. 😋

    EDIT 2: Wozu ich wissen muss, ob sendfile jetzt geklappt hat? Weil ich davon abhängig dann halt den Thread beenden muss. Bei Erfolg darf der Thread so lange leben, bis der HTTP-Timeout (wegen keep-alive, gell?) zuschlägt. Wenn aber ein Call aus welchen Gründen auch immer fehlschlägt, dann ,kann ich den Thread gleich killen und die Resourcen jemand anderem zuweisen.



  • Erst mal ist das ignorieren von SIGPIPE wohl unter Linux die einzige Möglichkeit.

    Das sieht eigentlich richtig aus. Was mir auffällt ist, dass Du den Rückgabewert auf < 1 testest. Bei 0 wird natürlich kein errno gesetzt. Aber das kann in Deinem Fall durchaus korrekt sein.

    Ich kann nicht erkennen, dass der Fehler in dem Codeschnipsel auftritt, den Du da zeigst. Ich verwende sendfile auch in meinem Tntnet und es funktioniert genau so, wie beschrieben. Ich habe es eben auf die schnelle noch mal ausprobiert. Es liefert ein errno=32: Broken pipe. Also genau das, was wir erwarten.

    Du kannst Dir gerne meinen Code anschauen. Beispielsweise unter github. Das Signal SIGPIPE wird beim Start auf SIG_IGN gesetzt.



  • tntnet schrieb:

    Das sieht eigentlich richtig aus. Was mir auffällt ist, dass Du den Rückgabewert auf < 1 testest. Bei 0 wird natürlich kein errno gesetzt. Aber das kann in Deinem Fall durchaus korrekt sein.

    Ich habe schon mal gesagt, der echte Code, so wie ich ihn gerade verwende, sieht so nicht mehr aus.

    errno = 0;
    sendfile_result = sendfile(w_socket,file_handle,file_begin,file_length);
    printf("XXXXXXXXXXXXXXX: %u|%ld\n",my_errno,sendfile_result);
    my_errno = errno;
    printf("XXXXXXXXXXXXXXX: %u|%ld\n",my_errno,sendfile_result);
    

    Ausgabe schrieb:

    (Beim ersten Request)
    XXXXXXXXXXXXXXX: 0|12452800
    XXXXXXXXXXXXXXX: 0|12452800

    (Beim zweiten Request)
    XXXXXXXXXXXXXXX: 1027494656|1336504
    XXXXXXXXXXXXXXX: 0|1336504

    Und jetzt kommt es ja: der Browser ja schon im Hintergrund ein paar Daten vom Server. Und die Anzahl der geladenen Bytes werden vom sendfile Call zurückgegeben. Selbst, wenn ich mittendrin ESC drücke und damit den Download noch einmal abbreche. Kann er ja machen - aber dann soll er auch EPIPE setzen, dafür haben wir das verf*ckte errno ja, dass sogar thread-lokal ist.

    tntnet schrieb:

    Ich kann nicht erkennen, dass der Fehler in dem Codeschnipsel auftritt, den Du da zeigst. Ich verwende sendfile auch in meinem Tntnet und es funktioniert genau so, wie beschrieben. Ich habe es eben auf die schnelle noch mal ausprobiert. Es liefert ein errno=32: Broken pipe. Also genau das, was wir erwarten.

    Ah, das hier erklärt es natürlich - ihr ruft die Funktion mehrmals hintereinander auf und macht dazwischen einen Längencheck.

    if (offset >= count || s == 0)
                break;
    

    In dem Code habt ihr übrigens eine Race-Condition, glaube ich. Zuerst macht ihr stat auf die Datei, und später erstellt ihr einen Fdfile in . Wenn sich zwischendurch die Datei ändert, könnt ihr hier leicht Probleme bekommen. Deswegen habe ich auch gesagt, dass man die Dateien eventuell locken muss. 😉

    Begeistert bin ich auch nicht wirklich davon, dass ich sendfile mehrmals aufrufen muss. Deswegen MACH ich den Call ja - der soll eine Datei aus dem Kernel-Space verschicken, und wenn das nicht klappt, soll er mir direkt einen Fehlercode liefern und die Anzahl der Bytes statt beim nächsten Call. Sowas würde ich als "Pfusch am Bau" gezeichnen - weil ich dadurch einen zusätzlichen Kontextwechsel auf mich nehmen muss. Geil finde ich das jetzt nicht.
    .
    Naja, trotzdem Danke!
    Ich werde einfach eine ordentliche Längenprüfung machen ohne wiederholtes sendfile . Die Funktion ist ja darauf ausgelegt - zumindest bei blockierenden Sockets, und was anderes dulde ich eh nicht - dass man sie nur einmal aufruft. Bei nicht-blockierenden Sockets hätte ich übrigens nichts gesagt, da ergibt das Verhalten von sendfile absolut Sinn.

    Vielen Dank für die Zeit und den Code. Schönes Wochenende noch!


Anmelden zum Antworten