Bartłomiej Rusiniak
b.rusiniak@students.mimuw.edu.pl
nr.leg. 171853

Systemy Wejścia-Wyjścia
Urządzenie "mem"

Wstęp

Dostęp do pamięci realizowany jest za pomocą urządzenia znakowego "mem" o numerze 1. Zapewnia ono funkcjonalność związaną z pisaniem do pamięci, odczytywaniem jej oraz generowaniem liczb pseudolosowych. Kod źródłowy tego modułu znajduje się w plikach mem.c oraz random.c .

Podurządzenia

Urządzenie "mem" posiada szereg podurządzeń:
Nazwa urządzenia MINOR(inode->i_rdev) Opis
mem 1 Realizuje operację pisania i czytania z/do pamięci fizycznej widzianej przez jądro
kmem 2 Podobnie jak powyżej, ale obsługuje pamięć wirtualną
null 3 Wszystko co jest zapisywane jest tracone. Odczytane dane mają zerową długość.
port 4 Zapewnia dostęp do portów
zero 5 Przy zapisywaniu zachowuje się jak null . Odczytanie powoduje wygenerowanie zerowych bajtów o podanym rozmiarze.
full 7 Przy odczytywaniu zachowuje się jak urządzenie zero. Przy zapisywaniu zwraca błąd braku miejsca
random 8 Generator "silnych" liczb losowych
urandom 9 Generator "słabych" liczb losowych
Do otwarcia głównego urządzeń służy funkcja:
static int memory_open(struct inode * inode, struct file * filp)
W zależności od numeru podurządzenia, do struktury file->f_op przypisywana jest odpowiednio zdefiniowana struktura file_operations w której wyszczególnione są dostępne operacje wraz z realizującymi je funkcjami. Struktury te mają postać:
static struct file_operations mem_fops = {
        llseek:        memory_lseek,
        read:          read_mem,
        write:         write_mem,
        mmap:          mmap_mem,
        open:          open_mem,
};

mem

Dostępne są następujące funkcje tego urządzenia:

Zapis/odczyt

Zapis danych do pamięci realizuje funkcja write_mem , która w rzeczywistości wywołuje funkcje do_write_mem . W funkcjach tych następuje sprawdzanie czy podany adres i rozmiar nie wykraczają poza __pa(high_mem) oznaczjącą maksymalną dostępną pamięć fizyczną w kontekscie procesu. Kopiowanie z bufora do pamięci realizuje makro copy_from_user zdefiniowane w pliku include/asm-xxx/uaccess.h , gdzie xxx oznacz nazwę architektury. Makro ta zależy ściśle od architektury procesora na którym jest wywoływane. Po operacji przepisania bufora następuje odpowiednie zwiększenie wskaźnika oznaczajacego pozycję w danym urządzeniu.

Odczyt danych realizowany jest przez funkcję read_mem i także sprowadza się do sprawdzenia poprawności adresu odczytywanej strony od której zacznie się przetwarzanie przez makro copy_to_user .

Pewną ciekawostką jest fakt że w komputerach klasy spark oraz m68k nie można uzyskać dostępu do strony o numerze 0. Dlatego w tym wypadku procedura zapisu i odczytu troszkę się komplikuje, ponieważ niezbędne jest dodatkowe ustalenie adresu z/do którego będziemy czytać/pisać. Jest to realizowane poprzez
unsigned long p = *ppos; //pozycja od której zaczynamy kopiowanie
unsigned long end_mem;//końca pamięci
end_mem = __pa(high_memory);
if (count > end_mem - p)
               count = end_mem - p;
if defined(__sparc__) || defined(__mc68000__)
        if (p < PAGE_SIZE) {
               unsigned long sz = PAGE_SIZE-p;
               if (sz > count) 
                       sz = count; 
               if (sz > 0) {
                       if (clear_user(buf, sz))
                               return -EFAULT;
                       buf += sz; 
                       p += sz; 
                       count -= sz; 
 
                }
        }
#endif
Dodatkowo w operacji read_mem niezbędne jest przygotowanie buforu do którego nastąpi odczyt. Realizowane jest to przy pomocy clear_user , której definicja znajduje się w include/asm/uaccess.h a ciało w arch/xxx/lib/usercopy.c , gdzie xxx jest związana z używanym procesorem.

Makra copy_to_user , copy_from_user oparte architekturze I386 w zależności od rozmiaru kopiowanego obszaru są realizowane za pomocą instrukcji movsb, movsw, movsl . Przed ich wykonywaniem sprawdzana jest poprawność podanego zakresu pamięci i czy jest do niego dostęp( access_ok ). W celu przyspieszenia działania procedury kopiującej, w zależności od tego czy jest to pamięć fizyczna czy wirtualna, wywoływana jest odpowiednia funkcja.
#define copy_to_user(to,from,n)				\
(__builtin_constant_p(n) ?			\
	__constant_copy_to_user((to),(from),(n)) :	\
	__generic_copy_to_user((to),(from),(n)))

mapowanie

Mapowanie polega na uzyskaniu dostępu do zadanej ilości stron w kontekscie procesu. Realizowane jest to poprzez funkcje
static int mmap_mem(struct file * file, struct vm_area_struct * vma)
Jest ona odpowiedzialna za ustawienie odpowiednich pól w strukturze struct vm_area_struct * vma oraz przemapowanie danego obszaru, co jest realizowane za pomocą:
remap_page_range(vma->vm_start, offset, vma->vm_end-vma->vm_start, vma->vm_page_prot)
Funkcja ta jest zdefiniowana w pliku linux/mm.h .

zmiana pozycji

Zmiana pozycji jest realizowana przez funkcję:
static loff_t memory_lseek(struct file * file, loff_t offset, int orig)
Jest ona odpowiedzialna za przesunięcie o offset wskaźnika w strukturze file.

otwarcie

Otworzenie urządzenia sprowadza się jedynie do wywołania funkcji
static inline int capable(int cap)
zdefinowanej w linux/sched.h . Sprawdza ona jedynie czy urządzenie może być wykorzystywane przez bieżący proces, co jest realizowane za pomocą cap_raised(current->cap_effective, cap) , gdzie pod cap wstawiana jest wartość CAP_SYS_RAWIO oznaczającą dostęp do "surowego" urządzenie IO.

kmem

Urządzenie ma taki sam zakres funkcjonalny co "mem". Operacja otwarcia, mapowania oraz pozycjonowania jest realizowana przez te same funkcje co "mem". Różnica jest jedynie w operacjach zapisu/odczytu.

Zapis

W operacji zapisu inaczej przeliczana jest pamięci dostępna. Przy określaniu end_mem nie jest wywoływane makro:
#define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)
Funkcje
copy_from_user
same dbają o wybór odpowiedniej funkcji przy stronach w wirtualnym i fizycznym obszarze.

Odczyt

Przy operacji odczytu sprawa nieco się komplikuje.
static ssize_t read_kmem(struct file *file, char *buf, 
                        size_t count, loff_t *ppos)
{
        unsigned long p = *ppos;
        ssize_t read = 0;
        ssize_t virtr = 0;
        
        // musimy alokować dodatkowy buffor ponieważ funkcja 
        //vread() popiera pamieć oznaczoną jako zablokowaną do 
        // odczytu/zapisu
		char * kbuf; 
        //Czytamy podobnie  jak w urządzeniu "mem" 
		(...) ;
        //jeżeli coś zostało do przeczytania a osiągneliśmy koniec dostępnej pamięci fizycznej
        if (count > 0) { 
            //alokujemy wolną stronę pamięci jako bufor pomocniczy
               kbuf = (char *)__get_free_page(GFP_KERNEL); 
               
               if (!kbuf)
                       return -ENOMEM;
               while (count > 0) {
                       int len = count;
                       if (len > PAGE_SIZE)
                               len = PAGE_SIZE;
                       //czytamy do alokowanego buffora pamięc wirtualną
                       len = vread(kbuf, (char *)p, len);
                       //kopiowanie do bufora uzytkownika
                       if (len && copy_to_user(buf, kbuf, len)) {
                               //kopiowanie lub odczytywanie sie nie powidło
                               free_page((unsigned long)kbuf);
                               return -EFAULT;
                       }
                       count -= len;
                       buf += len;
                       virtr += len;
                       p += len;
               }
               //zwalnianie bufora pomocniczego
               free_page((unsigned long)kbuf);
        }
        *ppos = p;
        return virtr + read;
}
Jak widać przy operacji czytania z "kmem" dopóki operujemy na dostępnej pamięci fizycznej wszystko odbywa się analogiczne do "mem". Jeżeli jednak pytamy się o pamięć wirtualną następuje odpowiednie szukanie strony za pomocą vread oraz alokowanie strony pomocniczej itp.

Null

Urządzenie to jest dość ubogie jeżeli chodzi o obsługę gdyż odpowiednio:

Port

Podurządzenie "port" realizuje funkcje otwarcia, zapisu, odczytu, pozycjonowania. Otwarcie oraz pozycjonowanie jest takie samo jak w "mem".

Zapis

Zapis do portu odbywa się za pomocą funkcji outb zdefiniowanej w io.h . Wartości przekopiowywane są z pamięci za pomocą __put_user zdefiniowanej w asm-xxx/uaccess.h . Najpierw jednak sprawdzana jest informacja, czy możemy tą pamięć odczytywać za pomocą funkcji access_ok opisanej wcześniej, wywoływanej przez verify_area(VERIFY_WRITE,buf,count) , która również jest zdefiniowana w pliku uaccess.h .

Odczyt

Realizowanie odczytu odbywa się analogicznie do zapisu, tylko wywoływane są odpowiednio funkcje:
inb(i);
verify_area(VERIFY_READ,buf,count);
__get_user(c, tmp);

Zero

Służy do generowania "pustej" pamięci.

Zapis

Zapis jest realizowany analogicznie jak dla urządzenia null.

Odczyt

Urządzenie to służy do odczytywania pustych bloków pamięci. W starszych wersjach jądra wykorzystywane do tego było makro __copy_user_zeroing(to,from,size) , które polegało na kopiowaniu zer mechanizmem stosl(bw). W tej wersji jest to tworzone w inny sposób niż do tej pory.
Dotychczasowe zaalokowane strony są zwalniane za pomocą zap_page_range i nowa strona z zerami jest "przemapowywana" za pomocą zeromap_page_range . Ciało obu tych funkcji znajduje się w pliku "/mm/memory.c"

random,urandom

Urządzenia te odpowiadają za generowanie liczb losowych. Alogorytm ich generowanie nie leży w zakresie tego referatu, warto jednak wspomnieć, że początkowe ziarno wyliczane jest za pomocą zliczania stanu: Ziarna (entropie) są przechowywane jako wielokrotność 16 32-bitowych słów. Przy wyliczaniu kolejnych wartości stosowane są ogólnie znane funkcje SHA czy MD5 (standardowo SHA)

kontrola

Kontrolę na urządzeniem przejmuje funkcja
static int random_ioctl(struct inode * inode, struct file * file,
unsigned int cmd, unsigned long arg)
Implementuje ona zestaw komend dostępnych dla urządzenia. Pozwalają one np:

czytanie

Funkcja
static ssize_t random_read(struct file * file, char * buf, size_t nbytes, loff_t *ppos)
pozwala na pobieranie liczb losowych i umieszczenie ich w pamięci za pomocą opisanej wcześniej funkcji copy_to_user . Ilość entropii do wyliczania liczb losowych jest stała i zainicjowana przez create_entropy_store . Zaimplementowany został mechanizm poolingu entropi.
Proces pragnący przeczytać dane z urządzenia, zostaje umieszczony na kolejce random_read_wait gdzie czeka na przydzielenie entropii z puli. Jeżeli doczeka się na swoją kolej (oraz entropia zawiera wystarczająco dużo bitów pozwalających na wygenerowanie liczb losowych - standardowo 8) wykonywana jest funkcja extract_entropy .

W przypadku urandom nie jest wykonywane czekanie na wolną entropie oraz sprawdzanie jej jakości. Z tego względu urządzenie to nie może być wykorzystywane np. do generowania kluczy RSA.

pisanie

Pisanie realizowane jest przez funkcje
static ssize_t random_write(struct file * file, const char * buffer, 
size_t count, loff_t *ppos)
i polega na dodaniu "szumu" do entropii.