Podsystem wejścia/wyjścia

Urządzenie mem

Filip Murlak



Spis rzeczy


1 Wstęp

Kontekst Dokument niniejszy powstał jako fragment prezentacji przygotowywanej w ramach projektu Scenariusze ćwiczeń z budowy systemu operacyjnego Linux realizowanego na zajęciach z Systemów Operacyjnych na Wydziale Matematyki, Informatyki i Mechaniki Uniwersytetu Warszawskiego w roku akademickim 2001/2002.

Uwagi techniczne Wszystkie informacje dotyczą jądra 2.4.7 i mogą nie być prawdziwe dla późniejszych wersji - dla wcześniejszych na pewno nie są! Opis dotyczy architektury i386. Wydruki kodu są poddane obróbce właściwej dla tej właśnie architektury (dyrektywy preprocesora, warunkowa kompilacja). Definicje struktur i funkcji pochodzą z - odpowiednio - include/linux/mm.h i drivers/char/mem.c.

1.1 Cel

Celem tego dokumentu jest prezentacja urządzenia mem z naciskiem na jego możliwości, jak również na cechy ilustrujące abstrakcję urządzenia znakowego.

1.2 Definicje

pseudourządzenie
program udostępniający interfejs urządzenia, ale nie sterujący żadnym faktycznym urządzeniem
we/wy
wejście/wyjście

1.3 Opis dalszej części dokumentu

W dalszej części dokumentu opiszemy charakterystyczne cechy urządzenia mem, wskażemy na ich źródła i zarysujemy przyjęte założenia implementacyjne.


2 Informacje ogólne

Linux udostępnia pamięć jako urządzenia znakowe o numerze głównym 1. Obejmuje ona następujące urządzenia podrzędne:

Numer podrzędny Plik Opis
1 /dev/mem pamięć fizyczna
2 /dev/kmem pamięć wirtualna jądra
3 /dev/null ujście dla niepotrzebnych danych
4 /dev/port porty
5 /dev/zero generator zer
7 /dev/full urządzenie pełne, przy zapisie zwraca ENOSPACE
8 /dev/random generator liczb pseudolosowych
9 /dev/urandom nieblokujący generator liczb pseudolsowych


Spośród wymienionych urządzeń mem, kmem i port można przy pewnej dozie dobrej woli uznać za urządzenia fizyczne, jednak pozostałe sześć urządzeń to zdecydowanie pseudourządzenia czyli usługi realizowane programowo, a jedynie udostępniające interfejs urządzenia.

Szczególnie sympatycznym urządzeniem jest /dev/null. Kompletna implementacja tego urządzenia wygląda następująco:

static ssize_t read_null (struct file * file, char * buf, 
			 size_t count, loff_t *ppos) {
    return 0;
}

static ssize_t write_null(struct file * file, const char * buf,
			  size_t count, loff_t *ppos) {
    return count;
}

static loff_t null_lseek(struct file * file, loff_t offset, int orig) {
    return file->f_pos = 0;
}

static struct file_operations null_fops = {
	llseek:		null_lseek,
	read:		read_null,
	write:		write_null,
};
Wystarczy je teraz zarejestrować i mamy gotowe urządzenie!

Przykład urządzenia, którego implementacja jest niebanalna stanowią rand i urand (drivers/char/random.c). Urządzenia te stanowią nową jakość w stosunku do funkcji bibliotecznych (rand() itp.), gdyż tamte zapewniają wprawdzie odpowiedni rozkład danych, ale charakteryzują się niską entropią (tzn. można przewidywać pewne wartości na podstawie innych). Implementacja ''losowych'' urządzeń oparta jest na przechwytywaniu informacji o czasie wystąpienia zdarzeń zewnętrznych (np. kliknięcie myszą, naciśnięcie klawisza na klawiaturze), co zdecydowanie podwyższa entropię. Na podstawie tych danych, przechowywanych w zbiorze entropii, urządzenie rand generuje tyle bajtów, ile (w pewnym sensie) znajduje się w tym zbiorze. Natomiast urand generuje dowolną ilość danych, ale nie gwarantuje przyzwoitego poziomu entropii.


3 Urządzenia podrzędne mem i kmem

3.1 Dostępne operacje

Na urządzeniu (k)mem chcemy udostępnić następujące operacje:

Struktura fops musi zatem wyglądać następująco:

static struct file_operations (k)mem_fops = {
	llseek:		memory_lseek,
	read:		read_(k)mem,
	write:		write_(k)mem,
	mmap:		mmap_(k)mem,
	open:		open_(k)mem,
};
Uwaga: Urządzenie (k)mem (także zero) jest wyjątkiem pośród urządzeń znakowych. Udostępnia odwzorowywanie w pamięci (mmap) i swobodny dostęp (lseek) - dlaczego?

3.2 Realizacja

Powyższe operacje są realizowane za pomocą następujących funkcji:
static loff_t memory_lseek(struct file * file, loff_t offset, int orig)
Ustawiamy (gdy orig==1 - przesuwamy) bieżącą pozycję w pliku file na offset.
Uwaga: nie można przesuwać względem ''końca pliku''.

static int open_port(struct inode * inode, struct file * filp)
Realizuje operację open, sprawdzając czy można skorzystać z we/wy wywołując makro capable(CAP_SYS_RAWIO). Ta funkcja obsługuje też otwieranie urządzenia port - stąd nazwa.

static int mmap_mem(struct file * file, struct vm_area_struct * vma)

Odwzorowuje urządzenie (k)mem w pamięci.
Uwaga: Przy mapowaniu urządzenia zero (mmap_zero()), gdy żądanie dotyczy pamięci dzielonej, dokonujemy zwykłego wyzerowania odpowiednich obszarów. Operacja ta może być długotrwała, więc po każdej stronie sprawdzamy flagę need_resched i ewentualnie oddajemy procesor wywołując funkcje schedule().

static ssize_t write_mem(struct file * file, const char * buf, 
			 size_t count, loff_t *ppos)

static ssize_t read_mem(struct file * file, char * buf,
		size_t count, loff_t *ppos)
Pisanie i czytanie do pamięci.

Do tłumaczenia adresów z fizycznego na wirtualny jądra i odwrotnie wykorzystujemy zdefiniowane w include/asm/page.h makra __va() i __pa() (zwykłe przesunięcie adresu o stałą PAGE_OFFSET w odpowiednią stronę).
Jeśli adres jest poprawny, to kopiujemy dane z pamięci do przestrzeni adresowej użytkownika (lub odwrotnie) za pomocą:

copy_to_user()
copy_from_user()
zdefiniowanych w include/asm/uaccess.h.

3.3 Różnice między mem i kmem

Różnice między urządzeniami mem i kmem polegają na interpretacji adresu, implementacje różnią się właściwie tylko wykorzystaniem __va() i __pa(), co pokażemy na przykładzie realizacji operacji write.
static ssize_t write_mem (struct file * file, const char * buf, 
				  size_t count, loff_t *ppos) {
    unsigned long p = *ppos;
    unsigned long end_mem;
    
    end_mem = __pa(high_memory);
    if (p >= end_mem)
    	return 0;
    if (count > end_mem - p)
	count = end_mem - p;
    return do_write_mem(file, __va(p), p, buf, count, ppos);
}

static ssize_t write_kmem (struct file * file, const char * buf, 
				   size_t count, loff_t *ppos) {
    unsigned long p = *ppos;

    if (p >= (unsigned long) high_memory)
	return 0;
    if (count > (unsigned long) high_memory - p)
    	count = (unsigned long) high_memory - p;
    return do_write_mem(file, (void*)p, p, buf, count, ppos);
}




4 Rejestracja urządzenia mem

Deklarujemy zainicjowane struktury xxx_fops (xxx - nazwa) dla każdego urządzenia podrzędnego (patrz wyżej) i pomocniczą strukturę:

struct file_operations memory_fops = {
    open: memory_open,	
};
gdzie int memory_open (struct inode *, struct filp *) jest uniwersalną funkcją otwierającą urządzenie główne mem, odpowiednio modyfikującą pole fops w zależności od numeru podrzędnego. Struktura ta jest potrzebna w przypadku starej obsługi plików specjalnych - wywołanie devfs_register_chrdev() realizujące register_chrdev() (jeśli podczas ładowania systemu nie ustawiono opcji devfs=only) - gdy korzystamy wyłącznie z devfs, jest to zbędne, bo dla każdego urządzenia podrzędnego pamiętana jest osobna struktura file_operations. Rejestracji dokona funkcja wyglądającą mniej więcej tak:
int inicjuj () {  /* to oczywiście NIE jest prawdziwy kod jądra */

  static const struct {
    unsigned short minor;
    char *name;
    umode_t mode;
    struct file_operations *fops;
  } list[] = {  
    {1,  "mem",   S_IFCHR,  &mem_fops },
    {2,  "kmem",  S_IFCHR,  &kmem_fops},
    /* ... {N, , , } */
  };
  int i;

  devfs_register_chrdev (MEM_MAJOR,"mem",&memory_fops);
  for (i = 0; i <= N; i++)
    devfs_register (NULL, list[i].name, DEVFS_FL_NONE, MEM_MAJOR, 
                    list[i].minor, list[i].mode, list[i].fops, NULL);
  rand_initialize ();
  return 0;
}



Filip Murlak
2001-12-13