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:
- otwarcie
- zapis
- odczyt
- zmiana pozycji
- mapowanie
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:
- read_null --- nic nie robi poza zwracaniem 0.
- write_null --- nic nie robi poza zwracaniem rozmiaru który wczytujemy
- null_lseek --- ustawia plik na początek
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:
- zegara systemowego
- klawiatury
- myszki
- przerwań
- urządzeń blokowych
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:
- RNDGETENTCNT - pozwala pobrać aktualną entropię.
- RNDADDTOENTCNT - przy wyliczaniu entropii pozwala na użycie zawartości podanego obszaru
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.