Queues Inhalt SMP, Synchronisations- und Lockingmechanismen


Erstellen und Beenden von Prozessen

Erzeugen eines Prozesses

Unter Linux gibt es drei verschiedene Arten von Prozessen:
- idle thread(s)
- kernel threads
- user task

Der erste idle thread wird beim Kompilieren erstellt, dann zur Boot-Zeit für jede CPU jeweils ein Thread. Alle idle threads haben die PID 0. Sie sind die einzigen, die das CLONE_PID Flag in clone() benutzen dürfen.
Kernel Threads zeichnen sich dadurch aus, daß sie keinen Speicher im Benutzer Adressbereich haben. Das Feld p->mm ist auf NULL gesetzt. Stattdessen arbeiten sie direkt auf dem vom Kernel allozierten Speicher. Kernel Threads haben alle I/O Privilegien, d.h. sie dürfen direkt auf die Kernelroutinen zugreifen und können vom Scheduler nicht unterbrochen werden. Sie werden mittels dem clone() Systemaufruf erzeugt.
Benutzer Tasks werden mit clone() oder fork() Systemaufrufen erzeugt.

fork() erzeugt einen Kindprozess, der sich lediglich in der PID und in der PPID vom Elternprozess unterscheidet, Filetable werden ebenfalls nicht vererbt. Dabei benutzt er die copy-on-write Technik, d.h. der Kindprozess benutzt den Speicher des Elternprozesses solange mit, bis eine Schreibaktivität erfolgt. Erst dann wird die entsprechende Speicherseite kopiert und dem Kindprozess exklusiv zur Verfügung gestellt.
clone() implementiert unter Linux sogenannte Lightweight Processes. Hiermit ist gemeint, daß das Kind sich den Usermode Adressbereich, sowie die Filetables mit seinem Elternprozess teilt. Die Absicht dieser Systemroutine ist Threads zu erzeugen, die dann gemeinsam auf einem Datenbereich arbeiten. Der clone() Befehl hat, unter anderem, einen Parameter fn, der eine Funktion darstellt, die bei Erzeugen des Threads ausgeführt wird, z.B. ein eigenes C-Programm. Ist diese Funktion beendet, so wird auch der Prozess beendet. Der Rückgabewert des Prozesses ist der Rückgabewert der Funktion.
Als dritte Möglichkeit einen Prozess zu erzeugen gibt es noch den POSIX konformen Aufruf vfork. Auch bei diesem teilen sich Eltern- und Kindprozess einen gemeinsamen Speicherraum. Um zu verhindern, daß sich die beiden in die Quere kommen, wird der Elternprozess solange suspendiert bis der Kindprozess beendet wurde oder er wiederum einen Kindprozess erzeugt hat.

Bei allen drei Möglichkeiten wird allerdings unter Linux eine Routine aufgerufen, die den neuen Prozess initialisiert: do_fork(). Wir wollen nun die wesentlichen Schritte betrachten, die in dieser Prozedur ausgeführt werden:
	retval = -EPERM;

	/* 
	 * CLONE_PID is only allowed for the initial SMP swapper
	 * calls
	 */
	if (clone_flags & CLONE_PID) {
		if (current->pid)
			goto fork_out;
	}
Zunächst wird überprüft, ob das CLONE_PID Flag gesetzt ist, falls dies der Fall ist und falls der Prozess kein idle Thread ist (PID == 0) wird als Fehlermeldung EPERM zurückgeliefert.
	retval = -ENOMEM;
	p = alloc_task_struct();
	if (!p)
		goto fork_out;
Danach wird versucht Speicher für den task_struct zu reservieren, falls dieses fehlschlägt wird ENOMEM als Fehlercode zurückgeliefert.
	*p = *current;
Hier werden nun mittels einem struct Zuweisung alle Daten des Elternprozess in den Descriptor des neuen Prozesses kopiert.
	retval = -EAGAIN;
	if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur)
		goto bad_fork_free;

	atomic_inc(&p->user->__count);
	atomic_inc(&p->user->processes);
Als nächstes wird getestet, ob das Limit der erlaubten Prozesse pro Benutzer überschritten wird, wenn dieser Prozess hinzukommt. Falls dies nicht der Fall ist, wird die Zahl der Benutzerprozesse mit einer atomaren Operation erhöht.
	if (nr_threads >= max_threads)
		goto bad_fork_cleanup_count;
Hier wird nun getestetet, ob die maximale Gesamtanzahl der erlaubten Prozesse im System mit dieser Anforderung überschritten wird.
	...
	p->pid = get_pid(clone_flags);
Nun wird eine neue process id aus den clone_flags generiert.
	p->run_list.next = NULL;
	p->run_list.prev = NULL;
Die run_list wird initialisiert.
	...
	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
	}
Falls es ein vfork Aufruf war, werden die notwendigen Initialisierungen vorgenommen, damit der Elternprozess wieder aufgeweckt wird.
	retval = p->pid;
	p->tgid = retval;
	INIT_LIST_HEAD(&p->thread_group);

	/* Need tasklist lock for parent etc handling! */
	write_lock_irq(&tasklist_lock);

	/* CLONE_PARENT and CLONE_THREAD re-use the old parent */
	p->p_opptr = current->p_opptr;
	p->p_pptr = current->p_pptr;
	if (!(clone_flags & (CLONE_PARENT | CLONE_THREAD))) {
		p->p_opptr = current;
		if (!(p->ptrace & PT_PTRACED))
			p->p_pptr = current;
	}

	if (clone_flags & CLONE_THREAD) {
		p->tgid = current->tgid;
		list_add(&p->thread_group, ¤t->thread_group);
	}

	SET_LINKS(p);
	hash_pid(p);
	nr_threads++;
	write_unlock_irq(&tasklist_lock);

	if (p->ptrace & PT_PTRACED)
		send_sig(SIGSTOP, p, 1);

	wake_up_process(p);		/* do this last */
	++total_forks;
	if (clone_flags & CLONE_VFORK)
		wait_for_completion(&vfork);
Zu guter Letzt wird hier als Rückgabewert die neue process id gesetzt und die Verwandschaftsverhältnisse werden klargestellt. Ist dies geschehen wird der Prozess mit SET_LINKS in die Prozessliste eingefügt, in die Hashtable eingetragen und zum Schluß aufgeweckt.

Beenden eines Prozesses

Zum Beenden eines Prozesses gibt des den Systemcall sys_exit, dieser ruft allerdings systemspezifisch do_exit in exit.c auf. Diese Funktion führt folgende Schritte aus:
  • Es wird ein Global Kernel Lock erzeugt
  • Der state wird auf TASK_ZOMBIE gesetzt
  • Die Kinder, falls vorhanden, werden mittels current->pdeath benachrichtigt.
  • Der Elternprozess wird mit current->exit_signal benachrichtigt, damit er die Resourcen entgültig freigeben kann.
  • Die Resourcen werden freigegeben (Speicher, Dateien...)
  • Und zum Schluß wird schedule() aufgerufen, damit ein neuer Prozess starten kann

Queues Inhalt SMP, Synchronisations- und Lockingmechanismen