Apprendere i processi in Linux

di Amit Saha

06 Gennaio 2007

L'articolo Learning about Linux Processes pubblicato su Linux Gazette introduce alcuni concetti su una delle astrazioni fondamentali di un sistema linux – il processo. Traduzione a cura di Cardelli Sandro e revisione a cura di Lucio Palmieri .


Allora, cosa è un processo?

Cito dal libro di Robert Love Linux Kernel Development , "Il Processo è una delle astrazioni fondamentali nei sistemi operativi Unix, essendo i file l'altra fondamentale astrazione". Un processo è un programma in esecuzione. È composto dal codice del programma in esecuzione, un insieme di risorse come i file aperti, i dati interni al kernel, uno spazio d'indirizzamento, uno o più thread di esecuzione e una sezione dati contenente le variabili globali.

Descrittori di processo

Ogni processo ha un descrittore di processo associato. Questo detiene le informazioni usate per tenere traccia di un processo in memoria. Tra le varie parti d'informazione memorizzate per un processo ci sono: il suo PID, lo stato, il processo parente, i figli, i fratelli, il contenuto dei registri del processore, la lista dei file aperti e l'informazione sullo spazio di indirizzamento.

Il kernel Linux usa una lista circolare doppiamente concatenata di struct task_struct per memorizzare questi descrittori di processo. Questa struttura è dichiarata in linux/sched.h . Ecco alcuni campi dal kernel 2.6.15-1.2054_FC5, iniziando dalla riga 701:

    701 struct task_struct {
    702         volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    703         struct thread_info *thread_info;
     .
     .
    767         /* PID/PID hash table linkage. */
    768         struct pid pids[PIDTYPE_MAX];
     .
     .
    798         char comm[TASK_COMM_LEN]; /* executable name excluding path

La prima riga della struttura definisce il campo state come volatile long . Questa variabile è usata per tenere traccia dello stato d'esecuzione del processo, definito dalle seguenti macro:

#define TASK_RUNNING            0
#define TASK_INTERRUPTIBLE      1
#define TASK_UNINTERRUPTIBLE    2
#define TASK_STOPPED            4
#define TASK_TRACED             8
/* in tsk->exit_state */
#define EXIT_ZOMBIE             16
#define EXIT_DEAD               32
/* in tsk->state again */
#define TASK_NONINTERACTIVE     64

La parola chiave volatile è degna di nota - vedere http://www.kcomputing.com/volatile.html per maggiori informazioni.

Liste concatenate

Prima di guardare come i task/processi (useremo le due parole come sinonimi) sono memorizzati dal kernel, occorre capire come il kernel implementa le liste circolari concatenate. L'implementazione che segue è uno standard che è usato in tutti i sorgenti del kernel. La lista concatenata è dichiarata in linux/list.h e la struttura dati è semplice:

 struct list_head {
         struct list_head *next, *prev;
 };

Questo file definisce anche molte macro e funzioni già pronte che è possibile usare per manipolare le liste concatenate. Questo rende standard l'implementazione delle liste concatenate per prevenire che le persone "reinventino la ruota" e introducano nuovi bug.

Ecco qui alcuni riferimenti alle liste concatenate del kernel:

La lista dei task del kernel

Guardiamo ora come il kernel linux usa le liste circolari doppiamente concatenate per memorizzare la strutture dei processi. Cercando la struct list_head dentro la definizione della struct task_struct troviamo:

struct list_head tasks;

Questa riga ci mostra che il kernel usa una lista circolare concatenata per memorizzare i task. Questo significa che è possibile usare le macro e le funzioni standard della lista concatenata del kernel per attraversare l'intera lista dei task.

In un sistema Linux init è il "padre di tutti i processi". Pertanto esso è rappresentato all'inizio della lista, anche se in senso stretto non c'è una testa poiché questa è una lista circolare . Il descrittore di processo del task init è allocato staticamente:

extern struct task_struct init_task;

Di seguito si mostra la rappresentazione della lista concatenata dei processi in memoria:

Linked List Figure

Molte altre macro e funzioni sono disponibili per aiutare a scorrere questa lista:

for_each_process() è una macro che itera sull'intera lista dei task. È definita come segue in linux/sched.h:

#define for_each_process(p) \
        for (p = &init_task ; (p = next_task(p)) != &init_task ; )

next_task() è una macro definita in linux/sched.h la quale ritorna il prossimo task nella lista:

#define next_task(p)    list_entry((p)->tasks.next, struct task_struct, tasks)

list_entry() è una macro definita in linux/list.h:

/*
 * list_entry - get the struct for this entry
 * @ptr:        the &struct list_head pointer.
 * @type:       the type of the struct this is embedded in.
 * @member:     the name of the list_struct within the struct.
 */
#define list_entry(ptr, type, member) \
        container_of(ptr, type, member)

La macro container_of() è definita come segue:

#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

Pertanto se possiamo attraversare l'intera lista dei task è possibile avere tutti i processi in esecuzione nel sistema. Questo può essere fatto con la macro for_each_process(task) , dove task è un puntatore al tipo struct task_struct. Ecco un modulo di esempio del kernel, dal Linux Kernel Development :

    /* ProcessList.c 
    Robert Love Chapter 3
    */
    #include < linux/kernel.h >
    #include < linux/sched.h >
    #include < linux/module.h >

    int init_module(void)
    {
    struct task_struct *task;
    for_each_process(task)
    {
    printk("%s [%d]\n",task->comm , task->pid);
    }
   
    return 0;
    }
   
    void cleanup_module(void)
    {
    printk(KERN_INFO "Cleaning Up.\n");
    }

La macro current è un link al descrittore di processo (un puntatore ad una task_struct ) del processo correntemente in esecuzione. Come current ottiene i suoi task è dipendente dall'architettura. Su un x86 questo è fatto dalla funzione current_thread_info() definita in asm/thread_info.h

   /* how to get the thread information struct from C */
   static inline struct thread_info *current_thread_info(void)
   {
           struct thread_info *ti;
           __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1)));
           return ti;
   }

Infine current dereferenzia il membro task della struttura thread_info , riprodotta sotto da asm/thread_info.h , mediante current_thread_info()->task;

   struct thread_info {
           struct task_struct      *task;          /* main task structure */
           struct exec_domain      *exec_domain;   /* execution domain */
           unsigned long           flags;          /* low level flags */
           unsigned long           status;         /* thread-synchronous flags */
           __u32                   cpu;            /* current CPU */
           int                     preempt_count;  /* 0 => preemptable, <0 => BUG */
   
   
           mm_segment_t            addr_limit;     /* thread address space:
                                                      0-0xBFFFFFFF for user-thread
                                                      0-0xFFFFFFFF for kernel-thread
                                                   */
           void                    *sysenter_return;
           struct restart_block    restart_block;
   
           unsigned long           previous_esp;   /* ESP of the previous stack in case
                                                      of nested (IRQ) stacks
                                                   */
           __u8                    supervisor_stack[0];
   };

Usando la macro current e init_task è possibile scrivere un modulo del kernel per tracciare dal processo corrente indietro fino a init .

/*
Traceroute to init
   traceinit.c
Robert Love Chapter 3
*/
  #include < linux/kernel.h >
  #include < linux/sched.h >
  #include < linux/module.h >
 
  int init_module(void)
  {
  struct task_struct *task;
  
   for(task=current;task!=&init_task;task=task->parent)
   //current is a macro which points to the current task / process
   {
   printk("%s [%d]\n",task->comm , task->pid);
   }
  
   return 0;
   }
   
   void cleanup_module(void)
   {
   printk(KERN_INFO "Cleaning up 1.\n");
   }

Bene, abbiamo appena iniziato nella conoscenza di una delle astrazioni fondamentali di un sistema linux – il processo. In un (possibile) futuro tale argomento verrà ampliato, daremo uno sguardo ad altri aspetti.

Fino ad allora, felice hacking!

Altre risorse:

     obj-m +=ProcessList.o
     obj-m +=traceinit.o
     all:
             make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
     
     clean:
             make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Talkback: Discuss this article with The Answer Gang


Bio picture

L'autore è al terzo anno di Computer Engineering Undergraduate al Haldia Institute of Technology, Haldia. Nei suoi interessi include i protocolli di rete, la sicurezza di rete, i sistemi operativi, e i microprocessori. È un fan di Linux e ama l'hacking del kernel Linux.


Copyright © 2006, Amit Saha. Released under the Open Publication license unless otherwise noted in the body of the article. Linux Gazette is not produced, sponsored, or endorsed by its prior host, SSC, Inc.

Published in Issue 133 of Linux Gazette, December 2006