[Anmerkungen lesen] Linux-Kernel-Design und Management von Implementierungsprozessen

1. Prozess

Ein Prozess ist ein Programm, das ausgeführt wird (der Zielcode wird auf einem Speichermedium gespeichert).

ps: Der Prozess ist nicht auf einen Abschnitt mit ausführbarem Code beschränkt (auch als Codeabschnitt, Textabschnitt bezeichnet). Normalerweise enthält der Prozess auch andere Ressourcen, z. B. geöffnete Dateien, angehaltene Semaphoren.

Ein Ausführungsthread oder kurz Thread ist ein Objekt, das in einem Prozess aktiv ist. Jeder Thread verfügt über einen unabhängigen Programmzähler, einen Prozessstapel und einen Satz von Prozessregistern.

Das vom Kernel geplante Objekt ist ein Thread, kein Prozess.

ps: Die Thread-Implementierung des Linux-Systems ist sehr speziell: Sie unterscheidet nicht speziell zwischen Threads und Prozessen. Das heißt, ein Thread ist nichts anderes als ein spezieller Prozess.

2. Prozessdeskriptor und Aufgabenstruktur

Der Kernel speichert die Liste der Prozesse in einer doppelt kreisförmig verknüpften Liste, die als Aufgabenliste bezeichnet wird.
Jedes Element in der verknüpften Liste ist vom Typ task_struct und wird als Prozessdeskriptorstruktur (Prozessdeskriptorstruktur) bezeichnet, die in <linux / sched.h> definiert ist.
Der Prozessdeskriptor enthält alle Informationen zu einem bestimmten Prozess.

Die im Prozessdeskriptor enthaltenen Daten können ein Programm, das ausgeführt wird, vollständig beschreiben: die geöffnete Datei, den Adressraum des Prozesses, ausstehende Signale, den Status des Prozesses usw.
Fügen Sie hier eine Bildbeschreibung ein

2.1 Prozessbeschreibungen zuweisen

Fügen Sie hier eine Bildbeschreibung ein

2.2 Speicherung von Prozessdeskriptoren-PID

Der Kernel identifiziert jeden Prozess durch einen eindeutigen Prozessidentifikationswert oder eine eindeutige PID.
PID ist eine Zahl, ausgedrückt als impliziter Typ von pid_t, der eigentlich ein int-Typ ist.
PID ist tatsächlich die maximale Anzahl gleichzeitiger Prozesse, die im System zulässig sind.
Der maximale Standardwert von PID ist 32768 (begrenzt durch den in <linux / thread.h> definierten Maximalwert von PID), der über / proc / sys / kernel / pid_max angezeigt werden kann.

2.3 Prozessstatus

Das Statusfeld im Prozessdeskriptor beschreibt den aktuellen Status des Prozesses. Die folgenden fünf Typen :

  1. TASK_RUNNING (Run R) - Der Prozess ist ausführbar: Er wird entweder ausgeführt oder wartet darauf, in der Ausführungswarteschlange ausgeführt zu werden.
  2. TASK_INTERRUPTIBLE (unterbrechbares S) - Der Prozess schläft (blockiert) und wartet darauf, dass bestimmte Bedingungen erfüllt sind.
  3. TASK_UNINTERRUPTIBLE (unterbrechungsfrei D) - Dieser Zustand ist der gleiche wie der unterbrechbare Zustand, außer dass er auch dann nicht geweckt wird oder betriebsbereit ist, selbst wenn ein Signal empfangen wird.
  4. __TASK_TRACED (Z) -Prozesse, die von anderen Prozessen verfolgt werden, z. B. dem Tracer-Prozessprozess über ptrace.
  5. __TASK_STOPPED (Stopp T) - Der Prozess wird nicht mehr ausgeführt. Der Prozess wird weder in Betrieb genommen noch kann er in Betrieb genommen werden. Normalerweise tritt dieser Zustand auf, wenn SIGSTOP-, SIGTSTP-, SIGTTIN-, SIGTTOU- und andere Signale empfangen werden. Außerdem bewirkt jedes während des Debuggens empfangene Signal, dass der Prozess in diesen Zustand wechselt.

Fügen Sie hier eine Bildbeschreibung ein

2.4 Legen Sie den aktuellen Prozessstatus fest - set_task_state

set_task_state(task,state);  /*等价于*/
task->state = state;

ps:

set_current_state(state); /* 等价于,参考<linux/sched.h> */
set_task_state(current,state);

2.5 Prozesskontext

Ausführbarer Programmcode ist ein wichtiger Teil des Prozesses.
Diese Codes werden zur Ausführung aus einer ausführbaren Datei in den Adressraum des Prozesses geladen.
Allgemeine Programme werden im User Space ausgeführt. Wenn ein Programmaufruf einen Systemaufruf ausführt oder eine Ausnahme auslöst, fällt er in den Kernelraum .
An dieser Stelle nennen wir den Kernel "im Auftrag des Prozesses ausgeführt" und im Kontext des Prozesses .

2.6 Prozessstammbaum - Alle Prozesse sind Nachkommen des Init-Prozesses mit PID 1

Die Beziehung zwischen Prozessen wird im Prozessdeskriptor gespeichert.
Jedes task_struct enthält einen Zeiger auf seinen übergeordneten Prozess tast_struct, j heißt parent, und enthält auch eine Liste von untergeordneten Prozessen, die untergeordnet genannt werden.

F: Wie erhalte ich den Prozessdeskriptor des übergeordneten Prozesses?
A:struct task_struct *my_parent = current->parent;

F: Wie greife ich auf den untergeordneten Prozess zu?
A:

struct task_struct *task;
struct list_head *list;

list_for_each(list,&current->children)
{
	task = list_entry(list,struct task_struct,sibling);
}

Der Prozessdeskriptor des Init-Prozesses wird statisch als init_task zugewiesen.
Wie zum Beispiel:

struct task_struct *task;
for(task = current;task != &init_task; task = task->parent);
/*task 现在指向init*/

ps: Das
Makro for_each_process (task) bietet die Möglichkeit, nacheinander auf die gesamte Taskwarteschlange zuzugreifen (bidirektionale zirkuläre verknüpfte Liste), aber die Kosten für das Durchlaufen aller Prozesse durch Wiederholung in einem System mit einer großen Anzahl von Prozessen sind sehr hoch.
Wenn es keinen guten Grund gibt (oder keinen anderen Weg gibt), tun Sie es daher nicht.
zB:

struct task_struct *task;
for_each_process(task){
	/*打印出每一个任务(进程)的名称和PID*/
	printk("%s[%d]\n",task->comm,task->pid);
}

3. Verarbeiten Sie die Funktionen der Creation-Fork- und Exec-Familie

fork () erstellt einen untergeordneten Prozess, indem der aktuelle Prozess kopiert wird.
Die Funktionen der exec-Familie kopieren und lesen die ausführbare Datei und laden sie in den Adressraum, um sie auszuführen.

3.1 Copy-on-Write-einer der Gründe, warum Linux Prozesse schnell ausführen kann

Die COW-Technologie bezieht sich nur dann auf die Ressourcenkopie, wenn sie geschrieben werden muss. Zuvor wurde sie nur schreibgeschützt freigegeben (dh die übergeordneten und untergeordneten Prozesse verwenden dieselbe Kopie).

3.2 Gabel ()

Linux implementiert fork () über den Systemaufruf clone ().
Das Folgende ist der allgemeine Anrufverlauf:

fork()->clone()->do_fork()->copy_process()

ps: do_fork () ist in der Datei kernel / fork.c definiert. (
Kann in verschiedenen Pfaden mit unterschiedlichen Kernelversionen vorhanden sein.) Die Funktion do_fork ruft die Funktion copy_process auf und ermöglicht der Stadt dann die Ausführung.
Der Workflow der Copy_process-Funktion lautet wie folgt:

  1. Rufen Sie dup_task_struct () auf, um einen Kernel-Stack, eine thread_info-Struktur und task_struct für den neuen Prozess zu erstellen. Diese Werte entsprechen den aktuellen Werten, die in die Stadt eingegeben wurden. Zu diesem Zeitpunkt sind die Deskriptoren des untergeordneten Prozesses und des übergeordneten Prozesses genau gleich.
  2. Stellen Sie sicher, dass nach dem neuen Erstellen dieses Unterprozesses die Anzahl der Prozesse, die dem aktuellen Benutzer gehören, die ihm zugewiesene Ressourcengrenze nicht überschreitet.
  3. Der untergeordnete Prozess soll sich vom übergeordneten Prozess unterscheiden. Viele Mitglieder im Prozessdeskriptor müssen gelöscht oder auf Anfangswerte gesetzt werden. Diejenigen, die keine vererbten Mitglieder des Eintragsdeskriptors sind, sind hauptsächlich statistische Informationen. Die meisten Daten in task_struct bleiben unverändert.
  4. Der Status des untergeordneten Prozesses wird auf TASK_UNINTERRUPTIBLE gesetzt, um sicherzustellen, dass er nicht in Betrieb genommen wird.
  5. copy_process () ruft copy_flags () auf, um das Flags-Mitglied von task_struct zu aktualisieren. Das PF_SUPERPRIV-Flag, das angibt, ob der Prozess über Superuser-Berechtigungen verfügt, wird auf 0 gelöscht. Das PF_FORKNOEXEC-Flag, das angibt, dass der Prozess noch nicht exec () aufgerufen hat, ist gesetzt.
  6. Rufen Sie alloc_pid () auf, um dem neuen Prozess eine gültige PID zuzuweisen.
  7. Gemäß den an clone () übergebenen Parameterflags kopiert oder teilt copy_process () geöffnete Dateien, Dateisysteminformationen, Signalverarbeitungsfunktionen, Prozessadressraum und Namespace. Unter normalen Umständen werden diese Ressourcen von allen Threads eines bestimmten Prozesses gemeinsam genutzt. Andernfalls unterscheiden sich diese Ressourcen für jeden Prozess und werden hier kopiert (COW).
  8. Schließlich erledigt copy_process () die Schwanzreinigung und gibt einen Zeiger auf den untergeordneten Prozess zurück.

Kehren Sie dann zur Funktion do_fork () zurück. Wenn die Funktion copy_process () erfolgreich zurückgegeben wird, wird der neu erstellte untergeordnete Prozess aktiviert und in Betrieb genommen.
Der Kernel wählt absichtlich zuerst den untergeordneten Prozess aus (was nicht immer der Fall ist). Im Allgemeinen ruft der untergeordnete Prozess sofort die Funktion exec family auf, wodurch der zusätzliche Kopieraufwand beim Schreiben vermieden werden kann. Wenn der übergeordnete Prozess zuerst ausgeführt wird, beginnt er möglicherweise mit dem Schreiben in den Adressraum.

3.3 vfork ()

Der Systemaufruf vfork () hat dieselbe Funktion wie fork (), außer dass der Seitentabelleneintrag des übergeordneten Prozesses nicht kopiert wird.
Der untergeordnete Prozess wird als separater Thread des übergeordneten Prozesses in seinem Adressraum ausgeführt. Der übergeordnete Prozess wird blockiert, bis der untergeordnete Prozess exec () beendet oder ausführt.
Untergeordnete Prozesse können nicht in den Adressraum schreiben.

ps: Idealerweise sollte das System weder vfork () aufrufen, noch muss der Kernel es implementieren.

4. Thread-Implementierung unter Linux

Der Threading-Mechanismus stellt eine Reihe von Threads bereit, die in einem gemeinsam genutzten Speicheradressraum innerhalb desselben Programms ausgeführt werden.
Linux behandelt alle Threads als Prozesse.
Ein Thread wird einfach als ein Prozess betrachtet, der bestimmte Ressourcen mit anderen Prozessen teilt . (Jeder Thread hat seine eigene task_struct. Im Kernel sieht es also wie ein gewöhnlicher Prozess aus, aber der Thread und einige andere Prozesse teilen sich bestimmte Ressourcen, z. B. den Adressraum.)

4.1 Erstellen Sie einen Thread

Das Erstellen von Threads ähnelt dem Erstellen gewöhnlicher Prozesse, außer dass Sie beim Aufrufen von clone () einige Parameterflags übergeben müssen, um die Ressourcen anzugeben, die gemeinsam genutzt werden müssen:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0);  /*结果和调用fork()差不多,只是父子俩共享地址空间,文件系统资源,文件描述符和信号处理程序*/

Das an die Klonfunktion übergebene Parameterflag bestimmt das Verhalten des neu erstellten Prozesses und den Typ der gemeinsam genutzten Ressourcen zwischen dem übergeordneten und dem untergeordneten Prozess.
Wie in der folgenden Tabelle gezeigt: (<linux / sched.h>)

Parameter-Flag Bedeutung
CLONE_FILES Übergeordnete und untergeordnete Prozesse teilen offene Dateien
CLONE_FS Eltern-Kind-Prozess freigegebene Dateisysteminformationen
CLONE_IDLETASK Setzen Sie die PID auf 0 (wird nur von Leerlaufprozessen verwendet).
CLONE_NEWNS Erstellen Sie einen neuen Namespace für den untergeordneten Prozess
CLONE_PARENT Gibt an, dass der untergeordnete Prozess und der übergeordnete Prozess denselben übergeordneten Prozess haben
CLONE_PTRACE Fahren Sie mit dem Debuggen des untergeordneten Prozesses fort
CLONE_SETTID Schreiben Sie die TID zurück in den Benutzerbereich
CLONE_SETTLS Erstellen Sie ein neues TLS für den untergeordneten Prozess
CLONE_SIGHAND Vater- und Sohnprozesse teilen sich Signalverarbeitungsfunktionen und blockierte Signale
CLONE_SYSVSEM Vater- und Sohnprozesse teilen die System V SEM_UNDO-Semantik
CLONE_THREAD Die übergeordneten und untergeordneten Prozesse werden in dieselbe Thread-Gruppe eingeordnet
CLONE_VFORK Vfork () wird aufgerufen, sodass der übergeordnete Prozess zum Schlafen bereit ist und darauf wartet, dass der untergeordnete Prozess ihn aktiviert
CLONE_UNTRACED Verhindern Sie, dass der Verfolgungsprozess CLONE_PTRACE im untergeordneten Prozess erzwingt
CLONE_STOP Starten Sie den Prozess im Status TASK_STOPPED
CLONE_SETTLS Erstellen Sie einen neuen TLS (Thread-Local Storage) für den untergeordneten Prozess
CLONE_CHILD_CLEARTID Löschen Sie die TID des untergeordneten Prozesses
CLONE_CHILD_SETTID Legen Sie die TID des untergeordneten Prozesses fest
CLONE_PARENT_SETTID Legen Sie die TID des übergeordneten Prozesses fest
CLONE_VM Gemeinsamer Adressraum des Eltern-Kind-Prozesses

ps:
Das Konzept des Leerlaufprozesses:
Sagen Sie einfach, Leerlauf ist ein Prozess und seine PID-Nummer ist 0. Sein Vorgänger war der erste vom System erstellte Prozess und der einzige Prozess, der nicht von fork () erzeugt wurde. Im smp-System hat jede Prozessoreinheit eine unabhängige Ausführungswarteschlange, und jede Ausführungswarteschlange hat einen Leerlaufprozess, dh so viele Prozessoreinheiten, wie Leerlaufprozesse vorhanden sind. Die Leerlaufzeit des Systems bezieht sich tatsächlich auf die "Laufzeit" des Leerlaufprozesses. Der Leerlaufprozess pid == o ist init_task.

4.2 Kernel-Threads - Standardprozesse, die unabhängig im Kernelraum ausgeführt werden

Der Unterschied zwischen Kernel-Threads und normalen Prozessen besteht darin, dass Kernel-Threads keinen unabhängigen Adressraum haben (tatsächlich ist der mm-Zeiger auf den Adressraum auf NULL gesetzt).
Sie werden nur im Kernelbereich ausgeführt und wechseln niemals in den Benutzerbereich.
Der Kernel-Prozess und der gewöhnliche Prozess bewegen sich weit weg und können aufgerufen und verhindert werden.

5. Prozessbeendigung - do_exit ()

do_exit () wird vom Systemaufruf exit () aufgerufen, der in kernel / exit.c definiert ist, und führt Folgendes aus:

  1. Setzen Sie das Flag-Mitglied in task_struct (im Buch als tast_struct geschrieben) auf PF_EXITING.
  2. Rufen Sie del_timer_sync () auf, um einen Core-Timer zu löschen. Basierend auf den zurückgegebenen Ergebnissen wird sichergestellt, dass keine Timer in die Warteschlange gestellt werden und keine Timer-Handler ausgeführt werden.
  3. Wenn die BSD-Prozessabrechnungsfunktion aktiviert ist, ruft do_exit () acct_update_integrals () auf, um Abrechnungsinformationen auszugeben.
  4. Rufen Sie dann die Funktion exit_mm () auf, um das vom Prozess belegte mm_struct freizugeben. Wenn kein anderer Prozess sie verwendet (dh dieser Adressraum wird nicht gemeinsam genutzt), geben Sie sie vollständig frei.
  5. Rufen Sie als nächstes die Funktion sem__exit () auf. Wenn sich der Prozess für das IPC-Signal in der Warteschlange befindet, verlässt er die Warteschlange.
  6. Rufen Sie exit_files () und exit_fs () auf, um die Referenzanzahl der Dateideskriptoren bzw. Dateisystemdaten zu verringern. Wenn der Wert einer der Referenzzählungen auf Null fällt, bedeutet dies, dass kein Prozess die entsprechenden Ressourcen verwendet und zu diesem Zeitpunkt freigegeben werden kann.
  7. Legen Sie dann den im exit_code-Member von task_struct gespeicherten Task-Exit-Code als den von exit () bereitgestellten Exit-Code fest oder führen Sie alle anderen vom Kernel-Mechanismus angegebenen Exit-Aktionen aus. Der Exit-Code wird hier gespeichert, damit der übergeordnete Prozess ihn jederzeit abrufen kann.
  8. Rufen exit_notify () sendet ein Signal an den Eltern - Prozess, der untergeordnete Prozess zum Wieder finden Adoptiwater, Adoptiwater zu anderen Threads in der Thread - Gruppe oder dem Init - Prozeß, und der Prozessstatus (gespeichert in exit_state task_struct Struktur) auf EXIT_ZOMBIE .
  9. do_exit () ruft Schedule () auf, um zum neuen Prozess zu wechseln. Da der Prozess im Status EXIT_ZOMBIE nicht mehr geplant wird, ist dies der letzte vom Prozess ausgeführte Code, und do_exit () wird nie zurückgegeben.

ps: Wenn der Prozess der einzige Benutzer dieser Ressourcen ist, kann der Prozess nicht ausgeführt werden (tatsächlich gibt es keinen Adressraum, in dem er ausgeführt werden kann) und befindet sich im Exit-Status EXIT_ZOMBIE. Der gesamte Speicher, den es belegt, ist der Kernel-Stack, die Struktur thread_info und die Struktur task_struct.
Der einzige Zweck des Prozesses zu diesem Zeitpunkt besteht darin, dem übergeordneten Prozess Informationen bereitzustellen. Nachdem der übergeordnete Prozess die Informationen abgerufen oder den Kernel darüber informiert hat, dass die Informationen irrelevant sind, wird der verbleibende Speicher des Prozesses freigegeben und an das System zurückgegeben.

5.1 Löschen Sie die Funktion der Prozessdeskriptor-Wartefamilie

Die am Ende des Prozesses erforderlichen Bereinigungsarbeiten und das Löschen des Prozessdeskriptors werden separat durchgeführt .
Die Funktionen der wait () -Familie werden durch den einzigen Systemaufruf wait4 () implementiert. Die Standardaktion besteht darin, den aufrufenden Prozess anzuhalten, bis einer der untergeordneten Prozesse beendet wird. Zu diesem Zeitpunkt gibt die Funktion die PID des untergeordneten Prozesses zurück.

Wenn es endlich notwendig ist, den Prozessdeskriptor freizugeben, wird release_task () aufgerufen. Der Prozess ist wie folgt:

  1. Es ruft __exit_signal () auf, das __unhash_process () aufruft, das wiederum separ_pid () aufruft, um den Prozess aus pidhash zu löschen, und löscht den Prozess auch aus der Aufgabenliste.
  2. __exit_signal () gibt alle verbleibenden Ressourcen frei, die im aktuellen Zombie-Prozess verwendet werden, und der Prozess wird schließlich gezählt und aufgezeichnet.
  3. Wenn dieser Prozess der letzte Prozess in der Thread-Gruppe ist und der Lead-Prozess tot ist, benachrichtigt release_task () den übergeordneten Prozess über den Zombie-Lead-Prozess.
  4. release_task () ruft put_task_struct () auf, um die Seite freizugeben, die vom Prozesskernstapel und der thread_info-Struktur belegt ist, und gibt den von task_struct belegten Plattencache frei.

5.2 Das Dilemma, das durch den Orphan-Prozess verursacht wird

Wenn der übergeordnete Prozess vor dem untergeordneten Prozess beendet wird , muss ein Mechanismus vorhanden sein, der sicherstellt, dass der untergeordnete Prozess einen neuen Vater findet. Andernfalls befinden sich diese verwaisten Prozesse beim Beenden immer in einem toten Zustand und verschwenden Ressourcen.
Dieser Mechanismus besteht darin, einen Thread als Vater in der aktuellen Thread-Gruppe für den untergeordneten Prozess zu finden. Wenn nicht, lassen Sie den Init-Prozess ihren übergeordneten Prozess sein .
Der ps: init-Prozess ruft routinemäßig wait () auf, um seine untergeordneten Prozesse zu überprüfen und alle zugehörigen Zombie-Prozesse zu entfernen.

Veröffentlicht 91 Originalarbeiten · erntete Lob 17 · Ansichten 50000 +

Ich denke du magst

Origin blog.csdn.net/qq_23327993/article/details/105065705
Empfohlen
Rangfolge