View Single Post
Old 11-01-2005, 12:32   #14
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Strategie di sincronizzazione

La sincronizzazione tra diversi eventi asincroni fa uso, implicito o esplicito, delle code di attesa (wait queue).
In questo esempio, ricco di tecniche nuove, vengono illustrati l'attesa su eventi e l'attesa di completamento:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/list.h>
#include <linux/kthread.h>
#include <linux/wait.h>
#include <linux/completion.h>
#include <linux/delay.h>
#include <asm/uaccess.h>

struct kmisc_request {
	struct list_head head;
	char *buf;
	int bsize;
	struct completion comp;
};

static struct kmisc_data {
	struct list_head reqlist;
	spinlock_t lock;
	wait_queue_head_t wq;
	struct task_struct *ktask;
} kdata;

static int kmisc_thread(void *arg)
{
	struct kmisc_data *kdata = arg;
	struct kmisc_request *kreq;
	int pos;
	while (1) {
		wait_event_interruptible(kdata->wq, !list_empty(&kdata->reqlist));
		if (unlikely(signal_pending(current)))
			break;

		BUG_ON(list_empty(&kdata->reqlist));
		kreq = list_entry(kdata->reqlist.next, struct kmisc_request, head);
		spin_lock(&kdata->lock);
		list_del(&kreq->head);
		spin_unlock(&kdata->reqlist);
		for (pos=0; pos<kreq->bsize; ++pos)
			kreq->buf[pos] = '0'+pos%10;
		complete(&kreq->comp);
	}
	while (!kthread_should_stop())
		msleep(1);
	return 0;
}

static ssize_t kmisc_read(struct file *filp, char __user *dst, size_t count, loff_t *off)
{
	struct kmisc_request *kreq;
	if (unlikely(!kdata.ktask))
		return -ENODEV;
	if (count>(PAGE_SIZE-sizeof(*kreq)))
		count=PAGE_SIZE-sizeof(*kreq);
	kreq = kmalloc(sizeof(*kreq)+count, GFP_KERNEL);
	if (unlikely(!kreq))
		return -ENOMEM;
	kreq->bsize = count;
	kreq->buf = (char *) (kreq+1);
	init_completion(&kreq->comp);
	spin_lock(&kdata.lock);
	list_add_tail(&kreq->head, &kdata.reqlist);
	spin_unlock(&kdata.lock);
	wake_up_interruptible(&kdata.wq);
	wait_for_completion(&kreq->comp);
	if (unlikely(copy_to_user(dst, kreq->buf, count)))
		count = -EFAULT;
	kfree(kreq);
	return count;
}

static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.read		= kmisc_read,
	.llseek		= no_llseek
};

static struct miscdevice kmisc = {
	.minor 	= MISC_DYNAMIC_MINOR,
	.name 	= "kmisc",
	.fops	= &kops
};

static int __init kmisc_init(void)
{
	int ret;
	struct task_struct *ktask;
	INIT_LIST_HEAD(&kdata.reqlist);
	spin_lock_init(&kdata.lock);
	init_waitqueue_head(&kdata.wq);
	ret = misc_register(&kmisc);
	if (!ret) {
		ktask = kthread_create(kmisc_thread, &kdata, "kmisc");
		if (IS_ERR(ktask)) {
			misc_deregister(&kmisc);
			ret = PTR_ERR(ktask);
		} else {
			wake_up_process(ktask);
			kdata.ktask = ktask;
		}
	}
	if (!ret)
		printk(KERN_INFO "kmisc registered on minor %d\n", kmisc.minor);
	return ret;
}

static void __exit kmisc_exit(void)
{
	misc_deregister(&kmisc);
	WARN_ON(!list_empty(&kdata.reqlist));
	force_sig(SIGKILL, kdata.ktask);
	kthread_stop(kdata.ktask);
}


module_init(kmisc_init);
module_exit(kmisc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("miscdevice module");
Spiegazione:
Questo modulo simula la lettura di dati da un dispositivo. La creazione effettiva dei dati fittizzi avviene in un kernel thread. Il modulo mantiene una lista di richieste di lettura dai processi utente (kdata.reqlist; v. linux/list.h per la gestione delle liste su linux). Il thread kmisc_thread rimane "a dormire" sulla waitqueue kdata.wq finché non viene "svegliato" dalla funzione read, tramite la wake_up_interruptible. Il secondo parametro della funzione wait_event_interruptible è una condizione, che viene esaminata prima di andare "a dormire" -- se risulta vera, il codice continua senza bloccare (serve ad evitare una race condition -- riuscite a vederla?). La funzione ritorna anche in presenza di un segnale, che utilizzeremo per terminare il thread (v. force_sig in kmisc_exit -- notate che force_sig invia il segnale anche se è normalmente bloccato).
La read, prima di svegliare il thread, accoda la richiesta alla lista di richieste. Visto che sia la read che il kernel thread girano in contesto di processo, possiamo proteggere la lista tramite un semaforo o uno spinlock (senza disabilitare gli irq). Visto che il codice da proteggere è veramente breve, utilizziamo lo spinlock (un semaforo richiederebbe uno schedule se risulta bloccato, e uno schedule richiede molte più istruzioni di quelle eseguite nella regione critica. Molto più efficiente un veloce spinlock, in questa situazione). Una volta aggiornata la lista, il kernel thread viene eventualmente svegliato, nel caso stia "dormendo", dalla wake_up_interruptible. Quindi, la read deve aspettare che la richiesta venga soddisfatta, e attende quindi il completamento dell'operazione andando a "dormire" su un oggetto completion (tramite wait_for_completion). Ogni volta che una richiesta è ultimata, il kernel thread lo segnala tramite una chiamata a complete(). Notate che un oggetto completion ha una waitqueue all'interno (v. linux/completion.h)

Come funzionano le waitqueue?
Ogni processo è contraddistinto da uno "stato" di esecuzione (current->state). Questo stato può essere "task in esecuzione" (TASK_RUNNING), "task non in esecuzione, non interrompibile da segnali" (TASK_UNINTERRUPTIBLE), "task non in esecuzione, interrompibile da segnali" (TASK_INTERRUPTIBLE). Ad ogni ciclo di scheduling, solo i task RUNNING vengono presi in considerazione per l'esecuzione. Gli altri sono semplicemente ignorati. Quando poniamo il nostro task "a dormire" su una waitqueue, semplicemente "cambiamo stato di esecuzione" in INTERRUPTIBLE oppure UNINTERRUPTIBLE e invochiamo lo scheduler. Lo aggiungiamo anche ad una lista di task in attesa, lista contenuta nell'oggetto wait_queue_head_t (v. linux/wait.h), in modo da poterlo rintracciare con le apposite funzioni di wake. Quando lo "svegliamo", semplicemente riportiamo il suo stato in RUNNING. Semplice ed efficiente.
Per un semplice esempio di come invocare direttamente lo scheduler, v. il codice di msleep in kernel/timer.c.

Q&A
- Cosa sono BUG_ON e WARN_ON?
Sono delle macro di debug, che vengono ottimizzate via se non si abilita il kernel debugging. La prima causa l'uccisione del processo se la condizione è vera, la seconda solo un warning. In entrambi i casi viene stampato il file e la riga di errore, e lo stack trace. Attenzione che un BUG in irq context implica un kernel panic (un irq non ha un "contesto di processo").

- Cosa è "unlikely"?
likely e unlikely sono delle macro che si espandono in direttive del compilatore, che indicano che la condizione è probabile o improbabile. Servono al compilatore per ottimizzare il codice, indicando quale è il path di esecuzione "più probabile".

- Posso usare le funzioni di wake up/completion in irq context?


- Posso dormire su una wait queue in irq context?
Mai

- Cosa posso allora fare se dentro un irq devo "attendere un evento"?
Probabilmente cambiare design. Oppure, demandare l'operazione ad una workqueue da eseguire successivamente in contesto di processo.

- Possono esserci più task in attesa su una wait queue?
Sì. wake_up ne sveglia solo uno però. Per svegliarli tutti, usare wake_up_all & co.

- Voglio che il task svegliato sia eseguito immediatamente. Cosa devo fare?
Usare la variante wake_up_sync.

- Hai usato un buffer temporaneo; perché la read non ha passato direttamente il puntatore dst al kernel thread?
Quel puntatore è valido solo per il contesto di processo corrente, ed è tabù altrove. Il kernel thread ha un proprio contesto.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:00.
ilsensine è offline