Hacken Sie einen Linux-Dateideskriptor

Sie haben den Begriff Dateideskriptor (Socket-Deskriptor) sowohl in Programmen, die Daten über das Netzwerk unter Linux verarbeiten, als auch in Programmen, die Daten in lokalen Dateien verarbeiten, gehört.

Es wurde auch als Socket-Deskriptor in Einführung in die in C-Sprache erlernte Socket-API, Teil 1 Server Edition angezeigt.

Die meisten Menschen verstehen, dass ein ganzzahliger Wert verwendet wird, um eine Datei zu identifizieren, die bereit ist, mit einem Prozess zu interagieren, aber nicht jeder weiß so tief darüber hinaus. Ist es?

Solange Sie eine App unter Linux erstellen, müssen Sie dies natürlich nicht mehr wissen. Wenn Sie die CRUD-bezogene API mithilfe des Dateideskriptors richtig auswählen und programmieren können, gibt es als Programmierer kein Problem. (Das Problem mit Dateideskriptoren besteht darin, dass der Prozess die maximale Anzahl von Dateideskriptoren überschritten hat, die er öffnen kann, aber dann müssen Sie diese Grenze erhöhen oder sie wirklich so weit öffnen. Wir werden das Anwendungsdesign überprüfen, um festzustellen, ob es möglich ist.)

In der Sprache C sieht es wie folgt aus.

python


#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char* argv[]) {

    int fd; // File Descriptor

    fd = open("tmp.txt", O_RDONLY);
    if (fd < 0) {
        perror("open() failed.");
        exit(EXIT_FAILURE);
    }   

    printf("%d\n", fd); // 3

    close(fd);

    return EXIT_SUCCESS;
}

Der Integer-Typ fd ist der Dateideskriptor. Wenn es normal ist, wird ein Wert von 0 oder mehr erfasst.

Ich denke auch, dass Dateideskriptoren normalerweise von Programmiersprachen und Bibliotheken umschlossen werden und CRUDs unter einer effizienteren Kontrolldatenstruktur ausgetauscht werden.

In den folgenden Fällen passt fopen die Anzahl der Systemaufrufe an und es werden andere Maßnahmen ergriffen, um eine effiziente E / A-Verarbeitung zu ermöglichen. Daher denke ich, dass diese APIs normalerweise verwendet werden. Andere Programmiersprachen haben ebenfalls solche APIs, und ich schreibe oft PHP, aber PHP hat auch eine Dienstprogrammfunktion namens fopen.

python


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

int main (int argc, char* argv[]) {

    FILE *fp; // File pointer

    fp = fopen("tmp.txt", "r");
    if (fp == NULL) {
        fprintf(stderr, "fopen() failed.\n");
        exit(EXIT_FAILURE);
    }   

    printf("%d\n", fp->_fileno); // 3

    fclose(fp);

    return EXIT_SUCCESS;
}

Linux (Unix) ist ein System, das alles im System in das abstrakte Konzept von Dateien sublimiert. Das allgemeine Konzept des Linux-Dateisystems ist nicht allzu kompliziert, um tagelang darüber zu sprechen. Es ist so einfach, dass es leicht zu besprechen ist.

Wenn sowohl Programmierer als auch allgemeine Benutzer die Nuancen des abstrakten Konzepts von Dateien verstehen können, wird eine Schnittstelle bereitgestellt, über die sie alles intuitiv und gleichermaßen bedienen können.

Der Dateideskriptor wird dann als Bezeichner verwendet, um auf die Entität der abstrakten Datei zuzugreifen.

Ich dachte, wenn ich etwas mehr über den Dateideskriptor wissen könnte, der die Grundlage des Systems bildet, könnte ich das System namens Linux besser nutzen, also habe ich es untersucht.

Ich weiß jedoch nicht, wie ich das untersuchen soll, selbst wenn es vage als Dateideskriptor bezeichnet wird. Deshalb habe ich mich entschlossen, den damit verbundenen Systemaufruf zu untersuchen.

Hack des offenen Systemaufrufs

Ich werde es untersuchen, indem ich die Reihe von Open-Prozessen verfolge, die zuvor herausgekommen sind. Sie können den Dateideskriptor als Ergebnis von open erhalten. Wenn Sie diesen Vorgang ausführen, sollten Sie etwas wissen.

python


 415879:   b8 02 00 00 00          mov    $0x2,%eax
 41587e:   0f 05                   syscall 

Lassen Sie uns die Verarbeitung der Funktion __libc_open von glibc, die der Funktion open entspricht, schnell zerlegen und überprüfen. Die Rufnummer des offenen Systems lautet 2.

include/asm-x86_64/unistd.h


#define __NR_open                                2
__SYSCALL(__NR_open, sys_open)

Sie können sehen, dass die entsprechende Funktion in der Tabelle sys_call_table sys_open ist.

Folgen wir nun dem Prozess von sys_open.

fs/open.c(933-941)


asmlinkage long sys_open(const char __user * filename, int flags, int mode)
{
    char * tmp; 
    int fd, error;

#if BITS_PER_LONG != 32
    flags |= O_LARGEFILE;
#endif

Aktivieren Sie zunächst für 64-Bit-Computer automatisch das Flag O_LARGEFILE. Auf diese Weise können Sie Dateien verarbeiten, die größer als 2 GB sind. Heutzutage müssen Sie sich auf 64-Bit-Computern keine Sorgen mehr machen, aber Sie müssen vorsichtig sein, wenn Sie den Prozess der Verarbeitung von Dateien auf 32-Bit-Computern in Ihrer App implementieren. Ich habe übrigens gehört, dass Sie keine Dateien mit mehr als 2 GB mit Modulen öffnen können, deren Entwicklung in der 32-Bit-Maschinen-Ära gestoppt wurde, aber Sie haben dieses Flag nicht angegeben.

fs/open.c(941)


    tmp = getname(filename);

Verwenden Sie die Funktion getname, um das Objekt mit dem Dateinamen vom Benutzerbereich in den Kernelbereich zu übertragen. Der Speicherbereich wird mit dem Plattenverteiler zugeordnet. Der Slab Allocator ist eine der Methoden, die Linux für eine effiziente Speicherzuweisung verwendet. Ich möchte mich bald mit dem Plattenverteiler befassen.

fs/open.c(942-944)


    fd = PTR_ERR(tmp);
    if (!IS_ERR(tmp)) {
        fd = get_unused_fd();

Wenn keine Fehler vorliegen, führen Sie die Funktion get_unused_fd aus, um einen freien Dateideskriptor zu finden. Nach dem Namen der Funktion zu urteilen, scheint dieser Prozess Ihnen zu sagen, was der Dateideskriptor ist.

fs/open.c(838-845)


int get_unused_fd(void)
{
    struct files_struct * files = current->files;
    int fd, error;

    error = -EMFILE;
    spin_lock(&files->file_lock);

Zum Zeitpunkt des Fork-Systemaufrufs wurde ebenfalls erläutert, aber current ist ein Zeiger auf den Prozessdeskriptor (task_struct-Struktur) des Prozesses, der auf der aktuellen CPU ausgeführt wird. Ist ein Makro zu bekommen.

current-> files enthält einen Zeiger auf die Datei files_struct, der Informationen zu den vom aktuellen Prozess geöffneten Dateien enthält.

Die Datei files_struct hat die folgende Struktur. Bitte beachten Sie, dass Sie hier am Ende noch einmal nachsehen müssen.

include/linux/file.h


/*
 * Open file table structure
 */
struct files_struct {
        atomic_t count;
        spinlock_t file_lock;     /* Protects all the below members.  Nests inside tsk->alloc_lock */
        int max_fds;
        int max_fdset;
        int next_fd;
        struct file ** fd;      /* current fd array */
        fd_set *close_on_exec;
        fd_set *open_fds;
        fd_set close_on_exec_init;
        fd_set open_fds_init;
        struct file * fd_array[NR_OPEN_DEFAULT];
};

files-> open_fds ist ein Zeiger auf die fd_set-Struktur files-> open_fds_init, und files-> open_fds_init repräsentiert den aktuell geöffneten Dateideskriptor als 64x16 = 1024-Bitmap. Unten finden Sie einen Auszug der Gründe für die von mir vorgenommene Berechnung.

include/linux/posix_types.h


#define __NFDBITS   (8 * sizeof(unsigned long))
#define __FD_SETSIZE    1024
#define __FDSET_LONGS   (__FD_SETSIZE/__NFDBITS)

typedef struct {
    unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;

Normalerweise reichen 1024 Bit aus, aber wenn dies nicht ausreicht, wird es durch die Funktion expand_files erweitert. Es hängt mit der Geschichte zusammen, über die ich am Anfang gesprochen habe: Wenn Sie weniger Eröffnungsprozesse haben, können Sie das Limit erhöhen.

fs/open.c(847)


    fd = find_next_zero_bit(files->open_fds->fds_bits,
                files->max_fdset,
                files->next_fd);

Verwenden Sie von hier aus die Funktion find_next_zero_bit, um freie Bits zu finden und abzurufen.

fs/open.c(872-874)


    FD_SET(fd, files->open_fds);
    FD_CLR(fd, files->close_on_exec);
    files->next_fd = fd + 1; 

Fügt der Gruppe der geöffneten Dateideskriptoren einen neuen Dateideskriptor hinzu und entfernt ihn aus der Gruppe der Deskriptoren, die während exec () geschlossen werden.

Sie können sehen, dass dieser Prozess einen Vorteil bei der Kommunikation mit untergeordneten Prozessen hat. Es bezieht sich auch auf die Verarbeitung von Rohren, die eine prozessübergreifende Kommunikation realisieren.

Aktualisieren Sie dann das Mitglied next_fd auf die maximale Anzahl zugewiesener Dateideskriptoren plus eins. Es wird für den nächsten Dateideskriptor-Scan verwendet.

Nun, ich habe versucht, den Dateideskriptor zu erhalten, aber ich kann den tatsächlichen Dateideskriptor immer noch nicht sehen. Trotzdem habe ich das Gefühl, dass ich vorerst gerade einen kostenlosen Dateideskriptor als Schlüssel bekommen habe.

Es kehrt wieder zur Verarbeitung von sys_open zurück. fd wird als Dateideskriptor zurückgegeben.

fs/open.c(945-951)


        if (fd >= 0) { 
            struct file *f = filp_open(tmp, flags, mode);
            error = PTR_ERR(f);
            if (IS_ERR(f))
                goto out_error;
            fd_install(fd, f);
        }    

Führen Sie die Funktion filp_open aus, um die Adresse der Dateistruktur abzurufen, die basierend auf dem Pfad, dem Zugriffsmodus und den Berechtigungsbits der als Argumente übergebenen Datei erhalten wurde.

Die Dateistruktur ist wie folgt. Da wir alle wichtigen Mitglieder haben, möchte ich nur für die Dateistruktur einen anderen Artikel erstellen.

include/linux/fs.h


struct file {
    struct list_head    f_list;
    struct dentry       *f_dentry;
    struct vfsmount         *f_vfsmnt;
    struct file_operations  *f_op;
    atomic_t        f_count;
    unsigned int        f_flags;
    mode_t          f_mode;
    int         f_error;
    loff_t          f_pos;
    struct fown_struct  f_owner;
    unsigned int        f_uid, f_gid;
    struct file_ra_state    f_ra;

    size_t          f_maxcount;
    unsigned long       f_version;
    void            *f_security;

    /* needed for tty driver, and maybe others */
    void            *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    spinlock_t      f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
};

Folgen wir nun der Verarbeitung der Funktion filp_open.

fs/open.c(753-762)


struct file *filp_open(const char * filename, int flags, int mode)
{
    int namei_flags, error;
    struct nameidata nd;

    namei_flags = flags;
    if ((namei_flags+1) & O_ACCMODE)
        namei_flags++;
    if (namei_flags & O_TRUNC)
        namei_flags |= 2;

Stellen Sie den Zugriffsmodus für namei_flags entsprechend ein. Beachten Sie, dass wir die Flags in namei_flags in ein spezielles Format konvertieren.

In der Binärdatei geht 00 (schreibgeschützt) auf 01, 01 (schreibgeschützt) auf 10 und 10 (schreibgeschützt und schreibgeschützt) auf 11. Mit anderen Worten, wenn 0 Bit gesetzt ist, bedeutet dies Lesen, und wenn 1 Bit gesetzt ist, bedeutet dies Schreiben.

Dieses konvertierte Flag wird für die spätere Verarbeitung verwendet.

fs/open.c(764)


    error = open_namei(filename, namei_flags, mode, &nd);

Die Funktion open_namei übernimmt den wichtigen Teil des eigentlichen Öffnens der Datei. Übergeben Sie als Argumente den Dateinamen, konvertierte Zugriffsmodus-Flags, Berechtigungsbits und einen Zeiger auf die Nameidata-Struktur.

Die Namensdatenstruktur ist wie folgt definiert.

include/linux/namei.h


struct nameidata {
    struct dentry   *dentry;
    struct vfsmount *mnt;
    struct qstr last;
    unsigned int    flags;
    int     last_type;
    unsigned    depth;
    char *saved_names[MAX_NESTED_LINKS + 1]; 

    /* Intent data */
    union {
        struct open_intent open;
    } intent;
};

Dies ist auch eine wichtige Struktur, daher würde ich mir gerne etwas Zeit nehmen, um mich damit zu befassen. Was ich jetzt beachten sollte, ist der Zustand des Zahnersatzes, der ein Zeiger auf die Zahnersatzstruktur ist, und das auf dem System montierte Dateisystem. Ein mnt-Mitglied, das ein Zeiger auf die vfsmount-Struktur ist, die aufzeichnet.

Die Aufgabe innerhalb der Funktion open_namei besteht darin, das Objekt der nameidata-Struktur basierend auf dem Pfadnamen und den Flags abzurufen.

Der Hauptteil der Verarbeitung erfolgt in einer anderen Funktion namens path_lookup, aber die Suchmethode wird entsprechend dem Zugriffsmodus und dem fs, das die dem Prozess zugeordneten Dateisysteminformationen aus dem aktuellen Prozessdeskriptor speichert, fein angepasst Der Suchvorgang wird basierend auf den Informationen gestartet.

Infolgedessen enthält die Namensdatenstruktur die Daten des Ergebnisses der Suche nach Pfadnamen.

fs/open.c(764-768)


    if (!error)
        return dentry_open(nd.dentry, nd.mnt, flags);

    return ERR_PTR(error);
}

Zu diesem Zeitpunkt haben Sie einen Zeiger auf die Dentry-Struktur für den Pfadnamen und einen Zeiger auf die vfsmount-Struktur. Die Funktion dentry_open erstellt ein Dateiobjekt basierend auf den beiden Informationen und den konvertierten Flags.

fs/open.c(945-951)


        if (fd >= 0) { 
            struct file *f = filp_open(tmp, flags, mode);
            error = PTR_ERR(f);
            if (IS_ERR(f))
                goto out_error;
            fd_install(fd, f);
        }    

Speichern Sie den Zeiger auf das von der Funktion dentry_open erhaltene Dateiobjekt in der Variablen f. Und wenn keine Fehler vorliegen, übergeben wir einen Zeiger auf den Dateideskriptor und das Dateiobjekt an die Funktion fd_install, die den Kern des Dateideskriptors bildet.

fs/open.c(945-951)


void fastcall fd_install(unsigned int fd, struct file * file)
{
    struct files_struct *files = current->files;
    spin_lock(&files->file_lock);
    if (unlikely(files->fd[fd] != NULL))
        BUG();
    files->fd[fd] = file;
    spin_unlock(&files->file_lock);
}

Das Mitglied des aktuellen Prozessdeskriptors (files_struct *) Das Mitglied der Dateien (fd_set *) fd speichert den Zeiger auf das Dateiobjekt, das zuvor von der Funktion filp_open erhalten wurde.

Dieses fd-Mitglied ist ein Zeiger auf ein Array von Zeigern auf das Dateiobjekt (Strukturdatei **), aber die Ganzzahlen im Dateideskriptor stimmen mit den Indizes in diesem Array überein.

Übrigens zeigt fd auf das fd_array-Mitglied in derselben Struktur, aber im Fall einer 64-Bit-Maschine wird ein neuer Bereich reserviert und auf diese Adresse verwiesen, wenn die Anzahl der Dateideskriptoren 64 überschreitet.

Mit anderen Worten, files-> fd [0] zeigt auf den Dateideskriptor 0 und files-> fd [1] zeigt auf das Dateiobjekt des Dateideskriptors 1.

Bekanntlich entspricht 0 normalerweise der Standardeingabe, 1 der Standardausgabe und 2 der Standardfehlerausgabe. Daher sollte der Deskriptor der ersten vom Benutzer geöffneten Datei 3 sein. Sogar das in der ersten Demo erstellte Programm wird als 3 angezeigt.

Was ist ein Dateideskriptor unter Linux? Wenn du darüber antwortest

__ Index des Arrays, das das für den aktuellen Prozess geöffnete Dateiobjekt enthält __

Es kann gesagt werden.

Es war ein wenig schwer in Worten zu verstehen, so intuitiv ist es wie folgt.

python


//[fd]Ist ein Dateideskriptor
current->files->fd[fd]

Anschließend wird die Operation für die geöffnete Datei ausgeführt, indem auf die Informationen des Dateiobjekts unter Verwendung des Index des fd-Elements zugegriffen wird.

Das Dateiobjekt verfügt über ein f_pos-Member, das den Offset für die übertragenen Bytes aufzeichnet. Sie können also die Dateiverarbeitung fortsetzen, indem Sie diesen Wert mit der Anzahl der übertragenen Bytes aktualisieren. Die Operation selbst wird unter Verwendung der Dateioperationsfunktionen (Lesen, Schreiben usw.) ausgeführt, die im f_op-Member im Dateiobjekt definiert sind.

Ab 2.6.14 wurde übrigens eine neue fdtable-Struktur erstellt und die files_struct-Struktur entsprechend geändert. Die folgende Struktur hat sich nach und nach geändert, aber die zugrunde liegende Struktur bleibt bis Linux 4.9 dieselbe.

c:Linux2.6.14


struct fdtable {
    unsigned int max_fds;
    int max_fdset;
    int next_fd;
    struct file ** fd;      /* current fd array */
    fd_set *close_on_exec;
    fd_set *open_fds;
    struct rcu_head rcu;
    struct files_struct *free_files;
    struct fdtable *next;
};

struct files_struct {
    atomic_t count;
    struct fdtable *fdt;
    struct fdtable fdtab;
    fd_set close_on_exec_init;
    fd_set open_fds_init;
    struct file * fd_array[NR_OPEN_DEFAULT];
    spinlock_t file_lock;     /* Protects concurrent writers.  Nests inside tsk->alloc_lock */
};

void fastcall fd_install(unsigned int fd, struct file * file)
{
    struct files_struct *files = current->files;
    struct fdtable *fdt;
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    BUG_ON(fdt->fd[fd] != NULL);
    rcu_assign_pointer(fdt->fd[fd], file);
    spin_unlock(&files->file_lock);
}

Anstatt das fd-Member direkt aus der files_struct-Struktur zu referenzieren, verweist der Ablauf daher auf fd (einen Zeiger auf ein Array von Zeigern auf das Dateiobjekt), indem das fdt-Member einmal durchlaufen wird.

Allzweck-Dateideskriptor

Gehen wir nun in Bezug auf Dateideskriptoren noch einen Schritt weiter.

Zu Beginn erwähnte ich, dass sowohl Programme, die Daten über das Netzwerk unter Linux verarbeiten, als auch Programme, die Daten in lokalen Dateien verarbeiten, Dateideskriptoren (Socket-Deskriptoren) verwenden.

Einführung in die in C Language gelernte Socket-API 3. Server / Client Edition Nr. 1 verwendet Socket-APIs wie recv und send, dieser Prozess wird jedoch gelesen. Es kann vollständig durch eine grundlegende Eingabe- / Ausgabe-API wie oder Schreiben ersetzt werden.

Dies liegt daran, dass der Socket einen Dateideskriptor erhält und die Entität, auf die er zugreifen kann, nichts anderes als ein Dateiobjekt ist. Das Folgende ist ein Teilauszug des Prozesses zum Öffnen eines Sockels.

net/socket.c(377)


struct file *file = get_empty_filp();

Holen Sie sich mit der Funktion get_empty_filp einen Zeiger auf ein neues Dateiobjekt. Diese Funktion reserviert auch einen Speicherbereich für das Dateiobjekt durch den Plattenzuordner.

net/socket.c(402-407)


        sock->file = file;
        file->f_op = SOCK_INODE(sock)->i_fop = &socket_file_ops;
        file->f_mode = FMODE_READ | FMODE_WRITE;
        file->f_flags = O_RDWR;
        file->f_pos = 0; 
        fd_install(fd, file);

Nehmen Sie verschiedene Einstellungen für das erfasste Dateiobjekt und schließlich fd_install vor. Es ist dasselbe wie der offene Systemaufruf.

Es sind nicht nur Steckdosen. Sogar die Verarbeitung von Rohren. Das Folgende ist ein Teilauszug der Verarbeitung des Rohrsystemaufrufs.

fs/pipe.c(402-407)


    f1 = get_empty_filp();
    if (!f1)
        goto no_files;

    f2 = get_empty_filp();
    if (!f2)
        goto close_f1;

f1 ist das Dateiobjekt zum Lesen und f2 ist das Dateiobjekt zum Schreiben.

fs/pipe.c(760-774)


    /* read file */
    f1->f_pos = f2->f_pos = 0;
    f1->f_flags = O_RDONLY;
    f1->f_op = &read_pipe_fops;
    f1->f_mode = FMODE_READ;
    f1->f_version = 0;

    /* write file */
    f2->f_flags = O_WRONLY;
    f2->f_op = &write_pipe_fops;
    f2->f_mode = FMODE_WRITE;
    f2->f_version = 0;

    fd_install(i, f1);
    fd_install(j, f2);

Sie können sehen, dass dies der gleiche Vorgang wie beim Öffnen ist, da für jedes Dateiobjekt verschiedene Einstellungen vorgenommen werden und das Ende weiterhin fd_install ist.

Wie Sie sehen können, spielt der Dateideskriptor eine sehr wichtige Rolle im System Linux, das versucht, alles mit Dateien zu betreiben. Nur für den Fall, ich habe auch den derzeit stabilen Linux 4.7-Code gelesen, aber diese prinzipielle Verarbeitung hat sich nicht geändert, und diese Idee lebt noch.

Ich möchte die verschiedenen Datenstrukturen und -prozesse näher betrachten, die ich diesmal nicht viel erklärt habe, selbst wenn ich das Dateisystem separat untersucht habe.

Referenzierter Quellcode

Linux-Kernel: 2.6.11 glibc:glibc-2.12.1 CPU:x86_64

Recommended Posts

Hacken Sie einen Linux-Dateideskriptor
[Linux] Dateisuche
Informationen zu Linux-Datei- und Verzeichnisberechtigungen
Hack Linux Fork Systemaufrufe
Linux-Lernsitzung 2 .: Dateibetrieb
[Linux] Häufig verwendete Linux-Befehle (Dateibetrieb)
Linux
[Linux] Datei- und Verzeichnisoperationsbefehle
[NFS] Dateifreigabe zwischen Linux-Hosts
Aufbau eines Linux-Dateiservers (Ubuntu & Samba)
Umbenennen basierend auf der Änderungszeit der Datei (Linux)
Ping-t-Gerät (Betreff 104), Linux-Dateisystem, FSH