Was macht der Systemaufruf brk ()?

184

Laut Linux-Programmierhandbuch:

brk () und sbrk () ändern den Ort der Programmunterbrechung, wodurch das Ende des Datensegments des Prozesses definiert wird.

Was bedeutet das Datensegment hier? Ist es nur das Datensegment oder Daten, BSS und Heap kombiniert?

Laut Wiki:

Manchmal werden die Daten-, BSS- und Heap-Bereiche gemeinsam als "Datensegment" bezeichnet.

Ich sehe keinen Grund, nur die Größe des Datensegments zu ändern. Wenn es sich um Daten, BSS und Heap handelt, ist dies sinnvoll, da Heap mehr Speicherplatz erhält.

Das bringt mich zu meiner zweiten Frage. In allen Artikeln, die ich bisher gelesen habe, sagt der Autor, dass der Haufen nach oben und der Stapel nach unten wächst. Was sie jedoch nicht erklären, ist, was passiert, wenn der Heap den gesamten Raum zwischen Heap und Stack einnimmt?

Geben Sie hier die Bildbeschreibung ein

nik
quelle
1
Was machst du also, wenn du keinen Platz mehr hast? Sie wechseln zur Festplatte. Wenn Sie Speicherplatz verwendet haben, geben Sie ihn für andere Informationen frei.
Igoris Azanovas
28
@Igoris: Sie verwechseln den physischen Speicher (den Sie bei Bedarf mithilfe des virtuellen Speichers auf die Festplatte austauschen können) und den Adressraum . Wenn Sie Ihren Adressraum ausfüllen, erhalten Sie diese Adressen in der Mitte ohne Austausch zurück.
Daniel Pryden
7
Nur zur Erinnerung, der brk()Systemaufruf ist in der Assemblersprache nützlicher als in C. In C malloc()sollte er anstelle von brk()Datenzuweisungszwecken verwendet werden - dies macht die vorgeschlagene Frage jedoch in keiner Weise ungültig.
Alecov
2
@Brian: Der Heap ist eine komplexe Datenstruktur für die Verarbeitung von Regionen unterschiedlicher Größe und Ausrichtung, freiem Pool usw. Thread-Stapel sind immer zusammenhängende (im virtuellen Adressraum) Sequenzen vollständiger Seiten. In den meisten Betriebssystemen gibt es einen Seitenzuordner, der Stapeln, Heap- und Speicherzuordnungsdateien zugrunde liegt.
Ben Voigt
2
@Brian: Wer hat gesagt, dass irgendein "Stack" von brk()und manipuliert wird sbrk()? Die Stapel werden vom Seitenzuweiser auf einer viel niedrigeren Ebene verwaltet.
Ben Voigt

Antworten:

231

In dem von Ihnen geposteten Diagramm ist die "Unterbrechung" - die von brkund manipulierte Adresse sbrk- die gepunktete Linie oben auf dem Heap.

vereinfachtes Bild des virtuellen Speicherlayouts

Die Dokumentation, die Sie gelesen haben, beschreibt dies als das Ende des "Datensegments", da in herkömmlichen (Pre-Shared-Libraries, Pre- mmap) Unix das Datensegment mit dem Heap kontinuierlich war. Vor dem Programmstart lud der Kernel die Blöcke "Text" und "Daten" ab Adresse Null in den RAM (tatsächlich etwas über Adresse Null, sodass der NULL-Zeiger wirklich auf nichts zeigte) und setzte die Unterbrechungsadresse auf das Ende des Datensegments. Der erste Aufruf von mallocwürde dann verwendet sbrk, um die Aufteilung zu verschieben und den Heap zwischen dem oberen Rand des Datensegments und der neuen, höheren Unterbrechungsadresse zu erstellen , wie im Diagramm gezeigt, und die anschließende Verwendung von mallocwürde ihn verwenden, um den Heap größer zu machen wie nötig.

In der Zwischenzeit beginnt der Stapel oben im Speicher und wächst nach unten. Der Stack benötigt keine expliziten Systemaufrufe, um ihn zu vergrößern. Entweder beginnt es mit so viel RAM, wie ihm jemals zugewiesen werden kann (dies war der traditionelle Ansatz), oder es gibt einen Bereich reservierter Adressen unterhalb des Stapels, dem der Kernel automatisch RAM zuweist, wenn er einen Versuch bemerkt, dort zu schreiben (Dies ist der moderne Ansatz). In beiden Fällen kann sich am unteren Rand des Adressraums ein "Schutz" -Bereich befinden oder nicht, der für den Stapel verwendet werden kann. Wenn diese Region existiert (alle modernen Systeme tun dies), wird sie dauerhaft nicht zugeordnet. wenn auch nichtWenn der Stapel oder der Heap versucht, hineinzuwachsen, wird ein Segmentierungsfehler angezeigt. Traditionell machte der Kernel jedoch keinen Versuch, eine Grenze durchzusetzen. Der Stapel könnte in den Heap hineinwachsen, oder der Heap könnte in den Stapel hineinwachsen, und so oder so würden sie sich gegenseitig über die Daten kritzeln und das Programm würde abstürzen. Wenn Sie sehr viel Glück hatten, würde es sofort abstürzen.

Ich bin nicht sicher, woher die Nummer 512 GB in diesem Diagramm stammt. Dies impliziert einen virtuellen 64-Bit-Adressraum, der nicht mit der sehr einfachen Speicherzuordnung übereinstimmt, die Sie dort haben. Ein echter 64-Bit-Adressraum sieht eher so aus:

weniger vereinfachter Adressraum

              Legend:  t: text, d: data, b: BSS

Dies ist nicht remote skalierbar und sollte nicht so interpretiert werden, wie es ein bestimmtes Betriebssystem macht (nachdem ich es gezeichnet habe, stellte ich fest, dass Linux die ausführbare Datei tatsächlich viel näher an die Adresse Null bringt, als ich dachte, und die gemeinsam genutzten Bibliotheken bei überraschend hohen Adressen). Die schwarzen Bereiche dieses Diagramms sind nicht zugeordnet - jeder Zugriff verursacht einen sofortigen Segfault - und sie sind im Verhältnis zu den grauen Bereichen gigantisch . Die hellgrauen Bereiche sind das Programm und seine gemeinsam genutzten Bibliotheken (es können Dutzende von gemeinsam genutzten Bibliotheken vorhanden sein). Jeder hat eine unabhängigeText- und Datensegment (und "bss" -Segment, das ebenfalls globale Daten enthält, jedoch auf All-Bit-Null initialisiert wird, anstatt Speicherplatz in der ausführbaren Datei oder Bibliothek auf der Festplatte zu belegen). Der Heap ist nicht mehr unbedingt kontinuierlich mit dem Datensegment der ausführbaren Datei - ich habe es so gezeichnet, aber es sieht so aus, als würde Linux das zumindest nicht tun. Der Stapel ist nicht mehr an den oberen Rand des virtuellen Adressraums gebunden, und der Abstand zwischen dem Heap und dem Stapel ist so groß, dass Sie sich keine Sorgen mehr machen müssen, ihn zu überqueren.

Die Pause ist immer noch die Obergrenze des Haufens. Was ich jedoch nicht gezeigt habe, ist, dass es Dutzende von unabhängigen Speicherzuordnungen geben könnte, die irgendwo schwarze Zahlen schreiben, die mit mmapstatt gemacht wurden brk. (Das Betriebssystem wird versuchen, diese von dem brkBereich fernzuhalten , damit sie nicht kollidieren.)

zwol
quelle
7
+1 für eine detaillierte Erklärung. Wissen Sie, ob Sie sich mallocnoch darauf verlassen brkoder ob es verwendet wird mmap, um separate Speicherblöcke "zurückgeben" zu können?
Anders Abel
18
Dies hängt von der spezifischen Implementierung ab, aber IIUC verwenden viele aktuelle Bereiche mallocden brkBereich für kleine Zuweisungen und einzelne mmaps für große Zuweisungen (z. B.> 128 KB). Siehe zum Beispiel die Diskussion von MMAP_THRESHOLD in der Linux- malloc(3)Manpage.
zwol
1
In der Tat eine gute Erklärung. Aber wie Sie sagten, befindet sich Stack nicht mehr oben im virtuellen Adressraum. Gilt dies nur für den 64-Bit-Adressraum oder sogar für den 32-Bit-Adressraum? Und wenn sich der Stapel oben im Adressraum befindet, wo finden dann anonyme Speicherzuordnungen statt? Befindet es sich oben im virtuellen Adressraum kurz vor dem Stapel?
Nik
3
@Nikhil: Es ist kompliziert. Bei den meisten 32-Bit-Systemen befindet sich der Stapel ganz oben im Adressraum im Benutzermodus , der häufig nur die unteren 2 oder 3 G des gesamten Adressraums darstellt (der verbleibende Speicherplatz ist für den Kernel reserviert). Ich kann momentan nicht an einen denken, der es nicht tat, aber ich kenne sie nicht alle. Bei den meisten 64-Bit-CPUs können Sie nicht den gesamten 64-Bit-Speicherplatz nutzen. Die hohen 10 bis 16 Bits der Adresse müssen alle Null oder alle Eins sein. Der Stapel wird im Allgemeinen nahe der Oberseite der verwendbaren niedrigen Adressen platziert. Ich kann dir keine Regel geben für mmap; Es ist extrem betriebssystemabhängig.
zwol
3
@RiccardoBestetti Es verschwendet Adressraum , aber das ist harmlos - ein virtueller 64-Bit-Adressraum ist so groß, dass Sie immer noch 500 Jahre brauchen würden , wenn Sie jede Sekunde ein Gigabyte davon durchbrennen würden. [1] Die meisten Prozessoren erlauben nicht einmal die Verwendung von mehr als 2 ^ 48 bis 2 ^ 53 Bit virtueller Adresse (die einzige mir bekannte Ausnahme ist POWER4 im Hash-Seitentabellenmodus). Es wird kein physischer RAM verschwendet. Die nicht verwendeten Adressen werden nicht dem RAM zugewiesen.
zwol
26

Minimal lauffähiges Beispiel

Was macht der Systemaufruf brk ()?

Bittet den Kernel, Sie lesen und in einen zusammenhängenden Speicherblock namens Heap schreiben zu lassen.

Wenn Sie nicht fragen, kann dies zu einem Segfault führen.

Ohne brk:

#define _GNU_SOURCE
#include <unistd.h>

int main(void) {
    /* Get the first address beyond the end of the heap. */
    void *b = sbrk(0);
    int *p = (int *)b;
    /* May segfault because it is outside of the heap. */
    *p = 1;
    return 0;
}

Mit brk:

#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>

int main(void) {
    void *b = sbrk(0);
    int *p = (int *)b;

    /* Move it 2 ints forward */
    brk(p + 2);

    /* Use the ints. */
    *p = 1;
    *(p + 1) = 2;
    assert(*p == 1);
    assert(*(p + 1) == 2);

    /* Deallocate back. */
    brk(b);

    return 0;
}

GitHub stromaufwärts .

Das Obige trifft möglicherweise nicht auf eine neue Seite und auch ohne das nicht auf Segfault. brkHier ist also eine aggressivere Version, die 16 MB zuweist und sehr wahrscheinlich ohne das brkfolgende Fehler auftritt :

#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>

int main(void) {
    void *b;
    char *p, *end;

    b = sbrk(0);
    p = (char *)b;
    end = p + 0x1000000;
    brk(end);
    while (p < end) {
        *(p++) = 1;
    }
    brk(b);
    return 0;
}

Getestet unter Ubuntu 18.04.

Visualisierung des virtuellen Adressraums

Vorher brk:

+------+ <-- Heap Start == Heap End

Nachher brk(p + 2):

+------+ <-- Heap Start + 2 * sizof(int) == Heap End 
|      |
| You can now write your ints
| in this memory area.
|      |
+------+ <-- Heap Start

Nachher brk(b):

+------+ <-- Heap Start == Heap End

Um Adressräume besser zu verstehen, sollten Sie sich mit Paging vertraut machen: Wie funktioniert x86-Paging? .

Warum brauchen wir beide brkund sbrk?

brkkönnte natürlich mit sbrk+ Offset-Berechnungen implementiert werden, beide existieren nur der Einfachheit halber.

Im Backend verfügt der Linux-Kernel v5.0 über einen einzigen Systemaufruf brk, mit dem beide implementiert werden: https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64. tbl # L23

12  common  brk         __x64_sys_brk

Ist brkPOSIX?

brkFrüher war es POSIX, aber es wurde in POSIX 2001 entfernt, sodass _GNU_SOURCEauf den Glibc-Wrapper zugegriffen werden muss.

Das Entfernen ist wahrscheinlich auf die Einführung zurückzuführen mmap, bei der es sich um eine Obermenge handelt, mit der mehrere Bereiche und mehr Zuordnungsoptionen zugewiesen werden können.

Ich denke, es gibt keinen gültigen Fall, in dem Sie brkanstelle von mallocoder mmapheutzutage verwenden sollten.

brk vs. malloc

brkist eine alte Möglichkeit der Implementierung malloc.

mmapist der neuere, streng leistungsfähigere Mechanismus, den wahrscheinlich alle POSIX-Systeme derzeit zur Implementierung verwenden malloc. Hier ist ein Beispiel für eine minimale lauffähige mmapSpeicherzuweisung .

Kann ich mischen brkund malloc?

Wenn Ihr mallocmit implementiert ist brk, habe ich keine Ahnung, wie das möglicherweise nicht explodieren kann, da brknur ein einziger Speicherbereich verwaltet wird.

Ich konnte jedoch nichts darüber in den glibc-Dokumenten finden, z.

Die Dinge werden wahrscheinlich nur dort funktionieren, da sie mmapwahrscheinlich für verwendet werden malloc.

Siehe auch:

Mehr Info

Intern entscheidet der Kernel, ob der Prozess so viel Speicher haben kann, und reserviert Speicherseiten für diese Verwendung.

Dies erklärt, wie der Stapel mit dem Heap verglichen wird: Welche Funktion haben die Push / Pop-Anweisungen, die für Register in der x86-Assembly verwendet werden?

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle
4
Da dies pein Zeiger auf Typ ist int, sollte dies nicht gewesen sein brk(p + 2);?
Johan Boulé
Kleine Anmerkung: Der Ausdruck in der for-Schleife der aggressiven Version sollte wahrscheinlich sein*(p + i) = 1;
lima.sierra
Übrigens, warum müssen wir ein verwenden, brk(p + 2)anstatt es einfach um zu erhöhen sbrk(2)? Ist brk wirklich notwendig?
Yi Lin Liu
1
@YiLinLiu Ich denke, es sind nur zwei sehr ähnliche C-Frontends für ein einzelnes Kernel-Backend ( brksyscall). brkEs ist etwas bequemer, zuvor zugewiesene Stapel wiederherzustellen.
Ciro Santilli 法轮功 冠状 病 六四 事件 21
1
@CiroSantilli 中心 改造 6 996ICU 六四 事件 Angesichts der Größe von int von 4 Bytes und der Größe von int * als 4 Bytes (auf einem 32-Bit-Computer) habe ich mich gefragt, ob es nicht nur um 4 Bytes erhöht werden soll (statt 8 - (2 * sizeof int)). Sollte es nicht auf den nächsten verfügbaren Heap-Speicher verweisen - der 4 Bytes entfernt ist (nicht 8). Korrigieren Sie mich, wenn mir hier etwas fehlt.
Saket Sharad
10

Sie können brkund sich sbrkselbst verwenden, um den "Malloc Overhead" zu vermeiden, über den sich alle immer beschweren. Sie können diese Methode jedoch nicht einfach in Verbindung mit verwenden, mallocsodass sie nur dann angemessen ist, wenn Sie freenichts benötigen . Weil du nicht kannst. Außerdem sollten Sie Bibliotheksaufrufe vermeiden, die möglicherweise mallocintern verwendet werden. Dh. strlenist wahrscheinlich sicher, aber fopenwahrscheinlich nicht.

Rufen sbrkSie an, wie Sie anrufen würden malloc. Es gibt einen Zeiger auf die aktuelle Unterbrechung zurück und erhöht die Unterbrechung um diesen Betrag.

void *myallocate(int n){
    return sbrk(n);
}

Während Sie einzelne Zuteilungen , die nicht frei ist (weil es keine ist malloc-Overhead , erinnern), Sie können kostenlos den gesamten Raum durch den Aufruf brkdurch den ersten Aufruf zurückgegeben mit dem Wert sbrk, also die brk Zurückspulen .

void *memorypool;
void initmemorypool(void){
    memorypool = sbrk(0);
}
void resetmemorypool(void){
    brk(memorypool);
}

Sie können diese Regionen sogar stapeln und die neueste Region verwerfen, indem Sie die Unterbrechung zum Start der Region zurückspulen.


Eine Sache noch ...

sbrkist auch nützlich im Code Golf, weil es 2 Zeichen kürzer als ist malloc.

luser droog
quelle
7
-1, weil: malloc/ dem Betriebssystem mit freeSicherheit Speicher zurückgeben kann (und dies auch tut). Sie tun dies möglicherweise nicht immer dann, wenn Sie dies wünschen, aber dies hängt davon ab, ob die Heuristiken nicht perfekt auf Ihren Anwendungsfall abgestimmt sind. Noch wichtiger ist, dass es unsicher ist,sbrk in einem Programm, das jemals aufgerufen wird, mit einem Argument ungleich Null aufzurufen malloc- und fast alle Funktionen der C-Bibliothek dürfen mallocintern aufgerufen werden . Die einzigen, die dies definitiv nicht tun werden, sind die asynchronen, signal-sicheren Funktionen.
zwol
Und mit "es ist unsicher" meine ich "Ihr Programm wird abstürzen".
zwol
Ich habe bearbeitet, um das zurückkehrende Speicherangebot zu entfernen , und die Gefahr erwähnt, dass Bibliotheksfunktionen intern verwendet werden malloc.
Luser Droog
1
Wenn Sie eine ausgefallene Speicherzuweisung vornehmen möchten, stützen Sie diese entweder auf malloc oder auf mmap. Berühren Sie nicht brk und sbrk, es sind Relikte aus der Vergangenheit, die mehr schaden als nützen (sogar die Manpages sagen Ihnen, dass Sie sich von ihnen
fernhalten sollen
3
Das ist dumm. Wenn Sie den Malloc-Overhead für viele kleine Zuordnungen vermeiden möchten, nehmen Sie eine große Zuweisung vor (mit malloc oder mmap, nicht sbrk) und verteilen Sie sie selbst. Wenn Sie die Knoten Ihres Binärbaums in einem Array behalten, können Sie 8b- oder 16b-Indizes anstelle von 64b-Zeigern verwenden. Dies funktioniert hervorragend, wenn Sie keinen Knoten löschen müssen, bis Sie bereit sind, alle Knoten zu löschen . (zB ein sortiertes Wörterbuch im laufenden Betrieb erstellen.) Die Verwendung sbrkhierfür ist nur für Code-Golf nützlich, da die manuelle Verwendung mmap(MAP_ANONYMOUS)in jeder Hinsicht besser ist, mit Ausnahme der Quellcode-Größe.
Peter Cordes
3

Es gibt eine speziell festgelegte anonyme Zuordnung des privaten Speichers (traditionell direkt hinter den Daten / BSS, aber modernes Linux passt den Speicherort tatsächlich mit ASLR an). Im Prinzip ist es nicht besser als jedes andere Mapping, mit dem Sie erstellen könnten mmap, aber Linux verfügt über einige Optimierungen, die es ermöglichen, das Ende dieses Mappings (mithilfe des brkSystemaufrufs) nach oben zu erweitern, wobei die Sperrkosten im Vergleich zu dem, was anfallen würde mmapoder fallen mremapwürde, reduziert werden . Dies macht es für mallocImplementierungen attraktiv , wenn sie den Hauptheap implementieren.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
Sie soll möglichst am Ende dieser Abbildung nach oben erweitern, nicht wahr?
zwol
Ja, behoben. Das tut mir leid!
R .. GitHub STOP HELPING ICE
0

Ich kann Ihre zweite Frage beantworten. Malloc schlägt fehl und gibt einen Nullzeiger zurück. Aus diesem Grund suchen Sie beim dynamischen Zuweisen von Speicher immer nach einem Nullzeiger.

Brian Gordon
quelle
Was nützt dann brk und sbrk?
Nik
3
@NikhilRathod: malloc()wird brk()und / oder sbrk()unter der Haube verwendet - und Sie können auch, wenn Sie Ihre eigene angepasste Version von implementieren möchten malloc().
Daniel Pryden
@ Daniel Pryden: Wie können brk und sbrk auf Heap arbeiten, wenn es sich zwischen Stapel und Datensegment befindet, wie in der obigen Abbildung gezeigt? Damit dies funktioniert, sollte der Haufen am Ende sein. Habe ich recht?
Nik
2
@ Brian: Daniel sagte , dass das Betriebssystem die Stapel verwaltet Segment , nicht die Stack - Zeiger ... sehr verschiedene Dinge. Der Punkt ist, dass es keinen sbrk / brk-Systemaufruf für das Stapelsegment gibt - Linux weist Seiten automatisch zu, wenn versucht wird, an das Ende des Stapelsegments zu schreiben.
Jim Balter
1
Und Brian, du hast nur die Hälfte der Frage beantwortet. Die andere Hälfte passiert, wenn Sie versuchen, auf den Stapel zu schieben, wenn kein Speicherplatz verfügbar ist. Sie erhalten einen Segmentierungsfehler.
Jim Balter
0

Der Heap wird zuletzt im Datensegment des Programms platziert. brk()wird verwendet, um die Größe des Heaps zu ändern (zu erweitern). Wenn der Heap nicht mehr wachsen kann, schlägt jeder mallocAufruf fehl.

Anders Abel
quelle
Sie sagen also, dass alle Diagramme im Internet, wie das in meiner Frage, falsch sind. Wenn möglich, können Sie mich bitte auf ein korrektes Diagramm hinweisen.
Nik
2
@Nikkhil Denken Sie daran, dass der obere Rand dieses Diagramms das Ende des Speichers ist. Die Oberseite des Stapels bewegt sich im Diagramm nach unten, wenn der Stapel wächst. Die Oberseite des Heaps bewegt sich im Diagramm nach oben, wenn es erweitert wird.
Brian Gordon
0

Das Datensegment ist der Teil des Speichers, der alle Ihre statischen Daten enthält, die beim Start aus der ausführbaren Datei eingelesen und normalerweise mit Nullen gefüllt werden.

monchalve
quelle
Es enthält auch nicht initialisierte statische Daten (die in der ausführbaren Datei nicht vorhanden sind), bei denen es sich möglicherweise um Müll handelt.
Luser Droog
Nicht initialisierte statische Daten ( .bss) werden vom Betriebssystem vor dem Programmstart auf All-Bit-Null initialisiert. Dies wird tatsächlich durch den C-Standard garantiert. Einige eingebettete Systeme könnten sich nicht darum kümmern (ich habe noch nie eines gesehen, aber ich arbeite nicht so
viel
@zwol: Linux hat eine Option zur Kompilierungszeit, um die von zurückgegebenen Seiten nicht auf Null zu setzen mmap, aber ich würde davon ausgehen, dass die Seite .bssimmer noch auf Null gesetzt wird. Der BSS-Raum ist wahrscheinlich der kompakteste Weg, um die Tatsache auszudrücken, dass ein Programm einige Null-Arrays wünscht.
Peter Cordes
1
@PeterCordes Der C-Standard besagt, dass globale Variablen, die ohne Initialisierer deklariert wurden, so behandelt werden, als ob sie auf Null initialisiert wären. Eine AC-Implementierung, die solche Variablen einfügt .bssund nicht Null ist, .bsswäre daher nicht konform. Aber nichts zwingt eine C-Implementierung dazu, .bssüberhaupt etwas zu verwenden oder sogar so etwas zu haben.
zwol
@PeterCordes Außerdem kann die Linie zwischen der "C-Implementierung" und dem Programm sehr unscharf sein, z. B. gibt es normalerweise einen kleinen Codeabschnitt aus der Implementierung, der statisch mit jeder zuvor ausgeführten ausführbaren Datei verknüpft ist main. Dieser Code könnte den .bssBereich auf Null setzen, anstatt ihn vom Kernel ausführen zu lassen, und das wäre immer noch konform.
zwol
0

malloc verwendet den Systemaufruf brk, um Speicher zuzuweisen.

einschließen

int main(void){

char *a = malloc(10); 
return 0;
}

Führen Sie dieses einfache Programm mit strace aus, es wird brk system aufrufen.

Skanzariya
quelle